mirror of
https://github.com/opengeos/GeoLibre.git
synced 2026-11-04 18:55:27 +00:00
* fix(cesium): restore startup and forward control events
* feat(cesium): load native KML and sample elevation profiles
* fix(cesium): fade KML labels and align Python tool coverage
* fix(cesium): avoid MapLibre placeholder warnings on native layers
* Address review feedback
- KmlSource: route the URL field label and the default layer name through
t(), and add addData.kml.url / addData.kml.defaultName to all 19 locale
catalogs, matching the sibling Add Data sources (Claude).
- LayerPanel: scope the placeholder-warning suppression to the layer kinds
Cesium actually draws, so a kind the globe cannot render (duckdb-query)
keeps its message while the globe is primary (Claude).
- cesium-control-host: convert hit positions through scene.globe?.ellipsoid
with a WGS84 fallback -- pickGlobeHit answers a globe-less scene with an
ellipsoid pick, which the old dereference would have thrown on
(CodeRabbit); skip the pick entirely when no control listens for the
event, so mousemove no longer pays for a terrain ray per pointer frame
that nothing consumes (Claude).
- cesium-layer-sync: re-append KML ScreenOverlay containers in store order
during sync(), so a panel reorder restacks overlapping overlays instead
of leaving them in load order (CodeRabbit).
- ElevationProfileControl: keep the native sampler's own error messages
("the terrain source changed", "the globe was closed") instead of
replacing them with the generic HTTP-path fallback (CodeRabbit).
- Tests: regression coverage for the globe-less pointer conversion, the
unlistened-event pick skip, and BOM-prefixed inline KML.
576 lines
30 KiB
Markdown
576 lines
30 KiB
Markdown
# Python package (Jupyter)
|
||
|
||
[](https://pypi.python.org/pypi/geolibre)
|
||
[](https://colab.research.google.com/github/opengeos/GeoLibre/blob/main/python/examples/getting-started.ipynb)
|
||
[](https://anaconda.org/conda-forge/geolibre)
|
||
[](https://github.com/conda-forge/geolibre-feedstock)
|
||
|
||
GeoLibre ships a Python package, **`geolibre`**, that embeds the full GeoLibre
|
||
app inside a Jupyter notebook cell as an [anywidget](https://anywidget.dev),
|
||
with a [leafmap](https://leafmap.org)-style API.
|
||
|
||
The widget loads the complete GeoLibre app (menus, panels, processing tools) in
|
||
an iframe. State syncs both ways through a single `.geolibre.json` project, so
|
||
data you add from Python appears in the UI, and edits you make in the UI
|
||
(panning, zooming, adding layers) are readable back from Python.
|
||
|
||
## Install
|
||
|
||
```bash
|
||
pip install geolibre
|
||
```
|
||
|
||
Or with conda from [conda-forge](https://anaconda.org/conda-forge/geolibre):
|
||
|
||
```bash
|
||
conda install -c conda-forge geolibre
|
||
```
|
||
|
||
Optional extras provide GeoPandas/Shapely support for GeoDataFrames and local
|
||
vector files, plus xarray/rioxarray/rasterio/rio-tiler support for in-memory rasters:
|
||
|
||
```bash
|
||
pip install "geolibre[all]" # both
|
||
pip install "geolibre[vector]" # GeoPandas/Shapely only
|
||
pip install "geolibre[raster]" # xarray/rioxarray/rasterio/rio-tiler only
|
||
```
|
||
|
||
`[all]` is the union of the two, so it now installs rasterio (and GDAL with it);
|
||
use `[vector]` to keep an existing vector-only environment as light as before.
|
||
|
||
The optional `[all]` extra is pip-only. If you installed via conda, add it with
|
||
`pip install "geolibre[all]"` inside the same environment.
|
||
|
||
## Quickstart
|
||
|
||
```python
|
||
from geolibre import Map
|
||
|
||
m = Map(center=(-100, 40), zoom=4)
|
||
m.add_geojson("https://example.com/data.geojson", name="Data")
|
||
m
|
||
```
|
||
|
||
The full GeoLibre UI renders in the cell. Add more data and drive the view:
|
||
|
||
```python
|
||
m.add_tile_layer(
|
||
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||
name="OpenStreetMap",
|
||
attribution="(c) OpenStreetMap contributors",
|
||
)
|
||
m.add_cog("https://example.com/dem.tif", name="DEM", colormap="terrain")
|
||
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
|
||
works directly in local Jupyter and VS Code. In Google Colab, where the kernel
|
||
proxy does not preserve the byte-range semantics required by browser COG
|
||
rendering, GeoLibre renders local rasters as PNG XYZ tiles in the kernel instead.
|
||
JupyterHub can route the COG through the kernel port when
|
||
`jupyter-server-proxy` is available. A static-server-extension-only deployment
|
||
cannot expose kernel files, so pass a hosted URL there. The served URL is
|
||
session-scoped, so a project saved with a local raster will not restore it when
|
||
reopened later — pass a hosted URL for durable projects.
|
||
|
||
Install `geolibre[raster]` to visualize an in-memory xarray object. Spatial
|
||
dimensions named `lon`/`lat` or `longitude`/`latitude` default to EPSG:4326;
|
||
otherwise provide CRS and dimension names explicitly. Dataset variables become
|
||
bands unless one is selected:
|
||
|
||
```python
|
||
m.add_raster(data_array, name="Temperature", colormap="viridis")
|
||
m.add_raster(
|
||
dataset,
|
||
name="Temperature",
|
||
array_args={"variable": "temperature", "isel": {"time": 0}},
|
||
)
|
||
```
|
||
|
||
The temporary Cloud-Optimized GeoTIFF backing an xarray layer is removed when the widget is
|
||
closed, so xarray layers have the same session-only limitation as local files.
|
||
Locally the browser reads that COG directly; in Colab rio-tiler renders it into
|
||
ordinary PNG XYZ tiles to avoid Colab's incompatible byte-range proxy behavior.
|
||
Call `m.close()` when you are done to remove it promptly; otherwise it is removed
|
||
when the `Map` is garbage collected or when the kernel exits normally. A kernel
|
||
that is killed outright leaves the file behind in the system temp directory.
|
||
|
||
Add markers and data-driven symbology without precomputing styles:
|
||
|
||
```python
|
||
m.add_marker(-122.4, 37.8, properties={"name": "San Francisco"})
|
||
m.add_marker_cluster([(-122.4, 37.8), (-122.3, 37.9), (-122.5, 37.7)])
|
||
m.add_heatmap([(-122.4, 37.8), (-122.3, 37.9)], radius=35)
|
||
m.add_csv("cities.csv", x="longitude", y="latitude", name="Cities")
|
||
m.add_choropleth(
|
||
"https://example.com/counties.geojson",
|
||
column="population",
|
||
colormap="blues",
|
||
scheme="quantile",
|
||
)
|
||
```
|
||
|
||
Add a legend, a colorbar, and a swipe (split-map) comparison:
|
||
|
||
```python
|
||
# A built-in land-cover legend, or your own {label: color} dict.
|
||
m.add_legend(builtin="nlcd")
|
||
m.add_legend(legend_dict={"Water": "#0000ff", "Land": "#00ff00"})
|
||
|
||
# A colorbar for a continuous raster.
|
||
m.add_colorbar(colormap="terrain", vmin=0, vmax=4000, label="Elevation", units="m")
|
||
|
||
# Compare two layers (or a layer against the basemap) with a swipe slider.
|
||
before = m.add_cog("https://example.com/before.tif", name="Before")
|
||
after = m.add_cog("https://example.com/after.tif", name="After")
|
||
m.split_map(before, after)
|
||
```
|
||
|
||
## Two-way sync
|
||
|
||
Because the project syncs both ways, you can pan or zoom the map in the UI and
|
||
then read the live state back from Python:
|
||
|
||
```python
|
||
proj = m.to_project()
|
||
proj["mapView"]["center"] # reflects the live UI view
|
||
[layer["name"] for layer in proj["layers"]]
|
||
```
|
||
|
||
Save and reload projects, fully interchangeable with the desktop and web apps:
|
||
|
||
```python
|
||
m.save_project("my-map.geolibre.json")
|
||
|
||
m2 = Map()
|
||
m2.load_project("my-map.geolibre.json")
|
||
m2
|
||
```
|
||
|
||
`to_project()`, `save_project()`, and `to_html()` redact credentials — API keys,
|
||
tokens, authenticated request headers, environment variables, geocoder keys, and
|
||
credential URL parameters — so anything you serialize, commit, or share is safe
|
||
by default. Pass `keep_credentials=True` to `to_project()` or `save_project()`
|
||
for a trusted local file that must keep working without re-entering them:
|
||
|
||
```python
|
||
m.save_project("private.geolibre.json", keep_credentials=True)
|
||
```
|
||
|
||
The `project` trait itself is unredacted, so live two-way sync with the widget
|
||
keeps authenticated layers rendering.
|
||
|
||
## Map options
|
||
|
||
```python
|
||
Map(
|
||
center=(-100, 40), # [lng, lat]
|
||
zoom=4,
|
||
basemap="dark", # a basemap name or a MapLibre style URL
|
||
height="800px",
|
||
layout="embed", # "embed" (compact UI), "full" (desktop UI), or "maponly"
|
||
theme="light", # "light" or "dark"
|
||
)
|
||
```
|
||
|
||
## Interactive scripting
|
||
|
||
Beyond adding data, the widget can **query the live app and react to it** — the
|
||
same surface as the in-app [Python Console](user-guide/python-console.md). These
|
||
calls round-trip to the running map, so the map must be displayed first (show
|
||
`m` in a cell), then run the queries in a later cell.
|
||
|
||
```python
|
||
m.get_center() # live [lng, lat], reflecting UI pans/zooms
|
||
m.get_bounds() # [west, south, east, north]
|
||
m.fly_to(-122.4, 37.8, zoom=10)
|
||
m.identify(-122.4, 37.8) # features at a point, like clicking the map
|
||
|
||
# Layer objects: read and mutate layers, read their features
|
||
for layer in m.layers:
|
||
layer.opacity = 0.6
|
||
features = m.layers[0].get_features() # list of GeoJSON Feature objects
|
||
|
||
# Run a processing algorithm; result layers are added to the map
|
||
m.list_algorithms()
|
||
m.run_algorithm("buffer", {"layer": layer_id, "distance": 1000})
|
||
|
||
# Read back what the user selected or drew on the map
|
||
m.get_selected_features() # the clicked feature(s) as Feature objects
|
||
m.get_drawn_features() # features sketched with the Geo Editor
|
||
m.user_rois # the drawn ROIs as a GeoJSON FeatureCollection
|
||
m.get_drawn_features(as_gdf=True) # the same, as a GeoDataFrame (needs GeoPandas)
|
||
|
||
png = m.to_image() # PNG bytes (or m.to_image("map.png"))
|
||
m.to_html("map.html") # standalone HTML embedding the live project
|
||
```
|
||
|
||
React to user interaction with event callbacks:
|
||
|
||
```python
|
||
m.on_click(lambda e: print("clicked", e["lngLat"]))
|
||
m.on_selection_change(lambda e: print("selected", e))
|
||
m.on_layer_change(lambda e: print("layers", e["layerIds"]))
|
||
```
|
||
|
||
!!! note "Blocking queries"
|
||
Interactive queries block the kernel until the app replies (via
|
||
`jupyter_ui_poll`, installed automatically). Pass `timeout=` for slow calls,
|
||
e.g. `m.run_algorithm(..., timeout=300)`.
|
||
|
||
## API reference
|
||
|
||
### Interactive queries, events & processing
|
||
|
||
| Method | Description |
|
||
| --- | --- |
|
||
| `get_view()` / `get_center()` / `get_bounds()` | Read the live camera / center / viewport bounds. |
|
||
| `fly_to(lng, lat, zoom=, bearing=, pitch=, duration=)` | Animate the camera. |
|
||
| `fit_bounds([w, s, e, n])` | Fit the camera to a bounding box. |
|
||
| `zoom_to_bounds([w, s, e, n])` / `zoom_to_layer(layer)` | Leafmap-style view helpers; layers may be addressed by id, name, or handle. |
|
||
| `identify(lng, lat, layer_id=None)` | Query rendered features at a point. |
|
||
| `get_features(layer_id)` | A layer's features as `Feature` objects. |
|
||
| `get_selected_features(as_gdf=False)` | The feature(s) selected in the app, as `Feature` objects (or a GeoDataFrame). |
|
||
| `get_drawn_features(as_gdf=False)` / `user_rois` | Features drawn with the Geo Editor; `user_rois` returns them as a FeatureCollection. |
|
||
| `layers` / `get_layer(id)` | `Layer` handles (read state; set `name`/`visible`/`opacity`, `set_style`, `get_features`, `zoom_to`, `remove`). |
|
||
| `layer_names` / `find_layer(name)` / `find_layer_index(name)` | Inspect layers by display name. |
|
||
| `set_layer_visibility(layer, visible)` / `set_layer_opacity(layer, opacity)` | Change a layer by id, name, or handle. |
|
||
| `list_algorithms()` | Available processing algorithms (`id`, `parameters`, …). |
|
||
| `run_algorithm(id, parameters=None, timeout=)` | Run an algorithm; returns `{logs, resultLayerIds}`. |
|
||
| `to_image(path=None, timeout=)` | Capture the map as PNG bytes, or write to `path`. |
|
||
| `to_html(path=None, title=, width=, height=, app_url=)` | Export a standalone HTML page that embeds the current project (credentials redacted); returns the HTML or writes to `path`. |
|
||
| `on(event, cb)` / `on_click` / `on_selection_change` / `on_layer_change` | Register event callbacks; returns an unsubscribe function. |
|
||
| `request(method, params=None, timeout=)` | Low-level command primitive behind the methods above. |
|
||
|
||
### Data, view & projects
|
||
|
||
| Method | Description |
|
||
| --- | --- |
|
||
| `Map(center, zoom, basemap=, height=, layout=, theme=)` | Create a map. |
|
||
| `add_geojson(data, name=, **style)` | Add GeoJSON from a dict, file path, URL, JSON string, or GeoDataFrame. |
|
||
| `add_gdf(gdf, name=, column=None, **style)` | Add a GeoDataFrame, optionally as a choropleth. |
|
||
| `add_csv(data, x="longitude", y="latitude", name=, **style)` / `add_xy_data(...)` | Add points from a CSV path, URL, text, DataFrame, or row mappings. |
|
||
| `add_marker(lng, lat, name=, properties=, color=, opacity=, radius=, stroke_color=, stroke_width=, shape=, size=, icon=, **style)` | Add a single point marker (`properties` appear on click). |
|
||
| `add_markers(points, name=, color=, opacity=, radius=, stroke_color=, stroke_width=, shape=, size=, icon=, **style)` | Add point markers from `(lng, lat)` pairs, `{lng/lon/x, lat/y, …}` dicts, GeoJSON, or a GeoDataFrame. |
|
||
| `add_circle_markers(points, name=, radius=, **style)` | Add circle markers with an explicit `radius`. |
|
||
| `add_marker_cluster(points, name=, cluster_radius=, cluster_max_zoom=, **style)` | Add clustered point markers. |
|
||
| `add_heatmap(points, name=, radius=, intensity=, color_ramp=, weight_field=, **style)` | Add point data using the density heatmap renderer, optionally weighted by a numeric field. |
|
||
| `add_choropleth(data, column, name=, class_count=, colormap=, scheme=, **style)` | Add a GeoJSON layer with graduated symbology computed from a numeric `column`. |
|
||
| `add_data(data, column=None, name=, **kwargs)` | Add data; a choropleth when `column` is given, else a plain GeoJSON layer (leafmap parity). |
|
||
| `add_vector(data, name=, render_mode=, data_format=, source_layer=, **style)` | Add a vector dataset from a URL (GeoParquet, FlatGeobuf, zipped Shapefile, GeoJSON, …) or a local file (read via GeoPandas and inlined). |
|
||
| `add_geoparquet(data, name=, **style)` | Add a GeoParquet dataset (URL or local file). |
|
||
| `add_flatgeobuf(data, name=, **style)` | Add a FlatGeobuf dataset (URL or local file). |
|
||
| `add_shp(data, name=, **style)` | Add a Shapefile (zipped URL or local `.shp`). |
|
||
| `add_kml(data, name=, **style)` / `add_gpkg(data, name=, layer=None, **style)` | Add KML/KMZ or GeoPackage data. |
|
||
| `add_polyline(polyline, name="Polyline", precision=5, **style)` | Add an Encoded Polyline layer from a string or list of strings (precision 5 or 6). |
|
||
| `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=, version=, bounds=, **style)` | Add a WMS layer (GetMap, tiled raster). `bounds` is `[west, south, east, north]`, needed for zoom-to-layer. |
|
||
| `add_wmts(url, name=, tile_size=, bounds=, **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). |
|
||
| `add_cog(url, name=, bands=, colormap=, rescale=, **style)` | Add a Cloud Optimized GeoTIFF (URL or a kernel-side local GeoTIFF path). |
|
||
| `add_raster(source, name=, bands=, colormap=, rescale=, array_args=, **style)` | Add a COG/GeoTIFF URL or path, or an xarray DataArray/Dataset (xarray needs `geolibre[raster]`). |
|
||
| `add_3d_tiles(url=None, name=, ion_asset_id=, altitude_offset=, request_headers=, **style)` | Add a 3D Tiles `tileset.json` URL, or a Cesium Ion tileset by asset id (3D globe only). |
|
||
| `add_cesium_ion(asset_id, name=, kind="3d-tiles", altitude_offset=, **style)` | Add a Cesium Ion asset by id: a 3D Tiles tileset or (`kind="imagery"`) an imagery layer. Renders on the 3D globe, with the app's Ion token. |
|
||
| `add_czml(url=None, name=, data=, source_path=, **style)` | Add a CZML (Cesium Language) dynamic 3D scene by URL or inline packets: orbits, vehicle tracks, moving models. Renders on the 3D globe, which follows the document's clock. |
|
||
| `add_cesium_kml(url=None, name=, data=, source_path=, **style)` | Native KML/KMZ on the globe with document styles and overlays. Supply a URL, inline XML, or a KMZ data URL; use `add_kml` for vector conversion. |
|
||
| `add_video(urls, coordinates, name=, **style)` | Add a georeferenced video (four `[lng, lat]` corners). |
|
||
| `add_basemap(basemap)` | Set the background basemap. |
|
||
| `split_map(left_layers=None, right_layers=None, orientation=, position=, control_position=)` | Add a swipe (split-map) comparison slider between two layer sets. |
|
||
| `add_legend(title=None, legend_dict=, labels=, colors=, builtin=, position=, shape=)` | Add a legend from a `{label: color}` dict, parallel `labels`/`colors`, or a `builtin` preset (`"nlcd"`, `"esa_worldcover"`). |
|
||
| `add_colorbar(colormap=, vmin=, vmax=, label=, units=, colors=, orientation=, position=)` | Add a colorbar for a continuous raster, from a named colormap or custom `colors`. |
|
||
| `add_colormap(colormap, vmin=, vmax=, label=, **kwargs)` | Add a colorbar from a named colormap (leafmap-style alias of `add_colorbar`). |
|
||
| `set_center(lng, lat, zoom=None)` | Center (and optionally zoom) the map. |
|
||
| `set_center_zoom(lng, lat, zoom=None)` | Alias of `set_center` (leafmap compatibility). |
|
||
| `set_zoom(zoom)` / `set_bearing(bearing)` / `set_pitch(pitch)` / `fit_project_bounds(bounds)` | Persist camera changes without requiring the widget to be displayed. |
|
||
| `center` / `zoom` / `bearing` / `pitch` / `basemap` / `name` | Read persisted project and camera state; `name` is writable. |
|
||
| `rename_layer(layer, name)` / `move_layer(layer, index)` / `duplicate_layer(layer, name=)` / `show_layer(layer)` / `hide_layer(layer)` | Manage layers by id, name, or `Layer` handle. |
|
||
| `layer_properties(layer)` / `column_values(layer, column)` / `describe()` | Inspect inlined data and summarize a project without a browser round trip. |
|
||
| `remove_layer(layer_id)` / `clear_layers()` | Remove one layer by id, name, or handle, or remove all layers. |
|
||
| `set_popup(layer, fields=None, click=, hover=, title=, title_expression=, body_expression=, show_feature_id=, tooltip=, merge=False)` | Choose what a click popup shows for a layer, and how each value is formatted. |
|
||
| `set_tooltip(layer, fields=True)` / `clear_popup(layer)` | Turn a hover tooltip on (or off), or drop the popup config and restore the default popup. |
|
||
| `to_project(keep_credentials=False)` | Return the current project as a dict, credentials redacted unless `keep_credentials=True`. |
|
||
| `load_project(src)` | Replace the project from a dict, JSON string, or `.geolibre.json` path. |
|
||
| `save_project(path, keep_credentials=False)` | Write the current project to a `.geolibre.json` file, credentials redacted unless `keep_credentials=True`. |
|
||
|
||
Style keyword arguments (for example `fillColor`, `strokeColor`, `strokeWidth`,
|
||
`circleRadius`) map to the GeoLibre [layer style fields](project-format.md).
|
||
|
||
### Marker symbology
|
||
|
||
`add_marker` and `add_markers` take the common point-symbology settings as named
|
||
arguments, so you do not have to know the underlying style keys:
|
||
|
||
```python
|
||
m.add_markers(points, color="#e11d48", radius=8, stroke_color="#ffffff", stroke_width=2)
|
||
m.add_markers(points, shape="pin", color="#e11d48", size=32)
|
||
m.add_markers(points, icon='<svg viewBox="0 0 24 24">…</svg>', size=28)
|
||
```
|
||
|
||
A point layer draws two ways. By default it is a MapLibre circle sized by
|
||
`radius`. Passing `shape`, `size`, or `icon` switches it to a **marker sprite**:
|
||
one of `circle`, `square`, `triangle`, `diamond`, `star`, `cross`, `pin`, or
|
||
`custom` (which needs `icon`, raw SVG markup or a data URL).
|
||
|
||
The two modes take different settings, and mixing them is an error rather than a
|
||
silent no-op. A sprite layer replaces the circle layer outright and draws its own
|
||
white halo, so `opacity`, `radius`, `stroke_color`, and `stroke_width` are
|
||
circle-only and are rejected when `shape`/`size`/`icon` is also given — use
|
||
`size` for a sprite's size. A sprite's `color` must be a hex color, because the
|
||
sprite baker accepts nothing else and would otherwise fall back to the default
|
||
blue in silence.
|
||
|
||
Where a named argument and its underlying style key are both passed
|
||
(`add_markers(pts, radius=8, circleRadius=20)`), the raw style key wins — it is
|
||
the low-level escape hatch.
|
||
|
||
### Popups and tooltips
|
||
|
||
Without any configuration, clicking a feature shows the layer name and every
|
||
visible property, and there is no hover tooltip. Every `add_*` method that
|
||
takes style overrides accepts `popup=` and `tooltip=` to change that (the
|
||
exception is `add_ee_layer`, which has a fixed signature), and `set_popup` /
|
||
`set_tooltip` / `clear_popup` change it on a layer that already exists.
|
||
|
||
```python
|
||
m.add_markers(
|
||
points,
|
||
popup={
|
||
"title": "name", # heading, instead of the layer name
|
||
"fields": [
|
||
{"field": "name", "label": "Site"},
|
||
{"field": "photo", "kind": "image", "label": "Photo"},
|
||
{"field": "url", "kind": "link", "link_label": "Read more"},
|
||
{"field": "pop", "kind": "number", "thousands": True, "suffix": " people"},
|
||
{"field": "surveyed", "kind": "date", "date_format": "datetime"},
|
||
],
|
||
},
|
||
tooltip="name",
|
||
)
|
||
|
||
m.set_popup("Sites", ["name", "pop"], title="name") # replace the config
|
||
m.set_tooltip("Sites", ["name"]) # add a hover tip
|
||
m.set_popup("Sites", click=False) # no popup on click
|
||
m.clear_popup("Sites") # back to the default
|
||
```
|
||
|
||
`popup=` also accepts shorter forms: a single property name (`popup="name"`), a
|
||
list of names (`popup=["name", "pop"]`), a list of field mappings, or `False` to
|
||
suppress the click popup. The full mapping form above takes the same keys as
|
||
`set_popup` — `fields`, `click`, `hover`, `title`, `title_expression`,
|
||
`body_expression`, `show_feature_id`, `tooltip` — and rejects a key it does not
|
||
know, so a misspelling is an error rather than a setting that quietly does
|
||
nothing. Those keys belong *inside* `popup=`; passed to `add_markers` directly
|
||
they would be taken for style keys. `tooltip=` takes a property name, a list
|
||
of names, `True` to put every configured popup field in the tip, or `False` to
|
||
turn it off.
|
||
|
||
A field's `kind` decides how the value renders:
|
||
|
||
| `kind` | Renders as |
|
||
| --- | --- |
|
||
| `auto` (default) | Text, except an inline base64 raster data URL, which becomes a thumbnail. |
|
||
| `text` | Text, with `prefix`/`suffix` applied. |
|
||
| `number` | A localized number; `decimals`, `thousands`, `prefix`, `suffix`. |
|
||
| `date` | A localized date; `date_format` is `date`, `datetime`, `time`, `iso`, or `year`. |
|
||
| `link` | An `http(s)` value becomes a link, labelled `link_label`. |
|
||
| `image` | An `http(s)` value or inline base64 raster data URL becomes a thumbnail. |
|
||
|
||
Two rules worth knowing before you port a popup from another library:
|
||
|
||
- **Raw HTML in a property is not rendered as markup.** A popup value that
|
||
arrives from a GeoJSON file is untrusted, so it is written as text rather
|
||
than parsed. Use `kind="image"` and `kind="link"` for pictures and links, and
|
||
`body_expression` (a [MapLibre expression](https://maplibre.org/maplibre-style-spec/expressions/),
|
||
as JSON text) when you want a composed sentence instead of a table:
|
||
`body_expression='["concat", ["get", "name"], " — ", ["get", "county"], " County"]'`.
|
||
The one exception is a KML `description` property, whose known markup is
|
||
sanitized and rendered, so a converted KML keeps its description card.
|
||
- **A tooltip needs fields flagged for hover, and flagging them narrows the
|
||
click popup.** The two share one field list: the tooltip shows the entries
|
||
flagged for hover, and the click popup shows *every* entry — but only falls
|
||
back to "all visible properties" while that list is empty. So
|
||
|
||
```python
|
||
m.add_markers(points, tooltip="name") # click popup now shows ONLY name
|
||
```
|
||
|
||
because naming a tooltip field creates the list. To keep the full click popup,
|
||
list the fields you want on click as well, and flag one for hover:
|
||
|
||
```python
|
||
m.add_markers(points, popup=["name", "pop", "county"], tooltip="name")
|
||
```
|
||
|
||
A tooltip that could never show anything is an error rather than a tip that
|
||
silently never appears — including one whose only flagged field is an
|
||
`image`, since images are dropped from tooltips (their value is a URL, which
|
||
would become the whole tip). The click popup still shows the picture.
|
||
|
||
## Use in marimo
|
||
|
||
[marimo](https://marimo.io/) can render GeoLibre's anywidget, but its browser
|
||
may not be able to reach the random `127.0.0.1` port where GeoLibre normally
|
||
serves the bundled app. The symptom is an iframe displaying
|
||
`127.0.0.1 refused to connect`. Point the widget at GeoLibre's hosted app before
|
||
displaying it:
|
||
|
||
```python
|
||
from geolibre import Map
|
||
|
||
m = Map(center=(-100, 40), zoom=4)
|
||
m._app_url = "https://web.geolibre.app/"
|
||
m.add_basemap("dark")
|
||
m.add_vector(
|
||
"https://data.source.coop/giswqs/opengeos/world_cities.geojson",
|
||
name="World cities",
|
||
)
|
||
m
|
||
```
|
||
|
||
This uses the same project-sync bridge as the regular widget; only the app's
|
||
location changes. Set `_app_url` before returning `m` from the cell so the
|
||
iframe uses the hosted URL on its first render.
|
||
|
||
The example uses `add_vector()` so the hosted browser app fetches the remote
|
||
GeoJSON directly. `add_geojson(url)` instead downloads and inlines the file in
|
||
Python, which can fail when a data host rejects Python's HTTP client.
|
||
|
||
Because the hosted app cannot access files exposed by the kernel's temporary
|
||
localhost server, use hosted URLs for rasters and other sources the browser
|
||
loads directly. Local GeoJSON, CSV, and vector files that GeoLibre reads in
|
||
Python and inlines into the project continue to work. The `_app_url` attribute
|
||
is currently an internal compatibility workaround rather than a public
|
||
constructor option.
|
||
|
||
**Privacy:** The widget sends its synchronized project, including any inlined
|
||
local data, to the origin in `_app_url` through `window.postMessage`. Use only a
|
||
trusted app URL, or host the GeoLibre app yourself, when working with sensitive
|
||
data.
|
||
|
||
## How it works
|
||
|
||
The wheel bundles the GeoLibre web build. At import time the package starts a
|
||
small localhost static server that serves the bundled app; the widget renders
|
||
that app in an iframe and exchanges the project over `window.postMessage`.
|
||
Adding data from Python rewrites the synced project and pushes it into the app;
|
||
UI edits flow back the same way.
|
||
|
||
<!-- markdownlint-disable MD046 -->
|
||
|
||
!!! note "Environment support"
|
||
|
||
The interactive widget works in **local Jupyter, VS Code, Google Colab,
|
||
JupyterHub / remote servers, and marimo**:
|
||
|
||
- **Local Jupyter / VS Code** - the app is served directly from localhost.
|
||
- **Google Colab** - routes through Colab's built-in port proxy
|
||
(`google.colab.kernel.proxyPort`) automatically.
|
||
- **JupyterHub** (including managed/shared hubs, detected at runtime via
|
||
`JUPYTERHUB_SERVICE_PREFIX`) - the front-end probes two same-origin routes
|
||
and uses whichever is live, so a host needs only **one** of them:
|
||
- the Jupyter Server extension bundled with `geolibre`, mounted at
|
||
`{base_url}geolibre/app/` on the notebook server's own origin. It is
|
||
enabled automatically on `pip install geolibre` and needs no
|
||
`jupyter-server-proxy` and no extra port, so it works on locked-down
|
||
hubs that block raw-port proxying -- but it only registers after the
|
||
Jupyter server restarts, since it loads from a startup config drop-in.
|
||
- `jupyter-server-proxy` at `{base_url}proxy/{port}/`, which reaches the
|
||
kernel's localhost bundle in the **running** server with no restart,
|
||
wherever `jupyter-server-proxy` is installed.
|
||
- **Other remote servers** (Binder, remote JupyterLab over SSH/network) -
|
||
pass `Map(server_proxy=True)` to use that same dual-route remote path.
|
||
- **marimo** - use the hosted app URL shown in [Use in
|
||
marimo](#use-in-marimo); Jupyter's proxy and server-extension routes are
|
||
not available in marimo.
|
||
|
||
Set `Map(server_proxy=False)` to force the direct localhost path. If the app
|
||
fails to load on a hub, either install `jupyter-server-proxy`, or confirm the
|
||
extension is enabled with `jupyter server extension list` (look for
|
||
`geolibre`; run `jupyter server extension enable geolibre` if absent) and
|
||
**restart** the Jupyter server so the extension loads.
|
||
|
||
<!-- markdownlint-enable MD046 -->
|
||
|
||
!!! warning "URL fetching"
|
||
|
||
`add_geojson(url)`, `add_csv(url)` / `add_xy_data(url)`, and `add_wfs()`
|
||
fetch the URL from the **kernel**, following redirects, so a notebook can
|
||
reach any host the kernel can. Every hop is checked, and a URL that resolves
|
||
to a non-public address (private, loopback, or link-local — including cloud
|
||
metadata endpoints such as `169.254.169.254`) is refused; responses are
|
||
capped at 50 MB. Tile and service layers are fetched by the **browser**
|
||
instead, so those can still point at a local server. Do not load untrusted
|
||
`.geolibre.json` projects or URLs on a shared/multi-tenant kernel.
|
||
|
||
## MCP server
|
||
|
||
The same package ships an [MCP](https://modelcontextprotocol.io) server that
|
||
authors `.geolibre.json` projects from an AI client, with no notebook and no
|
||
running app involved:
|
||
|
||
```bash
|
||
pip install "geolibre[mcp]"
|
||
geolibre-mcp --root ~/maps
|
||
```
|
||
|
||
It builds projects through the same builders this package uses, so anything it
|
||
writes opens in the widget (and in the desktop and web apps) unchanged. See
|
||
[MCP server](mcp.md) for the tool list and client configuration.
|
||
|
||
## Building from source
|
||
|
||
The package lives in [`python/`](https://github.com/opengeos/GeoLibre/tree/main/python).
|
||
The bundled app is produced from the monorepo with:
|
||
|
||
```bash
|
||
npm run build:embed # builds the app and stages it into the wheel
|
||
python -m build # builds the wheel
|
||
python -m twine upload dist/* # upload to PyPI
|
||
pip install -e python # editable install for development
|
||
```
|
||
|
||
Changes to the Python code are picked up on kernel restart. Changes to the app
|
||
(TypeScript) require re-running `npm run build:embed` and restarting the kernel.
|
||
|
||
## Cesium and mixed pane layouts
|
||
|
||
```python
|
||
m = Map(renderer="cesium", center=(-100, 40), zoom=4)
|
||
m.set_map_layout(1, 2, view_kinds=["cesium", "maplibre"], sync_view=True)
|
||
pane_id = m.project["secondaryMapViews"][0]["id"]
|
||
m.set_renderer("cesium", pane_id=pane_id)
|
||
assert m.get_renderer() == "cesium"
|
||
```
|
||
|
||
Renderer choices are `"maplibre"` and `"cesium"`. Omitting `pane_id` targets the
|
||
primary map. Grid dimensions are 1–4; `view_kinds` contains one renderer per
|
||
pane, primary first. Existing pane IDs, cameras, and visibility overrides survive
|
||
layout resizing. Save the project normally to preserve `primaryRenderer` and
|
||
each secondary pane's `viewKind`. `DashMap(renderer="cesium")` selects the same
|
||
initial renderer; Dash callbacks can update these fields through `project`.
|