Skip to content

swat.* commands and skills: open, read and edit a SWAT+ model (A1) - #4

Merged
andriyanto merged 301 commits into
mainfrom
a1-swat-commands
Oct 29, 2026
Merged

andriyanto merged 301 commits into
mainfrom
a1-swat-commands

Conversation

@andriyanto

Copy link
Copy Markdown
Owner

A1.0–A1.4 (first part) of the swatplus-rust roadmap. Stacked on #3 (Z/M) and #2 (sync): merge those first.

New swat.* commands (Analyze › SWAT+), built on the fork's rustgis-swat-model and rustgis-swat-txtinout; the project is held beside the map, not in it:

Command
swat.open reads file.cio, what it names and what those name (weather, recall), to a fixed point; confidential
swat.summary, swat.check period, object.cnt, files, missing; values outside cal_parms.cal's calibration ranges (listed, not faults)
swat.find, swat.get columns by part of their name with statistics and range; values, narrowed by match (glob on names) or where (glob on another column)
swat.set replace / add / multiply by (1 + v) as SWAT-CUP; keeps decimals and columns; refuses values outside cal_parms.cal unless force
swat.diff, swat.revert, swat.save, swat.close changes since opening; undo by file/column; write changed files (or all to path), every other byte as read
swat.confidential statistics-only answers; in NEVER (user-only), so the assistant can't lift it without the user

swat.save is in WRITES_FILES and swat.open names a path, so the assistant asks first. Skills swat-model and swat-edit added (and listed in docs). Docs: user-guide/analysis.md › SWAT+ models.

Running the model (swatplus-rust) and reading its outputs come next; swatplus-rust is LGPL-2.1 and RustGIS Apache-2.0, so that part will call the swatplus-rust program rather than link it, unless decided otherwise.

Tests: 6 in cmd::swat (summary, find/get/set by rule with decimals kept, range refusal, where, diff/revert, save byte-identical but the edit, confidential mode leaks no names or values); assistant tests (227) pass, including the check that skills name only real commands. Tried by hand on Batanghari (1,858 HRUs, 260 files, opened in 1.9 s).


Generated by Claude Code

pick2510 and others added 30 commits October 23, 2026 07:21
* feat(linux): draw an Adwaita title bar on Wayland

GNOME on Wayland leaves window decorations to the app. winit was built
without its `wayland-csd-adwaita` feature, so it fell back to
smithay-client-toolkit's plain grey frame. Turn the feature on for the
desktop app on Linux and the BSDs: the title bar now looks like GNOME's,
with minimise, maximise and close, following the light/dark theme.

New crates: sctk-adwaita (MIT) and tiny-skia/tiny-skia-path 0.11
(BSD-3-Clause), which it needs. THIRD-PARTY-LICENSES.txt refreshed with
`cargo xtask notices`.

* docs(linux): note that the winit entry must match eframe's version
Co-authored-by: yongyin-leon <269641072+yongyin-leon@users.noreply.github.com>
…pengeos#724)

A dataset added from the Connections tree is noted in Recent under the
connection's name, keeping the source's kind, address and extra. Direct,
selected and delayed (service tilesets, WCS, downloads, STAC signing)
adds are covered; failed or dropped adds are not renamed.
* feat(ui): pick several layers in the Layers pane

Ctrl/Cmd+click adds or drops a layer, Shift+click takes the rows from the
last one clicked, in the order the pane lists them. The layers picked
together are the layer selection, with the active layer among them; it
belongs to the project's state, not the saved file, and a layer the name
filter hides leaves it.

layer.select takes a list and a mode (replace, toggle, extend). Remove,
visibility, selectable, move and zoomTo take a list in `layer`, or the
layer selection when it is left out: one undo step, and an unknown layer
fails the whole command. Visibility toggles every layer to the opposite of
the active layer's state. Several layers move to the top or the bottom,
keeping their order.

Right-clicking one of several picked layers opens a menu of what acts on
all of them. Adds GLOSSARY.md for layer selection vs feature selection.

* fix(ui): group several picked layers as one layer is grouped

Move To Top/Bottom of several layers failed outright when one of them
was in a group that wasn't already at that end: each layer was sent to
index 0 (or the last), and a layer can't leave its group. A layer in a
group now goes to the top or bottom of its group, the others to the
map's, still one undo step.

The menu of several layers had only "Move To New Group"; it now has the
same Group submenu as one layer's (New Group, Remove From Group, Move To
a group), with layer.group taking a list. A group made from either menu
asks for its name, rather than leaving every one "New Group".

* fix(ui): outline the active layer instead of bolding it

With several layers picked, only the active one had a bold name, which
read as the only one picked. Every picked row now looks alike (tinted,
regular weight); the active layer, the one the Style and Data tabs act
on, has a thin accent outline, as a file browser marks the item with
focus.

* fix(ui): name the several-layer menu items after what they change

"Show Selected / Hide Selected" beside "Make Selectable / Make Not
Selectable" used one word for two things: the picked layers, and
whether the Select tool picks their features. The items are now Show
Layers / Hide Layers and Allow / Block Feature Selection, with a hover
text saying what the latter do.

* fix(engine): keep the layer selection with its project and its active layer

From review of opengeos#728:
- A project put aside moved its layer selection out instead of copying
  it: the next project's layers reuse the same ids, so they came back
  picked.
- A layer selection made around another active layer is cleared after
  the command that left it behind, so the old active layer coming back
  doesn't bring the old selection back.
- layer.group with a one-layer list grouped that layer, not the active one.
- A command's layer conditions read the first layer of a `layer` list,
  so a list works with no active layer (or a raster one).
- The name filter keeps the active layer active when it shows it.
- A double-click with Ctrl, Command or Shift held picks layers, not opens
  Symbology over one.
- layer_menu has its doc comment back; layer.select says that toggling
  out the last layer leaves none active.

* docs(engine): say what layer.move's "to" means for several layers

The params said several layers go to the bottom when "to" is past the
end; 2 of 4 layers at "to": 2 already do, the topmost landing at 2 as
one layer would. The text now says so, and the test pins "to" 2 and 3.
No behaviour changes.
* feat(map): stacked line strokes per class

A line layer's stacked strokes were one symbol for the whole layer, so a
motorway and a footpath on the same layer could only differ in colour
and width. A Unique Values or Graduated Colors class can now stack
strokes of its own: a casing and a core for main roads, a single thin
line for paths. A class without its own takes the layer's strokes, so
existing projects draw as before.

- map: `class_strokes` (and Unique Values' `other_strokes`) beside the
  hand-set legend labels, read from project files bounded and clamped
  like the layer's strokes; kept when the same symbology is applied
  again. `Layer::strokes(row)` picks a feature's stack.
- render: each feature draws its own stack; layers with any stacked
  strokes keep the CPU path, as before.
- engine: `layer.lineStrokes` names a class with `value`, `index` or
  `other`, as `layer.classStyle` does.
- ui: a "Strokes for" picker in the Strokes section; classes with
  strokes of their own are marked.

Tested with swisstopo's SMV25 QGIS project (281 road classes with up to
five strokes each): carried over, the roads and railways draw like
QGIS's rendering of the same project.

* chore(ui): open a combo box by what it shows in ui_shot scripts

* perf(map): skip classing a feature for strokes when no class has its own

* fix(ui): list a class value given twice once in the Strokes for picker

* perf(map): class a feature once for its symbol and strokes
…geos#731)

* feat(ui): pair quotes in the Script pane as braces and brackets

* docs(ui): name quotes in the Script pane's pairing comment

* refactor(ui): name where a Script quote opens and closes
…ngeos#737)

* fix(labels): let each label class label every feature it matches

* fix(labels): key each placed label by its class for lock and tilted views

* refactor(labels): name the label key and simplify its priority

* fix(labels): lock the labels again when a layer's label classes change
… area outlined (opengeos#682, part 2) (opengeos#706)

* Layouts: a publication layout's locator, a globe or an inset with the area outlined (opengeos#682, part 2)

layout.publication takes "locator": "globe" (a sheet's default: a new map
of the map's polygon layers on the globe, unlabelled, in a round frame at
the map's top left, sized from the renderer's own globe disc), "inset" (the
surrounding region on the map itself) or "none" (a figure's default). The
map frame's extent indicator outlines the area on either; the legend stops
short of it; undo takes the globe's map away with the layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the globe locator is a new map of what it draws; no legend forced over it

The globe's map was a clone of the whole map with its non-polygon layers
dropped, so it copied every layer's features first and kept the map's web
tiles, swipe, time slider and groups: a map on a web basemap drew its tiles
on the disc. It is now a new map with the basemap (the land and borders
around the area), graticule and body, and only the polygon layers that
show, unlabelled.

The legend's corner was given at least 10 mm, so a sheet whose frame was
near its smallest drew the legend over the locator (or the scale bar). A
corner too small now leaves the legend out, with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a narrow figure keeps its legend (pinned by a test); the globe's streamed layers left out, said

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a globe locator no layout shows any more is replaced by the next, not left behind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* print-layout skill: the locator's default by profile (globe on a sheet, none on a figure)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: only a locator takes the legend's room; a replaced locator map is said

The legend's width keeps its old 10 mm floor, so a figure or a sheet without
a locator lays its legend out as before; the legend is left out only when a
locator leaves it under 10 mm of height. Reusing a locator map no layout
showed now adds a warning naming it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts test: the reused locator map is rebuilt, not kept (a layer hidden meanwhile is gone)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the globe keeps a sketch of each polygon layer, simplified to its disc

Each layer copied onto the globe's map is simplified (Douglas-Peucker) to
a tenth of a millimetre at the globe's scale, so a project with large
polygon layers saves a sketch of them for the locator rather than a second
full copy. In memory the copy shared the source's features already
(Features is copy-on-write); the saved file is what grew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a polygon too small for the globe's simplification keeps its own ring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: only a globe map made as this command makes them is reused as a locator

A map is now reused only if it also has no web tiles and only unlabelled
polygon layers, so a globe map made by hand under a locator's name is left
alone. A note says the layer clone shares its rows (Features is
copy-on-write).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the simplified sketch named apart from the session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: elpidioassinaturas <elpidiofilho@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(connections): drop stale listings when an add dialog's address changes

Add From Database keeps its table list and chosen table only while the
address they were listed for is unchanged. STAC clears its collections,
items and chosen collection when another API is picked or typed. The S3
form selects the connection kept after a blank-name connect ("default").
Refs opengeos#699 (requirements 7 and 10).

* fix(connections): refill the S3 form when its saved connection is edited or removed

connection.edit changes an S3 connection's address or options and ends its
session, but the form kept the old endpoint, still read as the selected
connection and kept the old bucket listing. The dialog now compares the form's
connection with its saved definition: an edit refills the form and drops what
was listed; a removal lets the connection go.
Refs opengeos#699 (requirements 7 and 10).

* fix(connections): a failed table listing drops the old tables

A failed re-list cleared only the address it was tied to, so the earlier
table list and chosen table survived a later address edit and could be sent
against another database. Found in review of the stale-listing change.
Refs opengeos#699 (requirements 7 and 10).

* fix(connections): drop STAC answers still on their way when the API changes

Picking or typing another API cleared what was shown but left a pending
collections, search or children request and a static catalog's walk, whose
answers then landed under the new API. They are dropped now; an asset being
signed or a link being opened goes on. Found in review.
Refs opengeos#699 (requirements 7 and 10).

* fix(connections): reset STAC listing state fully on an API change

An API change now also forgets the address Save as Connection would keep
and the last listing's status, and a static catalog picked while a listing
was pending is browsed at once. Found in review.
Refs opengeos#699 (requirements 7 and 10).

* test(connections): a catalog picked during a pending listing is browsed

Covers the review fix that rechecks pending work after an API switch.
Refs opengeos#699.
…l comma (opengeos#682, part 3) (opengeos#709)

* Layouts: a publication layout's locator, a globe or an inset with the area outlined (opengeos#682, part 2)

layout.publication takes "locator": "globe" (a sheet's default: a new map
of the map's polygon layers on the globe, unlabelled, in a round frame at
the map's top left, sized from the renderer's own globe disc), "inset" (the
surrounding region on the map itself) or "none" (a figure's default). The
map frame's extent indicator outlines the area on either; the legend stops
short of it; undo takes the globe's map away with the layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a publication layout's surrounds keep off the data; a decimal comma (opengeos#682, part 3)

The north arrow, the legend, the scale bar and the locator each take the corner
where they cover the least of what the map draws (render::layout::frame_coverage:
the frame drawn coarse on a blank basemap), keeping their own corners when it
makes no difference; one warning names those crowded onto the data. "decimal":
"," (layout.publication, layout.page) sets Layout::decimal_comma: graticule,
scale bar and colorbar numbers and {scale} print 0,05 and 1:6.300.000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* layout.publication: a raster theme keyed by a colorbar in the legend's box; a UTM zone's EPSG code named by its zone and datum

Found making the greening lesson's map with it: the topmost visible raster
drawn on a ramp now gets a colorbar under the legend's rows (the legend lists
vectors only), and EPSG:31983 reads 'UTM zone 23S' on 'SIRGAS 2000' rather
than its code on its GRS 1980 ellipsoid (wkt::utm_of_code, the inverse of
the name-to-code table).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the globe locator is a new map of what it draws; no legend forced over it

The globe's map was a clone of the whole map with its non-polygon layers
dropped, so it copied every layer's features first and kept the map's web
tiles, swipe, time slider and groups: a map on a web basemap drew its tiles
on the disc. It is now a new map with the basemap (the land and borders
around the area), graticule and body, and only the polygon layers that
show, unlabelled.

The legend's corner was given at least 10 mm, so a sheet whose frame was
near its smallest drew the legend over the locator (or the scale bar). A
corner too small now leaves the legend out, with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a narrow figure keeps its legend (pinned by a test); the globe's streamed layers left out, said

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a globe locator no layout shows any more is replaced by the next, not left behind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* print-layout skill: the locator's default by profile (globe on a sheet, none on a figure)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a pale fill counts as data where the surrounds go

frame_coverage counted a pixel as drawn only when a channel fell under
245, so a pale fill (such as #FAFAF5) with no outline read as empty and a
surround could be placed on it. The blank basemap is pure opaque white
with no graticule, so anything else now counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: only a locator takes the legend's room; a replaced locator map is said

The legend's width keeps its old 10 mm floor, so a figure or a sheet without
a locator lays its legend out as before; the legend is left out only when a
locator leaves it under 10 mm of height. Reusing a locator map no layout
showed now adds a warning naming it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts test: the reused locator map is rebuilt, not kept (a layer hidden meanwhile is gone)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the corner search's bound stated (four surrounds, 256 picks), its repeat check without indexing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the globe keeps a sketch of each polygon layer, simplified to its disc

Each layer copied onto the globe's map is simplified (Douglas-Peucker) to
a tenth of a millimetre at the globe's scale, so a project with large
polygon layers saves a sketch of them for the locator rather than a second
full copy. In memory the copy shared the source's features already
(Features is copy-on-write); the saved file is what grew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: corners costed with the legend as laid out; the coverage drawing restored however it ends; the scale worked out once

The corner search costed the legend at its full size, so a legend that
would have been shortened to stop short of the locator above it paid the
overlap penalty and moved; it is now costed as it will be laid out.

frame_coverage also sets the map's graticule aside (the blank basemap
already draws it transparent) and puts the basemap, graticule and label
lock back through a guard, so a panic in the drawing leaves the map as it
was.

expand works the frame's scale out once for both its forms, and puts the
decimal-comma scale in before the page and layout names, so a name holding
"{scale}" isn't read as the token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a polygon too small for the globe's simplification keeps its own ring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: only a globe map made as this command makes them is reused as a locator

A map is now reused only if it also has no web tiles and only unlabelled
polygon layers, so a globe map made by hand under a locator's name is left
alone. A note says the layer clone shares its rows (Features is
copy-on-write).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the simplified sketch named apart from the session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* UTM names only for the zones EPSG defines (NAD 83 1-23, ETRS 89 28-38); a closure's parameter named apart from the session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: web tiles set aside with the basemap where the surrounds are placed; the sheet test pins its one placement warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: OVER_DATA's doc comment reads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: elpidioassinaturas <elpidiofilho@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(map): draw stacked strokes in levels

Each feature drew its whole stroke stack in turn, so where two roads met
the second one's casing cut across the first one's core: black bars at
every segment join of a cased road or motorway. A line layer can now draw
its strokes in levels (symbol levels): every feature's strokes of one
level before any of the next, so casings join underneath and the cores
run through.

- map: `LineStroke::level` (none: its place in the stack) and
  `Layer::stroke_levels`, off by default, so maps draw as before.
- render: with levels on, lines with stacked strokes are drawn after the
  rest, a level at a time; a streamed layer a batch at a time.
- engine: `layer.strokeLevels`; `layer.lineStrokes` takes a stroke's
  `level`.
- ui: "Draw strokes in levels" in the Strokes section, and a Level row
  (with Auto) on each stroke while it is on.

With swisstopo's SMV25 QGIS project, its rendering passes carried over as
levels, roads and railways now join like QGIS's rendering of the same
project; the export takes no longer.

* perf(render): visit a feature only for the stroke levels it has

* fix(ui): preview a layer's strokes in level order
…pengeos#740)

* fix(ui): stop a tiled map redrawing for good while a frame is drawn

The map view holds its key at the tile generation of the frame on screen
for 0.25 s after tiles arrive. A tiled flat frame is drawn on a worker,
and the hold never looked at it: when tiles arrived while a frame was
being drawn, the frame drawn with them and the one before kept starting
each other again. Each a full CPU frame and texture upload, it kept a
core busy and the app slow; a WMS layer in a projection other than Web
Mercator (EPSG:2056 with swisstopo's LK25), whose frames take longer to
warp, hit it nearly every time.

The hold now keeps the newest frame of the view: the one being drawn,
or else the one on screen.

* fix(ui): hold tiles at the newest frame of the view, drawn or shown
…le labels kept apart (opengeos#682, part 4) (opengeos#742)

* Layouts: a publication layout's locator, a globe or an inset with the area outlined (opengeos#682, part 2)

layout.publication takes "locator": "globe" (a sheet's default: a new map
of the map's polygon layers on the globe, unlabelled, in a round frame at
the map's top left, sized from the renderer's own globe disc), "inset" (the
surrounding region on the map itself) or "none" (a figure's default). The
map frame's extent indicator outlines the area on either; the legend stops
short of it; undo takes the globe's map away with the layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a publication layout's surrounds keep off the data; a decimal comma (opengeos#682, part 3)

The north arrow, the legend, the scale bar and the locator each take the corner
where they cover the least of what the map draws (render::layout::frame_coverage:
the frame drawn coarse on a blank basemap), keeping their own corners when it
makes no difference; one warning names those crowded onto the data. "decimal":
"," (layout.publication, layout.page) sets Layout::decimal_comma: graticule,
scale bar and colorbar numbers and {scale} print 0,05 and 1:6.300.000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* layout.publication: a raster theme keyed by a colorbar in the legend's box; a UTM zone's EPSG code named by its zone and datum

Found making the greening lesson's map with it: the topmost visible raster
drawn on a ramp now gets a colorbar under the legend's rows (the legend lists
vectors only), and EPSG:31983 reads 'UTM zone 23S' on 'SIRGAS 2000' rather
than its code on its GRS 1980 ellipsoid (wkt::utm_of_code, the inverse of
the name-to-code table).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the globe locator is a new map of what it draws; no legend forced over it

The globe's map was a clone of the whole map with its non-polygon layers
dropped, so it copied every layer's features first and kept the map's web
tiles, swipe, time slider and groups: a map on a web basemap drew its tiles
on the disc. It is now a new map with the basemap (the land and borders
around the area), graticule and body, and only the polygon layers that
show, unlabelled.

The legend's corner was given at least 10 mm, so a sheet whose frame was
near its smallest drew the legend over the locator (or the scale bar). A
corner too small now leaves the legend out, with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a narrow figure keeps its legend (pinned by a test); the globe's streamed layers left out, said

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a globe locator no layout shows any more is replaced by the next, not left behind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* print-layout skill: the locator's default by profile (globe on a sheet, none on a figure)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a pale fill counts as data where the surrounds go

frame_coverage counted a pixel as drawn only when a channel fell under
245, so a pale fill (such as #FAFAF5) with no outline read as empty and a
surround could be placed on it. The blank basemap is pure opaque white
with no graticule, so anything else now counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: only a locator takes the legend's room; a replaced locator map is said

The legend's width keeps its old 10 mm floor, so a figure or a sheet without
a locator lays its legend out as before; the legend is left out only when a
locator leaves it under 10 mm of height. Reusing a locator map no layout
showed now adds a warning naming it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts test: the reused locator map is rebuilt, not kept (a layer hidden meanwhile is gone)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the corner search's bound stated (four surrounds, 256 picks), its repeat check without indexing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the globe keeps a sketch of each polygon layer, simplified to its disc

Each layer copied onto the globe's map is simplified (Douglas-Peucker) to
a tenth of a millimetre at the globe's scale, so a project with large
polygon layers saves a sketch of them for the locator rather than a second
full copy. In memory the copy shared the source's features already
(Features is copy-on-write); the saved file is what grew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: corners costed with the legend as laid out; the coverage drawing restored however it ends; the scale worked out once

The corner search costed the legend at its full size, so a legend that
would have been shortened to stop short of the locator above it paid the
overlap penalty and moved; it is now costed as it will be laid out.

frame_coverage also sets the map's graticule aside (the blank basemap
already draws it transparent) and puts the basemap, graticule and label
lock back through a guard, so a panic in the drawing leaves the map as it
was.

expand works the frame's scale out once for both its forms, and puts the
decimal-comma scale in before the page and layout names, so a name holding
"{scale}" isn't read as the token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a polygon too small for the globe's simplification keeps its own ring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: only a globe map made as this command makes them is reused as a locator

A map is now reused only if it also has no web tiles and only unlabelled
polygon layers, so a globe map made by hand under a locator's name is left
alone. A note says the layer clone shares its rows (Features is
copy-on-write).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the simplified sketch named apart from the session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* UTM names only for the zones EPSG defines (NAD 83 1-23, ETRS 89 28-38); a closure's parameter named apart from the session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: web tiles set aside with the basemap where the surrounds are placed; the sheet test pins its one placement warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a publication sheet's style (classic, banner, panel, editorial) and accent colour (opengeos#682, part 4)

layout.publication takes "style": "classic" (the default, as before),
"banner" (the title and subtitle in a band of the accent colour across the
top), "panel" (the map at the left, a pale panel at the right with the
title, subtitle, locator and legend, the sources and reference at its foot)
or "editorial" (the title large in serif, a column at the right with a
heading, the note, the locator and the legend), on any sheet, portrait or
landscape; "accent" colours the band, tint, headings and rules. A figure
leaves the style to its caption.

The footer and the sources/reference sections are shared by the styles; a
style that sets the locator (and legend) in a column beside the map takes
them out of the corner search. report::serif_width_mm measures serif text
as the layout draws it, for the editorial title's wrapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a column's locator no larger than on an A4 portrait sheet, centred across the column

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: modular and poster styles; rectangles with rounded corners

layout.publication's "style" also takes "modular" (cards with rounded
corners on a light page: the map in one, the locator and the legend in one
each beside it, the sources and reference in tinted ones) and "poster" (the
page in the accent colour, the title in light text, the map in a light card,
the footer in darker blocks with amber headings).

A rectangle element can round its corners ("radius", in mm, 0 to 50; saved
only when rounded), from layout.addElement and layout.update. What a style
draws under the map (a poster's page, a card, the panel's tint) is added
before the map frame, so it no longer lies over the map or its legend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: graticule labels keep clear of each other

A layout's graticule labelled every line wherever it crossed a labelled
edge, so on a projected map a slanting meridian leaving through a side was
labelled over that side's parallel ("36°W" on "6°S"), and a side's label by
a corner ran into the top's.

Meridians are now labelled along the top and bottom and parallels along the
sides first; a line leaving through the other edges is labelled there only
well clear of the labels already set on that edge (a further length of its
own), and a side's labels keep two label heights from the corners. The
choice is a pure function (kept_labels), tested apart from the drawing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a panel's or an editorial page's map frame cut to its area's shape

A panel's or editorial page's map is as wide as its column, so a tall area
left the frame much taller than the area: the map showed a band of sea and
land above and below it. When the frame is clearly taller than the area
needs (with a seventh more of its surroundings), the sheet is laid out
again with the frame that tall, a pass or two more until it is, and the
whole is set in the middle of the page; the neatline and a poster's colour
keep the page's size. A panel whose frame is cut boxes its sources and
reference under the map, in the room the frame left, as a classic sheet
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a sheet given its size alone is turned to the area's shape

layout.publication takes "a4" (now the default), "a3" or "a2": the sheet is
turned to portrait or landscape, whichever the area's map comes out larger
on, from its extent east to west against north to south on the map's
projection and the room the style leaves the map (portrait winning a near
tie). A profile given with its orientation ("a4-portrait") is kept, so the
choice can be changed; the result names the profile used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts test: the arrangement checks on a portrait sheet, the orientation chosen checked apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: OVER_DATA's doc comment reads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the legend's room a pure function, tested; the coverage drawing documented

legend_room decides how far the legend is cut by the surrounds beside and
above or below it, and when the locator leaves it too little (it is then
left out, said). The shrinking sheets of publication_layouts_locate_their_area
may never reach that case, since the corner search keeps the legend off the
locator's side, so the rule is now tested directly.

The comment at frame_coverage's call says the map is drawn on purpose, as
the screen draws it: a coarse grid, streamed layers reading in the
background, no web tiles fetched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: page-wide elements laid on the full page; a failing later pass settles

A publication sheet's neatline and a poster's colour go in their own list,
laid on the full page whatever the frame-fit passes free, instead of being
picked out by name when the content is centred (opengeos#742 review). A pass after
the first that fails (its shorter page leaving the texts too little room)
settles on the last pass that held; only the first pass's failure is the
request's. Each pass is laid out in one closure for that, which re-indents
it (git diff -w shows the change).

Also: a comment on the corner search's NaN costs, the panel's error
indented, and the style test's doc comment in paragraphs, naming modular
and poster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: rounded corners refused off a rectangle; square ones filled as before

layout.addElement and layout.update refuse "radius" for anything but a
rectangle, as "font" is refused off a text, so a script doesn't take it as
applied; restyle's arm that could never run is gone (opengeos#742 review). A
rectangle with square corners is filled with fill_rect again, so existing
layouts draw to the same pixels. hostile_params_never_panic gets junk
style, accent, profile and radius values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: a style or accent not given as text refused; sizes named in the profile error

layout.publication refused an unknown style's or accent's name but took a
number in their place as not given; either is now refused (opengeos#742 review).
An unknown profile's message names a4, a3 and a2 (turned to the area's
shape) before the profiles with their orientation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Layouts: the editorial serif's fallback said; hostile inputs reach style and accent

An editorial sheet's title and subtitle are serif: where no serif is
installed (the web app, a minimal system) they are drawn in the sans, now
with a warning, as layout.addElement gives. hostile_params_never_panic's
new inputs leave the profile valid, so style, accent and radius are
reached; a rectangle's radius not given as a number is refused. The
print-layout skill names the styles and the accent (opengeos#742 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: elpidioassinaturas <elpidiofilho@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(symbology): add No Fill for polygon layers

* docs(symbology): describe No Fill helpers and tests

* docs(tests): describe UI test helpers
…rflow (opengeos#749)

The legend's box is its rows' size plus the padding; the inside is the
box less the padding again, which can come out a rounding error short
of the rows. With macOS's fonts a one-layer legend did, so
publication_surrounds_keep_off_the_data failed there on main. Compare
with a tolerance.
…repo (opengeos#747)

* docs: rework the roadmap, move GeoLibre parity into the repo, add CONTRIBUTING.md

The roadmap gets goals, an Underway list, releases 0.4-0.9 and an Exploratory list with
linked issues and a last-reviewed date. The GeoLibre parity checklist from opengeos#77 moves to
GEOLIBRE_PARITY.md, brought up to date, so anyone can tick items in the PR that lands them.
CONTRIBUTING.md says what to pick freely and what to coordinate first.

Refs opengeos#739

* docs(roadmap): group Next by theme instead of release numbers

Themes keep the useful part of the 0.4–0.9 grouping, the dependencies
between items, without reading as a release plan or going stale when
work lands out of order. GEOLIBRE_PARITY.md and CONTRIBUTING.md now
name the theme too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(roadmap): say what counts as underway; add opengeos#745 and opengeos#671

A rule anyone can check on GitHub lets the weekly review start from the
open pull requests instead of memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: coordinate first on shared contracts; point agents at the roadmap

The `.rgis` format, existing command ids and parameters, and the plugin
interface are depended on outside the code that changes them. Most pull
requests start from an agent reading AGENTS.md, so it now points at the
roadmap and the contributing guide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(roadmap): add a goal for staying free and self-contained

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: address review on the roadmap and parity checklist

Underway hands off on an issue or a pull request; GeoLibre's baseline links to its v3.4.0 tag;
the ArcGIS token line reads clearly; the COG check says what is unverified; the terrio bullet
is wrapped. Drops opengeos#671, which looks abandoned.

---------

Co-authored-by: mavrick <mavrickfitzgerald@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…geos#753)

Fill the strings added since the last catalog update (A5 grids, Zarr
time series, S3 and database connections, layout shapes, label
classes, kriging and trend tools, keymap) in all 16 catalogs and drop
27 stale entries whose English was reworded or removed.
…pengeos#750)

A translucent accent (#RRGGBBAA) would leave a banner, a poster's page or
the headings faint or unseen, so it is refused, as documented (#RRGGBB).
Column::legend was true wherever a column is made, so the field and the
branches for false are gone (opengeos#742 review).

Co-authored-by: elpidioassinaturas <elpidiofilho@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(map): a layer's reference scale

A layer can be given the scale its symbol and label sizes are set for
(`Layer::reference_scale`, QGIS's reference scale). Drawn at another
scale, its lines, markers and labels grow and shrink with the map, as on
a printed map: at half the reference scale twice as large, at twice it
half as large, kept within 1/100 and 32 times.

- map: `reference_scale` (skipped when unset; a project's out of
  1..1e9 is none) and `Layer::size_factor`.
- render: the layer is drawn with its points that much wider in pixels
  (one `Ctx` per scaled layer, so the CPU and GPU paths both follow),
  and its labels that much larger.
- engine: `layer.referenceScale={"scale": N|"current"|null}`.
- ui: Reference scale in the Style pane's Layer section: none, the usual
  scales, or the map's current one.
- docs: cartography.md › Reference scale, with the 0.28 mm pixel RustGIS
  takes for its scale against 96 dpi.

* fix(render): scale a layer once per draw, and its upright markers, for its reference scale

The scaled drawing context is built once per layer instead of per batch
of a streamed layer (keeping its marker sprites), and also covers what is
drawn after the batches (aggregation bins, extrusions). Point markers
standing up in a tilted view take the factor too. "current" refuses a
map without a scale, and the picker marks Current scale when it is the
layer's.

* fix(render): scale a label's gap with its reference scale, and a tilted view's labels at its own scale

A label is placed as at its own size and then moved that much farther
out, so the fixed gap to its feature grows with it. The labels a tilted
view's bands hand back are sized at the view's scale, as its upright
markers are, not each band's. Tests: the GPU meshes' stroke widths, and
a point label's size and distance at twice the reference scale.

* refactor(render): the scaled Ctx copies the view's fields by struct update
…none is said to be ignored (opengeos#579) (opengeos#766)

* cube.trend: a STAC collection's keys at the top of its description, so none is said to be ignored

The url, asset, bbox, maxCloud, cellSize and resampling keys sat inside the
"collection" entry's note, where param_keys doesn't read, so every STAC trend
run from the CLI or a script was told they were ignored, though they're used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.trend: the keys test refused before any search, off the network

a_stac_trends_keys_are_read reached an API on a closed local port. It now
gives a cell size of 0, refused before the search, and checks that error,
so nothing depends on how a sandbox treats the connection (opengeos#766 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.trend: the collection's note names its keys

"with the keys below" left the reader to find them; the note now names url,
asset, bbox, maxCloud, cellSize and resampling, unquoted inside its
parentheses, where they aren't read as keys (opengeos#766 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: elpidioassinaturas <elpidiofilho@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs: bring every page up to date with the code

* docs: address review comments (wording, LiDAR sample, plugin registry schemas, language count)

* docs: correct sslmode=prefer; limit the offline claim to local data
…pengeos#767)

A SecretStore trait, an opaque Secret and an in-memory store in rustgis-data
(wasm-safe); a Services::secrets hook; and a desktop implementation on
keyring-core (macOS Keychain, Windows Credential Manager, the Secret Service
over pure-Rust D-Bus). None in the browser and the CLI. No secret moves yet.

Refs opengeos#754
…engeos#772)

* fix(symbology): choose compatible renderer fields

* fix(ui): clear pending renderer when layer changes

* fix(ui): keep symbology retries scoped and explained

* fix(ui): end symbology retries with the pane, layer or renderer
…lay too (opengeos#776)

planet_layer_toggles_show_in_the_next_frame_and_stale_jobs_do_not_restore_them
checked the settled frame's centre with red > green, which asks whether
the Moon tile is there rather than whether the hidden overlay came back.
On macOS CI a settled frame showed the body's plain grey (227, 227, 227)
at the centre and failed. Check the overlay's green, as the preview
checks above it do.
…year (opengeos#579) (opengeos#774)

* Add cube.reduce: a statistic over time of any data cube (opengeos#579)

cube.reduce summarises each cell over the dates of any series cube.trend
takes (layers, a raster's bands, a Zarr variable, a STAC collection's
band): mean, min, max, sum, std, count or countAbove, for the whole
period or one layer per calendar year ("by": "year") or month of the
year ("by": "month"), with start/end/months and minValid as a share of
each group's dates.

The slices are folded into running totals as they are read, so a long
period takes no more memory than a short one; a STAC area is read in
pieces and stitched as for a trend. The reader loop, the grid check and
the stitching move out of TrendRun into shared helpers, and zarr.reduce
uses the same Stat and running totals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: every key at the top of its description, so none is said to be ignored

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: a STAC collection's band summarised, tested

A local STAC API and range-served COGs, as for the STAC trend: the mean of
the whole period and of each month matches each file's own scale and
offset, cell by cell, on the full images' grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: a group's totals made at its first date, finished at its last

Every group's running totals were made at once, up to 20 bytes a cell for
std, so a summary by year near the 32-million-value cap could hold some
640 MB of totals. A group's totals are now made when its first date is
read and turned into its output when its last is, so a summary by year
holds one year's at a time; a group with no data in a piece gives what
empty totals would. The collection's note names its keys, as cube.trend's
does (opengeos#774 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: classified for plugins and recorded steps as cube.trend is

A plugin with 'files' alone could reach a STAC API through cube.reduce's
url, since only cube.trend and zarr.reduce were in DATABASES; it now needs
'files' and 'network' for both, tested. cube.reduce defaults to the active
layer, so it joins DEFAULTS_TO_ACTIVE_LAYER, which every_optional_layer_is_
classified asks for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: Summarise, as zarr.reduce's label and the guide spell it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: the size cap checked across pieces; the order of arrival said

A cube that doesn't know its size had only each piece checked against
MAX_REDUCED; the cells of the pieces read so far are now checked too, as
the trend does. The notes say what the order of arrival does: read one at
a time a summary by year holds one year's totals, with several readers (a
STAC cube's) those of the dates in flight, and an output's last bit at
most can move (opengeos#774 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: a date that can't be read refused, not put in group 0; junk reaches its keys

A step whose date isn't finite would have gone to year or month 0; it is
now refused with what to give. hostile_params_never_panic gains two junk
sets with a statistic, so cube.reduce's threshold, groups, share, months
and dates are reached (opengeos#774 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: a cube a trend can't take is summarised; each run names itself

cube_from took cube.trend's limits for both commands: four dates at least
and a Zarr axis in increasing order, and its errors said "trend". It now
takes what the run needs (TREND or SUMMARY): a summary takes two layers or
bands, or a Zarr axis in any order, and the messages say trend or summary
(opengeos#774 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: a period named from its earliest date to its latest

A Zarr axis in decreasing order, which a summary takes, put its last date
first in the outputs' names; the names now take the earliest and latest of
the dates read, tested. The STAC cube's date cap, reached by both runs,
says trend or summary (opengeos#774 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: a summary takes two dates or more, layers and bands alike

A stack of one layer was taken while a raster of one band wasn't, and the
message read "1 layers". A summary now needs two dates either way; the
note on Needs says why (opengeos#774 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: a STAC summary compared within its last bit, not to the bit

A STAC cube's dates come as read, eight at a time, so the totals' order
and an output's last bit can differ between runs; the comparison of a
summary read whole, in pieces and by year failed once in CI for that.
It now allows 1e-6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cube.reduce: the totals' memory by month said, in the code and the guide

A summary by month keeps every month's running totals while it reads: up
to five times its outputs' memory for std, three for the other statistics,
some 640 MB for std at the cap on the desktop. MAX_REDUCED's note and the
guide's Size paragraph now say so (opengeos#774 review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: elpidioassinaturas <elpidiofilho@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(data): define source projection for Shapefile layers

* fix(data): normalize CRS options and report import failures
choupeanut and others added 29 commits October 28, 2026 07:14
* feat(ui): customize keyboard shortcuts

* fix(ui): address shortcut editor review

* fix(ui): use direct plugin command lookup

* fix(shortcuts): let Escape cancel a shortcut capture

While the shortcut editor waits for a key, a bare Escape now cancels
the capture instead of being assigned to the command. Clarify the
first-person comment (unmodified keys are paused, Escape included),
and name the ribbon button "Customize Shortcuts…" to match Command
Search. The editor's notice is a flag, since its text was never read,
and the notice string sits inside tl!() where it is drawn.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

Applied by the claude-autofix workflow.

* fix(ui): finish shortcut customization review fixes

* fix(ui): preserve rejected shortcut preferences and filter web commands

* fix(ui): rename shortcut editor to Keyboard Shortcuts

* fix(shortcuts): keep shortcut capture from reaching the map

On macOS, Ctrl+T pressed while capturing was stored as a bare T, since
Ctrl is not the command key there. Capture now skips a Ctrl-only press.
While a shortcut is being captured, first-person walking and the Escape
sketch cancel are paused, so they no longer act on the same keys.

Saved shortcuts that cannot be read now block saving over the stored
text, instead of being dropped by the next edit. Too many changes to
save get their own notice rather than being reported as unreadable.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

Applied by the claude-autofix workflow.

* fix(shortcuts): drop the ellipsis from Keyboard Shortcuts

The command opens a window, not a dialog asking for more input, so its
label reads Keyboard Shortcuts in the ribbon, the Settings menu, Command
Search and the docs.

* style: cargo fmt

* Address Claude review feedback

- Saving shortcut preferences now rejects empty or over-long command
  ids, the same per-record check loading applies, so a saved file can
  always be read back (shared MAX_COMMAND_LEN, with a test).

* Address review feedback

- On a Mac, shortcut capture skips Ctrl+Cmd presses as well as Ctrl
  alone, so the dropped Ctrl never turns Ctrl+Cmd+T into Cmd+T
  (CodeRabbit).
- Import Shapefile with Projection is left out of the shortcut editor
  and Command Search's customize entry: its source CRS comes only from
  its dialog, so a bare shortcut would always fail (CodeRabbit).
- A test captures Tab and an arrow key, which the editor must still
  see after the widgets drawn earlier in the frame (Claude).

* test(shortcuts): keep Command Search and the editor rows in step

Command Search's Keyboard Shortcuts entry uses can_customize, and the
editor lists rows() separately, so the two could drift. A new test
checks that every editor row can be customized, and that every
customizable command has a row.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

Applied by the claude-autofix workflow.

---------

Co-authored-by: Qiusheng Wu <giswqs@gmail.com>
Co-authored-by: giswqs <5016453+giswqs@users.noreply.github.com>
…ics (opengeos#1070)

* feat(analysis): several statistics and case fields in Summary Statistics

Summary Statistics takes a list of (field, statistic) pairs and several
case fields. New statistics: RANGE, VARIANCE, MEDIAN, P1-P99, IQR, CV,
MODE, MINORITY, UNIQUE, FIRST, LAST, CONCATENATE and NULLS, with text
statistics for text fields. Output gains FREQUENCY, and a group with no
values reports null instead of 0. Calls with a field alone keep their
six statistics under the same names.

Grouping and the statistics live in a new stats module, for the Pivot
Table tool to share.

Part of opengeos#1019.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analysis): address Summary Statistics review feedback

- UNIQUE counts whole-number fields exactly, past 2^53.
- Percentiles interpolate as a weighted sum, so extremes of opposite
  sign don't overflow.
- Each column is sorted once for every statistic that needs it.
- A statistics list holds at most 256 pairs.
- Every NaN is one group key; 1 sorts before 1.0; CONCATENATE writes
  -0 as 0.
- Case fields and output fields can't differ only in case.
- An empty statistics list no longer hides Statistics Field; Separator
  shows only for a list with CONCATENATE.
- Case Field help and user guide wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analysis): total group order and lazy sorting in Summary Statistics

- Group keys sort in a total order of their own (nulls, numbers by
  value, then text), so a field mixing text and numbers can't give the
  sort an inconsistent comparison.
- A row's key values are copied only when it starts a group.
- A column is sorted only for the statistics that read it sorted.
- CONCATENATE writes whole numbers exactly past 2^53.
- A percentile between equal neighbours is that value, even subnormal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analysis): exact group keys and a bounded separator in Summary Statistics

- Group keys: -0 is written 0 and every NaN is one value, after all
  numbers; whole numbers and decimals compare exactly past 2^53.
- Grouping hashes borrowed text instead of copying it per record.
- CONCATENATE writes whole numbers exactly in mixed columns too.
- Separator is at most 256 bytes.
- User guide: an empty table gives no row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analysis): linear output and exact UNIQUE in mixed columns

- Summary Statistics fills its output with push_next, so writing it is
  linear in the number of groups.
- UNIQUE compares whole numbers and decimals exactly when a column
  holds both.
- Numeric statistics report -0 as 0.
- Tests for the grouping order of large mixed keys; guide re-wrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analysis): exact MODE and MINORITY past 2^53 in Summary Statistics

- MODE and MINORITY tell whole numbers apart exactly, as UNIQUE does;
  the exact order is sorted once per column.
- Text group keys compare in lower case without allocating.
- Column equality ignores its sort caches.
- Test: UNIQUE counts -0, 0 and the whole number 0 as one value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s#1079) (opengeos#1082)

A Shapefile without a .cpg is decoded by its .dbf language driver ID. dbase
0.8 knows only some of them, and RustGIS added 0x57 by hand, so 0x58 and
0x59 (Western European and Spanish ANSI, Windows-1252) and the other IDs of
the DOS, Baltic, Korean, Chinese, Thai and Mac pages were read as UTF-8.
The ID is now mapped to its code page with the language driver table GDAL
reads, and decoded as a .cpg with that number is. A page RustGIS can't
decode is named in the note.
… drivers (opengeos#1087)

* perf(render): set the mitre limit once, and test the limits themselves

* fix(data): say which DOS pages take dbase's decoder, and pin what 0 means

* test(data): name every dbase code page mark in the pinning test

* docs(data): say which pages from_number leaves undecoded
* feat(analysis): add a Pivot Table tool

pivotTable turns one field's values into fields: a row for each
combination of the row fields' values and a field for each pivot value,
sorted by value. Without a statistic it reshapes, refusing a row and pivot
value with more than one record; with one it combines them through the
shared stats module, as Summary Statistics does. Empty cells are null,
a missing pivot value is an error, and pivot fields and cells are capped.

Part of opengeos#1019: tool tests, hostile parameters, engine gp.run and undo,
the assistant reading the table, the user guide and assistant skills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analysis): address Pivot Table review feedback

- Name decimal pivot values in full, not rounded to six places as values
  are shown, so values closer than that keep their own names.
- Read the separator only when CONCATENATE is used, in Pivot Table and
  Summary Statistics: the Tools pane hides it otherwise, so a value left
  there no longer fails the run.
- Refuse a non-text statistic, pivot or value field instead of treating it
  as missing.
- Count the row fields in the 10-million-cell limit.
- Rename the separator helper so it doesn't shadow its callers' locals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(analysis): address second Pivot Table review

- Trim underscores after the 64-character cut, so a cut at a run of other
  characters leaves none at the end of a pivot field's name.
- Say that no row fields give one row for the whole table, or none for an
  empty one, in the docs and the Row Field help.
- Refuse a bad separator on every run again, as Summary Statistics did
  before: the Tools pane already leaves Separator out of runs without
  CONCATENATE, so the previous commit's change wasn't needed.
- Move the naming of pivot fields into its own helper.
- Check the cell values the assistant reads back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(analysis): reword Pivot Table's rows without row fields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(analysis): describe how pivot names merge underscores

A run of other characters right after an underscore adds none
(North_-east gives North_east): say so in the doc comment and the user
guide, and pin it with a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(ui): open the Notebook beside the map on half the window

The right pane was capped at 640 points and every pane but Web Data
opened 320 wide, so a notebook was a narrow strip next to the map.
The Notebook pane now keeps its own width and opens on half of the room
right of the left pane, and any right pane can be dragged as wide as
the window allows while the map keeps at least 320 points.

Part of opengeos#1056

* fix(ui): size the Notebook pane without clamp's panic

clamp panics when its minimum passes its maximum, which a room under
320 points gave. Say why a pane keeps its old 640 points on mid-size
windows, say in the guide that the Notebook takes half the room right
of the left pane, and have the test hold the map to its full minimum.
…1090)

* docs(ui): say the Notebook pane opens at least 320 points wide

* docs(ui): the 320-point minimum holds where the window allows
…g order (opengeos#1086)

* fix(ui): a WMS layer's answer takes its place in the pop-up by drawing order

The pop-up appended what a WMS layer answered after the features identify
found, so with the default topmost mode a WMS layer drawn over Countries
showed Countries' feature, its own only behind the next arrow. Answers are
now ranked in the map's drawing order, as identify ranks what it finds:
with topmost, a WMS layer drawn over the feature found takes its place once
it answers with features; with the other modes it comes first.

* fix(ui): the pop-up stays on the same one of identical results

* fix(ui): a hidden layer's late answer doesn't take the pop-up

* fix(ui): a hidden tile layer's results leave the pop-up, and same-named ones rank by the one drawn
…pengeos#1095) (opengeos#1096)

* fix(data): write a .prj for Swiss, Krovak and named-meridian systems (opengeos#1095)

* fix(data): name a national datum by its ellipsoid and shift however they're written
… found together (opengeos#1098)

* fix(ui): a tile layer hidden with its result in the pop-up takes it out at once

* fix(ui): the pop-up waits for the WMS answers to show all it found together

* docs: with nothing else found, the pop-up waits for the WMS answers

* docs: the pop-up's wait for WMS answers, with and without a feature found
…on the time slider (opengeos#1100)

* feat(data): WMS layers with a time dimension, drawn and asked at their time

* fix(data): one TIME per WMS request, after the query, and a layer's own time dimension replaces its parent's

* feat: WMS and WMTS layers with a time dimension follow the time slider

* fix(data): month-end steps stay month ends, times with milliseconds, and the slider starts at the latest

* feat(ui): NASA GIBS layers follow the time slider too

* fix(data): review fixes for WMS time: empty 1.3.0 dimensions, times kept per document and the latest of them, ISO 8601 periods, long ranges within the slider

* fix: read a dimension's last times, refuse a second decimal point in a period, and list a long time list's ends
…ch with its visibility (opengeos#1101)

* feat(tiles): source layers of a vector archive in the Layers pane, each with its visibility

A vector PMTiles or MBTiles archive lists its vector_layers under its row in the Layers pane, each with a checkbox. A source layer switched off is left out of the resolved style, so it neither draws nor answers identify. map.setTiles takes hiddenLayers (undoable, kept with the project); map.tiles lists sourceLayers and hiddenLayers.

Part of opengeos#557

* fix(tiles): list only source layer names a tile layer can hide

An archive's empty or overlong vector_layers ids are left out of map.tiles's sourceLayers and the Layers pane, as the hidden list leaves them out.

* fix(map): repeated hidden source layer names don't use up the cap

* fix(tiles): list an archive's source layers once each, bounded before they are copied

Valid ids are picked from the PMTiles metadata or the MBTiles json entry, deduplicated in the archive's order and capped at 256 as they are read, so repeats or invalid ids don't push later names out and a hostile list isn't copied in full every frame. The doc comment says an MBTiles file's metadata is read on first access.

* fix(ui): an archive row with a legend and source layers says its arrow opens both

* fix(tiles): an archive's vector layers count once each in its default look

PMTiles and MBTiles Info::vector_layers list each id once in the archive's order, the first 256, so repeated ids don't push later layers out of the named-layer style or the basemap check.

* fix(tiles): one rule for an archive's source layers, worked out once

rustgis_data::source_layers keeps vector_layers ids that are non-empty and at most 256 characters, each once in the archive's order, the first 256. PMTiles and MBTiles Info store the result when their metadata is read, so the default look and the Layers pane use the same names and the pane doesn't rescan the metadata each frame; the hidden list uses the same rule.
…outdent (opengeos#836) (opengeos#1102)

* feat(layers): nested layer groups and outline commands to indent and outdent (opengeos#836)

* perf(ui): judge a filtered top-level group once per frame, not once per layer

* perf(map): Map::outline remembers listed items in a set

* fix(map): a group with no layers put at the end of its level goes after the others there

* fix(ui): Ctrl/⌘+arrow shortcuts still run with a layout element selected

* test(ui): drag a subgroup's row out of its group, and undo
…econd, fourth… step (opengeos#1110)

* docs: a period longer than the time slider's steps is stepped every second, fourth… step

* docs: word the long period's steps as review suggested

* docs: only a single long period is thinned; a list too long for the slider is added at its default
…engeos#1114)

* fix(render): a thin shapeburst's tiles stop at the frame's budget, shaded from the coarse grid past it

* fix(render): the coarse fallback is painted once, and only tiles in the picture are measured

* fix(render): a thin shapeburst's depth over every tile of the clip box again, the coarse grid's only for tiles the budget left out
…s#1124)

Zoomed in past where f32 holds the basemap's meshes, they are drawn on the CPU (opengeos#902) and the
GPU scene is shown as a picture, shifted and scaled until it is drawn again. The live raster
tiles in it were placed for the new camera and then moved again with the picture: a WMS layer
zoomed past its max zoom flickered and stepped back on each wheel step. In a picture they are
now placed for the camera it was drawn for.
Dataset.zm holds each feature's per-vertex Z and M (VertexZm), ignored once it
no longer fits the feature's vertices. Shapefiles of the Z and M types are read
with them and a layer with them is written as Z (or M) shapes; GeoJSON reads and
writes a third coordinate; wkb::decode_zm and encode_zm read and write ISO and
extended WKB Z, M and ZM. Projects save them. Layers without them are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L94WcvvLDssnAEZihGAwYo
A SWAT+ TxtInOut folder opens beside the map: summary, check, find, get, set
(replace, add, multiply as SWAT-CUP's v__/a__/r__, checked against
cal_parms.cal), diff, revert, save (only the changed values change), close.
A confidential project answers with statistics only; only the user can lift it.
Skills swat-model and swat-edit teach the assistant to use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L94WcvvLDssnAEZihGAwYo
…the web build)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L94WcvvLDssnAEZihGAwYo
@andriyanto
andriyanto merged commit e632b77 into main Oct 29, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.