Repository navigation
feat(layout): a legend shows a layer under a label instead of its name (#1654) - #1739
Conversation
opengeos#1654) A legend element keeps `labels: {"<layer name>": "<label>"}`, matched by name in any case as `exclude` is. `layout.update` (and `layout.addElement` for a legend) take `"labels": {"<layer>": "<label>"|null}`: each entry replaces that layer's label, null or blank shows its name, {} every name; a layer the map hasn't is refused. The legend draws and measures the label (rows, headings, a WMS/WMTS layer's heading), so its size and fit warnings follow it. Saved with the layout (BTreeMap, skipped when empty); labels follow a renamed layer and go with a removed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
| let label = match label { | ||
| Value::Null => None, | ||
| Value::String(t) if t.trim().is_empty() => None, | ||
| Value::String(t) => Some(t.chars().take(MAX_LEGEND_LABEL_CHARS).collect()), |
There was a problem hiding this comment.
Silent truncation (AGENTS.md "Never silent"). A label over 200 characters is cut here without any notice, so a script that sends a long label gets it shortened and is never told. The docs say "up to 200 characters", but AGENTS.md requires a command that truncates to say what, how much and where. The test legends_label_layers asserts the cut length, not a notice.
The smallest fix is to refuse over-long labels, as other bad inputs in this function are refused:
| Value::String(t) => Some(t.chars().take(MAX_LEGEND_LABEL_CHARS).collect()), | |
| Value::String(t) if t.chars().count() > MAX_LEGEND_LABEL_CHARS => { | |
| return Err(format!("a legend's label is at most {MAX_LEGEND_LABEL_CHARS} characters")); | |
| } | |
| Value::String(t) => Some(t.clone()), |
If cutting is kept instead, it needs a notice, and the test should check it. Confidence: high (about 80%).
There was a problem hiding this comment.
Fixed in 8795cb7. layout.addElement and layout.update now refuse a label over 200 characters, as you suggest, with a legend's label is at most 200 characters ('Rivers' has 250), and the element is left as it was. A command never cuts a label now; only a file opened or imported is cut, and that one says so (see the thread on sanitize). legends_label_layers checks that a 201-character label is refused with nothing changed and that a 200-character one is kept whole. The command help and docs/user-guide/layouts.md say a longer label is refused.
🤖 Addressed by Claude Code
| /// case, as the map spells it): its row, or the name over its classes (#1654). The layer | ||
| /// keeps its name everywhere else. A layer not in it shows its name. | ||
| #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] | ||
| labels: BTreeMap<String, String>, |
There was a problem hiding this comment.
Question for the maintainers: does this need a format bump? docs/maintaining.md says a format's version goes up "only when a file uses something an older reader would lose". An older RustGIS reads this field as unknown, drops it, and saves the layout again without labels. That is silent loss of the kind the rule is for. .rgislayout files carry no version, and the project's FORMAT in crates/engine/src/project.rs stays at 1.
The earlier legend columns and exclude fields were added the same way without a bump, so this may be a deliberate precedent, but it is not stated anywhere. Please decide it and note the decision in the PR. Confidence: medium (about 50%).
There was a problem hiding this comment.
No change here: this is for a maintainer, as you say, and the PR text now says it under Limits ("No format version change"). By the letter of docs/maintaining.md it is a loss: an older RustGIS shows the layers' names, and drops the labels if it saves the file again. No map data is lost. The legend's exclude (#518) and columns (#1494) and the text font family (#675) were added the same way, as optional fields with no version change, and the same question on #1494 was left for a maintainer. .rgislayout files have no version to raise. Raising the project's FORMAT would make 0.6.0 refuse every project this writes, labels or not, since write_format always writes the current one; a version written only when a legend has labels would avoid that, but would be the first of its kind. If a maintainer wants a version change here, I'll make it. Leaving this open for that view.
🤖 Addressed by Claude Code
| cut |= labels.len() < labelled; | ||
| for label in labels.values_mut() { | ||
| let end = label.char_indices().nth(MAX_LEGEND_LABEL_CHARS).map_or(label.len(), |(i, _)| i); | ||
| cut |= end < label.len(); |
There was a problem hiding this comment.
The import notice is now inaccurate for labels. This cut feeds elements_cut, which crates/engine/src/project.rs (layout_notices, not changed by this PR) reports as "a name, text, polygon points or legend exclusions cut to their limits". A label cut on import is therefore described as an exclusion cut. That notice should mention legend labels too. The PR doesn't touch project.rs, so I can't anchor the fix there. Also, no test checks the import notice for a cut label, which AGENTS.md's "test both ways" rule asks for. Confidence: high (about 80%).
There was a problem hiding this comment.
Fixed in 8795cb7. The notice in layout_notices now reads … layout element(s) with a name, text, polygon points, legend exclusions or legend labels cut to their limits …, and the doc comment on LayoutRepairs::elements_cut says the same. legends_label_layers now checks both ways: importing the saved .rgislayout as it is gives no such notice, and importing it with a 250-character label gives one notice naming legend labels, with the label cut to 200 characters.
🤖 Addressed by Claude Code
Code reviewBugs
Security
Performance
Quality
AGENTS.md
Prior reviews: none on this PR, so I left no earlier concerns alone. Change sizeCompared: the whole PR against main (merge base 7cc9bb6 to PR head 1d22259).
|
📖 Docs preview
GitHub Pages takes a minute or two to publish a new commit. |
…ls in the import notice (opengeos#1654) - layout.addElement and layout.update refuse a label longer than a legend keeps instead of cutting it (AGENTS.md "Never silent"). - The notice for layout elements cut on reading now names legend labels, which Layout::sanitize also cuts. - Tests: a 201-character label refused, a 200-character one kept whole; an imported layout file with a long label cut, with the notice; no notice for a clean file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| *columns = (*columns).clamp(1, MAX_LEGEND_COLUMNS); | ||
| // The same for the names labelled; a label is cut to its length, and a blank | ||
| // one shows the name, as it would without. | ||
| labels.retain(|_, label| !label.trim().is_empty()); |
There was a problem hiding this comment.
Two spellings of one layer are kept when a layout is read from a file (Never silent / consistency). The command path refuses {"Rivers": …, "rivers": …} because "which one counts is not the caller's to guess", and with_labels keeps one key per layer. This sanitize step only drops blank labels and cuts long ones, so a hand-edited or older .rgislayout with "Rivers" and "rivers" keeps both. shown() then picks the exact-case match or the first case-insensitive one in key order, and legends_follow_layer uses the last. Nothing is reported, and the legend's label depends on key order.
Suggested direction: when two keys match case-insensitively, keep one (the first in key order, say) and count it in cut so layout_notices reports it. A test would import a layout with both spellings and check the notice plus the single kept label. Confidence: medium (about 50%). The file is hand-edited input, so impact is limited, but the rule the PR states for commands should hold for files too.
There was a problem hiding this comment.
Fixed in d8f94a7. sanitize now keeps one label per layer: of two spellings of a name (ASCII case), the first in key order stays, here Rivers over rivers, and the other is counted in cut, so layout_notices reports … legend exclusions or legend labels cut to their limits …. legends_label_layers imports a .rgislayout with {"Rivers": "Rios", "rivers": "rios", "Lakes": "Lagos"}, and checks that one notice is given and that the legend keeps Lakes: Lagos and Rivers: Rios. The test fails with the dedupe taken out.
🤖 Addressed by Claude Code
Code reviewBugs
Security
Performance
Quality
AGENTS.md
Prior threads: 3 earlier threads on this PR. One remains open and I left it alone: the format-bump question ( Change sizeCompared with main (merge base 7cc9bb6 to PR head 8795cb7).
|
…ngeos#1654) Layout::sanitize now keeps one of two spellings of a layer's name in a legend's labels (the first in order) and counts the other as cut, so the import notice reports it, as a command refuses both spellings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| return Err(ONLY_LEGEND_COLUMNS.into()); | ||
| } | ||
| // (A grid's "labels" is a bool: only an object is a legend's.) | ||
| if kind != "legend" && p.get("labels").is_some_and(Value::is_object) { |
There was a problem hiding this comment.
labels is refused on other kinds only when it is an object, so other values are silently ignored. exclude and columns refuse any non-null value on a non-legend, and the PR body says labels is "refused like exclude". So layout.addElement {"kind": "title", "labels": "Rivers"} (or ["Rivers"]) is accepted and dropped with no error. The same gap is in layout.update (the _ if p.get("labels").is_some_and(Value::is_object) arm, about line 3255): a string or array labels on a title is ignored.
Suggested fix for this line: refuse anything non-null except a grid's boolean. The update arm should get the same treatment, minus the grid exception, since grid options are nested under "grid" there.
Confidence: about 65%. I did not run this; it is from reading the code and the exclude/columns checks above it.
| if kind != "legend" && p.get("labels").is_some_and(Value::is_object) { | |
| if kind != "legend" && p.get("labels").is_some_and(|v| !v.is_null() && !(kind == "grid" && v.is_boolean())) { |
There was a problem hiding this comment.
Fixed in f0a74f8. On an element that isn't a legend, layout.addElement now refuses labels of any non-null value with only legends label layers, as it does exclude and columns. On a grid, which has its own labels, anything other than true or false is refused with a grid's labels is true or false, instead of the legend's message. layout.update refuses any non-null labels on an element that isn't a legend, since a map frame's grid takes its options under "grid". On a legend, a value that isn't an object was already refused (labels is an object of layer names and labels). legends_label_layers now checks a string, a list and a bool on a title (update) and a scale bar (add), and "labels": "no" on a grid, with nothing added.
🤖 Addressed by Claude Code
Code reviewBugs
Security
Performance
Quality
These are minor and can wait for the release quality pass. AGENTS.md
Earlier review threads: 4 threads were already on the PR. I left one alone because it was already raised and answered and is still open for a maintainer: the format-version question. The other three are fixed in the current code and have tests, so I did not raise them again: the import notice wording, the two-spellings case on import, and the silent 200-character cut. Change sizeCompared: the whole PR against main (merge base 7cc9bb6 to PR head d8f94a7).
|
…ds (opengeos#1654) layout.addElement and layout.update refused "labels" on another kind of element only when it was an object; a string, list or bool was left unread. Now any non-null value is refused there, as "exclude" and "columns" are, and a grid's "labels" that isn't true or false is refused with its own message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // bears that name, whose label (or none) the renamed layer then shares. | ||
| let keys: Vec<String> = labels.keys().filter(|n| n.eq_ignore_ascii_case(old)).cloned().collect(); | ||
| let label = keys.iter().filter_map(|k| labels.remove(k)).last(); | ||
| if let (Some(n), Some(l), false) = (new, label, shared) { |
There was a problem hiding this comment.
Possible silent loss of a user's label (AGENTS.md "Never silent"), confidence about 35%. When a layer is renamed onto a name another layer already bears (shared), the label is taken off the old name and not put on the new one, so the text the user typed is discarded with no notice. The docs and doc comment state this as deliberate, and exclude behaves the same way, so this may be an accepted design choice. But Session::notify (one notice per kind, with a count) is the rule for a command that drops data, and a label is user-entered text. Either a notice when a non-empty label is dropped here (and on layer.remove at the None path), or a note in the PR that a label is treated as presentation state and not data. I did not find a test that covers the dropped-label case with a notice.
There was a problem hiding this comment.
Fixed in 3f959e5. Renaming onto a name another layer has is allowed (layer.rename doesn't refuse it), and the label was dropped without a word. It is now said once per rename, with the count: 'Hidrografia', renamed 'Cities', lost its label in 4 legend(s): another layer is called 'Cities', and a legend labels layers by name. legends_label_layers checks the notice: four legends there, the original and three imported layouts. It also checks that a plain rename and a removal give no such notice. On layer.remove the label goes with the layer's row, as its exclusion does, so nothing typed is lost while the layer stays; I left that path as it is.
🤖 Addressed by Claude Code
Code reviewReviewed the full PR diff (9 files), the staged repository guidelines, and the surrounding code for the changed paths. I could not build or run the tests in this environment, so the findings below come from reading the code. Bugs
Security
Performance
Quality
These are nits. Per AGENTS.md they can wait for the release quality pass, and I did not post them inline. AGENTS.md
Prior review threads
Change sizeCompared: the whole PR against main (merge base 7cc9bb6 to PR head f0a74f8).
|
…s#1654) A layer renamed onto a name another layer has is listed under that one's label (or name), so its own label is dropped. That is now said: "'Hidrografia', renamed 'Cities', lost its label in 4 legend(s): another layer is called 'Cities', and a legend labels layers by name". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| all_classes: bool, | ||
| hide_other: bool, | ||
| exclude: &[String], | ||
| (exclude, labels): (&[String], &BTreeMap<String, String>), |
There was a problem hiding this comment.
Nit (quality, confidence about 70%). legend_sizes_mm now takes (exclude, labels) as one tuple parameter. That isn't needed: the function would have six parameters, under clippy's default too_many_arguments limit of seven. Two plain parameters read more clearly at the call sites, which pass (&[], &NO_LABELS) and (&exclude, &labels). Left for the release quality pass if the maintainers prefer.
There was a problem hiding this comment.
Left as it is for now. The tuple keeps the two things a legend lists by layer, what it leaves out and what it renames, together at the call sites, and it is under clippy's limit either way. If a maintainer prefers two parameters I'll split it, or it can go in the release quality pass, as you say.
🤖 Addressed by Claude Code
| None => None, | ||
| }, | ||
| // (A map frame's grid takes its labels under "grid".) | ||
| _ if p.get("labels").is_some_and(|v| !v.is_null()) => return Err(ONLY_LEGEND_LABELS.into()), |
There was a problem hiding this comment.
Unclear error for grid labels (quality, confidence about 50%). layout.addElement takes a grid's labels (true or false) at the top level, but layout.update on a map frame refuses any top-level labels with only legends label layers. The grid's other options go under "grid", and the message doesn't say so. A script that sends {"element": <grid>, "labels": false} is told the element isn't a legend, when the fix is {"grid": {"labels": false}}. Suggest naming the place in the message for a map frame, e.g. a map frame's grid labels go under "grid", and keeping the refusal itself, which is correct under "Never silent".
There was a problem hiding this comment.
Fixed in 2b98ad1. On a map frame, layout.update with a top-level labels now refuses with a map frame's grid labels go under "grid": {"grid": {"labels": false}}. Every other element that isn't a legend still gets only legends label layers. legends_label_layers checks the message, and checks that {"grid": {"labels": true}} on the same frame sets them.
🤖 Addressed by Claude Code
| it, and a layer the map hasn't is refused. Each entry replaces that layer's label and leaves | ||
| the others; `null` or a blank label shows the layer's name again, and `{}` every layer's. The | ||
| label goes in the layer's row, or over its classes; a WMS or WMTS layer's goes over its | ||
| picture. It is up to 200 characters (a longer one is refused), and the legend measures it, not the name, so sizing a |
There was a problem hiding this comment.
Nit (docs). This line is about 120 columns, while the rest of the list wraps near 100. Rewrapping it would keep the diff readable.
There was a problem hiding this comment.
Fixed in 2b98ad1: the bullet is rewrapped near 100 columns. It also now says that a renamed layer loses its label when another layer has its new name, which the notice added in 3f959e5 reports.
🤖 Addressed by Claude Code
Code reviewI found no bugs, crash paths, or silent data loss in the changed code. The three prior-review fixes hold in the code. The findings below are quality nits. BugsNone found. I checked the SecurityNone found. Label text reaches SVG output through the existing PerformanceNone worth raising. The case-insensitive fallback in Quality
AGENTS.md
Prior review threadsI left 6 earlier concerns alone. The format-version question is answered by the author and left for maintainers. The other five were fixed and verified in the code: the import notice, two-spelling dedupe, silent truncation, the Change sizeCompared: the whole PR against main (merge base 7cc9bb6 to PR head 3f959e5). Sizes are in KB = 1024 bytes.
|
…abels docs (opengeos#1654) - layout.update with a top-level "labels" on a map frame now says where they go ({"grid": {"labels": false}}) instead of "only legends label layers". - docs: the labels bullet wrapped near 100 columns, and it says a renamed layer loses its label when another layer has its new name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| let hide_other = b(p, "hideOther").unwrap_or(false); | ||
| let exclude = legend_exclude(s, l, frame, p)?.unwrap_or_default(); | ||
| let columns = legend_columns_param(p)?.unwrap_or(1); | ||
| let labels = match legend_labels(s, l, frame, p)? { |
There was a problem hiding this comment.
A legend added with labels isn't checked against its box (Never silent, confidence about 80%).
layout.update runs say_legend_fit when a legend's labels change, but add_element runs it only when columns is given (crates/engine/src/cmd/layout.rs:2598: if p.get("columns").is_some_and(|v| !v.is_null())). So layout.addElement {"kind": "legend", "labels": {"Rivers": "<a label wider than the 60 mm box>"}} adds a single-column legend that draws the label past its box, with no warning. legend_fit_warnings exists to say exactly that: "its last rows or the ends of long labels are left out".
Suggested fix, at line 2598 (outside this diff's hunks, so not a committable suggestion here):
if p.get("columns").is_some_and(|v| !v.is_null()) || p.get("labels").is_some_and(|v| !v.is_null()) {A test for it would add a legend with a label wider than the box and check that a fit notice is given, and that a short label gives none. legends_label_layers checks the fit warning for update nowhere, so the add path has no coverage either way. Whether a legend added with no columns or labels should be checked too is a broader, pre-existing question, not part of this change.
There was a problem hiding this comment.
Fixed in a09323d. It was wider than the add path: layout.update also checked the fit only for a legend in columns (in_columns && refitted) or when columns was given, so changing labels on a one-column legend wasn't checked either. Both commands now run say_legend_fit whenever a legend is given labels, in one column too, so a label wider than its box gives … its last rows or the ends of long labels are left out. The new test a_legends_labels_say_when_they_dont_fit checks four cases: a legend added with a short label (no notice), one added with a label wider than its box (the notice), the first changed to that label (the notice), and the label taken off (no notice). It fails without the fix. I left out a legend added or changed with neither columns nor labels, which is the pre-existing question you mention.
🤖 Addressed by Claude Code
Code reviewBugs
Security
Performance
Quality (left for the release quality pass; not posted)
AGENTS.md
Prior threads
Change sizeCompared: the whole PR against main (merge base 7cc9bb6 to PR head 2b98ad1).
|
…pengeos#1654) layout.addElement checked a legend's fit only when "columns" was given, and layout.update only for a legend in columns. A legend given labels is now checked in one column too, added or changed, so a label wider than its box says that its end is left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| || p.get("exclude").is_some_and(|v| !v.is_null()) | ||
| || p.get("labels").is_some_and(|v| !v.is_null()); | ||
| // Labels given are told in one column too: a label is text chosen for the box. | ||
| let labelled = matches!(&e.kind, ElementKind::Legend { .. }) && p.get("labels").is_some_and(|v| !v.is_null()); |
There was a problem hiding this comment.
Performance (confidence about 45%, plausible). Giving a legend labels now runs say_legend_fit, which calls legend_sizes_mm with all_classes false. That goes through complete_shown_legends (crates/render/src/layout.rs), which does a full visit_complete pass over every chunked source (GeoPackage, GeoParquet, PostGIS and similar) on the calling thread. Before this PR only a columns change reached that path. Now a labels-only layout.update on a legend over a large chunked layer scans the whole source synchronously, with no progress or Cancel.
Suggested direction: either skip the fit check for label-only changes, or say in the doc comment why this command stays on the UI thread and how long a large source may take. The same applies to the add path at line 2600.
There was a problem hiding this comment.
Measured with this branch's rustgis-cli (a09323d, optimized dev build, Windows), median of three runs:
| Project | Base run | One labels-only layout.update |
One columns update (path before this PR) |
|---|---|---|---|
| Sample World Atlas, in-memory layers | 0.53 s | 20 updates in 0.50–0.63 s: within noise, under 5 ms each | — |
A streamed shapefile, 300,000 polygons (41 MB .shp, over STREAM_ABOVE) |
0.48 s | 0.97 s: about +0.47 s each (5 updates: 2.8 s) | 0.97 s: the same +0.47 s |
So a labels change costs what a columns change already costs: one pass over the chunked source. That is about half a second for 300,000 polygons, and nothing measurable for in-memory layers. This is the price of the fit warning, which the "Never silent" thread above asked for. Skipping it for labels would bring back the silent cut. Whether say_legend_fit should move off the frame with progress and Cancel applies to it as a whole, through columns and in-column w/h/title changes too, not to labels alone. I'd leave that to a maintainer as a separate change, so no change here.
🤖 Addressed by Claude Code
| *columns = (*columns).clamp(1, MAX_LEGEND_COLUMNS); | ||
| // The same for the names labelled; a label is cut to its length, and a blank | ||
| // one shows the name, as it would without. | ||
| labels.retain(|_, label| !label.trim().is_empty()); |
There was a problem hiding this comment.
Never silent (minor, confidence about 30%). A blank label read from a file is dropped here before labelled is counted, so it never reaches cut and no notice is given. Blank labels show the layer's name either way, so little is lost, but this is stored data dropped without a count, and the tests cover the cut and dedupe notices but not this one. Either count blank drops into cut, or say in the doc comment that they are not data.
There was a problem hiding this comment.
No change here, and that's on purpose. A blank label is no label: a command treats "" as null (it shows the layer's name again), and the legend draws a blank label as the name (LegendOptions::shown filters blank labels out). So dropping one on read changes nothing drawn, saved or matched. The comment above the retain says so: "a blank one shows the name, as it would without". Counting it in cut would make the import notice report "legend labels cut to their limits" for a file whose legend comes out the same, which would be noise. The PR text lists it under Limits ("Blank labels are dropped").
🤖 Addressed by Claude Code
Code reviewI found no bugs, crash paths, or security issues in the changed lines. The rename and label-following logic, the command validation order, the import sanitize, and the render path all check out. I did not run the test suite; this review is from reading the diff and the surrounding source. Bugs
Security
Performance
Quality
AGENTS.md
Earlier concerns
Change sizeCompared: the whole PR against main (merge base 7cc9bb6 to PR head a09323d).
|
Code reviewI found no new bugs, security issues, or AGENTS.md violations in the changed lines, so I posted no inline comments. This was a static read: I could not build or run the tests here. Checked: the Bugs: none found. Confidence: high for the paths read. Security: none. Labels are display text, bounded by count and length, and never used as paths or commands. Performance: no new finding. A label-only Quality (nits for the release quality pass, confidence about 40%):
AGENTS.md:
Earlier concerns: I left alone 12 prior review threads, all of which have replies from the PR author. Two of them still need a maintainer decision: the format-version question and the UI-thread scan above. Change sizeCompared: the whole PR against main (merge base 7cc9bb6 to PR head a09323d).
|
#1725 added a call to legend_sizes_mm with an empty slice for its last argument while #1739 changed that argument to a pair of the excluded layers and the labels, so main no longer builds. The call measures the legend at another scale, so it passes the same empty pair as the measurement at the asked scale above it.
Closes #1654
A layout legend shows each layer under its name in the project. Plugins name their layers so they can find them again: BrasilData's are
BrasilData: Setores censitários 2022 (MG),BrasilData: Indicadores por município, and every map it lays out shows that prefix in its legend. Renaming the layer breaks what finds it by name, and a copy under another name duplicates its data. With this PR a legend can show a layer under a label of its own, while the layer keeps its name everywhere else.Left: a publication figure (
layout.publication,figure-1.5col) of the sample World Atlas with its layers renamed as a plugin names them; the legend shows the names, and the longest runs into the next column. Right: the same figure afterlayout.update {"element": 4, "labels": {"BrasilData: ne_110m_populated_places": "Cidades (população)", "BrasilData: ne_110m_admin_0_countries": "População estimada (hab.)", …}}. Built from this branch withrustgis-cli run.What changes
labelson the legend element.ElementKind::Legendgetslabels: {"<layer name>": "<label>"}, matched asexcludeis: by the layer's name, any case. The label goes in a plain layer's row, over a classed layer's classes, and over a WMS or WMTS layer's picture.layout.updatetakes"labels": {"<layer>": "<label>"|null}. Each entry replaces that layer's label and leaves the others alone.nullor a blank label shows the layer's name again;{}shows every layer's name again.layout.addElementtakes the same object when it adds a legend.exclude: a layer the frame's map doesn't have (no layer 'Atlantis' in map 'World'), a value that isn't an object of strings, a label over 200 characters (a legend's label is at most 200 characters ('Rivers' has 250)), the same layer named twice in one call ({"Rivers": "a", "rivers": "b"}), andlabelsof any value given to another kind of element (only legends label layers; a grid's ownlabelsis true or false, and anything else there is refused too). An error leaves the element as it was.legend_sizes_mmreads the labels, so sizing a legend to its content and the warnings about its box (say_legend_fit, columns) follow the label, not the name. A legend given labels, added or changed, says when they don't fit its box, in one column too (the legend lists more than fits its box: its last rows or the ends of long labels are left out); before, only a legend in columns was checked..rgisand the.rgislayout, withskip_serializing_if = "BTreeMap::is_empty": files without labels don't change.excludedoes (legends_follow_layer): a renamed layer keeps its label under its new name, and a removed layer's label is dropped, so a later layer of that name isn't labelled unasked.Choices
BTreeMap, not aHashMap. The issue proposedHashMap::is_empty; aBTreeMapwrites its keys in the same order on every save, so saving a project twice gives the same file and diffs stay small."rivers"toRivers(and a WMS or WMTS layer by its name) before it stores the label, asexcludedoes. One layer has one key, so{"RIVERS": …}replaces the label set by{"rivers": …}.'Hidrografia', renamed 'Cities', lost its label in 1 legend(s): another layer is called 'Cities', and a legend labels layers by name), and the layer is shown under the label of the other one (or its name). Labels are matched by name, so two layers with the same name can't have different labels. This is whatexcludedoes in the same case.labelsis also the grid's boolean inlayout.addElement. Only an object is read as a legend's labels, so{"kind": "grid", "labels": false}works as before (tested). The description lists the key once, with both meanings, becauseparam_keys_reads_descriptionsrequires each key to appear only once.element_inputs,update_inputs,element_selectors) now holds the labelled layers along with the excluded ones, so the layers a call labels count among its inputs, the same way the layers it excludes do.Layout::sanitizecuts an opened or imported layout's labels to these limits, drops blank ones, and keeps one of two spellings of a layer's name (the first in order) as a command keeps one; the notice it gives now names them (… layout element(s) with a name, text, polygon points, legend exclusions or legend labels cut to their limits while importing the layout file).Tests
a_legend_shows_a_layers_label(render): a plain layer's row and a classed layer's heading show their labels, matched in any case; a blank label, or one for a layer the map doesn't have, changes nothing; the legend's measured width is narrower with a short label and wider with a long one, at the same height; the drawing ends within the width measured for the label.legends_label_layers(engine): labels set onlayout.addElementandlayout.update, entry by entry;null, blank and{}; the refusals above, with nothing changed;labelsrefused on another kind of element, as an object, a string, a list or a bool, with nothing added; a grid's"labels": "no"refused; a grid's"labels": falsestill works; a 201-character label refused and a 200-character one kept whole; a layout file with a 250-character label imported with it cut to 200 and the notice naming legend labels, a layout file withRiversandriversboth labelled imported with one label for the layer and the same notice; no such notice for a clean file; labels kept throughproject.saveAs/project.openandlayout.save/layout.import; not written when empty; following a rename, dropped with a removed layer, and dropped, with the notice, when a layer is renamed onto another layer's name; a WMS layer labelled by name and followed throughmap.setTilesrename.a_legends_labels_say_when_they_dont_fit(engine): a one-column legend added with a short label says nothing; one added with a label wider than its box, or changed to one, says its end is left out; going back to the name says nothing.cargo fmt --checkandcargo clippy --workspace --all-targets -- -D warningspass locally (Windows). The tests ofrustgis-engine,rustgis-mapandrustgis-renderpass exceptgdal_formats_open_when_gdal_is_installed, which needs GDAL 3.8 and this PC has an older one. The threearchive_render_revisiontests failed once in the full parallel run and passed when run alone.Limits
layout.publicationsizes the legend it lays out, but a label set afterwards doesn't resize it..rgislayoutwith labels shows the layers' names, and drops the labels if it saves the file again; no map data is lost.docs/maintaining.mdhas a format's version go up when a file uses something an older reader would lose, and by its letter this is such a case. The legend'sexclude(Layout: a legend can leave layers out though the map draws them (#517, point 1) #518) andcolumns(feat(layout): a legend in up to three columns, and publication layouts use them for long legends (#1379) #1494) and the text font family (Layouts: a font family for text, sans or serif, from the installed fonts (#517) #675) were added the same way, as optional fields with no version change, and.rgislayoutfiles have no version to raise. Raising the project format for one optional field would make RustGIS 0.6.0 refuse every project this writes, labels or not; a version written only when a legend has labels would avoid that, but would be the first of its kind. If a maintainer wants a version change here, I'll make it.excludeandlayer_by_namedo:Áandáare different keys.🤖 Generated with Claude Code
Summary by CodeRabbit