diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1c2d52b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/* diff --git a/shapefile.py b/shapefile.py index dbf846ac..6d0a8bb1 100644 --- a/shapefile.py +++ b/shapefile.py @@ -36,7 +36,7 @@ MULTIPOINTM = 28 MULTIPATCH = 31 -MISSING = [None,''] +MISSING = [None, ''] PYTHON3 = sys.version_info[0] == 3 @@ -46,6 +46,7 @@ else: from itertools import izip + def b(v): if PYTHON3: if isinstance(v, str): @@ -61,6 +62,7 @@ def b(v): # For python 2 assume str passed in and return str. return v + def u(v): if PYTHON3: # try/catch added 2014/05/07 @@ -71,32 +73,37 @@ def u(v): # the problem. This function could # be condensed further. try: - if isinstance(v, bytes): - # For python 3 decode bytes to str. - return v.decode('utf-8') - elif isinstance(v, str): - # Already str. - return v - else: - # Error. - raise Exception('Unknown input type') - except: return v + if isinstance(v, bytes): + # For python 3 decode bytes to str. + return v.decode('utf-8') + elif isinstance(v, str): + # Already str. + return v + else: + # Error. + raise Exception('Unknown input type') + except: + return v else: # For python 2 assume str passed in and return str. return v + def is_string(v): if PYTHON3: return isinstance(v, str) else: return isinstance(v, basestring) + class _Array(array.array): """Converts python tuples to lits of the appropritate type. Used to unpack different shapefile header parts.""" + def __repr__(self): return str(self.tolist()) + def signed_area(coords): """Return the signed area enclosed by a ring using the linear time algorithm. A value >= 0 indicates a counter-clockwise oriented ring. @@ -104,7 +111,8 @@ def signed_area(coords): xs, ys = map(list, zip(*coords)) xs.append(xs[1]) ys.append(ys[1]) - return sum(xs[i]*(ys[i+1]-ys[i-1]) for i in range(1, len(coords)))/2.0 + return sum(xs[i] * (ys[i + 1] - ys[i - 1]) for i in range(1, len(coords))) / 2.0 + class _Shape: def __init__(self, shapeType=NULL): @@ -125,19 +133,19 @@ def __init__(self, shapeType=NULL): def __geo_interface__(self): if self.shapeType in [POINT, POINTM, POINTZ]: return { - 'type': 'Point', - 'coordinates': tuple(self.points[0]) + 'type': 'Point', + 'coordinates': tuple(self.points[0]) } elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]: return { - 'type': 'MultiPoint', - 'coordinates': tuple([tuple(p) for p in self.points]) + 'type': 'MultiPoint', + 'coordinates': tuple([tuple(p) for p in self.points]) } elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]: if len(self.parts) == 1: return { - 'type': 'LineString', - 'coordinates': tuple([tuple(p) for p in self.points]) + 'type': 'LineString', + 'coordinates': tuple([tuple(p) for p in self.points]) } else: ps = None @@ -152,14 +160,14 @@ def __geo_interface__(self): else: coordinates.append(tuple([tuple(p) for p in self.points[part:]])) return { - 'type': 'MultiLineString', - 'coordinates': tuple(coordinates) + 'type': 'MultiLineString', + 'coordinates': tuple(coordinates) } elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: if len(self.parts) == 1: return { - 'type': 'Polygon', - 'coordinates': (tuple([tuple(p) for p in self.points]),) + 'type': 'Polygon', + 'coordinates': (tuple([tuple(p) for p in self.points]),) } else: ps = None @@ -184,25 +192,29 @@ def __geo_interface__(self): polys.append(poly) if len(polys) == 1: return { - 'type': 'Polygon', - 'coordinates': tuple(polys[0]) + 'type': 'Polygon', + 'coordinates': tuple(polys[0]) } elif len(polys) > 1: return { - 'type': 'MultiPolygon', - 'coordinates': polys + 'type': 'MultiPolygon', + 'coordinates': polys } + class _ShapeRecord: """A shape object of any type.""" + def __init__(self, shape=None, record=None): self.shape = shape self.record = record + class ShapefileException(Exception): """An exception to handle shapefile specific problems.""" pass + class Reader: """Reads the three files of a shapefile as a unit or separately. If one of the three files (.shp, .shx, @@ -221,6 +233,7 @@ class Reader: efficiently as possible. Shapefiles are usually not large but they can be. """ + def __init__(self, *args, **kwargs): self.shp = None self.shx = None @@ -260,14 +273,11 @@ def __init__(self, *args, **kwargs): self.dbf.seek(0) except (NameError, io.UnsupportedOperation): self.dbf = io.BytesIO(self.dbf.read()) - if self.shp or self.dbf: + if self.shp or self.dbf: self.load() else: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") - - - def load(self, shapefile=None): """Opens a shapefile from a filename or file-like object. Normally this method would be called by the @@ -288,7 +298,7 @@ def load(self, shapefile=None): except IOError: pass if not (self.shp and self.dbf): - raise ShapefileException("Unable to open %s.dbf or %s.shp." % (shapeName, shapeName) ) + raise ShapefileException("Unable to open %s.dbf or %s.shp." % (shapeName, shapeName)) if self.shp: self.__shpHeader() if self.dbf: @@ -325,7 +335,7 @@ def __shpHeader(self): self.shpLength = unpack(">i", shp.read(4))[0] * 2 # Shape type shp.seek(32) - self.shapeType= unpack("6i", 9994,0,0,0,0,0)) + f.write(pack(">6i", 9994, 0, 0, 0, 0, 0)) # File length (Bytes / 2 = 16-bit words) if headerType == 'shp': f.write(pack(">i", self.__shpFileLength())) @@ -776,7 +787,7 @@ def __shapefileHeader(self, fileObj, headerType='shp'): except error: raise ShapefileException("Failed to write shapefile bounding box. Floats required.") else: - f.write(pack("<4d", 0,0,0,0)) + f.write(pack("<4d", 0, 0, 0, 0)) # Elevation z = self.zbox() # Measure @@ -802,7 +813,7 @@ def __dbfHeader(self): headerLength = numFields * 32 + 33 recordLength = sum([int(field[2]) for field in self.fields]) + 1 header = pack('i", length)) f.seek(finish) @@ -944,28 +961,29 @@ def __dbfRecords(self): f = self.__getFileObj(self.dbf) for record in self.records: if not self.fields[0][0].startswith("Deletion"): - f.write(b(' ')) # deletion flag + f.write(b(' ')) # deletion flag for (fieldName, fieldType, size, deci), value in zip(self.fields, record): fieldType = fieldType.upper() size = int(size) - if fieldType in ("N","F"): + if fieldType in ("N", "F"): # numeric or float: number stored as a string, right justified, and padded with blanks to the width of the field. if value in MISSING: - value = str("*"*size) # QGIS NULL + value = str("*" * size) # QGIS NULL elif not deci: # force to int try: # first try to force directly to int. # forcing a large int to float and back to int # will lose information and result in wrong nr. - value = int(value) + value = int(value) except ValueError: # forcing directly to int failed, so was probably a float. value = int(float(value)) - value = format(value, "d")[:size].rjust(size) # caps the size if exceeds the field size + value = format(value, "d")[:size].rjust(size) # caps the size if exceeds the field size else: value = float(value) - value = format(value, ".%sf"%deci)[:size].rjust(size) # caps the size if exceeds the field size + value = format(value, ".%sf" % deci)[:size].rjust( + size) # caps the size if exceeds the field size elif fieldType == "D": # date: 8 bytes - date stored as a string in the format YYYYMMDD. if isinstance(value, date): @@ -973,24 +991,31 @@ def __dbfRecords(self): elif isinstance(value, list) and len(value) == 3: value = date(*value).strftime("%Y%m%d") elif value in MISSING: - value = b('0') * 8 # QGIS NULL for date type + value = b('0') * 8 # QGIS NULL for date type elif isinstance(value, str) and len(value) == 8: - pass # value is already a date string + pass # value is already a date string else: - raise ShapefileException("Date values must be either a datetime.date object, a list, a YYYYMMDD string, or a missing value.") + raise ShapefileException( + "Date values must be either a datetime.date object, a list, a YYYYMMDD string, or a missing value.") elif fieldType == 'L': # logical: 1 byte - initialized to 0x20 (space) otherwise T or F. if value in MISSING: - value = b(' ') # missing is set to space - elif value in [True,1]: + value = b(' ') # missing is set to space + elif value in [True, 1]: value = b("T") - elif value in [False,0]: + elif value in [False, 0]: value = b("F") else: - value = b(' ') # unknown is set to space + value = b(' ') # unknown is set to space else: # anything else is forced to string - value = str(value)[:size].ljust(size) + # value = b(str(value)) + # Intercepts a string of the specified byte length + value = self.__cuttingStr(str(value),size) + value = value[:size].ljust(size) + + + if len(value) != size: raise ShapefileException( "Shapefile Writer unable to pack incorrect sized value" @@ -998,6 +1023,36 @@ def __dbfRecords(self): value = b(value) f.write(value) + def __cuttingStr(self, value, lens): + if isinstance(value, bytes): + value = unicode(value, 'utf-8') + b_str = value.encode("utf-8") + return_str = "" + + if lens < len(b_str): + ratio = lens / len(b_str) + weight = int(ratio * len(value) + 0.5) + + if weight < len(value) - 1: + direction = -1 if len(value[:weight].encode("utf-8")) >= lens else 1 + previous = weight + for i in range(weight): + wei_value = direction * i + weight + wei_len = len(value[:wei_value].encode("utf-8")) + + if (direction > 0 and wei_len <= lens) or (direction < 0 and wei_len >= lens): + previous = wei_value + else: + previous += direction + break + return_str = value[:previous] + else: + return_str = value[:weight] + else: + return_str = value + + return return_str.encode("utf-8") + def null(self): """Creates a null shape.""" self._shapes.append(_Shape(NULL)) @@ -1024,10 +1079,10 @@ def poly(self, parts=[], shapeType=POLYGON, partTypes=[]): polyShape.parts = [] polyShape.points = [] # Make sure polygons are closed - if shapeType in (5,15,25,31): + if shapeType in (5, 15, 25, 31): for part in parts: - if part[0] != part[-1]: - part.append(part[0]) + if part[0] != part[-1]: + part.append(part[0]) for part in parts: polyShape.parts.append(len(polyShape.points)) for point in part: @@ -1125,8 +1180,8 @@ def save(self, target=None, shp=None, shx=None, dbf=None): be written exclusively using saveShp, saveShx, and saveDbf respectively. If target is specified but not shp,shx, or dbf then the target path and file name are used. If no options or specified, a unique base file name - is generated to save the files and the base file name is returned as a - string. + is generated to save the files and the base file name is returned as a + string. """ # Create a unique file name if one is not defined if shp: @@ -1138,9 +1193,9 @@ def save(self, target=None, shp=None, shx=None, dbf=None): elif not shp and not shx and not dbf: generated = False if not target: - temp = tempfile.NamedTemporaryFile(prefix="shapefile_",dir=os.getcwd()) + temp = tempfile.NamedTemporaryFile(prefix="shapefile_", dir=os.getcwd()) target = temp.name - generated = True + generated = True self.saveShp(target) self.shp.close() self.saveShx(target) @@ -1149,7 +1204,8 @@ def save(self, target=None, shp=None, shx=None, dbf=None): self.dbf.close() if generated: return target - + + class Editor(Writer): def __init__(self, shapefile=None, shapeType=POINT, autoBalance=1): self.autoBalance = autoBalance @@ -1207,12 +1263,18 @@ def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=Non shape type.""" # shape, part, point if shape and part and point: - try: self._shapes[shape] - except IndexError: self._shapes.append([]) - try: self._shapes[shape][part] - except IndexError: self._shapes[shape].append([]) - try: self._shapes[shape][part][point] - except IndexError: self._shapes[shape][part].append([]) + try: + self._shapes[shape] + except IndexError: + self._shapes.append([]) + try: + self._shapes[shape][part] + except IndexError: + self._shapes[shape].append([]) + try: + self._shapes[shape][part][point] + except IndexError: + self._shapes[shape][part].append([]) p = self._shapes[shape][part][point] if x: p[0] = x if y: p[1] = y @@ -1221,10 +1283,14 @@ def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=Non self._shapes[shape][part][point] = p # shape, part elif shape and part and not point: - try: self._shapes[shape] - except IndexError: self._shapes.append([]) - try: self._shapes[shape][part] - except IndexError: self._shapes[shape].append([]) + try: + self._shapes[shape] + except IndexError: + self._shapes.append([]) + try: + self._shapes[shape][part] + except IndexError: + self._shapes[shape].append([]) points = self._shapes[shape][part] for i in range(len(points)): p = points[i] @@ -1235,8 +1301,10 @@ def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=Non self._shapes[shape][part][i] = p # shape elif shape and not part and not point: - try: self._shapes[shape] - except IndexError: self._shapes.append([]) + try: + self._shapes[shape] + except IndexError: + self._shapes.append([]) # point # part @@ -1251,7 +1319,7 @@ def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=Non def validate(self): """An optional method to try and validate the shapefile as much as possible before writing it (not implemented).""" - #TODO: Implement validation method + # TODO: Implement validation method pass def balance(self): @@ -1270,6 +1338,7 @@ def __fieldNorm(self, fieldName): fieldName = fieldName.upper() fieldName.replace(' ', '_') + # Begin Testing def test(**kwargs): import doctest @@ -1281,7 +1350,8 @@ def test(**kwargs): if verbosity == 0 and failure_count == 0: print('All test passed successfully') return failure_count - + + if __name__ == "__main__": """ Doctests are contained in the file 'README.md'. This library was originally developed diff --git a/shapefile.pyc b/shapefile.pyc new file mode 100644 index 00000000..075d22eb Binary files /dev/null and b/shapefile.pyc differ diff --git a/shapefiles/test/dtype.dbf b/shapefiles/test/dtype.dbf index 273a1cb0..02707b65 100644 Binary files a/shapefiles/test/dtype.dbf and b/shapefiles/test/dtype.dbf differ diff --git a/shapefiles/test/line.dbf b/shapefiles/test/line.dbf index a4eee87b..266fad52 100644 Binary files a/shapefiles/test/line.dbf and b/shapefiles/test/line.dbf differ diff --git a/shapefiles/test/line.shp b/shapefiles/test/line.shp index 452d11e7..8724f204 100644 Binary files a/shapefiles/test/line.shp and b/shapefiles/test/line.shp differ diff --git a/shapefiles/test/line.shx b/shapefiles/test/line.shx index f429e35f..a4672b29 100644 Binary files a/shapefiles/test/line.shx and b/shapefiles/test/line.shx differ diff --git a/shapefiles/test/point.dbf b/shapefiles/test/point.dbf index c77d0978..d0cf8d9e 100644 Binary files a/shapefiles/test/point.dbf and b/shapefiles/test/point.dbf differ diff --git a/shapefiles/test/polygon.dbf b/shapefiles/test/polygon.dbf index 184088de..3d07876a 100644 Binary files a/shapefiles/test/polygon.dbf and b/shapefiles/test/polygon.dbf differ