feat: add button to exclude drawn areas from time range

This commit is contained in:
2025-08-15 02:36:29 +00:00
parent 1a3c11b5bb
commit d0a5461073

View File

@@ -199,9 +199,7 @@ function PolylineWithArrows({ coords, showDirection }) {
return null;
}
function Map({end, duration, slider, mapState, setMapState, setSubmenu, showDirection}) {
const [data, loading] = useSensor('owntracks', 'OwnTracks', end, duration);
function Map({data, loading, end, duration, slider, mapState, setMapState, setSubmenu, showDirection, setDrawnItems}) {
const range = useMemo(() => parseSlider(end, duration, slider), [end, duration, slider]);
const coords = useMemo(() => {
@@ -246,14 +244,25 @@ function Map({end, duration, slider, mapState, setMapState, setSubmenu, showDire
localStorage.setItem('api_key', api_key);
}
const onRectangleDrawn = (e) => {
const onCreated = (e) => {
const { layer } = e;
const bounds = layer.getBounds();
console.log('Rectangle drawn. Bounds:', {
northEast: bounds.getNorthEast(),
southWest: bounds.getSouthWest(),
setDrawnItems(items => [...items, {id: layer._leaflet_id, bounds: layer.getBounds()}]);
};
const onEdited = (e) => {
const { layers } = e;
layers.eachLayer(layer => {
setDrawnItems(items => items.map(item =>
item.id === layer._leaflet_id ? { ...item, bounds: layer.getBounds() } : item
));
});
// In the future, we can use these bounds to filter data or perform a search.
};
const onDeleted = (e) => {
const { layers } = e;
const deletedIds = [];
layers.eachLayer(layer => deletedIds.push(layer._leaflet_id));
setDrawnItems(items => items.filter(item => !deletedIds.includes(item.id)));
};
return (
@@ -273,7 +282,9 @@ function Map({end, duration, slider, mapState, setMapState, setSubmenu, showDire
<FeatureGroup>
<EditControl
position="topright"
onCreated={onRectangleDrawn}
onCreated={onCreated}
onEdited={onEdited}
onDeleted={onDeleted}
draw={{
rectangle: true,
polyline: false,
@@ -300,7 +311,7 @@ function Map({end, duration, slider, mapState, setMapState, setSubmenu, showDire
);
}
function Menu({duration, setDuration, end, setEnd, slider, setSlider, submenu, setSubmenu, showDirection, setShowDirection, setMapState, shareStart, shareEnd}) {
function Menu({data, duration, setDuration, end, setEnd, slider, setSlider, submenu, setSubmenu, showDirection, setShowDirection, setMapState, shareStart, shareEnd, drawnItems}) {
const [showRange, setShowRange] = useState(false);
const chooseDuration = (x) => {
@@ -361,6 +372,80 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider, submenu, s
setSubmenu(false);
};
const excludeArea = () => {
const drawnRectangles = drawnItems.map(item => item.bounds);
if (!drawnRectangles.length || !data || !Array.isArray(data)) {
if (!drawnRectangles.length) alert("Please draw one or more rectangles on the map first.");
setSubmenu(false);
return;
}
const isInsideExclusionZone = (lat, lon) => {
for (const rect of drawnRectangles) {
if (rect.contains([lat, lon])) {
return true;
}
}
return false;
};
const goodSegments = [];
let currentSegment = null;
for (const point of data) {
if (!point || typeof point.lat !== 'number' || typeof point.lon !== 'number' || !point.time) {
continue;
}
const isInside = isInsideExclusionZone(point.lat, point.lon);
if (!isInside) {
if (!currentSegment) {
currentSegment = { start: point.time, end: point.time };
} else {
currentSegment.end = point.time;
}
} else {
if (currentSegment) {
goodSegments.push(currentSegment);
currentSegment = null;
}
}
}
if (currentSegment) {
goodSegments.push(currentSegment);
}
if (!goodSegments.length) {
alert("No time ranges found outside the selected area(s).");
setSubmenu(false);
return;
}
let longestSegment = goodSegments[0];
for (let i = 1; i < goodSegments.length; i++) {
const durationCurrent = moment(longestSegment.end).diff(moment(longestSegment.start));
const durationNew = moment(goodSegments[i].end).diff(moment(goodSegments[i].start));
if (durationNew > durationCurrent) {
longestSegment = goodSegments[i];
}
}
const startUnix = moment(longestSegment.start).unix();
const endUnix = moment(longestSegment.end).unix();
const endOfWindowUnix = end.unix();
const newSliderStart = Math.round((startUnix - endOfWindowUnix) / duration.secs + duration.num);
const newSliderEnd = Math.round((endUnix - endOfWindowUnix) / duration.secs + duration.num);
const clampedStart = Math.max(0, newSliderStart);
const clampedEnd = Math.min(duration.num, newSliderEnd);
setSlider([clampedStart, clampedEnd]);
setSubmenu(false);
};
const range = parseSlider(end, duration, slider);
const startDate = moment(end).subtract(...duration.delta);
@@ -530,6 +615,7 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider, submenu, s
Show direction
</label>
<button onClick={recentreView}>Recentre view</button>
<button onClick={excludeArea}>Exclude area</button>
<button onClick={shareRange}>Share range</button>
<button onClick={resetToDefaults}>Reset page</button>
</>
@@ -592,6 +678,9 @@ function App() {
});
const [submenu, setSubmenu] = useState(false);
const [showDirection, setShowDirection] = useState(initialShowDirection);
const [drawnItems, setDrawnItems] = useState([]);
const [data, loading] = useSensor('owntracks', 'OwnTracks', end, duration);
const shareStart = shareStartParam ? moment.unix(shareStartParam) : null;
const shareEnd = shareEndParam ? moment.unix(shareEndParam) : null;
@@ -644,6 +733,8 @@ function App() {
setMapState={setMapState}
shareStart={shareStart}
shareEnd={shareEnd}
data={data}
drawnItems={drawnItems}
/>
<Map
@@ -654,6 +745,9 @@ function App() {
setMapState={setMapState}
setSubmenu={setSubmenu}
showDirection={showDirection}
data={data}
loading={loading}
setDrawnItems={setDrawnItems}
/>
</div>
);