mirror of
https://github.com/opengeos/GeoLibre.git
synced 2026-11-04 10:43:36 +00:00
bf47a7a766f1a447b41e17e7d26ae00fbd3fccec
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bf47a7a766 |
feat: draw WMS tiles from servers without EPSG:3857 in the desktop app (#2562)
* feat(desktop): draw WMS tiles from servers without EPSG:3857 When a WMS tile template names a geographic CRS (EPSG:4326, EPSG:4258, EPSG:6706, CRS:84), the desktop geolibre-wms protocol requests the tile's lon/lat extent in that CRS and redraws the latitude-linear image into Web Mercator in horizontal strips, as the KML Super-Overlay protocol does. Requests in EPSG:3857 are fetched unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(python): crs option for WMS layers wms_layer, Map.add_wms and the add_ogc_layer MCP tool accept crs, written into the GetMap template in place of EPSG:3857 while the bbox placeholder stays. Only Web Mercator and the geographic CRSs the desktop can redraw are accepted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(python): reject crs for WMTS; cross-reference the CRS allowlists Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(desktop): clamp latitude in the WMS strip redraw Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(desktop): skip degenerate extents in the WMS strip redraw Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(desktop): read any WMS 1.3.x as latitude first; name the server error when a geographic tile is not an image Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(python): replace a CRS already in the WMS endpoint; document desktop-only geographic CRS Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(desktop): cover the non-image path of the geographic WMS redraw Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(python): replace every GetMap key already in a WMS endpoint Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(python): require WMS 1.3.0 for crs='CRS:84' Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: decode endpoint key names before replacing them; trim the tile CRS Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test: keep the desktop and Python WMS CRS allowlists equal Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
dcef224bdc |
feat(popup): configurable popup width and image height (#2537)
* feat(popup): configurable popup width and image height Discussion #2304 asked for a `popup_max_width=` on `add_markers` and a way to make the pictures in a popup bigger. Both sizes were hard-coded: the popup root capped at 520px (420px once it carried a picture) and an image field's thumbnail at `min(50vh, 420px)`. `LayerPopupConfig` gains `maxWidth` and `imageHeight`, both in CSS pixels. `resolvePopupMaxWidth` / `resolvePopupImageHeight` clamp them the way the rest of the popup path treats untrusted project JSON, the renderer applies the width as inline style (so it beats the Tailwind cap and the wider image-popup rule) and widens the MapLibre shell to match, and publishes the height as `--geolibre-popup-image-height` for `.geolibre-popup-image` to read. The 2D, Mapbox and Cesium click popups all honor it; the grouped all-layer popup takes the widest width any hit layer asked for. The Style panel's Popup section gets both controls, and Python gets `max_width=` / `image_height=` on `popup_config`, `set_popup` and the MCP `set_layer_popup` tool, plus `popup_max_width=` / `popup_image_height=` shorthands that every `add_*` method accepts. Python raises on an out-of-range size rather than writing one the app would silently clamp. * fix(popup): address review on the popup size settings Three things the automated review caught: - A text-only popup was pinned to the configured width, not capped by it. The inline `width` exists to override the fixed `width: min(420px, …)` the `:has(.geolibre-popup-image)` rule sets, and to stop a `width: 100%` image shrinking to the text beside it -- neither applies without a picture, where the popup shrank to its content before this branch. `applyPopupWidth` now sets `width` only when the root carries an image, so it runs after the content is in place in both popup builders. - The size placeholders read "Default (520)" / "Default (420)", but the width default is 420px once the popup carries a picture and the image default is `min(50vh, 420px)`. Both now read "Default" in all 19 catalogs; the numbers live in the docs, where the two cases can be spelled out. - `popup_max_width=` did not fully win over a `max_width` key inside `popup=`: the mapping's value was still range-checked first, so an out-of-range one raised even though a valid argument was about to overwrite it. The mapping's copy is now dropped before validation, as an inline `tooltip` key already is. * fix(popup): surface an infinite size as the documented ValueError int(float("inf")) raises OverflowError, which neither numeric guard caught, so an infinite max_width, image_height or decimals escaped as an internal error instead of the ValueError each one documents. The decimals guard my helper was modeled on had the same hole, so both are fixed together. * fix(popup): let a text-only globe popup shrink to its content The globe's popup box took the configured width as its `width` whether or not it carried a picture, which is the behavior just corrected for the 2D and Mapbox popups -- a short text-only feature would have rendered as a mostly empty box at the author's width. It now mirrors `applyPopupWidth`: `width` is pinned only for a box with an image, and a text-only box keeps `width: auto` under the configured ceiling. |
||
|
|
ccfbc730db |
docs: add rendering engines guide and renderer references (#2471)
* docs: add rendering engines guide and renderer references GeoLibre now draws projects with MapLibre, Mapbox, Cesium, and ArcGIS, but the docs still described a MapLibre-plus-Cesium setup. Add a user guide for choosing and mixing engines, MapLibre and Cesium renderer references, and update the existing pages to match. * Address review feedback - Correct Cesium credential docs: the web version bundles a demo ion token, while desktop and mobile require the user's own token. - Restore the tutorial "limitations" bullet to an actual limitation (Claude review), in both the English and Chinese tutorials. |
||
|
|
da2d636f9c |
feat(cesium): native KML, terrain profiles, and control events (#2356)
* 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.
|
||
|
|
9d1599e67a |
feat(cesium): support CZML dynamic 3D scenes on the globe (#2350)
* feat(cesium): support CZML dynamic 3D scenes on the globe * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(cesium): resolve TypeScript typings, i18n keys, and test environments for CZML * Address Claude review feedback - Parse a serialized inline CZML document before handing it to CzmlDataSource.load, which treats a string as a URL to fetch; an unparseable string now surfaces as a layer error instead of a bogus fetch. - Store the inline document once under source.czmlData; the czml key is only read as a legacy fallback, so writing both doubled project size. - Drop the unused CzmlDataSource type import. - Let the first CZML document with a clock packet own the viewer clock; later CZML layers no longer reset the Time Slider's position or each other's interval, and ownership is released when that layer is removed. - Use the logical me-2 utility on the file-picker icon so it mirrors in RTL locales like the other Add Data sources. - Ship the Map.add_czml method and the add_czml_layer MCP tool the PR description promised, with tests and docs (python.md, mcp.md, agent skill references). * Address Claude review feedback - Hide the tileset symbology and quick-filter sections of the Style panel for CZML scenes: the globe sync only toggles their visibility, so those controls were silent no-ops; the footer now reads "czml" rather than "3d-tiles". - Drop metadata.customLayerType from the Python czml_layer builder so it matches createCzmlLayer and the Layer Library treats CZML layers the same regardless of which API authored them. * Address review feedback - Elect the CZML clock owner deterministically: the first layer in synced order whose loaded document carries a clock packet, re-elected (and its clock re-applied without a reload) when the owner is removed. Tests cover out-of-order loads and removal with an already-loaded fallback. - Let the add_czml_layer MCP tool accept a single packet dict as well as a packet list, matching Map.add_czml and czml_layer. - Treat an empty packet array as no document in czmlSource, and reject empty data in the Python builder, so a layer can never look ready with nothing to load. - Wrap the Add Data quick pick in the same error handling as the form submit and clone the sample packets so layers never share the exported constant. - Drop the unused czml option alias from createCzmlLayer. * Address review feedback - Elect the CZML clock owner only once the data source has joined the scene, so a rejected add never leaves a non-rendering layer driving the viewer clock; regression test added. - Skip writing an empty czmlData alongside a URL in the Python builder. * Address review feedback - Re-elect the CZML clock owner at the end of sync(), so reordering two already-loaded clocked documents hands the clock to the new first one; test extended. - Drop the never-written source.czml and metadata.czmlUrl fallbacks from czmlSource/createCzmlLayer; nothing produces either key. * Address Claude review feedback - Wrap a bare packet object in czmlSource() the way parseCzml does, so a single-packet document from the Python API or MCP tool reaches Cesium as an array instead of rendering nothing; test added. - Document that the viewer clock stays where the last CZML document left it once no CZML layer remains, since the Time Slider owns time then. * Address Claude review feedback - Compare the raw source.czmlData reference in needsRebuild instead of czmlSource().data, which wraps a bare packet in a fresh array per call and would have rebuilt such a layer on every unrelated sync; test asserts a spread layer does not reload. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: giswqs <giswqs@gmail.com> |
||
|
|
e1a003d576 |
feat(map): Cesium Ion assets by id on the globe (#2321)
* feat(map): Cesium Ion assets by id on the globe A 3D Tiles tileset or an imagery layer can now be referenced by its Cesium Ion asset id (issue #2290). The globe resolves the asset through an IonResource / IonImageryProvider with the app's Ion token, read at load time so a token added in Settings reaches the next sync; a missing token is a layer error rather than a silent blank. - `@geolibre/core` `cesium-ion.ts`: `createCesiumIonLayer`, the asset-id parser, `isCesiumOnlyLayer`, and the one-click picks (Cesium OSM Buildings, Bing Aerial through Ion). - Add Data → 3D → Cesium Ion Asset: asset id, tileset/imagery, altitude offset, quick picks, and a token hint; enabled when the globe is primary. - The Layers panel and the pane layer menus badge these layers "3D only" on the 2D map, the mirror of the existing "2D only" badge. - Python `add_3d_tiles(ion_asset_id=…)` / `add_cesium_ion(...)`, the MCP `add_3d_tiles_layer(ion_asset_id=…)` / `add_cesium_ion_layer` tools, and the docs and agent skill references. Refs #2290, #2259. * style: auto-format (ruff + oxfmt) [pre-commit.ci] * test(python): assert Ion asset ids survive the MCP save; document Ion imagery in the format table * fix: Ion asset follow-ups - three_d_tiles_layer rejects request_headers together with ion_asset_id instead of dropping them silently. - An Ion asset id counts as a restorable source, so Ion layers can be saved to My Data. - The Add Data form reports an unparseable altitude offset instead of coercing it to zero. * fix(core): name the Bing Aerial Ion asset for what it is; parse restorable Ion ids the globe's way * fix(core): an Ion asset id is restorable only under the cesium-ion source kind --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
7af7c17104 |
feat(python): popup/tooltip authoring and marker symbology arguments (#2306)
* feat(python): popup/tooltip authoring and marker symbology args The app has had a popup and tooltip designer (per-field labels, kinds, formats, title/body expressions, hover tips) and marker symbology (shape, color, size, custom SVG) since #2113, but none of it was reachable from the Python API. `add_markers(**style)` merged everything into the layer's `style` dict, and the popup config is a top-level layer key, so a notebook had no way to author one at all. Reported in discussion #2304 by someone porting popups with images and links over from another library. - `project.py`: `popup_field`, `popup_config`, `apply_tooltip`, `normalize_popup`, `marker_style`, and `normalize_hex_color` (a mirror of the sprite baker's own color check). `_layer_base` now lifts `popup=` and `tooltip=` out of the style overrides and onto the layer, so every `add_*` builder accepts them without threading two more arguments through each signature. - `authoring.py`: `set_popup` / `clear_popup`, so the widget and the MCP server share one implementation. - `Map.set_popup` / `set_tooltip` / `clear_popup`, `Layer.popup` and the same three methods on the handle, plus named marker arguments (`color`, `opacity`, `radius`, `stroke_color`, `stroke_width`, `shape`, `size`, `icon`) on `add_marker` and `add_markers`. - MCP: `set_layer_popup`. Two failure modes are raised rather than left to fail silently in the browser: a marker sprite with a non-hex color (the baker rejects it and draws the default blue), and a `tooltip=True` on a popup with no fields and no title (the tooltip element would never render). Docs, the agent skill's MCP and Python references, and tests across the four suites. * fix(python): name what a popup accepts when the value is not a field spec * Address review feedback - marker_style: raise when `icon` is given alongside an explicit non-custom `shape`. The app reads `markerSvg` only for `markerShape == "custom"`, so honoring the icon silently discarded the shape the caller asked for. - Move the "this tooltip could never render" guard out of the `tooltip=True` branch and onto the finished config (`_assert_tooltip_can_render`), so it also catches `hover=True` passed directly to `popup_config`, `set_popup`, or a `popup={"hover": True}` mapping, and a merge that leaves `hover` on with no flagged field left. - `tooltip=[]` now means the same as `tooltip=False` rather than storing a hover-enabled tooltip with no fields. The MCP tool's own empty-list mapping becomes redundant and is dropped. - Derive the accepted-key list in the unknown-popup-key error from `_POPUP_CONFIG_KEYS` instead of hand-writing it, so it cannot drift. - Docs: `add_ee_layer` has a fixed signature and takes no style overrides, so it does not accept `popup=`/`tooltip=`. Narrow the claim in `docs/python.md` and the agent skill's Python reference. Six new tests cover the icon/shape conflict, both routes to a dead tooltip, and the empty tooltip sequence. * Address review feedback - marker_style: reject the circle-only settings when shape/size/icon selects a marker sprite. Verified in layer-sync.ts that a sprite layer removes the circle layer outright and paints only icon-opacity from the layer opacity, and in markers.ts that drawBuiltinMarker strokes a fixed white halo -- so opacity, radius, stroke_color and stroke_width were not merely overridden, they were unreachable. - add_marker / add_markers: apply the named marker arguments with setdefault rather than update, so a raw style key passed alongside one still wins. This restores the precedence add_circle_markers had before this PR, where `circleRadius` in **style beat `radius=`. - Reword the ambiguous "change one later" sentence in the agent skill's Python reference. It was not a typo for "layer" (these change a popup, not a layer), but it read as one, so it now says what it means. - Document both rules in docs/python.md and the skill reference. Five new tests: the sprite guard from both entry points, and the style-key precedence including the add_circle_markers case. * Address review feedback - apply_tooltip: wrap the `list(tooltip)` coercion in the same try/except TypeError guard that popup_config already uses for `fields`, so a public-API typo like `tooltip=1` raises a ValueError naming what tooltip accepts instead of surfacing "'int' object is not iterable" to a notebook. The two near-identical coercions in this module now handle errors the same way. * Address review feedback - _assert_tooltip_can_render: a tooltip whose only flagged fields are `image` now raises too. resolvePopupRows drops image rows from the hover subset (the value is a URL that would become the whole tip), so flagging only an image left exactly the dead tooltip this guard exists to prevent. - Map.set_popup / Map.clear_popup delegate to authoring.set_popup / authoring.clear_popup instead of re-deriving the merge, matching what authoring.py's module docstring promises and what remove_layer and add_swipe already do. The merge semantics now live in one place. - Document that the tooltip and the click popup share one field list, so `tooltip="name"` on a popup that configured no fields narrows the click popup to `name` rather than leaving it showing everything. This is a constraint of the app's LayerPopupConfig schema (resolvePopupRows only falls back to "all visible properties" while `fields` is empty), not something this API can paper over, so it is now stated in docs/python.md, the agent skill, and the docstrings of set_popup, set_tooltip and apply_tooltip, with the two-line workaround. Three new tests. * Address review feedback - popup_field: reject a fractional or non-numeric `decimals` instead of truncating it. `decimals=2.9` silently became 2, formatting to a precision the caller never asked for, and the range check below could not catch it. An integral float (2.0) is still accepted. - The nested `format` block now rejects `hover` too. It is a field-level flag rather than a format concern, and accepting it there was a hole in the reject-what-you-do-not-recognize rule the rest of the module follows. Three new tests. |
||
|
|
5ec567d878 |
Add Cesium drawing, capture, and plugin lifecycle support (#2302)
* feat: support Cesium drawing capture and plugin lifecycle * feat: add keyless and COG-backed Cesium terrain * feat: expose Cesium renderer and mixed pane authoring APIs * Address review feedback - DesktopShell: dispatch plugin URL parameters before the Cesium early return so Cesium-capable plugins still receive them on the globe. - PrintLayoutDialog: gate the post-capture extent-box restore on request identity so a superseded capture cannot re-show it mid-capture; drop the redundant nested `if (map)`. - print-extent: keep the final Cesium extent preview after a successful draw and hand its disposer to the dialog, which now mirrors the MapLibre box lifecycle on the globe (re-show on open, hide during capture, clear on close/clear/unmount). - cesium-layer-sync: use the `tilesLoaded` flag (not the `allTilesLoaded` Event) for 3D Tiles readiness; turn the no-bounds, unsupported-WMTS and no-tile-URL early returns into load errors and report globe-supported kinds with an unusable source instead of leaving them pending forever. - extent-drawing/map-engine/cesium-engine: define MapExtent as unwrapped (west < east, east may exceed 180 across the antimeridian, MapLibre's convention); Cesium view bounds unwrap and the globe preview re-wraps. - map-controller: reuse suspendNavigation() in drawExtent so touchPitch is suspended while drawing. - plugin-manager: every scoped UI registration (map controls, toolbar menus, right panels) now requires the current activation generation and a live activation; deactivation advances the generation. Regression test for a retained state scope registering after deactivation. * Address CodeRabbit review feedback - useEmbedApi: hold the `ready` event until the engine for the current primary renderer is live, so a renderer hand-off (which bumps the readiness generation before the destination engine publishes) no longer advertises a bridge whose commands would answer "not ready". - authoring/geolibre: validate `secondaryMapViews` through one helper (a list of pane objects with a string id) so set_renderer, get_renderer and set_map_layout raise ValueError on hand-edited projects instead of KeyError or slicing errors; regression test via Map.load_project. * Address Claude review feedback - cesium-layer-sync: "2D only" layer kinds are skipped by getRenderStatus instead of reported as errors, so a mixed project no longer makes every globe capture, export, and readiness check fail. - cesium-engine: project() clamps a corner that sits behind the camera into the visible rectangle instead of throwing, so an extent drawn before a pan/tilt still crops to the canvas edge like MapLibre. - ControlsMenu: dispatch TERRAIN_SETTINGS_EVENT from @geolibre/map rather than a duplicated string literal. - isFullViewportMapCanvas lives once in @geolibre/map; the app copy in print-capture.ts is removed and its callers and test import the package. * Address CodeRabbit review feedback - authoring: secondary_panes() also rejects a present viewKind that is not maplibre or cesium (an omitted one still means maplibre), so get_renderer cannot return None or an unknown renderer and set_map_layout cannot keep one; regression test for omitted, valid and invalid values. * Address CodeRabbit review feedback - authoring: secondary_panes() rejects duplicate pane ids, since pane lookups take the first match and would silently read or update the wrong pane. - authoring: renderer values (viewKind, view_kinds entries, renderer) are type-checked before set membership, so a JSON array or object raises the documented ValueError instead of TypeError; regression cases added. * fix(ci): load isFullViewportMapCanvas without the @geolibre/map barrel The recorder and print export imported the helper from the package barrel, which pulls in MapLibre's stylesheet and fails under node's test runner (tests/map-recorder.test.ts). Expose it through a `@geolibre/map/map-capture` subpath (dev and publish exports, plus the tsdown entry) like the other node-safe modules, and import that instead. * fix(map): drop unrelated dependency changes swept into the CI fix The previous commit staged the whole package.json and picked up a cog-tiler-wasm dependency and a React peer-range bump from unrelated local work, without the matching lockfile entry. Revert those two hunks; the map-capture subpath entries stay. |
||
|
|
b0158bfae3 |
feat: add heatmap palette and weighting controls (#2241)
* feat: add heatmap palette and weighting controls
Let point heatmaps use a selectable color ramp and a numeric feature field as density weight. Keep legends and saved layer styles aligned with those choices.
* Address Claude review feedback
- Preserve stale heatmap weight fields in the selector so UI and rendering agree.
- Mirror heatmap palette and weighting settings in the Python API.
* Address automated review feedback
- Hide unsupported heatmap controls on control-rendered layers.
- Match the legend to the transparent density-zero stop.
- Remove stale documentation and update Python API references.
* Address Claude review feedback
- Recover GeoLibre heatmap ramps and weight fields from imported Mapbox styles.
- Warn when third-party heatmap expressions cannot be represented.
* Address Claude review feedback
- share the heatmap ramp expression builder with style import
- reuse the numeric field selector for heatmap weighting
* Address CodeRabbit review feedback
- import direct heatmap weight property expressions
- assert supported heatmap round trips do not warn
* Address automated review feedback
- guard malformed imported heatmap weight expressions
- use the configured heatmap default for invalid ramp names
* Address Claude review feedback
- Resolve the heatmap ramp through one shared `heatmapRampColors()` helper in
`@geolibre/core`, called by both `heatmapPaint()` (`@geolibre/map`) and the
auto-legend gradient. `getVectorColorRamp` falls back to the first ramp
("viridis") for an unknown name while the map fell back to the default
("turbo"), so an out-of-catalog `heatmapColorRamp` — a renamed ramp in a
stale project, or a hand-edited `.geolibre.json` — made the legend gradient
and the rendered heatmap disagree.
- Cover the mismatch with an auto-legend regression test asserting an unknown
ramp name yields the default ramp's colors in the legend.
Claude-Session: https://claude.ai/code/session_012zpXBAj8WPtxokSMCi2BU4
|
||
|
|
308a700925 |
feat(python): accept optional bounds on WMS and WMTS layers (#2244)
* feat(python): accept optional bounds on WMS and WMTS layers A service layer carries no geometry, so the app has nothing to derive its extent from: every WMS or WMTS layer written by wms_layer, wmts_layer or add_ogc_layer lands without source.bounds, and zoom-to-layer cannot reach it. tile_layer already takes an optional bounds and writes it only when given. Give wms_layer and wmts_layer the same parameter, and expose it on the MCP tool that builds both. Omitting bounds leaves the written source byte-identical to before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * fix(python): refuse OGC bounds that are not four values Review on #2244: a truthiness check dropped bounds=[] without a word, and a malformed list reached the project file as an extent the app cannot use. Validate the length and coerce to float, as new_project already does for center. tile_layer keeps its existing lenient behavior: it predates this and giving it a new exception path is not this PR's business. Also covers the WMTS branch of add_ogc_layer, which the first test missed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * docs(python): keep **style under Args in the wms_layer docstring The Raises: section landed before the **style line, which pushed it out of Args: and into Raises:, where a Google-style block expects an exception type. Moved after Returns:, as wmts_layer already had it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * test(python): cover the wmts float coercion path too wms_layer and wmts_layer validate and coerce bounds separately, so a test naming only one of them would miss a regression in the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * feat(python): expose bounds on Map.add_wms and Map.add_wmts Review on #2244 caught two things. The validate-and-coerce block was duplicated verbatim in wms_layer and wmts_layer; it is now one _resolve_bounds helper, alongside the existing _normalize_wms_version. It also reports a non-numeric value as a bounds error rather than letting float() raise its own. The widget API was left behind: Map.add_tile_layer already forwards bounds to tile_layer, but Map.add_wms and Map.add_wmts did not, so a notebook building the same multi-service map had no way to set an extent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * fix(python): refuse non-finite bounds float() takes nan and inf without complaint, so _resolve_bounds could store an extent the app cannot fit to. Map.fit_bounds already refuses them; the same check now guards the layer builders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * fix(python): check bounds latitudes, and catch OverflowError Review on #2244 asked why _resolve_bounds stops at finiteness when the sibling fit_bounds go further. Two of those checks belong here: - south > north is the axis-order mixup the docstrings warn about, and latitudes have no wraparound to excuse it; - a latitude outside +/-90 is not a coordinate. West > east stays legal. RFC 7946 section 5.2 writes an antimeridian box that way, and authoring.fit_bounds frames one rather than refusing it. float(10**1000) raises OverflowError, which the coercion did not catch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * docs(python): document what add_ogc_layer raises The other four entry points gained a Raises: section for malformed bounds and this one did not, though it is also the tool description an agent reads before calling it. Covers the two pre-existing errors as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * fix(python): measure bounds after coercion, not before len() on the raw argument raises TypeError for an iterable that is not sized, and _reports_its_errors only restates ValueError - so through the MCP server the agent would get "Error executing tool" with no sentence to act on, against the package's stated invariant that every validation failure is a ValueError. Building the list first works for any iterable and leaves the existing messages unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH * docs(python): name the right capabilities element for WMTS EX_GeographicBoundingBox is a WMS element; a WMTS capabilities document carries the extent as ows:WGS84BoundingBox. add_ogc_layer serves both, so pointing at the WMS field alone sent WMTS callers to something absent. Verified against a live WMTS (USGS basemap), which advertises WGS84BoundingBox and no EX_GeographicBoundingBox. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uQhcknLWhSS4pGKt6kYeH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3b54b06add |
fix: proxy local raster URLs in Colab (#2108)
* fix: proxy local raster URLs in Colab Route session-scoped raster files through Colab's kernel proxy so xarray layers can load remotely. Always strip outputs from every tracked notebook during pre-commit runs. * Address Claude review feedback - Support local raster rewriting through jupyter-server-proxy as well as Colab. - Rewrite sourcePath independently when it references a registered local file. - Preserve git stderr when notebook discovery fails. * Address CodeRabbit review feedback - Terminate nbstripout option parsing before passing tracked notebook paths. * fix: rebase stale Colab raster proxy URLs Colab gives separate widget views distinct proxy origins. Rebase tokenized raster URLs from an older view onto the current view before restoring layers. * fix(python): preserve raster range requests through Colab proxy * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(python): tunnel partial raster responses through Colab * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(python): render local rasters as XYZ tiles in Colab * Address review feedback - _server.py: gate the Content-Range/X-GeoLibre-Content-Range headers on a newly parsed range rather than on the presence of a Range header, so a non-bytes unit (e.g. "items=0-1") no longer emits a spec-invalid Content-Range alongside a full-file 200. - _server.py: send Access-Control-Expose-Headers for X-GeoLibre-Content-Range, without which a cross-origin fetch cannot read it and the app leaves the response at 200 instead of rebuilding the 206. - _frontend.js: require an empty port on the Colab proxy hostname, so a look-alike host on a foreign network port does not enter the rewrite path. - _frontend.js: extract the transport gate into an exported canProxyLocalFiles(base, port) and cover it in tests, so a future proxyBase/resolveBase refactor cannot silently stop the rewrite. - geolibre.py: resolve the local raster path once and hand the resolved path to both register_raster_tiles and rasterio.open — GDAL does not expand "~", so a "~/dem.tif" source silently lost its computed bounds. - kernel-proxy-range.ts: export KERNEL_CONTENT_RANGE_HEADER and cross-reference the three mirrored literals from _frontend.js and _server.py; a new test asserts both Python sources against the exported constants. - python-api.md: fix the duplicated "JupyterHub when" clause, matching the wording in docs/python.md. - tests: cover the malformed-range 200, the exposed range header, and both the populated and None bounds paths of add_cog's Colab branch. * Address Claude review feedback - add_cog: the `url:` docstring still claimed a local raster is only reachable when the browser runs on the kernel's host, which this PR made stale. Restate it the way docs/python.md now does: direct reads in local Jupyter/VS Code, kernel-rendered PNG XYZ tiles in Colab, the kernel port via jupyter-server-proxy on JupyterHub, and a hosted URL for an extension-only deployment. * Address Claude review feedback - Remove the Colab range-proxy bridge, which the PNG-tile path in |
||
|
|
b9a720d630 |
feat: support xarray raster layers in Python API (#2105)
* feat: support xarray raster layers in Python API Allow DataArray and Dataset inputs to be materialized as session-scoped COGs so they render directly in the browser. Include optional raster dependencies, focused tests, and a runnable notebook example. * style: auto-format (ruff + oxfmt) [pre-commit.ci] * Address CodeRabbit review feedback - Document raster dependencies included by the all extra. - Apply Dataset nodata metadata to each DataArray variable before COG export. * Address Claude review feedback - add stacklevel=2 to the "array_args is ignored" warning so it is attributed to the caller, matching every other warnings.warn in geolibre.py - register a weakref.finalize safety net for the GeoTIFFs materialized from xarray objects, so they are removed when the Map is collected and at interpreter exit rather than only on an explicit Map.close(); close() now clears the shared list in place so the finalizer stays valid afterwards, and the lifetime is documented in add_raster's docstring and docs/python.md - add a `vector` extra (geopandas + shapely) so environments that only need GeoDataFrame/local-vector support can opt out of the rasterio/GDAL stack that `all` now pulls in; document the split in docs/python.md and python/README.md - cover the finalizer cleanup path in python/tests/test_map.py * Address CodeRabbit review feedback - make the fake xarray Dataset.copy() return an independent Dataset (a new mapping over the same variables, as xarray's shallow copy does) instead of self, and assert the GeoTIFF is written from the copy rather than the caller's Dataset — removing the production data.copy() now fails the test * Address Claude review feedback - keep `add_raster(url=...)` working: the first parameter's rename to `source` broke that keyword for a published API, so `url` is back as a keyword-only deprecated alias that emits a DeprecationWarning, raises if combined with `source`, and still raises a clear TypeError when neither is given - carry the xarray behavior into skills/geolibre: the agent reference now documents `array_args` (variable, isel, x_dim/y_dim, crs, nodata, plus the rio.to_raster passthrough), the EPSG:4326-only-for-lon/lat rule and the errors raised otherwise, and the temp-file/close() cleanup caveat - cover the deprecated alias and both TypeErrors in python/tests/test_map.py * Address Claude review feedback - add `_server.unregister_local_file` and call it when a materialized GeoTIFF is removed: every xarray materialization writes to a fresh mkstemp path, so the token registry (which register_local_file scans linearly to dedup) grew one dead entry per add_raster call for the life of the kernel - pin the Dataset nodata path against real rioxarray: a round-trip test opens the written COG with rasterio and asserts the CRS, band count and nodata survive the copy + per-variable reassignment, which the fake xarray modules cannot model * Address CodeRabbit review feedback - note in the agent reference that `url=` remains a deprecated keyword alias of `add_raster`'s `source`, so an existing caller reading only the skill does not think the keyword was dropped - say that `driver` only defaults to "COG" and that overriding it through array_args really does change the written format, with the reason the default matters (the app range-reads a COG; a plain GeoTIFF is converted in the browser first) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
ba96ccebf4 |
feat: add Earth Engine layers to Python API (#2085)
* feat: add Earth Engine layers to Python API
Expose a geemap-style add_ee_layer method that turns authenticated Earth Engine objects into restorable raster tile layers. Document supported objects and cover image, collection, vector, validation, and error paths.
* Address review feedback
- add_ee_layer: classify Earth Engine types before the duck-typed `getMapId`
fallback. `ee.ImageCollection`, `ee.FeatureCollection` and `ee.Feature` all
expose `getMapId`, so real objects skipped the mosaic/style step entirely —
collections were never mosaicked and vector styling (`width`, `fillColor`,
`pointSize`) was dropped, since `FeatureCollection.getMapId` honours only
`color`. The `getMapId` duck-type is now the fallback for non-`ee` objects.
- Update the ImageCollection/FeatureCollection test fakes to expose `getMapId`
(asserting it is never called) so the dispatch order is guarded, and add a
test for the unsupported-type TypeError.
- Include the original error text in the RuntimeError raised when
`getMapId()` fails, so quota/vis_params failures are not reported as auth
problems, and document `RuntimeError` in the `Raises` section.
- python/README.md: note that `add_ee_layer` needs `earthengine-api`, call
`ee.Authenticate()` in the quickstart, and mention map-id expiry.
- docs/python.md: sync the docs-site copy — add the `add_ee_layer` row and the
Earth Engine quickstart example with the map-id expiry caveat.
- skills/geolibre/references/python-api.md: document that the stored tile URL
is tied to an expiring Earth Engine map id.
* Address Claude review feedback
- Validate vector `vis_params` against the keys `ee.FeatureCollection.style()`
actually declares (it takes explicit keywords, not `**kwargs`), so an
image-shaped `{"min": ..., "max": ...}` on a FeatureCollection raises a
ValueError naming the accepted keys instead of a raw
`TypeError: style() got an unexpected keyword argument 'min'` that collides
with the documented meaning of TypeError.
- Wrap `mosaic()`/`style()` in the same failure handling as `getMapId()`, so a
preparation failure surfaces as the documented RuntimeError; document the
vector key set on `vis_params` and widen the Raises entries.
- Move the Earth Engine snippet out of the primary quickstart block in
python/README.md and docs/python.md into its own clearly-optional example, so
copy-pasting the quickstart no longer runs a blocking `ee.Authenticate()` or
requires earthengine-api.
- Note the vector-only style keys in the agent skill reference.
- Tests for both new paths (rejected image vis_params, wrapped mosaic failure).
* Address review feedback
- Test the ee.Feature and ee.Geometry conversion chain in add_ee_layer: a
Geometry is wrapped into a Feature and a Feature into a single-element
FeatureCollection before style() runs, and the styled image is fetched with
empty map params. Those two branches were previously unexercised.
* Address Claude review feedback
- Validate that `vis_params` is a mapping, so a list or string raises the
method's own TypeError instead of a raw `dict()` error.
- Document that `add_ee_layer` produces a plain raster tile layer, not one of
the live layers the app's Earth Engine panel manages (that panel matches on
`metadata.sourceKind === "earth-engine-raster"`). This is intentional: the
Python path evaluates the object kernel-side into a static tile URL and has
no browser-side control to sync with.
* Address CodeRabbit review feedback
- Parameterize the non-mapping `vis_params` test over a list, a string, and an
int, so every shape the documented TypeError covers is exercised.
|
||
|
|
58d08f4ee7 |
feat: Encoded Polyline Encoder/Decoder Implementation (#2030)
* feat: Encoded Polyline Encoder/Decoder Implementation Implemented a comprehensive Google Encoded Polyline codec supporting standard precision 5 (Google, OSRM) and precision 6 (Valhalla, Mapbox), unescaping of backslashes, batch parsing with custom delimiters, interactive preview and coordinate inspector in Add Data UI, processing toolbox tools, layer export, and full Python package support with zero external dependencies. * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix: Sequence was used in the type annotation of add_polyline but was never imported Sequence was used in the type annotation of add_polyline() at line 1203, but was never imported. * fix(polyline): resolve review findings across codec, export, and processing tools for Encoding Polyline #2029 - PolylineSource: use translated previewInfo string and reset delimiters on sample select - tauri-io: deduplicate polyline decoding and validate coordinate ranges for precision detection - vector-export / UI: thread precision option and add distinct precision 5 & 6 export menu actions - core & python: prevent unsafe conversion of literal \n, \r, \t in unescapePolyline - processing: validate polyline ASCII character range and handle MultiLineString roundtrips symmetrically - python: import Sequence from collections.abc and align delta rounding to Math.round() behavior * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(polyline): localize UI labels and harden coordinate validation in processing and Python API #2029 - LayerPanel & AttributeTable: localize Encoded Polyline export options via dynamic precision catalog key - PolylineSource: localize sample route labels and metric distance units via t() - vector-tools: add bounds validation ([-180, 180], [-90, 90]) for decoded coordinates in decodePolylineTool - tests: add null assertion on decoded geometry and test for out-of-bounds polyline input - python/polyline: validate coordinate bounds in encode_polyline, check character byte range in decode_polyline, and default unescape to False * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(polyline): enhance decoder validation, geometry export restrictions, and UI formatting #2029 - packages/core: export `decodePolylineDetailed` to indicate whether polylines were fully consumed without truncation. - packages/processing: update `decodePolylineTool` to skip features with incomplete coordinate pairs using `decodePolylineDetailed`. - apps/geolibre-desktop: - Format distance preview using locale-invariant SI units (`m` / `km`). - Sanitize `baseProperties.source` to file basename instead of full local path. - Strict line-compatibility check in `layerSupportsPolylineExport` to reject mixed-geometry layers. - python: validate cumulative lat/lon bounds `[-90, 90]` and `[-180, 180]` in `decode_polyline`. - tests: add regression tests for truncated coordinate decoding, mixed geometry exports, and Python coordinate bounds. * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(polyline): reject overlong varint shifts and harden geometry export restrictions #2029 - packages/core: guard polyline varint decoding loops against shifts exceeding 30 bits or 32-bit payload overflows, returning incomplete status immediately. - apps/geolibre-desktop: ensure layerSupportsPolylineExport strictly requires all feature geometries to be line-compatible. - tests: add regression tests for the overlong "_______?" varint chunk in decodePolyline and decodePolylineTool. - python: add tests covering coordinate bounds validation on latitude and longitude axes. * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(polyline): correct sample escaped data and harden coordinate validation #2029 - apps/geolibre-desktop: - Update `sampleEscaped` sample polyline in `PolylineSource.tsx` with a valid string containing a properly escapable backslash varint sequence. - Harden `hasValidPolylineCoordinates` in `tauri-io.ts` to reject empty coordinate lists and enforce non-empty total point counts. - Document precision 5 vs 6 auto-detection heuristics and bounds handling during dropped-file ingestion. * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(polyline): enforce complete decoding in batch decoder and clean up imports #2029 - packages/core: require `complete === true` from `decodePolylineDetailed` in `batchDecodePolylines` before accepting features. - apps/geolibre-desktop: remove unused `Info` and `MapPin` imports from `PolylineSource.tsx`. - tests: add test verifying `batchDecodePolylines` skips lines with invalid characters or truncated varints. * style: auto-format (ruff + oxfmt) [pre-commit.ci] * Address review feedback - Fix the "Escaped backslashes (JSON string format)" sample, which failed to decode at all ("No valid polyline coordinates could be decoded"). The old value was not a real polyline: it carried one stray backslash that truncated a varint, and even ignoring that it decoded to coordinates off the planet (lat 130, lon 774). Replaced with a San Francisco -> Monterey route whose encoding genuinely contains a backslash, written doubled the way a JSON payload carries it, so the dialog's unescape step is what makes it decode. - Move SAMPLE_POLYLINES out of PolylineSource.tsx into a leaf module, lib/polyline-samples.ts, so the samples can be decoded in a test rather than only in the UI, and add tests/polyline-samples.test.ts covering every sample and the escaped one's with/without-unescape behaviour. - Document in parsePolylineFileLayers that the precision 5 -> 6 bounds check cannot resolve the ambiguous cases: the two decodes differ by exactly 10x, so in-bounds at precision 5 implies in-bounds at precision 6, and precision-6 data near the prime meridian/equator still imports 10x too large. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: giswqs <giswqs@gmail.com> |
||
|
|
dc23f9f0e0 |
docs(python): document marimo workaround (#1788)
* docs(python): document marimo workaround * docs(python): fetch marimo sample in browser * docs(python): address marimo review feedback |
||
|
|
7238e82b49 |
fix(python): trim Layer.source redaction cost and document save_project (#1772)
* Address Claude review feedback - Sweep only the source field in Layer.source. redact_layer copied an inlined geojson blob first and then discarded it, which .data pays for deliberately but .source has no reason to. - Document that the top-level save_project writes verbatim, credentials included, and why: it is the lossless primitive the MCP server round-trips a user's own file through, where redacting on every edit would strip their own API keys. Point at Map.save_project and redact_credentials for the sharing case. - Sync docs/python.md's Map API table with the methods this PR adds; it had drifted from the expanded table in python/README.md. * Address CodeRabbit review feedback Fix the remove_layer row in python/README.md rather than docs/python.md. The two tables disagreed and the README was the wrong one: the parameter is `layer_id`, so `remove_layer(layer=...)` raises TypeError. Both now match the signature, and the description already says it accepts an id, name, or handle. |
||
|
|
750a80c52e |
feat(python): add an MCP server for authoring GeoLibre projects (#1734)
* feat(python): add an MCP server for authoring GeoLibre projects
Adds `geolibre-mcp`, a headless stdio MCP server that writes real
`.geolibre.json` projects from an AI client. No browser, no running app,
and no bundled web build: it composes projects with the same builders the
Python package already uses, so anything it writes opens unchanged in the
desktop app, the web app, and the Jupyter widget.
Layering, so nothing is duplicated:
- `project.py` keeps *building* pieces (a layer, a plugin-state blob).
- New `authoring.py` *applies* them to a whole project: add/remove/reorder
and restyle layers, classify a choropleth, move the camera, compose the
legend/colorbar/swipe controls, and summarize a project back.
- `Map` now delegates its split-map, legend, colorbar, and choropleth
composition to `authoring.py` instead of holding its own copy, so the
widget and the MCP server cannot drift.
- `to_html` splits into a module-level `render_project_html()` the server
can call without a widget; `Map.to_html` delegates to it.
The server itself is `mcp/server.py` (21 tools), the only module that
imports the `mcp` SDK, kept behind the optional `geolibre[mcp]` extra.
`mcp/workspace.py` confines every read and write to roots given via
`--root` or `GEOLIBRE_MCP_ROOTS`, mirroring the sidecar's
`GEOLIBRE_CONVERSION_ROOTS`; symlinks out of a root are refused, writes
are limited to `.json`/`.html`, and an existing file needs `overwrite`.
Verified end to end: a client spawned the server over stdio, listed the
tools, and built a choropleth of 52 US states fetched from a live URL
plus COG, XYZ, legend, colorbar, and swipe layers; the resulting project
round-trips through the app's own `parseProject` with its symbology and
plugin state intact, and a workspace escape is refused over the wire.
`tests/test_mcp_server.py` skips itself without the SDK, so
publish-python.yml installs `mcp` explicitly rather than shipping the
server untested.
* test(python): cover every MCP tool, and fix add_vector_layer's documented render_mode
An audit of the tool surface found 6 of 21 tools with no test at the MCP
layer, and two more (add_tiles_layer, remove_layer) covered only on their
error paths. Some were covered one layer down in test_authoring.py, but
the argument plumbing in the tool wrappers was not.
Filling the gap turned up a real defect: add_vector_layer documented
render_mode as "geojson" or "vector-tiles", but project.vector_layer
accepts "geojson" or "tiles". A model following the docstring would have
hit a ValueError on every call. The docstring now names the real values,
and a test asserts the undocumented spelling is rejected, so the two
cannot drift again.
Adds happy-path coverage for every add_* tool (parametrized over the
builder each one plumbs, asserting the resulting layer type), plus
style_layer merge semantics, remove_layer, set_basemap, add_legend from
both a preset and paired lists, add_colorbar, and the legend/colorbar
coexistence in their shared settings blob.
* Address review feedback
- authoring.save_project: write to a temporary file in the destination's
directory and os.replace it into place, so an interrupted write cannot
truncate an existing project (the MCP edit path rewrites the whole file on
every tool call).
- authoring.column_values: GeoJSON permits `"properties": null`, which made
`.get(column)` raise AttributeError instead of the documented ValueError.
Guard with isinstance, matching layer_properties.
- mcp/workspace.Workspace: refuse an empty root set at construction. A
separators-only GEOLIBRE_MCP_ROOTS filtered down to no roots and every later
relative-path resolve raised IndexError instead of WorkspaceError.
- mcp/workspace.resolve_output: require a non-empty stem before the suffix, so
a bare dotfile named exactly `.json` or `.html` is refused as the comment
already claimed.
- mcp/server.edit: run the destination through the same extension allowlist
create_project writes through, and check the loaded JSON actually looks like
a GeoLibre project. load_project accepts any JSON object and seeds `layers`,
so a mutating tool could previously rewrite an unrelated `package.json`
inside a workspace root as a map.
- mcp/server.create_project: route the initial zoom through authoring.set_view
so it is clamped to [0, 24] like every other camera-mutating path.
- mcp/server.add_geojson_layer: resolve the destination before fetching or
reading `data`, so a bad path fails without paying for the download first.
- mcp/workspace: note that `.geolibre.json` in PROJECT_SUFFIXES is a subset of
`.json` and is listed to name the convention in the client-facing error.
- Tests for each of the above, and drop the dead `is_dir()` conditional in
test_workspace_rejects_traversal_out_of_a_root.
- CLAUDE.md: use the repository-relative `python/tests/test_mcp_server.py`.
- docs/mcp.md: document the two new write guards.
* Address review feedback (round 2)
- render_project_html: require app_url to be an http(s) URL with a host, and
post the project to that exact origin instead of "*". The MCP export_html
tool takes app_url straight from a tool call, so a model reading untrusted
content could be steered into posting a project — inlined features, layer
URLs, camera — to an attacker's origin. Verified the default host
(web.geolibre.app) serves the embed with no cross-origin redirect, so the
strict targetOrigin does not break the default export.
- authoring.save_project: chmod the temporary file to the destination's mode
before os.replace. NamedTemporaryFile creates at 0600, so the atomic write
added in
|
||
|
|
43ae1b48cd |
fix: redact project credentials on egress (#1702)
* fix: redact project credentials on egress * style: auto-format (ruff + oxfmt) [pre-commit.ci] * Address Copilot review feedback - Fail closed when project configuration exceeds the redaction depth cap. - Redact subscription-key URL parameters from Python project egress. * Address CodeRabbit review feedback - redact credential parameters from basemap style URLs - report individual redacted fields and correct Turkish wording * Handle projects without basemap style URLs * Address final automated review feedback - sanitize basemap and plugin manifest URLs across TypeScript and Python - harden URL parameter and userinfo redaction edge cases - align collaboration snapshots and credential counts * style: auto-format (ruff + oxfmt) [pre-commit.ci] * Normalize decoded credential parameter names * Close remaining credential egress gaps - redact geocoder endpoints in TypeScript and Python - fail closed at traversal depth in both implementations - detach inline GeoJSON in redacted projects * Unify credential URL redaction - reuse the central URL sanitizer for Layer Library exports - preserve legacy blank environment-variable counting - document intentionally structural project fields * Complete egress parity across export paths - reuse the central structural-key registry for Layer Library exports - redact Python layer source paths and document processing scope * Preserve credentials in trusted widget sync - distinguish the co-located anywidget host from external embed egress - retain redaction for standalone HTML and iframe hosts * Reconcile credential field and URL parameter registries - Extend PROJECT_CREDENTIAL_FIELDS.layerConfiguration with the credential names the URL registry already stripped but the object-key denylist missed (secret, bearer, auth, authKey, sasToken, subscriptionKey, signature, pwd), so a layer source storing one as a plain JSON field is no longer shipped in a "redacted" export. - Derive URL_CREDENTIAL_PARAMS from that same list plus the names that are credentials only in a query string (`key` and the Azure SAS positional parameters sv/sr/st/se/sp/sig/skoid), and document why they stay URL-only: as configuration field names they collide with ordinary state such as an ArcGIS `sr`. - Match names through a shared normalizer so apiKey/api_key/api-key/APIKEY are one entry on both sides, and route the Layer Library export sweep through the same isCredentialFieldName predicate instead of its own lowercase set, so the two paths cannot drift. - Cover the new spellings, and the deliberately-kept `sr`/`key` fields, in tests/project-credentials.test.ts. * Address remaining review-summary items - packages/core/src/layer-library.ts: `redactSourceValue` now fails closed past its depth cap instead of returning the value unswept, matching the fail-closed stance `redactConfigurationValue` takes in credentials.ts. Only the exported bundle loses the value; the local IndexedDB entry is untouched. - useEmbedBridge.ts / embedHost.ts: rewrite the trust-model docs, which still claimed the host receives the entire project state. Snapshots are redacted by default, and the `trustedWidget` opt-in is documented as self-declared by the host — a fidelity switch for the co-located widget, not a security boundary; the boundary is still the framing context. - useProjectFileActions.ts / ProjectFileDialogs.tsx: rename EnvStripPrompt/envStripPrompt/askStripEnvVars to the credential-neutral CredentialStripPrompt/credentialStripPrompt/askStripCredentials, since the prompt now covers every credential type, not just env vars. - docs/python.md: document that to_project()/save_project()/to_html() redact by default and that keep_credentials=True opts back out. * Cover the Layer Library export depth cap - Add a regression test for the fail-closed depth cap in redactSourceValue: a credential nested past MAX_REDACT_DEPTH must not reach the exported bundle. Fails against the previous fail-open behavior. * Mirror the credential registry into the Python widget - python/src/geolibre/project.py: `_CREDENTIAL_FIELD_NAMES` was missing the names added to the TS registry (secret, bearer, auth, authKey, sasToken, subscriptionKey, signature, pwd), so `to_project()`/`save_project()`/ `to_html()` could ship a credential the JS egress path strips. Derive `_CREDENTIAL_URL_PARAMS` from that same set plus the query-string-only names, and match through a `_normalize_credential_name` mirror of the JS normalizer, with a parity test in python/tests/test_scripting.py. - useProjectFileActions.ts: reword the stale HTML-export comment — this call site no longer strips environment variables itself; buildProjectHtml runs the central redaction pass. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
97825e8ea5 |
feat(python): add leafmap-parity data and layer helpers (#1600)
* feat(python): add leafmap-parity data and layer helpers Notebook users coming from leafmap expect add_csv/add_gdf/add_heatmap, KML and GeoPackage shortcuts, and name-based layer lookup and zoom helpers. Local GeoPackage reads now honor the requested table, and fit_bounds plus layer opacity reject malformed input instead of silently producing a broken view. * Address review feedback - Fetch remote CSV through project.py's public-only opener with redirect re-validation and a 50 MB read bound, and bound local CSV files the same way, so add_csv/add_xy_data match the SSRF/DoS defenses used for GeoJSON. - Give csv.DictReader a string restkey so a row with extra fields cannot put a None key in feature properties and break JSON serialization. - Reject non-finite values in fit_bounds, add_heatmap radius/intensity, and add_xy_data coordinates, which NaN and infinity previously slipped past. - Warn when source_layer is passed with a local (Geo)Parquet file, where read_parquet has no layer concept and the argument was silently dropped. - Document that local add_kml/add_gpkg need the [all] extra, and correct the URL-fetching warning to cover add_csv/add_xy_data and the public-only rule. - Tests: NaN/infinity cases, CSV restkey, non-public URL, oversized file, parquet source_layer warning, opacity lower bound, south > north bounds. |
||
|
|
540b0866ee |
feat(python): split_map, add_legend, and add_colorbar helpers (#347) (#366)
Add thin Python wrappers over the app's built-in map-control plugins,
driven entirely through the synced project dict (no new RPC):
- Map.split_map: enable the Layer Swipe comparison slider between two
layer sets (accepts ids, Layer objects, or lists; '__basemap__' valid).
- Map.add_legend: from a {label: color} dict, parallel labels/colors, or a
builtin preset; ships NLCD and ESA WorldCover presets in legends.py.
- Map.add_colorbar / add_colormap: named colormap or custom colors for a
continuous raster.
The legend/colorbar restore from their settings blob alone, so the
Components plugin is configured but not added to activePluginIds (which
would mount the full Components toolbar). A freshly created plugins block
is seeded with the app's default-active plugin ids so adding a control
does not tear down the layer control or the deck.gl raster overlay.
|
||
|
|
73a31b2b4e |
feat(python): typed read-back of selected/drawn features and to_html export (#365)
* feat(python): typed read-back of selected/drawn features and to_html export (#349) * Address Claude review feedback - to_html: insert embed=1 before any URL fragment so a "#..." in app_url no longer swallows the query param (the iframe would never activate the bridge). - to_html: validate width/height as CSS dimensions (reject "{};:") so a value cannot close the <style> rule and inject CSS; _html_escape did not cover this. - get_selected_features: document that only GeoJSON-layer selections read back; tile/service selections yield an empty list (use identify). - tests: cover get_selected_features(as_gdf=True), the fragment-before-query case, and CSS-injection rejection. |
||
|
|
d7d3d38f0f |
feat(python): local raster, marker/cluster, and choropleth APIs (#348) (#360)
* feat(python): local raster, marker/cluster, and choropleth APIs (#348) - add_raster/add_cog accept a kernel-side local GeoTIFF path, served by the bundled static server with HTTP Range support (geotiff.js needs ranges) - add_marker/add_markers/add_circle_markers/add_marker_cluster build point GeoJSON layers; cluster enables the point cluster renderer - add_choropleth/add_data compute graduated symbology from a numeric column via a new color_ramp module mirroring the app's color-ramp helpers * Address CodeRabbit review feedback - docs/python.md: clarify local-raster paths only work where the browser can reach the kernel's localhost (remote setups need a hosted URL) - docs/python.md: add **style to the add_cog/add_raster table signatures - color_ramp.get_color_ramp: return a copy so callers can't mutate the shared VECTOR_COLOR_RAMPS entries - add_markers: reject non-Point/MultiPoint GeoJSON inputs instead of silently rendering polygons/lines through a point-only markers layer - add_choropleth: raise when the column has no numeric value (was falling back to misleading index-based stops) - test_server: shut down the static server on fixture teardown to avoid leaking threads/sockets across the suite - tests: cover the two new validation errors * Address Claude review feedback - _server: add do_OPTIONS so a CORS preflight on the ranged COG fetch returns 200 (Range is safelisted in modern browsers, but older browsers/proxies that preflight would otherwise hit the stdlib 501 and fail to render local rasters) - _server.register_local_file: reuse the existing token for an already-registered file so repeated add_raster calls don't grow the registry unbounded - color_ramp.graduated_stops: cap class_count at 12 (mirrors clampClassCount in StylePanel.tsx); use math.isfinite; drop the unreachable break/color mismatch branch (both schemes always return `count` breaks) - add_choropleth: cap stored vectorStyleClassCount at 12; use math.isfinite - tests: cover the class-count cap, ramp-copy immutability, token reuse, and the OPTIONS preflight |
||
|
|
6f3f4ade7a |
feat(scripting): Python automation API + in-app Python Console (#327)
* feat(scripting): Python automation API + in-app Python Console Add two PyQGIS-style ways to script GeoLibre programmatically. Notebook API (geolibre.Map anywidget): - Request/reply command channel + event channel over the anywidget custom message bus, relayed by _frontend.js into the embedded app. - App-side dispatcher (useCommandBridge) backed by a shared scriptingApi factory; MapController gains flyTo() and identifyFeatures(). - Python request()/on() primitives (blocking via jupyter-ui-poll) plus get_center, get_bounds, fly_to, fit_bounds, identify, get_features, run_algorithm, list_algorithms, to_image, and a Layer/Feature model. In-app Python Console: - Dockable, resizable bottom panel (Processing -> Python Console), not a modal. - Main-thread Pyodide runtime (minimal/fast startup, on-demand packages, async execution) exposing a `geolibre` facade that drives the live store and map, sharing the same scriptingApi handlers as the notebook bridge. Extracts the embed-host handshake into embedHost.ts (shared by both bridges). * Address Claude review feedback - pyodide-console: destroy the result JsProxy in a finally so a throwing toString() can't leak it. - map-controller (featureIdForLayer): return null when the rendered feature has no properties, instead of vacuously matching the first source feature. - _frontend.js: cap the pre-ready pendingCommands queue at 500 as a defensive bound. - console_api to_image: return PNG bytes to match the notebook Map.to_image() return type (host-path writing isn't available in-browser). * Address CodeRabbit review feedback - test_scripting: inject a fake jupyter_ui_poll via sys.modules instead of patching by string path, fixing the package-publish CI job which lacks the optional dependency (ModuleNotFoundError). - map-controller (featureIdForLayer): only return a property-matched id for an unambiguous single hit (compare full key sets), else null — supersedes the empty-properties guard and avoids wrong ids for non-unique features. - geolibre.request: move send() inside the try so a failing send() still pops the pending slot. - console_api Layer: sync self._info in the visible/opacity setters so later reads on the same handle aren't stale; add -> None annotations. - PythonConsolePanel: show runtime-init failures as an error (no spinner), separate from loading progress. - embedHost: document the ?embed=1 trust model / deployment constraint. - scriptingApi.toImage: note the synchronous PNG encode blocks the main thread. * Address review feedback (round 2) - embedHost.isEmbedded: auto-trust only a SAME-ORIGIN framing parent by probing window.parent.location.href (which throws cross-origin); `window.parent !== window` does not throw cross-origin, so the old check would auto-activate the bridge for a third-party framer. Doc corrected. - useCommandBridge: dispatch only own-property handlers (Object.hasOwn), so an inherited member like "constructor"/"toString" can't be invoked as a command. - scriptingApi.setBasemap: validate the url is an http(s)/root-relative string (rejects undefined -> "undefined" and javascript:/data: schemes). - console_api.load_geojson: raise on non-ok HTTP responses instead of parsing an error body as GeoJSON. - to_image (both notebook geolibre.py and console_api): raise on a data URL with no comma instead of silently returning empty bytes. - PythonConsolePanel: tear down in-flight drag listeners on unmount. - pyodide-console: drop the redundant scriptPromise reset in script.onerror. - geolibre.request: register the pending slot inside the try. * docs: add Python Console guide and interactive scripting API reference - New user-guide page (user-guide/python-console.md) covering the in-app Python Console: opening/docking, first-run runtime load, driving the map, async await, on-demand packages, and a full geolibre/Layer/Feature API reference. - python.md: document the interactive scripting surface (live queries, events, processing, to_image, Layer/Feature) that the notebook widget shares with the console, with a new API-reference subsection. - mkdocs.yml: add the Python Console page to the User Guide nav. * feat(python-console): command history, autocomplete, eraser icon - Up/Down arrows recall previous/next submitted commands (only when the caret is on the first/last line, so multi-line editing still works); a draft is saved when history navigation starts. - Tab / Ctrl+Space autocomplete the name or attribute at the caret, backed by a new _geolibre_complete() that introspects the live Pyodide namespace (real geolibre methods, user variables, builtins, keywords; private names hidden). A single match inserts directly; multiple show a dropdown (Up/Down to choose, Enter/Tab to accept, Esc to dismiss). - Swap the Clear-output icon from Trash2 to Eraser. - Docs: document the editing shortcuts in the Python Console guide. * feat(python-console): QGIS-style script editor alongside the console - "Show editor" toggle splits a script editor into the Python Console panel (left), with a draggable vertical divider; hidden by default. - The editor runs in the SAME Pyodide interpreter as the console (shared globals), so scripts and the REPL share state. Run executes the whole buffer or the current selection; output goes to the console scrollback. - Open/Save/Save As/New for .py files via the app's cross-platform file I/O (openLocalDataFileWithFallback / saveTextFileWithFallback); desktop Save writes in place via new tauri-io writeTextFileToPath, web downloads. Dirty marker + discard confirmation. - Editor keys: Ctrl/Cmd+Enter run, Ctrl/Cmd+S save, Tab indent, Ctrl+Space autocomplete. - Factor the console autocomplete into a shared usePyCompletion hook (with a tabTriggers option) reused by both the console input and the editor. - Add i18n strings and document the script editor in the user guide. * Address review feedback (round 3) - featureIdForLayer: return null for zero-property features (no fake index id), and compare object/array property values by JSON (MapLibre re-parses them to fresh refs, so === always failed). - scriptingApi.setOpacity: reject non-finite and clamp to [0,1]. - notebook Map: add set_basemap alias (name parity with the console facade). - PythonConsolePanel: dispatch PANEL_RESIZE_END on unmount mid-drag so MapCanvas clears panelResizeActive. - pyodide-console: serialize runs through a queue so overlapping calls can't clobber the instance-global stdout/stderr capture; document the merged stdout/stderr stream. - usePyCompletion: clamp completion splice offsets against current code. - geolibre._wait_for_result: check the deadline before and after poll() so a slow pump can't overrun a small timeout. * feat(python-editor): clear button; address review feedback (round 4) - Add a "Clear editor" eraser button to the script editor toolbar (empties the buffer, keeping the filename; confirms when there are unsaved changes). - PythonConsolePanel: throttle the editor splitter drag through requestAnimationFrame with direct DOM writes (commit to state on mouseup), and give the vertical and horizontal drags separate cleanup refs so one can't overwrite the other's teardown. - _frontend.js: warn when the pre-ready command queue is full instead of silently dropping, so a full queue is diagnosable. - geolibre._wait_for_result: poll at 20 Hz (sleep 0.05) instead of 100 Hz to cut idle CPU; deadline is still checked each iteration. * Address review feedback (round 5) - PythonEditorPane: Tab now block-indents every selected line with 4 spaces (PEP 8) instead of replacing a multi-line selection with two spaces; the selection is preserved. - scriptingApi: validate layerId is a non-empty string across the layer handlers (getLayerFeatures/removeLayer/setVisibility/setOpacity/setStyle/zoomToLayer) via a shared requireLayerId, matching identify. - _frontend.js: post commands straight through once ready instead of a push→flush cycle; only buffer before ready. - pyodide-console: remove the failed <script> element on load error so retries don't orphan dead tags in <head>. |
||
|
|
94618bc707 |
feat(python): expand Map API to cover more Add Data types (#246) (#266)
* feat(python): expand Map API to cover more Add Data types (#246) Add leafmap-style methods so data can be added programmatically for the URL/cloud-native formats the Add Data menu supports, beyond the existing GeoJSON/XYZ/COG: - Web services: add_wms, add_wmts, add_wfs - Vector files (URL via the in-browser vector control, or local file read with GeoPandas and inlined): add_vector, add_geoparquet, add_flatgeobuf, add_shp - Tiled/cloud formats: add_vector_tiles, add_pmtiles - 3D / media: add_3d_tiles, add_video - add_raster (alias of add_cog) Each method builds a project-layer record that mirrors the TypeScript store-layer shapes, so the embedded app reconstructs the layer via the same restore paths (restoreVectorLayers / restoreRasterLayers / restoreThreeDTilesLayers) and core layer sync it uses for UI-added layers. Local vector files are inlined via the optional GeoPandas dependency since the browser cannot read a kernel-side file; remote URLs need no extras. Adds builder + Map-method tests and documents the new methods. * Address Claude and CodeRabbit review feedback - video_layer: add `source.type:"video"`, `metadata.sourceKind:"video-url"` and the corner bbox `metadata.bounds` so "Zoom to layer"/fitLayer works, matching the UI-created video layer (Claude). - video_layer: reject non-https:// URLs up front (browser media-src CSP is https-only), surfacing the problem at the call site instead of a silent blank render (Claude). - add_wfs: persist the WFS protocol fields (service/typeName/version/ outputFormat/srsName) on the source for round-trip editing and isWfsLayer(), and record metadata.featureCount like the UI does (Claude). - _read_local_vector: dispatch (Geo)Parquet to geopandas.read_parquet (the GDAL Parquet driver may be absent) and cap the converted GeoJSON at the same 50 MB limit as load_featurecollection (Claude). - tests: assert the new video/WFS fields, add a non-https video rejection test, and narrow the new pytest.raises(ValueError) assertions with match= (CodeRabbit). * Address Claude and CodeRabbit review feedback (round 2) - pmtiles_layer: populate metadata.nativeLayerIds (raster -> [`<id>-raster`], vector -> [`<id>`]) — isExternalNativeLayer() gates on a non-empty list and "pmtiles" has no fallback dispatch in syncLayer, so an empty list meant PMTiles layers never rendered (Claude, real bug). - _read_local_vector: accept a data_format hint that overrides suffix detection, and thread it from add_vector, so a GeoParquet under a non-standard name still uses geopandas.read_parquet (CodeRabbit). - video_layer: reject non-string / empty URL entries instead of silently dropping them (Claude). - add_vector: warn when render_mode / source_layer are passed for a local file, since they only apply to remote URLs (Claude). - vector_layer: document why metadata.nativeLayerIds is intentionally empty and the render_mode="tiles" fall-through caveat (Claude). - tests: assert pmtiles nativeLayerIds, non-string video URL rejection, the data_format threading, and the local-file ignored-kwargs warning. * Address Claude review feedback (round 3) - add_wfs: default max_features to 1000 (matching the UI) since the response is inlined; document that None requests every feature (Claude nit). - wmts_layer: clarify the docstring that the template is WMTS REST {z}/{y}/{x} (row before column), unlike XYZ {z}/{x}/{y}, to prevent users migrating from add_tile_layer from swapping the order (Claude nit). * Address Claude review feedback (round 4) - _append_query: split off any URL "#fragment" before appending the query string and re-attach it, so a service endpoint carrying a fragment no longer produces "…#frag?SERVICE=…" where the params are never sent (Claude). - add_vector: warn for ignored render_mode/data_format/source_layer on the __geo_interface__ path too, consistent with the local-file path (Claude). - vector_tiles_layer: warn when both source_layers and source_layer are given (source_layer is dropped), instead of silently ignoring it (Claude). - tests: cover the fragment handling and both new warnings. |
||
|
|
85644cfc6f |
fix(jupyter): probe both extension and server-proxy routes on remote hubs (#255)
* fix(jupyter): probe both extension and server-proxy routes on remote hubs (#250 follow-up) #250 switched the JupyterHub path from jupyter-server-proxy to a bundled Jupyter Server extension served at {base_url}geolibre/app/. That extension registers from a startup config drop-in, so it only exists after the Jupyter server restarts. A typical 'pip install geolibre' in a running notebook does not restart the server, so the route 404s and the widget shows 'the bundled app could not be loaded from the Jupyter server'. It also abandoned the jupyter-server-proxy path that already worked on hubs that ship it. Keep serving the localhost bundle under the remote path and have the front-end probe two same-origin routes, using whichever is live: - {base_url}geolibre/app/ (the server extension; needs a restart, but no jupyter-server-proxy), and - {base_url}proxy/{port}/ (jupyter-server-proxy; works in the running server with no restart). A host needs only one of them, so the extension covers locked-down hubs while the proxy recovers hubs where it previously worked without a restart. _remote_mode becomes 'remote' (probe both) instead of 'extension'; the localhost server is always started so the proxy route has a target. The extension and its hardening from #250/#252 are unchanged. * chore(pre-commit): add nbstripout hook for Jupyter notebooks * chore(pre-commit): format nbstripout hook entry in YAML configuration * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address Claude review feedback - _frontend.js: probe jupyter-server-proxy before the extension. The proxy reaches the bundle in the running server with no restart and is the common working case, so trying it first avoids waiting on a failed extension probe (the extension only registers after a server restart) on hubs with slow auth middleware. (Claude, _frontend.js:49) - _frontend.js: bound the whole candidate-probe sequence to one shared 15s AbortController owned by resolveBase, instead of a 15s timer per probe, so the worst-case wait when no route is reachable stays at 15s total rather than doubling to 30s. (Claude, _frontend.js:78) The notebook-output comments (Claude + CodeRabbit on getting-started.ipynb) are already resolved on the branch: the nbstripout pre-commit hook was added and pre-commit.ci stripped the committed outputs. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
7150603df3 |
fix(jupyter): serve embedded app via Jupyter Server extension on managed JupyterHub (#247) (#250)
* fix(jupyter): serve embedded app via Jupyter Server extension on hubs (#247) The widget reached the bundled app on JupyterHub through jupyter-server-proxy's raw-port route ({base_url}proxy/{port}/index.html). On managed/shared hubs that route 404s because jupyter-server-proxy is often absent or raw-port proxying is disabled, so the iframe stayed blank. Ship a Jupyter Server extension in the wheel that serves the bundled app from the notebook server's own origin at {base_url}geolibre/app/. It auto-enables on `pip install geolibre` via an etc/jupyter/jupyter_server_config.d drop-in, needs no jupyter-server-proxy and no extra port, and works on locked-down hubs. The widget uses this route under JupyterHub (auto) or server_proxy=True, keeping the localhost path for local Jupyter/VS Code and Colab's port proxy unchanged. The front-end HEAD-probes the route and shows an actionable message if the extension is not active. Drops the now-unused [hub] extra. Adds extension serving/auto-enable tests. * Address review feedback - _extension.py: import tornado lazily inside load_jupyter_server_extension so `pytest python/tests` collects without tornado (CI installs only anywidget/traitlets/pytest); fixes the failing Test Python package jobs. - _extension.py: log a warning when the bundled app dir is absent so a dev checkout (no `npm run build:embed`) gets a clear diagnostic instead of silent 404s; rewrite handler docstring to describe its actual role. - _server.py: validate the bundle only when booting the singleton, so a later serve_app() call with a missing/different path reuses the running server instead of raising FileNotFoundError. - _frontend.js: abort the reachability HEAD probe after 5s so a stalled request cannot hang widget rendering; add "then re-run this cell" recovery hint. - python/README.md: note that an already-running Jupyter server must be restarted for the auto-enabled extension to take effect. - tests: split tornado-dependent route tests into test_extension_routes.py (guarded by importorskip) and add a JupyterHub-style base_url=/user/alice/ case alongside the root case. * Update Python version support and increment package version * Address review feedback - _frontend.js: show a "connecting…" placeholder while the reachability probe runs (the cell was blank during the await), and raise the HEAD-probe abort deadline from 5s to 15s so cold/loaded hubs are not flagged spuriously; mount the iframe with replaceChildren to clear the placeholder. - _extension.py: correct the _AppStaticHandler docstring — a plain tornado handler sits outside Jupyter Server's per-request auth pipeline, so the public, data-free bundle is served unauthenticated by design (verified no "Unprotected handler" warning on jupyter_server 2.17). - test_extension_routes.py: add a HEAD-method assertion per base_url so a GET-only regression of the probe path is caught. * Update Python version requirement to 3.11 in pyproject.toml * Address review feedback - publish-python.yml: align the matrix comment with the raised minimum (requires-python >= 3.11); the matrix already starts at 3.11. - _frontend.js: cache positive reachability results per base URL so a re-render skips the redundant HEAD probe; leave negatives uncached so re-running the cell after enabling the extension still re-probes and recovers. - _server.py: clarify the first-boot-only validation comment (a later missing/different path 404s rather than raising, given the singleton always serves _STATIC_APP). * Address review feedback - _extension.py: when the bundled app dir is absent, emit only the warning and skip the 'Serving the bundled app at ...' info line, so the log no longer shows two contradictory messages. * Address review feedback - geolibre.py: fix a regression where Map(server_proxy=True) on Google Colab would switch to the server-extension route (which does not exist on Colab) and fail. _resolve_remote_mode now detects Colab and keeps the localhost + port-proxy path even when the extension is forced, so Colab works regardless of server_proxy. Adds tests for the Colab and non-Colab branches. |
||
|
|
1348863383 |
docs: add conda-forge install and video tutorial (#205)
* docs: add conda-forge install and video tutorial Document conda-forge installation alongside pip and add conda badges, plus link the GeoLibre 1.0 video tutorial in the README and getting started guide. * docs: add conda badges to getting started guide * Address Claude review feedback - Present the conda install in README as a separate code block with a prose intro instead of an inline bash comment, matching the other docs. - Note in docs/python.md that optional extras are pip-only and how to add them in a conda environment. --------- Co-authored-by: giswqs <giswqs@users.noreply.github.com> |
||
|
|
9f643c3d72 |
feat: add geolibre Python package for Jupyter notebooks (#197)
* feat: add geolibre Python package for Jupyter notebooks Embed the full GeoLibre app in a notebook cell as an anywidget, with a leafmap-style Python API (Map.add_geojson/add_tile_layer/add_cog/...) and two-way .geolibre.json project sync. - New python/ package: Map widget, project/layer builders, named basemaps, a localhost app server, and the postMessage bridge front-end. The web build is bundled into the wheel by a hatch build hook (npm run build:embed). - App-side useEmbedBridge hook applies host load-project messages and emits debounced project snapshots; wired into DesktopShell. New build:embed script builds with a relative asset base so the app loads from the wheel. - CI workflow to test and publish to PyPI via OIDC trusted publishing. - Docs: README section, docs/python.md guide, mkdocs nav, home feature card, and a v1.0 roadmap entry. * Address Claude review feedback; set default map height to 800px Review feedback (PR #197): - useEmbedBridge: only accept inbound messages from the embedding host (event.source === host) so a same-page script cannot inject a project. - useEmbedBridge: drop the dead `window.parent ?? window` fallback (window.parent is always defined in a browser); clarify the comment. - useEmbedBridge: stop the serialize -> JSON.parse round-trip in postState; send the project object directly, keeping the string only for the dedup check. - project.load_featurecollection: add a 30s timeout and a 50 MB size cap to the URL fetch so a slow or oversized response cannot hang or OOM the kernel. - _server: handle_error now only swallows ConnectionResetError/BrokenPipeError and re-raises genuine handler bugs. - geolibre.remove_layer: replace the lambda __setitem__ workaround and ambiguous `l` name with a named inner function. - _frontend.js: post to the iframe using its own origin instead of "*"; document the synchronous-change-dispatch dependency of the applyingRemoteState guard. Also: - Set the default Map height to 800px (trait default, __init__ default, JS fallback, and docs). * Address CodeRabbit review feedback - publish-python.yml: pin the third-party pypa/gh-action-pypi-publish action to a commit SHA (release/v1), matching the repo's convention of SHA-pinning third-party actions while leaving first-party actions/* on tags as in ci.yml. - useEmbedBridge: reset lastLoadedSeq to 0 when an inbound load omits seq, so a later snapshot cannot echo a stale, unrelated sequence number. - _frontend.js: also validate inbound event.origin against the app origin (defense in depth alongside the existing event.source check). - project.load_featurecollection: normalize URL transport failures (URLError, TimeoutError) to ValueError to match the documented contract. * Address Claude review feedback (round 2) - _frontend.js: replace the synchronous `applyingRemoteState` boolean loop guard with reference-identity tracking (`lastRemoteProject`), so echo suppression no longer depends on anywidget firing change:project synchronously. - useEmbedBridge: cache the echo-suppression string from the store *after* loadProject normalizes the project, so a normalized load is not re-echoed. - project.py: cap local GeoJSON file reads at 50 MB (matching the URL cap) via a shared _MAX_GEOJSON_BYTES constant. - geolibre.load_project: validate required top-level keys (version, name, mapView) and raise ValueError instead of pushing an invalid project that would only surface through the error trait. - docs: add a security note that add_geojson(url) fetches from the kernel and intentionally allows private/localhost URLs; not for shared kernels. (SSRF flag — a hard private-IP block would break legitimate local tile servers.) * Support Google Colab; address Claude/CodeRabbit review feedback (round 3) Colab support: - Serve the app port to the front-end (_app_port trait + _server.app_port()). - _frontend.js resolves the iframe base via google.colab.kernel.proxyPort on Colab (falling back to the localhost URL elsewhere), so the browser reaches the kernel through Colab's port proxy. Docs updated: Colab now supported. Review feedback: - save_project: create parent directories instead of raising FileNotFoundError. - build_empty_project: raise ValueError for a center with fewer than 2 elements. - _server.serve_app: document that static_dir is ignored after the first call; simplify the singleton guard to `_base_url is None`. - useEmbedBridge.postState: post the JSON-parsed snapshot so the wire payload matches the serialized .geolibre.json form and is structured-clone-safe (CodeRabbit); clarify the geolibre:error target-origin comment. * Bump Python package to v0.2.0 * Address Claude/CodeRabbit review feedback (round 4) - load_project: backfill a missing `layers` to [] and reject a non-list, so the add_*/remove_layer mutators cannot KeyError on a project loaded without it (CodeRabbit). - useEmbedBridge: learn the host's origin from its first message and scope outbound geolibre:state / geolibre:error posts to it (falling back to "*" only for the version-only ready ping), so a third-party framing page cannot read the project. Document why applyLoad re-stringifies an object for parseProject. - _frontend.js: note that traitlets.Dict value-based change detection keeps the kernel from re-broadcasting the just-sent project, so the identity loop guard is not defeated by the save round trip. - geolibre.py: use p.update({...}) instead of __setitem__ in clear_layers and add_basemap for readability. * Support JupyterHub and remote servers via jupyter-server-proxy The localhost app server is unreachable from the browser on remote kernels. Add a server-proxy path so the widget works on JupyterHub and other remote setups: - Map(server_proxy="auto"|True|False): "auto" (default) serves directly from localhost and switches to jupyter-server-proxy automatically under JupyterHub (detected via JUPYTERHUB_SERVICE_PREFIX). True forces it on any remote server; False forces the direct path. (_use_server_proxy trait + _resolve_server_proxy.) - _frontend.js: when proxying, load the app from {base_url}proxy/{port}/, reading the Jupyter base URL from the page's jupyter-config-data. Colab's own port proxy still takes precedence. The relative-asset build makes the app work under the proxy path prefix. - Add the `hub` extra (jupyter-server-proxy); document JupyterHub/remote support in the README and docs; add tests for the proxy resolution. * Address Claude review feedback - useEmbedBridge.ts: gate proactive postState on a host handshake so a third-party frame that never speaks can't keep receiving project snapshots via the "*" fallback; use an empty mount-only effect dep array instead of the stable mapControllerRef. - project.py: validate center has exactly 2 elements (no silent truncation); omit rescale from rasterState when None (match bands/colormap); reject a FeatureCollection without a 'features' list. - geolibre.py: mark set_center_zoom as a leafmap-compat alias; document that add_geojson inlines file/URL sources into the project. - hatch_build.py: add a 600s timeout to the npm build subprocess. - _frontend.js: document the anywidget object-identity invariant the echo-suppression guard relies on. - publish-python.yml: test against Python 3.10 and 3.12 (declared minimum). * Address Claude review feedback (round 2) - geolibre.py: when a source string starts with "{" but isn't valid JSON (e.g. a path like `{backup}/map.json`), fall back to reading it as a file instead of raising JSONDecodeError. - hatch_build.py: raise a clear RuntimeError (install Node.js / run npm ci) when npm isn't on PATH, instead of an opaque FileNotFoundError traceback. - project.py: use copy.deepcopy for the preferences default instead of a json.loads(json.dumps(...)) round-trip. - _frontend.js: reframe the loop-guard comment to call out the primary (traitlets value-based dedup) vs secondary (reference identity) protection. - useEmbedBridge.ts: document the embed trust model in the hook's JSDoc. - docs/python.md, python/README.md: list set_center_zoom in the API table. * Address Claude review feedback (round 3) - project.py: deep-copy DEFAULT_LAYER_STYLE before spreading so a caller mutating a returned layer's nested style (e.g. vectorStyleStops) can't corrupt the module constant shared by later layers. - project.py: add an SSRF guard to remote GeoJSON fetches — reject hosts that resolve to non-public addresses and re-validate every redirect hop, so a redirect to a cloud metadata endpoint (169.254.169.254) can't be followed. - geolibre.py: validate layout/theme against their allowed values in the constructor so a typo raises immediately instead of silently falling back. - geolibre.py: wrap load_project's file/JSON parse failures in the documented ValueError instead of leaking FileNotFoundError/JSONDecodeError. * Address Claude review feedback (round 4) - useEmbedBridge.ts: don't activate the bridge for a cross-origin parent unless ?embed=1 is present, so an arbitrary third-party iframe of a deployed app never receives project state (the widget and to_html() always pass ?embed=1; same-origin framing stays trusted). - useEmbedBridge.ts: advance lastLoadedSeq before parsing in applyLoad so a later state snapshot carries the right correlation id even when a load fails. - geolibre.py: in load_project, reject a non-dict mapView with a clear ValueError instead of letting set_center raise a confusing TypeError later. - python/README.md: note that add_geojson inlines data and steer large layers toward tile/COG sources. --------- Co-authored-by: giswqs <giswqs@users.noreply.github.com> |