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
21 changes: 21 additions & 0 deletions docs/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ m.add_basemap("dark")
m.set_center(-120, 47, zoom=8)
```

Google Earth Engine layers are optional and need `pip install earthengine-api`
plus credentials (`ee.Authenticate()` once, then a Google Cloud project):

```python
import ee

ee.Authenticate() # once per machine
ee.Initialize(project="your-google-cloud-project")
m.add_ee_layer(ee.Image("USGS/SRTMGL1_003"), {"min": 0, "max": 3000}, name="SRTM")
```

`add_ee_layer` evaluates the Earth Engine object in the kernel and adds the
resulting tile URL as a raster layer (ImageCollections are mosaicked, vector
objects are styled into raster tiles — for those, `vis_params` takes
`ee.FeatureCollection.style()` keys such as `color`, `fillColor`, `width`, and
`pointSize`, not image keys). That URL is tied to an Earth Engine map id that
expires, so a saved project may need the Earth Engine layer regenerated when it
is reopened. The result is a plain raster tile layer, not one of the live layers
the app's own Earth Engine panel manages.

`add_raster` / `add_cog` also accept a **local** GeoTIFF path on the kernel host:
the file is served by the bundled localhost server so the app can read it. This
only works where the **browser can reach the kernel's localhost** (local Jupyter,
Expand Down Expand Up @@ -240,6 +260,7 @@ m.on_layer_change(lambda e: print("layers", e["layerIds"]))
| `add_vector_tiles(url, name=, source_layers=, source_layer=, **style)` | Add a vector tile layer from a TileJSON endpoint. |
| `add_pmtiles(url, name=, tile_type=, source_layers=, **style)` | Add a PMTiles archive (vector or raster). |
| `add_tile_layer(url, name=, tile_size=, attribution=)` | Add a raster XYZ tile layer. |
| `add_ee_layer(ee_object, vis_params=, name=, shown=, opacity=)` | Add an authenticated Google Earth Engine object as raster tiles (needs `earthengine-api`). |
| `add_wms(endpoint, layers, name=, styles=, image_format=, transparent=, tile_size=, **style)` | Add a WMS layer (GetMap, tiled raster). |
| `add_wmts(url, name=, tile_size=, **style)` | Add a WMTS layer from a tile URL template. |
| `add_wfs(endpoint, type_name, name=, version=, output_format=, srs_name=, max_features=, **style)` | Add a WFS layer (GetFeature GeoJSON, fetched and inlined). |
Expand Down
17 changes: 17 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ m.add_basemap("dark")
m.set_center(-120, 47, zoom=8)
```

Google Earth Engine layers are optional and need `pip install earthengine-api`
plus credentials (`ee.Authenticate()` once, then a Google Cloud project):

```python
import ee

ee.Authenticate() # once per machine
ee.Initialize(project="your-google-cloud-project")
m.add_ee_layer(ee.Image("USGS/SRTMGL1_003"), {"min": 0, "max": 3000}, name="SRTM")
Comment thread
giswqs marked this conversation as resolved.
```

Round-trip the project:

```python
Expand All @@ -74,6 +85,7 @@ m.to_project()["mapView"]["center"]
| `add_vector_tiles(url, name=, source_layers=, source_layer=, **style)` | Add vector tiles from a TileJSON endpoint. |
| `add_pmtiles(url, name=, tile_type=, source_layers=, **style)` | Add a PMTiles archive (vector or raster). |
| `add_tile_layer(url, name=, tile_size=, attribution=)` | Add a raster XYZ tile layer. |
| `add_ee_layer(ee_object, vis_params=, name=, shown=, opacity=)` | Add an authenticated Google Earth Engine object as raster tiles. |
Comment thread
giswqs marked this conversation as resolved.
| `add_wms(endpoint, layers, name=, styles=, image_format=, transparent=, tile_size=, **style)` | Add a WMS (GetMap) tiled raster layer. |
| `add_wmts(url, name=, tile_size=, **style)` | Add a WMTS tile URL template. |
| `add_wfs(endpoint, type_name, name=, version=, output_format=, srs_name=, max_features=, **style)` | Add a WFS layer (GeoJSON, fetched and inlined). |
Expand Down Expand Up @@ -183,6 +195,11 @@ anywhere untrusted, or use `Map.save_project`, which redacts by default.
(works in the running server with no restart where it is installed). On other
remote servers (Binder, remote JupyterLab), pass `Map(server_proxy=True)` to
use that same remote path; `Map(server_proxy=False)` forces the direct path.
- `add_ee_layer` needs the Earth Engine Python API, which is **not** a
dependency of this package: `pip install earthengine-api`. Authenticate once
with `ee.Authenticate()` and initialize with `ee.Initialize(project=...)`
before adding a layer. The generated tile URL is tied to an Earth Engine map
id that expires, so a saved project may need the layer regenerated later.
- Optional extras: `pip install "geolibre[all]"` adds GeoPandas/Shapely support
for `add_geojson(geodataframe)` and for reading **local** vector files
(`add_vector`/`add_geoparquet`/`add_flatgeobuf`/`add_shp`/`add_kml`/`add_gpkg`),
Expand Down
174 changes: 173 additions & 1 deletion python/src/geolibre/geolibre.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import urllib.parse
import uuid
import warnings
from collections.abc import Callable, Sequence
from collections.abc import Callable, Mapping, Sequence
from typing import Any
from urllib.error import URLError

Expand All @@ -41,6 +41,24 @@
# same 50 MB ceiling applies to a fetched response or a local file.
_MAX_TABULAR_BYTES = _project._MAX_GEOJSON_BYTES

# ``ee.FeatureCollection.style()`` is declared with explicit keyword parameters,
# not ``**kwargs``, so an image-shaped ``vis_params`` (``min``/``max``/``palette``)
# would reach it as ``TypeError: style() got an unexpected keyword argument`` --
# indistinguishable, to the caller, from the ``TypeError`` add_ee_layer raises for
# an unsupported object. Validate against the accepted keys instead.
_EE_VECTOR_STYLE_KEYS = frozenset(
{
"color",
"pointSize",
"pointShape",
"width",
"fillColor",
"styleProperty",
"neighborhood",
"lineType",
}
)

# Column name for CSV fields beyond the header row. csv.DictReader's default
# restkey is ``None``, which would put a non-string key in the feature
# properties and break JSON serialization on the way to the widget.
Expand Down Expand Up @@ -1632,6 +1650,160 @@ def add_tile_layer(
)
)

def add_ee_layer(
self,
ee_object: Any,
vis_params: dict[str, Any] | None = None,
name: str = "Earth Engine",
shown: bool = True,
opacity: float = 1.0,
) -> str:
Comment on lines +1653 to +1660

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor API-surface nit: every other add_* layer method (add_tile_layer, add_wms, add_wmts, add_pmtiles, …) forwards **style: Any into the layer's style overrides, but add_ee_layer doesn't accept it, so there's no way to set minZoom/maxZoom/blend mode/etc. on the resulting tile layer inline the way you can for every other layer type — you'd have to fetch the layer back and mutate its style afterward. This may well be intentional (the signature mirrors geemap.Map.addLayer(ee_object, vis_params, name, shown, opacity) exactly), so treat this as a low-confidence consistency observation rather than a bug.

"""Add a Google Earth Engine object as a raster tile layer.

This follows the ``geemap``/``leafmap`` convention: Earth Engine is
evaluated in the Python kernel to obtain a map tile URL, while the
GeoLibre app renders that URL as a normal raster layer. Earth Engine
must already be authenticated and initialized (usually with
``ee.Authenticate()`` and ``ee.Initialize(project=...)``).

Args:
ee_object: An ``ee.Image``, ``ee.ImageCollection``,
``ee.FeatureCollection``, ``ee.Feature``, or ``ee.Geometry``.
A compatible object exposing ``getMapId`` is also accepted.
vis_params: Earth Engine visualization parameters, such as
``bands``, ``min``, ``max``, and ``palette``. For vector
objects these are ``ee.FeatureCollection.style()`` keys
instead (``color``, ``fillColor``, ``width``, ``pointSize``,
``pointShape``, ``lineType``, ``styleProperty``,
``neighborhood``).
name: Layer display name.
shown: Whether the layer is initially visible.
opacity: Initial opacity between 0 and 1.

Returns:
The id of the added layer.

Raises:
ImportError: If conversion requires the optional Earth Engine
Python package and it is not installed.
TypeError: If ``ee_object`` is not a supported Earth Engine object,
or ``vis_params`` is not a mapping.
ValueError: If Earth Engine returns no usable tile URL, opacity is
outside the range 0--1, or ``vis_params`` carries a key
``ee.FeatureCollection.style()`` does not accept.
RuntimeError: If Earth Engine fails to prepare the object or to
create map tiles (for example when it is not initialized, or
the request is rejected).

Note:
The generated tile URL is tied to the Earth Engine map ID. A saved
project may need the layer to be regenerated after that map ID
expires.

The layer is a plain raster tile layer, not one of the live layers
the app's own Earth Engine panel manages, so it is listed and
styled like any other tile layer rather than appearing in that
panel.
"""
try:
opacity_value = float(opacity)
except (TypeError, ValueError) as exc:
raise ValueError("opacity must be a finite number between 0 and 1") from exc
if not math.isfinite(opacity_value) or not 0 <= opacity_value <= 1:
raise ValueError("opacity must be a finite number between 0 and 1")

if vis_params is not None and not isinstance(vis_params, Mapping):
raise TypeError("vis_params must be a mapping of Earth Engine visualization keys")
params = dict(vis_params or {})
Comment thread
giswqs marked this conversation as resolved.
map_object = ee_object
map_params = params

try:
import ee
except ImportError:
ee = None

# Earth Engine types are classified *before* the duck-typed
# ``getMapId`` fallback: ``ee.ImageCollection``, ``ee.FeatureCollection``
# and ``ee.Feature`` all expose ``getMapId`` themselves, so a
# ``getMapId``-first check would silently skip the mosaic/style step and
# drop every vector option except ``color``.
ee_types = (
(ee.Image, ee.ImageCollection, ee.FeatureCollection, ee.Feature, ee.Geometry)
if ee is not None
else ()
)
if ee is not None and isinstance(map_object, ee_types):
is_vector = isinstance(map_object, (ee.FeatureCollection, ee.Feature, ee.Geometry))
if is_vector:
unsupported = sorted(set(params) - _EE_VECTOR_STYLE_KEYS)
if unsupported:
raise ValueError(
"vis_params for an Earth Engine FeatureCollection, Feature, or "
f"Geometry may only contain {sorted(_EE_VECTOR_STYLE_KEYS)}; got "
f"{unsupported}"
)
try:
if isinstance(map_object, ee.ImageCollection):
map_object = map_object.mosaic()
elif is_vector:
if isinstance(map_object, ee.Geometry):
map_object = ee.Feature(map_object)
if isinstance(map_object, ee.Feature):
map_object = ee.FeatureCollection([map_object])
vector_style = {
"color": "000000",
"fillColor": "00000000",
"width": 2,
"pointSize": 3,
"pointShape": "circle",
**params,
}
map_object = map_object.style(**vector_style)
map_params = {}
except Exception as exc:
raise RuntimeError(
f"Earth Engine could not prepare this object for display: {exc}"
) from exc
elif not callable(getattr(map_object, "getMapId", None)):
if ee is None:
raise ImportError(
"Adding this Earth Engine object requires the `earthengine-api` "
"package. Install it with `pip install earthengine-api`."
)
raise TypeError(
"ee_object must be an Earth Engine Image, ImageCollection, "
"FeatureCollection, Feature, or Geometry"
)

try:
map_id = map_object.getMapId(map_params)
except Exception as exc:
raise RuntimeError(
f"Earth Engine could not create map tiles: {exc}. Authenticate and "
"initialize Earth Engine before calling add_ee_layer(), and check "
"that vis_params are valid for this object."
) from exc
Comment thread
giswqs marked this conversation as resolved.

tile_fetcher = map_id.get("tile_fetcher") if isinstance(map_id, dict) else None
tile_url = getattr(tile_fetcher, "url_format", None)
if not tile_url and isinstance(map_id, dict):
tile_url = map_id.get("tile_url") or map_id.get("url_format")
if not isinstance(tile_url, str) or not tile_url:
raise ValueError("Earth Engine returned a map ID without a tile URL")

layer = _project.tile_layer(
name,
tile_url,
attribution="Google Earth Engine",
)
layer["visible"] = bool(shown)
layer["opacity"] = opacity_value
layer["metadata"]["provider"] = "earth-engine"
if isinstance(map_id, dict) and map_id.get("mapid"):
layer["metadata"]["earthEngineMapId"] = map_id["mapid"]
Comment thread
giswqs marked this conversation as resolved.
return self._add_layer(layer)

@staticmethod
def _resolve_raster_source(source: Any) -> str:
"""Resolve a raster source to a URL the in-iframe app can fetch.
Expand Down
Loading
Loading