mirror of
https://github.com/opengeos/GeoLibre.git
synced 2026-11-04 10:43:36 +00:00
ffe2665a3b9f32d99fe9bace47fc6fdac4e4e8ae
98
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ffe2665a3b |
feat(plugins): let plugins set a layer's visibility (#3182)
* feat(plugins): let plugins set a layer's visibility Adds setLayerVisibility(layerId, visible) to the external plugin API, delegating to the store action the Layers panel's eye button, the embed API and the AI Assistant already use, so the panel, the map and a saved project stay in step. Throws for an unknown layer id, like getLayerFeatures and importLayerStyle. Plugins could read visibility through listLayers but not set it, so a plugin hiding one of its own layers (e.g. a raster it has just vectorised) had to hide the MapLibre layer behind the store's back, leaving the eye icon wrong until the next sync re-showed it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(plugins): reject a non-boolean visibility in setLayerVisibility An untyped JavaScript plugin could pass e.g. the string "false", which is truthy and would leave a non-boolean in layer state. Throw a TypeError instead, as the embed API validates its own setLayerVisibility. Requested in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: agraham <agraham@aslenv.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
f8cf4a4a3a |
feat(plugins): pass the cancel signal to assistant tool specs (#3161)
* feat(plugins): pass the cancel signal to assistant tool specs
registerAssistantToolSpec dropped the SDK's per-call context, so a plugin
tool waiting on the network kept the assistant busy after Stop and the
next message failed with "already in progress". The spec callback now
gets an optional second argument, { signal }, wired to the tool
context's cancelSignal; existing one-argument callbacks are unaffected.
Closes #3155
* test: type the spec helper callback with the optional context
* Address Claude review feedback
- Fall back to a never-aborting signal when the tool context has no
cancelSignal: the fast path (runToolDirectly) invokes tools with a
context built from toolUse alone, so plugins would have received
signal: undefined and thrown on signal.aborted.
- Test that a spec callback gets a live AbortSignal without one.
|
||
|
|
12527f86b2 |
feat(assistant): let the assistant turn on an installed plugin and use its tools in the same turn (#3160)
* feat(assistant): let the assistant turn on an installed plugin
Plugin tools exist only while their plugin is on, so the assistant could
not reach an installed plugin that was off, and a plugin turned on during
a turn only showed its tools at the next prompt.
Plugins now declare `assistant: { description }` (in plugin.json or on
the plugin object). While such a plugin is installed and off, the
assistant gets an `activate_plugin` host tool listing it. Calling it
turns the plugin on the way the Plugins menu does, adds the tools it
registers to the live agent so they are callable in the same turn, and
returns them with the plugin's guidance.
Fixes #3156
* Address review feedback
- Keep polling after a plugin's first tool appears, until its tool list
has been unchanged for 250 ms, so tools registered in several steps
all reach the live agent in the same turn (CodeRabbit). Always waiting
the full timeout, as suggested, would stall every activation 3 s.
- Collapse whitespace in plugin ids, names and descriptions before they
go into the activate_plugin description, so a description cannot
forge list entries or lines (Claude review).
- Document the wait, including the 3 s stall for a plugin that
registers no tools.
* Address CodeRabbit review feedback
- State that 3 s is the total upper bound on waiting for a plugin's
tools, with the 250 ms quiet period inside it, and rewrap the line.
|
||
|
|
39dde26e45 |
feat(map): request headers for authenticated tile services (#3142)
* feat(map): request headers for authenticated tile services MapLibre takes one request transform per map, so a plugin that needs a header on its tile requests has to call setTransformRequest itself and replaces anyone else's (GeoLens did, for private rasters). GeoLibre now builds its MapLibre maps with a single transform backed by a header registry in @geolibre/map/tile-request-headers, keyed by owner and matched by URL prefix, and GeoLens registers its API keys there. Plugins get a headers option on addVectorTileLayer and setLayerRequestHeaders for any tile layer, so a token refresh needs no reload. Headers stay in memory: they never reach the store or a saved project, and opening another project or removeLayer drops them. Only MapLibre maps send them. Closes #3127 * fix(map): keep tile request headers to their own server and layer - A URL prefix must now name one host and the start of a path on it before it can carry headers. A template with a placeholder in its host matched on "https://", and a prefix without its trailing slash matched look-alike hosts, so a layer's credentials could reach basemaps and other servers. The registry throws for such a prefix, and setLayerRequestHeaders and addVectorTileLayer say why. - A layer's headers are dropped when the layer leaves the store, however it is removed (the Layers panel, undo, a plugin), not only through the plugin API's removeLayer. - Header names given twice in different cases are rejected, since fetch would join their values into one. * fix(map): drop tile headers when a layer's URL moves, and match fetch's rules - A layer's headers are recorded with the tile URL prefixes they were set for, and dropped when the layer's URL moves to other prefixes, so a new server doesn't inherit them and the old prefix doesn't keep them. A token refresh that only changes the query string after the placeholders keeps the same prefixes and the headers. - Header validation now also rejects what fetch won't send: names the browser sets itself (Cookie, Host, Sec-*, Proxy-* and the rest of the Fetch standard's forbidden names), which fetch silently drops, and values with characters above U+00FF, which make fetch throw inside MapLibre. - GeoLens checks the renderer before registering a private raster's API key, so the key isn't left registered when the call refuses. |
||
|
|
7106dd6911 |
fix(plugins): keep plugin metadata from rerouting a vector tile layer (#3143)
* fix(plugins): keep plugin metadata from rerouting a vector tile layer The map decides how to draw a layer from metadata keys such as sourceKind and customLayerType. A plugin passing sourceKind "maplibre-gl-vector" and a customLayerType in options.metadata sent its layer down the control-owned path, where nothing drew it. The layer now gets its own sourceKind, "plugin-vector-tiles", after the plugin's metadata, and the other render-routing keys are dropped from it. * test(plugins): cover every render-routing metadata key The addVectorTileLayer test now passes all seven keys it drops from plugin metadata (sourceIds, externalDeckLayer, controlOwnsPaint and nativeFillExtrusion as well) and checks each is removed. * fix(plugins): drop metadata.sourceId from vector tile layer metadata MapController and the layer sync read the singular sourceId as well as sourceIds, for instance to tell basemap sources apart, so a plugin setting it to a basemap source's id could affect basemap handling. It is now dropped with the other render-routing keys, and the test and docs list it. |
||
|
|
de62b66163 |
feat(plugins): GeoEditor session events and startEditSession for plugins (#3138)
* feat(plugins): GeoEditor session events and startEditSession for plugins Plugins couldn't tell when a GeoEditor session on their layer ended, or start one. A plugin that syncs edits to a server had to infer a session's end from the layer's visibility (hidden during a session, shown after), which can't tell Save from Cancel and fires when the user toggles the layer by hand (#3125). - getEditSessionLayerId reports the layer being edited. - onEditSessionChange reports sessions starting and ending, with whether the edits were saved. The GeoEditor records how its last session ended (getLastGeometryEditEnd) before notifying, covering save, cancel, and a layer removed mid-session. - startEditSession opens a session as a layer's Edit geometry action does. That logic moves from useLayerEditActions into lib/geometry-edit-session.ts, shared by the Layers panel and the plugin API; it takes the GeoEditor functions as arguments, so app-api.ts still doesn't import plugin runtime code. Closes #3125 * fix(plugins): serialize edit-session starts and report same-layer restarts Overlapping startEditSession calls could end the first session while it was still loading its features, so the first load might finish against the second session. Starts now run one after another, on the Layers panel path too. onEditSessionChange also compares the last end record as well as the layer id, so a session that ends and restarts on the same layer between two notifications is still reported as an end and a start. * fix(plugins): skip GeoEditor activation for a missing layer startEditSession on a layer id that isn't in the project now resolves false straight away instead of loading and activating the GeoEditor first. The docs now say the session left open after overlapping starts is the last call's only when that call succeeds. * fix(plugins): report a GeoEditor activation that throws as activation-failed PluginManager.activate rethrows when a plugin's activate() throws, so startEditSession rejected instead of resolving false as documented, and the Layers panel's Edit geometry action left an unhandled rejection. The shared start now reports it as activation-failed, which the panel shows as its existing error. * fix(plugins): only report saved for an end recorded with the session onEditSessionChange now reads saved from an end record only if it was written since the last notification, so a session that ends on a path that records nothing can't inherit an earlier session's saved value on the same layer. * fix(plugins): don't report a session as saved when its layer is gone syncEditTargetToStore returned silently when the target layer had been removed, and endLayerGeometryEdit still recorded the session as saved. It now returns whether it wrote the edits, and that is what the end record reports. A comment next to lastGeometryEditEnd states that every end path records before notifying, which onEditSessionChange relies on. * fix(plugins): isolate session listeners and wait for the sketches restore A geometry edit listener that threw (a plugin's onEditSessionChange callback) stopped the rest from being notified, so the Layers panel could miss a session ending. Each listener now runs on its own, and a failure is logged. A session ends and releases its layer before its sketches are restored, so a start arriving in that gap (from a plugin, while the user pressed Save) ran alongside the restore. A new session now waits for any restore in progress. * refactor(plugins): make GeoLibreEditSessionChange a discriminated union saved is now present exactly on an "ended" change, so plugins narrow on state instead of checking an optional field. The docs example narrows the same way, and the docs say a throwing callback is logged. * fix(plugins): deliver each edit-session change to a callback separately On a switch, onEditSessionChange delivers the old session's end and the new one's start in one notification. A callback that threw on the end never got the start, and its bookkeeping had already moved on, so that start was lost. Each change is now delivered on its own, and a throw is logged. * fix(plugins): stop queued edit-session starts waiting forever Starts run one at a time, so a start that never finished (a stalled editor package load, say) kept every later one waiting, including the Layers panel's Edit geometry toggle. A start that has waited a minute for earlier ones now gives up with start-failed. A running start is still never abandoned, and later starts keep waiting for it, so two starts never run at once. * fix(plugins): report a session start that throws as start-failed startLayerGeometryEdit can reject while it ends the previous session (its write-back or sketches restore), so startEditSession rejected instead of resolving false and the Layers panel skipped its error message. The shared start now logs the error and reports start-failed. |
||
|
|
5cf70dccd8 |
feat(plugins): add addVectorTileLayer to the plugin API (#3140)
* feat(plugins): add addVectorTileLayer to the plugin API Plugins can add raster tile layers but not vector tile layers, so the GeoLens plugin builds the store record itself and external plugins can't. addVectorTileLayer takes a tile URL template or a TileJSON URL and the source layers to draw, and adds the same record Add Data -> OGC Vector Tiles builds: it appears in the Layers panel, takes the Style panel's controls, and saves with the project. Part of #3127. Request headers for authenticated tile services follow in a separate change. * fix(plugins): validate addVectorTileLayer's options more strictly - A missing options object now throws the sourceLayers error rather than a raw TypeError. - A URL with some but not all of {z}, {x} and {y} throws, instead of being read as TileJSON and failing when the map loads it. - opacity must be a number from 0 to 1, and a NaN zoom is rejected. * fix(plugins): check addVectorTileLayer's extent and TileJSON zoom options - bounds must lie within WGS84 limits with west <= east and south <= north, so MapLibre's addSource can't reject it after the layer is in the store. - minzoom and maxzoom throw for a TileJSON URL, where MapLibre takes the zoom range from the TileJSON; bounds is still kept for Zoom to layer. - Duplicate source layer names are listed once. * refactor(plugins): tidy addVectorTileLayer's import and source layer check Sort the import with the other plugin-* modules, drop the array cast in validateSourceLayers, and note in the docs which tile URL placeholders are recognized. * fix(plugins): copy addVectorTileLayer's bounds and source layers The layer record kept the plugin's own bounds array, so a plugin that changed it later changed the stored layer outside the store. Both arrays are now copied. A test also covers a beforeLayerId that isn't a layer: the layer is added on top and the id is kept as a map anchor, as for the other add*Layer methods. * fix(plugins): reject a blank or non-string addVectorTileLayer name The name is the Layers panel label and is saved with the project, so a plain-JavaScript plugin passing a blank or non-string value now gets an error like the other options instead of an unlabelled layer. * test(plugins): cover addVectorTileLayer's URL trimming A TileJSON URL with surrounding whitespace is now tested to be stored trimmed, and a comment explains why source and metadata get separate copies of the arrays. |
||
|
|
a1c00ad86a |
feat(plugins): OAuth authorization-code sign-in for plugins (#3139)
* feat(plugins): OAuth authorization-code sign-in for plugins Add app.getOAuthRedirectUri and app.authorizeInPopup so a plugin can sign users in with OAuth 2.0 (authorization code with PKCE) without its own callback page or deep-link handling. The host runs only the round trip: a popup through oauth-callback.html on the web, the system browser and the custom-scheme callback on the desktop. The plugin builds the request, keeps its PKCE verifier and exchanges the code itself. The popup wait used by ArcGIS sign-in moves to lib/oauth-popup.ts so both flows share the origin, source and state checks. Closes #3126 * fix(plugins): refuse a second OAuth sign-in before opening its popup The popup has a fixed name, so a second authorizeInPopup call during a sign-in got the first one's popup back from window.open, navigated it away and closed it, cancelling the first sign-in. The request, the pending flag and timeoutMs are now checked before window.open, so a second call rejects with already-pending and leaves the first alone, and an invalid request never opens a popup. timeoutMs must be a positive number of milliseconds within setTimeout's range, or the call rejects with invalid-request instead of timing out at once. On the desktop, a failure to open the system browser rejects with the new launch-failed reason rather than a raw error. * fix(plugins): close the OAuth popup when a plugin sign-in fails After a timeout, a denial or a bad callback the popup stayed on the provider's page, and the next sign-in picked it up again by its fixed name. It is now closed when the sign-in fails. PluginOAuthFailure is now an alias of the public GeoLibreOAuthFailure rather than a second copy of the union, and the desktop callback failures map through a lookup instead of a nested ternary. |
||
|
|
e908594a10 |
feat(plugins): honor expected-status markers in nativeFetch diagnostics (#3124)
* feat(plugins): honour expected-status markers in nativeFetch diagnostics Plugins' app.nativeFetch recorded every response of 400 or above as an error in Diagnostics, even when the plugin expected it, such as the 400 authorization_pending answers an OAuth device sign-in polls through (RFC 8628). The webview's fetch capture already lets a request mark expected outcomes with the client-only x-geolibre-expected-status and x-geolibre-optional-resource headers. nativeFetch now honours the same markers: listed statuses, and any failure of an optional request, are recorded as info, and the markers are stripped before the request goes to the native client. Both markers are documented in the plugin API guide. * docs(plugins): document diagnosticRecord and installDiagnosticsCapture |
||
|
|
cc82ef4705 |
feat(plugins): add updateGeoJsonLayer and removeLayer to the plugin API (#3128)
* feat(plugins): add updateGeoJsonLayer and removeLayer to the plugin API A plugin that shows data held elsewhere had no way to replace a layer's features or remove a layer: it could only add layers. To show a remote service's current data it had to remove the layer through unregisterExternalNativeLayer, which removes any layer although it's meant for plugin-registered native layers, and add a new one, losing the layer's place in the order, group, style and visibility. updateGeoJsonLayer replaces a GeoJSON layer's features through the store's updateLayer, as saving a GeoEditor session or refreshing a layer does. It returns false, changing nothing, for an unknown or non-GeoJSON layer, or one open in the GeoEditor, so unsaved edits aren't overwritten. removeLayer removes a layer through the store's removeLayer, as the Layers panel does. * fix(plugins): clear a native layer's paint bridge when removeLayer removes it removeLayer removed the store record but left a plugin-painted layer's paint bridge registered, since bridges live outside the store. unregisterExternalNativeLayer already cleared it; removeLayer now does too. |
||
|
|
b81b19b630 |
feat(cesium): recording control facade so plugin panels run on the globe (#3090)
* feat(cesium): recording control facade so plugin panels run on the globe Port the ArcGIS control host's model to the Cesium globe (#3088). The MapLibre-shaped facade a plugin control receives on the globe now records its Style Spec calls into a shadow style instead of throwing: a layer the plugin mirrors into the store is drawn by CesiumLayerSync, and any other GeoJSON fill, line, circle or text layer is drawn as ground-clamped globe entities. Layer-scoped events and queryRenderedFeatures answer from the mirroring store layer or the overlay graphics, and dragPan/scrollZoom switch the globe's own camera inputs. - Move the pieces both hosts share into neutral modules (control-layer-events, shadow-overlay, geometry-hit) and share the store-mirror lookup (controlLayerMirrors). - Overlay lines and polygon edges use rhumb arcs, matching MapLibre's straight Mercator segments; a geodesic ground polyline spanning the globe (a graticule parallel) did not draw at all. Points and labels past the horizon are hidden instead of drawing through the Earth. - Carry text-anchor/text-offset in the overlay's text symbol, honoured by both hosts. - Read a raster record's {level}/{row}/{col} tile template from source.url on the globe, as ArcGIS does (Esri Wayback). - Graticule edge labels anchor to the screen edges on a globe projection. - getControlMap(app) resolves the globe facade (getCesiumControlMap). Opt in, each verified in the running app: Gridlines, the grid-factory DGGS grids (H3, S2, A5, OLC, Geohash, Tilecode), DGGAL, DGGRID, FEMA NFHL, USGS National Map, US EPA EnviroAtlas, NASA Earthdata and Esri Wayback. * feat(cesium): STAC, catalogs and COGs on the globe; guard GPU controls Continue #3088. - Refuse custom layers on the globe facade: a recording facade let maplibre-gl-raster's control mount on the globe, and the first COG add hung the tab. COG adds on the globe now import as store `cog` records (native-raster-import.ts, shared with ArcGIS). - Re-dispatch the mousedown/mousemove/mouseup the browser drops after Cesium cancels pointerdown, so drag-drawing controls (the STAC bbox) receive the drag. - Answer getLayer for a store layer's derived `layer-<id>-*` style layers and map them to that store layer for picks, so STAC footprint selection works on the globe. - Draw a `wms` record whose tile is an ArcGIS REST export from that template rather than sending GetMap to the REST endpoint (Earthdata GIS ImageServers). - Disable the LiDAR and Gaussian splat Add Data entries on the globe, where their controls cannot mount and used to do nothing. Opt in, each verified in the running app: the STAC browsers, Ocean Data Platform, Earthdata GIS, GeoLens and Hugging Face. Document the globe's plugin control host. * Address review feedback - Reject keyed GeoLens rasters on the Cesium globe as on Mapbox and ArcGIS: the API key rides MapLibre's request transform, so a private raster added there would 401 on every tile (CodeRabbit). The error message no longer names Mapbox alone. - Replace the per-entity CallbackProperty that hid overlay points and labels past the horizon with one pass over them on camera change and move end, writing only changed flags, so a dense grid's labels are not re-evaluated every frame (Claude review). * Address Claude review feedback - Probe both sides of a point for the globe overlay's hit tolerance and take the short way round across the antimeridian, so a probe that lands in space next to the limb no longer zeroes the tolerance and leaves overlay points and lines there unclickable. * Address Claude review feedback - Redraw the globe's plugin overlay on a store change only when the fields that decide mirroring (layer ids, nativeLayerIds, sourceId, sourceIds) change, so an opacity drag or restyle no longer rebuilds every overlay entity. * Address Claude review feedback - Match a store layer's derived style-layer id only when the whole remainder after `layer-<id>-` is a known suffix, so store layer `a` no longer claims `layer-a-b-fill` (layer `a-b`'s fill) for picks, mirror checks, or getLayer. - Report a Mercator projection from the globe facade in Cesium's 2D and Columbus scene modes, as the docs state, instead of always "globe". - Fix the test type-check ratchet: an excess-property object literal in the mirrorSignature test. |
||
|
|
94af13fc0d |
feat(plugins): report the GeoEditor from getActiveMapTool (#3029)
* feat(plugins): report the GeoEditor from getActiveMapTool getActiveMapTool now returns "geo-editor" while the GeoEditor plugin is active, so plugins that stand down for Identify and feature selection also stand down while the user draws or edits geometry (including a layer in "Edit geometry"). onActiveMapToolChange fires when the editor turns on and off by also subscribing to the plugin manager, where that state lives. Feature selection and Identify keep precedence. Adds a lightweight @geolibre/plugins/plugin-ids export so app-api.ts can read the editor's id without loading the plugins barrel under node. Fixes #3020 * test: type the map tool events with GeoLibreActiveMapTool The existing map tools test typed its events array with the old literal union, which no longer accepts the widened callback type and failed the test type-check ratchet. * Address Claude review feedback - Wrap the getActiveMapTool JSDoc line in types.ts to match the surrounding comment width. |
||
|
|
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. |
||
|
|
c802b78ac5 | feat(plugins): expose active map tool state (#2966) | ||
|
|
9395772216 |
feat(plugins): make WFS feature limit configurable (#2963)
* feat(plugins): make WFS feature limit configurable * style: auto-format (ruff + oxfmt) [pre-commit.ci] * style(plugins): separate WFS option doc blocks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
a8ca595a73 |
feat(s3): keep the S3 Browser open across project loads, open it at startup (#2941)
* feat(s3): keep the S3 Browser open across project loads, open it at startup Opening a project from the S3 Browser closed the panel: restoring the loaded project's plugin state deactivates every plugin the project does not list. Plugins can now declare `sessionScoped`, which a project load or a map swap leaves running and which is never saved in the project's active plugins. The S3 Browser declares it. Settings > Startup gains "Open the S3 Browser at startup". * Address Claude review feedback - Warn when the startup activation of the S3 Browser returns false, not only when it throws, matching the deep-link activation path. - Sort the useStartupPanels import after usePluginStateRestore. |
||
|
|
fff0083a04 |
docs: document deployment policy cutover (#2900)
* docs: document deployment policy cutover * docs: address deployment policy review feedback * docs: clarify legacy profile lock field * docs: drop dated release note and part numbers from availability * docs: restore dropped headings and list spacing * docs: separate Docker and desktop availability remedies --------- Co-authored-by: Qiusheng Wu <giswqs@gmail.com> |
||
|
|
55ce70447f |
feat(identify): skip WMS layers marked queryable="0", show WMS exceptions as errors (#2890)
* feat(identify): skip WMS layers marked queryable="0", show WMS exceptions as errors (#2887) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RJ1vRuz1nnmmsXQfZ2c3ZH * fix(identify): treat a WMS exception sent with an error status as an error Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RJ1vRuz1nnmmsXQfZ2c3ZH * docs(plugin-api): document the WMS queryable option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RJ1vRuz1nnmmsXQfZ2c3ZH * fix(identify): send WMS GetFeatureInfo in the layer's CRS (#2889) * feat(identify): skip non-queryable WMS and report exceptions * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(identify): fall back to EPSG:3857 when the projection cannot convert the click Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RJ1vRuz1nnmmsXQfZ2c3ZH --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Daniel Hofbauer <hofbauer.d@icloud.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
94fb172104 |
feat(i18n): complete the catalogs and let plugins ship translations (#2893)
* feat(i18n): complete the catalogs and let plugins ship translations Fill every key missing from the 20 non-English catalogs (the USGS 3DEP panel, the newer Manage Plugins policy and registry-trust strings, the keychain wording and the 3D model export), and translate the identical-to-English values in de/nl/id that were genuinely untranslated prose. All insertions are additive; no catalog is reordered. tests/i18n-catalogs.test.ts now fails when a bundled locale lacks a key that en.json has (plural keys compared by base), replacing the report-only coverage log, so new UI strings ship with translations. Plugins can register their own per-locale strings through app.registerTranslations (plugin.<id>.* keys only, never overriding the host catalog). Built-in plugins get createPluginTranslator and pluginDisplayTitle from @geolibre/plugins. The elevation profile, 3D Tiles key toggle, GeoAgent Earth Engine sign-in, annotation element list and point cloud annotation stop hard-coding English DOM text, and 15 docked plugin panels title their header from the translated plugin name. Part of #2858 * Address review feedback - Export pluginDisplayTitle from @geolibre/plugins, as docs/plugin-api.md promises (Claude, CodeRabbit). - Elevation Profile: title its panel with pluginDisplayTitle and reuse interpolatePluginText for the English fallback instead of a second regex (Claude, CodeRabbit). - 3D Tiles: re-label the API-key toggle and the layer visibility checkboxes on a language change instead of only at build time (CodeRabbit). - registerTranslations test: assert on Object.prototype and the i18next store, so it can catch a real prototype-pollution regression (Claude). |
||
|
|
7099ceb16a |
feat(plugins): honor the plugin registry's blocklist (#2873)
* feat(plugins): honor the plugin registry's blocklist The hosted plugin registry now publishes blocklist.json beside plugin-registry.json (opengeos/geolibre-plugins#77): plugins, or single bundles of them, that its maintainers have pulled. Removing a plugin from the registry only stops new installs; the blocklist also stops installed copies, which load from their saved manifest URL. - plugin-blocklist: fetch blocklist.json next to the configured registry once per session (4 s timeout), cache it for offline starts, and fail open: with no copy, nothing is blocked. Malformed entries are dropped individually. - evaluatePlugin: deny a plugin blocked outright ("blocklisted", with the reason) for every external source; bundled drop-ins are exempt. All install and load paths already go through this gate. - external-plugins: refuse a URL bundle whose hash is blocked, both on load and on Update, before its code runs. - External plugin loading waits for the blocklist first. * fix(plugins): address review feedback on the blocklist - plugin-blocklist: read the response through readBodyWithCap (now exported from plugin-registry with a label), so the 1 MB cap is enforced while streaming instead of after buffering the whole body. - Wait for the blocklist before an Update and before both zip install paths, not only before startup loading, so nothing is checked against a list that is still loading. The desktop zip install now also reads the manifest id when the blocklist has entries. - external-plugins: hash each startup bundle once and pass the hash to verifyPluginBundleIntegrity, which accepts a precomputed hash. - ensurePluginBlocklistLoaded never throws: a registry URL that can't be resolved means nothing is blocked. * fix(plugins): scope the cached blocklist to its registry - plugin-blocklist: store the blocklist URL with the cached copy and use the cache only for the same URL, so switching registries never applies one registry's blocklist to another's plugins while offline. * fix(plugins): treat a malformed blocklist as a failed fetch - plugin-blocklist: a 200 response without a "blocked" array no longer replaces the active list or the cache with an empty one; it falls back to the cached copy like any other failure. A 404 still means the registry has no blocklist. * fix(plugins): address review nits on the blocklist parser - plugin-blocklist: cap the number of valid entries rather than raw ones, so malformed items can't push valid entries past the limit. - plugin-blocklist: key single-bundle blocks with JSON.stringify([id, hash]) so the pair is unambiguous whatever an id contains. * fix(plugins): keep a cached blocklist through 404s; block bundles in zips - plugin-blocklist: a 404 keeps a cached list for that registry and never overwrites the cache, since a registry that published one is likelier mid-deploy than to have withdrawn it. A registry that never published a blocklist still blocks nothing. - external-plugins: assertBundleNotBlocklisted() hashes a bundle and refuses a blocklisted release. Zip and plugin-directory bundles are now checked at startup, by the web zip install, and by the desktop zip install before it persists the archive; URL bundles already were. - docs/plugin-api.md: hash blocks apply to every external source. * fix(plugins): one translated denial for every blocklist block - plugin-policy: blocklistedDecision() builds the "blocklisted" denial. The single-bundle checks (URL load, Update, zip and directory bundles) now raise PluginPolicyError with it instead of plain English errors, so the UI shows the same translated managePlugins.policyBlocklisted message as for a whole-plugin block. - plugin-blocklist: remember which blocklist URL the active list came from; a failed fetch with no cache keeps that registry's own list but drops another registry's. * perf(plugins): skip bundle hashing when the blocklist is empty - external-plugins: assertBundleNotBlocklisted returns early when no plugin or bundle is blocklisted, so zip and directory bundles aren't hashed on every scan in the usual case. |
||
|
|
d625dcbe4a |
fix(desktop): pin the CSP's jsDelivr script-src to versioned paths (#2875)
* fix(desktop): pin the CSP's jsDelivr script-src to versioned paths The desktop CSP allowed scripts from all of cdn.jsdelivr.net/npm/ and /pyodide/. Replace those with one version-pinned path per package the app actually executes from the CDN: Pyodide, PGlite and its PostGIS extension, onnxruntime-web, the duckdb-wasm worker that the components DuckDB converter loads, and the duckdb-wasm, sql.js, geojson-vt and vt-pbf URLs maplibre-gl-vector hard-codes (plus the modules jsDelivr's /+esm bundles for them import). tests/tauri-csp.test.ts re-derives every path from its owner (lockfile, PYODIDE_VERSION, ORT_VERSION, maplibre-gl-vector's dist) so a bump that moves a URL fails CI instead of the packaged app. 'unsafe-eval' stays: maplibre-gl-vector calls new Function() at module scope in the startup graph, and the packaged app boots to a blank window without it. docs/maintenance.md records that, the other eval consumers, and why connect-src keeps https: and http:. Part of #2858. * Address review feedback - docs/maintenance.md: scope the security claim to what path pinning does (blocks other packages and versions), and say that it does not verify content at an allowed path or survive redirects (CodeRabbit). - docs/maintenance.md: fix the garbled DuckDB converter table row (Claude review). |
||
|
|
232627700d |
feat(plugins): verify registry installs and updates against the published bundle hash (#2857)
* feat(plugins): verify registry installs and updates against the published bundle hash The hosted plugin registry now publishes a bundleSha256 for each plugin: the hash computePluginBundleHash produces for the bundle that was reviewed (opengeos/geolibre-plugins#71). Use it so a registry install no longer trusts whatever its URL serves first. - plugin-registry: parse bundleSha256 (lowercase hex SHA-256, otherwise ignored) and add pinRegistryEntryBundle. - Installing from Manage Plugins or a ?plugin= deep link pins the announced hash before the URL is installed, so the existing load-time check holds back a download that differs. - The Update action takes the entry's hash, hashes the download before importing it, and refuses a mismatch without evaluating the code. The computed hash is reused for the new pin. - Entries without bundleSha256 keep trust-on-first-use. * fix(plugins): warn when a registry bundleSha256 is malformed - plugin-registry: log a console warning when an entry carries a bundleSha256 that isn't 64 lowercase hex characters, so a registry typo that silently falls back to trust-on-first-use gets noticed. * Address CodeRabbit review feedback - Reload coalescing in reloadExternalUrlPlugin keyed the in-flight promise on the manifest URL alone, so an update started mid-reload with a different registry hash (or version) received the first call's result without its own hash ever being checked. The in-flight entry now records its expectedVersion/expectedHash; only a matching call shares it, and any other waits for it to settle and then runs a reload that checks its own expectations. Covered by a new concurrent update test that fails without the fix. |
||
|
|
6327719397 |
fix(plugins): close credential-store reads before external plugins load (#2861)
* fix(plugins): close credential-store reads before external plugins load External plugins are imported with the app's own privileges and can call any Tauri command, so secure_store_get_many handed every saved token (Settings tokens, Share refresh token, PostGIS DSNs, other plugins' app.credentials values) to any installed plugin. The app already reads every account in one call during startup hydration, before it renders and therefore before any external plugin is imported. Make that the only read: secure_store_get_many now answers once per page load of the calling webview (the read is used up even when it fails), the page-load hook reopens it when a webview starts a new document, and a new secure_store_seal command closes reads without reading. Hydration now always makes its read, and importExternalPlugin seals before evaluating any plugin code, refusing to load the plugin if sealing fails. Writes and deletes stay open, since the app saves credentials whenever Settings change and a write discloses nothing. This is a narrowing, not isolation: plugins still share the JS realm, so values already hydrated into memory remain reachable. The docs say so. Part of #2858 * Address Claude review feedback - readSecureCredentials now marks reads sealed only after the read succeeds. A failed call may never have reached the native command (an IPC transport error), so it no longer short-circuits the sealSecureCredentialReads backstop before external plugins load; the Rust-side seal is idempotent, so the extra call is harmless when the read did claim the gate. Adds a test for the failed-read path. |
||
|
|
5773b71463 |
feat(layers): show a service layer's source in its Metadata dialog, and let plugins add metadata (#2856)
* feat(layers): show a service layer's source in its Metadata dialog The Metadata dialog showed only layer.metadata, the name, the type and sourcePath, so a WMS layer listed little more than "service": "wms". It now also shows the service address and request fields GeoLibre keeps on the source of WMS, WMTS, XYZ and the other tile and service layers (layers, styles, format, version, CRS), with inlined data left out. The whole payload is scrubbed with the same credential rules a saved project uses, since the dialog offers Copy. WMS sources now record the request CRS, from both Add Data and the plugin API. Plugins can pass an optional metadata object to addTileLayer, addWmtsLayer, addWmsLayer and addWfsLayer, merged into the layer's metadata, so a catalogue plugin can leave a layer's provenance with it. Fixes #2855 * Address review feedback - Scrub the metadata, source summary, source URLs and sourcePath one by one, so a GeoJSON-looking `type` in the metadata cannot make the whole payload skip redaction. - Keep only HTTP(S) URLs under the source's data/geojson keys and always omit kerchunkRefs, so inline text never lands in the dialog. - Reject plugin metadata whose toJSON() serializes to a non-object. |
||
|
|
59dd8cdc3e |
feat(plugins): let users move a plugin's menu out of the toolbar (#2853)
Each installed plugin that registered a top-level toolbar menu now has a "Show menu in toolbar" switch in its Plugins → Installed entry. Turning it off takes the plugin's menus out of the banner and renders them as submenus inside that entry instead, so the user, not the plugin, decides what occupies the menu bar. The choice persists per plugin in the desktop settings (foldedPluginMenus). Built-in plugin menus are unaffected. Part of #2850. |
||
|
|
d24528d84a |
feat(plugins): let plugins add items to the built-in toolbar menus (#2852)
* feat(plugins): let plugins add items to the built-in toolbar menus Add app.registerMenuContribution(), the GeoLibre counterpart of QGIS's addPluginToVectorMenu: a plugin adds items to Add Data, Processing or Controls instead of claiming a top-level menu of its own. The host nests each plugin's items under a submenu named after the plugin, after a separator at the end of the target menu, sorted alphabetically, so a plugin cannot inject loose items or reorder the built-in entries. The PluginManager injects the owner id and name the same way it tags toolbar menus. An unknown target menu warns and is ignored rather than failing activation, so plugins written for a newer host still load. Contributions follow the plugins:install gate and are hidden in the viewer. Part of #2850. * fix(plugins): release menu contributions with their plugin and skip malformed items The PluginManager now drops a plugin's menu contributions wherever it already drops its assistant tools (deactivate, unregister, failed activation), so a plugin that forgets to dispose of them leaves nothing behind. Replacing another plugin's contribution id warns, and the shared menu renderer skips null items and submenus without an items array instead of throwing. * fix(plugins): only accept menu contributions from live plugins A restore callback can run for a plugin that stays inactive (applyProjectState); a contribution registered there would linger, since owner cleanup runs on deactivation. The scoped registrar now ignores contributions unless the plugin is active or activating. * fix(plugins): skip menu contributions with nothing renderable A contribution whose items are all malformed or separators would group into a plugin submenu that opens to nothing once renderItems drops them. * fix(plugins): scope unregisterMenuContribution to the calling plugin A plugin's scoped unregisterMenuContribution now removes only contributions that plugin registered, so one plugin cannot take another's menu items away. The host can still remove any id. * fix(plugins): build a plugin scope when only unregisterMenuContribution exists |
||
|
|
ec4722c2a7 |
feat(plugins): group external plugins in a Plugins → Installed submenu (#2851)
Plugins loaded from the registry, a zip, a manifest URL, or a bundled drop-in were appended to the flat Plugins menu, which had grown long. They now live in one "Installed" submenu at the bottom of the menu, sorted alphabetically by display name, with a Manage Plugins… shortcut at the end. The submenu only appears when an external plugin is loaded. The Web Services sort moves into a shared sortPluginsByDisplayName helper used by both submenus. Part of #2850. |
||
|
|
661fd5c791 |
feat(plugins): give desktop plugins a native fetch with a cookie jar (#2848)
* feat(plugins): give desktop plugins a native fetch with a cookie jar The Data to Science plugin signs in with a session cookie. In the desktop app the page runs at tauri://localhost, so the cookie D2S sets is third-party, and WKWebView on macOS drops it: the login returns 200 and the next request returns 401, shown as "Session expired". Plugins had no other route: the host only offered a GET-only fetchArrayBuffer, and the Tauri HTTP plugin is scoped to a fixed host list. Add app.nativeFetch on the desktop. It has the fetch signature and runs through a new plugin_http_request command: any method and body, no CORS, a cookie jar kept for the app session, Set-Cookie kept out of JavaScript, the existing SSRF guard (link-local and cloud-metadata addresses refused), 64 MiB bodies, a 120 s timeout, and AbortSignal cancellation. Requests appear in Diagnostics with credentials redacted. The ArcGIS transport's async client setup (SSRF resolver, enterprise CAs, mTLS identity) moves into a shared guarded_async_client_builder so both transports use it. Refs #2837 * Address review feedback - Check literal-IP redirect hops against the SSRF block list. The guarded DNS resolver re-checks hostnames, but a literal IP never reaches a resolver, so a redirect to 169.254.169.254 was followed (Claude review). - Accept any method token fetch accepts (PROPFIND and other extension methods) except CONNECT and TRACE, which fetch forbids too (CodeRabbit). |
||
|
|
ada13fce47 |
feat: expose WFS layer API to plugins (#2826)
* feat: expose WFS layer API to plugins * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix: guard WFS requests across project changes * fix: hydrate WFS layers when panels are hidden * fix plugin WFS review findings * refactor: align plugin WFS with the shared Add Data request path - Drop the plugin-specific destination/IP/DNS/redirect policy and the browser public-IP-literal restriction; validate only absolute HTTP(S) URLs, matching Add Data. - Remove the fetch_plugin_wfs_response command and its Vite proxy route; plugin WFS rides fetch_url_response / the WFS dev proxy. - Keep credential redaction in native diagnostics, now applied to every native command. - Restore fetch_url_bytes in the Tauri invoke handler. - Hoist refresh state into LayerRefreshProvider so WFS hydration runs outside the conditionally mounted layer panel. * refactor: keep plugin WFS on the existing Add Data path - Revert vite-proxy-guard.ts to main: the PR-added per-address IPv6 transition checks and the leftover userinfo-stripping indirection had no place in the browser/dev-proxy path once the plugin-only policy was dropped. - Desktop: keep the resolved-address checks in one place, the generic build_guarded_http_client_with_redirects; drop the single-caller generic resolver the plugin command needed. - Drop the LayerRefreshProvider hoist; the shell owns useLayerRefresh and passes it to LayerPanel as before, removing the shell-wide re-indent. - Drop the unused isCredentialUrlParam export; diagnostics redaction is unchanged. * fix: review findings on the plugin WFS path - Save a credential-free WFS reference with a `wfsCredentialsRedacted` marker: the embedded collection is the only copy a sanitized URL can render, and a later save of the reopened project could no longer tell that and dropped it. - Read diagnostics credential names from @geolibre/core's URL registry (`isCredentialUrlParam`) instead of a second, narrower list, and drop the redundant text pattern. - Drop a URL fragment before building the GetFeature request: the fragment swallowed the appended operation parameters. - Only `source.url` makes edited WFS geometry restorable; a stray `metadata.originalUrl` (a tile-layer field) could let a no-embed save discard features nothing can re-fetch. - Document that a plugin bbox must not cross the antimeridian. * docs: clarify credentialed WFS project saves --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
36c144e51f |
feat(plugins): let registry plugins keep project state through credential stripping (#2836)
* feat(plugins): let registry plugins keep project state through credential stripping A registry entry can now declare publishableSettings (true or a list of top-level keys). The host feeds those to the credential redaction so the plugin's state survives Strip credentials and shared exports. Built-in allowlists cannot be widened, and kept state is still swept for credentials. * Address review feedback - Gate the registry prefetch on the plugins:install capability like the marketplace - Note that the Python mirror does not know registry-declared settings * Address review feedback - Ignore registry publishableSettings for built-in plugin ids - Update the earlier plugin-state paragraph in the plugin API docs |
||
|
|
b378efc5c9 |
feat(plugins): expose host proj4 to external plugins (#2824)
* feat(plugins): expose host proj4 to external plugins * docs(plugins): recommend private proj4 CRS definitions |
||
|
|
815985a912 |
plugins: move Hugging Face, Mapillary and God's Eye View tokens to app.credentials (#2793)
* plugins: store Hugging Face, Mapillary and God's Eye View tokens via app.credentials (#2729) * style: auto-format (ruff + oxfmt) [pre-commit.ci] * plugin-credentials: keep legacy-key cleanup from failing a landed write * Update apps/geolibre-desktop/src/lib/plugin-credentials.ts Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * plugin-credentials: keep the session override when legacy cleanup fails --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
5786f9741e |
feat(plugins): add app.credentials API backed by the OS keychain on desktop (#2778)
* feat(plugins): add app.credentials API backed by the OS keychain on desktop (#2729) * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(plugins): make desktop credential set/delete retry-safe * style: auto-format (ruff + oxfmt) [pre-commit.ci] --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
54305ad477 |
feat(plugins): add importLayerStyle to the plugin API (#2702)
* feat(plugins): add importLayerStyle to the plugin API Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): import styles only into vector layers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugins): point the style module comment at both siblings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6c0e453f3c |
feat(plugins): accept an optional crs in addWmsLayer (#2701)
* feat(plugins): accept an optional crs in addWmsLayer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): normalize the WMS version inside normalizeWmsCrs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
27aad9ddd8 |
feat(arcgis): run MapLibre-control plugins on the ArcGIS renderer (#2683)
* feat(arcgis): run MapLibre-control plugins on the ArcGIS renderer Most Plugins-menu entries were greyed out on ArcGIS because their controls talk to a MapLibre style the SDK does not have. Give the control facade a style that controls can use there: - shadow-style.ts records addSource/addLayer/setPaintProperty/... with MapLibre's contract (errors as events, getStyle read-back). - A layer the plugin mirrors into the store is drawn by the engine from that record; any other GeoJSON layer (grids, selection outlines, draw previews) is drawn in one graphics overlay, with filters and paint evaluated per feature by the style spec (style-layer-evaluator.ts in @geolibre/core). - Layer-scoped events and point queryRenderedFeatures answer from whichever of the two draws the layer; dragPan.disable() stops the view panning so box drawing works; mousedown/mouseup are fired. - getArcgisControlMap / getControlMap(app) hand the facade to docked panels and plugins. Enabled on ArcGIS: the Web Services WMS catalogs (FEMA, NASA Earthdata, EnviroAtlas, National Map), Esri Wayback, the eight DGGS grids and the Graticule, STAC (plus Planet Open Data and Portolan), OpenAerialMap, Vantor, Fields of the World, Satellite Embeddings, IGN LiDAR HD, GeoLens, Earthdata GIS, Hugging Face, Source Cooperative and Natural Earth. Also fixes two pre-existing ArcGIS bugs found on the way: the engine stacked store layers upside down (the store is bottom to top, as the Layers panel and MapLibre's sync read it), and a WMS layer never drew under the dev server because its proxied template hid the LAYERS parameter from the WMSLayer parser. * fix(arcgis): address review on the plugin control facade - Shadow style: an unknown beforeId in addLayer/moveLayer reports an error instead of appending; a kept source view merges into the recorded spec and is inert after removeSource. - Layer events: layer-scoped on returns a Subscription, a listener-less once resolves with the next hit, off with the original listener removes a once, mousedown/mouseup dispatch, and negative numeric feature ids stay numbers. - Control host: no overlay redraw after destroy(). - IGN LiDAR HD: the unsupported-renderer message names ArcGIS in every locale, with a note tying the denylist to `engines`. * fix(arcgis): allowlist point-cloud renderers; mirror check without a source IGN LiDAR HD refuses any renderer not known to draw lidar-url layers again (now MapLibre, Mapbox and ArcGIS), so a renderer added later is not assumed to. The control host consults the store mirrors for a layer whose source it cannot resolve, since a mirror can match by id. * docs(arcgis): describe the control map's overlay; reattach moved JSDoc getControlMap and getArcgisControlMap still said a grid or highlight would not show on ArcGIS, which the overlay in this PR now draws. Two doc blocks had been separated from their methods by the inserted code, and a held shadow source's serialize() now reads the live entry. |
||
|
|
0f39d5be00 |
feat(plugins): add a Fields of the World plugin (#2612)
* feat(plugins): add a Fields of the World plugin Adds Plugins > Web Services > Fields of the World, which visualizes, searches, and downloads the FTW global agricultural field boundaries from the same Source Cooperative files the FTW inference app reads: - Field boundaries: the 2025 or 2024 global PMTiles archive as a regular pmtiles store layer, colored by confidence_mean with the FTW app's red-to-green ramp (expression mode, editable in the Style panel). - Field density: the global 500 m density COG, kept at the current view (new optional addCogLayer zoomTo) and hidden above zoom 12. - A confidence threshold slider (70% by default, mapped onto the model's 0-0.578 range like the FTW app) that writes each FTW field layer's filterExpression, so it persists with the project. - Search of the 1-degree download grid by map view or drawn box, with footprints in the Layers panel and click-to-select. - Per tile: Add to map, source GeoParquet, or GeoJSON, optionally clipped to the search area. Tiles are read one row group at a time (the largest hold ~3M fields in ~123k-row groups), so a small clip of a huge tile no longer exhausts the tab's memory. * Address review feedback - Mark the field-density COG with a plugin-owned metadata key instead of overwriting its sourceKind: the raster sync, Style panel and Mapbox COG path key on "maplibre-gl-raster" (CodeRabbit). - Check the abort signal before addCogLayer, and finish configuring the layer (marker, maxZoom, attribution) even when the task is aborted while it loads, so no orphaned density layer is left (Claude, CodeRabbit). - Report a cancelled save dialog as cancelled instead of "Saved": saveBlob now returns whether the host saver wrote the file (CodeRabbit). - Build the GeoJSON Blob from per-feature strings so a large export does not exceed V8's maximum string length (CodeRabbit). - Search a map view crossing the antimeridian as two boxes instead of the whole world; the grid search and clipping split such boxes (Claude). * Address review feedback - Disable the Field density button while a task runs, like the row actions, so a second click during addCogLayer cannot pass the duplicate check before the first layer lands (Claude, CodeRabbit). |
||
|
|
9c5324773f |
feat(assistant): load plugin tools on demand past 12 of them (#2595)
* feat(assistant): load plugin tools on demand past 12 of them Every active plugin's tools were sent to the model on every request, so the tool list, and the ambiguity between same-shaped tools from different domains, grew with every installed plugin. Up to 12 plugin tools are still sent in full. Past that, the system prompt lists each plugin tool by name and a one-line summary, grouped by plugin, and the model loads the ones it needs with a new load_plugin_tools host tool (exact names or a plugin id). The Strands SDK re-reads the agent's tool registry before every model call, so loaded tools are callable in the same turn; they stay loaded for the conversation. The catalog depends only on what is registered, so the prompt stays stable across turns. Host tools are always sent. Fixes #2586 * fix(assistant): keep abbreviations from cutting a tool summary short * Address review feedback - Count plugin tools sent in full as loaded, so crossing the eager limit mid-conversation keeps tools the model could already call (Claude). - Resolve an exact tool name before expanding a plugin id with the same spelling, with a collision regression test (CodeRabbit). * Address Claude review feedback - Move the per-conversation plugin tool bookkeeping (pruning removed tools, counting eagerly sent tools as loaded, recording loads) out of AssistantSession into ConversationPluginTools, and test it across refreshes, the eager-limit crossing, unregistration and reset. |
||
|
|
08b6eadb93 |
feat(plugins): expose moveLayerGroupToGroup and listLayerGroups on the plugin API (#2554)
* feat(plugins): expose moveLayerGroupToGroup and listLayerGroups on the plugin API The plugin API let a plugin create a Layers-panel group, move layers into one, and remove one, but there was no way to nest a group inside another group: the store's `moveLayerGroupToGroup` was wired into the Layers panel and the desktop shell only. A plugin could build a flat row of folders and nothing deeper. Expose that store action, and with it `listLayerGroups`. Without a read the new method would only reach groups the plugin created itself, since `addLayerGroup` was the one source of group ids a plugin could hold; the summary carries `parentId` (null at the root) so the flat array spells out the whole tree. The write is a passthrough: the store action already refuses an unknown id and any move that would make a group its own ancestor, so a plugin cannot cycle the tree through it. `listLayerGroups` goes in `plugin-layer-queries.ts` with the other read-only queries, which keeps it testable without pulling the built-in plugin registry into the test. Also documents the whole layer-group surface in docs/plugin-api.md, which had never mentioned groups at all. * Address CodeRabbit review feedback - Cover the public delegate, not just the store action. Every method on `GeoLibreAppAPI` is optional, so type checking would not notice `moveLayerGroupToGroup` going missing from `createAppAPI` or being handed its arguments in the wrong order, and the tests called the store directly. Moved the four group writes into `plugin-layer-groups.ts` (the same extraction `plugin-layer-queries.ts` already uses, and for the same reason: `usePlugins` pulls in the whole built-in plugin registry) and pointed the tests at that factory. Adds a case for the move-layers-in / remove-the-folder-alone round trip, which was untested too. - Guard `subBasins` before `removeLayerGroup` in the docs example. It comes from an optional call, so a plugin copying it verbatim failed type checking. * Address Claude review feedback - The module doc claimed `addLayerGroup` drops `beforeLayerId`; the store's `addLayerGroup(name?, layerIds?)` never took one. Only `moveLayersToGroup` does, and that is what the sentence now says. |
||
|
|
6a9c00a991 |
feat(plugins): IGN LiDAR HD Plugin (#2507)
* Added a plugin to load lidar hd data in geolibre * Fixed some bugs * Added translation files * Simplify plugin panel text * Prevent overlapping LiDAR auto-zoom suppression from corrupting shared state * Simplified the docs & improved the tests * Discarded package-lock.json modifications * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix: manage concurrent LiDAR restores * fix: reject invalid tiles count values * fix: compare matched tiles count with the returned page size * fix: handle bulk-add setup failures and restore the controls * fix: gate the live WFS test behind an explicit opt-in * fix: updated the docs & a GUI misleading message * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix: changed the default bounding box values * fix: zoom to extent of the resulting tiles --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
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. |
||
|
|
b45f3859de |
Host deck.gl visualizations on ArcGIS maps and local scenes (#2449)
* Add ArcGIS basemap picker and native layer control * Support native ArcGIS heatmaps, clusters, patterns and feature altitude * Render custom COG terrain in ArcGIS scenes * Render COG imagery on ArcGIS maps and scenes * Host deck.gl visualizations on ArcGIS maps and local scenes * Address review feedback - Reuse the existing ArcGIS translation key for the basemap section. - Read the basemap preferences action through component selectors. - Synchronize native visibility callbacks to prevent store updates losing toggles; add a regression test. - Clarify the shared primary-pane Layer Control default; keep the widget consistent with the active-by-default plugin. * Address review feedback - Read current preferences when selecting an ArcGIS basemap to preserve concurrent changes. - Document all ten supported Esri basemap choices. - Allow the two-boot Style Manager E2E flow sufficient total runtime on CI without changing assertion timeouts. * Address review feedback - Skip unused symbol evaluation and deduplication for heatmap points, preserving mixed geometry styling. - Explain the inclusive cluster zoom cutoff already covered by the crossover test. * Address review feedback - Restore custom terrain only for 3D scenes and isolate DEM failures from view initialization. - Keep a redacted diagnostic for failed restores while preserving the working map. * Address review feedback - Strip session-only raster byte URLs from saved and reopened projects while preserving desktop paths. - Initialize COG extents synchronously in Web Mercator from known bounds. - Fall back to COG when a source URL has no trailing name. * Address review feedback - Remove an unused vendored import and align ArcGIS scale constants and binding documentation. - Honor ArcGIS diagram zoom limits and decluttering using the native projection and settled camera updates. - Omit unprojectable diagram points and invalidate native-view decluttering on camera changes. * Address review feedback - Explicitly ground selection highlights when feature elevation is disabled. - Preserve finite zero-Z coordinates for ArcGIS absolute-height placement and altitude offsets. - Add regression coverage for both altitude cases without changing default Z detection in other renderers. * Address review feedback - Clear a failed remembered terrain source to avoid retrying it on every view rebuild. - Refresh cache recency for reused terrain neighbours during panning. * Address review feedback - Reuse opened COG sources across symbology rebuilds and forget removed sources. - Cache source statistics weakly and verify style edits use the new gamma without reopening data. * Address review feedback - Clear failed ArcGIS overlay mount promises so a transient CDN failure can be retried. - Reuse the SDK module URL helper and verify retry succeeds on the same view. * Address review feedback - Restore terrain only when enabled without delaying view startup, retaining the selection after transient failures. - Exercise overlapping terrain requests, failed replacements and disposal. - Distinguish elevation axes and return zero outside world Y bounds. * Address review feedback - Type the terrain registration fixture explicitly so tile URLs retain their tuple shape. * Address review feedback - Retry COG preparation after transient failures instead of retaining a rejected promise. - Skip raster lifetime scans when the layer list has not changed. * Address review feedback - Cover local raster restoration, blob lifetime, skipped sources and stale asynchronous reads. - Document browser blob reuse in the raster restore path. * Address review feedback - Clarify that deck layer ordering is internal to the shared overlay. - Include the lazy deck SDK modules in the ArcGIS upgrade checklist. - Explain projection-driven capabilities during ArcGIS view replacement. * Address review feedback - Measure scene camera distance relative to the focal elevation - Retry 2D resource initialization with bounded attempts and disposal guards * Address review feedback - Release every partial Deck and GPU allocation before retrying initialization |
||
|
|
2fb8249d7e |
feat(geoagent): run GeoAgent on the Mapbox renderer (#2428)
* feat(geoagent): run GeoAgent on the Mapbox renderer
Almost every GeoAgent tool already sat on the Style Spec surface both 2D engines
share. Four did not, and each broke differently on a mapbox-gl map:
`add_marker` built MapLibre's `Marker`/`Popup` (whose position update reads
`map._camera.transform` and throws), `set_projection` wrote `{ type }` where
Mapbox takes a name string, `get_map_state` read `projection.type`, and
`run_maplibre_script` handed user-authored code this package's `maplibre-gl`, so
a script that built a marker built the wrong one.
That matters more here than in a control that simply fails to mount: an agent
run breaks mid-way, after it has already changed the map.
maplibre-gl-geoagent 0.6.1 takes the engine as one option and all four follow
it. The plugin names the host's engine once (`geoagent-map-engine.ts`, its own
module so it can be unit-tested — the plugin's entry pulls the Earth Engine
browser client in at import time) and hands over the whole mapbox-gl namespace
rather than a narrowed subset, because `run_maplibre_script` passes it straight
to the script it runs. On MapLibre it passes nothing and the upstream default
stands.
`set_sky` / `clear_sky` stay engine-limited: mapbox-gl has no `setSky` (it draws
sky through a style layer), and the tool reports that rather than failing
silently.
Verified in the browser on a `primaryRenderer: "mapbox"` project in both themes,
driving `tools.runCommand` — the same entry point a model's tool calls land on,
so the engine-specific paths are exercised without an LLM provider: the tools
report the Mapbox engine and the namespace the renderer is drawing with;
`add_marker` places a mapbox-gl marker (and no maplibre one) that tracks the map
through a pan; `set_projection` switches to globe and `get_map_state` reads it
back; a script that builds a `Marker` and adds it succeeds, which MapLibre's
would not; and a GeoJSON overlay reaches the Layers panel as a plugin-owned row.
Covered by e2e/mapbox-geoagent.spec.ts plus unit tests for the engine resolver.
Part of #2420 (GeoAgent item). Upstream: opengeos/maplibre-gl-geoagent#33
(v0.6.0) and #34 (v0.6.1).
* review: resolve the agent's engine on access, cache only the module
Both reviewers landed on the same real hazard. `geoAgentControlPromise` memoized
the dynamic import *and* the control construction together, so the control was
built from whichever activation started the import — and GeoAgent's chunk is
large enough that a renderer swap easily lands inside that window — after which
every later activation reused that instance.
Two changes, either of which would close it, and both are worth having:
- Cache the module, not the control. The module is engine-neutral and safe to
share; the control is not.
- Commit to Mapbox on the renderer, which the store flips synchronously, and
resolve the namespace when it is read. The control stores the descriptor and
its tools only dereference `namespace` when a tool actually runs, long after
the map exists — so a control built while the engine was still mounting still
drives the right library. The unreachable case throws rather than silently
handing the agent MapLibre's classes.
Also replaces the e2e's console-text filter with failed-response tracking that
keeps the URL and status, ignoring only 401/403 from the Mapbox API hosts and
cancelled requests (every pan and projection switch abandons the tiles for the
view it left).
* Address review feedback
- Only the current activation may build the shared GeoAgent control.
Caching the module rather than the control was not enough on its own:
continuations on the shared import run in registration order, so an
activation superseded mid-import ran first and won the race to fill
`geoAgentControl` — with its now-stale engine baked in — leaving the
activation that was actually current to mount that wrong-engine
instance. `loadGeoAgentControl` now takes the generation, refuses to
construct when it is no longer the latest (or when GeoAgent was
deactivated meanwhile, which would otherwise leave a control nothing
tears down), and returns null; `mountGeoAgentControl` treats null as
a superseded mount.
- `geoAgentMapEngine` treats the renderer as authoritative instead of
OR-ing it with the namespace. Swapping Mapbox out flips
`primaryRenderer` before the outgoing engine is cleared, so a
`getMapboxGl()` that still answered built a Mapbox descriptor for a
MapLibre host — the same wrong-engine bug in the other direction. The
namespace now only stands in for a host that reports no renderer.
- e2e: moved the `bindAgent` doc comment back onto `bindAgent` (it had
drifted above `RESOURCE_FAILURE_ECHO` in a reorder), and dropped the
Mapbox-host exemption from the `requestfailed` watcher so a transport
failure is reported at every host; only `ERR_ABORTED` is still
ignored.
- Two unit cases cover the above: the renderer overruling a namespace
the outgoing engine still answers, and the generation gate in front
of the control construction.
* Address Claude review feedback
- Forget a GeoAgent chunk import that rejected. Splitting the memoized
promise in two moved the reset: `geoAgentControlPromise` is still
cleared in `.finally()`, but `geoAgentModulePromise` held the
rejection forever, so one failed load (offline, or a chunk orphaned
by a redeploy) made every later activation reject with the same stale
error until a page reload. On desktop nothing masks that — the
stale-chunk reload is web-only and skipped while the project is
dirty. The import now clears the cached promise on rejection and
rethrows, so the next activation retries, which is what
`mountGeoAgentControl`'s catch and the host-side rollback assume.
- Unit case covering the retry.
* Address CodeRabbit review feedback
- Drop the adverbial "any more" from the merged mapbox-renderer.md
sentence rather than spelling it "anymore": the clause reads the
same without it.
* Address CodeRabbit review feedback
- Drop the plugin roll call from the sentence that replaced the now-empty
"Still MapLibre-only" list. Naming GeoAgent, Street View and Layer Swipe
there repeated the three bullets directly above it. The sentence itself
stays: a reader scanning for what is still MapLibre-only deserves an
answer rather than silence, and the later "The offline (local PMTiles)
basemap is MapLibre-only as well" reads off it.
|
||
|
|
c6f7c23365 |
feat(swipe): run Layer Swipe on the Mapbox renderer (#2427)
* feat(swipe): run Layer Swipe on the Mapbox renderer The control drives both maps only through the Style Spec surface the two engines share — `getStyle`, `addSource`, `addLayer`, `setLayoutProperty`, `jumpTo`, `resize`, `isStyleLoaded`, `get`/`setProjection`, `on`, `remove`. The one MapLibre object it built itself was the clipped comparison pane, and a MapLibre map cannot be layered over a mapbox-gl canvas. maplibre-gl-swipe 0.13.0 takes a `createMap` factory, and the plugin feeds it mapbox-gl's `Map`. Two Mapbox specifics come with it. The pane is handed the access token explicitly: mapbox-gl reads its token from the global `mapboxgl.accessToken` unless the constructor is given one, and GeoLibre passes it per map, so a second map built without it renders nothing and logs every frame — `getMapboxAccessToken` on the engine and the plugin API exists for exactly that. And the basemap grouping is seeded with the upstream's new `basemapLayerIds` whenever the active basemap URL is not fetchable, because `fetch` rejects a `mapbox://` scheme outright; `getBasemapLayerIds` is the renderer-neutral read for it, the same seam the layer control already uses. The deck.gl raster provider stays MapLibre-only. It mirrors COG and maplibre-gl-raster layers onto the comparison pane, and both of those controls register MapLibre tile protocols, so neither draws on Mapbox at all. A project authored on MapLibre can still carry such layers; the panel omits them rather than offering sides for layers that are not on screen, and the store subscription that refreshes them is not registered there either. Also reads the style's layer order through `getStyle().layers` when `getLayersOrder` is absent — that helper is MapLibre's, and the project-layer id expansion runs on both engines now. Verified in the browser on a `primaryRenderer: "mapbox"` project in both themes: the comparison pane's canvas is mapbox-gl's (no maplibre canvas anywhere), the grouped Background row is present, assigning a layer to the right side hides every style layer it compiles to on the main map and draws it on the clipped pane, the slider drags, and the console is clean. Covered by e2e/mapbox-swipe.spec.ts plus unit tests for the factory, the token, the provider gating and the basemap-id fallback. Part of #2420 (Swipe item). Upstream: opengeos/maplibre-gl-swipe#26 (v0.12.0) and #27 (v0.13.0). * test(e2e): assert the swipe pane's token directly, not console text CI has no MAPBOX_TOKEN and the spec falls back to a placeholder, so mapbox-gl logs "A valid Mapbox access token is required" for the fake one — which the console-text assertion could not tell apart from "no token was passed", the regression it was actually for. Read the comparison pane's own access token and compare it to the main map's instead. That is exactly the thing being checked, and it is independent of whether the token is real: verified by dropping the accessToken from the factory, which fails the spec. * review: resolve project layer ids under Mapbox's naming scheme too The biggest of these: `styleLayerIdsForProjectLayer` only knew MapLibre's `layer-<id>-*` over `source-<id>`, but the Mapbox engine compiles the same store layer as `geolibre-mapbox-<id>-<sourceLayer>-<kind>` over `geolibre-mapbox-<id>`. A saved project names its swipe sides by store id, so on Mapbox those sides never resolved and the layer stayed drawn across the whole map — the bug #2161 is about, in a new engine. Both schemes are now matched unconditionally: they cannot collide, and only one is ever on a given map. The e2e clicked checkboxes rather than restoring saved sides, which is why it did not catch this (CodeRabbit). Also from review: - `basemapLayerIds` is passed only when there are some. An empty array is truthy upstream, so passing one suppressed the fetch *and* left the grouping empty. Mapbox Standard is exactly that case — it arrives as a style import, so the root style has no layers of its own — and there the fetch failing harmlessly is the better outcome (CodeRabbit). - `onBasemapChange` fires when the store's URL changes, before the engine has applied it, so a synchronous rebuild read the *previous* basemap's ids. Where the ids are the signal, the rebuild now waits for `style.load`, as the layer control host already does (CodeRabbit). - The e2e checked only that the comparison pane had a canvas. It now asserts the assignment the control applied there — the right-side layer visible, the left-side one hidden, exactly inverted from the main map (CodeRabbit). - Failed responses are recorded with their URL and status instead of dropping every "Failed to load resource" console line; only 401/403 from the Mapbox API hosts are ignored (CodeRabbit). - docs called the comparison pane a "MapLibre object" two lines before saying it is built from mapbox-gl's `Map` (CodeRabbit). * review: do not count a cancelled tile as a failed request Every pan and projection switch abandons the requests for the view it left, and `requestfailed` fires for those aborts too. A spec that drives the camera would report them as failures on a slower runner. * swipe: handle a refused control add, and record a Mapbox teardown leak Browser-testing a basemap change with the swipe active on Mapbox turned up two things. One is fixed here: `rebuildSwipeControl` ignored the result of `addMapControl`. The host refuses it once a later activation supersedes the app a subscription closed over, and publishing the refused control anyway left `swipeControl` naming one that was never mounted — a later remove would not find it. Every other call site in this plugin already handles that; this one now does too, and only publishes the control once the add succeeds. The other is not fixed, and is documented rather than left to be discovered: changing the basemap while the swipe is active leaves the previous comparison pane — and the live map inside it — orphaned on the Mapbox canvas. The swipe keeps working against the new basemap. The same sequence on MapLibre leaves one pane with identical plugin code, so it sits in the Mapbox control lifecycle rather than in this plugin. Filed as #2430 with the measurements and the five hypotheses already ruled out, including that it is not caused by this PR's deferred rebuild (forcing the previous synchronous path leaks identically). Also picks up maplibre-gl-swipe 0.13.1, whose `onAdd` tears down a previous mount instead of orphaning its slider and clipped pane — a real hazard for any host that repositions a control, found while investigating the above, though not the cause of it. * fix(swipe): show layer names, not style layer ids, in the Mapbox panel The Layer Swipe panel drives its two sides by *style* layer id, so its rows are named from a bridge the engine publishes on the window (`__GEOLIBRE_LAYER_LABELS__`, rewritten into the DOM by `swipe-style.ts`). MapController has always published it; MapboxEngine never did, so on Mapbox every row read `geolibre-mapbox-<uuid>-geojson-fill` while the same layer showed "Counties Polygons" on MapLibre — and store layer ids are UUIDs, so the ids were not merely ugly but useless. MapboxEngine now publishes the same bridge for its own id scheme, and the two engines share one module (`layer-labels.ts`) rather than two copies of the window contract: the key, the basemap row's key, and the suffix vocabulary that turns a style layer's kind into a word a user knows — fill to "Polygons", line to "Lines", circle to "Points" — so a layer reads identically whichever engine draws it. A layer that compiles to one style layer keeps its bare name. `setBackgroundLabel` is implemented rather than ignored on Mapbox for the same reason: the panel groups every basemap style layer under one row and takes its name from the bridge, so the translated label has to reach it. The bridge's change event is now dispatched defensively. The names are on the window either way and the app re-reads them on its own panel mutations, but a DOM that cannot dispatch — the headless one the engine tests run against — must not take a layer sync down with it. Verified in the browser: dropping a GeoJSON and activating the swipe shows "states Points / Lines / Polygons" on Mapbox, matching "states Lines / Polygons / Background" on MapLibre, where the labels are unchanged. * swipe: read the label bridge's contract from one place The app declared its own copy of the window interface and spelled the basemap row's key as a literal, next to an engine that now exports both. Import them instead, so the producer and the consumer cannot drift. * swipe: take a layer's style rows from its plan, not from an id prefix Naming scanned the whole style for `geolibre-mapbox-<id>-`, which a layer named `a` shares with one named `a-b`. Take the ids the engine actually compiled for the layer instead, plus any a plugin registered itself, filtered against the live style so nothing is named before it exists. The first version of the test for this passed either way: both layers' rows end up correctly named regardless, because the neighbour is processed second and overwrites them. The qualifier is what actually shows the over-reach — a layer drawing through one style layer is named bare, and four rows would wrongly make it "A Raster" — so the test now pairs a single-row raster with a three-row neighbour, and fails against the prefix scan. * swipe: reach the label bridge by subpath, not through the package barrel `swipe-style.ts` is a side-effect import from the app entry, and I had it importing from `@geolibre/map` — whose barrel pulls in the MapLibre, Mapbox and Cesium canvases that each work to stay off the boot path. Give `layer-labels` its own subpath export, as `style-layer-ids` and five other modules already have, and import that. To be clear about what this does and does not buy: Rollup tree-shook the barrel fine, and the App chunk is the same size either way (1,963.25 kB with the barrel, 1,963.72 kB with the subpath — noise). This is not a fix for measured bloat. It is a guard: a package barrel reached from the entry is one non-shakable side effect away from dragging an engine into the boot chunk, and the subpath states the dependency that is actually wanted. * test(e2e): wait for the published names before reading the swipe panel CI needed a retry on both themes of this spec, on the label assertion: the panel still held `geolibre-mapbox-West-geojson-fill` when it was read. The rename is asynchronous in two places, and the assertion raced both. The engine's `syncLayers` returns early while the style is still loading and publishes names only on the pass that completes, so the panel can be built from style layer ids before a name exists for any of them; and the app rewrites the rows on the animation frame after the panel mutates. Both self-heal — the publish fires the change event that re-runs the rewrite — which is exactly the shape a single read gets wrong on a slow runner and right on a fast one. So wait for the publish, then poll the DOM. Honest about the verification: this passes locally, but locally it passed before the change too. I tried to reproduce the CI timing by delaying the rewrite and could not — that probe delays the wrong half, and the repeated mutations keep re-scheduling it. CI is the real check. * Address review feedback - maplibre-swipe: keep at most one `style.load` rebuild outstanding, and cancel a waiting one on deactivate. Clicking through two basemaps before the first style landed left two handlers on one event; both fire in the same tick, so the first rebuild's control was torn down and replaced before it drew anything. A handler that outlived the activation rebuilt a control for an inactive plugin. - mapbox-engine: derive the row suffix from a plugin-adopted native id too, not only from ids this engine compiled. ArcGIS names its own style layers, so `parcels-fill` and `parcels-line` both came out as bare "Parcels" and the swipe panel offered the same row twice. - layer-labels: name a `raster` row, and title-case any suffix the table does not map (a vector-tile source layer's own name lands there) so it does not read as a stray lowercase token beside the mapped ones. - mapbox-engine: `getMapboxAccessToken()` returns `null`, not `""`, when no token is configured — matching what the plugin API documents, and letting a caller omit the key instead of handing a second mapbox-gl map a blank `accessToken`. - e2e/mapbox-swipe: forgive only Mapbox's bookkeeping URLs, not every request to a Mapbox host. The host check also swallowed DNS and connection failures, and both maps use a third-party style, so a failed Mapbox content request would be a genuine surprise. - docs/mapbox-renderer.md: restore the two-space list-continuation indent on the Geo Editor bullet's wrapped line. * Address review feedback - mapbox-engine: only the primary pane publishes and clears the swipe panel's layer-name bridge. It is one window global, and MapGrid mounts an independent MapboxEngine per Mapbox pane: a secondary pane draws the same layers filtered by its own visibility, so its publish republished a subset (wrong sibling count, wrong qualifiers) and its destroy wiped the primary's names until the primary's next sync, leaving the panel on raw style layer ids. MapboxCanvas passes `ownsLayerLabels: !viewId`. - maplibre-swipe: cancel a pending `style.load` rebuild on every basemap change, not only in the branch that queues one. Switching from a `mapbox://` basemap to an `https://` one rebuilt immediately while the earlier handler stayed attached, so it fired on the new style and tore down the just-built control and its comparison map's WebGL context. Both paths are covered by tests that fail against the previous code. |
||
|
|
37adc4725b |
feat(streetview): run Street View on the Mapbox renderer (#2426)
* feat(streetview): run Street View on the Mapbox renderer Everything `maplibre-gl-streetview` touches is on the Style Spec surface both 2D engines share — `getContainer`, `on`/`off`, a click event's `lngLat`, and `LngLat` only as a value carrier — except the location marker. MapLibre's `Marker` reads `map._camera.transform` on every position update, which a mapbox-gl map does not have, so the first map click threw there. maplibre-gl-streetview 0.8.0 takes a `createMarker` factory: the control still builds, styles and rotates its own marker element, and only the positioning comes from the engine. On a Mapbox host the plugin feeds it mapbox-gl's `Marker` through `app.getMapboxGl()`; on MapLibre it passes nothing and the upstream default (MapLibre's own `Marker`) stands. A renderer swap tears every active plugin down and re-activates it, so the rebuilt control picks up the factory of whichever engine is now primary. Verified in the browser on a `primaryRenderer: "mapbox"` project: the Plugins menu no longer greys the entry out, the control mounts in the Mapbox control corner, a map click places a `.mapboxgl-marker` (and no maplibre marker) that tracks the map through pans, Mapillary imagery loads in the panel, and a swap back to MapLibre rebuilds with MapLibre's marker — all with zero console errors. Covered by e2e/mapbox-streetview.spec.ts, which feeds the control a key through its own Keys tab and mocks the provider so it needs no Street View account, plus unit tests for the factory. Part of #2420 (Street View item). Upstream: opengeos/maplibre-gl-streetview#16, v0.8.0. * test(e2e): ignore tokenless 401s in the Mapbox Street View spec The spec asserted no console errors at all. CI has no MAPBOX_TOKEN, so every request to api.mapbox.com comes back 401/403 and the browser logs a bare "Failed to load resource" for each. That says nothing about the marker path under test, which is asserted through the DOM the control produces — the marker is mapbox-gl's, there is no maplibre one, and it tracks the map. * review: decide the marker engine on the renderer, not the namespace CodeRabbit spotted a real race. `getMapboxGl()` only answers once MapboxEngine has mounted, but the Plugins menu and PluginManager.supportsEngine both gate on the store's `primaryRenderer`, which flips synchronously. A plugin activated in that window got `undefined`, kept MapLibre's Marker for the control's whole lifetime, and threw on the first map click — the exact bug this PR is about. Commit on the renderer instead, and read the namespace when a marker is actually built: that happens inside the control's onAdd, by which point the map it is being added to necessarily exists. The unreachable case throws rather than silently placing a marker that would fail later with a confusing message. Also replaces the e2e's console-text filter, which dropped every "Failed to load resource" line and would have hidden a genuinely broken asset. Failed responses are now recorded with their URL and status, and only 401/403 from the Mapbox API hosts are ignored. * review: do not count a cancelled tile as a failed request Every pan and projection switch abandons the requests for the view it left, and `requestfailed` fires for those aborts too. A spec that drives the camera would report them as failures on a slower runner. * Address review feedback - streetViewMarkerFactory now decides on the renderer alone. OR-ing the renderer with the namespace was wrong in the reverse direction: swapping *away* from Mapbox flips `primaryRenderer` to "maplibre" while the engine ref still holds the outgoing MapboxEngine, so the OR committed a control being rebuilt for MapLibre to a Mapbox marker — which by `onAdd` has no namespace left and throws. The renderer is right in both directions; the namespace stays a fallback only for a host that reports no renderer (Claude review). - Moved the stranded "Stand in for Google's metadata and embed endpoints" doc back above `mockStreetViewProvider`, where it belongs. My helper insertion had left it above an unrelated const with its own doc (Claude review, twice). - The e2e no longer forgives Mapbox failures by token state. It ignores only the bookkeeping endpoints — telemetry and `map-sessions`, which serve no map content and reject even for a valid token — and does so unconditionally. The project points at a third-party style, so no Mapbox content is fetched and a content failure would be a genuine surprise worth reporting (CodeRabbit). |
||
|
|
bfb0c8ea3c |
feat(flight-simulator): fly the Mapbox renderer's free camera (#2425)
* feat(flight-simulator): fly the Mapbox renderer's free camera
MapLibre dropped Mapbox's `setFreeCameraOptions` and replaced it with
`calculateCameraOptionsFromCameraLngLatAltRotation`, which is how the simulator
has placed the camera on the 2D map; a mapbox-gl map has neither that helper nor
`setCenterClampedToGround`. It still has the free camera, so the plugin gains a
third adapter that writes the aircraft straight into it: a `MercatorCoordinate`
carrying the altitude plus `setPitchBearing`, from which mapbox-gl derives the
same center and zoom the MapLibre helper computes by hand. Center clamping has
no counterpart and needs none — the free camera positions the eye, not the map
center. The flight model, keyboard, HUD and animation loop are unchanged.
`flightTarget` now returns a tagged handle so the three engines pick their own
adapter, and the plugin declares `engines: ["maplibre", "mapbox", "cesium"]`.
Terrain is read through `app.isTerrainEnabled()` rather than `map.getTerrain()`:
the Mapbox Standard style imports its own terrain, so the map reports terrain
while GeoLibre's DEM is off, and the flight would measure its height above a DEM
it never turned on.
Each camera write carries the flight token, and `MapboxCanvas` skips tagged
`moveend` events the way `MapCanvas` does — without it the store's saved view
would be overwritten ~60 times a second. One setting cannot work here:
mapbox-gl 3 has no camera roll axis at all, so "Bank the horizon in turns"
leaves the horizon level (the aircraft still banks, and a bank still turns it);
the panel says so while Mapbox is primary.
Also guards the Atmospheric Effects halo against a non-finite disc. A steeply
pitched globe can project a silhouette sample behind the horizon, and
`createRadialGradient` throws on that, taking the effects render loop down —
reproducible on Mapbox with a bare `jumpTo({pitch: 78})`, and hit every frame
once a flight is in the air.
Verified in the browser on a `primaryRenderer: "mapbox"` project in both themes
(terrain on/off round trip, altitude above the queried DEM, arrow-key bank and
turn, zero console errors, full restore on Esc) and covered by
e2e/mapbox-flight-simulator.spec.ts plus unit tests for the new adapter.
Part of #2420 (Flight Simulator item).
* test(e2e): make the Mapbox flight spec survive a tokenless CI runner
Two CI-only failures, neither about the feature.
The spec asserted no console errors at all. CI has no MAPBOX_TOKEN, so every
request to api.mapbox.com — the DEM the simulator flies over among them — comes
back 401/403 and the browser logs a bare "Failed to load resource" for each.
Assert on app errors instead; the flight itself is checked through the map's own
camera and state, which those requests do not affect.
And the bank assertion held Arrow Right for a fixed 2s and expected a fixed
angle. The turn is integrated per animation frame, and a software-WebGL runner
draws far fewer of them, so it hold the key until the heading actually moves.
The movement and moveend assertions get the same treatment.
* review: name the roll axis in four locales, tighten the e2e failure filter
- ar/fa/ka/vi said "camera rotation", which is broader than the English
"camera roll" and could read as Mapbox having no bearing or pitch either.
Name the axis explicitly in each (CodeRabbit).
- The e2e dropped every "Failed to load resource" console line, which would
also hide a genuinely broken app asset. Record failed responses with their URL
and status instead, and ignore only 401/403 from the Mapbox API hosts — the
ones a tokenless CI run is expected to produce. requestfailed is handled the
same way (CodeRabbit).
- docs/architecture.md said these plugins "declare both engines" in a sentence
that now covers three (CodeRabbit).
* review: do not count a cancelled tile as a failed request
Every pan and projection switch abandons the requests for the view it left, and
`requestfailed` fires for those aborts too. A spec that drives the camera would
report them as failures on a slower runner.
|
||
|
|
379dc20547 |
feat(arcgis): add the ArcGIS Maps SDK for JavaScript as an optional CDN-loaded rendering engine (#2423)
* feat(arcgis): add the ArcGIS Maps SDK for JavaScript as an optional CDN-loaded rendering engine Adds "arcgis" as a fourth renderer next to MapLibre, Mapbox and Cesium. Nothing is bundled: the engine imports the SDK's ES modules from Esri's versioned CDN at runtime (packages/map/src/arcgis-sdk.ts), so dist/, the wheel and the installers stay the same size. - arcgis-layers.ts compiles store layers into SDK layer plans; GeoJSON symbology is evaluated per feature with the style-spec engine and baked into a unique-value renderer, filters and labels included. XYZ/WMTS templates, WMS GetMap templates, vector tiles, ArcGIS services and image overlays have translations. - arcgis-engine.ts implements MapEngine: camera (rotation vs bearing), layer sync and ordering, basemap translation via the shared cesium-imagery catalog plus Esri basemap styles when an API key is present, hit-test identify, highlight, extent drawing, manual placement, capture, and the SDK widgets as built-in controls. - ArcgisCanvas.tsx owns construction and the store subscription; PrimaryArcgisCanvas hosts it in the shell and split panes. - Device-local ArcGIS API key under Settings → Environment Variables, ARCGIS_API_KEY bridge in vite.config.ts, getArcgisApiKey in core. - Add Data greys out sources without an adapter; layer panels badge "No ArcGIS". - CSP allow-lists js.arcgis.com in tauri.conf.json and nginx.conf; the service worker caches the SDK for offline reuse. - Python, MCP, embed and agent-skill renderer lists accept "arcgis"; project format carries preferences.map.arcgisBasemap. - Tests: arcgis-layers, arcgis-engine (fake SDK), arcgis-renderer; opt-in e2e/arcgis-renderer.spec.ts; translations for every locale; docs/arcgis-renderer.md and a maintenance note. Refs #2421 * fix(arcgis): use the view's own attribution and stop rebuilding layers on opacity changes - Drop the deprecated Attribution widget: the 5.x view draws attribution itself while `attributionVisible` is on (Esri requires it), so the engine sets that instead of mounting a widget, which also removes the deprecation warning. - Ignore the SDK's "Aborted" layer-view errors and errors for layers the engine has already removed: a restyle rebuilds the native layer and the outgoing one's cancelled load was surfacing in the error banner once per slider tick. - Compile vector-tile styles at full opacity and visible so the VectorTileLayer's native opacity/visibility carry those, and an opacity change no longer rebuilds the layer. - `fullExtent` is read-only on VectorTileLayer; only tile and WMS layers receive it. Refs #2421 * Address CodeRabbit review feedback - useArcgisApiKey: read the device-local key directly as a fallback so an ArcGIS pane that mounts with the app gets the saved key before the first runtime-env projection (which fires no change event); the resolver still wins once it answers. - vite.config.ts: give the ArcGIS SDK its own `geolibre-arcgis-sdk` CacheFirst cache scoped to the versioned `/5.1/` path, so its few hundred modules cannot evict the other CDN engines from the shared 400-entry cache. - DesktopShell / e2e: anchor the file-drop target with `data-testid="desktop-shell"` and fail loudly in the spec when it is missing instead of falling back to `document.body`. - arcgis-sdk.ts: reject HTTP error responses before installing the stylesheet, and tag each theme request with a generation so a stale fetch can neither overwrite a newer theme nor clear its pending state. - ArcgisCanvas: accept a translated `closeLabel` for the identify popup's close button; the desktop caller passes `t("common.close")`. - test_map.py: round-trip a mixed 1x3 layout so a MapLibre pane survives beside the ArcGIS primary and pane. * Address CodeRabbit review feedback - Allow `https://js.arcgis.com/` (and `data:`) in `font-src` in both the Tauri and nginx content-security policies: the SDK's inlined theme stylesheet loads its Avenir Next and icon WOFF2 files from the CDN, which `default-src 'self'` alone would block. Docs updated to match. * Address CodeRabbit review feedback - docs/arcgis-renderer.md: name the dedicated `geolibre-arcgis-sdk` service-worker cache instead of the shared `geolibre-cdn-engines` rule the SDK no longer uses. * Address CodeRabbit review feedback - docs/arcgis-renderer.md: limit the offline claim to the cached SDK bootstrap; remote basemaps and services still need the network. * Address Claude review feedback - arcgis-layers.ts: translate a FeatureServer layer's filters (quick filters, expression, time and embed filters, rule visibility) into an SQL `definitionExpression` via `filterToSql`; a filter with no SQL form is flagged on the plan and the engine reports that the service draws unfiltered instead of staying silent. - arcgis-layers.ts / arcgis-engine.ts: place georeferenced images by their four corners through a `ControlPointsGeoreference` (image pixel size read on load) instead of collapsing them to an axis-aligned extent, so rotated and skewed fits match MapLibre. - arcgis-engine.ts: destroy the outgoing custom basemap on every swap, including into a named Esri style, and re-apply the stored basemap visibility/opacity once a named style's layers have loaded; a style that fails to load is reported. - arcgis-sdk.ts: redact `token=`/`apiKey=`/`api_key=` values without a leading `?`/`&` as well. * Address CodeRabbit review feedback - docker/nginx.conf: drop the Esri CDN from the JupyterLite prefix's CSP (script-src and the font-src directive added for it). The in-app notebook is an iframe whose client drives the parent GeoLibre document over postMessage, so the ArcGIS SDK only ever loads under the main application policy, which keeps its entries. * Address Claude review feedback - arcgis-layers.ts / arcgis-engine.ts: point markers now render. A Style-panel marker (shape or custom SVG) compiles to a placeholder carrying its per-class colour and scale; the engine bakes the same sprites MapLibre uses (`renderMarkerCanvas`) into `picture-marker` symbols and swaps them in, drawing the circle fallback until then. A KML feature's own icon becomes a `picture-marker` directly. - arcgis-engine.ts: `identifyFeatures` performs a real synchronous query for any coordinate by testing the store's GeoJSON geometry (point-in-polygon, pixel-tolerance for points and lines, filters and visibility honoured), so the scripting, notebook and command bridges get answers; the cached hit-test result still serves the clicked location. - arcgis-engine.ts / ArcgisCanvas.tsx: the async hit test bails out when the engine was destroyed mid-flight, guards the post-await `toMap`, and the canvas catches a rejection instead of leaving it unhandled. - Tests for the marker placeholder and KML icon plans, the geometry hit tests, the synchronous identify, and a hit test that outlives the engine. * Address Claude review feedback - scripts/credential-patterns.json: list `VITE_ARCGIS_API_KEY` so redistributable builds strip it and the credential scan covers it, like the sibling tokens. - store.ts: a basemap pick clears the pinned ArcGIS style whenever any pane draws with ArcGIS, not only the primary, so split panes follow the picker (Blank included). - arcgis-engine.ts: identify falls back to the hit graphic's own geometry (converted from the view's spatial reference) and object id for service layers whose features never live in the store. - arcgis-engine.ts: GeoJSON layers declare their three synthetic fields with explicit widths instead of letting the SDK infer them from sampled features, so long labels are not truncated. - arcgis-sdk.ts: redact both Esri key prefixes (`AAPK` legacy API keys and `AAPT` API key credentials). - add-data-renderer.ts: use the catalog's real ids (`deckgl-viz`, `gltf-model`) and also grey out the Vector and Raster panels, which are MapLibre controls with nowhere to mount on this engine. - useArcgisApiKey.ts: the device key fills in only while the runtime environment has no ArcGIS entry at all, so an explicit empty project override still suppresses it. - arcgis-layers.ts: remote GeoJSON-URL plans carry the resolver's `zoomDependent` flag so zoom-interpolated styles re-bake on zoom. |
||
|
|
7439287280 |
feat(overture): run Overture Maps on the Mapbox renderer (#2424)
* feat(overture): run Overture Maps on the Mapbox renderer mapbox-gl 3.30+ reads .pmtiles archives through its own tile provider, so the plugin no longer needs MapLibre's pmtiles:// protocol there. On a Mapbox host it hands maplibre-gl-overture-maps 0.4.0 its new nativePmtiles option (plain https archive URLs, no addProtocol) and a createPopup factory built from mapbox-gl's Popup, and declares engines: ["maplibre", "mapbox"]. The store mirrors record the engine-appropriate URL, so a renderer swap flips between the protocol URL MapLibre needs and the plain URL Mapbox reads. Engine side, the Overture mirrors are plugin-owned rows on Mapbox: their source.url would otherwise be compiled into a second copy of every theme, and the paint mirror now leaves control-rendered rows (customLayerType) alone the way MapLibre's ordering-only path does, so the control keeps its x-ray colors. The desktop CSP allows the PMTiles provider script mapbox-gl imports from api.mapbox.com/mapbox-gl-js/. Verified in the browser on the Mapbox renderer in both themes (buildings theme loads 12k features, click popup is a mapbox-gl popup, Layers-panel visibility round trip, Mapbox -> MapLibre -> Mapbox swap) and covered by e2e/mapbox-overture.spec.ts plus unit tests for the plugin host and engine. Part of #2420 (Overture Maps item). Upstream: opengeos/maplibre-gl-overture-maps#11, v0.4.0. * review: keep story fades reaching control-rendered layers on Mapbox The paint mirror now skips control-rendered rows, which also dropped a story chapter's transient opacity for them. Apply that opacity directly to the control's native layers (replacing it, as MapLibre's setStoryLayerOpacity does), remembering the control's own paint so restoreLayerStyles hands it back. The opacity-property table moves to map-engine.ts so both 2D engines share it (CodeRabbit). * review: mirror the PMTiles provider host into the web CSP, drop stale story backups - docker/nginx.conf's two script-src lists allow api.mapbox.com/mapbox-gl-js/ like the Tauri CSP, so the browser build can load mapbox-gl's PMTiles provider too. - A control-rendered row that leaves the store while a story fade is active gets the control's paint back immediately and its snapshot is dropped, so a row re-added under the same id is not handed an outdated one (claude-review). |
||
|
|
73728aca89 |
feat(geo-editor): run the Geo Editor on the Mapbox renderer (#2422)
* feat(geo-editor): run the Geo Editor on the Mapbox renderer Geoman routes every engine call through one map adapter, and only three of its members build MapLibre objects: the Marker behind vertex handles and the cursor marker, the LngLatBounds behind the cut tool's query box, and the promise-style loadImage for the default marker icon. On a mapbox-gl map those throw or never resolve, so the plugin swaps them for mapbox-gl's (geo-editor-mapbox.ts). Geoman also pushes MapLibre GeoJSONSource diffs through updateData, which mapbox-gl only offers as a whole-feature upsert on dynamic sources; each Geoman source wrapper now keeps a feature mirror and replaces the source with setData instead. The toolbar's rotate and feature-properties popups come from maplibre-gl-geo-editor's new createPopup option, fed mapbox-gl's Popup. Plugins reach the mapbox-gl namespace through the new app.getMapboxGl() (MapboxEngine.getMapboxGl), the Mapbox counterpart of the Cesium scene handle. The plugin reads the map through getStyleMap, declares both 2D engines, suppresses the Sketches store layer under the Mapbox engine's layer ids as well, and picks Mapbox's Open Sans glyphs for text markers when the style has no font to borrow. The load-features dialog resolves the map on either engine. mapbox-gl popups get the z-index lift MapLibre popups already receive from maplibre-gl-components, so they open above the Effects canvas. Refs #2420 * Address Claude review feedback - Clone the features `replaceFeatures` seeds the Geoman source mirror with, as the diff's `add` path already did, so later in-place diff updates cannot leak into Geoman's own objects. - Give `.mapboxgl-popup` its z-index lift with `!important`, matching the maplibre-gl-components rule it mirrors: mapbox-gl.css is loaded by a dynamic import after index.css, so a same-specificity rule there would win. - Set `crossOrigin = "anonymous"` on the marker image element so a remote icon would not taint the WebGL texture `addImage` uploads. - Drop the dialog's duplicate `styleMapOf`: `getStyleMap` now accepts anything with the `getMap` / `getMapboxMap` doors, is exported from @geolibre/plugins, and the load-features dialog passes the engine to it directly. * Address review feedback - Refuse to activate on a Mapbox map when the host does not hand out the mapbox-gl namespace, instead of mounting Geoman with MapLibre's marker and image paths on a mapbox-gl map (CodeRabbit). - Drop the unused FeatureCollection type import (Claude review). |
||
|
|
52ef0c399a |
feat(mapbox): enable the Plugins menu on the Mapbox renderer (#2419)
* feat(mapbox): enable the Plugins menu on the Mapbox renderer Most Plugins-menu entries were greyed out on the Mapbox renderer because every plugin defaulted to engines: ["maplibre"] and read the map through app.getMap(), which the Mapbox engine leaves null. Thirty-five plugins now declare Mapbox support and read the 2D map through getStyleMap(app), which falls back to app.getMapboxMap() and is honest for the Style Spec surface both engines share. Shared infrastructure: - style-map.ts (getStyleMap) and the docked right-panel bridge mount on the Mapbox map; the plugin engine audit scans Mapbox-declaring plugins for bare app.getMap() reads and MapLibre-only members. - The Mapbox engine adopts plugin-created raster/WMS layers under their native ids (no duplicate copy, rebuilt on style.load), treats source-less external native records, time-slider, timelapse and OAM footprints as plugin-owned, mirrors store visibility/opacity onto their native layers through the shared paint builders, and flushes a deferred sync when a source finishes loading (a map with an animated canvas source never idles). - index.css styles .mapboxgl-control-container like MapLibre's so the Effects canvases cannot bury the controls; DesktopShell reattaches the route animation above the nativeMapInstance gate. Ported: FEMA NFHL, NASA Earthdata, EnviroAtlas, National Map, USGS NLDI, Vantor, Planet/CKAN/Socrata/Natural Earth/Portolan, Earthdata GIS, OpenAerialMap, ArcGIS Hub, Source Cooperative, Hugging Face, GeoLens (public data), Esri Wayback, USGS LiDAR, Mapillary, Gridlines, H3, S2, A5, DGGRID, DGGAL, OLC, Geohash, Tilecode, Time Slider, Timelapse, Elevation Profile, Elements, Dimensions, Route Animation, Clouds, Precipitation, Effects, Sun. Still MapLibre-only, documented with reasons: Street View, Overture Maps, GeoAgent, Swipe, Geo Editor, Flight Simulator. * Address Claude review feedback - mapbox-engine: mirror the whole paint object from the shared builders onto plugin-owned native layers (with a per-property try/catch), not only the opacity key, so Style panel colour/width/radius edits on Mapbox reach layers like Mapillary's coverage; NATIVE_OPACITY_PROPERTY is gone. - Grid plugins (H3, S2, A5, DGGRID, DGGAL, OLC, Geohash, Tilecode, Gridlines): guard the teardown in deactivate against a removed mapbox-gl map, which throws from getLayer during a renderer swap. - Gridlines: cache the upright basemap font in pickTextFont like its siblings do. - Route Animation: the teardown catch is a no-op comment instead of an unrelated iconColor mutation. - Plugin engine audit: the getMapboxMap fallback is checked within the statement on comment-blanked source (wrapped fallbacks pass, trailing comments do not), and destructured or aliased getMap reads count as reads. - ArcGIS Hub: refresh the stale engines comment. * Address review feedback - mapbox-engine: derive the story-opacity layer before the plugin-owned branch so a chapter fade reaches mirrored native layers too (test added). - Timelapse: guard dispose/removeStack in deactivate against a removed mapbox-gl map and reset the store and module state in a finally block. - mapbox-engine test: the controlOwnsPaint case requests a different opacity and asserts no paint setter runs. - Plugin engine audit: subdirectory plugins (elevation-profile/index.ts) are enumerated as entry points. - Components: reword the two audit opt-outs to say the plugin is MapLibre-only and the reads are reached from the STAC closure. - style-map: setTerrain/getTerrain exist on mapbox-gl; the docblock no longer lists them as missing (terrain goes through the host either way). - Time Slider: enforceEngineSupport iterates a snapshot of getSources(). * test: note the receiver-name limit of the Mapbox getMap audit * Address Claude review feedback - Time Slider: explain why enforceEngineSupport tests the raw URL template synchronously (the extension it keys off never sits in the date placeholder), unlike the date-resolved read in ensureSourceBounds. |