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",