mirror of
https://github.com/opengeos/GeoLibre.git
synced 2026-11-04 10:43:36 +00:00
main
111
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82cea2a60e |
fix(plugins): harden the zip entry listing for file handlers (#3320)
* fix(plugins): validate the zip end record and decode CP437 entry names The scan from the end took the first end-record signature it found, so an archive comment containing the signature broke the listing. Accept a record only when its comment fits the remaining bytes and the central directory ends where the record (or the ZIP64 record) begins, otherwise keep scanning. Names without general purpose flag bit 11 are now decoded as CP437, the zip default, instead of UTF-8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugins): note on reading archives safely and folder file paths Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): accept a zip end record by where the directory starts Requiring the directory to end exactly at the end record rejected valid archives with bytes in front of them (self-extractors). A candidate is now accepted when a central directory entry signature sits at the declared offset, or, for a non-ZIP64 record, at the offset implied by the directory size. ZIP64 checks the signature at its own directory offset. An empty archive is accepted only when its record sits at the declared offset. At most 16 candidates are tried so a comment full of signatures stays cheap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugins): move the archive note to the dropped-files section Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): rank zip end records, keep unflagged UTF-8 names, skip spanned archives Prefer an end record whose directory also ends where the record begins over one that merely points at a directory start; try records with a fitting comment first and fall back to a truncated comment. Read unflagged names as UTF-8 when valid, CP437 otherwise. Accept empty classic (also shifted) and empty ZIP64 archives. Do not list spanned archives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): try the end record that closes the file first, handle signature records Order end-record candidates without extra reads: the one whose comment ends at the end of the file first, then other fitting ones. The cap now bounds only range reads, and a directory inside the tail is checked in memory, so a comment full of fake records no longer hides the real one. A digital signature record between the directory and the end record is accounted for when the offsets are shifted. The UTF-8 decoders are module constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): keep the real zip end record reachable, read signature records before the tail Rank candidates without reads (directory visibly ends at the record, exact comment fit, fitting, overrunning), evaluate every group within the read cap and stop only on a record the layout fully backs. Keep every signature record that ends at an end record, and read one bounded window when it starts before the tail. Reject ZIP64 records and locators that name a nonzero disk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(plugins): reduce the zip entry listing to the announced scope Keep the end record that closes the file first, then scan back through at most 16 candidates, accepting one only when a central directory entry really starts at its declared offset (or it is an empty archive). Drop the ranking, the prepended-data shift and the signature-record handling, and document in the code that such archives (and spanned ones) are deliberately not listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): accept an empty zip only when its directory sits at its end record Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): tighten zip end-record checks and docs Reject ZIP64 archives whose locator or ZIP64 record names more than one disk, and reject a classic record whose directory-disk field is set, so spanned archives are not listed. Reject a directory shorter than one central entry before probing it, so a lone entry signature cannot stop the end-record search. Restore an explicit bounds check on the directory before it is read. Drop the digital-signature clause from listZipEntryPaths' doc comment: the signature record sits after the declared directory, so signed zips list normally. Add regression tests for the ZIP64 disk checks and the short directory. Applied by the claude-autofix workflow. * refactor(plugins): collect zip end-record candidates without unshift Split the end-record candidates into exact and other arrays and concatenate them once, instead of prepending each exact match. The order is unchanged: exact records still come first, lowest offset first. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Applied by the claude-autofix workflow. * fix(plugins): harden zip listing reads and tighten its tests Check the ZIP64 end record read for a short read and return no listing instead of throwing a RangeError. Read a central directory signature from the tail already in memory when it falls inside it, so a comment full of candidates costs fewer IPC round trips on desktop. Add regression tests for a zero-length directory that does not sit at the end record, and for a short ZIP64 read. Applied by the claude-autofix workflow. * Address Claude review feedback - Pass ignoreBOM to both UTF-8 name decoders so a leading U+FEFF in an entry name is kept rather than silently stripped. - Rename the fake empty-archive test: the record carries a signature, so "all-zero" did not describe it. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: giswqs <5016453+giswqs@users.noreply.github.com> Co-authored-by: giswqs <giswqs@gmail.com> |
||
|
|
53345f73b7 |
feat(plugins): offer data loaded by URL to plugin file handlers (#3321)
* feat(plugins): offer data loaded by URL to plugin file handlers A file loaded with ?data= or the embed addData command never reached the file handlers added in #3305: a TASKDATA.zip failed as a GeoJSON zip even though an installed plugin could read it. After the download, and before the built-in classification, the file is offered to the registered handlers through the same routing as a drop (extension filter, claim with the zip entries, open, same error handling). A handler can also declare dataTypes; a URL with dataType=<name> then goes straight to its open, for URLs without an extension. lidar stays built in. The embed addData command now accepts dataType too. URL loads wait, bounded, for plugin activation before offering a file, keep every download ceiling, and reach handlers only where drops are allowed (not in the viewer, and with data:add). Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(plugins): offer an extensionless zip download to .zip file handlers A URL load already recognizes a zip by extension, content type, Content-Disposition or the zip signature. When the derived file name has no .zip extension, offer it as name.zip so the extension pre-filter and the zip entries work, for example for a signed link that returns application/zip. No other format is guessed. Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugins): drop the status line claim for URL-loaded files The data URL loader's success message is not shown in the UI, only its errors are, so the docs should not promise an "Opened by" status. Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): wait for the ?plugin= deep link before offering URL data to handlers The startup wait only covered the project plugin restore. A plugin that a ?plugin= link activates registers its file handler from a second effect, so a ?data= load combined with ?plugin= could offer the file too early. The settle gate now has two parts, restore and deep link, and whenPluginsSettled waits for both within the same 15 s bound. Without a ?plugin= link the deep link part settles at once. Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): bound the URL load wait, clean up after handlers, and tighten the details - One absolute 15 s deadline covers the settle parts and any activation still pending; once it passes, or startup has settled, later loads do not wait. - The restore part settles only for the restore that follows a ?url= project, and the deep link part only after that project has loaded. - A plugin hint combined with a style fails before downloading. A failed open, a style that arrives too late, or an abort removes the layers the handler added. Handled layers are framed by the shell like GeoJSON. - Remove the unused success message; sanitize the download file name (control characters, 255 characters, escaped quotes, empty names). - addData accepts dataType null; DataTypeHint keeps the lidar literal. - Docs: the handler's plugin must be active (use plugin= or a ?url= project), open must validate hinted bytes, every download outside the viewer waits up to the limit, and the first addData after ready may need a longer requestTimeoutMs. Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugins): say which plugins ?plugin= can activate for URL loads ?plugin= accepts built-in plugin names and registry plugin names. A plugin installed from a file is not found by it, so a link that relies on such a plugin needs a ?url= project that lists it as active. Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): address review findings on URL loads and file handler hints - Latch the ?url= settled flag so the loader returning to idle cannot un-settle plugin activation; settle the ?plugin= part only after registry plugins awaiting trust or installing have activated. - Count only the layers added while a handler's open runs as the handler's, so a layer the user adds during the download is never removed or framed on its behalf. - Send no style request for a hint a handler declares, and abort the download as soon as the style request fails (and vice versa). - Check the abort signal after the readiness wait, before offering. - A malformed dataType is dropped with a console warning in both the ?dataType= parameter and the embed addData option. - Registry: describe an invalid data type by its type so the warning cannot throw; re-registering an id now moves the handler to the end of the order, so hint ownership stays with the earlier handler as the warning says (the test that pinned "keeps the slot" changed). - DataTypeHint is plain string; docs say the style rule applies only to a hint a handler declares. Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plugins): address a further review round on URL file handlers - Wrap handlers for layer tracking with a proxy instead of a spread, so a class-instance handler keeps its prototype methods (claim) and every method runs with the original handler as this. - Revert the "replacement moves to the end" order change: re-registering an id keeps its slot again, as in #3305. Hint ownership is tracked apart from the order: first declaration wins, a replacement keeps the hints its predecessor owned, never takes one another live handler owns, and a hint it drops goes to the next handler declaring it. - The plugin readiness wait is raced against the load abort, for the offer, the hinted path and the style check. - A trusted registry plugin whose manifest reports a load issue counts as failed even when another plugin holds its id, so it cannot hold the startup gate until the deadline. - A dataType hint is trimmed and compared case-insensitively in both the ?dataType= parameter and embed addData; docs say so. - The startup ?data= load and the embed addData command share one queue, so two URL loads never run a handler at once. - Docs: bytes from a shared ?data= link or an allowed host reach active plugins' claim/open and must be treated as untrusted input. Refs #3283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plugins): state how URL loads reach file handlers Three review findings on the URL file handler docs are answered in docs/plugin-api.md. The layer attribution limit is now stated: a layer the user adds while a handler's open runs is counted as the handler's, since it cannot be told apart from the handler's own layers. Downloads that the built-in loaders take by extension (.tif, .tiff, .cog, .pmtiles, .parquet, .geoparquet, .las, .laz, EPT ept.json) are loaded before any download, so an extensions filter never sees them; a dataType hint still routes them to a handler. Code changes are left to maintainers: the layer attribution design, the plugin registry order on reactivation, the i18n of handler errors, the load-issue mismatch for trusted manifests, and the queue stall on a handler that never settles are marked unclear in the thread replies. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Applied by the claude-autofix workflow. * fix(embed): treat an empty addData dataType as no hint An empty dataType on addData logged a "malformed" warning, while an empty ?dataType= parameter is ignored silently. Treat the empty string as no hint in addData too, so both entry points behave the same, and cover it with a regression test. Applied by the claude-autofix workflow. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: giswqs <5016453+giswqs@users.noreply.github.com> |
||
|
|
64b4e058c9 |
feat(map): set the map CRS from a link and from plugins (#3309)
* feat(map): set the map CRS from a link and from plugins A link could not carry the map CRS, so a shared map relying on one opened on Web Mercator, where a WMS offered only in a national system stays blank. Add a `crs` host URL parameter taking an EPSG code or a preset id. It is seeded before the map exists, so the Web Mercator basemap never loads, overrides a `?url=` project's saved CRS, frames the CRS when the link has no view, and is ignored with a warning when it does not resolve. The web address now follows the CRS of the map, so a copied link reopens it. Add `setMapCrs(id | null)` to the plugin API, next to `getMapCrs` and `onMapCrsChange`, with the checks and effects of the Settings choice. Fixes #3308 * Address review feedback - Clear the controller's initial frame flag when a linked CRS fails, so a later switch does not reframe over a view the user set. - Recheck the engine in setMapCrs after the lookup, since a renderer swap can land while it is pending. Covered by a test. - Say in the embedding guide that a project keeps its saved view when a link names a CRS and no view. - Rewrap the long line in the plugin API guide. * Address review feedback - Fix the type of the fake engine in the renderer swap test, which failed the test type check. - Clear the initial frame request when a CRS has nothing to frame, so it cannot carry over to a later map. - State plainly in the comment and docs that a link's crs that does not resolve stays in the address. |
||
|
|
56a79c8f37 |
feat(plugins): let plugins register file handlers for drag and drop (#3305)
* feat(plugins): let plugins register file handlers for drag and drop A plugin could add a format's reader and an Add Data entry, but a dropped file never reached it: every extension belongs to a built-in group, .zip is always read as a shapefile, and the Tauri native drop never becomes a DOM event a plugin could listen for. Add registerFileHandler/unregisterFileHandler to the app API. Both drop paths now offer files to handlers after project files and before the built-in groups. A handler pre-filters by extension and claims by content: a .zip carries its entry list, read from the central directory, so a shapefile zip still loads as one. Dropped folders are expanded for handlers, and anything unclaimed loads exactly as before. The range read needs fs:allow-open, allow-read and allow-seek on desktop; the fs scope still limits them to paths that could already be read. Refs #3283 * Address review feedback - Count claimed keys so returning one of two same-named files claims only that one instead of both. - Lower the central directory cap to 16 MB, which still covers far more entries than a drop handler needs. - Reject non-string and empty extensions at registration, so one bad entry cannot break every later drop. - Move the drop status reporting into the tested plugin-file-drop module to stay within the untested-module allowance for useFileDrop.ts. * Address review feedback - Keep a list of dropped folders per name, so two same-named folders are both walked rather than the second twice. - Keep the success message when another handler fails, and show every failure instead of only the last. - Group the file handler imports after the menu contribution ones. * Address CodeRabbit review feedback - Align captured folders with the files list by index, so a plain file can never be mistaken for a same-named folder. - Set the success message after the error, since the error setter dismisses the current status. * Address Claude review feedback - Match files returned from claim by identity first and by key only for copies, so a handler gets the file it chose when two share a name. - Log a failed zip entry read instead of swallowing it silently. * Address review feedback - Match claim results inside the claim error handler, so a malformed result counts as not mine instead of failing the whole drop. - Skip an unreadable folder entry instead of abandoning the rest of the walk. - Index unmatched files by key so matching returned copies stays linear. |
||
|
|
4a6600ad25 |
feat(osm): improve downloader reliability and export (#3304)
* fix(osm): gate downloads by viewport area * fix(osm): add resilient download fallbacks * feat(osm): preview downloads before adding * fix(osm): allow export without preview * fix(osm): preserve preview while navigating * feat(osm): add geoparquet export * style: auto-format (ruff + oxfmt) [pre-commit.ci] * fix(osm): address review findings * fix(osm): ignore inherited keys in custom tag filter The map API fallback checked custom tag keys with `key in tags`, which also matches Object.prototype members. A filter on a key such as `constructor` with no value matched every tagged element. Use an own-property check instead and add a regression test. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Applied by the claude-autofix workflow. * fix(osm): match cached preview to current extent Panning keeps the OSM preview on the map, but Save GeoJSON and Save GeoParquet reused it without checking the bbox inputs. The export could then contain the old area while the panel showed the new one. Record the preview's bbox and reuse it only when it still matches. Also read the failed Overpass response body before the map-API fallback so its connection is released on every path. The error detail is kept. Adds a regression assertion for exporting after a pan. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Applied by the claude-autofix workflow. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: giswqs <5016453+giswqs@users.noreply.github.com> |
||
|
|
0359719211 |
fix(s3): re-probe a bucket's region after a network failure (#3278)
Follow-up to #3277. Only remember region probes S3 answered without naming a region; a network error or timeout says nothing about the bucket, so the next read probes again instead of signing for the default region for five minutes. Test added. Docs and JSDoc: an s3:// URL whose key has a "." or ".." segment rejects, since no URL can address it. |
||
|
|
130f6c8d10 |
feat(plugins): let external plugins read private S3 objects (#3277)
* feat(plugins): let external plugins read private S3 objects Add an optional resolveReadableUrl(url, signal?) member to the plugin API, backed by the host's own resolveReadableUrl: - an s3:// (or S3 HTTPS) URL in a bucket a configured S3 connection covers resolves to a presigned HTTPS URL, signed with that connection's credentials (on the desktop also AWS profiles, SSO, the environment and instance roles); - any other s3:// URL resolves to its public HTTPS object URL; - every other URL is returned unchanged. Plugins that read many files from S3 (an index and the files it lists, images) could not do so from private buckets: nothing on the plugin API presigned, and the host's credentials must not reach plugins. Only signed URLs do, and the host remembers each one's s3:// source so it is never saved. Document it in docs/plugin-api.md and cover it in the app API contract tests. * Address review feedback - resolveReadableUrl rejects a non-AbortSignal signal, never starts signing on an already-aborted signal, and rejects as soon as the signal aborts, even mid credential lookup (which ignores the signal); tests cover both abort paths and the signal check. - Docs: note whitespace trimming and the signal argument, state the trust model (any object a covered bucket holds), note that anonymous connections return non-expiring URLs, and resolve the example's relative file with new URL(), which does resolve against s3:// bases. * Address review feedback - resolveReadableUrl treats a null signal as no signal instead of rejecting it. - The S3 signer no longer passes one caller's signal into the shared region probe: a cancelled caller made the probe resolve null, so it cached a default-region URL and failed the probe for everyone sharing it. Callers now race their own signal against the probe and check it again after the credential lookup, so a cancelled caller gets no URL and caches none; a test covers both. - The in-flight abort test asserts the rejection before releasing the pending lookup, so a wait-for-lookup implementation cannot pass. - The docs example decodes the resolved URL, since new URL() percent-encodes names (a space as %20) and keys are taken literally. * fix(s3): bound the shared region probe and harden its test The shared bucket-region probe takes no signal, so a stalled browser fetch could hold its pending entry and hang every later presign for that bucket. Give it a 30 s timeout, matching the Tauri path. The cancellation test waited a fixed 10 ms for the mocked fetch to start, so under load it could hang. It now awaits a signal from the mock. The plugin-api docs example used decodeURI, which throws on a lone "%" and leaves "?" and "#" encoded. It now appends a plain relative name to the index's folder, and the prose says when keys need the plugin's own encoding. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Applied by the claude-autofix workflow. * docs(plugin-api): match S3 reading docs to the snippet Reword the S3 reading snippet comment and the paragraph after it so they describe the code shown: the file name is appended as written, and a `?` or `#` in an s3:// URL ends the key early. Drop the decodeURI reference, which the snippet never calls. Rename the local abort helper in app-api.ts to startUnlessAborted so it no longer shares its name with rejectOnAbort in abortable.ts. Its behavior is unchanged: it does not start the lookup once the signal has aborted. Applied by the claude-autofix workflow. * Address Claude review feedback - Fold the two "settle or reject on abort" helpers (app-api's startUnlessAborted and s3-signer's untilAborted) into one exported untilAborted in abortable.ts, beside its rejectOnAbort; the plugin wrapper still skips the lookup when the signal has already aborted. - Fix the doubled parenthetical in the signed-URL expiry bullet. * Address review feedback - The S3 signer no longer caches a URL signed for a guessed region (the probe timed out or S3 did not name the region), so a wrong-region signature is not reused for hours; the next read probes again. Test added. - untilAborted observes the dropped promise when the signal has already aborted, and both abort paths fall back to an AbortError for a signal-like object with no reason (shared abortReason helper). - The region-probe test's fetch mock honors its signal, so a caller's abort leaking into the shared probe fails it. - resolveReadableUrl's JSDoc notes the whitespace trimming. - Docs: keys with ? or # go through an encoded S3 HTTPS URL; s3:// keys are literal, so a space or % needs no encoding. * Address Claude review feedback - Remember a failed region probe per bucket for five minutes, so reads in a bucket without list permission no longer each pay for a probe now that URLs signed for a guessed region are not cached; the test covers both the skipped probe and the re-probe once the window passes. - Check the abort with signal.aborted and abortReason() rather than throwIfAborted(), which a signal-like object from a plugin may lack. * Address Claude review feedback - Check the signal before the credential lookup too, so a cancelled caller does not start an SSO refresh. - Docs: fetchArrayBuffer falls back to the webview, where CORS applies, for 401/403 and unreachable hosts. * Address Claude review feedback - Record a failed region probe inside the shared probe, so it holds even when every waiting caller cancelled, and forget it when the bucket's credentials are invalidated, since the failure may have come from the refused credentials. - Docs: an s3:// URL with an invalid bucket name is returned unchanged. * Address Claude review feedback - s3ObjectHttpsUrl refuses keys with a "." or ".." segment. URL parsers resolve those away (encoded or not), so s3://private/../other/x on a path-style connection was signed as a read of bucket "other" with the credentials of the connection covering "private". Tests cover the URL builder and the signer. - invalidateCredentials with only a connection id forgets just that connection's endpoint's failed probes. - Docs: a signed URL carries the access key ID (and session token), but never the secret. --------- Co-authored-by: Qiusheng Wu <qswu@amazon.com> Co-authored-by: giswqs <5016453+giswqs@users.noreply.github.com> |
||
|
|
81a02c86c2 |
feat(plugins): let external plugins add and render point clouds
Add two optional members to the plugin API: - addPointCloudLayer(source, options) adds a LAS/LAZ/COPC file or an EPT dataset as a GeoLibre LiDAR layer through the shared LiDAR control, the same path as Add Data -> LiDAR, the ?data= deep link and file drops. The layer is listed in the Layers panel, styled in the LiDAR panel, and a URL layer is saved with the project. Accepts an HTTP(S)/s3:// URL, a File, an ArrayBuffer or a Uint8Array, and resolves with the layer id. - getMaplibreGlLidar() hands plugins the host's own maplibre-gl-lidar module, like getMaplibreGlRaster(), so they can decode LAS/LAZ/COPC with the WebAssembly the host already serves and render on the host's single deck.gl instance instead of bundling a second copy. Document both in docs/plugin-api.md and cover them in the app API contract tests. |
||
|
|
73e088b94c |
feat/support XYZ Layer with {-y} placeholder (#3265)
* support XYZ Layer with {-y} placeholder
GeoLibre XYZ layers now accept {-y} in a tile URL and in a TileJSON entry, and flip that row the way QGIS and https://viewer.gishub.vn do.
* fix bot comments
* fix oxfmt
* fix minor bot comments
* fix storymap export to deal with {-y} raster tile
|
||
|
|
5f5048d2bc |
feat: let projects open Measure and Minimap collapsed, with Measure units (#3245)
* feat: let projects open Measure and Minimap collapsed, with Measure units
A project's interaction.controls entry for measure or minimap can now be
an object, {"visible": true, "collapsed": true, "distanceUnit": "feet",
"areaUnit": "acres"}, instead of a plain boolean. collapsed opens only
the control's on-map button, so a shared or HTML-exported map no longer
starts with the Measure panel over it and its tool turning the viewer's
first clicks into points. A plain true keeps today's expanded behaviour.
- Python: show_control(name, visible, *, collapsed, distance_unit,
area_unit), validated and round-tripped through save/load/to_html.
- openControlPanel accepts Measure options; Minimap gains collapsed.
- Measure builds its panel on first expand, so the terrain section and
resize styling now attach on that expand when it opens collapsed.
- New units on an open Measure control rebuild it, since its unit
dropdown only reads them when the panel is built.
Fixes #3241
* Address review feedback
- Widget replay forwards control options only for measure and minimap,
matching the desktop restore hook, so a stray option cannot keep
another panel closed.
- openMeasurePanel returns early when nothing changes on an open
control, and a rebuild for new units keeps a folded ruler folded.
- Minimap applies an explicit collapsed request immediately, so a
basemap refresh in the same turn rebuilds it in the requested state.
- show_control rejects a non-boolean collapsed instead of coercing it.
* Address Claude review feedback
- Add a test that the Python Measure unit sets match the TypeScript
lists in measure.ts, like the existing control-list drift test.
* Address Claude review feedback
- The widget replays every control on each _ui change; re-sending a
collapsed option would fold a Measure or Minimap panel the viewer had
since opened. Send a setting with options only when it changed, and
forget what was sent when the iframe reports ready again.
|
||
|
|
b8b6c18531 |
feat(plugins): tell plugins which CRS the map is drawn in (#3214)
Add getMapCrs and onMapCrsChange to the plugin app API. getMapCrs
returns { id, code, wmsSrs? } for a MapLibre map drawn in another CRS
(preferences.map.mapCrs), or null on the default Web Mercator map,
the globe and the other renderers. onMapCrsChange reports changes.
The map controller fires a geolibre:map-crs-change window event when
it enters or leaves a CRS, and when it is torn down in one, so a
renderer swap is reported too. The host reads the primary engine's
new getMapCrs and drops events that do not change the value.
Document the API, and note in the addWmsLayer docs that a WMS listed
only in the map's own CRS lines up on both builds.
Fixes #3213
|
||
|
|
c70f56ec78 |
Keep map views and plugin state in the web address (#3196)
* Keep map views and plugin parameters synchronized with web URLs * Address Claude review feedback Protect existing data, catalog, screenshot, and layout launch parameters from plugin URL writes and cover them with a regression test. * Address Claude review feedback Flush pending camera URL updates on pagehide and shell teardown, and keep the refresh directive above the shell imports. * Address plugin URL ownership review feedback Prevent conflicting plugin declarations from overwriting or clearing another plugin's URL parameters while preserving existing read dispatch. Preserve untouched inbound links when activation fails so plugins can retry. * Address CodeRabbit review feedback - Retain retired plugin URL names until host-backed cleanup so replacing a plugin cannot leave stale parameters behind. - Restrict replacement writes to current declarations and release retired claims after cleanup, with regression coverage for publishing, deactivation, and unregistration. * Address CodeRabbit review feedback - Contain host URL-writer failures so plugin deactivation, unregistration, and activation rollback can complete. - Retain retired URL claims after failed writes and mark publication only after success, preserving retries and untouched inbound links. - Cover lifecycle completion, stale callbacks, cleanup retries, and failed publication with regression tests. * Address CodeRabbit review feedback - Retain publication markers when URL cleanup fails so a later activation failure can retry removing stale published values. - Clear markers after successful cleanup or final unregistration, with regression coverage for declined, thrown, and rejected activations and fresh inbound links. |
||
|
|
a3002fe2c0 |
feat(plugins): let plugins open built-in control panels (#3190)
* feat(plugins): expose built-in control panels to plugins Share panel toggles with the scripting API so plugins can open the existing host controls. Allow Minimap zoom and size options while keeping menu visibility synchronized. * Address Claude review feedback Preserve Minimap visibility and collapsed state when updating its options. Keep repeated direct opens idempotent and retain collapsed state on basemap refreshes. * Address CodeRabbit review feedback Invalidate pending bookmark, search, measure, and print opens when closed or torn down. Forward scripting closes during initialization and cover both constructor loading and deferred show callbacks in regression tests. |
||
|
|
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). |