Skip to content

Shape

siapy.entities.shapes.shape

ShapeGeometryEnum

Bases: Enum

Geometry Types: - Point: Single coordinate point (x,y) - LineString: Series of connected points forming a line - Polygon: Closed shape with interior area - MultiPoint: Collection of independent points - MultiLineString: Collection of independent lines - MultiPolygon: Collection of independent polygons

POINT class-attribute instance-attribute

POINT = 'point'

LINE class-attribute instance-attribute

LINE = 'linestring'

POLYGON class-attribute instance-attribute

POLYGON = 'polygon'

MULTIPOINT class-attribute instance-attribute

MULTIPOINT = 'multipoint'

MULTILINE class-attribute instance-attribute

MULTILINE = 'multilinestring'

MULTIPOLYGON class-attribute instance-attribute

MULTIPOLYGON = 'multipolygon'

Shape dataclass

Shape(
    label: str = "",
    geometry: Optional[BaseGeometry] = None,
    geo_dataframe: Optional[GeoDataFrame] = None,
)

Unified shape class that can be created from shapefiles or programmatically.

This class uses GeoDataFrame as its primary internal representation. Direct initialization is possible but using class methods is recommended.

Source code in siapy/entities/shapes/shape.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def __init__(
    self,
    label: str = "",
    geometry: Optional[BaseGeometry] = None,
    geo_dataframe: Optional[gpd.GeoDataFrame] = None,
):
    """Initialize Shape with either a geometry or geodataframe"""
    self._label = label

    if geo_dataframe is not None and geometry is not None:
        raise ConfigurationError("Cannot provide both geometry and geodataframe")

    if geo_dataframe is not None:
        self._geodataframe = geo_dataframe
    elif geometry is not None:
        self._geodataframe = gpd.GeoDataFrame(geometry=[geometry])
    else:
        raise ConfigurationError("Must provide either geometry or geodataframe")

df property

df: GeoDataFrame

label property writable

label: str

geometry property writable

geometry: GeoSeries

shape_type property

shape_type: str

is_multi property

is_multi: bool

is_point property

is_point: bool

is_line property

is_line: bool

is_polygon property

is_polygon: bool

boundary property

boundary: GeoSeries

bounds property

bounds: DataFrame

centroid property

centroid: GeoSeries

convex_hull property

convex_hull: GeoSeries

envelope property

envelope: GeoSeries

exterior property

exterior: GeoSeries

open_shapefile classmethod

open_shapefile(
    filepath: str | Path, label: str = ""
) -> Shape
Source code in siapy/entities/shapes/shape.py
82
83
84
85
86
87
88
89
90
91
@classmethod
def open_shapefile(cls, filepath: str | Path, label: str = "") -> "Shape":
    filepath = Path(filepath)
    if not filepath.exists():
        raise InvalidFilepathError(filepath)
    try:
        geo_df = gpd.read_file(filepath)
    except Exception as e:
        raise InvalidInputError({"filepath": str(filepath)}, f"Failed to open shapefile: {e}") from e
    return cls(geo_dataframe=geo_df, label=label)

from_geometry classmethod

from_geometry(
    geometry: BaseGeometry, label: str = ""
) -> Shape
Source code in siapy/entities/shapes/shape.py
 93
 94
 95
 96
 97
 98
 99
100
101
@classmethod
def from_geometry(cls, geometry: BaseGeometry, label: str = "") -> "Shape":
    if not isinstance(geometry, BaseGeometry):
        raise InvalidTypeError(
            input_value=geometry,
            allowed_types=BaseGeometry,
            message="Geometry must be of type BaseGeometry",
        )
    return cls(geometry=geometry, label=label)

from_geodataframe classmethod

from_geodataframe(
    geo_dataframe: GeoDataFrame, label: str = ""
) -> Shape
Source code in siapy/entities/shapes/shape.py
103
104
105
106
107
108
109
110
111
@classmethod
def from_geodataframe(cls, geo_dataframe: gpd.GeoDataFrame, label: str = "") -> "Shape":
    if not isinstance(geo_dataframe, gpd.GeoDataFrame):
        raise InvalidTypeError(
            input_value=geo_dataframe,
            allowed_types=gpd.GeoDataFrame,
            message="GeoDataFrame must be of type GeoDataFrame",
        )
    return cls(geo_dataframe=geo_dataframe, label=label)

from_point classmethod

from_point(x: float, y: float, label: str = '') -> Shape
Source code in siapy/entities/shapes/shape.py
113
114
115
@classmethod
def from_point(cls, x: float, y: float, label: str = "") -> "Shape":
    return cls(geometry=Point(x, y), label=label)

from_multipoint classmethod

from_multipoint(
    points: Pixels | DataFrame | Iterable[CoordinateInput],
    label: str = "",
) -> Shape
Source code in siapy/entities/shapes/shape.py
117
118
119
120
121
122
123
@classmethod
def from_multipoint(cls, points: Pixels | pd.DataFrame | Iterable[CoordinateInput], label: str = "") -> "Shape":
    points = validate_pixel_input(points)
    if len(points) < 1:
        raise ConfigurationError("At least one point is required")
    coords = points.to_list()
    return cls(geometry=MultiPoint(coords), label=label)

from_line classmethod

from_line(
    pixels: Pixels | DataFrame | Iterable[CoordinateInput],
    label: str = "",
) -> Shape
Source code in siapy/entities/shapes/shape.py
125
126
127
128
129
130
131
@classmethod
def from_line(cls, pixels: Pixels | pd.DataFrame | Iterable[CoordinateInput], label: str = "") -> "Shape":
    pixels = validate_pixel_input(pixels)
    if len(pixels) < 2:
        raise ConfigurationError("At least two points are required for a line")

    return cls(geometry=LineString(pixels.to_list()), label=label)

from_multiline classmethod

from_multiline(
    line_segments: list[
        Pixels | DataFrame | Iterable[CoordinateInput]
    ],
    label: str = "",
) -> Shape
Source code in siapy/entities/shapes/shape.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@classmethod
def from_multiline(
    cls, line_segments: list[Pixels | pd.DataFrame | Iterable[CoordinateInput]], label: str = ""
) -> "Shape":
    if not line_segments:
        raise ConfigurationError("At least one line segment is required")

    lines = []
    for segment in line_segments:
        validated_segment = validate_pixel_input(segment)
        lines.append(LineString(validated_segment.to_list()))

    multi_line = MultiLineString(lines)
    return cls(geometry=multi_line, label=label)

from_polygon classmethod

from_polygon(
    exterior: Pixels
    | DataFrame
    | Iterable[CoordinateInput],
    holes: Optional[
        list[Pixels | DataFrame | Iterable[CoordinateInput]]
    ] = None,
    label: str = "",
) -> Shape
Source code in siapy/entities/shapes/shape.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
@classmethod
def from_polygon(
    cls,
    exterior: Pixels | pd.DataFrame | Iterable[CoordinateInput],
    holes: Optional[list[Pixels | pd.DataFrame | Iterable[CoordinateInput]]] = None,
    label: str = "",
) -> "Shape":
    exterior = validate_pixel_input(exterior)
    if len(exterior) < 3:
        raise ConfigurationError("At least three points are required for a polygon")

    exterior_coords = exterior.to_list()
    # Close the polygon if not already closed
    if exterior_coords[0] != exterior_coords[-1]:
        exterior_coords.append(exterior_coords[0])

    if holes:
        # Close each hole if not already closed
        closed_holes = []
        for hole in holes:
            validated_hole = validate_pixel_input(hole)
            hole_coords = validated_hole.to_list()
            if hole_coords[0] != hole_coords[-1]:
                hole_coords.append(hole_coords[0])
            closed_holes.append(hole_coords)
        geometry = Polygon(exterior_coords, closed_holes)
    else:
        geometry = Polygon(exterior_coords)

    return cls(geometry=geometry, label=label)

from_multipolygon classmethod

from_multipolygon(
    polygons: list[
        Pixels | DataFrame | Iterable[CoordinateInput]
    ],
    label: str = "",
) -> Shape
Source code in siapy/entities/shapes/shape.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
@classmethod
def from_multipolygon(
    cls, polygons: list[Pixels | pd.DataFrame | Iterable[CoordinateInput]], label: str = ""
) -> "Shape":
    if not polygons:
        raise ConfigurationError("At least one polygon is required")

    polygon_objects = []
    for pixels in polygons:
        validated_pixels = validate_pixel_input(pixels)
        coords = validated_pixels.to_list()
        # Close the polygon if not already closed
        if coords[0] != coords[-1]:
            coords.append(coords[0])
        polygon_objects.append(Polygon(coords))

    multi_polygon = MultiPolygon(polygon_objects)
    return cls(geometry=multi_polygon, label=label)

from_rectangle classmethod

from_rectangle(
    x_min: int,
    y_min: int,
    x_max: int,
    y_max: int,
    label: str = "",
) -> Shape
Source code in siapy/entities/shapes/shape.py
198
199
200
201
@classmethod
def from_rectangle(cls, x_min: int, y_min: int, x_max: int, y_max: int, label: str = "") -> "Shape":
    coords = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
    return cls(geometry=Polygon(coords), label=label)

from_circle classmethod

from_circle(
    center: PixelCoordinate, radius: float, label: str = ""
) -> Shape
Source code in siapy/entities/shapes/shape.py
203
204
205
206
207
@classmethod
def from_circle(cls, center: PixelCoordinate, radius: float, label: str = "") -> "Shape":
    point = Point(center)
    circle = point.buffer(radius)
    return cls(geometry=circle, label=label)

copy

copy() -> Shape

Create a deep copy of the Shape instance.

Source code in siapy/entities/shapes/shape.py
289
290
291
292
def copy(self) -> "Shape":
    """Create a deep copy of the Shape instance."""
    copied_df = self.df.copy(deep=True)
    return Shape(label=self.label, geo_dataframe=copied_df)

buffer

buffer(distance: float) -> Shape
Source code in siapy/entities/shapes/shape.py
294
295
296
297
298
def buffer(self, distance: float) -> "Shape":
    buffered_geometry = self.geometry.buffer(distance)
    result = self.copy()
    result.geometry = buffered_geometry
    return result

intersection

intersection(other: Shape) -> Shape
Source code in siapy/entities/shapes/shape.py
300
301
302
303
304
def intersection(self, other: "Shape") -> "Shape":
    intersection_geometry = self.geometry.intersection(other.geometry)
    result = self.copy()
    result.geometry = intersection_geometry
    return result

union

union(other: Shape) -> Shape
Source code in siapy/entities/shapes/shape.py
306
307
308
309
310
def union(self, other: "Shape") -> "Shape":
    union_geometry = self.geometry.union(other.geometry)
    result = self.copy()
    result.geometry = union_geometry
    return result

to_file

to_file(
    filepath: str | Path, driver: str = "ESRI Shapefile"
) -> None
Source code in siapy/entities/shapes/shape.py
312
313
def to_file(self, filepath: str | Path, driver: str = "ESRI Shapefile") -> None:
    self._geodataframe.to_file(filepath, driver=driver)

to_numpy

to_numpy() -> NDArray[floating[Any]]
Source code in siapy/entities/shapes/shape.py
315
316
def to_numpy(self) -> NDArray[np.floating[Any]]:
    return self.df.to_numpy()