|
16 | 16 | from typing import Any, Callable, NamedTuple, Optional, Sequence, Tuple, Union |
17 | 17 |
|
18 | 18 | import numpy as np |
19 | | -import shapely |
20 | 19 | import xarray as xr |
21 | 20 |
|
22 | 21 | from xrspatial.utils import ngjit |
|
31 | 30 | except ImportError: |
32 | 31 | cuspatial = None |
33 | 32 |
|
| 33 | +#: Cached shapely module, populated by :func:`_require_shapely` on first use. |
| 34 | +#: shapely is an optional dependency (the ``vector`` extra). It is imported |
| 35 | +#: lazily so ``import xrspatial`` does not pull in shapely (and GEOS) for users |
| 36 | +#: who never rasterize vector geometry. |
| 37 | +_shapely = None |
| 38 | + |
| 39 | + |
| 40 | +def _require_shapely(): |
| 41 | + """Import shapely or raise a helpful error, caching the module. |
| 42 | +
|
| 43 | + rasterize and polygonize are the only paths that need shapely. Every |
| 44 | + function here that touches the shapely array API calls this and binds the |
| 45 | + return value to a local ``shapely`` name, so the error surfaces clearly |
| 46 | + (including inside dask workers, which call the tile helpers directly rather |
| 47 | + than through :func:`rasterize`). |
| 48 | + """ |
| 49 | + global _shapely |
| 50 | + if _shapely is None: |
| 51 | + try: |
| 52 | + import shapely as _s |
| 53 | + except ImportError as e: |
| 54 | + raise ImportError( |
| 55 | + "shapely is required for rasterize/polygonize but is not " |
| 56 | + "installed. Install it with: pip install xarray-spatial[vector]" |
| 57 | + ) from e |
| 58 | + _shapely = _s |
| 59 | + return _shapely |
| 60 | + |
34 | 61 |
|
35 | 62 | # --------------------------------------------------------------------------- |
36 | 63 | # Allocation guard: reject output dimensions that would exhaust memory |
@@ -234,6 +261,7 @@ def _classify_geometries(geometries, props_array): |
234 | 261 | ([], empty_props.copy(), empty_idx.copy()), |
235 | 262 | ([], empty_props.copy(), empty_idx.copy())) |
236 | 263 |
|
| 264 | + shapely = _require_shapely() |
237 | 265 | type_ids = shapely.get_type_id(geom_arr) |
238 | 266 | empty = shapely.is_empty(geom_arr) |
239 | 267 | valid = ~empty |
@@ -348,6 +376,7 @@ def _extract_edges(geometries, geom_ids, bounds, height, width, |
348 | 376 | def _extract_edges_vectorized(geometries, geom_ids, bounds, |
349 | 377 | height, width, all_touched): |
350 | 378 | """Vectorized edge extraction using shapely 2.0 array ops.""" |
| 379 | + shapely = _require_shapely() |
351 | 380 | xmin, ymin, xmax, ymax = bounds |
352 | 381 | px = (xmax - xmin) / width |
353 | 382 | py = (ymax - ymin) / height |
@@ -472,6 +501,7 @@ def _extract_points(geometries, bounds, height, width): |
472 | 501 |
|
473 | 502 | def _extract_points_vectorized(geometries, bounds, height, width): |
474 | 503 | """Vectorized point extraction using shapely 2.0 array ops.""" |
| 504 | + shapely = _require_shapely() |
475 | 505 | xmin, ymin, xmax, ymax = bounds |
476 | 506 | px = (xmax - xmin) / width |
477 | 507 | py = (ymax - ymin) / height |
@@ -532,6 +562,7 @@ def _extract_line_segments(geometries, bounds, height, width): |
532 | 562 |
|
533 | 563 | def _extract_lines_vectorized(geometries, bounds, height, width): |
534 | 564 | """Vectorized line extraction with Liang-Barsky clipping.""" |
| 565 | + shapely = _require_shapely() |
535 | 566 | xmin, ymin, xmax, ymax = bounds |
536 | 567 | px = (xmax - xmin) / width |
537 | 568 | py = (ymax - ymin) / height |
@@ -2131,6 +2162,7 @@ def _geometry_bboxes(geometries): |
2131 | 2162 | """Return (N, 4) float64 array of [xmin, ymin, xmax, ymax] per geometry.""" |
2132 | 2163 | if len(geometries) == 0: |
2133 | 2164 | return np.empty((0, 4), dtype=np.float64) |
| 2165 | + shapely = _require_shapely() |
2134 | 2166 | return shapely.bounds(np.asarray(geometries, dtype=object)) |
2135 | 2167 |
|
2136 | 2168 |
|
@@ -2298,11 +2330,13 @@ def _polys_to_wkb(geoms): |
2298 | 2330 | """Pre-serialize polygon geometries to WKB for cheap pickling.""" |
2299 | 2331 | if not geoms: |
2300 | 2332 | return [] |
| 2333 | + shapely = _require_shapely() |
2301 | 2334 | return shapely.to_wkb(np.asarray(geoms, dtype=object)).tolist() |
2302 | 2335 |
|
2303 | 2336 |
|
2304 | 2337 | def _polys_from_wkb(wkb_list): |
2305 | 2338 | """Deserialize WKB back to shapely geometries.""" |
| 2339 | + shapely = _require_shapely() |
2306 | 2340 | geoms = shapely.from_wkb(wkb_list) |
2307 | 2341 | if not isinstance(geoms, (list, np.ndarray)): |
2308 | 2342 | geoms = [geoms] |
@@ -3048,6 +3082,10 @@ def rasterize( |
3048 | 3082 | >>> density = rasterize(gdf, width=100, height=100, |
3049 | 3083 | ... column='pop', merge='sum', fill=0) |
3050 | 3084 | """ |
| 3085 | + # Fail early with a clear message if the optional ``vector`` extra |
| 3086 | + # (shapely) is not installed, rather than deep inside a helper. |
| 3087 | + _require_shapely() |
| 3088 | + |
3051 | 3089 | if column is not None and columns is not None: |
3052 | 3090 | raise ValueError( |
3053 | 3091 | "'column' and 'columns' are mutually exclusive; use one or " |
|
0 commit comments