Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion component/frontend/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
"fa": "fa-solid fa-trash",
"mdi": "mdi-trash-can",
},
"eye": {
"fa": "fa-solid fa-eye",
"mdi": "mdi-eye",
},
"draw": {
"fa": "fa-solid fa-draw-polygon",
"mdi": "mdi-draw",
Expand Down Expand Up @@ -96,5 +100,4 @@

def icon(icon: str, lib: str = "mdi") -> str:
"""Return the icon class."""

return icons[icon][lib]
7 changes: 5 additions & 2 deletions component/model/aoi_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from traitlets import Any, Bool, Dict, Int

import component.parameter as cp
from component.scripts.aoi_geometry import _aoi_bbox
from component.scripts.aoi_geometry import _aoi_bbox, fc_from_source

logger = logging.getLogger("SEPLAN")

Expand Down Expand Up @@ -507,9 +507,12 @@ def get_ee_features(self) -> Tuple[DictType[str, AnyType], DictType[str, AnyType
}
}

# Analysis uses the EXACT geometry, rebuilt server-side from each
# feature's ``source`` descriptor (asset id / admin code). The geometry
# stored in ``custom_layers`` is only a simplified display copy.
custom_aois = {
feat["properties"]["name"]: {
"ee_feature": su.geojson_to_ee(feat),
"ee_feature": fc_from_source(feat["properties"].get("source"), feat),
"color": feat["properties"]["style"]["color"],
}
for feat in self.custom_layers["features"]
Expand Down
85 changes: 84 additions & 1 deletion component/scripts/aoi_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
(only ``ee``) so every module can import them at the top level.
"""

from typing import Union
from typing import Optional, Union

import ee

Expand All @@ -21,3 +21,86 @@ def _aoi_bbox(aoi: Union[ee.FeatureCollection, ee.Geometry]) -> ee.Geometry:
"""
fc = ee.FeatureCollection(aoi)
return fc.map(lambda feat: ee.Feature(feat.geometry().bounds())).geometry().bounds()


# Display simplification tolerance (meters). Dense AOIs (millions of vertices)
# are simplified server-side to a low-vertex outline so only a tiny geometry is
# ever pulled client-side for the map + hover label. The analysis still runs on
# the full-resolution server-side ``ee.FeatureCollection``.
DISPLAY_SIMPLIFY_MAX_ERROR = 1000.0


def simplify_fc(
aoi: Union[ee.FeatureCollection, ee.Geometry],
max_error: float = DISPLAY_SIMPLIFY_MAX_ERROR,
dissolve: bool = False,
) -> ee.FeatureCollection:
"""Per-feature, server-side geometry simplification for client display.

Materialising a dense AOI client-side (``get_info`` -> GeoDataFrame ->
``__geo_interface__`` -> ``ee.serializer``) pulls millions of vertices into
native + Python memory and OOM-kills the process. Simplifying each feature on
the server first means ``get_info`` only ever transfers a low-vertex outline.

Per-feature simplification (not ``Collection.geometry().simplify``) avoids
EE's 2M-edge dissolve limit on dense collections.

Args:
aoi: source collection / geometry (kept server-side, never downloaded).
max_error: simplification tolerance in meters.
dissolve: merge the already-simplified (low-edge) features into a single
geometry — safe because simplification runs first, so the union stays
well under EE's 2M-edge limit.

Returns:
An ``ee.FeatureCollection`` with simplified geometry, safe to materialise
via ``get_info``.
"""
fc = ee.FeatureCollection(aoi)
simplified = fc.map(
lambda feat: feat.setGeometry(feat.geometry().simplify(maxError=max_error))
)
if dissolve:
merged = simplified.geometry(maxError=max_error)
return ee.FeatureCollection([ee.Feature(merged)])
return simplified


def fc_from_source(source: Optional[dict], feat: dict) -> ee.FeatureCollection:
"""Rebuild the EXACT server-side FeatureCollection for a custom sub-AOI.

``custom_layers`` stores a *simplified* display geometry (so dense AOIs never
sit in client memory). Analysis and tile rendering need the full-resolution
geometry, so we reconstruct it server-side from a small ``source`` descriptor
instead of from the simplified geojson:

* ``{"type": "asset", "id", "column", "value"}`` -> ``ee.FeatureCollection``
(optionally filtered to one feature) — nothing is downloaded.
* ``{"type": "admin", "code"}`` -> ``pygaul.AdmItems`` — nothing is downloaded.
* no/unknown descriptor -> ``geojson_to_ee(feat)``, the stored geojson. DRAW
sub-AOIs are exact and tiny, so this is correct for them; any other path
degrades gracefully to the (possibly simplified) displayed geometry.

Args:
source: the descriptor stored on the feature's ``properties["source"]``.
feat: the GeoJSON feature (fallback geometry source).

Returns:
A server-side ``ee.FeatureCollection`` at full resolution.
"""
if source:
kind = source.get("type")
if kind == "asset" and source.get("id"):
fc = ee.FeatureCollection(source["id"])
column, value = source.get("column", "ALL"), source.get("value")
if column and column != "ALL" and value is not None:
fc = fc.filter(ee.Filter.eq(column, value))
return fc
if kind == "admin" and source.get("code"):
import pygaul

return pygaul.AdmItems(admin=source["code"])

from sepal_ui.scripts import utils as su

return ee.FeatureCollection(su.geojson_to_ee(feat))
Loading
Loading