Skip to content

feat: map measuring tape (#452)#458

Merged
fatherlinux merged 5 commits into
masterfrom
feature/452-measure-tape
Jun 1, 2026
Merged

feat: map measuring tape (#452)#458
fatherlinux merged 5 commits into
masterfrom
feature/452-measure-tape

Conversation

@fatherlinux
Copy link
Copy Markdown
Member

Summary

  • Adds a two-point measuring tape to the map, resolving the "how far apart are these POIs?" problem from [Feature]: Map Scale Key #452.
  • A ruler toggle joins the existing top-left control cluster (zoom / locate / satellite / measure).
  • Activating it drops two draggable endpoints A and B straddling the viewport center, joined by a dashed line with a live distance label.
  • Endpoints are anchored to lat/lng — they stay glued through zoom/pan; distance is geodesic (map.distance) and only changes when an endpoint moves.
  • Label shows imperial primary (ft / mi) with metric secondary (m / km), shrink-wrapped and centered on the midpoint.
  • Toggling off removes all layers; toggling on resets to center.

Frontend-only: frontend/src/components/Map.jsx + frontend/src/App.css. No backend or DB changes. Spec/plan under .specify/specs/032-measure-tape/.

Closes #452

Test plan

  • ./run.sh build passes (frontend compiles in-image)
  • Human-verified locally on :8082 — toggle, drag, live distance, zoom/pan stability, toggle-off cleanup, units, label sizing
  • Playwright smoke suite (./run.sh test, run on deploy)

🤖 Generated with Claude Code

fatherlinux and others added 2 commits May 31, 2026 22:08
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a toggleable measuring tool to the map. A ruler button joins the
existing top-left control cluster (zoom/locate/satellite); activating it
drops two draggable endpoints (A/B) straddling the viewport center,
connected by a dashed line with a live distance label.

- Endpoints are anchored to lat/lng, so they stay put through zoom/pan;
  distance is geodesic (map.distance) and only changes when an end moves.
- Label shows imperial primary (ft/mi) with metric secondary (m/km),
  shrink-wrapped and centered on the line midpoint.
- Toggling off removes all layers; toggling on resets to center.

Frontend-only (Map.jsx + App.css); no backend or DB changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a map measuring tape feature that allows users to measure the distance between two draggable endpoints on the map. It adds a toggle button to the map controls, introduces the MeasureTape component to manage the markers, polyline, and distance tooltip, and includes the necessary styling and specification documents. The review feedback highlights two key improvements: refactoring the ZoomLocateControl component's effect hook with a ref to prevent unnecessary re-renders when toggle states change, and disabling click propagation on the measurement handles to prevent unintended map zoom and POI deselection.

Comment thread frontend/src/components/Map.jsx Outdated
}
};
}, [map, handleLocate, useSatellite, onSatelliteToggle]);
}, [map, handleLocate, useSatellite, onSatelliteToggle, measureMode, onMeasureToggle]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ZoomLocateControl is currently recreated and re-added to the map every time measureMode or useSatellite changes because they are included in the useEffect dependency array. This causes the entire control cluster to flash and re-render in the DOM, which can disrupt accessibility (focus states) and degrade performance.

To prevent this, you can store the toggle callbacks in a useRef and keep the control's creation effect stable by only depending on map and handleLocate.

Here is how you can refactor it:

  const callbacksRef = useRef({ onSatelliteToggle, onMeasureToggle });
  useEffect(() => {
    callbacksRef.current = { onSatelliteToggle, onMeasureToggle };
  });

  useEffect(() => {
    // ... inside ZoomLocateControlClass onAdd:
    L.DomEvent.on(satelliteToggle, 'click', function(e) {
      L.DomEvent.preventDefault(e);
      if (callbacksRef.current.onSatelliteToggle) {
        callbacksRef.current.onSatelliteToggle();
      }
    });

    L.DomEvent.on(measure, 'click', function(e) {
      L.DomEvent.preventDefault(e);
      if (callbacksRef.current.onMeasureToggle) {
        callbacksRef.current.onMeasureToggle();
      }
    });
    // ...
  }, [map, handleLocate]);

Comment thread frontend/src/components/Map.jsx Outdated
Comment on lines +943 to +944
const markerA = L.marker(startA, { draggable: true, icon: makeHandle('A'), zIndexOffset: 1200, keyboard: false }).addTo(map);
const markerB = L.marker(startB, { draggable: true, icon: makeHandle('B'), zIndexOffset: 1200, keyboard: false }).addTo(map);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Clicking or double-clicking on the measurement handles (A and B) will propagate to the map. This causes the active POI in the sidebar to be deselected (via the map's click handler) and the map to zoom in (via double-click).

To prevent this, disable click propagation on the marker DOM elements once they are added to the map.

Suggested change
const markerA = L.marker(startA, { draggable: true, icon: makeHandle('A'), zIndexOffset: 1200, keyboard: false }).addTo(map);
const markerB = L.marker(startB, { draggable: true, icon: makeHandle('B'), zIndexOffset: 1200, keyboard: false }).addTo(map);
const markerA = L.marker(startA, { draggable: true, icon: makeHandle('A'), zIndexOffset: 1200, keyboard: false }).addTo(map);
const markerB = L.marker(startB, { draggable: true, icon: makeHandle('B'), zIndexOffset: 1200, keyboard: false }).addTo(map);
if (markerA._icon) L.DomEvent.disableClickPropagation(markerA._icon);
if (markerB._icon) L.DomEvent.disableClickPropagation(markerB._icon);

fatherlinux and others added 3 commits May 31, 2026 22:51
…458 review)

The ZoomLocateControl effect listed the satellite/measure state and inline
toggle handlers in its deps, so the entire Leaflet control was torn down and
recreated on every render/toggle (flicker, wasted DOM work).

- Make the toggle handlers stable with useCallback in Map.
- Stop reading useSatellite/measureMode inside onAdd; the dedicated sync
  effects already own each button's active state and run on mount.
- Effect now depends only on map + stable callbacks, so the control is
  built exactly once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop keyboard:false on the endpoint markers so they aren't removed from
the tab order; the option wasn't load-bearing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The measure button is the 5th button in the zoom-locate control cluster;
update the button-order test to assert its presence and position.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fatherlinux fatherlinux merged commit 77c3a17 into master Jun 1, 2026
3 checks passed
@fatherlinux fatherlinux deleted the feature/452-measure-tape branch June 1, 2026 03:03
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]: Map Scale Key

1 participant