Skip to content

Commit b7302b6

Browse files
committed
Polymorphic! (Errors)
1 parent 9201e48 commit b7302b6

File tree

1 file changed

+134
-100
lines changed

1 file changed

+134
-100
lines changed

src/shapefile.py

Lines changed: 134 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -844,111 +844,87 @@ def shapeTypeName(self) -> str:
844844
def __repr__(self):
845845
return f"Shape #{self.__oid}: {self.shapeTypeName}"
846846

847+
# pylint: disable=unused-argument
848+
def _get_and_set_bbox_from_shp_file(self, f):
849+
return None
850+
851+
@staticmethod
852+
def _get_nparts_from_shp_file(f):
853+
return None
854+
855+
@staticmethod
856+
def _get_npoints_from_shp_file(f):
857+
return None
858+
859+
def _set_parts_from_shp_file(self, f, nParts):
860+
pass
861+
862+
def _set_part_types_from_shp_file(self, f, nParts):
863+
pass
864+
865+
def _set_points_from_shp_file(self, f, nPoints):
866+
pass
867+
868+
def _set_z_from_shp_file(self, f, nPoints):
869+
pass
870+
871+
def _set_m_from_shp_file(self, f, nPoints, next_shape):
872+
pass
873+
874+
def _get_and_set_2D_point_from_shp_file(self, f):
875+
return None
876+
877+
def _set_single_point_z_from_shp_file(self, f):
878+
pass
879+
880+
def _set_single_point_m_from_shp_file(self, f, next_shape):
881+
pass
882+
883+
# pylint: enable=unused-argument
884+
847885
@classmethod
848886
def _from_shp_file(cls, f, next_shape, oid=None, bbox=None):
849-
# Previously, we also set __zmin = __zmax = __mmin = __mmax = None
850-
nParts: Optional[int] = None
851-
nPoints: Optional[int] = None
852-
853887
shape = cls(oid=oid)
854888

855-
# All shape types capable of having a bounding box
856-
# elif shapeType in (3, 13, 23, 5, 15, 25, 8, 18, 28, 31):
857-
if isinstance(shape, _CanHaveBBox):
858-
# record.bbox = tuple(_Array[float]("d", unpack("<4d", f.read(32))))
859-
shape.bbox = _Array[float]("d", unpack("<4d", f.read(32)))
860-
# if bbox specified and no overlap, skip this shape
861-
if bbox is not None and not bbox_overlap(bbox, tuple(shape.bbox)):
862-
# because we stop parsing this shape, skip to beginning of
863-
# next shape before we return
864-
return None
865-
# Shape types with parts
866-
# if shapeType in (3, 13, 23, 5, 15, 25, 31):
867-
if issubclass(cls, (Polyline, Polygon, MultiPatch)):
868-
nParts = unpack("<i", f.read(4))[0]
889+
bbox = shape._get_and_set_bbox_from_shp_file(f) # pylint: disable=assignment-from-none
869890

870-
# Shape types with points
871-
# if shapeType in (3, 13, 23, 5, 15, 25, 8, 18, 28, 31):
872-
if isinstance(shape, _CanHaveBBox):
873-
nPoints = unpack("<i", f.read(4))[0]
874-
# Read points - produces a list of [x,y] values
891+
# if bbox specified and no overlap, skip this shape
892+
if bbox is not None and not bbox_overlap(bbox, tuple(shape.bbox)): # pylint: disable=no-member
893+
# because we stop parsing this shape, skip to beginning of
894+
# next shape before we return
895+
return None
875896

876-
if nParts:
877-
shape.parts = _Array[int]("i", unpack(f"<{nParts}i", f.read(nParts * 4)))
897+
nParts: Optional[int] = shape._get_nparts_from_shp_file(f)
898+
nPoints: Optional[int] = shape._get_npoints_from_shp_file(f)
899+
# Previously, we also set __zmin = __zmax = __mmin = __mmax = None
878900

879-
# Read part types for Multipatch - 31
880-
# if shapeType == 31:
881-
if cls is MultiPatch:
882-
shape.partTypes = _Array[int](
883-
"i", unpack(f"<{nParts}i", f.read(nParts * 4))
884-
)
901+
if nParts:
902+
shape._set_parts_from_shp_file(f, nParts)
903+
shape._set_part_types_from_shp_file(f, nParts)
885904

886905
if nPoints:
887-
flat = unpack(f"<{2 * nPoints}d", f.read(16 * nPoints))
888-
shape.points = list(zip(*(iter(flat),) * 2))
889-
890-
# Read z extremes and values
891-
# if shapeType in (13, 15, 18, 31):
892-
if isinstance(shape, _HasZ):
893-
__zmin, __zmax = unpack("<2d", f.read(16))
894-
shape.z = _Array[float](
895-
"d", unpack(f"<{nPoints}d", f.read(nPoints * 8))
896-
)
906+
shape._set_points_from_shp_file(f, nPoints)
897907

898-
# Read m extremes and values
899-
# if shapeType in (13, 23, 15, 25, 18, 28, 31):
900-
if isinstance(shape, _HasM):
901-
if next_shape - f.tell() >= 16:
902-
__mmin, __mmax = unpack("<2d", f.read(16))
903-
# Measure values less than -10e38 are nodata values according to the spec
904-
if next_shape - f.tell() >= nPoints * 8:
905-
shape.m = []
906-
for m in _Array[float](
907-
"d", unpack(f"<{nPoints}d", f.read(nPoints * 8))
908-
):
909-
if m > NODATA:
910-
shape.m.append(m)
911-
else:
912-
shape.m.append(None)
913-
else:
914-
shape.m = [None for _ in range(nPoints)]
908+
shape._set_z_from_shp_file(f, nPoints)
909+
910+
shape._set_m_from_shp_file(f, nPoints, next_shape)
915911

916912
# Read a single point
917913
# if shapeType in (1, 11, 21):
918-
if cls is Point:
919-
x, y = _Array[float]("d", unpack("<2d", f.read(16)))
920-
921-
shape.points = [(x, y)]
922-
if bbox is not None:
923-
# create bounding box for Point by duplicating coordinates
924-
# skip shape if no overlap with bounding box
925-
if not bbox_overlap(bbox, (x, y, x, y)):
926-
return None
927-
928-
# Read a single Z value
929-
# if shapeType == 11:
930-
if cls is PointZ:
931-
shape.z = tuple(unpack("<d", f.read(8)))
932-
933-
# Read a single M value
934-
# if shapeType in (21, 11):
935-
if cls is PointM:
936-
if next_shape - f.tell() >= 8:
937-
(m,) = unpack("<d", f.read(8))
938-
else:
939-
m = NODATA
940-
# Measure values less than -10e38 are nodata values according to the spec
941-
if m > NODATA:
942-
shape.m = (m,)
943-
else:
944-
shape.m = (None,)
914+
point_2D = shape._get_and_set_2D_point_from_shp_file(f) # pylint: disable=assignment-from-none
945915

946-
return shape
916+
if bbox is not None and point_2D is not None:
917+
x, y = point_2D # pylint: disable=unpacking-non-sequence
918+
# create bounding box for Point by duplicating coordinates
919+
# skip shape if no overlap with bounding box
920+
if not bbox_overlap(bbox, (x, y, x, y)):
921+
return None
947922

948-
# pylint: enable=attribute-defined-outside-init
949-
# Seek to the end of this record as defined by the record header because
950-
# the shapefile spec doesn't require the actual content to meet the header
951-
# definition. Probably allowed for lazy feature deletion.
923+
shape._set_single_point_z_from_shp_file(f)
924+
925+
shape._set_single_point_m_from_shp_file(f, next_shape)
926+
927+
return shape
952928

953929

954930
def _read_shape_from_shp_file(
@@ -967,6 +943,9 @@ def _read_shape_from_shp_file(
967943
ShapeClass = SHAPE_CLASS_FROM_SHAPETYPE[shapeType]
968944
shape = ShapeClass._from_shp_file(f, next_shape, oid=oid, bbox=bbox)
969945

946+
# Seek to the end of this record as defined by the record header because
947+
# the shapefile spec doesn't require the actual content to meet the header
948+
# definition. Probably allowed for lazy feature deletion.
970949
f.seek(next_shape)
971950

972951
return shape
@@ -983,16 +962,43 @@ class _CanHaveBBox(Shape):
983962
# Not a BBox because the legacy implementation was a list, not a 4-tuple.
984963
bbox: Optional[Sequence[float]] = None
985964

965+
def _get_and_set_bbox_from_shp_file(self, f):
966+
# record.bbox = tuple(_Array[float]("d", unpack("<4d", f.read(32))))
967+
self.bbox = _Array[float]("d", unpack("<4d", f.read(32)))
968+
return self.bbox
969+
970+
@staticmethod
971+
def _get_npoints_from_shp_file(f):
972+
return unpack("<i", f.read(4))[0]
973+
974+
def _set_points_from_shp_file(self, f, nPoints):
975+
flat = unpack(f"<{2 * nPoints}d", f.read(16 * nPoints))
976+
self.points = list(zip(*(iter(flat),) * 2))
977+
978+
979+
class _CanHaveParts(_CanHaveBBox):
980+
@staticmethod
981+
def _get_nparts_from_shp_file(f):
982+
return unpack("<i", f.read(4))[0]
983+
984+
def _set_parts_from_shp_file(self, f, nParts):
985+
self.parts = _Array[int]("i", unpack(f"<{nParts}i", f.read(nParts * 4)))
986+
986987

987988
class Point(Shape):
988989
shapeType = POINT
989990

991+
def _get_and_set_2D_point_from_shp_file(self, f):
992+
x, y = _Array[float]("d", unpack("<2d", f.read(16)))
993+
994+
self.points = [(x, y)]
995+
990996

991-
class Polyline(_CanHaveBBox):
997+
class Polyline(_CanHaveParts):
992998
shapeType = POLYLINE
993999

9941000

995-
class Polygon(_CanHaveBBox):
1001+
class Polygon(_CanHaveParts):
9961002
shapeType = POLYGON
9971003

9981004

@@ -1003,21 +1009,53 @@ class MultiPoint(_CanHaveBBox):
10031009
class _HasM(Shape):
10041010
m: Sequence[Optional[float]]
10051011

1012+
def _set_m_from_shp_file(self, f, nPoints, next_shape):
1013+
if next_shape - f.tell() >= 16:
1014+
__mmin, __mmax = unpack("<2d", f.read(16))
1015+
# Measure values less than -10e38 are nodata values according to the spec
1016+
if next_shape - f.tell() >= nPoints * 8:
1017+
self.m = []
1018+
for m in _Array[float]("d", unpack(f"<{nPoints}d", f.read(nPoints * 8))):
1019+
if m > NODATA:
1020+
self.m.append(m)
1021+
else:
1022+
self.m.append(None)
1023+
else:
1024+
self.m = [None for _ in range(nPoints)]
1025+
10061026

10071027
class _HasZ(Shape):
10081028
z: Sequence[float]
10091029

1030+
def _set_z_from_shp_file(self, f, nPoints):
1031+
__zmin, __zmax = unpack("<2d", f.read(16)) # pylint: disable=unused-private-member
1032+
self.z = _Array[float]("d", unpack(f"<{nPoints}d", f.read(nPoints * 8)))
1033+
10101034

1011-
class MultiPatch(_HasM, _HasZ, _CanHaveBBox):
1035+
class MultiPatch(_HasM, _HasZ, _CanHaveParts):
10121036
shapeType = MULTIPATCH
10131037

1038+
def _set_part_types_from_shp_file(self, f, nParts):
1039+
self.partTypes = _Array[int]("i", unpack(f"<{nParts}i", f.read(nParts * 4)))
1040+
10141041

10151042
class PointM(Point, _HasM):
10161043
shapeType = POINTM
10171044
# same default as in Writer.__shpRecord (if s.shapeType in (11, 21):)
10181045
# PyShp encodes None m values as NODATA
10191046
m = (None,)
10201047

1048+
def _set_single_point_m_from_shp_file(self, f, next_shape):
1049+
if next_shape - f.tell() >= 8:
1050+
(m,) = unpack("<d", f.read(8))
1051+
else:
1052+
m = NODATA
1053+
# Measure values less than -10e38 are nodata values according to the spec
1054+
if m > NODATA:
1055+
self.m = (m,)
1056+
else:
1057+
self.m = (None,)
1058+
10211059

10221060
class PolylineM(Polyline, _HasM):
10231061
shapeType = POLYLINEM
@@ -1036,6 +1074,9 @@ class PointZ(PointM, _HasZ):
10361074
# same default as in Writer.__shpRecord (if s.shapeType == 11:)
10371075
z: Sequence[float] = (0.0,)
10381076

1077+
def _set_single_point_z_from_shp_file(self, f):
1078+
self.z = tuple(unpack("<d", f.read(8)))
1079+
10391080

10401081
class PolylineZ(PolylineM, _HasZ):
10411082
shapeType = POLYLINEZ
@@ -1735,7 +1776,6 @@ def __shpHeader(self):
17351776
"Shapefile Reader requires a shapefile or file-like object. (no shp file found"
17361777
)
17371778

1738-
# pylint: disable=attribute-defined-outside-init
17391779
shp = self.shp
17401780
# File length (16-bit word * 2 = bytes)
17411781
shp.seek(24)
@@ -1756,14 +1796,11 @@ def __shpHeader(self):
17561796
else:
17571797
self.mbox.append(None)
17581798

1759-
# pylint: enable=attribute-defined-outside-init
1760-
17611799
def __shape(
17621800
self, oid: Optional[int] = None, bbox: Optional[BBox] = None
17631801
) -> Optional[Shape]:
17641802
"""Returns the header info and geometry for a single shape."""
17651803

1766-
# pylint: disable=attribute-defined-outside-init
17671804
f = self.__getFileObj(self.shp)
17681805

17691806
shape = _read_shape_from_shp_file(f, oid, bbox)
@@ -1901,7 +1938,6 @@ def iterShapes(self, bbox: Optional[BBox] = None) -> Iterator[Optional[Shape]]:
19011938
def __dbfHeader(self):
19021939
"""Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger"""
19031940

1904-
# pylint: disable=attribute-defined-outside-init
19051941
if not self.dbf:
19061942
raise ShapefileException(
19071943
"Shapefile Reader requires a shapefile or file-like object. (no dbf file found)"
@@ -1948,8 +1984,6 @@ def __dbfHeader(self):
19481984
self.__fullRecStruct = recStruct
19491985
self.__fullRecLookup = recLookup
19501986

1951-
# pylint: enable=attribute-defined-outside-init
1952-
19531987
def __recordFmt(self, fields: Optional[Container[str]] = None) -> tuple[str, int]:
19541988
"""Calculates the format and size of a .dbf record. Optional 'fields' arg
19551989
specifies which fieldnames to unpack and which to ignore. Note that this

0 commit comments

Comments
 (0)