Skip to content

feat(processing): enter distances in meters on WGS84 layers - #1560

Merged
giswqs merged 15 commits into
mainfrom
feat/issue-1540-distance-units
Jul 31, 2026
Merged

feat(processing): enter distances in meters on WGS84 layers#1560
giswqs merged 15 commits into
mainfrom
feat/issue-1540-distance-units

Conversation

@giswqs

@giswqs giswqs commented Jul 30, 2026

Copy link
Copy Markdown
Member

Fixes #1540

Problem

Vector layers reach a Whitebox tool as GeoJSON, which RFC 7946 fixes to WGS84, so every spacing / search_radius / tolerance value is measured in degrees. There is no CRS left on the layer to say so, and reprojecting a layer does not help because the reprojected result is converted back to WGS84 when it is added to the map (the exact confusion reported in the issue).

What this adds

Distance parameters in the Whitebox toolbox now carry a unit picker: Degrees (the default, unchanged behavior), Meters, Kilometers, Feet, Miles.

  • Entering a metric value converts it to the degrees the tool actually reads, anchored at the input layer's center latitude, and the field shows the degree value it produced along with the latitude it converted at.
  • The parameter is still stored in degrees, so Copy link, Processing History and Edit & re-run keep recording what the tool ran with.
  • A note above the form states that the selected layers use a geographic CRS and points at reprojection for exact results.

The conversion uses the geometric mean of the meridional and parallel degree lengths at that latitude. A single scalar cannot be right in both directions on a geographic CRS, so the mean splits the error instead of favoring one axis, and the field says the result is approximate.

Where it does not appear

The picker is only offered when the tool's coordinates are known to be WGS84, that is, every dataset input is a vector layer with in-memory GeoJSON. A tool with a raster or LiDAR input keeps that dataset's own CRS (GeoLibre never reprojects those), and a vector input still set to a file path is read from disk in whatever CRS it carries. In both cases the field stays a plain number box. Integer parameters are excluded too, since a metric distance almost always converts to a fractional number of degrees.

The name rule (*_dist, *_radius, spacing, tolerance, *_length, resolution, cell_size, and whole-name width/height) lives in whitebox-distance-params.ts alongside the conversion, so there is no per-tool table to keep in sync with the catalog. Counts, angles, penalties, speeds and times are deliberately left out.

Verification

Driven in the real app with Playwright against r15_osm_roads.geojson (927 OSM roads near Eugene, Oregon, WGS84), using Points Along Lines, the tool named in the issue:

  • Input on "Path": plain number field, no picker, no note. Selecting the layer: picker and geographic-CRS note appear.
  • Entering 200 with Meters selected sends 0.0021193307 to the tool, converted at 44.0 deg N, and the run succeeds (1171 points added to the map).
  • Measuring the ground distance between consecutive full-step stations along those roads puts the realized spacing between 170 m (east-west roads) and 236 m (north-south), centered on the 200 m requested, which is exactly the bound the geometric mean predicts and the field's "approximate" wording describes.
  • Switching the unit back to Degrees restores 200 when switched to Meters again, so the round trip preserves the distance.
  • Checked in both light and dark themes.

tests/whitebox-distance-params.test.ts covers the name rule (real parameter names from the catalog on both sides), the latitude scale including the pole clamp, unit round-trips, and the decimal formatting that keeps small degree values out of exponent notation.

Full frontend suite (4421 tests) and npm run build pass; pre-commit is clean.

Summary by CodeRabbit

  • New Features
    • Added a unit picker (degrees, meters, kilometers, feet, miles) for eligible distance parameters, with automatic conversion using the available WGS84 GeoJSON layers’ geographic context.
    • Displays a one-time geographic note when degree-based distance interpretation applies.
  • Bug Fixes
    • Improved distance value formatting for cleaner tool inputs (no trailing zeros/exponent) and safe handling of non-finite values.
  • Documentation
    • Added new localized UI text for distance unit selection and conversion/explanation messaging.
  • Tests
    • Added tests covering distance-parameter detection, geographic scaling, unit conversion/round-trips, input parsing, formatting, and layer-id extraction logic.

Vector layers reach a Whitebox tool as GeoJSON, which RFC 7946 fixes to
WGS84, so every spacing, radius and tolerance value is measured in
degrees with no CRS left on the layer to say so.

Distance parameters now carry a unit picker (degrees, meters,
kilometers, feet, miles). Entering a metric value converts it to the
degrees the tool reads, anchored at the input layer's center latitude,
and the field shows the degree value it produced. A note above the form
states that the selected layers use a geographic CRS and points at
reprojection for exact results.

The picker only appears when the tool's coordinates are known to be
WGS84: every dataset input is a vector layer with in-memory GeoJSON. A
raster or LiDAR input keeps its own CRS, and a file path is read in
whatever CRS it carries, so those tools are left alone.

Fixes #1540
Copilot AI review requested due to automatic review settings July 30, 2026 02:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Processing distance fields now detect WGS84 layer context, provide unit selection for eligible parameters, convert selected units to degrees for Whitebox tools, and display localized geographic-distance guidance. Conversion and formatting behavior is covered by new tests.

Changes

WGS84 distance unit inputs

Layer / File(s) Summary
Distance detection and conversion utilities
apps/geolibre-desktop/src/lib/whitebox-distance-params.ts, tests/whitebox-distance-params.test.ts
Adds distance-parameter recognition, WGS84 latitude-based conversions, input parsing, formatting helpers, layer-token inference, and utility tests.
WGS84 layer latitude resolution
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
Resolves eligible WGS84 vector inputs, calculates a bounding-box-based reference latitude, and passes distance context to processing parameters.
Processing distance input UI
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx, apps/geolibre-desktop/src/i18n/locales/en.json
Adds localized guidance and a unit-aware distance input that converts draft values to degrees for Whitebox tools.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProcessingDialog
  participant DistanceInput
  participant WhiteboxTool
  ProcessingDialog->>DistanceInput: provide WGS84 reference latitude
  DistanceInput->>DistanceInput: convert selected unit to degrees
  DistanceInput->>WhiteboxTool: submit degree-based distance value
Loading

Poem

A rabbit hops through degrees so wide,
Picks meters and miles with fluffy pride.
At latitude’s line, conversions gleam,
Whitebox receives the value downstream.
“Hop, hop!” says Bunny, “the units align!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the core feature: entering distances in meters for WGS84-backed processing layers.
Linked Issues check ✅ Passed The PR adds selectable distance units, WGS84 layer detection, and a reprojection warning/notice as requested in #1540.
Out of Scope Changes check ✅ Passed The changes stay focused on distance-unit handling, CRS-aware guidance, localization, and tests for the new feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-1540-distance-units

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://649b4d55.geolibre-preview.pages.dev
Demo app https://649b4d55.geolibre-preview.pages.dev/demo/
Commit 9318547

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site Deploy failed. See the job log.
Demo app Unavailable
Commit 9318547

Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • DistanceInput (ProcessingDialog.tsx:2421-2449) doesn't resync its stored degrees or displayed draft when the latitude prop changes while a non-degrees unit is selected — e.g. switching a tool's vector input to a layer at a different latitude leaves the previously-entered metric value converted at the old latitude, while the "converted at X" note updates to show the new latitude, so the note misrepresents what was actually sent to the tool. Medium confidence.

Performance

  • latitudeByLayer (ProcessingDialog.tsx:796-805) runs an unbounded turf bbox() scan over every feature of every GeoJSON layer whenever the layers array reference changes (any layer mutation while the dialog is open), unlike the neighboring fieldsByLayer memo which deliberately caps its scan to the first 1000 features per layer to stay cheap on the React commit path. Could be scoped to just the layers the selected tool's vector inputs actually reference. Low-medium confidence.

Security

  • None found.

Quality

  • The rest of the change (name-rule matching in whitebox-distance-params.ts, the geometric-mean conversion, formatDistanceValue's significant-digit formatting) is well-documented and covered by targeted unit tests that match the implementation's actual edge cases (pole clamp, whole-name width/height, exponent-notation avoidance).

CLAUDE.md

  • New UI strings go through t()/en.json as required, and the new layout classes (grid, gap-2, etc.) avoid physical-direction Tailwind utilities, consistent with the RTL guidance. No violations noted.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 264-295: Update wgs84ReferenceLatitude so any vector_in parameter
whose value is not a layer token returns null, regardless of whether
param.required is true. Remove the required-only condition and preserve the
existing latitude lookup and averaging behavior for valid layer-token inputs.
- Around line 2421-2492: Update the DistanceInput component to resynchronize its
local unit and draft state when the externally supplied value or latitude
changes, including tool switches, history reruns, and layer changes. Add the
necessary effect or equivalent reconciliation around value, latitude, and the
existing hasDegrees conversion logic so the displayed non-degree draft matches
the current stored degrees value, while preserving user edits and the existing
changeUnit/changeDraft behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee224602-b958-41b6-87e5-cb2e1d8b9027

📥 Commits

Reviewing files that changed from the base of the PR and between 2856ef8 and 11f24bc.

📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/whitebox-distance-params.ts
  • tests/whitebox-distance-params.test.ts

Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
- Re-convert a metric distance when the input layer changes: the field is
  keyed by parameter name, so switching a tool's vector input from a layer
  at 44N to one at 9N kept the typed value on screen while the stored
  degrees still came from the old latitude, and Run sent the stale value.
  An effect guarded by the previous latitude now pushes the recomputed
  degrees up.
- Measure only the layers the selected tool actually reads instead of
  running an unbounded turf bbox over every GeoJSON layer in the project
  on the React commit path. The vector-input helper now returns layer ids,
  joined into a memo key so the extent scan survives keystrokes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx (1)

248-286: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Optional vector_in left on a path still bypasses the WGS84 safety check.

The docstring (lines 251-256) states any vector input "still set to a path" disqualifies the tool from the unit picker, but the loop body (unchanged from the prior commit) only enforces this via if (param.required) return null; at line 280 — an optional vector_in explicitly left on a file path is simply continued past. Its geometry is still read from disk in its own CRS by runSelectedTool (a raw path string, not layer_inputs), so the picker can end up claiming a WGS84-known unit context based only on the other inputs while this one silently contributes projected coordinates.

🐛 Proposed fix
   for (const param of vectorInputs) {
     const value = values[param.name];
-    if (typeof value !== "string" || !value.startsWith(LAYER_TOKEN_PREFIX)) {
-      // A required input still on "Path" (or empty) means the run may read a
-      // projected file instead; don't claim to know the units.
-      if (param.required) return null;
+    if (typeof value !== "string" || value.trim() === "") {
+      // Nothing selected yet: a required one can't run regardless, and an
+      // optional one that's genuinely empty contributes no geometry.
+      if (param.required) return null;
       continue;
     }
+    if (!value.startsWith(LAYER_TOKEN_PREFIX)) {
+      // Any vector input — required or not — left on a path is read from
+      // disk in its own CRS, so the picker can't claim to know the units.
+      return null;
+    }
     ids.push(value.slice(LAYER_TOKEN_PREFIX.length));
   }

This mirrors a previously-flagged concern on wgs84ReferenceLatitude's equivalent logic, which is still unresolved after the rename to wgs84VectorLayerIds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx` around
lines 248 - 286, Update wgs84VectorLayerIds so any vector_in value that is
missing or not a LAYER_TOKEN_PREFIX path, including optional inputs, immediately
returns null. Remove the required-only exception while preserving collection of
layer IDs for valid layer-token values.
♻️ Duplicate comments (1)
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx (1)

2418-2462: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Latitude-staleness fixed; value-staleness on tool switch/rerun still open.

The new lastLatitude ref + effect correctly fixes the previously-flagged bug where switching a WGS84 input layer left the displayed draft/unit converted at a stale latitude — it re-runs unitToDegrees whenever latitude changes while unit !== "degrees", and the ref-guard prevents any loop from onChange's new identity each render.

However, unit/draft still never resync when the value prop changes for reasons other than latitude:

  • Switching tools: ParameterField key={param.name} (line 1972) has no tool id, so a different tool reusing a same-named distance param (e.g. radius, distance, tolerance) keeps this DistanceInput instance mounted while value resets to the new tool's unrelated default (line 1137-1144's setValues(createDefaultValues(selectedTool))).
  • Re-running from Processing History: setValues({...defaults, ...rerun.parameters}) (line 1167-1171) changes value without touching local state.

In both cases the stored degrees is correct, but the UI can keep showing a stale unit + number pairing that no longer matches it.

🐛 Proposed fix: reconcile local state whenever an externally-pushed value is detected
 function DistanceInput({ id, latitude, onChange, value }: DistanceInputProps) {
   const { t } = useTranslation();
   const [unit, setUnit] = useState<DistanceUnit>("degrees");
   const [draft, setDraft] = useState("");
+  // Tracks the last value *this component* pushed via onChange, so an
+  // externally-changed `value` (tool switch or rerun prefill) can be told
+  // apart from our own edits.
+  const lastPushedRef = useRef<string | null>(null);
+
+  useEffect(() => {
+    if (value === lastPushedRef.current) return;
+    setUnit("degrees");
+    setDraft("");
+  }, [value]);

This is the same gap flagged on an earlier commit of this component.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx` around
lines 2418 - 2462, Update DistanceInput to resynchronize its local unit and
draft whenever the externally supplied value changes, not only when latitude
changes. Add a value-change effect that detects external updates, resets the
display to the degrees representation (clearing draft or converting the new
value consistently), and keeps the existing latitude reconversion behavior for
metric units without creating an update loop.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 248-286: Update wgs84VectorLayerIds so any vector_in value that is
missing or not a LAYER_TOKEN_PREFIX path, including optional inputs, immediately
returns null. Remove the required-only exception while preserving collection of
layer IDs for valid layer-token values.

---

Duplicate comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 2418-2462: Update DistanceInput to resynchronize its local unit
and draft whenever the externally supplied value changes, not only when latitude
changes. Add a value-change effect that detects external updates, resets the
display to the degrees representation (clearing draft or converting the new
value consistently), and keeps the existing latitude reconversion behavior for
metric units without creating an update loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 08e01eff-20fa-45ce-a3d2-b5c98903bbc9

📥 Commits

Reviewing files that changed from the base of the PR and between 11f24bc and 4bd57eb.

📒 Files selected for processing (1)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx

Comment thread apps/geolibre-desktop/src/lib/whitebox-distance-params.ts
Comment thread tests/whitebox-distance-params.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • ProcessingDialog.tsx: ParameterField is keyed only by param.name (not tool id), so DistanceInput's local unit/draft state can survive a switch to a different Whitebox tool that happens to reuse the same distance-parameter name (and vector-input name). The latitude-recompute effect can then push a conversion of stale leftover draft text into the new tool's parameter, silently overwriting the freshly-computed default from createDefaultValues. The codebase already guards an analogous same-named-parameter hazard elsewhere (browsedInputsRef.current.clear() on tool switch) but not for this new component's local state. Medium confidence.

Security

  • None found. No injected input reaches HTML/URLs, all values flow through controlled React inputs, and the numeric conversions are pure math with no external I/O.

Performance

  • No issues found. The layer-bounds/latitude computation is properly memoized (distanceLayerKey/distanceLatitude) so it only recomputes when the actually-wired layer ids change, not on every keystroke.

Quality

  • The generic length/resolution name segments in whitebox-distance-params.ts could false-positive on non-ground-distance parameters (e.g. a photogrammetry focal_length), though this is largely mitigated in practice by the raster/lidar/file-input exclusion in wgs84VectorLayerIds. Low confidence.
  • The new stateful DistanceInput component (unit/draft state, the latitude-change re-conversion effect) has no test coverage; only the pure helper functions in whitebox-distance-params.ts are tested, missing exactly the logic most likely to regress. Low-medium confidence.

CLAUDE.md

  • No violations found: new user-facing strings go through t()/en.json, the new grid layout uses direction-agnostic Tailwind classes (no ml-/left- physical utilities), and the new helper module follows the existing lib/whitebox-*-params.ts convention. This PR doesn't touch geolibre-wasm/menu-catalog-affecting dependencies, so the catalog-regeneration rule doesn't apply.

The core conversion math (metersPerDegreeAt, unitToDegrees/degreesToUnit, formatDistanceValue) was checked against the standard WGS84 degree-length series and against the accompanying unit tests, and is correct.

- Treat any vector input left on a file path as unknown units, not just a
  required one. An optional input pointing at a projected file is still
  read from disk in its own CRS, so the picker must not claim to know the
  tool's units. An empty optional input still contributes nothing.
- Key each parameter field by tool id as well as parameter name. Dozens of
  tools share names like tolerance or radius, so without the tool id React
  can reuse a field instance across a tool switch and carry a distance
  field's unit and typed draft onto another tool's default.
- Reset a distance field to plain degrees when its stored value is
  rewritten from outside, such as a re-run pre-filled from Processing
  History, so the displayed unit and number cannot describe a distance
  that is no longer stored.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx (1)

2436-2492: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Latitude-reconversion effect can clobber a simultaneous external value rewrite.

The two effects run in declaration order within the same commit, both reading the pre-update unit/draft captured by that render. When a Processing History rerun for the same tool also swaps the wired vector-input layer, value (this param, from rerun.parameters) and latitude (via distanceLatitude, derived from the new layer) change together in one setValues commit. The reset effect (2470-2475) correctly detects the external rewrite and clears unit/draft, but the reconversion effect (2483-2492) still fires because latitude is in its own deps and changed — using the stale draft/unit from that same render — and pushes a recomputed value that overwrites the just-restored, correct externally-set value. The tool then silently runs with a wrong distance parameter.

🐛 Proposed fix: give the external-rewrite case priority in a single effect
-  useEffect(() => {
-    if (value === lastPushed.current) return;
-    lastPushed.current = value;
-    setUnit("degrees");
-    setDraft("");
-  }, [value]);
-
-  const lastLatitude = useRef(latitude);
-  useEffect(() => {
-    if (lastLatitude.current === latitude) return;
-    lastLatitude.current = latitude;
-    if (unit === "degrees") return;
-    const parsed = Number.parseFloat(draft);
-    if (!Number.isFinite(parsed)) return;
-    const next = formatDistanceValue(unitToDegrees(parsed, unit, latitude), 8);
-    lastPushed.current = next;
-    onChange(next);
-  }, [draft, latitude, onChange, unit]);
+  const lastLatitude = useRef(latitude);
+  useEffect(() => {
+    lastLatitude.current = latitude;
+    if (value !== lastPushed.current) {
+      // External rewrite (e.g. a Processing History re-run, possibly paired
+      // with a vector-input swap that also shifted `latitude`). Trust it over
+      // any stale draft/unit rather than re-converting from leftover text.
+      lastPushed.current = value;
+      setUnit("degrees");
+      setDraft("");
+      return;
+    }
+    if (unit === "degrees") return;
+    const parsed = Number.parseFloat(draft);
+    if (!Number.isFinite(parsed)) return;
+    const next = formatDistanceValue(unitToDegrees(parsed, unit, latitude), 8);
+    lastPushed.current = next;
+    onChange(next);
+  }, [value, draft, latitude, onChange, unit]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx` around
lines 2436 - 2492, Consolidate the effects watching value and latitude so the
external value-rewrite path has priority over latitude reconversion. In the
effect around lastPushed, detect value !== lastPushed.current, update
lastPushed, reset unit to degrees and draft to empty, then return before any
reconversion or onChange call; only reconvert the existing metric draft when the
value was not externally rewritten.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 2436-2492: Consolidate the effects watching value and latitude so
the external value-rewrite path has priority over latitude reconversion. In the
effect around lastPushed, detect value !== lastPushed.current, update
lastPushed, reset unit to degrees and draft to empty, then return before any
reconversion or onChange call; only reconvert the existing metric draft when the
value was not externally rewritten.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: db62d34d-546d-4f9d-8b24-23a648bc5fb1

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd57eb and 360a886.

📒 Files selected for processing (1)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • changeDraft in DistanceInput (ProcessingDialog.tsx:2455-2465) uses Number.parseFloat(text), which parses only a numeric prefix instead of rejecting malformed input. A value like "12,000" (or a comma-decimal locale input like "3,14") silently parses to 12/3 and gets converted+stored as that smaller number while the draft still displays the original text — an invisible data-corruption risk, worse than the plain degrees field elsewhere which just passes bad text through unparsed (failing loudly downstream instead of silently reinterpreting it). Suggested using Number(text) instead, which rejects trailing garbage. Confidence: medium-high.

Performance

  • distanceLatitude's useMemo depends on the whole layers array reference (ProcessingDialog.tsx:799-812), so it recomputes getLayerBounds (a full-collection turf.bbox pass) whenever any layer in the project changes, not only when the tool's actual input layers change. Likely low-impact in practice but worth noting for large GeoJSON layers. Confidence: low.

Quality

  • changeUnit (ProcessingDialog.tsx:2445-2453) reformats the stored degrees value in the newly-selected unit using the default 6 significant digits, while changeDraft stores degrees with 8. Toggling units doesn't corrupt the stored value by itself, but editing the draft immediately after a unit switch derives the new degrees from this lower-precision display, quietly losing precision from the original entry. Confidence: low.

Security

  • None found. This is a self-contained frontend form feature with no new server calls, shell/command construction, or unsanitized-input-to-injection paths.

CLAUDE.md adherence

  • New user-facing strings go through t() and are added to en.json as required; no physical ml-/left- classes were introduced (grid layout is direction-agnostic). No violations found.

Overall the core conversion math (metersPerDegreeAt, unitToDegrees/degreesToUnit, the WGS84-detection logic in wgs84VectorLayerIds), the name-matching heuristic (isDistanceParameterName), and the effect-based state synchronization in DistanceInput all held up well under scrutiny, including boundary cases (poles, empty/degenerate layer bounds, tool switching, required vs. optional vector inputs, and int-vs-double exclusion against real catalog parameter names).

- Parse a typed distance with Number instead of parseFloat. parseFloat
  stops at the first character it cannot read, so "12,000" became 12 and
  was converted as a distance a thousand times too small while the field
  still showed the original text. Malformed text is now stored as typed,
  which fails at the tool, and the field says so instead of claiming a
  conversion that did not happen.
- Extract the per-input WGS84 rule and the draft parser into
  whitebox-distance-params.ts and unit-test them. That branching is where
  the optional-path bug lived, and it is now covered directly.
- Note the residual false-positive risk of the generic length and
  resolution name segments, which the vector-only tool gate handles today
  but a future tool could defeat.
Comment thread apps/geolibre-desktop/src/lib/whitebox-distance-params.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/whitebox-distance-params.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found with high confidence. The unit-conversion math (metersPerDegreeAt, unitToDegrees/degreesToUnit), the draft/push state machine in DistanceInput, and the "external value changed" detection in ProcessingDialog.tsx all check out against their tests and against how updateValue stores values (verbatim, so the own-edit vs. external-change distinction doesn't misfire). Confidence: high (verified against surrounding source, not diff alone).

Security

  • No issues found. No injection, unsafe input handling, or secrets in the changed files.

Performance

  • No issues found. The layer-bounds scan (getLayerBounds) driving distanceLatitude is correctly memoized so it only re-runs when the selected vector layer(s) change, not on every keystroke, despite values (the whole form-state object) being a dependency of the intermediate memo. Confidence: high.

Quality

  • apps/geolibre-desktop/src/lib/whitebox-distance-params.ts:128-137: the JSDoc block intended for formatDistanceValue is immediately followed by a second, unrelated JSDoc block that actually documents parseDistanceInput — so parseDistanceInput ends up double-documented while formatDistanceValue (defined later at line 189) has no doc-comment at all. Looks like a copy/paste or reorder slip. Confidence: high.
  • apps/geolibre-desktop/src/lib/whitebox-distance-params.ts:29-50: the name-based isDistanceParameterName heuristic (segments like tolerance, radius, length, resolution) can match a double parameter that isn't actually a ground distance — e.g. corridor_tolerance in the bundled catalog snapshot is a 0–1 fraction, not a distance. It's currently harmless only because that particular tool also has a file_in input, which disqualifies it for an unrelated reason. The code's doc comment already flags this exact class of risk for *_length but not for the other segments; a future/renamed vector-only tool could hit it. Confidence: medium.
  • Minor: the new UI logic in ProcessingDialog.tsx (DistanceInput, wgs84ToolLayerIds, the latitude-change re-conversion effect) has no direct test coverage — the added test file only covers the pure helpers in whitebox-distance-params.ts. Not blocking, since the pure conversion/formatting logic (the riskiest part) is well tested. Confidence: medium.

CLAUDE.md

  • New user-facing strings correctly go through t() and are added to en.json as the source-of-truth catalog, consistent with the i18n conventions. No RTL/logical-CSS concerns since the new markup uses grid/gap utilities rather than physical ml-/left- classes. No other CLAUDE.md guidance (Tauri CSP, whitebox menu catalog regen, etc.) applies to this change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx (1)

2463-2488: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Re-run + latitude change in the same commit can silently clobber the restored distance value.

When "Edit & re-run" targets the same tool (no ParameterField remount) and the re-run also changes a wired vector-input layer, setValues({...defaults, ...rerun.parameters}) updates both this parameter's value and — via the distanceLayerKey/distanceLatitude memos — the latitude prop in one render. Both effects then fire in declaration order: the latitude effect reads the stale, pre-reset unit/draft (captured in the same render, before the value-effect's setUnit/setDraft calls have applied) and calls onChange with a bogus reconversion of leftover draft text — overwriting the just-restored rerun value in the parent's values state.

This is a different combination than the tool-switch/external-rewrite/pure-latitude-change races already fixed in prior rounds.

🐛 Proposed fix
   const lastLatitude = useRef(latitude);
   useEffect(() => {
     if (lastLatitude.current === latitude) return;
     lastLatitude.current = latitude;
     if (unit === "degrees") return;
+    // If `value` was also rewritten externally in this same render (e.g. a
+    // same-tool re-run that restores this parameter and shifts the inferred
+    // latitude together), let the value-reset effect above win instead of
+    // reconverting stale draft text over the freshly-restored value.
+    if (value !== lastPushed.current) return;
     const parsed = parseDistanceInput(draft);
     if (parsed === null) return;
     const next = formatDistanceValue(unitToDegrees(parsed, unit, latitude), 8);
     lastPushed.current = next;
     onChange(next);
-  }, [draft, latitude, onChange, unit]);
+  }, [draft, latitude, onChange, unit, value]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx` around
lines 2463 - 2488, Coordinate the value-reset and latitude-reconversion effects
so a render that changes both value and latitude skips reconversion using the
stale unit/draft. In the latitude effect near lastLatitude, detect that value
changed in the same render and only update latitude tracking; retain
reconversion for genuine latitude-only changes and existing guards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/i18n/locales/en.json`:
- Line 3450: Update the en.json `convertedEmpty` translation to describe the
degree approximation or interpretation without implying that a conversion has
already occurred, matching its use by `DistanceInput` when `draft.trim() ===
""`.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx`:
- Around line 2463-2488: Coordinate the value-reset and latitude-reconversion
effects so a render that changes both value and latitude skips reconversion
using the stale unit/draft. In the latitude effect near lastLatitude, detect
that value changed in the same render and only update latitude tracking; retain
reconversion for genuine latitude-only changes and existing guards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0ca6f5db-8dd7-4e9f-bd0d-2e9f6ca60fd5

📥 Commits

Reviewing files that changed from the base of the PR and between 360a886 and db2fe35.

📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/whitebox-distance-params.ts
  • tests/whitebox-distance-params.test.ts

Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
- Exclude corridor_tolerance from the distance-name rule. It is a fraction
  above optimal cost in 0-1, not a width, so the tolerance segment was
  claiming it. Only the tool-level gate keeps it out of reach today, which
  is a coincidence rather than a guarantee, so it now has an explicit
  exclusion list. A catalog scan for a matching double whose description
  reads as a fraction, ratio, angle or weight turned up no other case.
- Move the formatDistanceValue docstring back onto formatDistanceValue; it
  was left stranded above parseDistanceInput when the helpers were added.
- Reword the empty-field note: it is shown before anything is typed, so it
  now reads "Values will be converted" instead of claiming a conversion
  that has not happened.
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • No functional bugs found. The DistanceInput unit-conversion/state machine (draft vs. stored degrees, unit switching, latitude-change re-conversion, external-rewrite detection) is carefully sequenced and I traced through the round-trip, keystroke-by-keystroke conversion, and effect ordering without finding a stale-value, infinite-loop, or lost-precision path. Confidence: medium-high (based on static trace; not executed).

Security

  • None found. No injection, unsafe DOM/HTML sinks, or secret handling in the diff; i18n interpolation goes through react-i18next's default escaping.

Performance

  • None found. The expensive getLayerBounds/bbox computation is properly memoized off a stable, id-only distanceLayerKey string, so it does not re-run on every keystroke, and the feature is entirely opt-in per field.

Quality

  • parseDistanceInput (whitebox-distance-params.ts:161) uses Number(text), which also accepts hex/octal/binary literals (e.g. "0x64"100), slightly at odds with the function's stated intent of only accepting a plain decimal a user would recognize as a distance. Low impact, low confidence this ever triggers in practice.
  • distanceLatitude (ProcessingDialog.tsx:770-783) averages the center latitudes of all wired vector inputs when a tool has more than one; for two inputs at very different latitudes this can anchor the conversion at a point representative of neither dataset, and the UI copy only ever describes a single input layer's latitude. Minor/low severity given the conversion is already presented as approximate.

CLAUDE.md

  • No violations found. New UI strings go through t() with catalog entries in en.json; new JSX uses logical Tailwind utilities (no physical ml-/left- forms); the CLAUDE.md addition documenting the DISTANCE_SEGMENTS/NON_DISTANCE_NAMES maintenance mirror is accurate against the code and consistent with the file's existing mirror-documentation pattern.

- Require a plain decimal before converting a typed distance. Number also
  reads the 0x/0b/0o literals and Infinity, so a pasted "0x64" would have
  converted as 100 metres instead of passing through untouched like other
  text the field cannot read. Partly typed decimals ("5.", ".5") still
  convert, so the field keeps up as the user types.
- Anchor a multi-input conversion at the middle of the layers' combined
  extent rather than the mean of their centres. Averaging each layer's own
  centre weights a city-sized input the same as a country-sized one and can
  land on a latitude neither input is near: with a local road network and a
  worldwide point set it gave 26.7N, where the combined extent gives 9.3N.
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/lib/whitebox-distance-params.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. The state machine in DistanceInput (draft/unit tracking, latitude-change reconversion, external-rewrite detection) was traced through carefully and behaves correctly in all the scenarios checked, including unit round-trips, mid-typing states, negative numbers, and malformed input.

Security

  • None found. All new inputs are parsed with a strict decimal regex (parseDistanceInput) before being used numerically; no injection or unsafe-eval surface.

Performance

  • None found. distanceLatitude/distanceLayerKey are properly memoized and scoped to only the one or two layers actually wired to the selected tool, not the full layer list.

Quality

  • apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx:2630 — in degrees mode, NumberStepperInput's onChange bypasses the local push() helper, so lastPushed.current goes stale and the "external rewrite" reset effect fires as a no-op on every keystroke. Currently harmless, but a real inconsistency in the tracking invariant. (Low-medium confidence; my first suggested one-liner fix didn't typecheck under this repo's strict settings — flagged and corrected in a follow-up comment.)
  • apps/geolibre-desktop/src/lib/whitebox-distance-params.ts:181wgs84VectorLayerIds only recognizes "layer:"-prefixed values as WGS84. A vector input filled by directly browsing a local .geojson/.json file (not added as a map layer) is also fed to the tool as in-memory WGS84 GeoJSON, but never gets the unit picker since it isn't layer:-prefixed. Conservative rather than wrong, but a missed case worth a follow-up. (Low confidence.)
  • Both findings are minor; the core name-matching heuristic, latitude-based conversion math (verified against standard WGS84 degree-length formulas), and number formatting (formatDistanceValue) all check out correctly and match their test coverage.

CLAUDE.md

  • The new DISTANCE_SEGMENTS/NON_DISTANCE_NAMES mirror rule was correctly added to CLAUDE.md's Conventions section, consistent with the repo's existing pattern for documenting catalog-drift risks tied to geolibre-wasm bumps. New UI strings correctly go through en.json/t(), and no physical-direction Tailwind utilities were introduced.

The metric box records what it pushed so the field can tell an outside
rewrite from its own edit, but the degrees stepper was wired straight to
the parent, leaving that record stale and making every keystroke look like
an outside rewrite to the reconcile effect. It is a no-op today because the
reset it triggers matches the state already there, but the invariant should
hold before the effect grows.
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs: None found. Traced the WGS84-detection gating (wgs84ToolLayerIds, wgs84VectorLayerIds), the latitude/extent memoization, the degrees↔unit conversion math (metersPerDegreeAt, unitToDegrees/degreesToUnit — verified the geometric-mean formula against the test's expected bounds by hand), the DistanceInput state machine (external-rewrite detection via lastPushed, latitude-change reconversion via lastLatitude), and the parseDistanceInput/formatDistanceValue parsing/formatting edge cases (empty input, malformed text, exponent-notation avoidance, pole clamping). All behave as documented and match their tests. Confidence: high.

Security: None found. No injected HTML, no eval, purely numeric client-side conversion. Confidence: high.

Performance: None found. The extent scan (getLayerBounds) is confined to the one or two layers actually wired to the selected tool, not the whole project, as intended. Confidence: high.

Quality:

  • Minor precision inconsistency in changeUnit (ProcessingDialog.tsx:2563): it formats the redisplayed draft with the default 6 significant digits, while changeDraft uses 8 for the same kind of value. Since the actually-submitted degrees value isn't touched by changeUnit, this only affects the round-tripped display (e.g. typing a 7-8 sig-fig value, switching to Degrees, then back), but it can look like the field silently lost precision. Left an inline suggestion to pass 8 there too. Confidence: low (cosmetic only).

CLAUDE.md: Followed — new user-facing strings go through t()/en.json, no physical ml-/left- classes were introduced (the new grid layout is direction-neutral), and the PR proactively adds a new maintenance bullet documenting the DISTANCE_SEGMENTS/NON_DISTANCE_NAMES name-rule's coupling to the geolibre-wasm catalog, consistent with the file's existing "mirror" conventions. Confidence: high.

Redisplaying the stored degrees in a newly picked unit rounded to 6
significant digits while the stored value carries 8, so a value typed as
200.12345 came back as 200.123 after a trip through Degrees. I had assumed
matching the digits would make ordinary values read as 199.99999; measuring
it shows otherwise, since 200, 2000 and 1.5 all still round-trip exactly.
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • Possible stale-state race in DistanceInput's two reconciliation useEffects (ProcessingDialog.tsx:2587-2609): when a single update changes both the stored value and the input layer's latitude at once — as happens with setValues({...defaults, ...rerun.parameters}) on "Edit & re-run" (ProcessingDialog.tsx:1147) while the same tool stays selected — the "latitude changed" effect can still see the pre-commit unit/draft and overwrite the just-restored degrees value with a reconversion of stale metric draft text at the new latitude. Confidence: medium — reasoned from the effect dependency/closure semantics, not runtime-verified.

Security

  • None found.

Performance

  • Minor: the "latitude changed" effect re-runs its body on every parent re-render (since the inline onChange closure is a new dependency each time), though the lastLatitude guard makes it a no-op in practice. Confidence: low, cosmetic.
  • The distanceLatitude/getLayerBounds extent scan re-runs whenever the layers array reference changes (any layer edit, not just the relevant one), matching an existing pattern elsewhere in this file rather than a new regression. Confidence: low.

Quality

  • Switching a distance field to a non-degrees unit drops the +/- stepper buttons in favor of a plain text input, a small UX inconsistency with other double fields. Confidence: low.
  • The equator (latitude === 0) is labeled "0.0°N", which reads oddly. Confidence: low, cosmetic.
  • DistanceInput's effect-driven reconciliation logic (external value rewrite vs. latitude change) has no test coverage — only the pure helpers in whitebox-distance-params.ts are unit-tested — which is exactly where the race above lives undetected by CI.

CLAUDE.md

  • The new Conventions bullet documenting DISTANCE_SEGMENTS/NON_DISTANCE_NAMES correctly names the actual exported constants in whitebox-distance-params.ts and follows the file's existing mirror-documentation pattern; no drift found.

Overall the core conversion math (metersPerDegreeAt, unitToDegrees/degreesToUnit, formatDistanceValue, parseDistanceInput, wgs84VectorLayerIds) is careful and well-tested, including boundary cases (pole clamping, exponent-notation avoidance, malformed input rejection). The main concern is the effect-ordering issue in the stateful DistanceInput component noted above.

An outside rewrite and a latitude change were handled by separate effects,
and a Processing History re-run can do both in one setValues: it replaces
the stored value and can rebind the input layer. The latitude effect then
still saw the pre-reset unit and draft from that render's closure and
pushed a reconversion of the stale draft over the value the reset was
restoring, so History replayed something other than what ran. One effect
now handles both with the precedence stated: an outside rewrite wins.

Also label a conversion anchored at 0.0 as "the equator" rather than
"0.0°N", which a worldwide layer's combined extent reaches often enough
for the hemisphere suffix to read as a mistake.
Comment on lines +2591 to +2615
const lastLatitude = useRef(latitude);
useEffect(() => {
const latitudeChanged = lastLatitude.current !== latitude;
lastLatitude.current = latitude;
if (value !== lastPushed.current) {
// Someone else rewrote the parameter, so the typed draft and its unit
// describe a distance that is no longer stored: show the degrees that are,
// and let that win over any re-conversion.
lastPushed.current = value;
setUnit("degrees");
setDraft("");
return;
}
// The input layer moved under a metric entry: switching a tool's vector
// input from a layer at 44°N to one at 10°N leaves the typed distance on
// screen while the stored degrees still come from the old latitude, so the
// note would show the new latitude beside a value converted at the old one
// and Run would send the stale degrees.
if (!latitudeChanged || unit === "degrees") return;
const parsed = parseDistanceInput(draft);
if (parsed === null) return;
const next = formatDistanceValue(unitToDegrees(parsed, unit, latitude), 8);
lastPushed.current = next;
onChange(next);
}, [draft, latitude, onChange, unit, value]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reconcile effect depends on onChange, but the parent passes a fresh inline closure on every render (onChange={(value) => updateValue(param.name, value)}, further down in this file). Since values changes on every keystroke anywhere in the form, ProcessingDialog re-renders and every mounted DistanceInput's onChange identity changes with it — so this effect re-runs for every distance field on every keystroke in the whole dialog, not just its own. The value !== lastPushed.current / latitudeChanged guards make each extra run a no-op, so this isn't a correctness bug, just churn. A useCallback around the per-field onChange (or dropping onChange from the dependency array, since it's stable in spirit) would avoid the redundant invocations.

Confidence: low-medium — purely a performance/quality nit, no observed incorrect behavior since the effect is idempotent when nothing relevant changed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct analysis, but I am leaving it. Each redundant run is two reference comparisons before the effect returns (value !== lastPushed.current, then latitudeChanged), across the handful of distance fields a tool exposes — that is not a cost worth buying back.

Both fixes have a real downside: dropping onChange from the dependency array makes the list dishonest and trips exhaustive-deps, and routing it through a ref adds indirection to the one effect in this component whose ordering already needed care (see the History re-run bug just above). I would rather the effect stay plainly readable than shave comparisons nobody can measure.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • No functional/logic bugs found. Traced the DistanceInput reconcile effect (unit/draft/latitude/value interplay, external-overwrite detection, latitude-change reconversion) and the conversion/formatting/parsing helpers in whitebox-distance-params.ts in detail; all handle the edge cases they claim to (empty input, malformed text, unit round-trips, pole clamping, zero-latitude labeling, tool-switch remounts via the new composite key). Confidence: medium-high that the core logic is correct.

Security

  • None found. The only interpolated dynamic value in translated strings is the user's own numeric distance, rendered as text — no injection surface.

Performance

  • Minor: the DistanceInput reconcile useEffect re-runs on every keystroke anywhere in the dialog (not just its own field) because it depends on onChange, which the parent recreates as a fresh closure every render. Harmless (guarded to no-op), but avoidable churn — flagged inline. Confidence: low-medium.
  • The all-or-nothing distanceLatitude computation hides the unit picker for an entire tool if any one vector input (even an unrelated optional one) resolves to a layer with no computable extent, rather than degrading only the affected field — flagged inline. Confidence: low.

Quality

  • The name-based heuristic (DISTANCE_SEGMENTS/NON_DISTANCE_NAMES) is inherently approximate, but this is explicitly acknowledged and documented by the author (in-code comments, tests, and the new CLAUDE.md entry), so it's not a fresh finding.
  • Test coverage for the new pure functions (whitebox-distance-params.ts) is thorough (name matching, latitude scaling incl. pole clamp, unit round-trips, formatting, input parsing, layer-id extraction). The stateful DistanceInput component logic itself isn't covered by a component-level test, but that matches this repo's existing convention (no @testing-library/react in use; frontend tests are pure-function node:test style), so not treated as a gap.

CLAUDE.md

  • The new CLAUDE.md bullet accurately describes DISTANCE_SEGMENTS/NON_DISTANCE_NAMES and their locations, and correctly follows the file's established pattern for documenting these "no per-tool table, re-check on geolibre-wasm bump" mirrors. No discrepancies found between the doc and the actual code.

Two inline comments posted (both low/low-medium confidence, non-blocking): the all-or-nothing extent-bailout behavior in distanceLatitude, and the unstable onChange dependency causing redundant (but harmless) effect re-invocations.

A layer whose features all carry a null geometry (an attribute table)
contributes no coordinates to the operation, so it should not decide the
conversion anchor for inputs that do have an extent. Binding one to an
optional input was hiding the unit picker for every distance field on the
tool. It is now skipped, and only an input with no in-memory GeoJSON at
all still disqualifies the tool, since that one is read from a path in its
own CRS.
Comment thread apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Both inline comments posted successfully. Now the final summary.

Code review

Bugs: None found with high confidence. The core conversion math (metersPerDegreeAt, unitToDegrees/degreesToUnit), the reconciliation useEffect in DistanceInput (external value rewrite vs. latitude-change re-conversion), the wgs84VectorLayerIds/wgs84ToolLayerIds gating, and the key={selectedTool.id}:${param.name}} fix for stale field state across tool switches were all traced through carefully and check out — including the edge cases the PR's own tests target (pole clamping, empty/malformed input, round-tripping units, significant-digit formatting without exponent notation).

Security: None applicable — purely client-side numeric parsing/formatting and display text, no injected markup or external input handling.

Performance: None of concern. The extent scan (getLayerBounds/turf bbox) is memoized against a derived layer-id key so it only reruns when the tool's actual input layers change, not on every keystroke.

Quality:

  • The new distance/unit-picker branch in the ParameterField switch is ordered after isPathParameter(param), unlike the other special-cased branches (CRS picker, map-extent, subset-URL picker), which were deliberately placed before isPathParameter specifically to avoid its generic "path/file/folder/directory" keyword heuristic shadowing them. A double-kind distance parameter whose description happens to contain one of those words would silently lose the unit picker. No current catalog collision found, but the asymmetry with the other branches looks like an oversight (medium-low confidence). — flagged inline at ProcessingDialog.tsx:2434.
  • The new processing.distance.geographicNote and the pre-existing processing.whitebox.vectorUnitsNote both render, stacked, in the common case (local run, vector input, distance parameter present), saying largely the same thing in different words — a minor redundant-messaging nit (low-medium confidence). — flagged inline at ProcessingDialog.tsx:1930.

CLAUDE.md: The new maintenance-mirror entry for DISTANCE_SEGMENTS/NON_DISTANCE_NAMES follows the existing convention for these repo-specific "re-check on dependency bump" notes and accurately describes the code it documents.

- Move the distance branch above isPathParameter, like the CRS and
  map-extent pickers already are. That fallback matches path/file/folder
  anywhere in a parameter's name or description and does not exclude a
  numeric kind, so a distance whose wording mentioned one would have
  rendered as a file browser with no error. Nothing in the catalog collides
  today; the other branches guard against this for the same reason.
- Show only one coordinate-units note. The older degrees warning and the
  new geographic-CRS note were stacking in exactly the case this feature
  targets, saying the same thing except that only the new one tells the
  user what to do about it.
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. I traced the DistanceInput reconciliation useEffect (external-rewrite vs. latitude-change branches, combined-change precedence), the unit-switch/round-trip math, formatDistanceValue's significant-digit/exponent handling, parseDistanceInput's strict-decimal regex, and wgs84VectorLayerIds'/wgs84ToolLayerIds's eligibility gating against runSelectedTool's actual GeoJSON-passing logic (which is not gated on runLocal, matching the picker's own scope) — all held up under the edge cases I checked (empty/partial input, malformed text, pole clamping, tool switching, input-layer switching mid-edit, Processing History rewrites). High confidence this is correct.

Security

  • None identified; no new input reaches injection-sensitive sinks, and the numeric parsing intentionally rejects anything that isn't a plain decimal.

Performance

  • Low confidence: distanceLatitude (ProcessingDialog.tsx:793) recomputes a turf.bbox pass over the relevant input layer(s) whenever the layers array reference changes, not only when the layers it actually reads change — see inline comment. distanceLayerKey is already memoized to sidestep the same issue for values/keystrokes, but layers isn't narrowed the same way. Likely immaterial in practice.

Quality

  • The new whitebox-distance-params.ts module is thoroughly documented and its pure-function surface (name matching, unit conversion, latitude scaling, formatting, layer-id extraction) has solid, well-reasoned unit test coverage. The key={${selectedTool.id}:${param.name}} fix is a good general correctness improvement (previously same-named params across different tools could share stale field state on a tool switch), not just needed for this feature.
  • The DistanceInput component's stateful reconciliation logic (draft/unit/lastPushed/lastLatitude refs) is intricate but only exercised by manual/Playwright testing per the PR description, consistent with this codebase's existing convention of unit-testing extracted logic rather than the dialog component itself — not flagging as a gap.

CLAUDE.md

  • The new Conventions bullet documenting DISTANCE_SEGMENTS/NON_DISTANCE_NAMES as a catalog-drift risk to re-check on geolibre-wasm bumps is accurate against the code and consistent with the file's existing style for similar mirrored/unexported constants.

Overall this is a carefully engineered change with good attention to edge cases (pole clamping, hemisphere labeling, round-trip precision, note precedence, stale-state resets); only one low-confidence performance nit was worth flagging inline.

The extent scan reran whenever the layers array was replaced, which any
layer mutation in the app does: a visibility toggle, a restyle, an
unrelated layer being added. For a large input layer that is a full
collection pass on the React commit path each time. A WeakMap keyed on the
FeatureCollection itself means the scan happens once per layer and repeats
only when that layer's data actually changes.
Comment thread apps/geolibre-desktop/src/lib/whitebox-distance-params.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • No high-confidence logic bugs found in the new unit-conversion math, name-matching regex, or the DistanceInput state-reconciliation effect — all checked against the accompanying tests and matched expected behavior (unit round-trips, latitude clamping, malformed-input handling, tool-switch key remount, and the note-mutual-exclusion logic).

Security

  • None identified. Input parsing (parseDistanceInput) deliberately rejects lenient/ambiguous number formats (comma separators, hex/octal/binary literals, Infinity) rather than silently misinterpreting them — a good defensive choice, not a vulnerability.

Performance

  • None of concern. The new distanceLatitude/distanceLayerKey memoization is deliberately designed to avoid re-scanning layer geometry on unrelated re-renders (string-keyed memo + WeakMap bounds cache keyed on the FeatureCollection object), matching the codebase's existing patterns for this kind of cost.

Quality

  • Low-medium confidence: wgs84VectorLayerIds (apps/geolibre-desktop/src/lib/whitebox-distance-params.ts:192) disqualifies a tool whenever a vector input's value isn't a layer:-prefixed token — but a locally-browsed .geojson file is also fed to the tool as raw in-memory WGS84 GeoJSON (see ProcessingDialog.tsx ~1519–1526), just under a bare filename value. That case silently misses the new unit picker/geographic note even though the same degrees-vs-metres situation applies. Posted inline — may be intentional per the PR's stated scope ("a vector layer with in-memory GeoJSON"), but worth confirming.
  • Low confidence: the DistanceInput reconciliation useEffect lists onChange in its dependency array, and the parent passes a freshly-created arrow function on every ProcessingDialog render, so the effect re-runs far more often than the logic it guards actually needs. Harmless today because the internal value !== lastPushed.current / latitudeChanged checks no-op on spurious runs, but worth a useCallback if this ever gets more expensive.

CLAUDE.md

  • The new bullet documenting DISTANCE_SEGMENTS/NON_DISTANCE_NAMES follows the file's existing "mirror" convention (explains the drift risk, the trigger for re-checking, and the failure mode) consistently with the other entries in that section — no issues.

@giswqs
giswqs merged commit 8272233 into main Jul 31, 2026
24 checks passed
@giswqs
giswqs deleted the feat/issue-1540-distance-units branch July 31, 2026 00:46
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.

[Feature]: Tools used for Spatial Analysis to be able to detect the CRS units of measure

2 participants