diff --git a/README.md b/README.md index a8ed822..52b8de7 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ and the bounding box area the shapefile covers: >>> len(sf) 663 >>> sf.bbox - [-122.515048, 37.652916, -122.327622, 37.863433] + (-122.515048, 37.652916, -122.327622, 37.863433) Finally, if you would prefer to work with the entire shapefile in a different format, you can convert all of it to a GeoJSON dictionary, although you may lose diff --git a/src/shapefile.py b/src/shapefile.py index 41f6359..0c1b401 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -24,7 +24,6 @@ from typing import ( IO, Any, - Collection, Container, Generic, Iterable, @@ -34,7 +33,6 @@ Protocol, Reversible, Sequence, - TypedDict, TypeVar, Union, overload, @@ -109,12 +107,16 @@ T = TypeVar("T") Point2D = tuple[float, float] -PointZ = tuple[float, float, float] -PointZM = tuple[float, float, float, float] +Point3D = tuple[float, float, float] +PointM = tuple[float, float, Optional[float]] +PointZ = tuple[float, float, float, Optional[float]] -Coord = Union[Point2D, PointZ, PointZM] +Coord = Union[Point2D, Point2D, Point3D] Coords = list[Coord] +Point = Union[Point2D, PointM, PointZ] +Points = list[Point] + BBox = tuple[float, float, float, float] @@ -131,19 +133,12 @@ def tell(self): ... BinaryFileT = Union[str, IO[bytes]] BinaryFileStreamT = Union[IO[bytes], io.BytesIO, BinaryWritableSeekable] -FieldTuple = tuple[str, str, int, bool] +FieldTuple = tuple[str, str, int, int] RecordValue = Union[ bool, int, float, str, date ] # A Possible value in a Shapefile record, e.g. L, N, F, C, D types -class GeoJsonShapeT(TypedDict): - type: str - coordinates: Union[ - tuple[()], Point2D, PointZ, PointZM, Coords, list[Coords], list[list[Coords]] - ] - - class HasGeoInterface(Protocol): @property def __geo_interface__(self) -> Any: ... @@ -242,7 +237,7 @@ def is_cw(coords: Coords) -> bool: return area2 < 0 -def rewind(coords: Reversible[Coord]) -> list[Coord]: +def rewind(coords: Reversible[Coord]) -> Coords: """Returns the input coords in reversed order.""" return list(reversed(coords)) @@ -254,11 +249,11 @@ def ring_bbox(coords: Coords) -> BBox: return bbox -def bbox_overlap(bbox1: BBox, bbox2: Collection[float]) -> bool: +def bbox_overlap(bbox1: BBox, bbox2: BBox) -> bool: """Tests whether two bounding boxes overlap.""" xmin1, ymin1, xmax1, ymax1 = bbox1 xmin2, ymin2, xmax2, ymax2 = bbox2 - overlap = xmin1 <= xmax2 and xmax1 >= xmin2 and ymin1 <= ymax2 and ymax1 >= ymin2 + overlap = xmin1 <= xmax2 and xmin2 <= xmax1 and ymin1 <= ymax2 and ymin2 <= ymax1 return overlap @@ -266,11 +261,11 @@ def bbox_contains(bbox1: BBox, bbox2: BBox) -> bool: """Tests whether bbox1 fully contains bbox2.""" xmin1, ymin1, xmax1, ymax1 = bbox1 xmin2, ymin2, xmax2, ymax2 = bbox2 - contains = xmin1 < xmin2 and xmax1 > xmax2 and ymin1 < ymin2 and ymax1 > ymax2 + contains = xmin1 < xmin2 and xmax2 < xmax1 and ymin1 < ymin2 and ymax2 < ymax1 return contains -def ring_contains_point(coords: list[Coord], p: Point2D) -> bool: +def ring_contains_point(coords: Coords, p: Point2D) -> bool: """Fast point-in-polygon crossings algorithm, MacMartin optimization. Adapted from code by Eric Haynes @@ -319,7 +314,7 @@ class RingSamplingError(Exception): pass -def ring_sample(coords: list[Coord], ccw: bool = False) -> Point2D: +def ring_sample(coords: Coords, ccw: bool = False) -> Point2D: """Return a sample point guaranteed to be within a ring, by efficiently finding the first centroid of a coordinate triplet whose orientation matches the orientation of the ring and passes the point-in-ring test. @@ -369,14 +364,14 @@ def itercoords(): ) -def ring_contains_ring(coords1: list[Coord], coords2: list[Point2D]) -> bool: +def ring_contains_ring(coords1: Coords, coords2: list[Point2D]) -> bool: """Returns True if all vertexes in coords2 are fully inside coords1.""" return all(ring_contains_point(coords1, p2) for p2 in coords2) def organize_polygon_rings( - rings: Iterable[list[Coord]], return_errors: Optional[dict[str, int]] = None -) -> list[list[list[Coord]]]: + rings: Iterable[Coords], return_errors: Optional[dict[str, int]] = None +) -> list[list[Coords]]: """Organize a list of coordinate rings into one or more polygons with holes. Returns a list of polygons, where each polygon is composed of a single exterior ring, and one or more interior holes. If a return_errors dict is provided (optional), @@ -510,7 +505,7 @@ class Shape: def __init__( self, shapeType: int = NULL, - points: Optional[list[Coord]] = None, + points: Optional[Points] = None, parts: Optional[Sequence[int]] = None, partTypes: Optional[Sequence[int]] = None, oid: Optional[int] = None, @@ -546,7 +541,7 @@ def __init__( # self.bbox: Optional[_Array[float]] = None @property - def __geo_interface__(self) -> GeoJsonShapeT: + def __geo_interface__(self): if self.shapeType in [POINT, POINTM, POINTZ]: # point if len(self.points) == 0: @@ -1434,7 +1429,7 @@ def __shpHeader(self): shp.seek(32) self.shapeType = unpack("= 2046: raise ShapefileException(