mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 02:10:18 +01:00
Arrows + marker click focus
This commit is contained in:
@@ -38,7 +38,7 @@
|
|||||||
- [ ] Voir les traces et évènements des teams
|
- [ ] Voir les traces et évènements des teams
|
||||||
- [ ] Voir l'incertitude de position des teams
|
- [ ] Voir l'incertitude de position des teams
|
||||||
- [x] Focus une team cliquée
|
- [x] Focus une team cliquée
|
||||||
- [ ] Refaire les flèches de chasse sur la map
|
- [x] Refaire les flèches de chasse sur la map
|
||||||
- [ ] Mettre en évidence le menu paramètre (configuration)
|
- [ ] Mettre en évidence le menu paramètre (configuration)
|
||||||
- [ ] Afficher un feedback quand un paramètre est sauvegardé
|
- [ ] Afficher un feedback quand un paramètre est sauvegardé
|
||||||
- [ ] Pouvoir définir la zone de départ de chaque équipe
|
- [ ] Pouvoir définir la zone de départ de chaque équipe
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Marker, Tooltip, Polyline, Polygon, Circle } from "react-leaflet";
|
import { Marker, Tooltip, Polygon, Circle } from "react-leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import 'leaflet-polylinedecorator';
|
||||||
|
import { Arrow } from "@/components/layer";
|
||||||
import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
|
import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import { GameState, ZoneTypes } from "@/util/types";
|
import { GameState, ZoneTypes } from "@/util/types";
|
||||||
@@ -14,7 +16,7 @@ const positionIcon = new L.Icon({
|
|||||||
shadowSize: [30, 30],
|
shadowSize: [30, 30],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
|
export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
|
||||||
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
||||||
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
||||||
|
|
||||||
@@ -40,34 +42,20 @@ export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, map
|
|||||||
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function Arrow({pos1, pos2}) {
|
|
||||||
if (pos1 && pos2) {
|
|
||||||
return (
|
|
||||||
<Polyline positions={[pos1, pos2]} pathOptions={{ color: 'black', weight: 3 }}/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Zones() {
|
function Zones() {
|
||||||
if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null;
|
if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null;
|
||||||
|
|
||||||
switch (zoneType) {
|
switch (zoneType) {
|
||||||
case ZoneTypes.CIRCLE:
|
case ZoneTypes.CIRCLE:
|
||||||
return (
|
return (<>
|
||||||
<div>
|
{ zoneExtremities.begin && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} color="red" fillColor="red" />}
|
||||||
{ zoneExtremities.begin && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} color="red" fillColor="red" />}
|
{ zoneExtremities.end && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} color="green" fillColor="green" />}
|
||||||
{ zoneExtremities.end && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} color="green" fillColor="green" />}
|
</>);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case ZoneTypes.POLYGON:
|
case ZoneTypes.POLYGON:
|
||||||
return (
|
return (<>
|
||||||
<div>
|
{ zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
||||||
{ zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
{ zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
|
||||||
{ zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
|
</>);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -80,14 +68,12 @@ export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, map
|
|||||||
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
|
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
|
||||||
<MapEventListener onDragStart={() => setIsFocusing(false)}/>
|
<MapEventListener onDragStart={() => setIsFocusing(false)}/>
|
||||||
<Zones/>
|
<Zones/>
|
||||||
{teams.map((team) => team.currentLocation && !team.captured &&
|
{teams.map((team) => team.currentLocation && !team.captured && <>
|
||||||
<>
|
<Marker key={team.id} position={team.currentLocation} icon={positionIcon} eventHandlers={{click: () => onSelected(team.id)}}>
|
||||||
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
|
||||||
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
|
</Marker>
|
||||||
</Marker>
|
{showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>}
|
||||||
{showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>}
|
</>)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CustomMapContainer>
|
</CustomMapContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default function AdminPage() {
|
|||||||
<div className="flex-1 h-full">
|
<div className="flex-1 h-full">
|
||||||
<LiveMap
|
<LiveMap
|
||||||
selectedTeamId={selectedTeamId}
|
selectedTeamId={selectedTeamId}
|
||||||
|
onSelected={onSelected}
|
||||||
isFocusing={isFocusing}
|
isFocusing={isFocusing}
|
||||||
setIsFocusing={setIsFocusing}
|
setIsFocusing={setIsFocusing}
|
||||||
mapStyle={mapStyle}
|
mapStyle={mapStyle}
|
||||||
|
|||||||
@@ -16,38 +16,16 @@ const EditMode = {
|
|||||||
function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
||||||
const { center: maxCenter, radius: maxRadius, handleLeftClick: maxLeftClick, handleRightClick: maxRightClick, handleMouseMove: maxHover } = useMapCircleDraw(maxZone, setMaxZone);
|
const { center: maxCenter, radius: maxRadius, handleLeftClick: maxLeftClick, handleRightClick: maxRightClick, handleMouseMove: maxHover } = useMapCircleDraw(maxZone, setMaxZone);
|
||||||
const { center: minCenter, radius: minRadius, handleLeftClick: minLeftClick, handleRightClick: minRightClick, handleMouseMove: minHover } = useMapCircleDraw(minZone, setMinZone);
|
const { center: minCenter, radius: minRadius, handleLeftClick: minLeftClick, handleRightClick: minRightClick, handleMouseMove: minHover } = useMapCircleDraw(minZone, setMinZone);
|
||||||
|
|
||||||
function handleLeftClick(e) {
|
|
||||||
if (editMode == EditMode.MAX) {
|
|
||||||
maxLeftClick(e);
|
|
||||||
} else {
|
|
||||||
minLeftClick(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRightClick(e) {
|
|
||||||
if (editMode == EditMode.MAX) {
|
|
||||||
maxRightClick(e);
|
|
||||||
} else {
|
|
||||||
minRightClick(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseMove(e) {
|
|
||||||
if (editMode == EditMode.MAX) {
|
|
||||||
maxHover(e);
|
|
||||||
} else {
|
|
||||||
minHover(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (<>
|
||||||
<div>
|
<MapEventListener
|
||||||
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove}/>
|
onLeftClick={editMode == EditMode.MAX ? maxLeftClick : minLeftClick}
|
||||||
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="red" fillColor="red" />}
|
onRightClick={editMode == EditMode.MAX ? maxRightClick : minRightClick}
|
||||||
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="blue" fillColor="blue" />}
|
onMouseMove={editMode == EditMode.MAX ? maxHover : minHover}
|
||||||
</div>
|
/>
|
||||||
);
|
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="red" fillColor="red" />}
|
||||||
|
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="blue" fillColor="blue" />}
|
||||||
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) {
|
export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) {
|
||||||
|
|||||||
@@ -1,98 +1,27 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Polyline, Polygon, CircleMarker, Marker } from "react-leaflet";
|
import { Polyline, Polygon, Marker } from "react-leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { GreenButton } from "@/components/button";
|
import { GreenButton } from "@/components/button";
|
||||||
import { ReorderList } from "@/components/list";
|
import { ReorderList } from "@/components/list";
|
||||||
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
||||||
import { TextInput } from "@/components/input";
|
import { TextInput } from "@/components/input";
|
||||||
|
import { Node, LabeledPolygon } from "@/components/layer";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
|
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
|
||||||
import useLocalVariable from "@/hook/useLocalVariable";
|
import useLocalVariable from "@/hook/useLocalVariable";
|
||||||
|
|
||||||
function Drawings({ polygons, addPolygon, removePolygon }) {
|
function Drawings({ polygons, addPolygon, removePolygon }) {
|
||||||
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
|
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
|
||||||
const nodeSize = 5; // px
|
|
||||||
const lineThickness = 3; // px
|
|
||||||
|
|
||||||
function DrawNode({pos, color}) {
|
return (<>
|
||||||
return (
|
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
||||||
<CircleMarker center={pos} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} />
|
{polygons.map((polygon, i) => <LabeledPolygon key={i} polygon={polygon} number={i+1} />)}
|
||||||
);
|
{ currentPolygon.length > 0 && <>
|
||||||
}
|
<Node pos={currentPolygon[0]} color={"red"} />
|
||||||
|
<Polyline positions={currentPolygon} pathOptions={{ color: "red", weight: 3 }} />
|
||||||
function DrawLine({pos1, pos2, color}) {
|
</>}
|
||||||
return (
|
{highlightNodes.map((node, i) => <Node key={i} pos={node} color={"black"} />)}
|
||||||
<Polyline positions={[pos1, pos2]} pathOptions={{ color: color, weight: lineThickness }} />
|
</>);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawUnfinishedPolygon({polygon}) {
|
|
||||||
const length = polygon.length;
|
|
||||||
if (length > 0) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DrawNode pos={polygon[0]} color={"red"} zIndexOffset={1000} />
|
|
||||||
{polygon.map((_, i) => {
|
|
||||||
if (i < length-1) {
|
|
||||||
return <DrawLine key={i} pos1={polygon[i]} pos2={polygon[i+1]} color={"red"} />;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawPolygon({polygon, number}) {
|
|
||||||
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="
|
|
||||||
font-size: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 25px;
|
|
||||||
">${number}</div>`,
|
|
||||||
className: 'custom-number-icon',
|
|
||||||
iconSize: [30, 30],
|
|
||||||
iconAnchor: [15, 15]
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Polygon positions={polygon} pathOptions={{ color: 'black', fillColor: 'black', fillOpacity: '0.5', weight: lineThickness }} />
|
|
||||||
<Marker position={meanPoint} icon={numberIcon} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
|
||||||
{polygons.map((polygon, i) => <DrawPolygon key={i} polygon={polygon} number={i+1} />)}
|
|
||||||
<DrawUnfinishedPolygon polygon={currentPolygon} />
|
|
||||||
{highlightNodes.map((node, i) => <DrawNode key={i} pos={node} color={"black"} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PolygonZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) {
|
export default function PolygonZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) {
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
export function BlueButton({ children, ...props }) {
|
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">
|
return (
|
||||||
{children}
|
<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">
|
||||||
</button>)
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RedButton({ children, ...props }) {
|
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">
|
return (
|
||||||
{children}
|
<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">
|
||||||
</button>)
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GreenButton({ children, ...props }) {
|
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">
|
return (
|
||||||
{children}
|
<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">
|
||||||
</button>)
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
139
traque-front/components/layer.jsx
Normal file
139
traque-front/components/layer.jsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -11,7 +11,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^16.6.0",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-defaulticon-compatibility": "^0.1.2",
|
"leaflet-defaulticon-compatibility": "^0.1.2",
|
||||||
|
"leaflet-polylinedecorator": "^1.6.0",
|
||||||
"next": "^14.2.9",
|
"next": "^14.2.9",
|
||||||
"next-runtime-env": "^3.2.2",
|
"next-runtime-env": "^3.2.2",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
|||||||
Reference in New Issue
Block a user