2828 Generic ,
2929 Iterable ,
3030 Iterator ,
31+ Literal ,
3132 NoReturn ,
3233 Optional ,
3334 Protocol ,
3435 Reversible ,
3536 Sequence ,
37+ TypedDict ,
3638 TypeVar ,
3739 Union ,
3840 overload ,
111113PointM = tuple [float , float , Optional [float ]]
112114PointZ = tuple [float , float , float , Optional [float ]]
113115
114- Coord = Union [Point2D , Point2D , Point3D ]
116+ Coord = Union [Point2D , Point3D ]
115117Coords = list [Coord ]
116118
117119Point = Union [Point2D , PointM , PointZ ]
@@ -144,6 +146,86 @@ class HasGeoInterface(Protocol):
144146 def __geo_interface__ (self ) -> Any : ...
145147
146148
149+ class GeoJSONPoint (TypedDict ):
150+ type : Literal ["Point" ]
151+ # We fix to a tuple (to statically check the length is 2, 3 or 4) but
152+ # RFC7946 only requires: "A position is an array of numbers. There MUST be two or more
153+ # elements. "
154+ # RFC7946 also requires long/lat easting/northing which we do not enforce,
155+ # and despite the SHOULD NOT, we may use a 4th element for Shapefile M Measures.
156+ coordinates : Union [Point , tuple [()]]
157+
158+
159+ class GeoJSONMultiPoint (TypedDict ):
160+ type : Literal ["MultiPoint" ]
161+ coordinates : Points
162+
163+
164+ class GeoJSONLineString (TypedDict ):
165+ type : Literal ["LineString" ]
166+ # "Two or more positions" not enforced by type checker
167+ # https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.4
168+ coordinates : Points
169+
170+
171+ class GeoJSONMultiLineString (TypedDict ):
172+ type : Literal ["MultiLineString" ]
173+ coordinates : list [Points ]
174+
175+
176+ class GeoJSONPolygon (TypedDict ):
177+ type : Literal ["Polygon" ]
178+ # Other requirements for Polygon not enforced by type checker
179+ # https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6
180+ coordinates : list [Points ]
181+
182+
183+ class GeoJSONMultiPolygon (TypedDict ):
184+ type : Literal ["MultiPolygon" ]
185+ coordinates : list [list [Points ]]
186+
187+
188+ GeoJSONHomogeneousGeometryObject = Union [
189+ GeoJSONPoint ,
190+ GeoJSONMultiPoint ,
191+ GeoJSONLineString ,
192+ GeoJSONMultiLineString ,
193+ GeoJSONPolygon ,
194+ GeoJSONMultiPolygon ,
195+ ]
196+
197+
198+ class GeoJSONGeometryCollection (TypedDict ):
199+ type : Literal ["GeometryCollection" ]
200+ geometries : list [GeoJSONHomogeneousGeometryObject ]
201+
202+
203+ # RFC7946 3.1
204+ GeoJSONObject = Union [GeoJSONHomogeneousGeometryObject , GeoJSONGeometryCollection ]
205+
206+
207+ class GeoJSONFeature (TypedDict ):
208+ type : Literal ["Feature" ]
209+ properties : Optional [
210+ dict [str , Any ]
211+ ] # RFC7946 3.2 "(any JSON object or a JSON null value)"
212+ geometry : Optional [GeoJSONObject ]
213+
214+
215+ class GeoJSONFeatureCollection (TypedDict ):
216+ type : Literal ["FeatureCollection" ]
217+ features : list [GeoJSONFeature ]
218+
219+
220+ class GeoJSONFeatureCollectionWithBBox (GeoJSONFeatureCollection , total = False ):
221+ # bbox is optional
222+ # typing.NotRequired requires Python 3.11
223+ # and we must support 3.9 (at least until October)
224+ # https://docs.python.org/3/library/typing.html#typing.Required
225+ # Is there a backport?
226+ bbox : list [float ]
227+
228+
147229# Helpers
148230
149231MISSING = [None , "" ]
@@ -211,7 +293,7 @@ def __repr__(self):
211293
212294
213295def signed_area (
214- coords : Coords ,
296+ coords : Points ,
215297 fast : bool = False ,
216298) -> float :
217299 """Return the signed area enclosed by a ring using the linear time
@@ -229,22 +311,22 @@ def signed_area(
229311 return area2 / 2.0
230312
231313
232- def is_cw (coords : Coords ) -> bool :
314+ def is_cw (coords : Points ) -> bool :
233315 """Returns True if a polygon ring has clockwise orientation, determined
234316 by a negatively signed area.
235317 """
236318 area2 = signed_area (coords , fast = True )
237319 return area2 < 0
238320
239321
240- def rewind (coords : Reversible [Coord ]) -> Coords :
322+ def rewind (coords : Reversible [Point ]) -> Points :
241323 """Returns the input coords in reversed order."""
242324 return list (reversed (coords ))
243325
244326
245- def ring_bbox (coords : Coords ) -> BBox :
327+ def ring_bbox (coords : Points ) -> BBox :
246328 """Calculates and returns the bounding box of a ring."""
247- xs , ys = zip (* coords )
329+ xs , ys = map ( list , list ( zip (* coords ))[: 2 ]) # ignore any z or m values
248330 bbox = min (xs ), min (ys ), max (xs ), max (ys )
249331 return bbox
250332
@@ -265,7 +347,7 @@ def bbox_contains(bbox1: BBox, bbox2: BBox) -> bool:
265347 return contains
266348
267349
268- def ring_contains_point (coords : Coords , p : Point2D ) -> bool :
350+ def ring_contains_point (coords : Points , p : Point2D ) -> bool :
269351 """Fast point-in-polygon crossings algorithm, MacMartin optimization.
270352
271353 Adapted from code by Eric Haynes
@@ -314,7 +396,7 @@ class RingSamplingError(Exception):
314396 pass
315397
316398
317- def ring_sample (coords : Coords , ccw : bool = False ) -> Point2D :
399+ def ring_sample (coords : Points , ccw : bool = False ) -> Point2D :
318400 """Return a sample point guaranteed to be within a ring, by efficiently
319401 finding the first centroid of a coordinate triplet whose orientation
320402 matches the orientation of the ring and passes the point-in-ring test.
@@ -364,14 +446,15 @@ def itercoords():
364446 )
365447
366448
367- def ring_contains_ring (coords1 : Coords , coords2 : list [Point2D ]) -> bool :
449+ def ring_contains_ring (coords1 : Points , coords2 : list [Point ]) -> bool :
368450 """Returns True if all vertexes in coords2 are fully inside coords1."""
369- return all (ring_contains_point (coords1 , p2 ) for p2 in coords2 )
451+ # Ignore Z and M values in coords2
452+ return all (ring_contains_point (coords1 , p2 [:2 ]) for p2 in coords2 )
370453
371454
372455def organize_polygon_rings (
373- rings : Iterable [Coords ], return_errors : Optional [dict [str , int ]] = None
374- ) -> list [list [Coords ]]:
456+ rings : Iterable [Points ], return_errors : Optional [dict [str , int ]] = None
457+ ) -> list [list [Points ]]:
375458 """Organize a list of coordinate rings into one or more polygons with holes.
376459 Returns a list of polygons, where each polygon is composed of a single exterior
377460 ring, and one or more interior holes. If a return_errors dict is provided (optional),
@@ -541,7 +624,7 @@ def __init__(
541624 # self.bbox: Optional[_Array[float]] = None
542625
543626 @property
544- def __geo_interface__ (self ):
627+ def __geo_interface__ (self ) -> GeoJSONHomogeneousGeometryObject :
545628 if self .shapeType in [POINT , POINTM , POINTZ ]:
546629 # point
547630 if len (self .points ) == 0 :
@@ -922,17 +1005,19 @@ def __init__(self, shape: Optional[Shape] = None, record: Optional[_Record] = No
9221005 self .record = record
9231006
9241007 @property
925- def __geo_interface__ (self ):
1008+ def __geo_interface__ (self ) -> GeoJSONFeature :
9261009 return {
9271010 "type" : "Feature" ,
928- "properties" : self .record .as_dict (date_strings = True ),
1011+ "properties" : None
1012+ if self .record is None
1013+ else self .record .as_dict (date_strings = True ),
9291014 "geometry" : None
930- if self .shape .shapeType == NULL
1015+ if self .shape is None or self . shape .shapeType == NULL
9311016 else self .shape .__geo_interface__ ,
9321017 }
9331018
9341019
935- class Shapes (list ):
1020+ class Shapes (list [ Optional [ Shape ]] ):
9361021 """A class to hold a list of Shape objects. Subclasses list to ensure compatibility with
9371022 former work and to reuse all the optimizations of the builtin list.
9381023 In addition to the list interface, this also provides the GeoJSON __geo_interface__
@@ -942,17 +1027,17 @@ def __repr__(self):
9421027 return f"Shapes: { list (self )} "
9431028
9441029 @property
945- def __geo_interface__ (self ):
1030+ def __geo_interface__ (self ) -> GeoJSONGeometryCollection :
9461031 # Note: currently this will fail if any of the shapes are null-geometries
9471032 # could be fixed by storing the shapefile shapeType upon init, returning geojson type with empty coords
948- collection = {
949- " type" : "GeometryCollection" ,
950- " geometries" : [shape .__geo_interface__ for shape in self ],
951- }
1033+ collection = GeoJSONGeometryCollection (
1034+ type = "GeometryCollection" ,
1035+ geometries = [shape .__geo_interface__ for shape in self if shape is not None ],
1036+ )
9521037 return collection
9531038
9541039
955- class ShapeRecords (list ):
1040+ class ShapeRecords (list [ ShapeRecord ] ):
9561041 """A class to hold a list of ShapeRecord objects. Subclasses list to ensure compatibility with
9571042 former work and to reuse all the optimizations of the builtin list.
9581043 In addition to the list interface, this also provides the GeoJSON __geo_interface__
@@ -962,12 +1047,11 @@ def __repr__(self):
9621047 return f"ShapeRecords: { list (self )} "
9631048
9641049 @property
965- def __geo_interface__ (self ):
966- collection = {
967- "type" : "FeatureCollection" ,
968- "features" : [shaperec .__geo_interface__ for shaperec in self ],
969- }
970- return collection
1050+ def __geo_interface__ (self ) -> GeoJSONFeatureCollection :
1051+ return GeoJSONFeatureCollection (
1052+ type = "FeatureCollection" ,
1053+ features = [shaperec .__geo_interface__ for shaperec in self ],
1054+ )
9711055
9721056
9731057class ShapefileException (Exception ):
@@ -1284,10 +1368,12 @@ def __iter__(self):
12841368 yield from self .iterShapeRecords ()
12851369
12861370 @property
1287- def __geo_interface__ (self ):
1371+ def __geo_interface__ (self ) -> GeoJSONFeatureCollectionWithBBox :
12881372 shaperecords = self .shapeRecords ()
1289- fcollection = shaperecords .__geo_interface__
1290- fcollection ["bbox" ] = list (self .bbox )
1373+ fcollection = GeoJSONFeatureCollectionWithBBox (
1374+ bbox = list (self .bbox ),
1375+ ** shaperecords .__geo_interface__ ,
1376+ )
12911377 return fcollection
12921378
12931379 @property
@@ -2793,22 +2879,22 @@ def pointz(self, x: float, y: float, z: float = 0.0, m: Optional[float] = None):
27932879 pointShape .points .append ((x , y , z , m ))
27942880 self .shape (pointShape )
27952881
2796- def multipoint (self , points : Coords ):
2882+ def multipoint (self , points : Points ):
27972883 """Creates a MULTIPOINT shape.
27982884 Points is a list of xy values."""
27992885 shapeType = MULTIPOINT
28002886 # nest the points inside a list to be compatible with the generic shapeparts method
28012887 self ._shapeparts (parts = [points ], shapeType = shapeType )
28022888
2803- def multipointm (self , points : list [ PointM ] ):
2889+ def multipointm (self , points : Points ):
28042890 """Creates a MULTIPOINTM shape.
28052891 Points is a list of xym values.
28062892 If the m (measure) value is not included, it defaults to None (NoData)."""
28072893 shapeType = MULTIPOINTM
28082894 # nest the points inside a list to be compatible with the generic shapeparts method
28092895 self ._shapeparts (parts = [points ], shapeType = shapeType )
28102896
2811- def multipointz (self , points ):
2897+ def multipointz (self , points : Points ):
28122898 """Creates a MULTIPOINTZ shape.
28132899 Points is a list of xyzm values.
28142900 If the z (elevation) value is not included, it defaults to 0.
@@ -2817,7 +2903,7 @@ def multipointz(self, points):
28172903 # nest the points inside a list to be compatible with the generic shapeparts method
28182904 self ._shapeparts (parts = [points ], shapeType = shapeType )
28192905
2820- def line (self , lines : list [Coords ]):
2906+ def line (self , lines : list [Points ]):
28212907 """Creates a POLYLINE shape.
28222908 Lines is a collection of lines, each made up of a list of xy values."""
28232909 shapeType = POLYLINE
@@ -2838,7 +2924,7 @@ def linez(self, lines: list[Points]):
28382924 shapeType = POLYLINEZ
28392925 self ._shapeparts (parts = lines , shapeType = shapeType )
28402926
2841- def poly (self , polys : list [Coords ]):
2927+ def poly (self , polys : list [Points ]):
28422928 """Creates a POLYGON shape.
28432929 Polys is a collection of polygons, each made up of a list of xy values.
28442930 Note that for ordinary polygons the coordinates must run in a clockwise direction.
@@ -2865,7 +2951,7 @@ def polyz(self, polys: list[Points]):
28652951 shapeType = POLYGONZ
28662952 self ._shapeparts (parts = polys , shapeType = shapeType )
28672953
2868- def multipatch (self , parts : list [list [ PointZ ] ], partTypes : list [int ]):
2954+ def multipatch (self , parts : list [Points ], partTypes : list [int ]):
28692955 """Creates a MULTIPATCH shape.
28702956 Parts is a collection of 3D surface patches, each made up of a list of xyzm values.
28712957 PartTypes is a list of types that define each of the surface patches.
@@ -2891,7 +2977,7 @@ def multipatch(self, parts: list[list[PointZ]], partTypes: list[int]):
28912977 # write the shape
28922978 self .shape (polyShape )
28932979
2894- def _shapeparts (self , parts , shapeType ):
2980+ def _shapeparts (self , parts : list [ Points ] , shapeType : int ):
28952981 """Internal method for adding a shape that has multiple collections of points (parts):
28962982 lines, polygons, and multipoint shapes.
28972983 """
@@ -2908,10 +2994,11 @@ def _shapeparts(self, parts, shapeType):
29082994 # set part index position
29092995 polyShape .parts .append (len (polyShape .points ))
29102996 # add points
2911- for point in part :
2912- # Ensure point is list
2913- point_list = list (point )
2914- polyShape .points .append (point_list )
2997+ # for point in part:
2998+ # # Ensure point is list
2999+ # point_list = list(point)
3000+ # polyShape.points.append(point_list)
3001+ polyShape .points .extend (part )
29153002 # write the shape
29163003 self .shape (polyShape )
29173004
0 commit comments