Last active
December 2, 2015 22:50
-
-
Save randomradio/cf04c48bd33ad843ebb6 to your computer and use it in GitHub Desktop.
shape file writer works on appengine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# create buffer to write binaries | |
# assume you have the writer created and recored inserted already | |
shp_buffer = StringIO.StringIO() | |
shx_buffer = StringIO.StringIO() | |
dbf_buffer = StringIO.StringIO() | |
shape_writer.saveShp(shp_buffer) | |
shape_writer.saveShx(shx_buffer) | |
shape_writer.saveDbf(dbf_buffer) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ShapeWriter: | |
"""Provides write support for ESRI Shapefiles.""" | |
def __init__(self, shapeType=None): | |
self._shapes = [] | |
self.fields = [] | |
self.records = [] | |
self.shapeType = shapeType | |
self.shp = None | |
self.shx = None | |
self.dbf = None | |
# Geometry record offsets and lengths for writing shx file. | |
self._offsets = [] | |
self._lengths = [] | |
# Use deletion flags in dbf? Default is false (0). | |
self.deletionFlag = 0 | |
def __getFileObj(self, f): | |
"""Safety handler to verify file-like objects""" | |
if not f: | |
raise ShapefileException("No file-like object available.") | |
elif hasattr(f, "write"): | |
return f | |
else: | |
pth = os.path.split(f)[0] | |
if pth and not os.path.exists(pth): | |
os.makedirs(pth) | |
return open(f, "wb") | |
def __shpFileLength(self): | |
"""Calculates the file length of the shp file.""" | |
# Start with header length | |
size = 100 | |
# Calculate size of all shapes | |
for s in self._shapes: | |
# Add in record header and shape type fields | |
size += 12 | |
# nParts and nPoints do not apply to all shapes | |
#if self.shapeType not in (0,1): | |
# nParts = len(s.parts) | |
# nPoints = len(s.points) | |
if hasattr(s,'parts'): | |
nParts = len(s.parts) | |
if hasattr(s,'points'): | |
nPoints = len(s.points) | |
# All shape types capable of having a bounding box | |
if self.shapeType in (3,5,8,13,15,18,23,25,28,31): | |
size += 32 | |
# Shape types with parts | |
if self.shapeType in (3,5,13,15,23,25,31): | |
# Parts count | |
size += 4 | |
# Parts index array | |
size += nParts * 4 | |
# Shape types with points | |
if self.shapeType in (3,5,8,13,15,23,25,31): | |
# Points count | |
size += 4 | |
# Points array | |
size += 16 * nPoints | |
# Calc size of part types for Multipatch (31) | |
if self.shapeType == 31: | |
size += nParts * 4 | |
# Calc z extremes and values | |
if self.shapeType in (13,15,18,31): | |
# z extremes | |
size += 16 | |
# z array | |
size += 8 * nPoints | |
# Calc m extremes and values | |
if self.shapeType in (23,25,31): | |
# m extremes | |
size += 16 | |
# m array | |
size += 8 * nPoints | |
# Calc a single point | |
if self.shapeType in (1,11,21): | |
size += 16 | |
# Calc a single Z value | |
if self.shapeType == 11: | |
size += 8 | |
# Calc a single M value | |
if self.shapeType in (11,21): | |
size += 8 | |
# Calculate size as 16-bit words | |
size //= 2 | |
return size | |
def __bbox(self, shapes, shapeTypes=[]): | |
x = [] | |
y = [] | |
for s in shapes: | |
shapeType = self.shapeType | |
if shapeTypes: | |
shapeType = shapeTypes[shapes.index(s)] | |
px, py = list(zip(*s.points))[:2] | |
x.extend(px) | |
y.extend(py) | |
return [min(x), min(y), max(x), max(y)] | |
def __zbox(self, shapes, shapeTypes=[]): | |
z = [] | |
for s in shapes: | |
try: | |
for p in s.points: | |
z.append(p[2]) | |
except IndexError: | |
pass | |
if not z: z.append(0) | |
return [min(z), max(z)] | |
def __mbox(self, shapes, shapeTypes=[]): | |
m = [0] | |
for s in shapes: | |
try: | |
for p in s.points: | |
m.append(p[3]) | |
except IndexError: | |
pass | |
return [min(m), max(m)] | |
def bbox(self): | |
"""Returns the current bounding box for the shapefile which is | |
the lower-left and upper-right corners. It does not contain the | |
elevation or measure extremes.""" | |
return self.__bbox(self._shapes) | |
def zbox(self): | |
"""Returns the current z extremes for the shapefile.""" | |
return self.__zbox(self._shapes) | |
def mbox(self): | |
"""Returns the current m extremes for the shapefile.""" | |
return self.__mbox(self._shapes) | |
def __shapefileHeader(self, fileObj, headerType='shp'): | |
"""Writes the specified header type to the specified file-like object. | |
Several of the shapefile formats are so similar that a single generic | |
method to read or write them is warranted.""" | |
f = self.__getFileObj(fileObj) | |
f.seek(0) | |
# File code, Unused bytes | |
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())) | |
elif headerType == 'shx': | |
f.write(pack('>i', ((100 + (len(self._shapes) * 8)) // 2))) | |
# Version, Shape type | |
f.write(pack("<2i", 1000, self.shapeType)) | |
# The shapefile's bounding box (lower left, upper right) | |
if self.shapeType != 0: | |
try: | |
f.write(pack("<4d", *self.bbox())) | |
except error: | |
raise ShapefileException("Failed to write shapefile bounding box. Floats required.") | |
else: | |
f.write(pack("<4d", 0,0,0,0)) | |
# Elevation | |
z = self.zbox() | |
# Measure | |
m = self.mbox() | |
try: | |
f.write(pack("<4d", z[0], z[1], m[0], m[1])) | |
except error: | |
raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.") | |
def __dbfHeader(self): | |
"""Writes the dbf header and field descriptors.""" | |
f = self.__getFileObj(self.dbf) | |
f.seek(0) | |
version = 3 | |
year, month, day = time.localtime()[:3] | |
year -= 1900 | |
# Remove deletion flag placeholder from fields | |
for field in self.fields: | |
if field[0].startswith("Deletion"): | |
self.fields.remove(field) | |
numRecs = len(self.records) | |
numFields = len(self.fields) | |
headerLength = numFields * 32 + 33 | |
recordLength = sum([int(field[2]) for field in self.fields]) + 1 | |
header = pack('<BBBBLHH20x', version, year, month, day, numRecs, | |
headerLength, recordLength) | |
f.write(header) | |
# Field descriptors | |
for field in self.fields: | |
name, fieldType, size, decimal = field | |
name = name | |
name = name.replace(' ', '_') | |
name = name.ljust(11).replace(' ', '\x00') | |
fieldType = fieldType | |
size = int(size) | |
fld = pack('<11sc4xBB14x', name, fieldType, size, decimal) | |
f.write(fld) | |
# Terminator | |
f.write('\r') | |
def __shpRecords(self): | |
"""Write the shp records""" | |
f = self.__getFileObj(self.shp) | |
f.seek(100) | |
recNum = 1 | |
for s in self._shapes: | |
self._offsets.append(f.tell()) | |
# Record number, Content length place holder | |
f.write(pack(">2i", recNum, 0)) | |
recNum += 1 | |
start = f.tell() | |
# Shape Type | |
if self.shapeType != 31: | |
s.shapeType = self.shapeType | |
f.write(pack("<i", s.shapeType)) | |
# All shape types capable of having a bounding box | |
if s.shapeType in (3,5,8,13,15,18,23,25,28,31): | |
try: | |
f.write(pack("<4d", *self.__bbox([s]))) | |
except error: | |
raise ShapefileException("Falied to write bounding box for record %s. Expected floats." % recNum) | |
# Shape types with parts | |
if s.shapeType in (3,5,13,15,23,25,31): | |
# Number of parts | |
f.write(pack("<i", len(s.parts))) | |
# Shape types with multiple points per record | |
if s.shapeType in (3,5,8,13,15,23,25,31): | |
# Number of points | |
f.write(pack("<i", len(s.points))) | |
# Write part indexes | |
if s.shapeType in (3,5,13,15,23,25,31): | |
for p in s.parts: | |
f.write(pack("<i", p)) | |
# Part types for Multipatch (31) | |
if s.shapeType == 31: | |
for pt in s.partTypes: | |
f.write(pack("<i", pt)) | |
# Write points for multiple-point records | |
if s.shapeType in (3,5,8,13,15,23,25,31): | |
try: | |
[f.write(pack("<2d", *p[:2])) for p in s.points] | |
except error: | |
raise ShapefileException("Failed to write points for record %s. Expected floats." % recNum) | |
# Write z extremes and values | |
if s.shapeType in (13,15,18,31): | |
try: | |
f.write(pack("<2d", *self.__zbox([s]))) | |
except error: | |
raise ShapefileException("Failed to write elevation extremes for record %s. Expected floats." % recNum) | |
try: | |
if hasattr(s,"z"): | |
f.write(pack("<%sd" % len(s.z), *s.z)) | |
else: | |
[f.write(pack("<d", p[2])) for p in s.points] | |
except error: | |
raise ShapefileException("Failed to write elevation values for record %s. Expected floats." % recNum) | |
# Write m extremes and values | |
if s.shapeType in (13,15,18,23,25,28,31): | |
try: | |
if hasattr(s,"m"): | |
f.write(pack("<%sd" % len(s.m), *s.m)) | |
else: | |
f.write(pack("<2d", *self.__mbox([s]))) | |
except error: | |
raise ShapefileException("Failed to write measure extremes for record %s. Expected floats" % recNum) | |
try: | |
[f.write(pack("<d", p[3])) for p in s.points] | |
except error: | |
raise ShapefileException("Failed to write measure values for record %s. Expected floats" % recNum) | |
# Write a single point | |
if s.shapeType in (1,11,21): | |
try: | |
f.write(pack("<2d", s.points[0][0], s.points[0][1])) | |
except error: | |
raise ShapefileException("Failed to write point for record %s. Expected floats." % recNum) | |
# Write a single Z value | |
if s.shapeType == 11: | |
if hasattr(s, "z"): | |
try: | |
if not s.z: | |
s.z = (0,) | |
f.write(pack("<d", s.z[0])) | |
except error: | |
raise ShapefileException("Failed to write elevation value for record %s. Expected floats." % recNum) | |
else: | |
try: | |
if len(s.points[0])<3: | |
s.points[0].append(0) | |
f.write(pack("<d", s.points[0][2])) | |
except error: | |
raise ShapefileException("Failed to write elevation value for record %s. Expected floats." % recNum) | |
# Write a single M value | |
if s.shapeType in (11,21): | |
if hasattr(s, "m"): | |
try: | |
if not s.m: | |
s.m = (0,) | |
f.write(pack("<1d", s.m[0])) | |
except error: | |
raise ShapefileException("Failed to write measure value for record %s. Expected floats." % recNum) | |
else: | |
try: | |
if len(s.points[0])<4: | |
s.points[0].append(0) | |
f.write(pack("<1d", s.points[0][3])) | |
except error: | |
raise ShapefileException("Failed to write measure value for record %s. Expected floats." % recNum) | |
# Finalize record length as 16-bit words | |
finish = f.tell() | |
length = (finish - start) // 2 | |
self._lengths.append(length) | |
# start - 4 bytes is the content length field | |
f.seek(start-4) | |
f.write(pack(">i", length)) | |
f.seek(finish) | |
def __shxRecords(self): | |
"""Writes the shx records.""" | |
f = self.__getFileObj(self.shx) | |
f.seek(100) | |
for i in range(len(self._shapes)): | |
f.write(pack(">i", self._offsets[i] // 2)) | |
f.write(pack(">i", self._lengths[i])) | |
def __dbfRecords(self): | |
"""Writes the dbf records.""" | |
f = self.__getFileObj(self.dbf) | |
for record in self.records: | |
if not self.fields[0][0].startswith("Deletion"): | |
f.write(' ') # deletion flag | |
for (fieldName, fieldType, size, dec), value in zip(self.fields, record): | |
fieldType = fieldType.upper() | |
size = int(size) | |
if fieldType.upper() == "N": | |
value = str(value).rjust(size) | |
elif fieldType == 'L': | |
value = str(value)[0].upper() | |
else: | |
value = str(value)[:size].ljust(size) | |
if len(value) != size: | |
raise ShapefileException( | |
"Shapefile Writer unable to pack incorrect sized value" | |
" (size %d) into field '%s' (size %d)." % (len(value), fieldName, size)) | |
value = value | |
f.write(value) | |
def null(self): | |
"""Creates a null shape.""" | |
self._shapes.append(_Shape(NULL)) | |
def point(self, x, y, z=0, m=0): | |
"""Creates a point shape.""" | |
pointShape = _Shape(self.shapeType) | |
pointShape.points.append([x, y, z, m]) | |
self._shapes.append(pointShape) | |
def line(self, parts=[], shapeType=POLYLINE): | |
"""Creates a line shape. This method is just a convienience method | |
which wraps 'poly()'. | |
""" | |
self.poly(parts, shapeType, []) | |
def poly(self, parts=[], shapeType=POLYGON, partTypes=[]): | |
"""Creates a shape that has multiple collections of points (parts) | |
including lines, polygons, and even multipoint shapes. If no shape type | |
is specified it defaults to 'polygon'. If no part types are specified | |
(which they normally won't be) then all parts default to the shape type. | |
""" | |
polyShape = _Shape(shapeType) | |
polyShape.parts = [] | |
polyShape.points = [] | |
# Make sure polygons are closed | |
if shapeType in (5,15,25,31): | |
for part in parts: | |
if part[0] != part[-1]: | |
part.append(part[0]) | |
for part in parts: | |
polyShape.parts.append(len(polyShape.points)) | |
for point in part: | |
# Ensure point is list | |
if not isinstance(point, list): | |
point = list(point) | |
# Make sure point has z and m values | |
while len(point) < 4: | |
point.append(0) | |
polyShape.points.append(point) | |
if polyShape.shapeType == 31: | |
if not partTypes: | |
for part in parts: | |
partTypes.append(polyShape.shapeType) | |
polyShape.partTypes = partTypes | |
self._shapes.append(polyShape) | |
def field(self, name, fieldType="C", size="50", decimal=0): | |
"""Adds a dbf field descriptor to the shapefile.""" | |
self.fields.append((name, fieldType, size, decimal)) | |
def record(self, *recordList, **recordDict): | |
"""Creates a dbf attribute record. You can submit either a sequence of | |
field values or keyword arguments of field names and values. Before | |
adding records you must add fields for the record values using the | |
fields() method. If the record values exceed the number of fields the | |
extra ones won't be added. In the case of using keyword arguments to specify | |
field/value pairs only fields matching the already registered fields | |
will be added.""" | |
record = [] | |
fieldCount = len(self.fields) | |
# Compensate for deletion flag | |
if self.fields[0][0].startswith("Deletion"): fieldCount -= 1 | |
if recordList: | |
[record.append(recordList[0][i]) for i in range(fieldCount)] | |
elif recordDict: | |
for field in self.fields: | |
if field[0] in recordDict: | |
val = recordDict[field[0]] | |
if val is None: | |
record.append("") | |
else: | |
record.append(val) | |
if record: | |
self.records.append(record) | |
def shape(self, i): | |
return self._shapes[i] | |
def shapes(self): | |
"""Return the current list of shapes.""" | |
return self._shapes | |
def saveShp(self, target): | |
"""Save an shp file.""" | |
if not hasattr(target, "write"): | |
target = os.path.splitext(target)[0] + '.shp' | |
if not self.shapeType: | |
self.shapeType = self._shapes[0].shapeType | |
self.shp = self.__getFileObj(target) | |
self.__shapefileHeader(self.shp, headerType='shp') | |
self.__shpRecords() | |
def saveShx(self, target): | |
"""Save an shx file.""" | |
if not hasattr(target, "write"): | |
target = os.path.splitext(target)[0] + '.shx' | |
if not self.shapeType: | |
self.shapeType = self._shapes[0].shapeType | |
self.shx = self.__getFileObj(target) | |
self.__shapefileHeader(self.shx, headerType='shx') | |
self.__shxRecords() | |
def saveDbf(self, target): | |
"""Save a dbf file.""" | |
if not hasattr(target, "write"): | |
target = os.path.splitext(target)[0] + '.dbf' | |
self.dbf = self.__getFileObj(target) | |
self.__dbfHeader() | |
self.__dbfRecords() | |
def save(self, target=None, shp=None, shx=None, dbf=None): | |
"""Save the shapefile data to three files or | |
three file-like objects. SHP and DBF files can also | |
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. | |
""" | |
# Create a unique file name if one is not defined | |
if shp: | |
self.saveShp(shp) | |
if shx: | |
self.saveShx(shx) | |
if dbf: | |
self.saveDbf(dbf) | |
elif not shp and not shx and not dbf: | |
generated = False | |
if not target: | |
temp = tempfile.NamedTemporaryFile(prefix="shapefile_",dir=os.getcwd()) | |
target = temp.name | |
generated = True | |
self.saveShp(target) | |
self.shp.close() | |
self.saveShx(target) | |
self.shx.close() | |
self.saveDbf(target) | |
self.dbf.close() | |
if generated: | |
return target |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment