Arrows + marker click focus

This commit is contained in:
Sebastien Riviere
2025-09-05 00:11:38 +02:00
parent d0a3351f55
commit 999c9a8b77
8 changed files with 196 additions and 155 deletions

View File

@@ -1,17 +1,23 @@
export function BlueButton({ children, ...props }) {
return (<button {...props} className="bg-blue-600 hover:bg-blue-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children}
</button>)
return (
<button {...props} className="bg-blue-600 hover:bg-blue-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children}
</button>
);
}
export function RedButton({ children, ...props }) {
return (<button {...props} className="bg-red-600 hover:bg-red-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children}
</button>)
return (
<button {...props} className="bg-red-600 hover:bg-red-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children}
</button>
);
}
export function GreenButton({ children, ...props }) {
return (<button {...props} className="bg-green-600 hover:bg-green-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children}
</button>)
return (
<button {...props} className="bg-green-600 hover:bg-green-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children}
</button>
);
}

View File

@@ -0,0 +1,139 @@
import { useEffect, useState } from "react";
import { Marker, CircleMarker, Polygon, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import 'leaflet-polylinedecorator';
export function Node({pos, nodeSize = 5, color = 'black'}) {
return (
<CircleMarker center={pos} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} />
);
}
export function LabeledPolygon({polygon, number, color = 'black', opacity = '0.5', border = 3, iconSize = 24, iconColor = 'white'}) {
const length = polygon.length;
if (length < 3) return null;
const sum = polygon.reduce(
(acc, coord) => ({
lat: acc.lat + coord.lat,
lng: acc.lng + coord.lng
}),
{ lat: 0, lng: 0 }
);
// meanPoint can be out of the polygon
// Idea : take the mean point of the largest connected subpolygon
const meanPoint = {lat: sum.lat / length, lng: sum.lng / length}
const numberIcon = L.divIcon({
html: `<div style="
display: flex;
align-items: center;
justify-content: center;
color: ${iconColor};
font-weight: bold;
font-size: ${iconSize}px;
">${number}</div>`,
className: 'custom-number-icon',
iconSize: [iconSize, iconSize],
iconAnchor: [iconSize / 2, iconSize / 2]
});
return (<>
<Polygon positions={polygon} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }} />
<Marker position={meanPoint} icon={numberIcon} />
</>);
}
export function Arrow({ pos1, pos2, color = 'black', weight = 5, arrowSize = 20, insetPixels = 25 }) {
const map = useMap();
const [insetPositions, setInsetPositions] = useState(null);
useEffect(() => {
const updateInsetLine = () => {
if (!pos1 || !pos2) {
setInsetPositions(null);
return;
}
// Convert lat/lng to screen coordinates
const point1 = map.latLngToContainerPoint(pos1);
const point2 = map.latLngToContainerPoint(pos2);
// Calculate direction vector
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// If the points are too close, do not render
if (distance <= 2*insetPixels) {
setInsetPositions(null);
return;
}
// Normalize direction vector
const unitX = dx / distance;
const unitY = dy / distance;
// Calculate new start and end points in screen coordinates
const newStartPoint = {
x: point1.x + (unitX * insetPixels),
y: point1.y + (unitY * insetPixels)
};
const newEndPoint = {
x: point2.x - (unitX * insetPixels),
y: point2.y - (unitY * insetPixels)
};
// Convert back to lat/lng
const newStartLatLng = map.containerPointToLatLng(newStartPoint);
const newEndLatLng = map.containerPointToLatLng(newEndPoint);
setInsetPositions([[newStartLatLng.lat, newStartLatLng.lng], [newEndLatLng.lat, newEndLatLng.lng]]);
};
updateInsetLine();
// Update when map moves or zooms
map.on('zoom move', updateInsetLine);
return () => map.off('zoom move', updateInsetLine);
}, [pos1, pos2]);
useEffect(() => {
if (!insetPositions) return;
// Create the base polyline
const polyline = L.polyline(insetPositions, {
color: color,
weight: weight
}).addTo(map);
// Create the arrow decorator
const decorator = L.polylineDecorator(polyline, {
patterns: [{
offset: '100%',
repeat: 0,
symbol: L.Symbol.arrowHead({
pixelSize: arrowSize,
polygon: false,
pathOptions: {
stroke: true,
weight: weight,
color: color
}
})
}]
}).addTo(map);
// Cleanup function
return () => {
map.removeLayer(polyline);
map.removeLayer(decorator);
};
}, [insetPositions])
return null;
}