from pathlib import Path
from typing import Any
import geopandas as gpd
import numpy as np
from rasterio.io import DatasetReader
from rasterio.windows import Window
from shapely.geometry.point import Point
from shapely.strtree import STRtree
from shapelib.core.geometries.base import BaseShapes
from shapelib.core.geometries.polygons import Polygons
from shapelib.tools.converters import mask_to_points, points_to_mask
[docs]
class Points(BaseShapes):
"""
A class for managing Point geometries in a GeoDataFrame.
This class provides specific functionality for handling Point geometries,
including buffering by metric distances and nearest-neighbor analysis.
Parameters
----------
data : gpd.GeoDataFrame or pd.DataFrame, optional
The input data for the GeoDataFrame, can be a GeoDataFrame or a DataFrame.
geometry : shapely.geometry.Point, optional
The geometry type to use (Point in this case). Defaults to None.
crs : str, int, or pyproj.CRS, optional
Coordinate reference system (CRS) to use for the GeoDataFrame.
path : str or pathlib.Path, optional
The file path for the data (if reading from a file).
driver : str, optional
The file driver to use when reading/writing data.
Attributes
----------
geometry_type : type
The expected geometry type for this class (Point).
Examples
--------
Create a Points object:
>>> pts = Points(data=gdf)
>>> buffered = pts.buffer_m(10) # 10 meters
"""
_metadata = BaseShapes._metadata
def __init__(
self,
data: Any = None,
*args: Any,
geometry: Any = None,
crs: Any = None,
path: str | Path | None = None,
driver: str | None = None,
**kwargs: Any,
) -> None:
super().__init__(data, *args, geometry=geometry, crs=crs, path=path, driver=driver, **kwargs)
self.geometry_type = Point
[docs]
def buffer_m(self, length_m: float, cap_style: int = 1) -> Polygons:
"""
Create a buffer around each Point geometry in meters and return the result
as a Polygons object.
This method creates a buffer of the specified length (in meters) around each
Point geometry and returns a new `Polygons` object with the buffered
geometries.
Parameters
----------
length_m : float
The buffer distance in meters.
cap_style : int, optional
Buffer cap style. Kept for API parity with lines; not used by points.
Returns
-------
Polygons
A new `Polygons` object containing the buffered geometries.
Examples
--------
>>> buffered_polys = points.buffer_m(25)
>>> isinstance(buffered_polys, Polygons)
True
"""
original_crs = self.crs
# Ensure a valid UTM CRS to do metric buffering
if self.utm_crs is None:
self.estimate_utm_crs()
if self.utm_crs is None:
raise ValueError("UTM CRS could not be determined.")
with self.temp_crs(self.utm_crs):
buffered_series = self.buffer(length_m, cap_style=cap_style)
buffered_gdf = gpd.GeoDataFrame(geometry=buffered_series, crs=self.crs)
buffered_polys = Polygons(buffered_gdf)
buffered_polys.to_crs(original_crs, inplace=True)
return buffered_polys
[docs]
def calculate_average_nearest_neighbor_distance(self, max_distance: float = 30.0) -> float:
"""
Calculate the average nearest neighbor distance between points.
For each point, this method finds the closest other point and computes the
average of these distances. Distances are computed in the current CRS units.
Parameters
----------
max_distance : float, default 30.0
Distances greater than this threshold are ignored in the average.
Returns
-------
float
Average nearest neighbor distance.
Examples
--------
>>> avg = points.calculate_average_nearest_neighbor_distance()
>>> isinstance(avg, float)
True
"""
if len(self) < 2:
return 0.0
geometries = self.geometry.values
tree = STRtree(geometries)
distances: list[float] = []
for _i, geom in enumerate(geometries):
if geom.is_empty:
continue
neighbor_index = tree.query_nearest(
geom,
return_distance=False,
exclusive=True,
all_matches=False,
)[0]
distance = geom.distance(geometries[neighbor_index])
if distance < max_distance:
distances.append(float(distance))
return float(np.mean(distances)) if distances else 0.0
[docs]
@classmethod
def from_mask(
cls,
mask: np.ndarray[Any, Any],
dataset_reader: DatasetReader,
window: Window | None = None,
) -> "Points":
"""
Create a Points object from a labeled or binary mask and a rasterio dataset reader.
Parameters
----------
mask : np.ndarray
A 2D NumPy array where 0 represents background and any nonzero (or labeled)
value represents features to be converted into points.
dataset_reader : rasterio.io.DatasetReader
A rasterio dataset that the mask is derived from, used for transform and CRS.
window : rasterio.windows.Window, optional
If provided, the transform will be adjusted for the window region.
The mask should correspond to the windowed region.
Returns
-------
Points
A Points object containing the extracted points as georeferenced geometries.
"""
gdf = mask_to_points(mask, dataset_reader, window=window)
return cls(gdf)
[docs]
def to_mask(
self,
dataset_reader: DatasetReader,
fill_value: int = 1,
background_value: int = 0,
all_touched: bool = True,
window: Window = None,
) -> np.ndarray:
"""
Convert the Points object to a binary mask using a rasterio dataset reader.
Parameters
----------
dataset_reader : rasterio.io.DatasetReader
Rasterio dataset reader that defines the target raster properties
(transform, CRS, height, width) for the output mask.
fill_value : int, optional
Value to assign to pixels that coincide with point geometries. Defaults to 1.
background_value : int, optional
Value to assign to pixels that do not coincide with any point. Defaults to 0.
all_touched : bool, optional
If True, all pixels touched by geometries will be burned in.
window : rasterio.windows.Window, optional
If provided, only rasterize the region inside the window. The output mask
will have shape (window.height, window.width).
Returns
-------
np.ndarray
A 2D numpy array (mask) with the same dimensions as the input raster or
window, where pixels corresponding to points have the fill_value and
background pixels have the background_value.
"""
return points_to_mask(
self,
dataset_reader,
fill_value=fill_value,
background_value=background_value,
all_touched=all_touched,
window=window,
)