mirror of
https://github.com/opengeos/GeoLibre.git
synced 2026-11-04 10:43:36 +00:00
38ed979d3b0b626c727e27c5d14788a8c002f1c1
43
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
38ed979d3b |
docs: review the docs against the code and fix drift (#3008)
* docs: review the docs against the code and fix drift Check every docs page against the current code and correct what no longer matches: menu paths and labels, plugin placement (most plugins now open in a docked panel), Cesium ion tokens being optional, desktop OAuth sign-in for sharing, browser project save/open, project schema fields, server and collaboration endpoints, Python and MCP APIs, and stale paths in the source-code deep dives. Fix the Spatial SQL tutorial's ST_Area_Spheroid query, which needs ST_FlipCoordinates for lat/lon axis order. Document the Settings sections that were listed but never described (Language, Appearance, Interface, Geocoding, AI Providers), and add the USGS NLDI and ArcGIS feature editing pages to the nav. * docs: keep the NLDI and ArcGIS editing pages out of the nav * Address CodeRabbit review feedback - collaboration.md: give both snapshot-cap defaults, 10 MB for the relay and 1 MB in the Docker Compose setup. - features.md, cesium-renderer.md: without an Ion token the globe still gets terrain from keyless Terrarium elevation (or a configured COG), not a bare ellipsoid (cesium-engine.ts enableWorldTerrain). * Address CodeRabbit review feedback - project-format.md: type blankBackgroundColor and selectedLayerId as string | null, matching ProjectFile in packages/core/src/types.ts. |
||
|
|
49f083b334 |
feat(bookmarks): save bookmarks in the project (#3006)
* feat(bookmarks): save bookmarks in the project Bookmarks lived only in the browser's localStorage, so they did not travel with a .geolibre.json and the Python package and MCP server could not author them. - Project format: optional `bookmarks` and `bookmarkGroups` (folders), normalized on load (camera range checks, duplicate ids, bearing wrap, dangling folder ids dropped) and omitted when empty. - Store: `bookmarks`, `bookmarkGroups` and `setBookmarks`; saved, autosaved and shared in collaboration like other project sections. - Bookmarks panel: no longer persists to localStorage. It opens with the project's bookmarks, writes every edit (including host-dialog imports) back to the project, and rebuilds when a project load or a collaborator replaces them. Bookmarks saved by older versions move into the first project opened that has none, once. - Python `Map.add_bookmark` / `remove_bookmark` and MCP tools `add_bookmark` / `remove_bookmark` (camera from the saved view by default, folder by id or name, layers to show), plus bookmarks in `describe_project`. The Python round-trip fixture now carries bookmarks through the app's parseProject. - Docs: project format, Python, MCP, user guide, and the agent skill. Part of #2869 * Address review feedback - Trim a bookmark's groupId before matching it to the (trimmed) folder ids, so a hand-edited id with spaces is not silently un-filed (Claude review). - Legacy migration no longer deletes the old localStorage bookmarks: a marker key records the one-time copy, so bookmarks copied into the wrong project stay recoverable from browser storage (Claude review). - Python add_bookmark refuses a 501st bookmark before changing the project, matching the app's MAX_PROJECT_BOOKMARKS (500), with a drift test that pins the two together (CodeRabbit). * Address review feedback - Write the legacy-bookmark migration marker only once the project that received them is saved; a project discarded or replaced unsaved leaves it unset so the next empty project gets the copy (CodeRabbit). - Correct the user guide: the old browser copy is left in place but no longer updated, not kept as a backup (Claude). - Move the bookmarkGroups normalization above the print-layout comment so the comment stays attached to its code (Claude). |
||
|
|
81fb86faaf |
feat(layers): editable layer metadata with STAC Item export (#2904)
* feat(layers): editable layer metadata with STAC Item export Add an optional `descriptiveMetadata` block to GeoLibreLayer (title, abstract, keywords, license, attribution, contact, lineage, temporal extent, links). It is normalized on load and save, so old projects load unchanged and an empty block is never written. The field is named apart from the existing internal `metadata` record the renderers and plugins key off. The layer Metadata dialog becomes an editable form above the read-only source details, with email, URL and date validation. Save commits the draft through setLayerDescriptiveMetadata, one undoable store update. "Export as STAC Item" writes a STAC 1.0 Item (extent, datetime or start/end, providers, processing:lineage, links, a data asset for a remote source URL). GeoParquet exports carry the block in the Parquet key-value metadata under `geolibre:metadata`. The layer library captures and restores the block, and the Python package and MCP server gain set_layer_metadata / clear_layer_metadata, pinned by the cross-language round-trip fixture. Part of #2858 * Address review feedback - MCP set_layer_metadata: merge temporal_start and temporal_end one at a time, so passing only one bound keeps the stored other bound. - isRealDate: use setUTCFullYear instead of Date.UTC, which maps years 0-99 to 1900-1999 and rejected valid dates such as 0099-01-01. - metadataDateToRfc3339: normalize a compact +HHMM offset to +HH:MM before parsing; the compact form is not an ECMAScript date-time. - Python link validation error names every accepted URL scheme. * Address Claude review feedback - Metadata dialog status line shows "Unsaved changes" alongside a save or STAC export note, so an export note cannot hide draft edits that the export (built from the stored record) did not include. - STAC footprint of a box degenerate on one axis is a LineString, not a zero-area Polygon. * Address Claude review feedback - metadataDateToRfc3339 upper-cases the date-time before parsing: the validator is case-insensitive, but ECMAScript only promises to read an upper-case T and Z, so a lower-case z could export the export time. - The Python mirror upper-cases too, covering a lower-case t separator. * Address Claude review feedback - A {z}/{x}/{y} tile template is exported as a `tiles` asset (role `tiles`, no guessed media type) instead of a `data` asset, since it cannot be fetched as-is. Documented in project-format.md. |
||
|
|
126c51afb1 |
feat(python): author filters, labels, plugin state and story maps; live processing in MCP (#2892)
* feat(python): author filters, labels, plugin state and story maps Add first-class authoring for layer filters, labels, plugin state and story map chapters to geolibre.authoring, with thin wrappers on Map and new MCP tools (set_layer_filter, set_labels, set_plugin_state, set_story_map, add/remove/move_story_chapter). project.py builds the pieces (labels object, filter expression, chapter) mirroring the TypeScript defaults and vocabularies, and repo drift tests compare them with types.ts and the plugin sources. Add live_list_algorithms and live_run_algorithm MCP tools over the desktop relay; processing runs in the app, so it is live-only. A pinned fixture written by the authoring functions is loaded through the app's parseProject in a new frontend test, so the produced JSON is proven to round-trip unchanged. Bookmarks are not authored: the app keeps them in browser storage, not in the project file. Part of #2858 * Address review feedback - set_labels (MCP): also refuse field/expression/enabled inside options, which previously crashed with a TypeError instead of a readable tool error. - filter_expression: case/match/coalesce/let are accepted only when every branch they can return is boolean, so a numeric case no longer slips past the pre-check and gets dropped by the app on load. - set_plugin_state: clearing settings (state=None) no longer activates the plugin or moves its control as a side effect; documented. - set_labels: enabled now defaults to None and keeps the current state, so a restyle no longer re-shows labels hidden with enabled=False. - docs/python.md: state must be plain JSON. - test_mcp_live: import ToolError after the SDK skip guard. * Address review feedback - set_plugin_state: an omitted state now keeps the stored settings, so a call that only repositions or (de)activates a plugin cannot wipe them; removing them takes an explicit clear=True (Map and MCP too). - Expressions: a RecursionError from a deeply nested expression is now the documented ValueError, for filters and label expressions. - set_labels: a stored labels object without an enabled key counts as on, so labels added over a hand-edited {} are visible. - label_style: document that data-defined expressions are checked for shape, not result type. |
||
|
|
06b120c503 |
feat(desktop): keep project-file credentials in the OS keychain (2/2) (#2728)
* feat(desktop): keep project-file credentials in the OS keychain Geocoding API keys, secret environment variables, and layer request headers are stored device-wide by name in the OS keychain on desktop and left out of the saved project file (#1667). Environment variables gain a per-row `secret` flag; rows marked `secret: false` stay in the file and survive share/export. The web build keeps today's keep/strip save prompt. * fix(desktop): keep Settings secrets until stored, guard the credential index - Settings keeps the edited plaintext in the store and writes only changed entries to the keychain, so a failed write can no longer lose a value; the next project save strips it or falls back to the keep/strip prompt. commitPreferenceCredentials becomes changedPreferenceCredentials. - rememberProjectCredentials skips names the credential store rejects (over 512 bytes, control characters) so one bad name cannot make the device-wide index unreadable. - Seed the Settings credential ref from the same overlaid preferences as the draft. * fix: preserve ambiguous secrets and show accurate keychain warnings * feat: reference environment variables in request headers instead of storing them by layer ID Layer request headers may now contain ${NAME}, resolved from the project's enabled Environment Variables when the request is made (keychain-backed secret values on desktop). The template is saved and shared as typed. This replaces the device-wide project.layer.<id>.requestHeaders keychain entries, which any opened project reusing a layer ID could have received. Redaction (JS and Python) keeps header values that are only an optional scheme word plus one reference. The 3D Tiles panel and Settings explain the syntax. * fix(desktop): clearing a project-file override keeps the shared keychain value Settings deleted the device-wide geocoding key (or secret variable) whenever a field was cleared, even when the field showed a plaintext override from the opened project. Other projects then lost the shared value. A cleared field now deletes the stored value only when it showed that value; clearing a differing override just drops it, and the shared value applies again. * Update apps/geolibre-desktop/src/lib/project-credentials.ts Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix: address review findings on credential saves and header references - Undo a save's keychain writes when the save never writes the project (cancelled prompt or picker, failed write, project switched), so an abandoned save cannot replace a device-wide value. - Rebuild Cesium imagery/3D Tiles and Mapbox 3D Tiles when a variable a ${NAME} header references changes. - Honour "Fly to tileset after load" for templated MapLibre tilesets. - Drop the duplicated index comment in project-credentials.ts. * fix: keep 3D Tiles credentials on HTTPS, finish save rollback, rebuild MapLibre tilesets on variable change - Request headers go only to HTTPS (or loopback) tileset URLs on Cesium, Mapbox/deck.gl and the MapLibre panel. Cesium and deck.gl also refuse plaintext child-tile requests (Cesium via a resource proxy, deck.gl via a fetch function). The shared check moves to @geolibre/core. - Await an abandoned save's keychain rollback before the save guard is released, and report a failed restore. - Rebuild native MapLibre tilesets whose ${NAME} headers resolve differently after an environment variable change. * fix: undo partial keychain writes after a Keep save, prune resolved 3D Tiles headers - When only some credentials reached the keychain and the user keeps the rest in the file, undo the writes that landed. After Strip they stay, as the keychain is then the only copy. - Forget the resolved headers of a removed MapLibre tileset when the layer leaves the store, not only on the next variable change. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
29b389e548 |
fix: keep the layout, theme and collapsed Layers panel in HTML exports (#2768)
* fix: keep the layout, theme and collapsed Layers panel in HTML exports Map.to_html() framed the app with only ?embed=1, so a map-only or dark notebook map exported with the default chrome. It now passes the map's layout and theme as the same query flags the widget front-end uses. The in-app Export as HTML likewise carries the exporting app's chrome: its layout query params, the current theme, and panels=collapsed while the Layers panel is collapsed. Fixes #2764 * fix: honor maponly values and let collapse replace a panels value in exports * fix: keep query flags the to_html app_url already sets * Address Claude review feedback - to_html: force embed=1 again when app_url carries a disabling embed value. The app reads only the first "embed", so the disabling pair is dropped from app_url instead of appended after. - Move the chrome query parsing (hidden-panel values, maponly truthiness, panelsHidden) into lib/layout-params.ts, shared by useLayoutOptions and the HTML exporter so the two cannot drift. |
||
|
|
ce6c1303aa |
feat: headless point cloud pre-labels and labelled full-file rewrites (#2771)
* feat: headless point cloud pre-labels and labelled full-file rewrites geolibre.pointcloud maps COPC node keys to file order, encodes labels like the app, pre-labels a local copy of a LiDAR layer with the app's Whitebox classifiers (whitebox-workflows) and streams a LAS/LAZ/COPC file with the saved labels and instance ids applied. MCP gains prelabel_point_cloud and write_labeled_point_cloud; Map gains the same. The sidecar gets a /pointcloud job (laspy[lazrs] installed into the conversion runtime on first use), and the annotator a "Full file with labels" export that runs it. * Address review feedback - Sidecar decoder matches the Python reader: a decoded-entry cap alongside the byte budget, a bad node skipped rather than failing the job, and instance ids above 32 bits rejected. - The legacy-to-1.4 upgrade (Python and sidecar) keeps the extra-bytes dimensions, identifiers and creation date, and rewrites the CRS as WKT. - Remove the label payload when a job cannot start. - Retry a failed job poll a few times; recheck sidecar availability at each session start. - Map.write_labeled_point_cloud checks the URL is a LiDAR layer. - Pre-labelling reads only classification codes, chunk by chunk. * Drop an unused laspy binding in prelabel_point_cloud * Address Claude review feedback - Install point cloud support under the conversion runtime's own setup lock, since it mutates the venv that lock guards. - Charge a failed label record's full cap to the budget, as the Python reader does, so malformed records cannot bypass it. * Address Claude review feedback - Keep job label payloads in their own temp folder and remove any older than an hour when a job is prepared, so a job that dies before its script runs cannot leave them behind. - Upgrade the waveform formats 4 and 5 to 9 and 10 (not 6 and 7), so their waveform fields carry over; in both the sidecar and the module. * Fix the waveform-format test fixture and wrap a docstring - The test fixture writes format 4/5 as LAS 1.3, which those formats require (1.2 only allows 0-3). - Wrap an over-long docstring line. * Address review feedback - write_labeled_point_cloud refuses to write over its input, which it would truncate mid-read (the MCP tool and sidecar already refuse). - Use a private payload folder when the shared one in the temp directory is owned by someone else or writable by others, so no other local user can swap a job's labels. * Address Claude review feedback - Refuse COPC hierarchy pages that lie outside the file and cap the hierarchy walk at the file size, in the module and the sidecar, so a crafted page size cannot force a huge read. |
||
|
|
3c295b6a0b |
feat: 3D polylines, polygons and keypoints for point cloud annotation (#2765)
* feat: 3D polylines, polygons and keypoints for point cloud annotation A 3D vectors section draws polylines (V), 3D polygons and keypoints (K) whose vertices snap to the nearest drawn point and take its elevation. They are saved with the project, exported as 3D GeoJSON and a Segments.ai pointcloud-vector label in the source CRS, and read by the Python package and MCP. A dblclick listener now finishes vectors and closes selection polygons, since pointer events carry no click count. * Address review feedback - Mark the project as changed on every annotation edit (point labels, instances, custom classes, 3D boxes and vectors), so closing an edited project prompts to save; these live in plugin state, not store layers. - Backspace on the last vertex ends the draft and releases the camera. - Switching vector kinds mid-draw reports that the draft was discarded. - Cover signed-URL redaction of vectors in get_point_cloud_annotations. * Address Claude review feedback - Clamp the vertex snap loop to the points the live buffers cover, like the selection and highlight readers, since a streamed cloud's point count can run ahead of its buffers. |
||
|
|
30503be2e9 |
feat: point cloud class schema and instances in Python and MCP (#2763)
* feat: point cloud class schema and instances in Python and MCP Map.set_point_cloud_classes and the set_point_cloud_classes MCP tool define the annotator's custom classes (its label schema) in a project, keeping existing labels and boxes. point_cloud_annotations now also decodes instance ids (the app's varint encoding) and returns the custom classes; get_point_cloud_annotations summarizes instances as point counts per id. * Address Claude review feedback - Validate custom class colors with a strict #rrggbb pattern, like the app's parseHexColor; int(..., 16) accepted signs and "_" separators that the app would then drop on load. |
||
|
|
c2f11ae6de |
feat(python): add_lidar and point cloud annotation readers, MCP tools (#2758)
* feat(python): add_lidar and point cloud annotation readers, MCP tools Phase 5 of #2749. - Map.add_lidar(url) / project.lidar_layer: a LAS/LAZ/COPC/EPT layer in the shape the app's LiDAR control writes, so it re-streams on open. - Map.point_cloud_annotations() / project.point_cloud_annotations: decode the labels (per URL, node key, point index) and 3D boxes the app's annotator saves; project.apply_point_labels writes a whole-file source's labels onto e.g. laspy classification. - MCP: add_lidar_layer and get_point_cloud_annotations (a per-class count, not every point). - Docs and the agent skill references updated. * Address review feedback - Cap label decompression like the app: 16 MB per node via decompressobj(max_length), 256 MB per project (Claude review, CodeRabbit). - Skip malformed sources, cuboid entries and boxes instead of raising (Claude review). - apply_point_labels validates every key and index before changing anything (CodeRabbit). - get_point_cloud_annotations redacts credentials from source URLs (CodeRabbit). * Address review feedback - Cap the decoded label entry count across a project (Python memory, not just inflated bytes) (CodeRabbit). - Reject a varint longer than five bytes, which also stops a quadratic bigint loop on a crafted record (Claude review). - apply_point_labels rejects negative indices (Claude review). - Direct Map.add_lidar / point_cloud_annotations tests (Claude review). * Address Claude review feedback - Charge a rejected label node's inflate cap to the shared budget, so a project full of bad nodes cannot force unbounded decompression work. - Merge label counts when two signed URLs redact to the same cloud in get_point_cloud_annotations instead of overwriting them. - Skip cuboid entries without a string URL, matching the sources loop. * Address Claude review feedback - Charge a decoded node's inflated size to the budget before the entry cap check, so nodes skipped for exceeding the remaining entry count still draw the decompression budget down. * Address review feedback - Charge each decoded node's actual inflated size to the label budget; varints run to five bytes, so two bytes per edit understated it. - Merge repeated source entries for one URL instead of dropping the first. - Derive the default LiDAR layer name only from a string URL, so a bad URL reaches lidar_layer's ValueError rather than an AttributeError. |
||
|
|
3bbeb7b38c |
fix: restore saved legend/colorbar panels and add set_map_legend (#2720)
* fix: restore saved legend/colorbar panels and add set_map_legend A project that carries a Components legend, colorbar, or HTML panel lost it on reopen. Those panels restore from settings without activating the Components plugin and mount only after a dynamic import resolves, but the restore-scoped app refused any control added after the synchronous turn from an inactive plugin. Restore scopes now stay valid until the next restore (a project load or renderer swap) supersedes them; the activation-generation check still rejects a deactivated plugin. Add set_map_legend to the Python package, Map, and the MCP server. It writes the project-level legend behind Controls > Legend, whose rows come from each layer's symbology, so a classified layer gets a matching legend without restating its colors. * Address review feedback - set_map_legend: visible/collapsed default to None and keep the saved panel state, so a rename no longer re-expands a collapsed legend; a project with no legend still opens it (Claude review). - Plugin manager: a scope also goes stale when the renderer changes or the plugin is re-registered, closing the window before the next restore bumps the pass (CodeRabbit). |
||
|
|
c3ad29ceff |
feat(desktop): reproject WMS layers in projected CRSs into Web Mercator (#2695)
* feat(desktop): reproject WMS layers in projected CRSs into Web Mercator A WMS without EPSG:3857 stored with a projected crs (UTM, national grids) is requested over an extent covering the tile and warped into Web Mercator. Python/MCP crs accepts any EPSG code. Refs #2686. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzcPLNhH3B7ZhPE3cqohhs * fix(desktop): warp geographic EPSG codes outside the strip list; log resolver errors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzcPLNhH3B7ZhPE3cqohhs * refactor(desktop): lazy-load proj4 in wms-projected; clarify resolver comments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzcPLNhH3B7ZhPE3cqohhs * fix(desktop): warp against the returned WMS image size, not the requested one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzcPLNhH3B7ZhPE3cqohhs * fix(desktop): send WMS request as is when a projected tile edge collapses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzcPLNhH3B7ZhPE3cqohhs * refactor(desktop): drop the unneeded cast on the toProj4 call Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzcPLNhH3B7ZhPE3cqohhs --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
188b9a4f8a |
feat(python): save set_identify/show_control state; identify a layer list (#2689)
* feat(python): save set_identify/show_control state; identify a layer list set_identify and show_control only lived in the widget's synced _ui trait, so save_project, to_project and to_html dropped them and a shared map opened with Identify off and the default controls (#2688). Projects gain an optional `interaction` block (identify target and control visibility). The Python Map writes it from _ui on export and reads it back in load_project. The app arms Identify from it in loadProject and shows or hides the named controls and panels once per loaded project through the same setControlVisible command the widget replays, and writes the block back unchanged on save. set_identify (and the setIdentify command) also take a list of layers. The store keeps it as the all-layers Identify mode restricted by a new identifyLayerIds field, honoured by the MapLibre, Mapbox, ArcGIS and Cesium identify paths and the DuckDB pick bridge. Removing a listed layer narrows the list; the in-app Identify buttons clear it. Fixes #2688 * fix(python): reject a non-list set_identify argument with ValueError * fix(identify): honour the layer list in the Layers panel; keep it on group ungroup A row outside a restricted Identify list no longer counts as owning map clicks, so editing or selecting on it does not disarm Identify. Deleting a group but keeping its layers leaves them in the Identify target. |
||
|
|
0ad0a8e699 |
fix(3d-tiles): place 3D BAG on its footprint, keep it visible at pitch 0, add it as a sample (#2613)
* fix(3d-tiles): place large tilesets on their true footprint (#2606) Bump maplibre-gl-3d-tiles to 0.5.9, which anchors the tileset's local tangent-plane frame at the view center instead of once at the tileset center. Nationwide tilesets such as 3D BAG (center near Utrecht) drew Amsterdam buildings tens of meters off the basemap and ~100 m low, because the earth curves away from a single tangent plane and Mercator stretches with latitude. * fix(3d-tiles): keep 3D BAG visible at pitch 0 and add it as a sample Bump maplibre-gl-3d-tiles to 0.5.10. 3d-tiles-renderer built a bounding box's culling planes from its half-axes, which is only correct when they are perpendicular; 3D BAG's root box is slightly skewed, so its top plane sat 501 m low, under the ground near the tileset center. At pitch 0 MapLibre's frustum ends at the ground, so the whole tileset was culled past ~z13 (e.g. in Zeist). 0.5.10 builds the planes from the true face normals. Add "3D BAG Netherlands (LoD 2.2)" to the 3D Tiles sample dropdown with an altitude offset of -43 (3D BAG stores ellipsoidal heights; the geoid is ~43 m above the ellipsoid in the Netherlands), using the per-sample altitudeOffset added in 0.5.10. * docs(readme): drop the DashMap note from the Python package link * Address CodeRabbit review feedback - docs/python.md: document the `geolibre[dash]` extra and the `from geolibre import DashMap` import in the Install section, since the README no longer carries that note and the guide only mentioned DashMap in passing. |
||
|
|
38b3961423 |
feat(python): arm Identify, toggle controls, and set projection from Python (#2603)
* feat(python): arm Identify, toggle controls, and set projection from Python Add Map.set_identify, show_control/hide_control, and set_projection so a notebook can open a map with click popups armed, the Bookmarks and Search panels shown, and a flat map instead of the globe. Identify and control visibility ride a synced _ui trait the widget replays on load and after every project push, so they work before the map is displayed. Refs #2600 * Address review feedback - Record every script-set map control at module scope in ui-controls.ts and replay it from a new useScriptControlRestore hook in DesktopShell, keyed on the controller and project generations. The toolbar was the only thing re-applying a scripted show_control/hide_control, and it is unmounted in `?maponly` embeds, so a hidden control came back on a renderer swap or project load. This follows the split useTerrainRestore already uses for terrain, which exists for exactly this reason. - The same record covers a script that runs before the map controller exists: it is created asynchronously and `geolibre:ready` is emitted from a mount-only effect, so a command flushed right after ready could find no controller and be silently dropped. It is now applied once the controller is ready. - Note in TopToolbar that its listener only mirrors the checkmark, so the restore responsibility is not moved back there later. - Cover the record in tests/scripting-ui-controls.test.ts: what is recorded, last-write-wins per control, and that it starts empty. * Address review feedback - Drop a script's recorded control override when the user toggles that control from the Controls menu. Without it the replay added in the previous commit would force the scripted value back on the next renderer swap or project load, overriding an explicit user choice. Covered by three tests, which fail without the drop. - Factor the duplicated `controllerRef` getter literal in scriptingApi.ts into a shared `controllerRefFrom` helper, removing the repeated cast. - Disarm Identify before pushing the project in `remove_layer` and `clear_layers`. The two traits sync independently, so clearing afterwards left a window where the front end could replay `identify` for a layer the project push had just deleted. * Address review feedback - Drop a pinned Identify target in `load_project` when the incoming project does not carry that layer. The guard added for `remove_layer` and `clear_layers` missed the most drastic replacement of all: a stale layer id survived in `_ui` and was replayed on the next sync, which the front end rejects into a reply nobody reads, disarming Identify via a stray error rather than cleanly. "all" and None survive any project, as before. - Cover it in test_ui_controls.py: the layer is gone, the layer survives, "all" is untouched, and the existing remove_layer path. * Address review feedback - Forget every scripted control override when the user starts a New Project. That reset returns all controls to their defaults, so the overrides are spent; without this, useScriptControlRestore re-applied them to the live map on the same project-generation bump (parent effects run after the toolbar's) and left the map disagreeing with the checkmarks just reset. - Cleared in the toolbar's New Project reset rather than in the store's newProject/loadProject: the widget's project push goes through useAppStore.loadProject, so clearing there would wipe the controls a script set on every push, which is the case the app-side replay exists to serve. * Address review feedback - Guard the hand-mirrored control lists against drift: MAP_PANELS and MAP_CONTROLS in geolibre.py duplicate SCRIPTABLE_PANELS and SCRIPTABLE_MAP_CONTROLS in ui-controls.ts by hand, and nothing failed when the two diverged, so a control added on one side only would be accepted by show_control and rejected by the app (or the reverse). The test reads the TypeScript constants and compares them, and skips where the frontend source is not in the checkout. CLAUDE.md calls out silently drifting hand-mirrors as a recurring trap in this repo. * Address review feedback Document two behaviours reviewers asked about, neither of which changes: - Why setControlVisible does not gate on setBuiltInControlVisible's return value: it is not a clean success flag, since the MapLibre engine returns false when a control is already mounted, so an idempotent show_control would report failure. Separating that from a real refusal needs a visibility getter on MapEngine, which does not exist. - That the "all" Identify sentinel is matched before the layer lookup, so a layer whose id or display name is literally "all" cannot be targeted on its own. Noted on both the TypeScript helper and Map.set_identify. --------- Co-authored-by: giswqs <5016453+giswqs@users.noreply.github.com> |
||
|
|
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> |