feat: Display total polyline distance in menu with unit conversion

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
2025-08-17 18:50:44 +00:00
parent 2ef752dc75
commit 24a65b7f79

View File

@@ -222,44 +222,7 @@ function PolylineWithArrows({ coords, showDirection, showPoints }) {
return null; return null;
} }
function Map({data, loading, end, duration, slider, mapState, setMapState, setSubmenu, showDirection, showPoints, setDrawnItems}) { function Map({data, loading, coords, mapState, setMapState, setSubmenu, showDirection, showPoints, setDrawnItems}) {
const range = useMemo(() => parseSlider(end, duration, slider), [end, duration, slider]);
const coords = useMemo(() => {
// 1. Guard against invalid top-level data
if (!Array.isArray(data)) {
return [];
}
const result = [];
const [startTime, endTime] = range;
// 2. Loop through the data
for (const point of data) {
// 3. Guard against malformed points
if (!point || typeof point !== 'object') {
continue;
}
const { lat, lon, time } = point;
// 4. Guard against invalid time
if (typeof time !== 'string' || time.length === 0) {
continue;
}
// 5. Guard against invalid coordinates (null, undefined, NaN, non-number)
if (typeof lat !== 'number' || typeof lon !== 'number' || !isFinite(lat) || !isFinite(lon)) {
continue;
}
// 6. Now that all data is known to be valid, filter by time
if (time >= startTime && time <= endTime) {
result.push([lat, lon]);
}
}
return result;
}, [data, range]);
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
@@ -342,7 +305,7 @@ function Map({data, loading, end, duration, slider, mapState, setMapState, setSu
); );
} }
function Menu({data, duration, setDuration, end, setEnd, slider, setSlider, submenu, setSubmenu, showDirection, setShowDirection, showPoints, setShowPoints, setMapState, shareStart, shareEnd, drawnItems}) { function Menu({data, duration, setDuration, end, setEnd, slider, setSlider, submenu, setSubmenu, showDirection, setShowDirection, showPoints, setShowPoints, setMapState, shareStart, shareEnd, drawnItems, coords}) {
const [showRange, setShowRange] = useState(false); const [showRange, setShowRange] = useState(false);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [searchResults, setSearchResults] = useState(null); const [searchResults, setSearchResults] = useState(null);
@@ -648,6 +611,25 @@ function Menu({data, duration, setDuration, end, setEnd, slider, setSlider, subm
setSearchResults(newResults); setSearchResults(newResults);
}; };
const totalDistance = useMemo(() => {
if (!coords || coords.length < 2) {
return null;
}
let distance = 0;
for (let i = 0; i < coords.length - 1; i++) {
const p1 = leaflet.latLng(coords[i]);
const p2 = leaflet.latLng(coords[i+1]);
distance += p1.distanceTo(p2);
}
if (distance < 1000) {
return `${distance.toFixed(0)} m`;
} else {
return `${(distance / 1000).toFixed(2)} km`;
}
}, [coords]);
const range = parseSlider(end, duration, slider); const range = parseSlider(end, duration, slider);
const startDate = moment(end).subtract(...duration.delta); const startDate = moment(end).subtract(...duration.delta);
@@ -739,7 +721,7 @@ function Menu({data, duration, setDuration, end, setEnd, slider, setSlider, subm
return ( return (
<div className='menu'> <div className='menu'>
{(showRange || !!submenu) && <div className='range'> {(showRange || !!submenu) && <div className='range'>
{rangeTime(range[0])} - {rangeTime(range[1])} <span style={{ whiteSpace: 'nowrap' }}>({rangeDelta(range)})</span> {rangeTime(range[0])} - {rangeTime(range[1])} <span style={{ whiteSpace: 'nowrap' }}>({rangeDelta(range)})</span>{totalDistance && <span style={{ whiteSpace: 'nowrap' }}> ({totalDistance})</span>}
</div>} </div>}
<div className='time-slider'> <div className='time-slider'>
@@ -968,6 +950,44 @@ function App() {
const [data, loading] = useSensor('owntracks', 'OwnTracks', end, duration); const [data, loading] = useSensor('owntracks', 'OwnTracks', end, duration);
const range = useMemo(() => parseSlider(end, duration, slider), [end, duration, slider]);
const coords = useMemo(() => {
// 1. Guard against invalid top-level data
if (!Array.isArray(data)) {
return [];
}
const result = [];
const [startTime, endTime] = range;
// 2. Loop through the data
for (const point of data) {
// 3. Guard against malformed points
if (!point || typeof point !== 'object') {
continue;
}
const { lat, lon, time } = point;
// 4. Guard against invalid time
if (typeof time !== 'string' || time.length === 0) {
continue;
}
// 5. Guard against invalid coordinates (null, undefined, NaN, non-number)
if (typeof lat !== 'number' || typeof lon !== 'number' || !isFinite(lat) || !isFinite(lon)) {
continue;
}
// 6. Now that all data is known to be valid, filter by time
if (time >= startTime && time <= endTime) {
result.push([lat, lon]);
}
}
return result;
}, [data, range]);
const shareStart = shareStartParam ? moment.unix(shareStartParam) : null; const shareStart = shareStartParam ? moment.unix(shareStartParam) : null;
const shareEnd = shareEndParam ? moment.unix(shareEndParam) : null; const shareEnd = shareEndParam ? moment.unix(shareEndParam) : null;
@@ -1026,12 +1046,10 @@ function App() {
shareEnd={shareEnd} shareEnd={shareEnd}
data={data} data={data}
drawnItems={drawnItems} drawnItems={drawnItems}
coords={coords}
/> />
<Map <Map
end={end}
duration={duration}
slider={slider}
mapState={mapState} mapState={mapState}
setMapState={setMapState} setMapState={setMapState}
setSubmenu={setSubmenu} setSubmenu={setSubmenu}
@@ -1040,6 +1058,7 @@ function App() {
data={data} data={data}
loading={loading} loading={loading}
setDrawnItems={setDrawnItems} setDrawnItems={setDrawnItems}
coords={coords}
/> />
</div> </div>
); );