core-lens 0.1.dev106__tar.gz → 0.1.dev109__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/PKG-INFO +4 -1
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/pyproject.toml +4 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/aoi.py +18 -30
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/base/namespaces/plot.py +136 -42
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/base/result.py +1 -5
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/export/formats.py +14 -8
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/utils/polars_utils.py +4 -10
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/utils/spatial.py +65 -40
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/uv.lock +6 -0
- core_lens-0.1.dev106/plot_wb.py +0 -29
- core_lens-0.1.dev106/wb_annual.txt +0 -11
- core_lens-0.1.dev106/wb_static.txt +0 -11
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.gitignore +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.gitmessage +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/.python-version +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/LICENSE +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/README.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/SKILLS.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/Makefile +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/make.bat +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/base/entity.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/base/namespaces/stats.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/base/view.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/schema/detection.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/schema/profile.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/src/core_lens/utils/season.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev106 → core_lens-0.1.dev109}/tests/unit/test_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev109
|
|
4
4
|
Summary: Query, analyse, and visualise CoreStack's microwatershed and Earth science data through a clean, composable Python API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ApoorvaKashyap/core-lens
|
|
6
6
|
Project-URL: Issues, https://github.com/ApoorvaKashyap/core-lens/issues
|
|
@@ -23,6 +23,7 @@ Requires-Dist: pydantic<3,>=2.13.3; extra == 'core'
|
|
|
23
23
|
Provides-Extra: full
|
|
24
24
|
Requires-Dist: bokeh<4,>=3.6.0; extra == 'full'
|
|
25
25
|
Requires-Dist: duckdb<2,>=1.5.3; extra == 'full'
|
|
26
|
+
Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'full'
|
|
26
27
|
Requires-Dist: geopandas<2,>=1.1.3; extra == 'full'
|
|
27
28
|
Requires-Dist: lonboard<1,>=0.16.0; extra == 'full'
|
|
28
29
|
Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full'
|
|
@@ -38,6 +39,7 @@ Requires-Dist: bokeh<4,>=3.6.0; extra == 'full-gpu'
|
|
|
38
39
|
Requires-Dist: cudf-cu13==26.6.*; extra == 'full-gpu'
|
|
39
40
|
Requires-Dist: cudf-polars-cu13==26.6.*; extra == 'full-gpu'
|
|
40
41
|
Requires-Dist: duckdb<2,>=1.5.3; extra == 'full-gpu'
|
|
42
|
+
Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'full-gpu'
|
|
41
43
|
Requires-Dist: geopandas<2,>=1.1.3; extra == 'full-gpu'
|
|
42
44
|
Requires-Dist: lonboard<1,>=0.16.0; extra == 'full-gpu'
|
|
43
45
|
Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full-gpu'
|
|
@@ -70,6 +72,7 @@ Requires-Dist: scipy<2,>=1.17.1; extra == 'stats'
|
|
|
70
72
|
Requires-Dist: statsmodels<1,>=0.14.6; extra == 'stats'
|
|
71
73
|
Provides-Extra: viz
|
|
72
74
|
Requires-Dist: bokeh<4,>=3.6.0; extra == 'viz'
|
|
75
|
+
Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'viz'
|
|
73
76
|
Requires-Dist: lonboard<1,>=0.16.0; extra == 'viz'
|
|
74
77
|
Requires-Dist: matplotlib<4,>=3.10.9; extra == 'viz'
|
|
75
78
|
Requires-Dist: polars<2,>=1.39.0; extra == 'viz'
|
|
@@ -55,6 +55,7 @@ spatial = [
|
|
|
55
55
|
viz = [
|
|
56
56
|
"core-lens[core]",
|
|
57
57
|
"lonboard>=0.16.0,<1",
|
|
58
|
+
"geoarrow-rust-core>=0.3.0",
|
|
58
59
|
"matplotlib>=3.10.9,<4",
|
|
59
60
|
"bokeh>=3.6.0,<4",
|
|
60
61
|
]
|
|
@@ -107,3 +108,6 @@ explicit = true
|
|
|
107
108
|
[tool.uv.sources]
|
|
108
109
|
cudf-cu13 = { index = "nvidia" }
|
|
109
110
|
cudf-polars-cu13 = { index = "nvidia" }
|
|
111
|
+
|
|
112
|
+
[tool.coverage.run]
|
|
113
|
+
source = ["src/core_lens"]
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, '
|
|
21
|
+
__version__ = version = '0.1.dev109'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev109')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -247,6 +247,7 @@ class AoI:
|
|
|
247
247
|
|
|
248
248
|
layers = []
|
|
249
249
|
|
|
250
|
+
# Base layer: single AoI boundary (one Shapely geom — GeoPandas is fine here).
|
|
250
251
|
aoi_gdf = gpd.GeoDataFrame(geometry=[self.geometry], crs="EPSG:4326")
|
|
251
252
|
base_layer = lonboard.PolygonLayer.from_geopandas(
|
|
252
253
|
aoi_gdf,
|
|
@@ -261,33 +262,19 @@ class AoI:
|
|
|
261
262
|
if hasattr(overlay, "has_geometry") and not overlay.has_geometry:
|
|
262
263
|
overlay = overlay.with_geometry()
|
|
263
264
|
|
|
264
|
-
if hasattr(overlay, "
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
polys = [
|
|
278
|
-
g
|
|
279
|
-
for g in getattr(geom, "geoms", [])
|
|
280
|
-
if g.geom_type in ("Polygon", "MultiPolygon")
|
|
281
|
-
]
|
|
282
|
-
if polys:
|
|
283
|
-
return MultiPolygon(polys)
|
|
284
|
-
return None
|
|
285
|
-
|
|
286
|
-
gdf["geometry"] = gdf.geometry.apply(_extract_polygons)
|
|
287
|
-
gdf = gdf[gdf.geometry.notna()].copy()
|
|
288
|
-
|
|
289
|
-
overlay_layer = lonboard.PolygonLayer.from_geopandas(
|
|
290
|
-
gdf,
|
|
265
|
+
if hasattr(overlay, "entity") and hasattr(overlay, "data"):
|
|
266
|
+
# --- Direct Polars → GeoArrow → Lonboard (no Shapely, no GeoPandas) ---
|
|
267
|
+
from core_lens.base.namespaces.plot import _wkb_to_arrow_table
|
|
268
|
+
|
|
269
|
+
geom_col = overlay.entity.geometry_col
|
|
270
|
+
key_cols = (
|
|
271
|
+
list(overlay.key_cols) if hasattr(overlay, "key_cols") else []
|
|
272
|
+
)
|
|
273
|
+
arrow_table = _wkb_to_arrow_table(
|
|
274
|
+
overlay.data, geom_col, extra_cols=key_cols
|
|
275
|
+
)
|
|
276
|
+
overlay_layer = lonboard.PolygonLayer(
|
|
277
|
+
arrow_table,
|
|
291
278
|
get_fill_color=[255, 0, 0, 100],
|
|
292
279
|
get_line_color=[255, 0, 0, 200],
|
|
293
280
|
line_width_min_pixels=1,
|
|
@@ -332,7 +319,6 @@ class AoI:
|
|
|
332
319
|
ValueError: If the filters match zero rows.
|
|
333
320
|
"""
|
|
334
321
|
import shapely.ops as sops
|
|
335
|
-
import shapely.wkb as swkb
|
|
336
322
|
|
|
337
323
|
# Find the registered entity whose key_col or known attribute column
|
|
338
324
|
# matches one of the filter keys.
|
|
@@ -374,7 +360,7 @@ class AoI:
|
|
|
374
360
|
or col in candidate.schema_profile.extra_static_cols
|
|
375
361
|
):
|
|
376
362
|
if isinstance(val, list):
|
|
377
|
-
|
|
363
|
+
lf = lf.join(pl.LazyFrame({col: val}), on=col, how="inner")
|
|
378
364
|
else:
|
|
379
365
|
filter_expr = filter_expr & (pl.col(col) == val)
|
|
380
366
|
|
|
@@ -386,7 +372,9 @@ class AoI:
|
|
|
386
372
|
f"in {candidate.static_path!r}."
|
|
387
373
|
)
|
|
388
374
|
|
|
389
|
-
|
|
375
|
+
import shapely
|
|
376
|
+
|
|
377
|
+
geoms = shapely.from_wkb(df[geom_col].to_numpy())
|
|
390
378
|
return sops.unary_union(geoms) if len(geoms) > 1 else geoms[0]
|
|
391
379
|
|
|
392
380
|
@classmethod
|
|
@@ -45,6 +45,110 @@ _PALETTE = [
|
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _wkb_to_arrow_table(
|
|
49
|
+
df: Any,
|
|
50
|
+
geom_col: str,
|
|
51
|
+
extra_cols: list[str] | None = None,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Convert a Polars DataFrame with a WKB geometry column to a GeoArrow PyArrow Table.
|
|
54
|
+
|
|
55
|
+
Uses ``geoarrow.rust.core.from_wkb`` — a zero-copy C extension bundled
|
|
56
|
+
transitively by lonboard via ``geoarrow-rust-core``. No Shapely objects
|
|
57
|
+
are ever instantiated for the common case (Polygon / MultiPolygon rows).
|
|
58
|
+
|
|
59
|
+
GeometryCollection rows are handled by extracting their polygon parts via
|
|
60
|
+
Shapely, matching the behaviour of the previous GeoPandas code path. Because
|
|
61
|
+
GeometryCollections are rare in practice the Shapely overhead is bounded.
|
|
62
|
+
|
|
63
|
+
WKB type byte encoding (after the 1-byte endian flag):
|
|
64
|
+
1=Point, 2=LineString, 3=Polygon, 4=MultiPoint, 5=MultiLineString,
|
|
65
|
+
6=MultiPolygon, 7=GeometryCollection.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
df: A Polars DataFrame containing the WKB binary column.
|
|
69
|
+
geom_col: Name of the WKB binary column in *df*.\
|
|
70
|
+
extra_cols: Additional columns to include in the output table.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
An ``arro3.core.Table`` whose geometry column is GeoArrow-typed.
|
|
74
|
+
"""
|
|
75
|
+
import geoarrow.rust.core as ga
|
|
76
|
+
import polars as pl
|
|
77
|
+
|
|
78
|
+
keep = [geom_col] + (extra_cols or [])
|
|
79
|
+
df = df.select([c for c in keep if c in df.columns])
|
|
80
|
+
|
|
81
|
+
def _wkb_geom_type(b: bytes) -> int:
|
|
82
|
+
"""Return the WKB geometry type integer (1-7), or 0 on invalid input."""
|
|
83
|
+
if not b or len(b) < 5:
|
|
84
|
+
return 0
|
|
85
|
+
endian = b[0] # 1 = little-endian, 0 = big-endian
|
|
86
|
+
raw = int.from_bytes(b[1:5], "little" if endian == 1 else "big")
|
|
87
|
+
return raw & 0xFFFF # strip ISO Z/M/SRID flags
|
|
88
|
+
|
|
89
|
+
geom_type_series = df[geom_col].map_elements(_wkb_geom_type, return_dtype=pl.Int32)
|
|
90
|
+
|
|
91
|
+
# --- Fast path: pure Polygon / MultiPolygon rows (no Shapely) ---
|
|
92
|
+
poly_mask = geom_type_series.is_in([3, 6])
|
|
93
|
+
df_poly = df.filter(poly_mask)
|
|
94
|
+
|
|
95
|
+
# --- Slow path: GeometryCollection rows — extract polygon parts via Shapely ---
|
|
96
|
+
gc_mask = geom_type_series == 7
|
|
97
|
+
df_gc = df.filter(gc_mask)
|
|
98
|
+
|
|
99
|
+
if df_gc.height > 0:
|
|
100
|
+
import shapely.wkb as swkb
|
|
101
|
+
from shapely.geometry import MultiPolygon
|
|
102
|
+
|
|
103
|
+
repaired_rows: list[dict[str, Any]] = []
|
|
104
|
+
other_cols = [c for c in df_gc.columns if c != geom_col]
|
|
105
|
+
|
|
106
|
+
for row in df_gc.iter_rows(named=True):
|
|
107
|
+
raw_wkb = row[geom_col]
|
|
108
|
+
geom = swkb.loads(raw_wkb)
|
|
109
|
+
polys = [
|
|
110
|
+
g
|
|
111
|
+
for g in getattr(geom, "geoms", [])
|
|
112
|
+
if g.geom_type in ("Polygon", "MultiPolygon")
|
|
113
|
+
]
|
|
114
|
+
if not polys:
|
|
115
|
+
continue
|
|
116
|
+
merged = MultiPolygon(polys) if len(polys) > 1 else polys[0]
|
|
117
|
+
new_row = {geom_col: swkb.dumps(merged)}
|
|
118
|
+
for col in other_cols:
|
|
119
|
+
new_row[col] = row[col]
|
|
120
|
+
repaired_rows.append(new_row)
|
|
121
|
+
|
|
122
|
+
if repaired_rows:
|
|
123
|
+
schema = {geom_col: pl.Binary}
|
|
124
|
+
for col in other_cols:
|
|
125
|
+
schema[col] = df_gc[col].dtype
|
|
126
|
+
df_gc_repaired = pl.DataFrame(repaired_rows, schema=schema)
|
|
127
|
+
df_poly = pl.concat([df_poly, df_gc_repaired], how="diagonal")
|
|
128
|
+
|
|
129
|
+
import arro3.core as ac
|
|
130
|
+
|
|
131
|
+
if df_poly.height == 0:
|
|
132
|
+
# Return an empty table with the correct schema rather than crashing.
|
|
133
|
+
arrow_table = df_poly.to_arrow()
|
|
134
|
+
return ac.Table.from_arrow(arrow_table)
|
|
135
|
+
|
|
136
|
+
arrow_table = df_poly.to_arrow()
|
|
137
|
+
wkb_col = arrow_table.column(geom_col).combine_chunks()
|
|
138
|
+
geo_col = ga.from_wkb(wkb_col)
|
|
139
|
+
|
|
140
|
+
idx = arrow_table.schema.get_field_index(geom_col)
|
|
141
|
+
schema = arrow_table.schema.remove(idx)
|
|
142
|
+
|
|
143
|
+
tbl_cols = [ac.ChunkedArray(arrow_table.column(c)) for c in schema.names]
|
|
144
|
+
tbl_cols.insert(idx, ac.ChunkedArray([ac.Array(geo_col)]))
|
|
145
|
+
names = list(schema.names)
|
|
146
|
+
names.insert(idx, geom_col)
|
|
147
|
+
|
|
148
|
+
tbl = ac.Table.from_arrays(tbl_cols, names=names)
|
|
149
|
+
return tbl
|
|
150
|
+
|
|
151
|
+
|
|
48
152
|
def _color_for(index: int) -> str:
|
|
49
153
|
"""Get a color from the palette by index.
|
|
50
154
|
|
|
@@ -137,64 +241,52 @@ class PlotNamespace:
|
|
|
137
241
|
subplot_col = subplot_on.value if subplot_on is not None else None
|
|
138
242
|
|
|
139
243
|
res = self.result if self.result.has_geometry else self.result.with_geometry()
|
|
140
|
-
gdf = res.gdf()
|
|
141
244
|
|
|
142
|
-
if column not in
|
|
245
|
+
if column not in res.columns:
|
|
143
246
|
raise ValueError(f"Column {column!r} not found in Result.")
|
|
144
247
|
|
|
145
|
-
if subplot_col is not None and subplot_col not in
|
|
248
|
+
if subplot_col is not None and subplot_col not in res.columns:
|
|
146
249
|
raise ValueError(
|
|
147
250
|
f"PlotNamespace.choropleth: subplot_on column {subplot_col!r} not found "
|
|
148
251
|
"in Result. Ensure fortnightly data is materialised and temporal columns "
|
|
149
252
|
"are present (they are added automatically by the materialisation layer)."
|
|
150
253
|
)
|
|
151
254
|
|
|
152
|
-
# Lonboard PolygonLayer only supports Polygon and MultiPolygon.
|
|
153
|
-
# Extract polygon parts from GeometryCollections rather than dropping them.
|
|
154
|
-
def _extract_polygons(geom: Any) -> Any:
|
|
155
|
-
if geom is None:
|
|
156
|
-
return None
|
|
157
|
-
if geom.geom_type in ("Polygon", "MultiPolygon"):
|
|
158
|
-
return geom
|
|
159
|
-
if geom.geom_type == "GeometryCollection":
|
|
160
|
-
from shapely.geometry import MultiPolygon
|
|
161
|
-
|
|
162
|
-
polys = [
|
|
163
|
-
g
|
|
164
|
-
for g in getattr(geom, "geoms", [])
|
|
165
|
-
if g.geom_type in ("Polygon", "MultiPolygon")
|
|
166
|
-
]
|
|
167
|
-
if polys:
|
|
168
|
-
return MultiPolygon(polys)
|
|
169
|
-
return None
|
|
170
|
-
|
|
171
|
-
gdf["geometry"] = gdf.geometry.apply(_extract_polygons)
|
|
172
|
-
gdf = gdf[gdf.geometry.notna()].copy()
|
|
173
|
-
|
|
174
255
|
import matplotlib as mpl
|
|
175
256
|
import numpy as np
|
|
257
|
+
import polars as pl
|
|
258
|
+
|
|
259
|
+
geom_col = res.entity.geometry_col
|
|
260
|
+
extra = [column] + (res.key_cols or [])
|
|
261
|
+
if subplot_col is not None:
|
|
262
|
+
extra.append(subplot_col)
|
|
263
|
+
|
|
264
|
+
df = res.data
|
|
176
265
|
|
|
177
266
|
if subplot_col is not None:
|
|
178
|
-
# Render
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
unique_vals = sorted(gdf[subplot_col].to_list())
|
|
267
|
+
# Render most-recent unique value of subplot_on only.
|
|
268
|
+
# True multi-panel subplots not supported by Lonboard's single-Map API.
|
|
269
|
+
unique_vals = sorted(df[subplot_col].drop_nulls().unique().to_list())
|
|
182
270
|
if unique_vals:
|
|
183
|
-
|
|
271
|
+
df = df.filter(pl.col(subplot_col) == unique_vals[-1])
|
|
184
272
|
|
|
185
|
-
|
|
273
|
+
# --- Direct Polars → GeoArrow → Lonboard path (zero Shapely, zero GeoPandas) ---
|
|
274
|
+
arrow_table = _wkb_to_arrow_table(df, geom_col, extra_cols=extra)
|
|
275
|
+
|
|
276
|
+
# Extract colormap values from the already-filtered arrow_table.
|
|
277
|
+
# arrow_table has the same row order as df post-filter, so indices align.
|
|
278
|
+
values = np.asarray(arrow_table.column(column).to_pylist())
|
|
186
279
|
v_min, v_max = np.nanmin(values), np.nanmax(values)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
280
|
+
norm_values = (
|
|
281
|
+
(values - v_min) / (v_max - v_min)
|
|
282
|
+
if v_max > v_min
|
|
283
|
+
else np.zeros_like(values)
|
|
284
|
+
)
|
|
191
285
|
|
|
192
286
|
cmap = mpl.colormaps["plasma"]
|
|
193
287
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
layer = lonboard.PolygonLayer.from_geopandas(
|
|
197
|
-
gpd.GeoDataFrame(gdf),
|
|
288
|
+
layer = lonboard.PolygonLayer(
|
|
289
|
+
arrow_table,
|
|
198
290
|
get_fill_color=apply_continuous_cmap(norm_values, cmap),
|
|
199
291
|
)
|
|
200
292
|
return lonboard.Map(layers=[layer])
|
|
@@ -368,7 +460,9 @@ class PlotNamespace:
|
|
|
368
460
|
unique_entities = entity_df[key_col].unique()
|
|
369
461
|
if len(unique_entities) > top_n:
|
|
370
462
|
entities = unique_entities.limit(top_n).to_list()
|
|
371
|
-
entity_df = entity_df.
|
|
463
|
+
entity_df = entity_df.join(
|
|
464
|
+
pl.DataFrame({key_col: entities}), on=key_col, how="inner"
|
|
465
|
+
)
|
|
372
466
|
entity_pdf = entity_df.to_pandas()
|
|
373
467
|
|
|
374
468
|
fig_entity = _make_fig(entity_pdf, f"Timeseries — {y_col} (Per Entity)")
|
|
@@ -420,7 +514,7 @@ class PlotNamespace:
|
|
|
420
514
|
unique_entities = df[key_col].unique()
|
|
421
515
|
if len(unique_entities) > top_n:
|
|
422
516
|
entities = unique_entities.limit(top_n).to_list()
|
|
423
|
-
df = df.
|
|
517
|
+
df = df.join(pl.DataFrame({key_col: entities}), on=key_col, how="inner")
|
|
424
518
|
|
|
425
519
|
df_pd = df.to_pandas()
|
|
426
520
|
|
|
@@ -491,7 +585,7 @@ class PlotNamespace:
|
|
|
491
585
|
unique_entities = df[key_col].unique()
|
|
492
586
|
if len(unique_entities) > top_n:
|
|
493
587
|
entities = unique_entities.limit(top_n).to_list()
|
|
494
|
-
df = df.
|
|
588
|
+
df = df.join(pl.DataFrame({key_col: entities}), on=key_col, how="inner")
|
|
495
589
|
|
|
496
590
|
df_pd = df.to_pandas()
|
|
497
591
|
|
|
@@ -148,11 +148,7 @@ class Result:
|
|
|
148
148
|
geo_df = collect_lf(
|
|
149
149
|
pl.scan_parquet(static_path)
|
|
150
150
|
.select(key_cols + [geom_col])
|
|
151
|
-
.
|
|
152
|
-
pl.col(key_cols[0]).is_in(self.data[key_cols[0]].to_list())
|
|
153
|
-
if len(key_cols) == 1
|
|
154
|
-
else pl.lit(True)
|
|
155
|
-
)
|
|
151
|
+
.join(self.data.select(key_cols).unique().lazy(), on=key_cols, how="inner")
|
|
156
152
|
)
|
|
157
153
|
|
|
158
154
|
joined = self.data.join(geo_df, on=key_cols, how="left")
|
|
@@ -4,6 +4,18 @@ from typing import TYPE_CHECKING, Any
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
5
|
from core_lens.base.result import Result
|
|
6
6
|
|
|
7
|
+
_DUCKDB_CONN = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_duckdb_conn() -> Any:
|
|
11
|
+
global _DUCKDB_CONN
|
|
12
|
+
if _DUCKDB_CONN is None:
|
|
13
|
+
import duckdb
|
|
14
|
+
|
|
15
|
+
_DUCKDB_CONN = duckdb.connect()
|
|
16
|
+
_DUCKDB_CONN.execute("INSTALL spatial; LOAD spatial;")
|
|
17
|
+
return _DUCKDB_CONN
|
|
18
|
+
|
|
7
19
|
|
|
8
20
|
def parquet(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
9
21
|
"""Export the result data to a standard Parquet file.
|
|
@@ -105,8 +117,6 @@ def geoparquet(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> Non
|
|
|
105
117
|
"Call .with_geometry() first to join the static geometry column before exporting to geoparquet."
|
|
106
118
|
)
|
|
107
119
|
|
|
108
|
-
import duckdb
|
|
109
|
-
|
|
110
120
|
df = result.df()
|
|
111
121
|
geom_col = result.entity.geometry_col
|
|
112
122
|
|
|
@@ -119,8 +129,7 @@ def geoparquet(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> Non
|
|
|
119
129
|
|
|
120
130
|
select_clause = ", ".join(cols)
|
|
121
131
|
|
|
122
|
-
conn =
|
|
123
|
-
conn.execute("INSTALL spatial; LOAD spatial;")
|
|
132
|
+
conn = _get_duckdb_conn()
|
|
124
133
|
|
|
125
134
|
# We must escape single quotes in path if any exist.
|
|
126
135
|
path_str = str(path).replace("'", "''")
|
|
@@ -173,8 +182,6 @@ def geojson(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
|
173
182
|
"Call .with_geometry() first to join the static geometry column before exporting to geojson."
|
|
174
183
|
)
|
|
175
184
|
|
|
176
|
-
import duckdb
|
|
177
|
-
|
|
178
185
|
df = result.df()
|
|
179
186
|
geom_col = result.entity.geometry_col
|
|
180
187
|
|
|
@@ -187,8 +194,7 @@ def geojson(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
|
187
194
|
|
|
188
195
|
select_clause = ", ".join(cols)
|
|
189
196
|
|
|
190
|
-
conn =
|
|
191
|
-
conn.execute("INSTALL spatial; LOAD spatial;")
|
|
197
|
+
conn = _get_duckdb_conn()
|
|
192
198
|
|
|
193
199
|
path_str = str(path).replace("'", "''")
|
|
194
200
|
|
|
@@ -102,16 +102,10 @@ def scan_with_key_filter(
|
|
|
102
102
|
"""
|
|
103
103
|
lf = pl.scan_parquet(path)
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
else:
|
|
110
|
-
# Composite key: filter each column independently. A small over-selection
|
|
111
|
-
# is acceptable because the subsequent join at collect time is exact.
|
|
112
|
-
for key in key_cols:
|
|
113
|
-
values = key_values[key].to_list()
|
|
114
|
-
lf = lf.filter(pl.col(key).is_in(values))
|
|
105
|
+
# Use an inner join to filter the parquet file down to the exact requested keys.
|
|
106
|
+
# This avoids building a massive literal expression tree (which consumes gigabytes
|
|
107
|
+
# of RAM) that occurs when passing large lists to pl.col().is_in().
|
|
108
|
+
lf = lf.join(key_values.lazy(), on=key_cols, how="inner")
|
|
115
109
|
|
|
116
110
|
if time_expr is not None:
|
|
117
111
|
lf = lf.filter(time_expr)
|
|
@@ -6,11 +6,10 @@ import pathlib
|
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
import polars as pl
|
|
9
|
-
import shapely
|
|
10
|
-
import shapely.wkt as swkt
|
|
9
|
+
import shapely
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
13
|
-
|
|
12
|
+
pass
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def resolve_path(path: str) -> str:
|
|
@@ -81,19 +80,27 @@ def build_bbox_index(
|
|
|
81
80
|
df = pl.read_parquet(static_path, columns=cols_to_read)
|
|
82
81
|
geom_series = df[geometry_col]
|
|
83
82
|
|
|
83
|
+
geom_array = geom_series.to_numpy()
|
|
84
|
+
|
|
84
85
|
if geometry_type == "wkb":
|
|
85
|
-
geoms =
|
|
86
|
+
geoms = shapely.from_wkb(geom_array)
|
|
86
87
|
else:
|
|
87
|
-
geoms =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
geoms = shapely.from_wkt(geom_array)
|
|
89
|
+
|
|
90
|
+
if len(geoms) > 0:
|
|
91
|
+
bnds = shapely.bounds(geoms)
|
|
92
|
+
return df.select(key_cols).with_columns(
|
|
93
|
+
pl.Series("minx", bnds[:, 0], dtype=pl.Float64),
|
|
94
|
+
pl.Series("miny", bnds[:, 1], dtype=pl.Float64),
|
|
95
|
+
pl.Series("maxx", bnds[:, 2], dtype=pl.Float64),
|
|
96
|
+
pl.Series("maxy", bnds[:, 3], dtype=pl.Float64),
|
|
97
|
+
)
|
|
91
98
|
|
|
92
99
|
return df.select(key_cols).with_columns(
|
|
93
|
-
pl.Series("minx",
|
|
94
|
-
pl.Series("miny",
|
|
95
|
-
pl.Series("maxx",
|
|
96
|
-
pl.Series("maxy",
|
|
100
|
+
pl.Series("minx", [], dtype=pl.Float64),
|
|
101
|
+
pl.Series("miny", [], dtype=pl.Float64),
|
|
102
|
+
pl.Series("maxx", [], dtype=pl.Float64),
|
|
103
|
+
pl.Series("maxy", [], dtype=pl.Float64),
|
|
97
104
|
)
|
|
98
105
|
|
|
99
106
|
|
|
@@ -175,23 +182,19 @@ def exact_spatial_filter(
|
|
|
175
182
|
if candidates.is_empty():
|
|
176
183
|
return candidates.select(key_cols)
|
|
177
184
|
|
|
178
|
-
# Build a
|
|
179
|
-
full_df =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
full_df = full_df.filter(pl.col(key).is_in(candidate_keys))
|
|
186
|
-
else:
|
|
187
|
-
# Composite key: join on all key columns.
|
|
188
|
-
full_df = full_df.join(candidates.select(key_cols), on=key_cols, how="inner")
|
|
185
|
+
# Build a lazy scan and push down an inner join to load only required geometries.
|
|
186
|
+
full_df = (
|
|
187
|
+
pl.scan_parquet(static_path)
|
|
188
|
+
.select(key_cols + [geometry_col])
|
|
189
|
+
.join(candidates.select(key_cols).lazy(), on=key_cols, how="inner")
|
|
190
|
+
.collect()
|
|
191
|
+
)
|
|
189
192
|
|
|
190
|
-
|
|
193
|
+
geom_array = full_df[geometry_col].to_numpy()
|
|
191
194
|
if geometry_type == "wkb":
|
|
192
|
-
geoms =
|
|
195
|
+
geoms = shapely.from_wkb(geom_array)
|
|
193
196
|
else:
|
|
194
|
-
geoms =
|
|
197
|
+
geoms = shapely.from_wkt(geom_array)
|
|
195
198
|
|
|
196
199
|
if relationship == "centroid":
|
|
197
200
|
# Centroid mode: entity centroid must lie within the AoI geometry.
|
|
@@ -209,10 +212,10 @@ def exact_spatial_filter(
|
|
|
209
212
|
hit_indices = []
|
|
210
213
|
for i in intersect_idx:
|
|
211
214
|
entity_geom = geoms[i]
|
|
212
|
-
entity_area = entity_geom.area
|
|
215
|
+
entity_area = entity_geom.area # pyright: ignore[reportAttributeAccessIssue]
|
|
213
216
|
if entity_area == 0.0:
|
|
214
217
|
continue
|
|
215
|
-
inter_area = entity_geom.intersection(aoi_geometry).area
|
|
218
|
+
inter_area = entity_geom.intersection(aoi_geometry).area # pyright: ignore[reportAttributeAccessIssue]
|
|
216
219
|
if inter_area / entity_area >= threshold:
|
|
217
220
|
hit_indices.append(i)
|
|
218
221
|
|
|
@@ -267,15 +270,24 @@ def execute_spatial_join(
|
|
|
267
270
|
other_df = pl.read_parquet(other_static, columns=other_cols)
|
|
268
271
|
|
|
269
272
|
# Decode other geometries.
|
|
270
|
-
|
|
273
|
+
other_geom_array = other_df[other_geom_col].to_numpy()
|
|
271
274
|
if other_geom_type == "wkb":
|
|
272
|
-
other_geoms =
|
|
275
|
+
other_geoms = shapely.from_wkb(other_geom_array)
|
|
273
276
|
else:
|
|
274
|
-
other_geoms =
|
|
277
|
+
other_geoms = shapely.from_wkt(other_geom_array)
|
|
275
278
|
|
|
276
279
|
# Build STRtree from other entity geometries.
|
|
277
280
|
other_tree = shapely.STRtree(other_geoms)
|
|
278
|
-
|
|
281
|
+
|
|
282
|
+
# Only convert the columns we actually need to aggregate to Pandas, avoiding
|
|
283
|
+
# a massive memory copy of the WKB geometry strings and unused keys.
|
|
284
|
+
agg_cols_only = [
|
|
285
|
+
c for c in agg.keys() if c not in ("count", "area") and c in other_df.columns
|
|
286
|
+
]
|
|
287
|
+
if agg_cols_only:
|
|
288
|
+
other_df_np = other_df.select(agg_cols_only).to_pandas()
|
|
289
|
+
else:
|
|
290
|
+
other_df_np = None
|
|
279
291
|
|
|
280
292
|
# Decode primary geometries.
|
|
281
293
|
if primary_geom_col not in primary_df.columns:
|
|
@@ -285,18 +297,28 @@ def execute_spatial_join(
|
|
|
285
297
|
"with_geometry() first."
|
|
286
298
|
)
|
|
287
299
|
|
|
288
|
-
|
|
300
|
+
primary_geom_array = primary_df[primary_geom_col].to_numpy()
|
|
289
301
|
if primary_geom_type == "wkb":
|
|
290
|
-
primary_geoms =
|
|
302
|
+
primary_geoms = shapely.from_wkb(primary_geom_array)
|
|
291
303
|
else:
|
|
292
|
-
primary_geoms =
|
|
304
|
+
primary_geoms = shapely.from_wkt(primary_geom_array)
|
|
293
305
|
|
|
294
306
|
# For each primary entity, find overlapping other entities and aggregate.
|
|
295
307
|
result_rows: list[dict[str, Any]] = []
|
|
296
308
|
primary_keys = primary_df.select(primary_key_cols).to_dicts()
|
|
297
309
|
|
|
310
|
+
# Vectorized STRtree query over all primary geometries at once
|
|
311
|
+
# Returns a 2D array of shape (2, N): [[primary_indices], [other_indices]]
|
|
312
|
+
idx_pairs = other_tree.query(primary_geoms, predicate="intersects")
|
|
313
|
+
|
|
314
|
+
from collections import defaultdict
|
|
315
|
+
|
|
316
|
+
hits_by_primary = defaultdict(list)
|
|
317
|
+
for p_idx, o_idx in zip(idx_pairs[0], idx_pairs[1]):
|
|
318
|
+
hits_by_primary[p_idx].append(o_idx)
|
|
319
|
+
|
|
298
320
|
for i, (pgeom, pkey) in enumerate(zip(primary_geoms, primary_keys)):
|
|
299
|
-
hit_idx =
|
|
321
|
+
hit_idx = hits_by_primary.get(i, [])
|
|
300
322
|
row: dict[str, Any] = dict(pkey)
|
|
301
323
|
|
|
302
324
|
if not hit_idx:
|
|
@@ -306,7 +328,10 @@ def execute_spatial_join(
|
|
|
306
328
|
result_rows.append(row)
|
|
307
329
|
continue
|
|
308
330
|
|
|
309
|
-
|
|
331
|
+
if other_df_np is not None:
|
|
332
|
+
matched_other = other_df_np.iloc[hit_idx]
|
|
333
|
+
else:
|
|
334
|
+
matched_other = None
|
|
310
335
|
|
|
311
336
|
for col, fn in agg.items():
|
|
312
337
|
out_col = f"{other_entity_name}_{col}"
|
|
@@ -314,10 +339,10 @@ def execute_spatial_join(
|
|
|
314
339
|
row[out_col] = len(hit_idx)
|
|
315
340
|
elif fn == "area":
|
|
316
341
|
# Sum of intersection areas (geographic, approx).
|
|
317
|
-
total = sum(pgeom.intersection(other_geoms[j]).area for j in hit_idx)
|
|
342
|
+
total = sum(pgeom.intersection(other_geoms[j]).area for j in hit_idx) # pyright: ignore[reportAttributeAccessIssue]
|
|
318
343
|
row[out_col] = total
|
|
319
|
-
elif col in matched_other.columns:
|
|
320
|
-
vals = matched_other[col].dropna()
|
|
344
|
+
elif matched_other is not None and col in matched_other.columns:
|
|
345
|
+
vals = matched_other[col].dropna()
|
|
321
346
|
if fn == "mean":
|
|
322
347
|
row[out_col] = float(vals.mean()) if len(vals) else None
|
|
323
348
|
elif fn == "sum":
|
|
@@ -554,6 +554,7 @@ core = [
|
|
|
554
554
|
full = [
|
|
555
555
|
{ name = "bokeh" },
|
|
556
556
|
{ name = "duckdb" },
|
|
557
|
+
{ name = "geoarrow-rust-core" },
|
|
557
558
|
{ name = "geopandas" },
|
|
558
559
|
{ name = "lonboard" },
|
|
559
560
|
{ name = "matplotlib" },
|
|
@@ -570,6 +571,7 @@ full-gpu = [
|
|
|
570
571
|
{ name = "cudf-cu13" },
|
|
571
572
|
{ name = "cudf-polars-cu13" },
|
|
572
573
|
{ name = "duckdb" },
|
|
574
|
+
{ name = "geoarrow-rust-core" },
|
|
573
575
|
{ name = "geopandas" },
|
|
574
576
|
{ name = "lonboard" },
|
|
575
577
|
{ name = "matplotlib" },
|
|
@@ -606,6 +608,7 @@ stats = [
|
|
|
606
608
|
]
|
|
607
609
|
viz = [
|
|
608
610
|
{ name = "bokeh" },
|
|
611
|
+
{ name = "geoarrow-rust-core" },
|
|
609
612
|
{ name = "lonboard" },
|
|
610
613
|
{ name = "matplotlib" },
|
|
611
614
|
{ name = "polars" },
|
|
@@ -653,6 +656,9 @@ requires-dist = [
|
|
|
653
656
|
{ name = "duckdb", marker = "extra == 'full'", specifier = ">=1.5.3,<2" },
|
|
654
657
|
{ name = "duckdb", marker = "extra == 'full-gpu'", specifier = ">=1.5.3,<2" },
|
|
655
658
|
{ name = "duckdb", marker = "extra == 'spatial'", specifier = ">=1.5.3,<2" },
|
|
659
|
+
{ name = "geoarrow-rust-core", marker = "extra == 'full'", specifier = ">=0.3.0" },
|
|
660
|
+
{ name = "geoarrow-rust-core", marker = "extra == 'full-gpu'", specifier = ">=0.3.0" },
|
|
661
|
+
{ name = "geoarrow-rust-core", marker = "extra == 'viz'", specifier = ">=0.3.0" },
|
|
656
662
|
{ name = "geopandas", marker = "extra == 'full'", specifier = ">=1.1.3,<2" },
|
|
657
663
|
{ name = "geopandas", marker = "extra == 'full-gpu'", specifier = ">=1.1.3,<2" },
|
|
658
664
|
{ name = "geopandas", marker = "extra == 'spatial'", specifier = ">=1.1.3,<2" },
|
core_lens-0.1.dev106/plot_wb.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import polars as pl
|
|
2
|
-
from core_lens import AoI
|
|
3
|
-
from core_lens.base.entity import BaseEntity
|
|
4
|
-
|
|
5
|
-
from core_lens.entities.waterbody import WaterbodyEntity
|
|
6
|
-
|
|
7
|
-
def main():
|
|
8
|
-
# Register the entity
|
|
9
|
-
AoI.register(WaterbodyEntity)
|
|
10
|
-
|
|
11
|
-
# Init AoI with a bounding box for India (or you can adjust bbox)
|
|
12
|
-
DATA_ROOT = "data/"
|
|
13
|
-
aoi = AoI(DATA_ROOT, bbox=(68.0, 6.0, 98.0, 38.0))
|
|
14
|
-
|
|
15
|
-
# Materialise the static waterbodies data
|
|
16
|
-
print("Loading waterbodies...")
|
|
17
|
-
wb_result = aoi.waterbody.static
|
|
18
|
-
|
|
19
|
-
# Plot using the area column for choropleth colors
|
|
20
|
-
print("Plotting waterbodies...")
|
|
21
|
-
choropleth_map = wb_result.plot.choropleth("area_ha")
|
|
22
|
-
|
|
23
|
-
# Save the map to HTML
|
|
24
|
-
output_file = "wb_map.html"
|
|
25
|
-
choropleth_map.to_html(output_file)
|
|
26
|
-
print(f"Saved waterbodies map to {output_file}")
|
|
27
|
-
|
|
28
|
-
if __name__ == "__main__":
|
|
29
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|