Skip to content

Commit

Permalink
Add oid and repr to Shape objects too
Browse files Browse the repository at this point in the history
  • Loading branch information
karimbahgat committed Aug 31, 2021
1 parent c695e3d commit 103bad0
Show file tree
Hide file tree
Showing 16 changed files with 56 additions and 9 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ index which is 7.


>>> s = sf.shape(7)
>>> s
Shape #7: POLYGON

>>> # Read the bbox of the 8th shape to verify
>>> # Round coordinates to 3 decimal places
Expand All @@ -329,11 +331,18 @@ shapeType Point do not have a bounding box 'bbox'.
... if not name.startswith('_'):
... name
'bbox'
'oid'
'parts'
'points'
'shapeType'
'shapeTypeName'

* oid: The shape's index position in the original shapefile.


>>> shapes[3].oid
3

* shapeType: an integer representing the type of shape as defined by the
shapefile specification.

Expand Down
36 changes: 27 additions & 9 deletions shapefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ def organize_polygon_rings(rings, return_errors=None):
return polys

class Shape(object):
def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None):
def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None, oid=None):
"""Stores the geometry of the different shape types
specified in the Shapefile spec. Shape types are
usually point, polyline, or polygons. Every shape type
Expand All @@ -418,8 +418,15 @@ def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None):
self.parts = parts or []
if partTypes:
self.partTypes = partTypes

# and a dict to silently record any errors encountered
self._errors = {}

# add oid
if oid is not None:
self.__oid = oid
else:
self.__oid = -1

@property
def __geo_interface__(self):
Expand Down Expand Up @@ -504,17 +511,17 @@ def __geo_interface__(self):
# if VERBOSE is True, issue detailed warning about any shape errors
# encountered during the Shapefile to GeoJSON conversion
if VERBOSE and self._errors:
header = 'Possible issue encountered when converting Shape to GeoJSON: '
header = 'Possible issue encountered when converting Shape #{} to GeoJSON: '.format(self.oid)
orphans = self._errors.get('polygon_orphaned_holes', None)
if orphans:
msg = header + 'GeoJSON format requires that all polygon interior holes be contained by an exterior ring, \
msg = header + 'Shapefile format requires that all polygon interior holes be contained by an exterior ring, \
but the Shape contained interior holes (defined by counter-clockwise orientation in the shapefile format) that were \
orphaned, i.e. not contained by any exterior rings. The rings were still included but were \
encoded as GeoJSON exterior rings instead of holes.'
logging.warning(msg)
only_holes = self._errors.get('polygon_only_holes', None)
if only_holes:
msg = header + 'GeoJSON format requires that polygons contain at least one exterior ring, \
msg = header + 'Shapefile format requires that polygons contain at least one exterior ring, \
but the Shape was entirely made up of interior holes (defined by counter-clockwise orientation in the shapefile format). The rings were \
still included but were encoded as GeoJSON exterior rings instead of holes.'
logging.warning(msg)
Expand Down Expand Up @@ -610,10 +617,18 @@ def _from_geojson(geoj):
shape.parts = parts
return shape

@property
def oid(self):
"""The index position of the shape in the original shapefile"""
return self.__oid

@property
def shapeTypeName(self):
return SHAPETYPE_LOOKUP[self.shapeType]

def __repr__(self):
return 'Shape #{}: {}'.format(self.__oid, self.shapeTypeName)

class _Record(list):
"""
A class to hold a record. Subclasses list to ensure compatibility with
Expand Down Expand Up @@ -1097,10 +1112,10 @@ def __shpHeader(self):
else:
self.mbox.append(None)

def __shape(self):
def __shape(self, oid=None):
"""Returns the header info and geometry for a single shape."""
f = self.__getFileObj(self.shp)
record = Shape()
record = Shape(oid=oid)
nParts = nPoints = zmin = zmax = mmin = mmax = None
(recNum, recLength) = unpack(">2i", f.read(8))
# Determine the start of the next record
Expand Down Expand Up @@ -1205,7 +1220,7 @@ def shape(self, i=0):
if j == i:
return k
shp.seek(offset)
return self.__shape()
return self.__shape(oid=i)

def shapes(self):
"""Returns all shapes in a shapefile."""
Expand All @@ -1219,7 +1234,8 @@ def shapes(self):
shp.seek(100)
shapes = Shapes()
while shp.tell() < self.shpLength:
shapes.append(self.__shape())
i = len(shapes)
shapes.append(self.__shape(oid=i))
return shapes

def iterShapes(self):
Expand All @@ -1229,8 +1245,10 @@ def iterShapes(self):
shp.seek(0,2)
self.shpLength = shp.tell()
shp.seek(100)
i = 0
while shp.tell() < self.shpLength:
yield self.__shape()
yield self.__shape(oid=i)
i += 1

def __dbfHeader(self):
"""Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger"""
Expand Down
Binary file modified shapefiles/test/balancing.dbf
Binary file not shown.
Binary file modified shapefiles/test/contextwriter.dbf
Binary file not shown.
Binary file modified shapefiles/test/dtype.dbf
Binary file not shown.
Binary file modified shapefiles/test/line.dbf
Binary file not shown.
Binary file modified shapefiles/test/linem.dbf
Binary file not shown.
Binary file modified shapefiles/test/linez.dbf
Binary file not shown.
Binary file modified shapefiles/test/multipatch.dbf
Binary file not shown.
Binary file modified shapefiles/test/multipoint.dbf
Binary file not shown.
Binary file modified shapefiles/test/onlydbf.dbf
Binary file not shown.
Binary file modified shapefiles/test/point.dbf
Binary file not shown.
Binary file modified shapefiles/test/polygon.dbf
Binary file not shown.
Binary file modified shapefiles/test/shapetype.dbf
Binary file not shown.
Binary file modified shapefiles/test/testfile.dbf
Binary file not shown.
20 changes: 20 additions & 0 deletions test_shapefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,26 @@ def test_record_oid():
assert shaperec.record.oid == i


def test_shape_oid():
"""
Assert that the shape's oid attribute returns
its index in the shapefile.
"""
with shapefile.Reader("shapefiles/blockgroups") as sf:
for i in range(len(sf)):
shape = sf.shape(i)
assert shape.oid == i

for i,shape in enumerate(sf.shapes()):
assert shape.oid == i

for i,shape in enumerate(sf.iterShapes()):
assert shape.oid == i

for i,shaperec in enumerate(sf.iterShapeRecords()):
assert shaperec.shape.oid == i


def test_shaperecords_shaperecord():
"""
Assert that shapeRecords returns a list of
Expand Down

0 comments on commit 103bad0

Please sign in to comment.