Skip to content

Commit 103bad0

Browse files
committed
Add oid and repr to Shape objects too
1 parent c695e3d commit 103bad0

16 files changed

+56
-9
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ index which is 7.
315315

316316

317317
>>> s = sf.shape(7)
318+
>>> s
319+
Shape #7: POLYGON
318320

319321
>>> # Read the bbox of the 8th shape to verify
320322
>>> # Round coordinates to 3 decimal places
@@ -329,11 +331,18 @@ shapeType Point do not have a bounding box 'bbox'.
329331
... if not name.startswith('_'):
330332
... name
331333
'bbox'
334+
'oid'
332335
'parts'
333336
'points'
334337
'shapeType'
335338
'shapeTypeName'
336339

340+
* oid: The shape's index position in the original shapefile.
341+
342+
343+
>>> shapes[3].oid
344+
3
345+
337346
* shapeType: an integer representing the type of shape as defined by the
338347
shapefile specification.
339348

shapefile.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ def organize_polygon_rings(rings, return_errors=None):
401401
return polys
402402

403403
class Shape(object):
404-
def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None):
404+
def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None, oid=None):
405405
"""Stores the geometry of the different shape types
406406
specified in the Shapefile spec. Shape types are
407407
usually point, polyline, or polygons. Every shape type
@@ -418,8 +418,15 @@ def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None):
418418
self.parts = parts or []
419419
if partTypes:
420420
self.partTypes = partTypes
421+
421422
# and a dict to silently record any errors encountered
422423
self._errors = {}
424+
425+
# add oid
426+
if oid is not None:
427+
self.__oid = oid
428+
else:
429+
self.__oid = -1
423430

424431
@property
425432
def __geo_interface__(self):
@@ -504,17 +511,17 @@ def __geo_interface__(self):
504511
# if VERBOSE is True, issue detailed warning about any shape errors
505512
# encountered during the Shapefile to GeoJSON conversion
506513
if VERBOSE and self._errors:
507-
header = 'Possible issue encountered when converting Shape to GeoJSON: '
514+
header = 'Possible issue encountered when converting Shape #{} to GeoJSON: '.format(self.oid)
508515
orphans = self._errors.get('polygon_orphaned_holes', None)
509516
if orphans:
510-
msg = header + 'GeoJSON format requires that all polygon interior holes be contained by an exterior ring, \
517+
msg = header + 'Shapefile format requires that all polygon interior holes be contained by an exterior ring, \
511518
but the Shape contained interior holes (defined by counter-clockwise orientation in the shapefile format) that were \
512519
orphaned, i.e. not contained by any exterior rings. The rings were still included but were \
513520
encoded as GeoJSON exterior rings instead of holes.'
514521
logging.warning(msg)
515522
only_holes = self._errors.get('polygon_only_holes', None)
516523
if only_holes:
517-
msg = header + 'GeoJSON format requires that polygons contain at least one exterior ring, \
524+
msg = header + 'Shapefile format requires that polygons contain at least one exterior ring, \
518525
but the Shape was entirely made up of interior holes (defined by counter-clockwise orientation in the shapefile format). The rings were \
519526
still included but were encoded as GeoJSON exterior rings instead of holes.'
520527
logging.warning(msg)
@@ -610,10 +617,18 @@ def _from_geojson(geoj):
610617
shape.parts = parts
611618
return shape
612619

620+
@property
621+
def oid(self):
622+
"""The index position of the shape in the original shapefile"""
623+
return self.__oid
624+
613625
@property
614626
def shapeTypeName(self):
615627
return SHAPETYPE_LOOKUP[self.shapeType]
616628

629+
def __repr__(self):
630+
return 'Shape #{}: {}'.format(self.__oid, self.shapeTypeName)
631+
617632
class _Record(list):
618633
"""
619634
A class to hold a record. Subclasses list to ensure compatibility with
@@ -1097,10 +1112,10 @@ def __shpHeader(self):
10971112
else:
10981113
self.mbox.append(None)
10991114

1100-
def __shape(self):
1115+
def __shape(self, oid=None):
11011116
"""Returns the header info and geometry for a single shape."""
11021117
f = self.__getFileObj(self.shp)
1103-
record = Shape()
1118+
record = Shape(oid=oid)
11041119
nParts = nPoints = zmin = zmax = mmin = mmax = None
11051120
(recNum, recLength) = unpack(">2i", f.read(8))
11061121
# Determine the start of the next record
@@ -1205,7 +1220,7 @@ def shape(self, i=0):
12051220
if j == i:
12061221
return k
12071222
shp.seek(offset)
1208-
return self.__shape()
1223+
return self.__shape(oid=i)
12091224

12101225
def shapes(self):
12111226
"""Returns all shapes in a shapefile."""
@@ -1219,7 +1234,8 @@ def shapes(self):
12191234
shp.seek(100)
12201235
shapes = Shapes()
12211236
while shp.tell() < self.shpLength:
1222-
shapes.append(self.__shape())
1237+
i = len(shapes)
1238+
shapes.append(self.__shape(oid=i))
12231239
return shapes
12241240

12251241
def iterShapes(self):
@@ -1229,8 +1245,10 @@ def iterShapes(self):
12291245
shp.seek(0,2)
12301246
self.shpLength = shp.tell()
12311247
shp.seek(100)
1248+
i = 0
12321249
while shp.tell() < self.shpLength:
1233-
yield self.__shape()
1250+
yield self.__shape(oid=i)
1251+
i += 1
12341252

12351253
def __dbfHeader(self):
12361254
"""Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger"""

shapefiles/test/balancing.dbf

0 Bytes
Binary file not shown.

shapefiles/test/contextwriter.dbf

0 Bytes
Binary file not shown.

shapefiles/test/dtype.dbf

0 Bytes
Binary file not shown.

shapefiles/test/line.dbf

0 Bytes
Binary file not shown.

shapefiles/test/linem.dbf

0 Bytes
Binary file not shown.

shapefiles/test/linez.dbf

0 Bytes
Binary file not shown.

shapefiles/test/multipatch.dbf

0 Bytes
Binary file not shown.

shapefiles/test/multipoint.dbf

0 Bytes
Binary file not shown.

shapefiles/test/onlydbf.dbf

0 Bytes
Binary file not shown.

shapefiles/test/point.dbf

0 Bytes
Binary file not shown.

shapefiles/test/polygon.dbf

0 Bytes
Binary file not shown.

shapefiles/test/shapetype.dbf

0 Bytes
Binary file not shown.

shapefiles/test/testfile.dbf

0 Bytes
Binary file not shown.

test_shapefile.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,26 @@ def test_record_oid():
444444
assert shaperec.record.oid == i
445445

446446

447+
def test_shape_oid():
448+
"""
449+
Assert that the shape's oid attribute returns
450+
its index in the shapefile.
451+
"""
452+
with shapefile.Reader("shapefiles/blockgroups") as sf:
453+
for i in range(len(sf)):
454+
shape = sf.shape(i)
455+
assert shape.oid == i
456+
457+
for i,shape in enumerate(sf.shapes()):
458+
assert shape.oid == i
459+
460+
for i,shape in enumerate(sf.iterShapes()):
461+
assert shape.oid == i
462+
463+
for i,shaperec in enumerate(sf.iterShapeRecords()):
464+
assert shaperec.shape.oid == i
465+
466+
447467
def test_shaperecords_shaperecord():
448468
"""
449469
Assert that shapeRecords returns a list of

0 commit comments

Comments
 (0)