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
39 changes: 38 additions & 1 deletion python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,46 @@ m.to_project()["mapView"]["center"]
| `set_center_zoom(lng, lat, zoom=None)` | Alias of `set_center` (leafmap compatibility). |
| `zoom_to_bounds(bounds)` / `zoom_to_layer(layer)` | Fit the view to bounds or a layer id/name/handle. |
| `layer_names` / `find_layer(name)` / `set_layer_visibility` / `set_layer_opacity` | Inspect and update layers conveniently. |
| `remove_layer(layer_id)` / `clear_layers()` | Remove layers. |
| `rename_layer` / `move_layer` / `duplicate_layer` / `show_layer` / `hide_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)` / `clear_layers()` | Remove one layer by id, name, or handle, or remove all layers. |
| `center` / `zoom` / `bearing` / `pitch` / `basemap` / `name` | Read persisted project and camera state; `name` is writable. |
| `set_zoom` / `set_bearing` / `set_pitch` / `fit_project_bounds` | Persist camera changes without requiring the widget to be displayed. |
| `to_project()` / `load_project(src)` / `save_project(path)` | Project I/O. |

Layer handles provide the same operations in an object-oriented form:

```python
m.add_geojson("https://example.com/roads.geojson", name="Roads")

roads = m.find_layer("Roads") # None when no layer has that name
roads.opacity = 0.6
roads.set_style(lineColor="#e63946", lineWidth=3)
roads.move(0)

print(roads.properties()) # sampled values for every property
print(roads.column("highway")) # one value per feature
roads_copy = roads.duplicate(name="Roads (proposed)")
```

For headless authoring and scripts that do not need a widget, commonly used
project utilities are available directly from the top-level package:

```python
from geolibre import (
basemap_catalog,
builtin_legend_names,
color_ramp_names,
describe_project,
load_project,
save_project,
)

project = load_project("my-map.geolibre.json")
print(describe_project(project))
save_project("copy.geolibre.json", project)
Comment thread
giswqs marked this conversation as resolved.
```

## Notes

- The bundled app is served from a localhost HTTP server, so the interactive
Expand Down
21 changes: 20 additions & 1 deletion python/src/geolibre/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@

from typing import Any

from .authoring import (
basemap_catalog,
color_ramp_names,
describe_project,
load_project,
save_project,
)
from .geolibre import Feature, Layer, Map
from .legends import builtin_legend_names

__version__ = "2.5.0"
__all__ = ["Feature", "Layer", "Map", "__version__"]
__all__ = [
"Feature",
"Layer",
"Map",
"__version__",
"basemap_catalog",
"builtin_legend_names",
"color_ramp_names",
"describe_project",
"load_project",
"save_project",
]
Comment thread
giswqs marked this conversation as resolved.


def _jupyter_server_extension_points() -> list[dict[str, str]]:
Expand Down
15 changes: 12 additions & 3 deletions python/src/geolibre/authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ def layer_summary(layer: dict[str, Any]) -> dict[str, Any]:
"""Summarize one layer for display, omitting any inlined data.

A GeoJSON layer's ``geojson`` blob can be tens of megabytes, so it is
reported as a feature count rather than echoed back.
reported as a feature count rather than echoed back. The source URL is
reported with its credentials stripped: a summary exists to be shown, and
both callers show it somewhere untrusted (a notebook cell that gets
committed, an MCP tool result that goes to a model client).

Args:
layer: A layer dict.
Expand All @@ -239,7 +242,7 @@ def layer_summary(layer: dict[str, Any]) -> dict[str, Any]:
if isinstance(source, dict):
url = source.get("url") or (source.get("tiles") or [None])[0]
if url:
summary["source"] = url
summary["source"] = _project.redact_url(str(url))
geojson = layer.get("geojson")
if isinstance(geojson, dict):
features = geojson.get("features")
Expand All @@ -257,6 +260,9 @@ def layer_summary(layer: dict[str, Any]) -> dict[str, Any]:
def describe_project(project: dict[str, Any]) -> dict[str, Any]:
"""Summarize a project: its camera, basemap, layers, and map controls.

URLs come back with their credentials stripped, as in :func:`layer_summary`;
several basemap providers put an API key in the style URL itself.

Args:
project: The project dict.

Expand All @@ -279,11 +285,14 @@ def describe_project(project: dict[str, Any]) -> dict[str, Any]:
components = settings.get(_project.COMPONENTS_PLUGIN_ID)
if isinstance(components, dict):
controls.extend(key for key in ("legend", "colorbar") if key in components)
basemap_url = project.get("basemapStyleUrl")
return {
"name": project.get("name"),
"version": project.get("version"),
"mapView": project.get("mapView"),
"basemapStyleUrl": project.get("basemapStyleUrl"),
"basemapStyleUrl": (
_project.redact_url(str(basemap_url)) if basemap_url is not None else basemap_url
),
"layerCount": len(layers_of(project)),
"layers": [layer_summary(layer) for layer in layers_of(project) if isinstance(layer, dict)],
"mapControls": controls,
Expand Down
Loading
Loading