Skip to content

Commit de5df25

Browse files
authored
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.
1 parent 3a9771c commit de5df25

17 files changed

Lines changed: 125 additions & 52 deletions

File tree

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ authors:
99
email: giswqs@gmail.com
1010
affiliation: "University of Tennessee, Knoxville, USA"
1111
orcid: "https://orcid.org/0000-0001-5437-4073"
12-
version: 2.1.0
13-
date-released: 2026-07-13
12+
version: 2.2.0
13+
date-released: 2026-07-18
1414
doi: 10.5281/zenodo.20785400
1515
repository-code: "https://github.com/opengeos/GeoLibre"
1616
url: "https://geolibre.app"

README.md

Lines changed: 14 additions & 10 deletions
Large diffs are not rendered by default.

apps/geolibre-desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "geolibre-desktop",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

apps/geolibre-desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/geolibre-desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "geolibre-desktop"
3-
version = "2.1.0"
3+
version = "2.2.0"
44
description = "GeoLibre Desktop — lightweight cloud-native desktop GIS"
55
authors = ["GeoLibre Contributors"]
66
edition = "2021"

apps/geolibre-desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "GeoLibre Desktop",
4-
"version": "2.1.0",
4+
"version": "2.2.0",
55
"identifier": "org.geolibre.desktop",
66
"build": {
77
"beforeDevCommand": "npm run dev",

apps/geolibre-desktop/src/hooks/useLanguage.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useCallback, useRef } from "react";
1+
import { useCallback } from "react";
22
import { useTranslation } from "react-i18next";
33

4-
import { AVAILABLE_LANGUAGES, loadCatalog } from "../i18n";
4+
import { AVAILABLE_LANGUAGES, setActiveLanguage } from "../i18n";
55
import {
66
DEFAULT_LANGUAGE,
77
languageOptions,
@@ -32,41 +32,29 @@ const OPTIONS = languageOptions(AVAILABLE_LANGUAGES);
3232
export function useLanguage(): UseLanguageResult {
3333
const { i18n } = useTranslation();
3434
const setDesktopSettings = useDesktopSettingsStore((s) => s.setDesktopSettings);
35-
// The most recently requested language. Rapidly picking two uncached locales
36-
// races their lazy catalog fetches; without this guard a slower earlier fetch
37-
// could resolve last and clobber the newer selection.
38-
const latestRequestRef = useRef<string | null>(null);
3935

4036
const setLanguage = useCallback(
4137
(code: string) => {
42-
latestRequestRef.current = code;
43-
// Import the target locale's lazy catalog chunk before switching, then
44-
// persist only after the language has actually switched — so a catalog
45-
// that fails to load leaves neither the UI nor the persisted setting on a
46-
// language with no strings. English is bundled, so switching to it needs
47-
// no fetch.
48-
loadCatalog(code)
49-
.then(() => {
50-
// A newer selection superseded this one while its catalog loaded —
51-
// drop this stale request so it can't override the latest choice.
52-
if (latestRequestRef.current !== code) return undefined;
53-
return i18n.changeLanguage(code);
54-
})
55-
.then(() => {
56-
if (latestRequestRef.current !== code) return;
38+
// `setActiveLanguage` lazily imports the target locale's catalog before
39+
// switching and applies the module-scope "latest request wins" guard, so
40+
// rapidly picking two uncached locales can't let a slower earlier fetch
41+
// clobber the newer selection. It resolves `true` only when this call
42+
// actually applied the language — persist the choice only then, so a
43+
// superseded or failed switch leaves neither the UI nor the setting on a
44+
// language with no strings.
45+
setActiveLanguage(code)
46+
.then((applied) => {
47+
if (!applied) return;
5748
const current = useDesktopSettingsStore.getState().desktopSettings;
5849
setDesktopSettings({ ...current, language: code });
5950
})
6051
.catch((error: unknown) => {
61-
// A newer selection already superseded this one — its failure is for an
62-
// abandoned request and not worth surfacing.
63-
if (latestRequestRef.current !== code) return;
64-
// Keep the current language (its catalog is still loaded) rather than
65-
// switch to an empty one; surface the failed fetch.
52+
// Only the latest request's genuine fetch failure rejects here; keep
53+
// the current language (its catalog is still loaded) and surface it.
6654
console.error("[GeoLibre] Failed to change language", error);
6755
});
6856
},
69-
[i18n, setDesktopSettings],
57+
[setDesktopSettings],
7058
);
7159

7260
// i18n.language can be a full tag (e.g. `en-US`); reuse the shared resolver to

apps/geolibre-desktop/src/i18n/index.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,65 @@ export async function loadCatalog(code: string): Promise<void> {
5050
i18n.addResourceBundle(code, "translation", mod.default, true, true);
5151
}
5252

53+
/**
54+
* Monotonic token for the most recent `setActiveLanguage` call. Kept at module
55+
* scope (not in a hook's ref) so the "latest request wins" guard holds across
56+
* *every* caller — multiple components, or a remount — not just one hook
57+
* instance.
58+
*/
59+
let languageRequestToken = 0;
60+
61+
/**
62+
* Serializes the `i18n.changeLanguage` calls. Catalog fetches may finish out of
63+
* order, but the actual switches run one at a time in the order they were
64+
* issued, so a slower earlier switch can't resolve last and flip the active
65+
* language back to a superseded selection. Each queued switch skips itself if a
66+
* newer request has already superseded it by the time its turn comes up.
67+
*/
68+
let languageSwitchQueue: Promise<void> = Promise.resolve();
69+
70+
/**
71+
* Load a locale's catalog (if needed) and switch to it, ignoring any request a
72+
* newer call has since superseded — so rapidly picking two uncached locales
73+
* can't let a slower earlier request clobber the newer selection (neither the
74+
* persisted choice nor the visible language). Resolves `true` only when this
75+
* call actually applied the language (callers persist the choice on `true`),
76+
* `false` when it was superseded. Rejects only when the *latest* request fails
77+
* — its catalog fetch or the switch itself; a superseded request's failure is
78+
* swallowed.
79+
*/
80+
export async function setActiveLanguage(code: string): Promise<boolean> {
81+
const token = ++languageRequestToken;
82+
try {
83+
await loadCatalog(code);
84+
} catch (error) {
85+
if (token === languageRequestToken) throw error;
86+
return false;
87+
}
88+
if (token !== languageRequestToken) return false;
89+
90+
// Chain onto the switch queue so overlapping `changeLanguage` calls apply in
91+
// issue order rather than resolution order. The queue promise never rejects
92+
// (failures are captured below), so the chain always advances.
93+
let failure: unknown;
94+
const run = languageSwitchQueue.then(async () => {
95+
// A newer request superseded this one before its turn — leave the language
96+
// to that newer switch.
97+
if (token !== languageRequestToken) return;
98+
try {
99+
await i18n.changeLanguage(code);
100+
} catch (error) {
101+
// Surface only the latest request's failure; a superseded one's is moot.
102+
if (token === languageRequestToken) failure = error;
103+
}
104+
});
105+
languageSwitchQueue = run;
106+
await run;
107+
108+
if (failure) throw failure;
109+
return token === languageRequestToken;
110+
}
111+
53112
const QUERY_PARAM_KEYS = ["locale", "lang"];
54113

55114
/**

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,4 @@ Other parameters control the toolbar, panels, and theme. See [Embedding & Sharin
159159

160160
## Project status
161161

162-
GeoLibre 2.1 is a stable release. It includes the map workspace, the `.geolibre.json` project format with Save, Open, and Share, the plugin API, and the plugin marketplace for installing, updating, and removing external plugins. Data support spans browser vector import, DuckDB-WASM Spatial loading, the full Add Data surface (files, web services, cloud formats, 3D layers, and databases), and cloud integrations through the Planetary Computer and Earth Engine panels, the Overture Maps plugin, and the federal Web Services plugins. Processing covers the vector tools (Turf.js with an optional GeoPandas sidecar), the raster tools (rasterio sidecar with a client-side fallback), a Spectral Index toolbox, a Raster Georeferencer, a Spatial Statistics toolbox, network analysis (isochrones, service areas, OD cost matrices), the Conversion menu (GeoParquet, FlatGeobuf, PMTiles, COG), the Whitebox toolbox, AI Segmentation via SamGeo/SAM 3, and the SQL Workspace for DuckDB Spatial SQL (with PGlite PostGIS and Apache Sedona engines). The release also ships a docked Notebook panel that runs Jupyter beside the map (JupyterLite on the web, a desktop JupyterLab server), a Field Collection tool for capturing point, line, and polygon observations, real-time multi-user collaboration, a scroll-driven story map builder, a natural-language AI assistant and in-app Python Console, multi-provider geocoding, the Time Slider plugin, a Controls menu (Measure, Bookmark, Minimap, View State), a Print menu, Layout settings, runtime environment variables, diagnostics, embed-friendly URL parameters including the `maponly` mode, cross-platform installers (including a macOS Homebrew Cask and a Windows Microsoft Store listing), and Docker support for the browser app. GeoLibre also ships as a native **Android** app built from the same codebase via Tauri v2 mobile (see [Android](android.md)), with a responsive touch layout for phones, and offline improvements (a Download Offline Area tool plus service-worker caching of the CDN-loaded Pyodide and PGlite/PostGIS engines). Version 2.0 adds a CesiumJS 3D globe view for any map pane, planetary mapping (Mars and the Moon from OpenPlanetaryMap, plus Mercury, Venus, the Galilean moons, Titan, Pluto, and Charon from USGS Astrogeology reprojected to Web Mercator, with a per-project ellipsoid and a planet switcher in the Layers panel), symbology interchange that imports and exports vector styling as OGC SLD, QGIS QML, and Mapbox GL style JSON, editable source layers that write vector edits back to GeoPackage, GeoJSON, and PostGIS, a Weather menu with live cloud and precipitation radar overlays and a sun position simulation, and new Mapillary, Historical Imagery, and Elevation Profile plugins. Version 2.1 adds a QGIS-style Browser panel (Data Source Manager) for browsing services, PostGIS databases, local files, and favorites from one place; route animation that sends a marker along a line layer with 3D track-follow camera controls and MP4 export; in-browser ONNX/YOLO object detection; map recording of the canvas or a drawn bounding box to video; a native-resolution geotagged photo viewer; Wikipedia knowledge cards; USGS basemaps for nine more celestial bodies; and a new OpenAerialMap imagery search plugin. See the [roadmap](roadmap.md) for the full release history and what comes next.
162+
GeoLibre 2.2 is a stable release. It includes the map workspace, the `.geolibre.json` project format with Save, Open, and Share, the plugin API, and the plugin marketplace for installing, updating, and removing external plugins. Data support spans browser vector import, DuckDB-WASM Spatial loading, the full Add Data surface (files, web services, cloud formats, 3D layers, and databases), and cloud integrations through the Planetary Computer and Earth Engine panels, the Overture Maps plugin, and the federal Web Services plugins. Processing covers the vector tools (Turf.js with an optional GeoPandas sidecar), the raster tools (rasterio sidecar with a client-side fallback), a Spectral Index toolbox, a Raster Georeferencer, a Spatial Statistics toolbox, network analysis (isochrones, service areas, OD cost matrices), the Conversion menu (GeoParquet, FlatGeobuf, PMTiles, COG), the Whitebox toolbox, AI Segmentation via SamGeo/SAM 3, and the SQL Workspace for DuckDB Spatial SQL (with PGlite PostGIS and Apache Sedona engines). The release also ships a docked Notebook panel that runs Jupyter beside the map (JupyterLite on the web, a desktop JupyterLab server), a Field Collection tool for capturing point, line, and polygon observations, real-time multi-user collaboration, a scroll-driven story map builder, a natural-language AI assistant and in-app Python Console, multi-provider geocoding, the Time Slider plugin, a Controls menu (Measure, Bookmark, Minimap, View State), a Print menu, Layout settings, runtime environment variables, diagnostics, embed-friendly URL parameters including the `maponly` mode, cross-platform installers (including a macOS Homebrew Cask and a Windows Microsoft Store listing), and Docker support for the browser app. GeoLibre also ships as a native **Android** app built from the same codebase via Tauri v2 mobile (see [Android](android.md)), with a responsive touch layout for phones, and offline improvements (a Download Offline Area tool plus service-worker caching of the CDN-loaded Pyodide and PGlite/PostGIS engines). Version 2.0 adds a CesiumJS 3D globe view for any map pane, planetary mapping (Mars and the Moon from OpenPlanetaryMap, plus Mercury, Venus, the Galilean moons, Titan, Pluto, and Charon from USGS Astrogeology reprojected to Web Mercator, with a per-project ellipsoid and a planet switcher in the Layers panel), symbology interchange that imports and exports vector styling as OGC SLD, QGIS QML, and Mapbox GL style JSON, editable source layers that write vector edits back to GeoPackage, GeoJSON, and PostGIS, a Weather menu with live cloud and precipitation radar overlays and a sun position simulation, and new Mapillary, Historical Imagery, and Elevation Profile plugins. Version 2.1 adds a QGIS-style Browser panel (Data Source Manager) for browsing services, PostGIS databases, local files, and favorites from one place; route animation that sends a marker along a line layer with 3D track-follow camera controls and MP4 export; in-browser ONNX/YOLO object detection; map recording of the canvas or a drawn bounding box to video; a native-resolution geotagged photo viewer; Wikipedia knowledge cards; USGS basemaps for nine more celestial bodies; and a new OpenAerialMap imagery search plugin. Version 2.2 adds a styling overhaul (a rule-based renderer with per-rule symbol properties, scale-dependent visibility, and nested rules, a Style Manager preset library, diagram symbology, and a symbology pack of inverted-polygon masks, arrow and marker lines, and geometry generators); a shared Expression Builder wired into filters, labels, styling, and selection, driving a data-defined labeling engine and Select by Expression; virtual fields, persistent attribute joins, an attribute form designer, and a Raster Attribute Table; Atlas / map series generation in the Print Layout; browser-native COG, FlatGeobuf, Shapefile, GeoPackage, and Vector to PMTiles conversions; live GPS tracking; data quality tools (check validity, fix geometries, check topology); a Processing History panel; and new Natural Earth and Source Cooperative data browsers. See the [roadmap](roadmap.md) for the full release history and what comes next.

docs/roadmap.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@
316316
- [x] All 13 locale catalogs completed, with the remaining hardcoded panel and dialog strings migrated to the translation system
317317
- [x] The AI assistant can read provider API keys from OS environment variables, and the desktop diagnostics network log now captures native Tauri HTTP requests and classifies failed `fetch()` errors
318318

319-
## v2.1: A data-source Browser panel, route animation, in-browser object detection, and map recording (current)
319+
## v2.1: A data-source Browser panel, route animation, in-browser object detection, and map recording
320320

321321
- [x] A QGIS-style **Browser panel** (Data Source Manager) for exploring and adding data from one place: browse map Services and Recent items, connect to PostGIS databases and browse their schemas and tables, drill into local files, save and reopen Favorites, add a New connection per service kind, and navigate the whole tree from the keyboard
322322
- [x] **Route animation**: animate a marker along any line layer, follow the track in 3D with camera controls, and export the whole animation as an MP4 video
@@ -332,6 +332,28 @@
332332
- [x] Bundled plugin drop-ins can set `activeByDefault` in their manifest, deployments can opt out of the welcome dialog, and the app now defaults to the Advanced interface and skips the welcome dialog on first run
333333
- [x] Optional HTTP Basic Auth for the web (Docker) container
334334

335+
## v2.2: A styling overhaul, expression-driven fields and labels, print atlas, and browser-native conversions (current)
336+
337+
- [x] A **rule-based renderer** with per-rule symbol properties, scale-dependent visibility, nested rules, and per-rule toggles, so a single layer can carry a full hierarchy of styling rules and hide anything that matches no rule when the else rule is off
338+
- [x] A **Style Manager** that saves reusable symbol, color-ramp, and label presets to a personal library and applies them across projects
339+
- [x] A symbology pack covering inverted-polygon masks, arrow and marker lines, and geometry generators, plus data-driven proportional sizing for marker icons
340+
- [x] **Diagram symbology**: draw pie, donut, and bar charts on features straight from their attributes
341+
- [x] A **data-defined labeling engine** with expression-driven label properties, placement priority, and full control over how labels are drawn
342+
- [x] A shared **Expression Builder** — a function reference, searchable field list, live preview, and reusable variables — wired into filters, labels, styling, and selection
343+
- [x] **Virtual fields**: expression-backed computed columns that update as the underlying data changes, plus a Raster Attribute Table for single-band categorical rasters
344+
- [x] An **attribute form designer** with edit widgets, validation constraints, and conditional field visibility for structured data entry
345+
- [x] **Persistent attribute joins** configured in layer properties, so a table can enrich a layer's features and stick across sessions
346+
- [x] **Select by Expression** and **Select by Location**, plus live query layers backed by DuckDB SQL that re-run as the data updates
347+
- [x] **Atlas / map series** in the Print Layout — generate one page per feature, or a uniform series of pages along a line such as a river or trail — and drop attribute-table and chart blocks onto the page
348+
- [x] Browser-native format conversion for COG, FlatGeobuf, Shapefile, and GeoPackage, and Vector to PMTiles running on a background worker, no Python sidecar required
349+
- [x] **Live GPS tracking**: a moving position marker, a recorded track log, and digitizing new features straight from the GPS feed
350+
- [x] Data quality tools to check validity, fix geometries, and check topology rules, catching and repairing bad geometries before they cause trouble
351+
- [x] A **Processing History** panel that lists every tool run, re-runs it with one click, and copies the equivalent Python code
352+
- [x] Timelapse mode now animates EOX Sentinel-2 cloudless annual basemaps and NASA GIBS providers (Landsat/WELD and MODIS land cover) with a provider picker and legend
353+
- [x] Desktop gains OS trust store and mTLS client-certificate support for native HTTP, automatic reload when local files change on disk, and Esri File Geodatabase (`.gdb`) layer support
354+
- [x] New Natural Earth and Source Cooperative data browsers under Plugins > Web Services, including opening or streaming large GeoParquet from Source Cooperative
355+
- [x] Terrain-aware 3D measurements in the Measure tool, optional title/source captions and on-map panel capture (HTML, legend, colorbar) in video recordings, and a new Georgian locale alongside full Arabic right-to-left support
356+
335357
## Plugin marketplace and registry (design)
336358

337359
This captures the design for the `v1.0` "Plugin marketplace / registry" item. It

0 commit comments

Comments
 (0)