2
2
shapefile.py
3
3
Provides read and write support for ESRI Shapefiles.
4
4
author: jlawhead<at>geospatialpython.com
5
- date: 20110927
6
- version: 1.1.4
5
+ date: 20130622
6
+ version: 1.1.7
7
7
Compatible with Python versions 2.4-3.x
8
8
"""
9
+ __version__ = "1.1.7"
9
10
10
11
from struct import pack , unpack , calcsize , error
11
12
import os
12
13
import sys
13
14
import time
14
15
import array
16
+ import tempfile
15
17
#
16
18
# Constants for shape types
17
- default_encoding = 'utf-8'
18
19
NULL = 0
19
20
POINT = 1
20
21
POLYLINE = 3
32
33
33
34
PYTHON3 = sys .version_info [0 ] == 3
34
35
36
+ if PYTHON3 :
37
+ xrange = range
38
+
35
39
def b (v ):
36
40
if PYTHON3 :
37
41
if isinstance (v , str ):
38
42
# For python 3 encode str to bytes.
39
- return v .encode (default_encoding )
43
+ return v .encode ('utf-8' )
40
44
elif isinstance (v , bytes ):
41
45
# Already bytes.
42
46
return v
@@ -51,7 +55,7 @@ def u(v):
51
55
if PYTHON3 :
52
56
if isinstance (v , bytes ):
53
57
# For python 3 decode bytes to str.
54
- return v .decode (default_encoding )
58
+ return v .decode ('utf-8' )
55
59
elif isinstance (v , str ):
56
60
# Already str.
57
61
return v
@@ -74,6 +78,16 @@ class _Array(array.array):
74
78
def __repr__ (self ):
75
79
return str (self .tolist ())
76
80
81
+ def signed_area (coords ):
82
+ """Return the signed area enclosed by a ring using the linear time
83
+ algorithm at http://www.cgafaq.info/wiki/Polygon_Area. A value <= 0
84
+ indicates a counter-clockwise oriented ring.
85
+ """
86
+ xs , ys = map (list , zip (* coords ))
87
+ xs .append (xs [1 ])
88
+ ys .append (ys [1 ])
89
+ return sum (xs [i ]* (ys [i + 1 ]- ys [i - 1 ]) for i in range (1 , len (coords )))/ 2.0
90
+
77
91
class _Shape :
78
92
def __init__ (self , shapeType = None ):
79
93
"""Stores the geometry of the different shape types
@@ -88,6 +102,78 @@ def __init__(self, shapeType=None):
88
102
self .shapeType = shapeType
89
103
self .points = []
90
104
105
+ @property
106
+ def __geo_interface__ (self ):
107
+ if self .shapeType in [POINT , POINTM , POINTZ ]:
108
+ return {
109
+ 'type' : 'Point' ,
110
+ 'coordinates' : tuple (self .points [0 ])
111
+ }
112
+ elif self .shapeType in [MULTIPOINT , MULTIPOINTM , MULTIPOINTZ ]:
113
+ return {
114
+ 'type' : 'MultiPoint' ,
115
+ 'coordinates' : tuple ([tuple (p ) for p in self .points ])
116
+ }
117
+ elif self .shapeType in [POLYLINE , POLYLINEM , POLYLINEZ ]:
118
+ if len (self .parts ) == 1 :
119
+ return {
120
+ 'type' : 'LineString' ,
121
+ 'coordinates' : tuple ([tuple (p ) for p in self .points ])
122
+ }
123
+ else :
124
+ ps = None
125
+ coordinates = []
126
+ for part in self .parts :
127
+ if ps == None :
128
+ ps = part
129
+ continue
130
+ else :
131
+ coordinates .append (tuple ([tuple (p ) for p in self .points [ps :part ]]))
132
+ ps = part
133
+ else :
134
+ coordinates .append (tuple ([tuple (p ) for p in self .points [part :]]))
135
+ return {
136
+ 'type' : 'MultiLineString' ,
137
+ 'coordinates' : tuple (coordinates )
138
+ }
139
+ elif self .shapeType in [POLYGON , POLYGONM , POLYGONZ ]:
140
+ if len (self .parts ) == 1 :
141
+ return {
142
+ 'type' : 'Polygon' ,
143
+ 'coordinates' : (tuple ([tuple (p ) for p in self .points ]),)
144
+ }
145
+ else :
146
+ ps = None
147
+ coordinates = []
148
+ for part in self .parts :
149
+ if ps == None :
150
+ ps = part
151
+ continue
152
+ else :
153
+ coordinates .append (tuple ([tuple (p ) for p in self .points [ps :part ]]))
154
+ ps = part
155
+ else :
156
+ coordinates .append (tuple ([tuple (p ) for p in self .points [part :]]))
157
+ polys = []
158
+ poly = [coordinates [0 ]]
159
+ for coord in coordinates [1 :]:
160
+ if signed_area (coord ) < 0 :
161
+ polys .append (poly )
162
+ poly = [coord ]
163
+ else :
164
+ poly .append (coord )
165
+ polys .append (poly )
166
+ if len (polys ) == 1 :
167
+ return {
168
+ 'type' : 'Polygon' ,
169
+ 'coordinates' : tuple (polys [0 ])
170
+ }
171
+ elif len (polys ) > 1 :
172
+ return {
173
+ 'type' : 'MultiPolygon' ,
174
+ 'coordinates' : polys
175
+ }
176
+
91
177
class _ShapeRecord :
92
178
"""A shape object of any type."""
93
179
def __init__ (self , shape = None , record = None ):
@@ -128,7 +214,7 @@ def __init__(self, *args, **kwargs):
128
214
self .__dbfHdrLength = 0
129
215
# See if a shapefile name was passed as an argument
130
216
if len (args ) > 0 :
131
- if type (args [0 ]) is type ( "stringTest" ):
217
+ if is_string (args [0 ]):
132
218
self .load (args [0 ])
133
219
return
134
220
if "shp" in kwargs .keys ():
@@ -221,6 +307,8 @@ def __shape(self):
221
307
record = _Shape ()
222
308
nParts = nPoints = zmin = zmax = mmin = mmax = None
223
309
(recNum , recLength ) = unpack (">2i" , f .read (8 ))
310
+ # Determine the start of the next record
311
+ next = f .tell () + (2 * recLength )
224
312
shapeType = unpack ("<i" , f .read (4 ))[0 ]
225
313
record .shapeType = shapeType
226
314
# For Null shapes create an empty points list for consistency
@@ -248,12 +336,12 @@ def __shape(self):
248
336
if shapeType in (13 ,15 ,18 ,31 ):
249
337
(zmin , zmax ) = unpack ("<2d" , f .read (16 ))
250
338
record .z = _Array ('d' , unpack ("<%sd" % nPoints , f .read (nPoints * 8 )))
251
- # Read m extremes and values
252
- if shapeType in (13 ,15 ,18 ,23 ,25 ,28 ,31 ):
339
+ # Read m extremes and values if header m values do not equal 0.0
340
+ if shapeType in (13 ,15 ,18 ,23 ,25 ,28 ,31 ) and not 0.0 in self . measure :
253
341
(mmin , mmax ) = unpack ("<2d" , f .read (16 ))
254
342
# Measure values less than -10e38 are nodata values according to the spec
255
343
record .m = []
256
- for m in _Array ('d' , unpack ("%sd" % nPoints , f .read (nPoints * 8 ))):
344
+ for m in _Array ('d' , unpack ("< %sd" % nPoints , f .read (nPoints * 8 ))):
257
345
if m > - 10e38 :
258
346
record .m .append (m )
259
347
else :
@@ -267,6 +355,10 @@ def __shape(self):
267
355
# Read a single M value
268
356
if shapeType in (11 ,21 ):
269
357
record .m = unpack ("<d" , f .read (8 ))
358
+ # Seek to the end of this record as defined by the record header because
359
+ # the shapefile spec doesn't require the actual content to meet the header
360
+ # definition. Probably allowed for lazy feature deletion.
361
+ f .seek (next )
270
362
return record
271
363
272
364
def __shapeIndex (self , i = None ):
@@ -296,9 +388,10 @@ def shape(self, i=0):
296
388
i = self .__restrictIndex (i )
297
389
offset = self .__shapeIndex (i )
298
390
if not offset :
299
- # Shx index not available so use the full list.
300
- shapes = self .shapes ()
301
- return shapes [i ]
391
+ # Shx index not available so iterate the full list.
392
+ for j ,k in enumerate (self .iterShapes ()):
393
+ if j == i :
394
+ return k
302
395
shp .seek (offset )
303
396
return self .__shape ()
304
397
@@ -311,6 +404,14 @@ def shapes(self):
311
404
shapes .append (self .__shape ())
312
405
return shapes
313
406
407
+ def iterShapes (self ):
408
+ """Serves up shapes in a shapefile as an iterator. Useful
409
+ for handling large shapefiles."""
410
+ shp = self .__getFileObj (self .shp )
411
+ shp .seek (100 )
412
+ while shp .tell () < self .shpLength :
413
+ yield self .__shape ()
414
+
314
415
def __dbfHeaderLength (self ):
315
416
"""Retrieves the header length of a dbf file header."""
316
417
if not self .__dbfHdrLength :
@@ -416,12 +517,23 @@ def records(self):
416
517
records .append (r )
417
518
return records
418
519
520
+ def iterRecords (self ):
521
+ """Serves up records in a dbf file as an iterator.
522
+ Useful for large shapefiles or dbf files."""
523
+ if not self .numRecords :
524
+ self .__dbfHeader ()
525
+ f = self .__getFileObj (self .dbf )
526
+ f .seek (self .__dbfHeaderLength ())
527
+ for i in xrange (self .numRecords ):
528
+ r = self .__record ()
529
+ if r :
530
+ yield r
531
+
419
532
def shapeRecord (self , i = 0 ):
420
533
"""Returns a combination geometry and attribute record for the
421
534
supplied record index."""
422
535
i = self .__restrictIndex (i )
423
- return _ShapeRecord (shape = self .shape (i ),
424
- record = self .record (i ))
536
+ return _ShapeRecord (shape = self .shape (i ), record = self .record (i ))
425
537
426
538
def shapeRecords (self ):
427
539
"""Returns a list of combination geometry/attribute records for
@@ -675,7 +787,8 @@ def __shpRecords(self):
675
787
except error :
676
788
raise ShapefileException ("Failed to write elevation extremes for record %s. Expected floats." % recNum )
677
789
try :
678
- [f .write (pack ("<d" , p [2 ])) for p in s .points ]
790
+ #[f.write(pack("<d", p[2])) for p in s.points]
791
+ f .write (pack ("<%sd" % len (s .z ), * s .z ))
679
792
except error :
680
793
raise ShapefileException ("Failed to write elevation values for record %s. Expected floats." % recNum )
681
794
# Write m extremes and values
@@ -768,7 +881,9 @@ def poly(self, parts=[], shapeType=POLYGON, partTypes=[]):
768
881
polyShape .parts = []
769
882
polyShape .points = []
770
883
for part in parts :
771
- polyShape .parts .append (len (polyShape .points ))
884
+ # Make sure polygon is closed
885
+ if shapeType in (5 ,15 ,25 ,31 ) and part [0 ] != part [- 1 ]:
886
+ part .append (part [0 ])
772
887
for point in part :
773
888
# Ensure point is list
774
889
if not isinstance (point , list ):
@@ -777,6 +892,7 @@ def poly(self, parts=[], shapeType=POLYGON, partTypes=[]):
777
892
while len (point ) < 4 :
778
893
point .append (0 )
779
894
polyShape .points .append (point )
895
+ polyShape .parts .append (len (polyShape .points ))
780
896
if polyShape .shapeType == 31 :
781
897
if not partTypes :
782
898
for part in parts :
@@ -806,10 +922,10 @@ def record(self, *recordList, **recordDict):
806
922
for field in self .fields :
807
923
if field [0 ] in recordDict :
808
924
val = recordDict [field [0 ]]
809
- if val :
810
- record .append (val )
811
- else :
925
+ if val is None :
812
926
record .append ("" )
927
+ else :
928
+ record .append (val )
813
929
if record :
814
930
self .records .append (record )
815
931
@@ -851,21 +967,33 @@ def saveDbf(self, target):
851
967
def save (self , target = None , shp = None , shx = None , dbf = None ):
852
968
"""Save the shapefile data to three files or
853
969
three file-like objects. SHP and DBF files can also
854
- be written exclusively using saveShp, saveShx, and saveDbf respectively."""
855
- # TODO: Create a unique filename for target if None.
970
+ be written exclusively using saveShp, saveShx, and saveDbf respectively.
971
+ If target is specified but not shp,shx, or dbf then the target path and
972
+ file name are used. If no options or specified, a unique base file name
973
+ is generated to save the files and the base file name is returned as a
974
+ string.
975
+ """
976
+ # Create a unique file name if one is not defined
856
977
if shp :
857
978
self .saveShp (shp )
858
979
if shx :
859
980
self .saveShx (shx )
860
981
if dbf :
861
982
self .saveDbf (dbf )
862
- elif target :
983
+ elif not shp and not shx and not dbf :
984
+ generated = False
985
+ if not target :
986
+ temp = tempfile .NamedTemporaryFile (prefix = "shapefile_" ,dir = os .getcwd ())
987
+ target = temp .name
988
+ generated = True
863
989
self .saveShp (target )
864
990
self .shp .close ()
865
991
self .saveShx (target )
866
992
self .shx .close ()
867
993
self .saveDbf (target )
868
994
self .dbf .close ()
995
+ if generated :
996
+ return target
869
997
870
998
class Editor (Writer ):
871
999
def __init__ (self , shapefile = None , shapeType = POINT , autoBalance = 1 ):
@@ -992,7 +1120,7 @@ def test():
992
1120
993
1121
if __name__ == "__main__" :
994
1122
"""
995
- Doctests are contained in the module 'pyshp_usage.py '. This library was developed
1123
+ Doctests are contained in the file 'README.txt '. This library was originally developed
996
1124
using Python 2.3. Python 2.4 and above have some excellent improvements in the built-in
997
1125
testing libraries but for now unit testing is done using what's available in
998
1126
2.3.
0 commit comments