From 999c9a8b777f2ebce776759790d301e5bb20c534 Mon Sep 17 00:00:00 2001 From: Sebastien Riviere Date: Fri, 5 Sep 2025 00:11:38 +0200 Subject: [PATCH] Arrows + marker click focus --- doc/TODO.md | 2 +- traque-front/app/admin/components/liveMap.jsx | 50 +++---- traque-front/app/admin/page.js | 1 + .../components/circleZoneSelector.jsx | 40 ++--- .../components/polygonZoneSelector.jsx | 93 ++---------- traque-front/components/button.jsx | 24 +-- traque-front/components/layer.jsx | 139 ++++++++++++++++++ traque-front/package.json | 2 + 8 files changed, 196 insertions(+), 155 deletions(-) create mode 100644 traque-front/components/layer.jsx diff --git a/doc/TODO.md b/doc/TODO.md index 1bf8700..2d547ce 100644 --- a/doc/TODO.md +++ b/doc/TODO.md @@ -38,7 +38,7 @@ - [ ] Voir les traces et évènements des teams - [ ] Voir l'incertitude de position des teams - [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) - [ ] Afficher un feedback quand un paramètre est sauvegardé - [ ] Pouvoir définir la zone de départ de chaque équipe diff --git a/traque-front/app/admin/components/liveMap.jsx b/traque-front/app/admin/components/liveMap.jsx index 0c9bbf8..cb5ecbe 100644 --- a/traque-front/app/admin/components/liveMap.jsx +++ b/traque-front/app/admin/components/liveMap.jsx @@ -1,6 +1,8 @@ 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-polylinedecorator'; +import { Arrow } from "@/components/layer"; import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map"; import useAdmin from "@/hook/useAdmin"; import { GameState, ZoneTypes } from "@/util/types"; @@ -14,7 +16,7 @@ const positionIcon = new L.Icon({ 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 [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"); } - function Arrow({pos1, pos2}) { - if (pos1 && pos2) { - return ( - - ); - } else { - return null; - } - } - function Zones() { if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null; switch (zoneType) { case ZoneTypes.CIRCLE: - return ( -
- { zoneExtremities.begin && } - { zoneExtremities.end && } -
- ); + return (<> + { zoneExtremities.begin && } + { zoneExtremities.end && } + ); case ZoneTypes.POLYGON: - return ( -
- { zoneExtremities.begin && } - { zoneExtremities.end && } -
- ); + return (<> + { zoneExtremities.begin && } + { zoneExtremities.end && } + ); default: return null; } @@ -80,14 +68,12 @@ export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, map {isFocusing && } setIsFocusing(false)}/> - {teams.map((team) => team.currentLocation && !team.captured && - <> - - {showNames && {team.name}} - - {showArrows && } - - )} + {teams.map((team) => team.currentLocation && !team.captured && <> + onSelected(team.id)}}> + {showNames && {team.name}} + + {showArrows && } + )} ) diff --git a/traque-front/app/admin/page.js b/traque-front/app/admin/page.js index 37bf127..7393e67 100644 --- a/traque-front/app/admin/page.js +++ b/traque-front/app/admin/page.js @@ -77,6 +77,7 @@ export default function AdminPage() {
- - {minCenter && minRadius && } - {maxCenter && maxRadius && } -
- ); + return (<> + + {minCenter && minRadius && } + {maxCenter && maxRadius && } + ); } export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) { diff --git a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx index fc782d6..ad58143 100644 --- a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx +++ b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx @@ -1,98 +1,27 @@ 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 { GreenButton } from "@/components/button"; import { ReorderList } from "@/components/list"; import { CustomMapContainer, MapEventListener } from "@/components/map"; import { TextInput } from "@/components/input"; +import { Node, LabeledPolygon } from "@/components/layer"; import useAdmin from "@/hook/useAdmin"; import useMapPolygonDraw from "@/hook/useMapPolygonDraw"; import useLocalVariable from "@/hook/useLocalVariable"; function Drawings({ 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 ( - - ); - } - - function DrawLine({pos1, pos2, color}) { - return ( - - ); - } - - function DrawUnfinishedPolygon({polygon}) { - const length = polygon.length; - if (length > 0) { - return ( -
- - {polygon.map((_, i) => { - if (i < length-1) { - return ; - } else { - return null; - } - })} -
- ); - } - } - - 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: `
${number}
`, - className: 'custom-number-icon', - iconSize: [30, 30], - iconAnchor: [15, 15] - }); - - return ( -
- - -
- ); - } - - return ( -
- - {polygons.map((polygon, i) => )} - - {highlightNodes.map((node, i) => )} -
- ); + return (<> + + {polygons.map((polygon, i) => )} + { currentPolygon.length > 0 && <> + + + } + {highlightNodes.map((node, i) => )} + ); } export default function PolygonZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) { diff --git a/traque-front/components/button.jsx b/traque-front/components/button.jsx index 2d4f279..bf63a6d 100644 --- a/traque-front/components/button.jsx +++ b/traque-front/components/button.jsx @@ -1,17 +1,23 @@ export function BlueButton({ children, ...props }) { - return () + return ( + + ); } export function RedButton({ children, ...props }) { - return () + return ( + + ); } export function GreenButton({ children, ...props }) { - return () + return ( + + ); } diff --git a/traque-front/components/layer.jsx b/traque-front/components/layer.jsx new file mode 100644 index 0000000..f8197db --- /dev/null +++ b/traque-front/components/layer.jsx @@ -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 ( + + ); +} + +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: `
${number}
`, + className: 'custom-number-icon', + iconSize: [iconSize, iconSize], + iconAnchor: [iconSize / 2, iconSize / 2] + }); + + return (<> + + + ); +} + +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; +} diff --git a/traque-front/package.json b/traque-front/package.json index 819d808..1a1b555 100644 --- a/traque-front/package.json +++ b/traque-front/package.json @@ -11,7 +11,9 @@ }, "dependencies": { "@hello-pangea/dnd": "^16.6.0", + "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", + "leaflet-polylinedecorator": "^1.6.0", "next": "^14.2.9", "next-runtime-env": "^3.2.2", "react": "^18",