def geo_bounds(geometries): """Calculate bounds in WGS84 coordinates for each geometry. As a faster approximation, only the the bounding coordinates are projected to WGS84 before calculating the outer bounds. Coordinates are rounded to 5 decimal places. Parameters ---------- flowlines : ndarray of pygeos geometries Returns ------- ndarray of (xmin, ymin, xmax, ymax) for each geometry """ transformer = Transformer.from_crs(CRS, GEO_CRS, always_xy=True) xmin, ymin, xmax, ymax = pg.bounds(geometries).T # transform all 4 corners then take min/max x1, y1 = transformer.transform(xmin, ymin) x2, y2 = transformer.transform(xmin, ymax) x3, y3 = transformer.transform(xmax, ymin) x4, y4 = transformer.transform(xmax, ymax) return (np.array([ np.min([x1, x2], axis=0), np.min([y1, y3], axis=0), np.max([x3, x4], axis=0), np.max([y2, y4], axis=0), ]).round(5).astype("float32").T)
def _morphological_tessellation(self, gdf, unique_id, limit, shrink, segment, verbose, check=True): objects = gdf if shrink != 0: print("Inward offset...") if verbose else None mask = objects.type.isin(["Polygon", "MultiPolygon"]) objects.loc[mask, objects.geometry.name] = objects[mask].buffer( -shrink, cap_style=2, join_style=2) objects = objects.reset_index(drop=True).explode() objects = objects.set_index(unique_id) print("Generating input point array...") if verbose else None points, ids = self._dense_point_array(objects.geometry.values.data, distance=segment, index=objects.index) hull = pygeos.convex_hull(limit) bounds = pygeos.bounds(hull) width = bounds[2] - bounds[0] leng = bounds[3] - bounds[1] hull = pygeos.buffer(hull, 2 * width if width > leng else 2 * leng) hull_p, hull_ix = self._dense_point_array( [hull], distance=pygeos.length(hull) / 100, index=[0]) points = np.append(points, hull_p, axis=0) ids = ids + ([-1] * len(hull_ix)) print("Generating Voronoi diagram...") if verbose else None voronoi_diagram = Voronoi(np.array(points)) print("Generating GeoDataFrame...") if verbose else None regions_gdf = self._regions(voronoi_diagram, unique_id, ids, crs=gdf.crs) print("Dissolving Voronoi polygons...") if verbose else None morphological_tessellation = regions_gdf[[unique_id, "geometry" ]].dissolve(by=unique_id, as_index=False) morphological_tessellation = gpd.clip( morphological_tessellation, gpd.GeoSeries(limit, crs=gdf.crs)) if check: self._check_result(morphological_tessellation, gdf, unique_id=unique_id) return morphological_tessellation
def bounds(data): if compat.USE_PYGEOS: return pygeos.bounds(data) # ensure that for empty arrays, the result has the correct shape if len(data) == 0: return np.empty((0, 4), dtype="float64") # need to explicitly check for empty (in addition to missing) geometries, # as those return an empty tuple, not resulting in a 2D array bounds = np.array([ geom.bounds if not (geom is None or geom.is_empty) else (np.nan, np.nan, np.nan, np.nan) for geom in data ]) return bounds
def as_boxes(x): """Convert an array of geometries to an array of bounding boxes polygons. Args: x (numpy.ndarray): An array of points. Returns: numpy.ndarray: An array of polygons. """ coordinates = bounds(x) return box(coordinates[:, 0], coordinates[:, 1], coordinates[:, 2], coordinates[:, 3])
def length_width_diff(collection): """ The Eig & Seitzinger (1981) shape measure, defined as: L - W Where L is the maximal east-west extent and W is the maximal north-south extent. Defined as measure LW_5 in Altman (1998) """ ga = _cast(collection) box = pygeos.bounds(ga) (xmin, xmax), (ymin, ymax) = box[:, [0, 2]].T, box[:, [1, 3]].T width, height = numpy.abs(xmax - xmin), numpy.abs(ymax - ymin) return width - height
def diameter_ratio(collection, rotated=True): """ The Flaherty & Crumplin (1992) length-width measure, stated as measure LW_7 in Altman (1998). It is given as the ratio between the minimum and maximum shape diameter. """ ga = _cast(collection) if rotated: box = pygeos.minimum_rotated_rectangle(ga) coords = pygeos.get_coordinates(box) a, b, c, d = (coords[0::5], coords[1::5], coords[2::5], coords[3::5]) widths = numpy.sqrt(numpy.sum((a - b)**2, axis=1)) heights = numpy.sqrt(numpy.sum((a - d)**2, axis=1)) else: box = pygeos.bounds(ga) (xmin, xmax), (ymin, ymax) = box[:, [0, 2]].T, box[:, [1, 3]].T widths, heights = numpy.abs(xmax - xmin), numpy.abs(ymax - ymin) return numpy.minimum(widths, heights) / numpy.maximum(widths, heights)
def window(geometries, distance): """Return windows around geometries bounds +/- distance Parameters ---------- geometries : Series or ndarray geometries to window distance : number or ndarray radius of window if ndarry, must match length of geometries Returns ------- Series or ndarray polygon windows """ minx, miny, maxx, maxy = pg.bounds(geometries).T windows = pg.box(minx - distance, miny - distance, maxx + distance, maxy + distance) if isinstance(geometries, pd.Series): return pd.Series(windows, index=geometries.index) return windows
def test_bounds_dimensions(geom, shape): assert pygeos.bounds(geom).shape == shape
def test_bounds(geom, expected): assert_array_equal(pygeos.bounds(geom), expected)
"geometry": pg.union_all(state_df.loc[state_df.id.isin( REGION_STATES[region])].geometry.values.data), "id": region, } for region in REGION_STATES], crs=CRS, ) write_dataframe(bnd_df, out_dir / "region_boundary.gpkg") bnd_df.to_feather(out_dir / "region_boundary.feather") bnd = bnd_df.geometry.values.data[0] sarp_bnd = bnd_df.loc[bnd_df.id == "se"].geometry.values.data[0] bnd_geo = bnd_df.to_crs(GEO_CRS) bnd_geo["bbox"] = pg.bounds(bnd_geo.geometry.values.data).round(2).tolist() # used to render maps with open(ui_dir / "region_bounds.json", "w") as out: out.write(bnd_geo[["id", "bbox"]].to_json(orient="records")) # create mask world = pg.box(-180, -85, 180, 85) bnd_mask = bnd_geo.copy() bnd_mask["geometry"] = pg.normalize( pg.difference(world, bnd_mask.geometry.values.data)) write_dataframe(bnd_mask, out_dir / "region_mask.gpkg") ### Extract HUC4 units that intersect boundaries print("Extracting HUC2...")
def _(shape: pygeos.Geometry): """ If we know we're working with a pygeos polygon, then use pygeos.bounds """ return pygeos.bounds(shape)
def test_bounds_empty(): actual = pygeos.bounds(empty) assert np.isnan(actual).all()
def test_bounds_missing(): actual = pygeos.bounds(None) assert np.isnan(actual).all()
def test_bounds_array(): actual = pygeos.bounds([[point, multi_point], [polygon, None]]) assert actual.shape == (2, 2, 4)
def test_bounds(geom, expected): actual = pygeos.bounds(geom) assert actual.tolist() == expected
def __init__( self, gdf, unique_id, limit=None, shrink=0.4, segment=0.5, verbose=True, enclosures=None, enclosure_id="eID", threshold=0.05, use_dask=True, n_chunks=None, **kwargs, ): self.gdf = gdf self.id = gdf[unique_id] self.limit = limit self.shrink = shrink self.segment = segment self.enclosure_id = enclosure_id if limit is not None and enclosures is not None: raise ValueError( "Both `limit` and `enclosures` cannot be passed together. " "Pass `limit` for morphological tessellation or `enclosures` " "for enclosed tessellation.") gdf = gdf.copy() if enclosures is not None: enclosures = enclosures.copy() bounds = enclosures.total_bounds centre_x = (bounds[0] + bounds[2]) / 2 centre_y = (bounds[1] + bounds[3]) / 2 gdf.geometry = gdf.geometry.translate(xoff=-centre_x, yoff=-centre_y) enclosures.geometry = enclosures.geometry.translate(xoff=-centre_x, yoff=-centre_y) self.tessellation = self._enclosed_tessellation( gdf, enclosures, unique_id, enclosure_id, threshold, use_dask, n_chunks, ) else: if isinstance(limit, (gpd.GeoSeries, gpd.GeoDataFrame)): limit = limit.unary_union if isinstance(limit, BaseGeometry): limit = pygeos.from_shapely(limit) bounds = pygeos.bounds(limit) centre_x = (bounds[0] + bounds[2]) / 2 centre_y = (bounds[1] + bounds[3]) / 2 gdf.geometry = gdf.geometry.translate(xoff=-centre_x, yoff=-centre_y) # add convex hull buffered large distance to eliminate infinity issues limit = (gpd.GeoSeries(limit, crs=gdf.crs).translate( xoff=-centre_x, yoff=-centre_y).values.data[0]) self.tessellation = self._morphological_tessellation( gdf, unique_id, limit, shrink, segment, verbose) self.tessellation["geometry"] = self.tessellation[ "geometry"].translate(xoff=centre_x, yoff=centre_y)
def _morphological_tessellation(self, gdf, unique_id, limit, shrink, segment, verbose, check=True): objects = gdf.copy() if isinstance(limit, (gpd.GeoSeries, gpd.GeoDataFrame)): limit = limit.unary_union if isinstance(limit, BaseGeometry): limit = pygeos.from_shapely(limit) bounds = pygeos.bounds(limit) centre_x = (bounds[0] + bounds[2]) / 2 centre_y = (bounds[1] + bounds[3]) / 2 objects["geometry"] = objects["geometry"].translate(xoff=-centre_x, yoff=-centre_y) if shrink != 0: print("Inward offset...") if verbose else None mask = objects.type.isin(["Polygon", "MultiPolygon"]) objects.loc[mask, "geometry"] = objects[mask].buffer(-shrink, cap_style=2, join_style=2) objects = objects.reset_index(drop=True).explode() objects = objects.set_index(unique_id) print("Generating input point array...") if verbose else None points, ids = self._dense_point_array(objects.geometry.values.data, distance=segment, index=objects.index) # add convex hull buffered large distance to eliminate infinity issues series = gpd.GeoSeries(limit, crs=gdf.crs).translate(xoff=-centre_x, yoff=-centre_y) width = bounds[2] - bounds[0] leng = bounds[3] - bounds[1] hull = series.geometry[[0]].buffer(2 * width if width > leng else 2 * leng) # pygeos bug fix if (hull.type == "MultiPolygon").any(): hull = hull.explode() hull_p, hull_ix = self._dense_point_array( hull.values.data, distance=pygeos.length(limit) / 100, index=hull.index) points = np.append(points, hull_p, axis=0) ids = ids + ([-1] * len(hull_ix)) print("Generating Voronoi diagram...") if verbose else None voronoi_diagram = Voronoi(np.array(points)) print("Generating GeoDataFrame...") if verbose else None regions_gdf = self._regions(voronoi_diagram, unique_id, ids, crs=gdf.crs) print("Dissolving Voronoi polygons...") if verbose else None morphological_tessellation = regions_gdf[[unique_id, "geometry" ]].dissolve(by=unique_id, as_index=False) morphological_tessellation = gpd.clip(morphological_tessellation, series) morphological_tessellation["geometry"] = morphological_tessellation[ "geometry"].translate(xoff=centre_x, yoff=centre_y) if check: self._check_result(morphological_tessellation, gdf, unique_id=unique_id) return morphological_tessellation
# write out for tiles write_dataframe(df.rename(columns={ "ECO4": "id", "ECO4Name": "name" }), out_dir / "sarp_eco4.gpkg") ### Extract bounds and names for unit search in user interface print("Projecting geometries to geographic coordinates for search index") print("Processing state and county") state_geo_df = (gp.read_feather(out_dir / "region_states.feather", columns=["geometry", "STATEFIPS"]).rename(columns={ "STATEFIPS": "id" }).to_crs(GEO_CRS)) state_geo_df["bbox"] = pg.bounds( state_geo_df.geometry.values.data).round(1).tolist() states_geo = (gp.read_feather(out_dir / "states.feather", columns=["geometry", "STATEFIPS"]).rename(columns={ "STATEFIPS": "id" }).to_crs(GEO_CRS)) county_geo_df = (county_df.loc[county_df.STATEFIPS.isin(states)].rename( columns={ "COUNTYFIPS": "id", "County": "name", "STATEFIPS": "state" }).to_crs(GEO_CRS)) county_geo_df["bbox"] = pg.bounds( county_geo_df.geometry.values.data).round(2).tolist()