Path: blob/main/tools/contributed/sumopy/coremodules/misc/shapefile.py
169689 views
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo1# Copyright (C) 2016-2025 German Aerospace Center (DLR) and others.2# SUMOPy module3# Copyright (C) 2012-2021 University of Bologna - DICAM4# This program and the accompanying materials are made available under the5# terms of the Eclipse Public License 2.0 which is available at6# https://www.eclipse.org/legal/epl-2.0/7# This Source Code may also be made available under the following Secondary8# Licenses when the conditions for such availability set forth in the Eclipse9# Public License 2.0 are satisfied: GNU General Public License, version 210# or later which is available at11# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html12# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later1314# @file shapefile.py15# @author Joerg Schweizer16# @date 20121718"""19shapefile.py20Provides read and write support for ESRI Shapefiles.21author: jlawhead<at>geospatialpython.com22date: 2011092723version: 1.1.424Compatible with Python versions 2.4-3.x25"""2627from struct import pack, unpack, calcsize, error28import os29import sys30import time31import array32#33# Constants for shape types34NULL = 035POINT = 136POLYLINE = 337POLYGON = 538MULTIPOINT = 839POINTZ = 1140POLYLINEZ = 1341POLYGONZ = 1542MULTIPOINTZ = 1843POINTM = 2144POLYLINEM = 2345POLYGONM = 2546MULTIPOINTM = 2847MULTIPATCH = 314849PYTHON3 = sys.version_info[0] == 3505152def b(v):53if PYTHON3:54if isinstance(v, str):55# For python 3 encode str to bytes.56return v.encode('utf-8')57elif isinstance(v, bytes):58# Already bytes.59return v60else:61# Error.62raise Exception('Unknown input type')63else:64# For python 2 assume str passed in and return str.65return v666768def u(v):69if PYTHON3:70if isinstance(v, bytes):71# For python 3 decode bytes to str.72return v.decode('utf-8')73elif isinstance(v, str):74# Already str.75return v76else:77# Error.78raise Exception('Unknown input type')79else:80# For python 2 assume str passed in and return str.81return v828384def is_string(v):85if PYTHON3:86return isinstance(v, str)87else:88return isinstance(v, basestring)899091class _Array(array.array):92"""Converts python tuples to lits of the appropritate type.93Used to unpack different shapefile header parts."""9495def __repr__(self):96return str(self.tolist())979899class _Shape:100def __init__(self, shapeType=None):101"""Stores the geometry of the different shape types102specified in the Shapefile spec. Shape types are103usually point, polyline, or polygons. Every shape type104except the "Null" type contains points at some level for105example verticies in a polygon. If a shape type has106multiple shapes containing points within a single107geometry record then those shapes are called parts. Parts108are designated by their starting index in geometry record's109list of shapes."""110self.shapeType = shapeType111self.points = []112113114class _ShapeRecord:115"""A shape object of any type."""116117def __init__(self, shape=None, record=None):118self.shape = shape119self.record = record120121122class ShapefileException(Exception):123"""An exception to handle shapefile specific problems."""124pass125126127class Reader:128"""Reads the three files of a shapefile as a unit or129separately. If one of the three files (.shp, .shx,130.dbf) is missing no exception is thrown until you try131to call a method that depends on that particular file.132The .shx index file is used if available for efficiency133but is not required to read the geometry from the .shp134file. The "shapefile" argument in the constructor is the135name of the file you want to open.136137You can instantiate a Reader without specifying a shapefile138and then specify one later with the load() method.139140Only the shapefile headers are read upon loading. Content141within each file is only accessed when required and as142efficiently as possible. Shapefiles are usually not large143but they can be.144"""145146def __init__(self, *args, **kwargs):147self.shp = None148self.shx = None149self.dbf = None150self.shapeName = "Not specified"151self._offsets = []152self.shpLength = None153self.numRecords = None154self.fields = []155self.__dbfHdrLength = 0156# See if a shapefile name was passed as an argument157if len(args) > 0:158if type(args[0]) is type("stringTest"):159# print '\n\nReader.__init__: load',args[0]160self.load(args[0])161return162if "shp" in kwargs.keys():163if hasattr(kwargs["shp"], "read"):164self.shp = kwargs["shp"]165if hasattr(self.shp, "seek"):166self.shp.seek(0)167if "shx" in kwargs.keys():168if hasattr(kwargs["shx"], "read"):169self.shx = kwargs["shx"]170if hasattr(self.shx, "seek"):171self.shx.seek(0)172if "dbf" in kwargs.keys():173if hasattr(kwargs["dbf"], "read"):174self.dbf = kwargs["dbf"]175if hasattr(self.dbf, "seek"):176self.dbf.seek(0)177if self.shp or self.dbf:178self.load()179else:180raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.")181182def load(self, shapefile=None):183"""Opens a shapefile from a filename or file-like184object. Normally this method would be called by the185constructor with the file object or file name as an186argument."""187if shapefile:188(shapeName, ext) = os.path.splitext(shapefile)189self.shapeName = shapeName190try:191self.shp = open("%s.shp" % shapeName, "rb")192except IOError:193raise ShapefileException("Unable to open %s.shp" % shapeName)194try:195self.shx = open("%s.shx" % shapeName, "rb")196except IOError:197raise ShapefileException("Unable to open %s.shx" % shapeName)198try:199self.dbf = open("%s.dbf" % shapeName, "rb")200except IOError:201raise ShapefileException("Unable to open %s.dbf" % shapeName)202if self.shp:203self.__shpHeader()204if self.dbf:205self.__dbfHeader()206207def __getFileObj(self, f):208"""Checks to see if the requested shapefile file object is209available. If not a ShapefileException is raised."""210if not f:211raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.")212if self.shp and self.shpLength is None:213self.load()214if self.dbf and len(self.fields) == 0:215self.load()216return f217218def __restrictIndex(self, i):219"""Provides list-like handling of a record index with a clearer220error message if the index is out of bounds."""221if self.numRecords:222rmax = self.numRecords - 1223if abs(i) > rmax:224raise IndexError("Shape or Record index out of range.")225if i < 0:226i = range(self.numRecords)[i]227return i228229def __shpHeader(self):230"""Reads the header information from a .shp or .shx file."""231if not self.shp:232raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shp file found")233shp = self.shp234# File length (16-bit word * 2 = bytes)235shp.seek(24)236self.shpLength = unpack(">i", shp.read(4))[0] * 2237# Shape type238shp.seek(32)239self.shapeType = unpack("<i", shp.read(4))[0]240# The shapefile's bounding box (lower left, upper right)241self.bbox = _Array('d', unpack("<4d", shp.read(32)))242# Elevation243self.elevation = _Array('d', unpack("<2d", shp.read(16)))244# Measure245self.measure = _Array('d', unpack("<2d", shp.read(16)))246247def __shape(self):248"""Returns the header info and geometry for a single shape."""249f = self.__getFileObj(self.shp)250record = _Shape()251nParts = nPoints = zmin = zmax = mmin = mmax = None252(recNum, recLength) = unpack(">2i", f.read(8))253shapeType = unpack("<i", f.read(4))[0]254record.shapeType = shapeType255# For Null shapes create an empty points list for consistency256if shapeType == 0:257record.points = []258# All shape types capable of having a bounding box259elif shapeType in (3, 5, 8, 13, 15, 18, 23, 25, 28, 31):260record.bbox = _Array('d', unpack("<4d", f.read(32)))261# Shape types with parts262if shapeType in (3, 5, 13, 15, 23, 25, 31):263nParts = unpack("<i", f.read(4))[0]264# Shape types with points265if shapeType in (3, 5, 8, 13, 15, 23, 25, 31):266nPoints = unpack("<i", f.read(4))[0]267# Read parts268if nParts:269record.parts = _Array('i', unpack("<%si" % nParts, f.read(nParts * 4)))270# Read part types for Multipatch - 31271if shapeType == 31:272record.partTypes = _Array('i', unpack("<%si" % nParts, f.read(nParts * 4)))273# Read points - produces a list of [x,y] values274if nPoints:275record.points = [_Array('d', unpack("<2d", f.read(16))) for p in range(nPoints)]276# Read z extremes and values277if shapeType in (13, 15, 18, 31):278(zmin, zmax) = unpack("<2d", f.read(16))279record.z = _Array('d', unpack("<%sd" % nPoints, f.read(nPoints * 8)))280# Read m extremes and values281if shapeType in (13, 15, 18, 23, 25, 28, 31):282(mmin, mmax) = unpack("<2d", f.read(16))283# Measure values less than -10e38 are nodata values according to the spec284record.m = []285for m in _Array('d', unpack("%sd" % nPoints, f.read(nPoints * 8))):286if m > -10e38:287record.m.append(m)288else:289record.m.append(None)290# Read a single point291if shapeType in (1, 11, 21):292record.points = [_Array('d', unpack("<2d", f.read(16)))]293# Read a single Z value294if shapeType == 11:295record.z = unpack("<d", f.read(8))296# Read a single M value297if shapeType in (11, 21):298record.m = unpack("<d", f.read(8))299return record300301def __shapeIndex(self, i=None):302"""Returns the offset in a .shp file for a shape based on information303in the .shx index file."""304shx = self.shx305if not shx:306return None307if not self._offsets:308# File length (16-bit word * 2 = bytes) - header length309shx.seek(24)310shxRecordLength = (unpack(">i", shx.read(4))[0] * 2) - 100311numRecords = shxRecordLength // 8312# Jump to the first record.313shx.seek(100)314for r in range(numRecords):315# Offsets are 16-bit words just like the file length316self._offsets.append(unpack(">i", shx.read(4))[0] * 2)317shx.seek(shx.tell() + 4)318if not i is None:319return self._offsets[i]320321def shape(self, i=0):322"""Returns a shape object for a shape in the geometry323record file."""324shp = self.__getFileObj(self.shp)325i = self.__restrictIndex(i)326offset = self.__shapeIndex(i)327if not offset:328# Shx index not available so use the full list.329shapes = self.shapes()330return shapes[i]331shp.seek(offset)332return self.__shape()333334def shapes(self):335"""Returns all shapes in a shapefile."""336shp = self.__getFileObj(self.shp)337shp.seek(100)338shapes = []339while shp.tell() < self.shpLength:340shapes.append(self.__shape())341return shapes342343def __dbfHeaderLength(self):344"""Retrieves the header length of a dbf file header."""345if not self.__dbfHdrLength:346if not self.dbf:347raise ShapefileException(348"Shapefile Reader requires a shapefile or file-like object. (no dbf file found)")349dbf = self.dbf350(self.numRecords, self.__dbfHdrLength) = \351unpack("<xxxxLH22x", dbf.read(32))352return self.__dbfHdrLength353354def __dbfHeader(self):355"""Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger"""356if not self.dbf:357raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)")358dbf = self.dbf359headerLength = self.__dbfHeaderLength()360numFields = (headerLength - 33) // 32361for field in range(numFields):362fieldDesc = list(unpack("<11sc4xBB14x", dbf.read(32)))363name = 0364idx = 0365if b("\x00") in fieldDesc[name]:366idx = fieldDesc[name].index(b("\x00"))367else:368idx = len(fieldDesc[name]) - 1369fieldDesc[name] = fieldDesc[name][:idx]370fieldDesc[name] = u(fieldDesc[name])371fieldDesc[name] = fieldDesc[name].lstrip()372fieldDesc[1] = u(fieldDesc[1])373self.fields.append(fieldDesc)374terminator = dbf.read(1)375assert terminator == b("\r")376self.fields.insert(0, ('DeletionFlag', 'C', 1, 0))377378def __recordFmt(self):379"""Calculates the size of a .shp geometry record."""380if not self.numRecords:381self.__dbfHeader()382fmt = ''.join(['%ds' % fieldinfo[2] for fieldinfo in self.fields])383fmtSize = calcsize(fmt)384return (fmt, fmtSize)385386def __record(self):387"""Reads and returns a dbf record row as a list of values."""388f = self.__getFileObj(self.dbf)389recFmt = self.__recordFmt()390recordContents = unpack(recFmt[0], f.read(recFmt[1]))391if recordContents[0] != b(' '):392# deleted record393return None394record = []395for (name, typ, size, deci), value in zip(self.fields,396recordContents):397if name == 'DeletionFlag':398continue399elif not value.strip():400record.append(value)401continue402elif typ == "N":403value = value.replace(b('\0'), b('')).strip()404if value == b(''):405value = 0406elif deci:407value = float(value)408else:409try:410value = int(float(value))411except:412value = None413elif typ == b('D'):414try:415y, m, d = int(value[:4]), int(value[4:6]), int(value[6:8])416value = [y, m, d]417except:418value = value.strip()419elif typ == b('L'):420value = (value in b('YyTt') and b('T')) or \421(value in b('NnFf') and b('F')) or b('?')422else:423value = u(value)424value = value.strip()425record.append(value)426return record427428def record(self, i=0):429"""Returns a specific dbf record based on the supplied index."""430f = self.__getFileObj(self.dbf)431if not self.numRecords:432self.__dbfHeader()433i = self.__restrictIndex(i)434recSize = self.__recordFmt()[1]435f.seek(0)436f.seek(self.__dbfHeaderLength() + (i * recSize))437return self.__record()438439def records(self):440"""Returns all records in a dbf file."""441if not self.numRecords:442self.__dbfHeader()443records = []444f = self.__getFileObj(self.dbf)445f.seek(self.__dbfHeaderLength())446for i in range(self.numRecords):447r = self.__record()448if r:449records.append(r)450return records451452def shapeRecord(self, i=0):453"""Returns a combination geometry and attribute record for the454supplied record index."""455i = self.__restrictIndex(i)456return _ShapeRecord(shape=self.shape(i),457record=self.record(i))458459def shapeRecords(self):460"""Returns a list of combination geometry/attribute records for461all records in a shapefile."""462shapeRecords = []463return [_ShapeRecord(shape=rec[0], record=rec[1])464for rec in zip(self.shapes(), self.records())]465466467class Writer:468"""Provides write support for ESRI Shapefiles."""469470def __init__(self, shapeType=None):471self._shapes = []472self.fields = []473self.records = []474self.shapeType = shapeType475self.shp = None476self.shx = None477self.dbf = None478# Geometry record offsets and lengths for writing shx file.479self._offsets = []480self._lengths = []481# Use deletion flags in dbf? Default is false (0).482self.deletionFlag = 0483484def __getFileObj(self, f):485"""Safety handler to verify file-like objects"""486if not f:487raise ShapefileException("No file-like object available.")488elif hasattr(f, "write"):489return f490else:491pth = os.path.split(f)[0]492if pth and not os.path.exists(pth):493os.makedirs(pth)494return open(f, "wb")495496def __shpFileLength(self):497"""Calculates the file length of the shp file."""498# Start with header length499size = 100500# Calculate size of all shapes501for s in self._shapes:502# Add in record header and shape type fields503size += 12504# nParts and nPoints do not apply to all shapes505# if self.shapeType not in (0,1):506# nParts = len(s.parts)507# nPoints = len(s.points)508if hasattr(s, 'parts'):509nParts = len(s.parts)510if hasattr(s, 'points'):511nPoints = len(s.points)512# All shape types capable of having a bounding box513if self.shapeType in (3, 5, 8, 13, 15, 18, 23, 25, 28, 31):514size += 32515# Shape types with parts516if self.shapeType in (3, 5, 13, 15, 23, 25, 31):517# Parts count518size += 4519# Parts index array520size += nParts * 4521# Shape types with points522if self.shapeType in (3, 5, 8, 13, 15, 23, 25, 31):523# Points count524size += 4525# Points array526size += 16 * nPoints527# Calc size of part types for Multipatch (31)528if self.shapeType == 31:529size += nParts * 4530# Calc z extremes and values531if self.shapeType in (13, 15, 18, 31):532# z extremes533size += 16534# z array535size += 8 * nPoints536# Calc m extremes and values537if self.shapeType in (23, 25, 31):538# m extremes539size += 16540# m array541size += 8 * nPoints542# Calc a single point543if self.shapeType in (1, 11, 21):544size += 16545# Calc a single Z value546if self.shapeType == 11:547size += 8548# Calc a single M value549if self.shapeType in (11, 21):550size += 8551# Calculate size as 16-bit words552size //= 2553return size554555def __bbox(self, shapes, shapeTypes=[]):556x = []557y = []558for s in shapes:559shapeType = self.shapeType560if shapeTypes:561shapeType = shapeTypes[shapes.index(s)]562px, py = list(zip(*s.points))[:2]563x.extend(px)564y.extend(py)565return [min(x), min(y), max(x), max(y)]566567def __zbox(self, shapes, shapeTypes=[]):568z = []569for s in shapes:570try:571for p in s.points:572z.append(p[2])573except IndexError:574pass575if not z:576z.append(0)577return [min(z), max(z)]578579def __mbox(self, shapes, shapeTypes=[]):580m = [0]581for s in shapes:582try:583for p in s.points:584m.append(p[3])585except IndexError:586pass587return [min(m), max(m)]588589def bbox(self):590"""Returns the current bounding box for the shapefile which is591the lower-left and upper-right corners. It does not contain the592elevation or measure extremes."""593return self.__bbox(self._shapes)594595def zbox(self):596"""Returns the current z extremes for the shapefile."""597return self.__zbox(self._shapes)598599def mbox(self):600"""Returns the current m extremes for the shapefile."""601return self.__mbox(self._shapes)602603def __shapefileHeader(self, fileObj, headerType='shp'):604"""Writes the specified header type to the specified file-like object.605Several of the shapefile formats are so similar that a single generic606method to read or write them is warranted."""607f = self.__getFileObj(fileObj)608f.seek(0)609# File code, Unused bytes610f.write(pack(">6i", 9994, 0, 0, 0, 0, 0))611# File length (Bytes / 2 = 16-bit words)612if headerType == 'shp':613f.write(pack(">i", self.__shpFileLength()))614elif headerType == 'shx':615f.write(pack('>i', ((100 + (len(self._shapes) * 8)) // 2)))616# Version, Shape type617f.write(pack("<2i", 1000, self.shapeType))618# The shapefile's bounding box (lower left, upper right)619if self.shapeType != 0:620try:621f.write(pack("<4d", *self.bbox()))622except error:623raise ShapefileException("Failed to write shapefile bounding box. Floats required.")624else:625f.write(pack("<4d", 0, 0, 0, 0))626# Elevation627z = self.zbox()628# Measure629m = self.mbox()630try:631f.write(pack("<4d", z[0], z[1], m[0], m[1]))632except error:633raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.")634635def __dbfHeader(self):636"""Writes the dbf header and field descriptors."""637f = self.__getFileObj(self.dbf)638f.seek(0)639version = 3640year, month, day = time.localtime()[:3]641year -= 1900642# Remove deletion flag placeholder from fields643for field in self.fields:644if field[0].startswith("Deletion"):645self.fields.remove(field)646numRecs = len(self.records)647numFields = len(self.fields)648headerLength = numFields * 32 + 33649recordLength = sum([int(field[2]) for field in self.fields]) + 1650header = pack('<BBBBLHH20x', version, year, month, day, numRecs,651headerLength, recordLength)652f.write(header)653# Field descriptors654for field in self.fields:655name, fieldType, size, decimal = field656name = b(name)657name = name.replace(b(' '), b('_'))658name = name.ljust(11).replace(b(' '), b('\x00'))659fieldType = b(fieldType)660size = int(size)661fld = pack('<11sc4xBB14x', name, fieldType, size, decimal)662f.write(fld)663# Terminator664f.write(b('\r'))665666def __shpRecords(self):667"""Write the shp records"""668f = self.__getFileObj(self.shp)669f.seek(100)670recNum = 1671for s in self._shapes:672self._offsets.append(f.tell())673# Record number, Content length place holder674f.write(pack(">2i", recNum, 0))675recNum += 1676start = f.tell()677# Shape Type678f.write(pack("<i", s.shapeType))679# All shape types capable of having a bounding box680if s.shapeType in (3, 5, 8, 13, 15, 18, 23, 25, 28, 31):681try:682f.write(pack("<4d", *self.__bbox([s])))683except error:684raise ShapefileException("Falied to write bounding box for record %s. Expected floats." % recNum)685# Shape types with parts686if s.shapeType in (3, 5, 13, 15, 23, 25, 31):687# Number of parts688f.write(pack("<i", len(s.parts)))689# Shape types with multiple points per record690if s.shapeType in (3, 5, 8, 13, 15, 23, 25, 31):691# Number of points692f.write(pack("<i", len(s.points)))693# Write part indexes694if s.shapeType in (3, 5, 13, 15, 23, 25, 31):695for p in s.parts:696f.write(pack("<i", p))697# Part types for Multipatch (31)698if s.shapeType == 31:699for pt in s.partTypes:700f.write(pack("<i", pt))701# Write points for multiple-point records702if s.shapeType in (3, 5, 8, 13, 15, 23, 25, 31):703try:704[f.write(pack("<2d", *p[:2])) for p in s.points]705except error:706raise ShapefileException("Failed to write points for record %s. Expected floats." % recNum)707# Write z extremes and values708if s.shapeType in (13, 15, 18, 31):709try:710f.write(pack("<2d", *self.__zbox([s])))711except error:712raise ShapefileException(713"Failed to write elevation extremes for record %s. Expected floats." % recNum)714try:715[f.write(pack("<d", p[2])) for p in s.points]716except error:717raise ShapefileException(718"Failed to write elevation values for record %s. Expected floats." % recNum)719# Write m extremes and values720if s.shapeType in (23, 25, 31):721try:722f.write(pack("<2d", *self.__mbox([s])))723except error:724raise ShapefileException("Failed to write measure extremes for record %s. Expected floats" % recNum)725try:726[f.write(pack("<d", p[3])) for p in s.points]727except error:728raise ShapefileException("Failed to write measure values for record %s. Expected floats" % recNum)729# Write a single point730if s.shapeType in (1, 11, 21):731try:732f.write(pack("<2d", s.points[0][0], s.points[0][1]))733except error:734raise ShapefileException("Failed to write point for record %s. Expected floats." % recNum)735# Write a single Z value736if s.shapeType == 11:737try:738f.write(pack("<1d", s.points[0][2]))739except error:740raise ShapefileException("Failed to write elevation value for record %s. Expected floats." % recNum)741# Write a single M value742if s.shapeType in (11, 21):743try:744f.write(pack("<1d", s.points[0][3]))745except error:746raise ShapefileException("Failed to write measure value for record %s. Expected floats." % recNum)747# Finalize record length as 16-bit words748finish = f.tell()749length = (finish - start) // 2750self._lengths.append(length)751# start - 4 bytes is the content length field752f.seek(start-4)753f.write(pack(">i", length))754f.seek(finish)755756def __shxRecords(self):757"""Writes the shx records."""758f = self.__getFileObj(self.shx)759f.seek(100)760for i in range(len(self._shapes)):761f.write(pack(">i", self._offsets[i] // 2))762f.write(pack(">i", self._lengths[i]))763764def __dbfRecords(self):765"""Writes the dbf records."""766f = self.__getFileObj(self.dbf)767for record in self.records:768if not self.fields[0][0].startswith("Deletion"):769f.write(b(' ')) # deletion flag770for (fieldName, fieldType, size, dec), value in zip(self.fields, record):771fieldType = fieldType.upper()772size = int(size)773if fieldType.upper() == "N":774value = str(value).rjust(size)775elif fieldType == 'L':776value = str(value)[0].upper()777else:778value = str(value)[:size].ljust(size)779# print ' __dbfRecords',fieldName,value,len(value),size780assert len(value) == size781value = b(value)782f.write(value)783784def null(self):785"""Creates a null shape."""786self._shapes.append(_Shape(NULL))787788def point(self, x, y, z=0, m=0):789"""Creates a point shape."""790pointShape = _Shape(self.shapeType)791pointShape.points.append([x, y, z, m])792self._shapes.append(pointShape)793794def line(self, parts=[], shapeType=POLYLINE):795"""Creates a line shape. This method is just a convienience method796which wraps 'poly()'.797"""798self.poly(parts, shapeType, [])799800def poly(self, parts=[], shapeType=POLYGON, partTypes=[]):801"""Creates a shape that has multiple collections of points (parts)802including lines, polygons, and even multipoint shapes. If no shape type803is specified it defaults to 'polygon'. If no part types are specified804(which they normally won't be) then all parts default to the shape type.805"""806polyShape = _Shape(shapeType)807polyShape.parts = []808polyShape.points = []809for part in parts:810polyShape.parts.append(len(polyShape.points))811for point in part:812# Ensure point is list813if not isinstance(point, list):814point = list(point)815# Make sure point has z and m values816while len(point) < 4:817point.append(0)818polyShape.points.append(point)819if polyShape.shapeType == 31:820if not partTypes:821for part in parts:822partTypes.append(polyShape.shapeType)823polyShape.partTypes = partTypes824self._shapes.append(polyShape)825826def field(self, name, fieldType="C", size="50", decimal=0):827"""Adds a dbf field descriptor to the shapefile."""828self.fields.append((name, fieldType, size, decimal))829830def record(self, *recordList, **recordDict):831"""Creates a dbf attribute record. You can submit either a sequence of832field values or keyword arguments of field names and values. Before833adding records you must add fields for the record values using the834fields() method. If the record values exceed the number of fields the835extra ones won't be added. In the case of using keyword arguments to specify836field/value pairs only fields matching the already registered fields837will be added."""838record = []839fieldCount = len(self.fields)840# Compensate for deletion flag841if self.fields[0][0].startswith("Deletion"):842fieldCount -= 1843if recordList:844[record.append(recordList[i]) for i in range(fieldCount)]845elif recordDict:846for field in self.fields:847if field[0] in recordDict:848val = recordDict[field[0]]849if val:850record.append(val)851else:852record.append("")853if record:854self.records.append(record)855856def shape(self, i):857return self._shapes[i]858859def shapes(self):860"""Return the current list of shapes."""861return self._shapes862863def saveShp(self, target):864"""Save an shp file."""865if not hasattr(target, "write"):866target = os.path.splitext(target)[0] + '.shp'867if not self.shapeType:868self.shapeType = self._shapes[0].shapeType869self.shp = self.__getFileObj(target)870self.__shapefileHeader(self.shp, headerType='shp')871self.__shpRecords()872873def saveShx(self, target):874"""Save an shx file."""875if not hasattr(target, "write"):876target = os.path.splitext(target)[0] + '.shx'877if not self.shapeType:878self.shapeType = self._shapes[0].shapeType879self.shx = self.__getFileObj(target)880self.__shapefileHeader(self.shx, headerType='shx')881self.__shxRecords()882883def saveDbf(self, target):884"""Save a dbf file."""885if not hasattr(target, "write"):886target = os.path.splitext(target)[0] + '.dbf'887self.dbf = self.__getFileObj(target)888self.__dbfHeader()889self.__dbfRecords()890891def save(self, target=None, shp=None, shx=None, dbf=None):892"""Save the shapefile data to three files or893three file-like objects. SHP and DBF files can also894be written exclusively using saveShp, saveShx, and saveDbf respectively."""895# TODO: Create a unique filename for target if None.896if shp:897self.saveShp(shp)898if shx:899self.saveShx(shx)900if dbf:901self.saveDbf(dbf)902elif target:903self.saveShp(target)904self.shp.close()905self.saveShx(target)906self.shx.close()907self.saveDbf(target)908self.dbf.close()909910911class Editor(Writer):912def __init__(self, shapefile=None, shapeType=POINT, autoBalance=1):913self.autoBalance = autoBalance914if not shapefile:915Writer.__init__(self, shapeType)916elif is_string(shapefile):917base = os.path.splitext(shapefile)[0]918if os.path.isfile("%s.shp" % base):919r = Reader(base)920Writer.__init__(self, r.shapeType)921self._shapes = r.shapes()922self.fields = r.fields923self.records = r.records()924925def select(self, expr):926"""Select one or more shapes (to be implemented)"""927# TODO: Implement expressions to select shapes.928pass929930def delete(self, shape=None, part=None, point=None):931"""Deletes the specified part of any shape by specifying a shape932number, part number, or point number."""933# shape, part, point934if shape and part and point:935del self._shapes[shape][part][point]936# shape, part937elif shape and part and not point:938del self._shapes[shape][part]939# shape940elif shape and not part and not point:941del self._shapes[shape]942# point943elif not shape and not part and point:944for s in self._shapes:945if s.shapeType == 1:946del self._shapes[point]947else:948for part in s.parts:949del s[part][point]950# part, point951elif not shape and part and point:952for s in self._shapes:953del s[part][point]954# part955elif not shape and part and not point:956for s in self._shapes:957del s[part]958959def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=None, addr=None):960"""Creates/updates a point shape. The arguments allows961you to update a specific point by shape, part, point of any962shape type."""963# shape, part, point964if shape and part and point:965try:966self._shapes[shape]967except IndexError:968self._shapes.append([])969try:970self._shapes[shape][part]971except IndexError:972self._shapes[shape].append([])973try:974self._shapes[shape][part][point]975except IndexError:976self._shapes[shape][part].append([])977p = self._shapes[shape][part][point]978if x:979p[0] = x980if y:981p[1] = y982if z:983p[2] = z984if m:985p[3] = m986self._shapes[shape][part][point] = p987# shape, part988elif shape and part and not point:989try:990self._shapes[shape]991except IndexError:992self._shapes.append([])993try:994self._shapes[shape][part]995except IndexError:996self._shapes[shape].append([])997points = self._shapes[shape][part]998for i in range(len(points)):999p = points[i]1000if x:1001p[0] = x1002if y:1003p[1] = y1004if z:1005p[2] = z1006if m:1007p[3] = m1008self._shapes[shape][part][i] = p1009# shape1010elif shape and not part and not point:1011try:1012self._shapes[shape]1013except IndexError:1014self._shapes.append([])10151016# point1017# part1018if addr:1019shape, part, point = addr1020self._shapes[shape][part][point] = [x, y, z, m]1021else:1022Writer.point(self, x, y, z, m)1023if self.autoBalance:1024self.balance()10251026def validate(self):1027"""An optional method to try and validate the shapefile1028as much as possible before writing it (not implemented)."""1029# TODO: Implement validation method1030pass10311032def balance(self):1033"""Adds a corresponding empty attribute or null geometry record depending1034on which type of record was created to make sure all three files1035are in synch."""1036if len(self.records) > len(self._shapes):1037self.null()1038elif len(self.records) < len(self._shapes):1039self.record()10401041def __fieldNorm(self, fieldName):1042"""Normalizes a dbf field name to fit within the spec and the1043expectations of certain ESRI software."""1044if len(fieldName) > 11:1045fieldName = fieldName[:11]1046fieldName = fieldName.upper()1047fieldName.replace(' ', '_')10481049# Begin Testing105010511052def test():1053import doctest1054doctest.NORMALIZE_WHITESPACE = 11055doctest.testfile("README.txt", verbose=1)105610571058if __name__ == "__main__":1059"""1060Doctests are contained in the module 'pyshp_usage.py'. This library was developed1061using Python 2.3. Python 2.4 and above have some excellent improvements in the built-in1062testing libraries but for now unit testing is done using what's available in10632.3.1064"""1065test()106610671068