mirror of
https://github.com/opengeos/GeoLibre.git
synced 2026-11-04 10:43:36 +00:00
main
67
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5a826b2fbb | chore(release): v3.6.1 (#3313) | ||
|
|
44b9c42a16 |
chore(release): v3.6.0 (#3299)
* chore(release): v3.6.0 * Address review feedback - Say plugins do not receive the S3 secret key, since a presigned URL carries the access key ID, and note the trust scope. |
||
|
|
5f8582e75a | chore(release): v3.5.2 (#3268) | ||
|
|
085c75735c | chore(release): v3.5.1 (#3246) | ||
|
|
ffcf4b82e0 | chore(release): v3.5.0 (#3209) | ||
|
|
31264e594f | chore(release): v3.4.0 (#3115) | ||
|
|
20b04f9ce8 |
feat(assistant): add Vercel AI Gateway as a provider (#3018)
* feat(assistant): add Vercel AI Gateway as a provider One AI Gateway key reaches the gateway catalog through the existing Chat Completions client, with a public model picker, so it no longer has to be entered as a custom endpoint. Co-authored-by: Cursor <cursoragent@cursor.com> * style: auto-format (ruff + oxfmt) [pre-commit.ci] * Address CodeRabbit review feedback - docs/roadmap.md: separate keyless model catalog discovery from the AI_GATEWAY_API_KEY used for Chat Completions requests. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: giswqs <giswqs@gmail.com> |
||
|
|
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. |
||
|
|
c2a74d5a43 | chore(release): v3.3.0 (#2942) | ||
|
|
aa28b2a4f9 | chore(release): v3.2.0 (#2791) | ||
|
|
5b5b3b8146 | chore(release): v3.1.0 (#2680) | ||
|
|
b1221d9d40 |
feat(cesium): add Google Photorealistic 3D Tiles and Asset Depot samples to the Ion quick picks (#2551)
* feat(cesium): add Google Photorealistic 3D Tiles to the Ion quick picks Ion asset 2275207 joins Cesium OSM Buildings and Bing Aerial as a quick pick in Add Data -> Cesium Ion Asset, and the picks move from a row of buttons to a dropdown so the list can grow without widening the dialog. The dropdown mirrors the form instead of holding its own state: typing an asset id by hand, or switching the layer type, drops it back to the placeholder rather than leaving a stale pick selected. * feat(cesium): add Asset Depot sample datasets to the Ion quick picks Six samples from the Cesium ion Asset Depot join the quick picks: Japan 3D Building Data, Melbourne Photogrammetry, Melbourne Point Cloud, Montreal Point Cloud, New York City 3D Buildings, and Washington DC 2017. Every id is the one Cesium's own Sandcastle and engine samples use. Depot assets only stream once an account has added them, so the picks are split into two optgroups and the hint under the dropdown swaps to say so when a depot sample is selected -- otherwise the asset endpoint 404s and the layer silently never paints. Japan Regional Terrain is left out: it is an Ion terrain asset, and terrain is a scene setting rather than a layer here. Filed as #2552. * Address Claude review feedback - Re-picking the placeholder in the Ion quick-picks dropdown now clears the asset id, layer type, and layer name instead of returning without a state change, which left the native select showing the placeholder while the fields below still held the previous pick (the DeckVizSource "Custom model" precedent). - Moved the quick-pick assertions out of the "does not mistake an ordinary layer for an Ion asset" test into their own it() block, so a failure names the behaviour it checks. * fix(cesium): zoom to a layer whose extent only the globe knows Adding a Cesium Ion asset left the camera where it was, and the Layers panel's zoom-to did nothing for it: both go through fitLayer, which resolves an extent from the store, and an Ion asset (like a tileset by URL, CZML, or KML) keeps none there. The extent belongs to the loaded Cesium object. CesiumLayerSync.zoomToLayer flies to the entry's handle -- Viewer.flyTo reads a tileset's bounding sphere, an imagery layer's rectangle, or a data source's entities, and an I3S provider's rectangle is used directly since it is not a flyTo target. A layer added a moment ago has no handle yet, so the request is held and runs when that entry finishes loading, and is dropped if the layer goes away first. CesiumEngine.fitLayer hands over to it when the store has no bounds, and the Add Data Ion source now asks for the fit. * fix(cesium): surface a globe layer that fails to load A tileset or imagery layer whose load rejects was invisible: the sync recorded the failure on the entry, but only screenshot readiness and capture ever read it, so the Layers panel showed a healthy row over an empty globe. That is what an Ion Asset Depot sample looks like when the account has not added it -- the endpoint 404s, nothing draws, and the camera has no extent to fit, which reads as "zoom to layer is broken". The sync now reports the failure through an onLayerError dep, the Cesium engine forwards it as a MapDiagnosticEvent, and CesiumCanvas / PrimaryCesiumCanvas pass it to the shell's diagnostics handler -- the same channel MapCanvas and MapboxCanvas already use. A failed Ion asset now reads "<layer>: Request has failed. Status Code: 404" in Diagnostics. |
||
|
|
faf582b9f8 |
feat(basemap): offer Cesium and Mapbox basemaps in the picker (#2511)
* feat(basemap): offer Cesium and Mapbox basemaps in the picker The basemap picker only listed engine-specific choices for ArcGIS, so the Cesium globe and the native Mapbox renderer had no way to switch basemaps from the dialog. Picking a shared style now also clears a pinned Cesium override, matching how Mapbox and ArcGIS already behave. * fix(basemap): remove keyed Stadia choices from Cesium * style: auto-format (ruff + oxfmt) [pre-commit.ci] * Address CodeRabbit review feedback - Clear a pinned Cesium basemap override for secondary Cesium panes too, not only when Cesium is the primary renderer: split panes pick their renderer independently and CesiumCanvas reads the shared preference, so such a pane would otherwise ignore the newly picked basemap (matches the ArcGIS rule). - Cover the split-pane case in the Cesium basemap test. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
9778da6cbf | chore(release): v3.0.0 (#2378) | ||
|
|
4c4c6c7584 |
chore(release): v2.9.0 (#2220)
* chore(release): v2.9.0 Bump every version-locked file to 2.9.0: the root, app, and packages/* package.json, the Tauri crate Cargo.toml and tauri.conf.json, the geolibre Python package __init__.py, and CITATION.cff (version plus date-released, which the Validate CITATION.cff job checks against package.json). workers/* stay pinned at 0.0.0 and backend/geolibre_server keeps its own version. Add the v2.9 roadmap section and mark v2.8 as no longer current, and extend the features page with the capabilities this release opens up. Claude-Session: https://claude.ai/code/session_017x81cDoYXbJK4YwKoq1drz * Address Claude review feedback Two new top-level bullets in docs/features.md were inserted between a parent bullet and its own sub-bullets, so the sub-bullets would have re-nested under the new bullet when the list rendered: - The GeoParquet metadata bullet landed above "Layers whose features exist only in memory or in a local file embed them behind a size cap", which belongs to the Layer Library entry. Moved below it. - The global Identify bullet landed above the popup and tooltip designer's four sub-bullets, which describe the designer. Moved below them. Also moved the "catalog plugins are mutually exclusive" note to the end of the Data catalog browsers sub-list. It is a cross-cutting note rather than a browser, and it was splitting the named browsers in two. Audited the other four insertions on this branch for the same class of problem: the DashMap sub-bullet, the Planet and Vantor sub-bullets, and the docked Web Services bullet all sit at the right level. A structural check now reports no orphaned sub-bullets in the file. Claude-Session: https://claude.ai/code/session_017x81cDoYXbJK4YwKoq1drz |
||
|
|
074973372b |
3D globe: offer the pane without a Cesium Ion token and render the store basemap on it (#2205)
* feat(cesium): offer the 3D globe without an Ion token and render the project basemap on it
The globe pane was hidden unless a Cesium Ion token was configured, and it
ignored the project basemap entirely — showing Ion World Imagery with a token
and OpenStreetMap without. A project saved with a globe pane silently reopened
as the 2D map, and switching the project to a dark basemap left a bright globe
in the corner.
The pane is now offered either way, and draws the project's own basemap:
- `basemapToCesiumImagery()` (`@geolibre/core`) translates `basemapStyleUrl`
into a plain, engine-free descriptor. Planetary and regional basemaps are
already raster tile sets and carry straight over (TMS included, plus a hybrid
basemap's labels overlay); GeoLibre's vector styles map to a keyless raster
basemap of matching tone; the blank basemap draws a bare ellipsoid. Anything
with no raster form reports `default` and leaves the fallback to the renderer.
- `applyBasemapImagery()` (`packages/map/src/cesium-basemap.ts`) turns that into
imagery layers, inserted at the bottom of the stack so they stay below the
data layers CesiumLayerSync appends. It falls back to Ion World Imagery when a
token is present — preserving the old behaviour for projects that rely on
it — and to OpenStreetMap when not.
- `MapGrid` always shows the 2D/3D toggle, and a tokenless globe pane carries a
one-line hint about what a token would add rather than disappearing.
The tone analogues use OpenStreetMap and EOX Maps rather than the obvious CARTO
raster endpoints: verified in a browser, CARTO now stamps every raster tile with
an "API KEY REQUIRED" watermark.
Attribution is forwarded into Cesium's credit display, so the keyless raster
basemaps stay licence-clean.
Refs #2180
Claude-Session: https://claude.ai/code/session_01E9PXzrYZMVNBKhxFEU5ksY
* Address review feedback
- Honor `basemapVisible` and `basemapOpacity` on the globe (CodeRabbit).
`CesiumCanvas` selected only `basemapStyleUrl`, so hiding or fading the
Background row left the globe opaque while the 2D map dimmed. Adds
`applyBasemapAppearance()` and a live effect that re-styles the drawn layers
rather than rebuilding them; a hybrid basemap's overlay fades with its
imagery, matching the 2D map treating the pair as one background.
- Stop `basemapToCesiumImagery()` throwing on a malformed percent-escape
(CodeRabbit). A URL keeps `%E0%A4` in its pathname but decoding it raises
`URIError`, and the call sits inside a render, so a hand-edited project could
take the pane down. Decoding now degrades to `{ kind: "default" }`.
- Draw the basemap once per mount (Claude). `ready` flips right after the mount
effect's own draw, so the `[ready, basemapImagery]` effect immediately redrew
the same descriptor — rebuilding providers, restarting the first tile
requests, and flashing a hybrid basemap's overlay. Guarded on the applied
descriptor.
- Qualify the `getCesiumIonToken` docstring (CodeRabbit): the globe draws the
project basemap only when it has a raster form, and falls back to Ion World
Imagery, or OpenStreetMap without a token, for the ones that do not.
- Align the token summaries in `docs/features.md` and `docs/roadmap.md` with
that same fallback behaviour (CodeRabbit).
Claude-Session: https://claude.ai/code/session_01E9PXzrYZMVNBKhxFEU5ksY
* Address review feedback
- Compare basemap descriptors by value, not reference (Claude). The redraw
guard added in
|
||
|
|
477e9cfb4e | chore(release): v2.8.0 (#2131) | ||
|
|
ea91c93730 |
feat(statistics): composite score builder for suitability and index analysis (#2124)
* feat(statistics): composite score builder for suitability and index analysis Adds a Spatial Statistics tool that normalizes several numeric fields, weights them, and writes a single 0-100 index column, then hands the result to the graduated renderer so the layer lands already styled by the score. Each field gets a normalization (min-max, z-score, rank, or quantile), a relative weight, and the end of its range that scores well. The fields combine with a weighted arithmetic mean, or a geometric mean when a strong field should not be able to compensate for a weak one. - Rank and quantile share ranks across ties (mid-rank), so equal inputs always score equally; a field with no spread contributes a neutral 0.5. - Null handling is an explicit choice with no default: drop the feature, or renormalize its weights over the fields it has. - The weights are shown as the share of the total each field carries. - Optional per-field component columns, so a score can be audited. New pieces this needed: a `field-weights` parameter type with its own row editor, offered only the layer's numeric fields; a `ResultLayerOptions` argument on `addResultLayer` so a tool can ask for graduated styling; and a placeholder option on a required select that has no default. Fixes #2116 * Address review feedback - Compute min/max with a loop instead of spreading a per-feature array into Math.min/Math.max, in the min-max normalization and in the run summary. A composite score has no MAX_WEIGHTS_FEATURES cap, so a large layer would have hit the engine's argument limit. (CodeRabbit, Claude) - Keep a zero weight as "no contribution" rather than silently restoring it to 1, matching what the editor's share preview already shows; guard the zero total in computeCompositeScores so it yields null scores instead of NaN, disable Run when no field carries weight, and error with "give at least one field a weight above zero" on a run that reaches the tool anyway. (Claude) - Detect numeric fields by the value's own type: booleans, blank strings, and single-element arrays all survive Number(), and none of them belongs in a scoring picker. fieldColumn now applies the same rule, so the dialog offers exactly what the tool can score. Quoted numbers still count. (CodeRabbit, Claude) - Document the score range as 0-100 by default with 0-1 on request, in the features list, the roadmap, and the user guide. (CodeRabbit) Adds six tests over the zero-weight, boolean, and numeric-string paths. * Address review feedback - Skip zero-weight fields before reading their components in computeCompositeScores, so a null in a field the user weighted out no longer marks the feature missing and drops it under "drop" handling. Covered by a new regression test. - Warn when the user-named score field (or a kept component column) already exists on the layer, instead of silently overwriting it. Every other tool in the file writes a fixed name, so this is the first place a collision is possible. Covered by a new test. - Reword the "renormalize" unscored log to "had no positively-weighted value", which is accurate for both ways the per-feature weight sum can land at zero. - Distinguish "one numeric value" from "not numeric" in buildSymbologyStyle's graduated-mode error, so a composite run that scores a single feature no longer reports the wrong cause. - Complete the roadmap wording for the composite score item. * Address Claude review feedback - Clamp a negative or non-finite field weight to zero inside computeCompositeScores. The dialog path already clamped in readFieldWeights, but the function is exported, and a negative share would push the combined mean outside the [0, 1] the components live on, putting the score outside its documented range. Covered by a test. - Extract the "is this property value numeric" rule as numericFieldValue in the processing package and have both fieldColumn and the dialog's field picker call it, so the fields offered and the fields actually scored cannot drift apart. * Address Claude review feedback - Pull the score's decimal-precision rule into one scoreDecimals helper instead of repeating `scale >= 100 ? 2 : 4` in both computeCompositeScores and the tool's summary statistics, so a third score range cannot round the scores and the log differently. |
||
|
|
fafaa6ece3 |
feat(layers): search the loaded layers' attributes from the search box (#2121)
* feat(layers): search the loaded layers' attributes from the search box
The Layers panel search box forward-geocoded the typed query and resolved
a lat/lon coordinate and an H3 cell index locally, but it never looked at
the data on the map. Typing a parcel id, a station code, or a well name
from the layer you just loaded returned a place somewhere else entirely,
so finding a known feature meant opening the attribute table and
filtering.
The box now searches both, independently:
- `lib/feature-search.ts` matches the query against the string and
stringifiable attributes of the loaded vector layers, ranked exact
match first, then prefix, then contains (shorter value wins a tie), one
row per feature on its best-ranked field. It is capped per layer by
feature count and by a wall-clock budget, and a scan that stopped early
labels its group partial. Fields marked hidden or excluded are skipped,
as are hidden layers and tile-backed layers holding no local features.
- The component runs that scan on its own short debounce, so data matches
land while the geocoder call is still in flight, and renders them as
per-layer groups above the place results. The two halves share one flat
row index, so the arrow keys walk the whole dropdown.
- Selecting a feature makes it the live selection on its layer and frames
it through `highlightFeature(..., { fit: true })`, the same path the
attribute table and Zoom to selection already use.
A query with no data match renders exactly as before, place results with
no heading above them.
* Address Claude review feedback, and clear the selection on Clear search
- `isSearchableLayer` folded group visibility in through
`effectiveLayerRenderState`. `layer.visible` is a raw per-layer flag that a
hidden parent group never writes to, so a layer inside a hidden group was
still searched and could be flown to, the exact case the docstring says it
avoids. `searchLayerFeatures` takes the project's `layerGroups` and builds
the id map once; the component passes them from the store.
- `settle()` aborts the in-flight forward-geocode. The local scan answers on a
much shorter debounce, so a row can be picked while the network call is still
outstanding, and letting it resolve repopulated the dropdown with results for
a query already moved away from.
- The local-scan effect resets `activeIndex`. It re-runs on any `layers`
change, not only on typing, so rebuilding shorter rows under a stale index
could leave it past the end of the list; the Enter handler now also ignores a
row that is not there.
- Added a call-wide `totalBudgetMs` (120ms). Each layer had its own budget and
a layer matching nothing still paid a full scan, so the cost grew with the
layer count; a layer can no longer spend more than the call has left.
- Dropped the unreachable `exactCount` decrement: the rank guard above it
already keeps an exact match from being overwritten.
- The component gates the local scan on the exported `MIN_FEATURE_QUERY_LENGTH`
rather than its own geocoder minimum, so the two cannot drift.
Clear search now also clears the feature selection, so the highlight a picked
row left on the map goes with the query text (through the store, not by
touching MapLibre).
* Address CodeRabbit and Claude review feedback
- The local scan no longer runs while the box is idle. The effect depends on
`layers`, and the query keeps a picked label, so every layer mutation (a
refresh, a time filter, a visibility toggle) paid for a scan nobody would
see; it now returns early unless the dropdown is open or the input has focus.
- The places half enters the loading state when the query changes rather than
when its timer fires, and drops the previous query's rows there. The local
scan can open the dropdown within 120ms while the geocoder waits >=500ms, so
the "Places" heading could sit over nothing, or over the last query's results,
for the gap between the two.
- `rows` carries place rows only while they are actually rendered. They were
always in the list, so during a follow-up geocode the arrow keys walked rows
that were not on screen, Enter flew to a place from the previous query, and
`aria-activedescendant` named an id with no element.
- `handleClear` only takes back a selection this box made. Escape runs it too,
and `selectFeature(null)` writes the same app-wide selection the attribute
table and map click-select use, so focusing the input and pressing Escape
discarded an unrelated selection.
- The row cap marks its group partial. Slicing to `maxPerLayer` dropped matches
while leaving `truncated` false, so the group claimed to show everything.
- `settledQuery` replaces the `skipNextSearch` flag. Both debounce effects read
it and neither clears it (the input's own `onChange` does), so the two halves
no longer depend on the order React runs them in.
- Tightened `BUDGET_CHECK_INTERVAL` to 128 features and documented that the
budgets are best-effort to that granularity, not hard ceilings.
* Address CodeRabbit and Claude review feedback
- Selection ownership is tracked by identity, not a flag. The flag stayed true
after the attribute table or the map moved the shared selection elsewhere, so
Clear or Escape discarded a selection this box no longer held. `handleClear`
now clears only while the live selection is still exactly the feature a row
picked, through `holdsOwnedSelection`, which is unit tested for the sequence
the review named (pick here, pick elsewhere, Escape here).
- Photo and internal fields are no longer matched. `photo`/`photo_full` hold
image data URLs that can run to megabytes, and a base64 alphabet makes a
two-character query hit nearly all of them, so the whole blob could land in a
dropdown row. The scan now skips `isInternalPopupField` keys and `photo` the
way `visiblePopupFields` does, and `searchableText` rejects any `data:` value,
which covers a blob on a field the two key names do not name.
- The per-layer budget is checked after the first feature as well as every
interval, so a layer smaller than one interval is no longer exempt from it
however expensive its property sets are.
- A feature row whose layer has been removed since the scan no longer sets a
`selectedLayerId` pointing at nothing.
- The "Places" heading now requires that the places half has something under it
(rows, a spinner, or a message), so it cannot be stranded by the geocoder's
minimum query length and the local scan's minimum diverging.
* Address CodeRabbit and Claude review feedback
- The local scan's gate reads `open` from a ref rather than a dependency. The
scan is also what opens the dropdown, so with `open` in the dependency list
the first match re-ran the effect and scheduled a second, identical scan whose
`setActiveIndex(-1)` silently cleared a row the user had already highlighted
with the arrow keys.
- Both debounce timers re-check the settle marker before applying. Selecting a
row whose label matches the text already in the input makes `setQuery` a
no-op, so the effects never re-run and their cleanups never cancel the pending
timers, which then reopened the dropdown on a box the user had settled.
- `searchableText` rejects a data URL case-insensitively: the scheme is
case-insensitive, so `DATA:` carried the same blob past the check.
* Address Claude review feedback
- Picking a place, a coordinate, or a cell now releases the feature selection
this box had made. Only the feature branch of `handleSelect` touched
`ownedSelection`, so searching again and choosing a place flew the map away
while the old feature stayed selected in the store, still drawn by the
highlight overlay off-screen and still shown as selected by the attribute
table. A feature row keeps its own branch, since it replaces the selection.
|
||
|
|
9ce647b64c | chore(release): v2.7.0 (#2048) | ||
|
|
54934d0f7a |
feat(style): drive geometry-generator size and buffer distance by an attribute (#1894)
* feat(style): drive geometry-generator size and buffer distance by an attribute The geometry generator could only draw its derived symbols at one fixed size: every centroid took the same `Point radius`, and every buffer the same distance in meters. Asked on the discussion below, a polygon layer's centroids should be able to scale with a field (proportional symbols on the derived points), and buffers should be able to take their distance from a column instead of a single typed number. Both are now attribute-driven: - `geometryGeneratorSizeProperty` (plus its value/radius range) turns the derived centroid circles into an `interpolate` over the chosen field. It is deliberately separate from `proportionalSizeProperty`, which also feeds `line-width` — reusing it would have resized a polygon layer's outlines as a side effect of sizing its centroids. - `geometryGeneratorBufferProperty` buffers each feature by its own value in meters. Following QGIS's data-defined overrides, a missing, blank or non-numeric value falls back to the flat buffer distance, and a feature whose distance resolves to zero is skipped rather than drawn as its own untouched geometry (turf's zero buffer returns the source). Both pickers list only numeric columns, and choosing a size field seeds the value range from the data so the first render is already meaningful. Discussion: https://github.com/opengeos/GeoLibre/discussions/1877 * Address Claude review feedback - Keep a stored generator size/distance field selected in the picker even when it is not a numeric column on the active layer (a pasted style, a `?style=` import, or a changed schema). The browser was falling back to "None (fixed)" while the style still sized by a field that resolves to null everywhere, collapsing every centroid onto the minimum radius with nothing in the UI to explain it. Mirrors the diagram field pickers, and the select now only disables when there is neither an option nor a stored value to show. * Address Claude review feedback - Treat a whitespace-only buffer-field value as a missing value. Number() trims before coercing, so " " read as a real zero-distance buffer and the feature was skipped instead of falling back to the flat distance — the same failure mode the empty-string guard was already closing. - Factor the shared field-driven size validation out of proportionalSizeRange and generatorSizeRange, so a future tightening of the rules cannot land on one and miss the other. * Address review feedback - Only coerce numbers and numeric strings into a buffer distance. Number() turned `true` into a 1 m buffer and `[1000]` into a 1 km one; those are no more a distance than a name is, so they take the flat fallback now. - Seed the generator's size range from the defaults, not from whatever the previous field left behind, when the chosen column has a single numeric value and so no spread to derive a range from. The comment above claimed a numeric column always yields a usable range; it does not. - de: use "Entfernung" for the buffer distance field, per the locale's distance/spacing convention. - es: the hint said the entity "is stored in a buffer" and referred to the "previous" distance; reword it around the section's own "zona de influencia" wording and the fixed fallback. - vi: fix two pre-existing strings in the same block, both flagged as wrong on review: generator strokeColor read "Chữ" (text) and typeConvexHull was an unrelated sentence about string conversion. * Address CodeRabbit review feedback - Make the buffer-fallback test prove the behavior it names. It asserted only that every width matched, with a numeric string equal to the flat distance, so a regression to a bare Number(raw) could slip `[1000]` through and a broken numeric-string path would look identical to a fallback. The flat distance and the string value now differ from each other and from the values a coercion regression would produce; the test fails against the pre-fix implementation. * Address Claude review feedback - Say in the buffer distance-field hint that a value of 0 draws nothing. The tooltip explained the missing-value fallback but not the skip, so a column with legitimate zeros made features vanish with no explanation. * Address review feedback - Cover the layer-sync wiring in symbology-pack-sync: one case asserting the generated centroid layer's circle-radius becomes the interpolate, and one asserting the buffer distance field reaches the derived geometry (two squares with different values buffer to different extents). Both paths were unit-tested in isolation but nothing checked that applyGeometryGeneratorLayers threads them through. - ar: reword the buffer hint, which could be read as "features without the value of the distance above"; it now says the fixed distance applies to features with no valid distance value. * Address Claude review feedback - Keep the flat generator point-radius input visible once a size field is chosen. It is the radius generatorCircleRadiusValue falls back to when the field's range turns out degenerate, so hiding it left the radius actually in use with no way to see or change it. |
||
|
|
ef1604f66b | chore(release): v2.6.0 (#1887) | ||
|
|
360f3a932f |
docs(ios): GeoLibre is on the App Store (#1869)
* docs(ios): GeoLibre is on the App Store The iOS app has been approved and is live, so the docs no longer describe it as scaffolding that has never shipped. Adds the App Store badge and install links, and rewrites the iOS page around a published app: an Install section, per-release submission steps, and the App Store Connect onboarding marked done. * Address Claude review feedback - docs/ios.md: mark every completed App Store Connect onboarding step with an inline *(Done.)* so the numbered list reads correctly on its own, instead of the intro enumerating step numbers that would drift if the list is ever renumbered. * Address CodeRabbit review feedback - docs/comparison.md: add iOS to the opening platform summary, which still listed only desktop, Android, and Jupyter after the table row was updated. - docs/ios.md, docs/downloads.md: scope the sideload wording to GeoLibre. Saying Apple installs apps only through the App Store or TestFlight overstates it, since Ad Hoc, enterprise, custom apps, and regional alternative marketplaces exist. What is true is that GeoLibre does not publish a sideloadable .ipa for end users. - README.md, docs/getting-started.md: recolor the Mac App Store badge from black to Apple blue, so every store badge carries its platform's brand color instead of one outlier. |
||
|
|
e895829626 |
chore(release): v2.5.0 (#1728)
* chore(release): v2.5.0 * Address review feedback - Bump `packages/embed/package.json` to 2.5.0. It was left at 2.4.1 while every sibling workspace moved, and `@geolibre/embed`'s version is documented as tracking the app's, so the v2.5.0 GitHub Release would have published the embed client as 2.4.1. - Regenerate `package-lock.json` so its workspace entries match the bumped `package.json` files. `npm ci` treats that drift as an out-of-sync lockfile and fails. The diff is the nine version fields only, with no dependency churn. |
||
|
|
d3ba817289 |
docs: add the Mac App Store listing and its limitations (#1656)
* docs: add the Mac App Store listing and its limitations GeoLibre Desktop is now on the Mac App Store, so the badge and download links point at it. The Store build is sandboxed, so the docs spell out what it drops relative to the Homebrew and DMG build (Python sidecar engines, PostGIS, the local Jupyter server, Earth Engine sign-in, and external plugin installs) and keep Homebrew as the recommended route. * Address review feedback - Say "App Store ID" for the numeric listing identifier instead of "Apple ID", which reads as a user account login, and note what App Store Connect calls it. - Qualify what the Store build drops as server-backed PostgreSQL/PostGIS via martin, so the in-browser PGlite/PostGIS engine is not read as missing too. - Name Apache Sedona in the retained SQL Workspace row and scope the sidecar row to the SedonaDB sidecar: IS_MAS_BUILD skips the sidecar probe and runs Sedona on the in-browser CereusDB engine, so Sedona SQL still works there. * Address Claude review feedback - Split the comma-spliced macOS packaging bullet so the Homebrew Cask is its own sentence rather than a trailing fragment. |
||
|
|
ca8c8ec954 |
docs: add the Google Play listing and refresh project status (#1545)
* docs: add the Google Play listing and refresh project status GeoLibre is now published on Google Play as org.geolibre.app, so the README badges, Getting Started, Downloads, and Android pages point users at the store instead of only the sideload APKs. The Project status summary had also stalled at 2.2 while the repo shipped 2.4. * docs: raise the Google Play badge contrast The 414141 fill sat almost on top of the badge's default gray label half, so the two sides ran together. Google Play's brand green separates them the way the Microsoft Store badge already does. * Address Claude review feedback - docs/android.md: "Routine updates are steps 2-3" was wrong. Step 2 is Play App Signing, a one-time console action; only step 3 (upload the AAB, bump versionCode) recurs per release. - docs/features.md: reword the Android bullet so the App Bundle and the sideload APKs read as two outputs of the same workflow. * Address Claude review feedback - docs/getting-started.md: drop "and tablets" from the Android touch-layout claim. The rest of the docs scope the responsive layout to phones, and widening it is unrelated to adding the Play listing. |
||
|
|
874b024d5e | chore(release): v2.4.0 (#1517) | ||
|
|
4c3a188702 |
feat(search): resolve H3 cell indexes in the place-search box (#1497)
* feat(search): resolve H3 cell indexes in the place-search box Typing an H3 cell index into the Layers panel search box now resolves it locally, with no geocoder round-trip, and jumps to that cell. Both spellings people copy around are accepted: the hexadecimal string (8928308280fffff, optionally 0x-prefixed and in any case) and the unsigned 64-bit integer (617700169958293503). Selecting the result fits the view to the cell rather than flying to a fixed zoom, since a cell spans anything from a continent at resolution 0 to under a square meter at resolution 15, and outlines the cell on the map so the match is visible. The outline is ephemeral, cleared on the next selection, on Clear, and on unmount. Parsing lives in a standalone lib module alongside coordinates.ts so it unit tests in isolation, and unwraps boundary longitudes around the cell center so a cell crossing the antimeridian yields a contiguous ring. Fixes #1492 * i18n: localize the search placeholder's coordinate and H3 hints The English placeholder advertises place, lat/lon, and H3 input, but every other locale still read "Search places...", so the two locally resolved query forms were undiscoverable outside English. Bring all 15 remaining locales in line. |
||
|
|
2c8ef73506 |
feat(geolens): edit and save vector data back to a GeoLens dataset (#1430)
* feat(geolens): edit and save vector data back to a GeoLens dataset
A GeoLens dataset added as GeoJSON is already an ordinary editable GeoLibre
layer — the GeoEditor can redraw it in place and the attribute table can retype
it — but nothing carried those edits back to the server. GeoLens does expose
per-feature CRUD (`POST /api/datasets/{id}/features/`, and `PUT`/`PATCH`/
`DELETE` on `.../features/{gid}`), so the round trip only needed a client.
The plugin now tracks a baseline for every dataset it loads, shows what changed
in an Edits section, and writes the difference back:
- Identity is the integer row id GeoLens already returns as the GeoJSON `id`,
so it survives the store, the attribute table, and the GeoEditor's
tag-and-restore round trip. Features it never held are inserts; baseline ids
no current feature claims are deletes.
- Attribute changes go out as PUT (full replacement) because GeoLens does not
document whether PATCH merges the properties map, and a merge would silently
keep an attribute the user cleared. A geometry-only move PATCHes.
- There is no bulk/transaction endpoint, so a save is one request per changed
feature, issued sequentially with progress. A rejected write is reported and
the rest of the plan continues; the baseline then advances only for the
writes that landed, so a partial save leaves exactly the failures pending
rather than absorbing them.
- Row ids assigned to inserted features are stamped back onto the layer, so a
second save updates those rows instead of inserting duplicates.
- Saving is offered only when the server's `enable_dataset_editing` flag is on
(read from the public `/api/settings/feature-flags/`) and the connection
carries an API key — the write endpoints reject anonymous requests. The panel
says which of the two is missing instead of showing a dead button.
- A restored project has no in-memory baseline, so the first save reads the
dataset back from GeoLens and diffs against what it holds now. Reload
discards local changes the same way.
Verified in the browser against a live GeoLens (datasets.geolibre.app, editing
flag off → Save correctly disabled, counts still track a real attribute-table
edit) and against a local stand-in with editing enabled: a GeoEditor-drawn
feature POSTs, an attribute edit PUTs, and the row settles back to no pending
changes.
* feat(geolens): add a Sample server dropdown to the panel
Trying the plugin previously meant already knowing a GeoLens URL. The panel now
offers the two public deployments — datasets.geolibre.app and
demo.getgeolens.com — above the URL field. Picking one fills the field and
connects (that is the whole intent of the choice; leaving Connect to a second
click would only add a step), then resets to the placeholder, because the URL
field stays the source of truth and the user can edit it afterwards.
Also makes a blocked request legible. demo.getgeolens.com serves its catalog to
curl but sends no `Access-Control-Allow-Origin` at all, so no browser can reach
it — where datasets.geolibre.app allowlists the requesting origin and works.
`fetch` reports that as a bare TypeError ("Failed to fetch") with no detail by
design, which read as "GeoLibre is broken" rather than "this server does not
allow browser access". Every failure this module raises itself is a plain
Error, so the constructor cleanly separates the two, and a transport failure
now names the host and says the server refused a cross-origin request.
Verified in the browser, light and dark: the dropdown lists both entries, the
GeoLibre catalog loads (22 datasets), and the demo entry shows the CORS message
instead of a bare fetch error.
* fix(geolens): clear the catalog when switching or failing to reach a server
Connecting to a different server left the previous one's dataset cards on
screen, so a failed connect showed an error above what looked like that
server's catalog. Those cards are worse than stale: their Add buttons build
source paths from the old base URL, and the panel has already dropped the
client, so they describe a server it is no longer pointed at.
Connect now empties the list before querying the new server, so switching
clears immediately rather than after the response arrives, and any failed
request empties it too — after a request that never landed the panel does not
know what the server holds, and leaving earlier results up presents them as the
answer to a query that never ran.
Verified in the browser: connect to datasets.geolibre.app (22 datasets),
switch to the demo server that no browser can reach (0 datasets, just the CORS
message), switch back (22 datasets).
* fix(geo-editor): keep a layer's attributes through an in-place geometry edit
Geoman claims eleven property names as its own "shape properties" — id, shape,
center, width, height, xSemiAxis, ySemiAxis, angle, text, disableEdit, group.
On import it reads them from a feature's plain attributes, and on export
`parseExtraProperties` deletes both the plain and the prefixed form and
re-emits the value as `__gm_<name>`. So a layer with a `height` or `id` column
— building footprints, anything carrying a source id — came back from a pure
geometry edit with those columns *renamed* to `__gm_height` / `__gm_id`, plus a
`__gm_shape` column that was never in the data. The attribute table showed the
renamed columns; the original ones were gone from the layer.
The session only ever edits geometry, so the attributes it started with are the
attributes it must end with. `startLayerGeometryEdit` now snapshots them (from
the already-tagged collection, before Geoman sees it) and the write-back
restores them by feature tag, dropping Geoman's `__gm_*` bookkeeping. A feature
drawn during the session has no snapshot, so it keeps its own properties minus
that bookkeeping, which would otherwise appear as columns in the layer.
Found via the GeoLens plugin, where it also made every feature look edited: an
edit session on the 540-feature Las Vegas Buildings demo dataset marked all 540
changed and would have PUT `{}` over every row's attributes — the geometry was
byte-identical, only the renamed columns differed.
Also stops GeoLens counting a dropped null-valued attribute as a change: a
GeoLens row exposes every column, so an empty feature loads as
`{"id": null, "height": null}` and returns from the editor as `{}`. Absent and
null both leave the column NULL, so folding them together drops writes that
could not change anything, while a real value that disappears still registers.
Verified against the live dataset: a session with no edits went from 540 writes
to none ("No local changes", Save disabled) with the attribute table keeping its
id/height columns, and a single-feature attribute edit now issues exactly one
PUT (HTTP 200, "Saved 1 change to GeoLens"). The demo row used for that check
was restored to its original values.
* feat(attribute-table): show feature, filtered and selected counts in a status bar
The table had no readout of how much data it was showing: a layer's size was
only visible in the Layers panel, and the size of a selection nowhere at all.
A status bar under the table now reports the layer's feature count and how many
features are selected. The count of rows currently shown appears only when a
search or the Selected view is narrowing the table — showing it always would
print the same number twice in the ordinary case and teach the eye to skip it.
It sits outside the scroll area so it stays put while scrolling, and is hidden
when the panel is collapsed or the layer has no attributes.
Counts come from the same collections the table renders (`attributeRows`,
`filtered`, `selectedFeatureIds`), so they cannot drift from what is on screen.
Strings are plural-aware (`_one`/`_other`) in en.json; the other catalogs fall
back to English until translated.
Covered by a hermetic e2e spec over the existing smoke fixture: totals, the
"shown" count appearing only while filtered, and selection.
* Address CodeRabbit review feedback
- saveLayerEdits: baseline the collection that was actually diffed and written,
not a fresh read of the store. An edit made while the save was in flight was
being snapshotted as if the server already held it, so it would read as
unchanged forever and could never be saved. Covered by a test that fails
against the previous logic.
- Prune editSessions/pendingCountsCache for layers that have left the store.
Each entry holds a full copy of its dataset, so adding and removing GeoLens
layers retained one dataset per discarded layer until the plugin deactivated.
- Reload now confirms before discarding unsaved changes, naming the counts it
would throw away. A clean layer still reloads on a single click.
- Clear the API key when the target server changes origin (sample-server pick
or a retyped host), so a key issued by a private deployment is not carried to
a different one. Manual edits check on `change`, not per keystroke, so fixing
a path within one host leaves the key alone.
- Reword the transport-failure message: a bare fetch TypeError also covers DNS,
offline and TLS failures, so it no longer asserts the server refused CORS —
it says the request never completed and names CORS as one possibility.
- captureEditedProperties reads the pre-tag collection, so a feature whose
properties were null stays null through tag → edit → reconcile instead of
becoming {}. Regression test added for the null round trip.
- Test the properties-only PATCH branch (mode "patch" with no geometry), both
in updateFeature and in the diffFeatures fallback that selects it.
* feat(geolens): load Add GeoJSON from the current map view
Add GeoJSON took the first N features of a dataset, which on a large catalog
layer is an arbitrary slice with no relationship to what the user is looking
at. It now asks the server for the features inside the current map view
(OGC `bbox`), so the feature limit caps what is loaded *from that area*
instead. Verified against the Las Vegas Buildings demo dataset: zoomed into a
few blocks, the load returned 49 of 534 features — the ones on screen.
- Settings gains "Only load features in the current map view", persisted like
the feature limit, and on by default: a catalog dataset is usually far larger
than the area being looked at. A view that spans the world, wraps the
antimeridian, or has no map sends no bbox at all rather than a wrong half.
- A view-filtered layer is named "<dataset> (current view)", because the Layers
panel is where someone will later wonder why the layer is a subset.
- The load terms (limit and extent) are recorded on the layer, and the baseline
and Reload now re-read on those terms rather than the panel's current
settings. Without this a view-filtered layer would diff against the whole
dataset and the next save would DELETE every feature outside the view. The
same recording fixes the pre-existing case of changing the limit between load
and save.
- A save that would delete features now asks first, naming the count. Deletions
are the only part of a plan that can appear without the user touching a
feature — one the editor silently failed to load is simply absent afterwards,
which diffs identically to a deliberate delete — and the one part that cannot
be undone.
* fix(map): re-render vector tiles when their endpoint changes
Adding a dataset as vector tiles, editing it as GeoJSON and saving left the
tiles showing pre-edit data at every zoom the user had already looked at, while
zooms visited for the first time came back correct — removing and re-adding the
layer fixed it. Three things were wrong, in order of depth:
- `syncVectorTileLayer` created its source once and never touched it again, so
a changed tile template never reached MapLibre. It now pushes the new
endpoint into the live source with setTiles/setUrl, and only when it actually
changed (a needless reload blanks the layer for a frame). This also repairs
the GeoLens token refresh, which patched the store every few minutes and, as
it turns out, never affected the map at all — a layer left open past its
token's lifetime would have started 404ing.
- Saving edits did not tell the tiles anything had changed. A successful save
now re-points every GeoLens vector-tile layer showing that dataset, which
drops MapLibre's cached tiles and re-requests them.
- Re-minting a token is not enough to do that: GeoLens returns the *same*
signature and expiry for the rest of its time bucket, so the URL would be
unchanged and both MapLibre and the HTTP cache would answer from cache. The
refresh stamps a `_v` parameter instead. Signature validation ignores unknown
parameters — verified against the live server: the same tile returns
byte-identical content (5414 bytes, HTTP 200) with and without it.
Verified end to end on the Las Vegas Buildings demo dataset: with the map at
zoom 15 and those tiles already rendered, saving one edit re-requested them
immediately, and every request after further navigation carried the new version.
Also fixes the attribute-table type inference that this exposed. A cell holding
null carried no type, so typing into one stored a string — which made a number
column mixed, and a PostGIS-backed dataset rejected the write outright ("a
value is incompatible with a column's type or constraints"). The type now comes
from the column across the layer, falling back to the raw string only when the
column is empty everywhere.
* feat(geolens): re-scope a loaded layer to the current map view
Moving to a different area meant removing the GeoJSON layer and adding it
again. Each Edits row now has a "Load this view" button that replaces the
layer's features with the ones in the current view, in place.
Unsaved work is never discarded silently. With pending changes the button
offers to save them first; a save that did not fully succeed aborts the reload,
so the features those writes failed on are still there to retry. Declining the
save asks separately about discarding, so save, discard and cancel are all
reachable from the one button — the existing Reload keeps its narrower meaning
of "discard and re-read the same extent".
The refresh records the new extent and limit on the layer and re-baselines from
what it loaded, so the features that just left the view are not mistaken for
deletions on the next save. An auto-generated layer name is kept accurate
(gaining or losing the "(current view)" suffix); a name the user changed is left
alone, which is why the dataset title is now recorded in the layer metadata.
|
||
|
|
417e52911e |
chore(release): v2.3.0 (#1424)
* chore(release): v2.3.0 * docs: link GeoLens to its upstream repo |
||
|
|
de5df2599f |
chore(release): v2.2.0 (#1334)
* chore(release): v2.2.0 * docs(release): bump homepage project status to v2.2 * Address Claude review feedback - i18n: hoist the language-switch race guard from a per-hook-instance ref to a module-scope token via a new setActiveLanguage(). The old useRef only guarded races within one mounted useLanguage() instance, so it would silently break if a second call site (or a remount) were added. setActiveLanguage loads the target catalog, applies "latest request wins" across all callers, resolves true only when it actually switched (callers persist only then), and rejects only for the latest request's genuine fetch failure. - useLanguage: use setActiveLanguage; drop the now-redundant ref/guard. * Address Claude review feedback - i18n: apply the "reject only for the latest request" contract to setActiveLanguage's changeLanguage step too, not just loadCatalog. If changeLanguage rejects (rare) for a request a newer switch has already superseded, swallow it instead of logging a misleading "Failed to change language" — matching the catalog-fetch guard above. * Address Claude review feedback - i18n: serialize setActiveLanguage's changeLanguage calls through a module-scope queue so overlapping switches apply in issue order, not resolution order. The token guard already prevented persisting a superseded selection, but two in-flight changeLanguage calls could still resolve out of order and flip the visible i18next language back to a stale one. Queued switches now run one at a time and skip themselves once superseded, so the newest request always wins the final active language. |
||
|
|
3ba3a290e8 |
feat(plugins): Source Cooperative data browser (Plugins > Web Services) (#1262)
* feat(plugins): Source Cooperative data browser (Plugins > Web Services) Adds a Source Cooperative (https://source.coop) browser: search the catalog, walk a product's files, and add GeoParquet, PMTiles, COG, GeoJSON, and FlatGeobuf straight to the map or download them. Two properties of the platform shape the design: - `data.source.coop` sends `Access-Control-Allow-Origin: *` and supports byte ranges, so data is read directly from the browser -- a 115 GB PMTiles archive streams without a proxy. - `source.coop/api/v1` sends no CORS headers at all, so metadata cannot be fetched from a browser. Desktop reads it over Tauri's native HTTP; web/dev/embed go through a new origin-gated `/source-coop` route on the tiles Worker, mirroring the existing `/oam/meta` proxy. The route exposes only the two public unauthenticated reads and cannot be pointed at the API-key routes. There is also no public search endpoint -- source.coop's own search is a Next.js Server Action, not REST, and unknown `/api/v1` paths return an HTML 404 body with status 200. Search is therefore assembled client-side from the three public listings that do exist (the RSS feed's 50 newest, `/products/featured`, and `/products/{account}`) and filtered locally, with `account/product` queries resolved directly so any product stays reachable by id. Every read validates the parsed shape rather than trusting `response.ok`, because of that HTML-404 trap. Adding a layer delegates to the controls that already know each format -- a new `addPMTilesLayerFromUrl` and `addVectorLayerFromUrl` expose the PMTiles and vector panels' own load paths without opening their UI -- so the result lands in the Layers panel, styles, and persists exactly like the same file added by hand through Add Data. Verified in a browser against live source.coop data: the catalog loads, search narrows, subfolder navigation works, and both GeoParquet (cholmes/admin-boundaries) and PMTiles (protomaps/openstreetmap, 115 GB) render and appear in the Layers panel. * Address review feedback - Surface PMTiles load failures instead of reporting success (Claude). PMTilesLayerControl._addLayer catches its errors into `state.error` and emits "error" rather than rejecting, so `addLayer()` resolved even for an unreachable or malformed archive and addPMTilesLayerFromUrl returned true. Because the control is mounted hidden, its own on-panel error was never visible either, so a failed add did nothing at all with no message. Now checks `getState().error` after the await and throws, the same way the sibling addLayerWithCogRasterControl handles CogLayerControl. Confirmed against a forced failure: previously silent, now reports "Failed to load PMTiles: …" in the panel. - Encode the account segment in buildListObjectsUrl (CodeRabbit). It interpolated `accountId` raw while buildObjectUrl encoded it; the value comes from unvalidated API/feed text, so a `#` or `?` could truncate the path. Added a regression test. - Correct the classifyKey extension-table comment (Claude): it claimed the order mattered against a `.json` rule that does not exist. The patterns are mutually exclusive; the comment now says why `.geo.json` needs its own rule. - Document downloadObject's cross-origin behavior accurately (CodeRabbit). The `download` attribute is ignored cross-origin, so the comment no longer implies it renames the file, and records why the direct link is deliberate (a proxy would stream 100+ GB archives through the tiles Worker). - Fix the indentation of the new toolbar.plugin key in en.json (Claude). |
||
|
|
e5c945625a |
chore(release): v2.1.0 (#1230)
* chore(release): v2.1.0 * Address Claude review feedback - Regenerate package-lock.json so its root and workspace version entries match the 2.1.0 bump in package.json; without this, CI's `npm ci` fails with an out-of-sync lockfile error. * docs: update project status on homepage for v2.1 - Bump "GeoLibre 2.0 is a stable release" to 2.1 and add a Version 2.1 summary sentence (Browser panel, route animation, object detection, map recording, photo viewer, Wikipedia cards, planetary basemaps, OpenAerialMap plugin) to the docs homepage Project status section. |
||
|
|
c3510460a7 |
feat(planetary): USGS basemaps for nine more celestial bodies (#1196)
* feat(planetary): USGS basemaps for nine more celestial bodies Add Mercury, Venus, the four Galilean moons, Titan, Pluto, and Charon as selectable planetary basemaps. USGS Astrogeology only serves these as equirectangular WMS, so the tiles Worker now reprojects each tile to Web Mercator on the fly. Group the new bodies under one collapsible "Other celestial bodies" section, and make the on-map scale bar use the active body's radius instead of assuming Earth. * Address Claude review feedback - WMS path: skip negative-caching 5xx/429 upstream failures so a transient USGS outage/throttle isn't pinned as a blank tile, matching the OPM path. - Cap the /wms reprojection zoom (MAX_WMS_ZOOM) so a direct client can't run up Worker CPU + USGS load with unlimited high-z cache keys. - Add a [limits] cpu_ms block to wrangler.toml so the CPU-bound reprojection has a concrete budget and the code comment references a real setting. * Address CodeRabbit review feedback - Use Object.hasOwn() for both the OPM and WMS dataset lookups (ES2022, clearer than the hasOwnProperty.call form). * Address CodeRabbit review feedback - Replace the hand-rolled upng-js ambient declaration with the published @types/upng-js, switching to a namespace import; verified the reprojection still decodes/encodes correctly at runtime. * feat(planetary): use Venus MDIR colour in the planet switcher The Layers-panel celestial-body switcher now applies the Magellan C3-MDIR colour mosaic for Venus instead of the grayscale FMAP. * Address Claude review feedback - Lower MAX_WMS_ZOOM from 10 to 8 (one level above the datasets' native max) to shrink the high-zoom cache-key space a client could enumerate. - Log WMS reprojection failures (dataset/status/content-type) so a misconfigured dataset surfaces in Worker logs instead of failing silently as a blank tile. - Add tests asserting the picker groups: the nine non-Earth/Moon/Mars bodies land in the "other" section and it is sorted alphabetically by body name. - CollapsibleSection: pair aria-expanded with aria-controls via useId. * docs: document the expanded celestial-body basemaps Update the README, docs index, roadmap, and Adding Data guide to cover the new USGS Astrogeology basemaps (Mercury, Venus, the Galilean moons, Titan, Pluto, Charon) reprojected to Web Mercator, alongside the existing OpenPlanetaryMap Mars and Moon sets, the per-project ellipsoid, and the planet switcher. * Address Claude review feedback - Refresh the body-aware scale bar in applyMapPreferences, so changing the ellipsoid via Settings (without changing the basemap) updates the bar immediately instead of waiting for the next pan/zoom. * feat(planetary): auto-expand the Other bodies section for the active basemap The "Other celestial bodies" picker section stays collapsed by default but now opens automatically when the currently-active (or selected) basemap is one of those bodies, so the highlighted selection is never hidden. * Address Claude review feedback - Negative-cache WMS decode/warp/encode failures as a transparent tile (with a logged warning) instead of returning an uncached 502, so a misconfigured dataset degrades once rather than re-running the CPU-bound path forever. |
||
|
|
40afc031c2 | chore(release): v2.0.0 (#1180) | ||
|
|
70125ded30 |
chore(release): v1.10.0 (#1067)
* chore(release): v1.10.0 * Address review feedback - Refresh package-lock.json to record version 1.10.0 for the root package and every bumped workspace entry (was left at 1.9.0 by bump-version.sh, which only syncs Cargo.lock). Ran `npm install --package-lock-only`; no dependency ranges changed, only the version fields. |
||
|
|
71a3f20da0 |
chore(release): v1.9.0 (#1040)
* chore(release): v1.9.0 * Address review feedback - Sync package-lock.json workspace versions to 1.9.0 (Copilot, Claude): the bump left the lockfile at 1.8.0 for the root and the geolibre-desktop, core, map, plugins, processing, and ui workspace entries, which would break `npm ci` in CI. Updated those 8 entries surgically (left the unrelated xmlhttprequest dependency at 1.8.0); `npm ci --dry-run` now passes. |
||
|
|
ce8ff71330 |
chore: move web viewer to web.geolibre.app (#929)
The web viewer now resolves at web.geolibre.app, so point the Cloudflare custom-domain route, embed/HTML-export base URLs, docs, and tests at the new hostname. |
||
|
|
7436225ec0 | chore(release): v1.8.0 (#904) | ||
|
|
50c1c46be6 |
chore(release): v1.7.0 (#764)
* chore(release): v1.7.0 * Address Claude review feedback - Regenerate package-lock.json so the root and all eight GeoLibre workspace entries record version 1.7.0, matching the bumped package.json manifests (the bump script synced Cargo.lock but missed the npm lockfile). Only the GeoLibre workspace version fields changed; no dependency resolution changed. |
||
|
|
b7baab8b4f |
docs: add Microsoft Store install option and distribution badges (#758)
* docs: add Microsoft Store as a Windows install option * docs: add Microsoft Store, AUR, and FlatPark badges Surface the new Windows Microsoft Store listing and the existing Arch (AUR) and Flatpak (FlatPark) install paths as badges, and correct the stable release version to 1.6. * Address Claude review feedback - Clarify that winget installs the unsigned GitHub Release build, not the signed, auto-updating Microsoft Store build |
||
|
|
de14289bae |
chore(release): v1.6.0 (#698)
* chore(release): v1.6.0 * chore(release): update CITATION.cff to v1.6.0 * Address Claude review feedback - Regenerate package-lock.json so the root and all workspace entries (apps/geolibre-desktop, packages/*) record 1.6.0 instead of the stale 1.5.0, keeping the lockfile in sync with the bumped package.json files. - Add a "reference copy; CI regenerates per release" header to the three packaging/winget/*.yaml manifests, matching the note already on the aur/copr/linux packaging files, so the intentionally-lagging PackageVersion is signposted (winget-releaser owns the real submission to winget-pkgs). * Address review feedback (round 2) - README: reword the Vector menu bullet to fold the new movement, space-time, and cell-coverage tools into the parenthetical tool list, so the restrictive "that run in the browser" clause covers them without the spurious comma the Claude bot flagged. - packaging/winget/*.yaml: make the header's README cross-reference unambiguous — point to the "How CI keeps it current" section of the README.md in this directory (which documents the version-lag policy), rather than the bare "See README.md" that read as the root README. |
||
|
|
5b89731c02 |
chore(release): v1.5.0 (#581)
* chore(release): v1.5.0 * docs(roadmap): place v1.5 after v1.4 (ascending order) * Address Claude review feedback - Regenerate package-lock.json after the version bump so all seven workspace entries record 1.5.0 (root, apps/geolibre-desktop, packages/core, map, ui, plugins, processing); bump-version.sh synced Cargo.lock but not the npm lockfile, which would fail `npm ci` consistency checks in CI. |
||
|
|
6f22532351 |
feat(toolbar): add View menu with viewport history navigation (#510) (#515)
* feat(toolbar): add View menu with viewport history navigation (#510) Add a View menu after the Edit menu offering browser-style Previous/Next View navigation through recent map extents, addressing the accidental loss of a carefully framed view described in #510. - useViewportHistory hook records each user-settled viewport (moveend) into a back/forward stack and restores entries via the controller's applyView, flagging its own moves so they are not re-recorded. - ViewMenu surfaces Previous/Next View, disabled at the stack ends. - Previous/Next View also added to the command palette's View group. - The View menu trigger uses secondaryButtonClass (hidden below md), so it collapses on small mobile screens and the menu bar stays one row. * feat(toolbar): add Reset North and Reset Pitch & Bearing to View menu Add two camera-reset items to the View menu (issue #510 follow-up): - Reset North levels the compass (bearing -> 0), leaving pitch/center/zoom. - Reset Pitch & Bearing flattens and faces north (bearing 0, pitch 0). Both wrap MapLibre's resetNorth / resetNorthPitch via new thin MapController methods, are also in the command palette's View group, and are registered in the UI-profile catalog so they honour the active profile and appear in Settings -> Interface. * feat(toolbar): add Zoom In/Out and Set View to the View menu Extend the View menu (issue #510 follow-up): - Zoom In / Zoom Out at the top, wrapping MapLibre's zoomIn/zoomOut. - Set View… opens a dialog to jump to an exact camera (center longitude/latitude, zoom, pitch, bearing). The dialog reads the live camera via the controller's readView() each time it opens, so the fields always reflect the current view; submitting animates there via flyTo. Pitch and bearing default to 0 when left blank. All items are also in the command palette's View group and registered in the UI-profile catalog so they honour the active profile and appear in Settings -> Interface. * Address Claude review feedback - useViewportHistory: guard the controller before setting restoringRef in restore() so an absent map can't leave the flag stuck true and swallow the next user pan. - useViewportHistory: clear restoringRef on effect (re)attach and in cleanup so a restore in flight during a map reinit can't leave a stuck flag on the new map. - useViewportHistory: add a projectGeneration parameter and reset the stack when a different project loads, so "Previous View" can't cross into the previous project's extents. - Animate viewport restores: add MapController.easeToView (easeTo) and use it in restore() instead of the instant applyView/jumpTo, matching the browser-style framing and the animated reset actions. - viewsEqual: reword the doc-comment to say "identical (exact equality)" rather than "close enough", which implied epsilon matching. - ViewMenu: skip the heading/separator when a profile hides every item so the dropdown isn't an empty shell. * fix(e2e): seed onboarding as completed so the wizard overlay doesn't block tests The first-launch UI-profile onboarding wizard (issue #500) defaults to `onboarded: false`, so it opened on every fresh Playwright context. Its modal overlay intercepted pointer events, timing out any spec that clicked through the UI (the a11y suite failed at the "Layer actions" click). Seed `geolibre.desktopSettings` with `uiProfile.onboarded: true` via a project-level storageState so the wizard never renders in e2e. All 10 e2e specs pass locally. * Address Claude/CodeRabbit review feedback (round 2) - useViewportHistory: replace the boolean restoring flag with a counter. Rapid Previous/Next clicks cancel each in-flight easeTo and fire one moveend per call; a boolean was cleared by the first, letting a cancelled mid-animation position leak into the stack. The counter ignores exactly one moveend per driven move. - SetViewDialog: reject blank longitude/latitude/zoom instead of coercing via Number("")===0, which silently submitted zoom 0 (whole-earth) or 0,0. - ViewMenu: return null when a profile hides every item, so the trigger can't open an empty dropdown shell. * fix(ui-profile): keep Atmosphere Effects toggle visible for Beginners Atmosphere Effects is activeByDefault, so gating its Controls-menu toggle at the advanced tier left Beginners with the effect on and no way to turn it off. Lower controls.atmosphereEffects to the basic tier so the toggle is always reachable. * refactor: rename "Atmosphere Effects" to "Atmospheric Effects" Use the grammatically correct adjective form for the plugin's display name and toggle labels (plugin name, Controls-menu item, command-palette toggle), plus the matching docs and code comments. * Address Claude review feedback (round 3) - useViewportHistory: skip moveends carrying a storyCameraToken so scripted story-presenter / chapter-preview camera moves are not recorded as user navigation; tag flyToView (editor preview) with the token too so both story paths are excluded. - SetViewDialog: reject negative zoom/pitch (always invalid — MapLibre's min zoom/pitch are >= 0) with an error instead of a silent clamp; upper bounds stay with MapLibre since maxZoom/maxPitch are per-project. - SetViewDialog: add HTML min/max to the number inputs (longitude ±180, latitude ±90, zoom/pitch >= 0) for native spinner bounds and screen-reader range hints. - playwright.config: import DESKTOP_SETTINGS_STORAGE_KEY instead of hardcoding the localStorage key so a future rename is caught at compile time. |
||
|
|
6f5e9ff59b |
chore(release): v1.4.0 - version bump and docs update (#399)
* chore(release): v1.4.0 - version bump and docs update Bump the app and Python package minor version 1.3.0 -> 1.4.0 and document features added since PR #382 (Notebook panel, Spectral Index toolbox, Raster Georeferencer, Field Collection, runtime env overrides), excluding bug fixes. Flatten the v1.3 and v1.4 roadmap sections to reduce page nav clutter. * Address Claude review feedback - README: add a build-time example for VITE_DUCKDB_SPATIAL_EXTENSION_PATH (full path/URL to the spatial extension file loaded via LOAD) alongside the existing Pyodide block, so air-gapped mirror builds have a value to follow. - roadmap: move the Homebrew Cask item ahead of the Android/offline items in the flattened v1.3 list so macOS packaging no longer reads as an offline/mobile concern. |
||
|
|
04f581cc75 |
feat(android,pwa): native Android build + offline caching + region download (#377)
* feat(android,pwa): native Android build + offline caching + region download
Android (Tauri v2 mobile):
- Make the crate compile for Android: cfg-gate the desktop-only OAuth-popup
window code (WebviewWindowBuilder::{on_new_window, window_features} don't
exist on the mobile runtime) behind #[cfg(desktop)]. Desktop behavior is
unchanged; a debug APK now builds via `tauri android build`.
- Add .github/workflows/android.yml to build the debug APK in CI (JDK 21,
Android SDK + NDK r27 LTS, Rust android targets) and upload it as an artifact.
- Document the port status, verified toolchain, and remaining work in
docs/android-port-spike.md.
PWA / offline:
- Cache the CDN-loaded heavy engines (Pyodide, PGlite/PostGIS) in a new
geolibre-cdn-engines CacheFirst rule so browser SQL + Python work offline
after first use; raise the basemap cache cap for region downloads.
- Add a "Download Offline Area" feature (Project menu): pick the current view +
zoom range and pre-fetch basemap tiles/style/sprite/glyphs into the SW cache.
New lib/offline-tiles.ts (tile math + cache warming) with unit tests, and
OfflineRegionDialog.
i18n: add the offline.* and toolbar.item.offlineRegionEllipsis strings (en).
* feat(mobile): responsive panel overlays + touch-capable resizing
- On phone-width viewports (max-md), expanded Layers/Style panels now overlay
the map as slide-over sheets (absolute, z-30) instead of squeezing it into a
sliver; collapsed thin bars stay in-flow. The map stays full-bleed and
primary. Verified at 390x844 (map 640px tall; expanded panel overlays).
- Make panel resize handles touch-capable: convert the layer/style resize
handlers and handle elements from mouse events to Pointer Events (+touch-none),
so resizing works with touch/stylus, not just a mouse. Desktop is unchanged
(pointer events also fire for mouse).
- Extract the duplicated inline isMobileViewport() checks into a shared, reactive
useIsMobileViewport hook (with a getIsMobileViewport one-shot for initial
state); LayerPanel/StylePanel now import it.
Also removes docs/android-port-spike.md.
* feat(mobile): hide sidecar/desktop-only tools on mobile (isMobile gating)
isTauri() is true on Android, so sidecar-backed tools previously appeared in the
UI and failed at runtime. Add a UA-based isMobile() helper (distinct from the
narrow-viewport hook) and hide the tools that can't run on a mobile OS:
- Processing menu: Whitebox, format Conversion, Raster tools, and AI Segmentation
(all require the Python sidecar) are hidden on mobile. Vector (Turf), SQL
(PGlite/DuckDB), Python (Pyodide), geocode, statistics, network, and the
assistant run client-side and remain.
- Add Data menu: the PostgreSQL source (served via the Martin binary, no Android
build) is hidden on mobile.
Verified in a real browser: with an Android UA those entries are absent while the
client-side tools remain; desktop is unchanged. Adds tests/is-mobile.test.ts.
* build(android): ship slim release APKs + sign in CI
The debug APK was ~208 MB, dominated by the unstripped debug Rust .so (210 MB).
Switch the Android workflow to a release build, which uses the existing
size-optimized + stripped Cargo profile → ~41 MB APK (.so ~38 MB), a ~5x cut.
- .github/workflows/android.yml: build release (not --debug), then zipalign +
sign. Uses release-keystore secrets (ANDROID_KEYSTORE_BASE64 / _PASSWORD /
ANDROID_KEY_ALIAS / _PASSWORD) when present; otherwise falls back to a
throwaway debug keystore so the CI artifact is still installable for testing.
- tauri.android.conf.json: drop bundle.resources (the Python backend) on Android
— it can't run there and only bloats the package.
* fix(android): safe-area insets, launcher icon, per-ABI APK splits
Three issues found testing the APK on a phone:
- Status bar overlap / unclickable menus: the webview drew edge-to-edge, so the
top toolbar rendered under the Android status bar — which intercepts taps,
making those menu buttons unresponsive. Add viewport-fit=cover and pad #root by
env(safe-area-inset-*) so the chrome clears the system bars and cutouts. Insets
are 0 on desktop/web, so it only affects mobile. Verified on an emulator.
- Wrong launcher icon: `tauri android init` writes default Tauri icons. CI now
copies the checked-in GeoLibre icons (src-tauri/icons/android) over the
generated mipmaps after init. Verified the GeoLibre globe shows in the launcher.
- Oversized CI APK (~150 MB): the workflow built a universal APK bundling all
four ABIs. Build --split-per-abi instead and sign each, giving ~40 MB
per-architecture APKs (install the arm64-v8a one on real phones).
* feat(android): name the app "GeoLibre" (not "GeoLibre Desktop")
Override productName to "GeoLibre" in tauri.android.conf.json so the Android
launcher label and activity title read "GeoLibre". The desktop build keeps
"GeoLibre Desktop" (base tauri.conf.json). Verified a clean `tauri android init`
generates app_name=GeoLibre in strings.xml.
* fix(android): pad adaptive launcher icon into the safe zone
The foreground filled the full 108dp canvas, so the launcher's adaptive mask
cropped the globe's edges. Scale the foreground to ~64% centered (all densities)
so the whole icon stays within the ~66dp safe zone and is never clipped.
* docs: document Android app, mobile, and offline features for v1.3
- README: native Android app + Download Offline Area + engine offline caching in
the v1.3 feature list, an Android build/run section, and a Reference link.
- New docs/android.md: toolchain setup, build, signing, sideload, and emulator
guide (added to mkdocs nav).
- roadmap: a "Mobile, offline, and Android" subsection under v1.3.
- index: mention Android/mobile/offline in the v1.3 status summary.
Also: build the Android APK only on published releases (and on demand), not on
every PR — the Rust cross-compile + Gradle build is slow.
* fix(ui): keep long dropdown menus within the visible viewport on mobile
On Android the dropdown max-height used 100vh / Radix's available-height, both
based on the layout viewport, which extends under the system status/navigation
bars. Long menus (e.g. Add Data) then ran their last items under the nav bar
where they were clipped and unreachable. Cap the max-height with dvh minus the
top/bottom safe-area insets so the menu (and its scroll area) stays within the
visible region and scrolls to the end. Desktop/web are unchanged (dvh == vh and
insets are 0).
* Address Copilot/Claude/CodeRabbit review feedback
offline-tiles.ts:
- collectOfflineUrls: accept an AbortSignal (Cancel now interrupts TileJSON
discovery), thread source `subdomains` into expandTileUrl, and handle the
MapLibre array `sprite` format so multi-sprite styles cache their icons.
- expandTileUrl: drop `{s}` together with its trailing dot when no subdomains,
avoiding invalid `https://.host/...` URLs.
- Glyphs: stop encodeURIComponent-ing the fontstack so the warmed URL matches
MapLibre's actual (browser-normalized) glyph request and the SW cache key hits.
- countTiles/enumerateTiles: split antimeridian-crossing bboxes (west > east).
- warmUrls: clamp concurrency to >=1, and don't increment `done` on aborted
fetches (finally ran after the catch `return`).
OfflineRegionDialog.tsx: catch collectOfflineUrls errors (no false "done"),
abort the in-flight download when the dialog closes, only clear abortRef if it
still points to this run, disable Download when no service worker is active, and
warn when the area exceeds the SW cache cap.
DesktopShell.tsx: panel resize handles now setPointerCapture, listen for
pointercancel, and tear down on unmount, so a canceled/interrupted drag can't
leave listeners attached or document.body styles stuck.
is-mobile.ts: detect iPadOS 13+ desktop-UA via maxTouchPoints (+ tests).
AddDataMenu/ProcessingMenu: memoize the isMobile() check (stable per session).
android.yml: pin android-actions/setup-android to a commit SHA, rename the job
to "(release)", fail fast when keystore secrets are partially set, and pass
apksigner passwords via files instead of CLI args.
|
||
|
|
1c0ad2d627 |
chore(release): v1.3.0 — version bump and docs update (#373)
* chore(release): bump version to 1.3.0 and update docs Bump the app and Python package minor version 1.2.0 -> 1.3.0 and document features added since PR #290 in the README, docs site, and roadmap (excluding bug fixes). * Address Claude review feedback - README: drop "distributed" from the Apache Sedona SQL description; the engine runs via the SedonaDB sidecar, not a Spark/Flink cluster, so "distributed spatial SQL" is misleading. - docs/index.md: note the real-time collaboration card requires VITE_GEOLIBRE_COLLAB_URL (the feature is gated/hidden when unset). * Address Claude review feedback - docs/index.md: link the Collaboration card to collaboration.md and user-guide/storymaps.md, and drop the print-layout sentence (print is not a collaboration feature). - docs/index.md: link the geocoding portion of the Network analysis card to user-guide/data-integrations.md#geocoding (no dedicated network page exists to link the network tools to). - README: note real-time collaboration requires the VITE_GEOLIBRE_COLLAB_URL build variable and link docs/collaboration.md. |
||
|
|
579d8b4252 |
chore: release v1.2.0 (version bump, README, docs, roadmap) (#289)
Bump every JS workspace, the Tauri config and Rust crate, and the geolibre Python package from 1.1.x to 1.2.0, and document the features shipped since v1.1.0 in the README, roadmap, and docs landing page. |
||
|
|
5dc75986c2 |
chore: release v1.1.0 (version bump, README, docs, roadmap) (#224)
* chore: release v1.1.0 — bump version, update README, docs, and roadmap
- Bump version 1.0.0 -> 1.1.0 across the JS workspaces (root + @geolibre/*
+ geolibre-desktop), the Tauri config and Rust crate (Cargo.toml/lock),
the package-lock, and the geolibre Python package.
- README: retitle Features (v1.1) and document the features added since v1.0
(Add Vector Layer auto-refresh, layer rename / open-table / export actions,
categorized/graduated/expression styling incl. Add Vector Layer layers,
attribute-table column management + export, and app.getDeckGL for plugins).
- docs/roadmap.md: add a v1.1 section listing every feature shipped since v1.0
and drop "(current)" from v1.0.
- docs/plugin-api.md: document app.getDeckGL().
- docs/index.md: refresh the feature cards (attribute-table management,
symbology modes, atmosphere effects plugin).
* Address review feedback
- docs/plugin-api.md: add the GeoLibreDeckGL interface definition so plugin
authors using app.getDeckGL() have a typed contract for the returned
deck.gl modules instead of only the prose description. (Claude, CodeRabbit)
- packages/{map,ui}: revert react/react-dom from ^19.0.0 back to ^19.2.7 — the
release npm install had downgraded those ranges, leaving them below the
@types/react ^19.2.17 dev dependency. This was unintended scope for the
version bump; restoring ^19.2.7 realigns the runtime range with the types and
with main. (Claude)
* Address Claude review feedback
- docs/plugin-api.md: move the GeoLibreDeckGL interface above GeoLibreAppAPI so
it is defined before getDeckGL references it, matching the declaration order
in packages/plugins/src/types.ts and removing the forward reference.
|
||
|
|
9f643c3d72 |
feat: add geolibre Python package for Jupyter notebooks (#197)
* feat: add geolibre Python package for Jupyter notebooks Embed the full GeoLibre app in a notebook cell as an anywidget, with a leafmap-style Python API (Map.add_geojson/add_tile_layer/add_cog/...) and two-way .geolibre.json project sync. - New python/ package: Map widget, project/layer builders, named basemaps, a localhost app server, and the postMessage bridge front-end. The web build is bundled into the wheel by a hatch build hook (npm run build:embed). - App-side useEmbedBridge hook applies host load-project messages and emits debounced project snapshots; wired into DesktopShell. New build:embed script builds with a relative asset base so the app loads from the wheel. - CI workflow to test and publish to PyPI via OIDC trusted publishing. - Docs: README section, docs/python.md guide, mkdocs nav, home feature card, and a v1.0 roadmap entry. * Address Claude review feedback; set default map height to 800px Review feedback (PR #197): - useEmbedBridge: only accept inbound messages from the embedding host (event.source === host) so a same-page script cannot inject a project. - useEmbedBridge: drop the dead `window.parent ?? window` fallback (window.parent is always defined in a browser); clarify the comment. - useEmbedBridge: stop the serialize -> JSON.parse round-trip in postState; send the project object directly, keeping the string only for the dedup check. - project.load_featurecollection: add a 30s timeout and a 50 MB size cap to the URL fetch so a slow or oversized response cannot hang or OOM the kernel. - _server: handle_error now only swallows ConnectionResetError/BrokenPipeError and re-raises genuine handler bugs. - geolibre.remove_layer: replace the lambda __setitem__ workaround and ambiguous `l` name with a named inner function. - _frontend.js: post to the iframe using its own origin instead of "*"; document the synchronous-change-dispatch dependency of the applyingRemoteState guard. Also: - Set the default Map height to 800px (trait default, __init__ default, JS fallback, and docs). * Address CodeRabbit review feedback - publish-python.yml: pin the third-party pypa/gh-action-pypi-publish action to a commit SHA (release/v1), matching the repo's convention of SHA-pinning third-party actions while leaving first-party actions/* on tags as in ci.yml. - useEmbedBridge: reset lastLoadedSeq to 0 when an inbound load omits seq, so a later snapshot cannot echo a stale, unrelated sequence number. - _frontend.js: also validate inbound event.origin against the app origin (defense in depth alongside the existing event.source check). - project.load_featurecollection: normalize URL transport failures (URLError, TimeoutError) to ValueError to match the documented contract. * Address Claude review feedback (round 2) - _frontend.js: replace the synchronous `applyingRemoteState` boolean loop guard with reference-identity tracking (`lastRemoteProject`), so echo suppression no longer depends on anywidget firing change:project synchronously. - useEmbedBridge: cache the echo-suppression string from the store *after* loadProject normalizes the project, so a normalized load is not re-echoed. - project.py: cap local GeoJSON file reads at 50 MB (matching the URL cap) via a shared _MAX_GEOJSON_BYTES constant. - geolibre.load_project: validate required top-level keys (version, name, mapView) and raise ValueError instead of pushing an invalid project that would only surface through the error trait. - docs: add a security note that add_geojson(url) fetches from the kernel and intentionally allows private/localhost URLs; not for shared kernels. (SSRF flag — a hard private-IP block would break legitimate local tile servers.) * Support Google Colab; address Claude/CodeRabbit review feedback (round 3) Colab support: - Serve the app port to the front-end (_app_port trait + _server.app_port()). - _frontend.js resolves the iframe base via google.colab.kernel.proxyPort on Colab (falling back to the localhost URL elsewhere), so the browser reaches the kernel through Colab's port proxy. Docs updated: Colab now supported. Review feedback: - save_project: create parent directories instead of raising FileNotFoundError. - build_empty_project: raise ValueError for a center with fewer than 2 elements. - _server.serve_app: document that static_dir is ignored after the first call; simplify the singleton guard to `_base_url is None`. - useEmbedBridge.postState: post the JSON-parsed snapshot so the wire payload matches the serialized .geolibre.json form and is structured-clone-safe (CodeRabbit); clarify the geolibre:error target-origin comment. * Bump Python package to v0.2.0 * Address Claude/CodeRabbit review feedback (round 4) - load_project: backfill a missing `layers` to [] and reject a non-list, so the add_*/remove_layer mutators cannot KeyError on a project loaded without it (CodeRabbit). - useEmbedBridge: learn the host's origin from its first message and scope outbound geolibre:state / geolibre:error posts to it (falling back to "*" only for the version-only ready ping), so a third-party framing page cannot read the project. Document why applyLoad re-stringifies an object for parseProject. - _frontend.js: note that traitlets.Dict value-based change detection keeps the kernel from re-broadcasting the just-sent project, so the identity loop guard is not defeated by the save round trip. - geolibre.py: use p.update({...}) instead of __setitem__ in clear_layers and add_basemap for readability. * Support JupyterHub and remote servers via jupyter-server-proxy The localhost app server is unreachable from the browser on remote kernels. Add a server-proxy path so the widget works on JupyterHub and other remote setups: - Map(server_proxy="auto"|True|False): "auto" (default) serves directly from localhost and switches to jupyter-server-proxy automatically under JupyterHub (detected via JUPYTERHUB_SERVICE_PREFIX). True forces it on any remote server; False forces the direct path. (_use_server_proxy trait + _resolve_server_proxy.) - _frontend.js: when proxying, load the app from {base_url}proxy/{port}/, reading the Jupyter base URL from the page's jupyter-config-data. Colab's own port proxy still takes precedence. The relative-asset build makes the app work under the proxy path prefix. - Add the `hub` extra (jupyter-server-proxy); document JupyterHub/remote support in the README and docs; add tests for the proxy resolution. * Address Claude review feedback - useEmbedBridge.ts: gate proactive postState on a host handshake so a third-party frame that never speaks can't keep receiving project snapshots via the "*" fallback; use an empty mount-only effect dep array instead of the stable mapControllerRef. - project.py: validate center has exactly 2 elements (no silent truncation); omit rescale from rasterState when None (match bands/colormap); reject a FeatureCollection without a 'features' list. - geolibre.py: mark set_center_zoom as a leafmap-compat alias; document that add_geojson inlines file/URL sources into the project. - hatch_build.py: add a 600s timeout to the npm build subprocess. - _frontend.js: document the anywidget object-identity invariant the echo-suppression guard relies on. - publish-python.yml: test against Python 3.10 and 3.12 (declared minimum). * Address Claude review feedback (round 2) - geolibre.py: when a source string starts with "{" but isn't valid JSON (e.g. a path like `{backup}/map.json`), fall back to reading it as a file instead of raising JSONDecodeError. - hatch_build.py: raise a clear RuntimeError (install Node.js / run npm ci) when npm isn't on PATH, instead of an opaque FileNotFoundError traceback. - project.py: use copy.deepcopy for the preferences default instead of a json.loads(json.dumps(...)) round-trip. - _frontend.js: reframe the loop-guard comment to call out the primary (traitlets value-based dedup) vs secondary (reference identity) protection. - useEmbedBridge.ts: document the embed trust model in the hook's JSDoc. - docs/python.md, python/README.md: list set_center_zoom in the API table. * Address Claude review feedback (round 3) - project.py: deep-copy DEFAULT_LAYER_STYLE before spreading so a caller mutating a returned layer's nested style (e.g. vectorStyleStops) can't corrupt the module constant shared by later layers. - project.py: add an SSRF guard to remote GeoJSON fetches — reject hosts that resolve to non-public addresses and re-validate every redirect hop, so a redirect to a cloud metadata endpoint (169.254.169.254) can't be followed. - geolibre.py: validate layout/theme against their allowed values in the constructor so a typo raises immediately instead of silently falling back. - geolibre.py: wrap load_project's file/JSON parse failures in the documented ValueError instead of leaking FileNotFoundError/JSONDecodeError. * Address Claude review feedback (round 4) - useEmbedBridge.ts: don't activate the bridge for a cross-origin parent unless ?embed=1 is present, so an arbitrary third-party iframe of a deployed app never receives project state (the widget and to_html() always pass ?embed=1; same-origin framing stays trusted). - useEmbedBridge.ts: advance lastLoadedSeq before parsing in applyLoad so a later state snapshot carries the right correlation id even when a load fails. - geolibre.py: in load_project, reject a non-dict mapView with a clear ValueError instead of letting set_center raise a confusing TypeError later. - python/README.md: note that add_geojson inlines data and steer large layers toward tile/COG sources. --------- Co-authored-by: giswqs <giswqs@users.noreply.github.com> |