From 75f8b10ecda7b821fce1304b581ebc67dcaeaba2 Mon Sep 17 00:00:00 2001 From: Sebastien Riviere Date: Sun, 7 Sep 2025 17:24:29 +0200 Subject: [PATCH] Added placement zones --- doc/TODO.md | 7 +- traque-back/admin_socket.js | 9 +- traque-back/game.js | 14 +++ traque-front/app/admin/components/liveMap.jsx | 5 + .../app/admin/components/teamViewer.jsx | 8 +- .../components/circleZoneSelector.jsx | 99 +++++++++------ .../components/placementZoneSelector.jsx | 69 +++++++++++ .../components/playingZoneSelector.jsx | 40 ++++++ .../components/polygonZoneSelector.jsx | 117 +++++++++++------- .../parameters/components/teamManager.jsx | 58 ++++++--- traque-front/app/admin/parameters/page.js | 107 +++++++--------- traque-front/components/layer.jsx | 10 +- traque-front/components/list.jsx | 12 +- traque-front/hook/useAdmin.jsx | 6 +- ...useMapCircleDraw.jsx => useCircleDraw.jsx} | 20 +-- traque-front/hook/useMultipleCircleDraw.jsx | 39 ++++++ ...eMapPolygonDraw.jsx => usePolygonDraw.jsx} | 17 +-- 17 files changed, 436 insertions(+), 201 deletions(-) create mode 100644 traque-front/app/admin/parameters/components/placementZoneSelector.jsx create mode 100644 traque-front/app/admin/parameters/components/playingZoneSelector.jsx rename traque-front/hook/{useMapCircleDraw.jsx => useCircleDraw.jsx} (60%) create mode 100644 traque-front/hook/useMultipleCircleDraw.jsx rename traque-front/hook/{useMapPolygonDraw.jsx => usePolygonDraw.jsx} (95%) diff --git a/doc/TODO.md b/doc/TODO.md index de55d1b..637fc88 100644 --- a/doc/TODO.md +++ b/doc/TODO.md @@ -41,12 +41,11 @@ - [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é -- [ ] (IMPORTANT) Pouvoir définir la zone de départ de chaque équipe -- [ ] (IMPORTANT) Nommer les polygons par des lettres de l'alphabet +- [x] Pouvoir définir la zone de départ de chaque équipe +- [x] Nommer les polygons par des lettres de l'alphabet - [ ] Faire un menu quand on arrive sur la traque - [ ] Pouvoir load des paramètres enregistrés -- [ ] (IMPORTANT) Améliorer le système de création zone (cercle et polygone) -- [ ] (IMPORTANT) Améliorer la sélection du système de zone +- [ ] Améliorer le système de création zone (cercle et polygone) - [ ] Penser l'affichage en fin de traque ### Améliorations du jeu de la traque diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js index b6b0167..364a000 100644 --- a/traque-back/admin_socket.js +++ b/traque-back/admin_socket.js @@ -75,9 +75,14 @@ export function initAdminSocketHandler() { game.reorderTeams(newOrder); }); - socket.on("capture_team", (teamId, newTeam) => { + socket.on("capture_team", (teamId) => { if (!loggedIn) return; - game.captureTeam(teamId, newTeam); + game.captureTeam(teamId); + }); + + socket.on("placement_team", (teamId, placementZone) => { + if (!loggedIn) return; + game.placementTeam(teamId, placementZone); }); socket.on("change_state", (state) => { diff --git a/traque-back/game.js b/traque-back/game.js index 9890491..266c5b3 100644 --- a/traque-back/game.js +++ b/traque-back/game.js @@ -114,6 +114,7 @@ export default { team.finishDate = null; sendUpdatedTeamInformations(team.id); } + this.updateChasingChain(); secureAdminBroadcast("teams", this.teams); }, @@ -317,6 +318,19 @@ export default { return true; }, + placementTeam(teamId, placementZone) { + // Test of parameters + if (!this.hasTeam(teamId)) return false; + // Variables + const team = this.getTeam(teamId); + // Make the capture + team.startingArea = placementZone; + // Broadcast new infos + secureAdminBroadcast("teams", this.teams); + sendUpdatedTeamInformations(team.id); + return true; + }, + reorderTeams(newOrder) { // Update teams const teamMap = new Map(this.teams.map(team => [team.id, team])); diff --git a/traque-front/app/admin/components/liveMap.jsx b/traque-front/app/admin/components/liveMap.jsx index cb5ecbe..c5245eb 100644 --- a/traque-front/app/admin/components/liveMap.jsx +++ b/traque-front/app/admin/components/liveMap.jsx @@ -68,6 +68,11 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF {isFocusing && } setIsFocusing(false)}/> + {teams.map((team) => gameState == GameState.PLACEMENT && team.startingArea && + + {team.name} + + )} {teams.map((team) => team.currentLocation && !team.captured && <> onSelected(team.id)}}> {showNames && {team.name}} diff --git a/traque-front/app/admin/components/teamViewer.jsx b/traque-front/app/admin/components/teamViewer.jsx index f22cd97..0d59813 100644 --- a/traque-front/app/admin/components/teamViewer.jsx +++ b/traque-front/app/admin/components/teamViewer.jsx @@ -2,12 +2,12 @@ import { List } from '@/components/list'; import useAdmin from '@/hook/useAdmin'; import { getStatus } from '@/util/functions'; -function TeamViewerItem({ team, itemSelected, onSelected }) { +function TeamViewerItem({ team }) { const { gameState } = useAdmin(); const status = getStatus(team, gameState); return ( -
onSelected(team.id)}> +
0 ? "green" : "red"}.png`} className="w-4 h-4" /> @@ -27,9 +27,9 @@ export default function TeamViewer({selectedTeamId, onSelected}) { const { teams } = useAdmin(); return ( - + {(team) => ( - + )} ); diff --git a/traque-front/app/admin/parameters/components/circleZoneSelector.jsx b/traque-front/app/admin/parameters/components/circleZoneSelector.jsx index 76cb337..6264d42 100644 --- a/traque-front/app/admin/parameters/components/circleZoneSelector.jsx +++ b/traque-front/app/admin/parameters/components/circleZoneSelector.jsx @@ -5,8 +5,10 @@ import { CustomButton } from "@/components/button"; import { CustomMapContainer, MapEventListener } from "@/components/map"; import { TextInput } from "@/components/input"; import useAdmin from "@/hook/useAdmin"; -import useMapCircleDraw from "@/hook/useMapCircleDraw"; +import useMapCircleDraw from "@/hook/useCircleDraw"; import useLocalVariable from "@/hook/useLocalVariable"; +import { defaultZoneSettings } from "@/util/configurations"; +import { ZoneTypes } from "@/util/types"; const EditMode = { MIN: 0, @@ -28,17 +30,40 @@ function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) { ); } -export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) { - const {outOfZoneDelay, updateSettings} = useAdmin(); +export default function CircleZoneSelector() { + const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin(); + const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e})); const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e})); const [editMode, setEditMode] = useState(EditMode.MIN); useEffect(() => { - setEditMode(editMode == EditMode.MIN ? EditMode.MAX : EditMode.MIN); - }, [zoneSettings.min, zoneSettings.max]); + if (localZoneSettings.type != ZoneTypes.CIRCLE) { + setLocalZoneSettings(defaultZoneSettings.circle); + } + }, [localZoneSettings]); - function handleSettingsSubmit() { - applyZoneSettings(); + useEffect(() => { + setEditMode(editMode == EditMode.MIN ? EditMode.MAX : EditMode.MIN); + }, [localZoneSettings.min, localZoneSettings.max]); + + function setMinZone(minZone) { + setLocalZoneSettings({...localZoneSettings, min: minZone}); + } + + function setMaxZone(maxZone) { + setLocalZoneSettings({...localZoneSettings, max: maxZone}); + } + + function updateReductionCount(reductionCount) { + setLocalZoneSettings({...localZoneSettings, reductionCount: reductionCount}); + } + + function updateDuration(duration) { + setLocalZoneSettings({...localZoneSettings, duration: duration}); + } + + function handleSubmit() { + applyLocalZoneSettings(); applyLocalOutOfZoneDelay(); } @@ -48,38 +73,40 @@ export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, ap return (
-
- - modifyZoneSettings("min", e)} maxZone={zoneSettings.max} setMaxZone={(e) => modifyZoneSettings("max", e)} editMode={editMode} /> - -
-
-
- {editMode == EditMode.MIN && setEditMode(EditMode.MAX)}>Click to edit first zone} - {editMode == EditMode.MAX && setEditMode(EditMode.MIN)}>Click to edit last zone} + {localZoneSettings.type == ZoneTypes.CIRCLE && <> +
+ + +
-
-

Reduction number

-
- modifyZoneSettings("reductionCount", customStringToInt(e.target.value))} /> +
+
+ {editMode == EditMode.MIN && setEditMode(EditMode.MAX)}>Click to edit first zone} + {editMode == EditMode.MAX && setEditMode(EditMode.MIN)}>Click to edit last zone} +
+
+

Reduction number

+
+ updateReductionCount(customStringToInt(e.target.value))} /> +
+
+
+

Zone duration

+
+ updateDuration(customStringToInt(e.target.value))} /> +
+
+
+

Timeout

+
+ setLocalOutOfZoneDelay(customStringToInt(e.target.value))} /> +
+
+
+ Apply
-
-

Zone duration

-
- modifyZoneSettings("duration", customStringToInt(e.target.value))} /> -
-
-
-

Timeout

-
- setLocalOutOfZoneDelay(customStringToInt(e.target.value))} /> -
-
-
- Apply -
-
+ }
); } diff --git a/traque-front/app/admin/parameters/components/placementZoneSelector.jsx b/traque-front/app/admin/parameters/components/placementZoneSelector.jsx new file mode 100644 index 0000000..e8e3a3a --- /dev/null +++ b/traque-front/app/admin/parameters/components/placementZoneSelector.jsx @@ -0,0 +1,69 @@ +import { useEffect, useState } from "react"; +import { Circle, Tooltip } from "react-leaflet"; +import { List } from "@/components/list"; +import { CustomMapContainer, MapEventListener } from "@/components/map"; +import useAdmin from '@/hook/useAdmin'; +import useMultipleCircleDraw from "@/hook/useMultipleCircleDraw"; + +function Drawings({ placementZones, addZone, removeZone, handleRightClick }) { + const { handleLeftClick, handleRightClick: handleRightClickDrawing } = useMultipleCircleDraw(placementZones, addZone, removeZone, 30); + + function modifiedHandleRightClick(e) { + handleRightClickDrawing(e); + handleRightClick(); + } + + return (<> + + { placementZones.map(placementZone => + + {placementZone.name} + + )} + ); +} + +export default function PlacementZoneSelector() { + const { teams, getTeam, placementTeam } = useAdmin(); + const [selectedTeamId, setSelectedTeamId] = useState(null); + const [placementZones, setPlacementZones] = useState([]); + + useEffect(() => { + setPlacementZones(teams.filter(team => team.startingArea).map(team => ({id: team.id, name: team.name, center: team.startingArea.center, radius: team.startingArea.radius}))); + }, [teams]); + + function addPlacementZone(center, radius) { + if (!selectedTeamId) return; + const placementZonesFiltered = placementZones.filter(placementZone => placementZone.id !== selectedTeamId); + const newZone = {id: selectedTeamId, name: getTeam(selectedTeamId).name, center: center, radius: radius}; + placementTeam(selectedTeamId, newZone); + setPlacementZones([...placementZonesFiltered, newZone]); + } + + function removePlacementZone(id) { + placementTeam(id, null); + setPlacementZones(placementZones.filter((placementZone) => placementZone.id !== id)); + } + + return ( +
+
+ + setSelectedTeamId(null)} /> + +
+
+
+

Teams

+
+ setSelectedTeamId(selectedTeamId != id ? id : null)}> + { (team) => +
+

{team.name}

+
+ } +
+
+
+ ); +} diff --git a/traque-front/app/admin/parameters/components/playingZoneSelector.jsx b/traque-front/app/admin/parameters/components/playingZoneSelector.jsx new file mode 100644 index 0000000..55484a5 --- /dev/null +++ b/traque-front/app/admin/parameters/components/playingZoneSelector.jsx @@ -0,0 +1,40 @@ +import { useState } from "react"; +import dynamic from "next/dynamic"; +import { ZoneTypes } from "@/util/types"; + +// Imported at runtime and not at compile time +const CircleZoneSelector = dynamic(() => import('./circleZoneSelector'), { ssr: false }); +const PolygonZoneSelector = dynamic(() => import('./polygonZoneSelector'), { ssr: false }); + +function ZoneTypeButton({title, onClick, isSelected}) { + const grayStyle = "bg-gray-300 hover:bg-gray-400"; + const blueStyle = "bg-custom-light-blue"; + + return ( +
+

{title}

+
+ ); +} + +export default function PlayingZoneSelector() { + const [zoneType, setZoneType] = useState(ZoneTypes.POLYGON); + + return ( +
+
+

Type de zone :

+ setZoneType(ZoneTypes.CIRCLE)} isSelected={zoneType == ZoneTypes.CIRCLE} /> + setZoneType(ZoneTypes.POLYGON)} isSelected={zoneType == ZoneTypes.POLYGON} /> +
+
+ {zoneType == ZoneTypes.CIRCLE && + + } + {zoneType == ZoneTypes.POLYGON && + + } +
+
+ ); +} diff --git a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx index 4a6e51b..7ae4bed 100644 --- a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx +++ b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx @@ -7,15 +7,24 @@ 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 useMapPolygonDraw from "@/hook/usePolygonDraw"; import useLocalVariable from "@/hook/useLocalVariable"; +import { defaultZoneSettings } from "@/util/configurations"; +import { ZoneTypes } from "@/util/types"; -function Drawings({ polygons, addPolygon, removePolygon }) { - const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon); +function Drawings({ localZoneSettings, addZone, removeZone }) { + const [polygons, setPolygons] = useState([]); + const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addZone, removeZone); + + useEffect(() => { + if (localZoneSettings.type == ZoneTypes.POLYGON) { + setPolygons(localZoneSettings.polygons.map(zone => zone.polygon)); + } + }, [localZoneSettings]); return (<> - {polygons.map((polygon, i) => )} + {localZoneSettings.polygons.map((zone, i) => )} { currentPolygon.length > 0 && <> @@ -24,73 +33,87 @@ function Drawings({ polygons, addPolygon, removePolygon }) { ); } -export default function PolygonZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) { +export default function PolygonZoneSelector() { const defaultDuration = 10; - const [polygons, setPolygons] = useState([]); - const {outOfZoneDelay, updateSettings} = useAdmin(); + const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin(); + const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e})); const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e})); useEffect(() => { - if (zoneSettings) { - setPolygons(zoneSettings.polygons.map((zone) => zone.polygon)); + if (localZoneSettings.type != ZoneTypes.POLYGON) { + setLocalZoneSettings(defaultZoneSettings.polygon); } - }, [zoneSettings]); + }, [localZoneSettings]); - function idFromPolygon(polygon) { - return (polygon[0].lat + polygon[1].lat + polygon[2].lat).toString() + (polygon[0].lng + polygon[1].lng + polygon[2].lng).toString(); + function getNewPolygonName() { + const existingIds = new Set(localZoneSettings.polygons.map(zone => zone.id)); + for (let i = 0; i < 26; i++) { + const letter = String.fromCharCode(65 + i); + if (!existingIds.has(letter)) { + return letter; + } + } + return "XXX"; } - function addPolygon(polygon) { - const newPolygons = [...zoneSettings.polygons, {id: idFromPolygon(polygon), polygon: polygon, duration: defaultDuration}]; - modifyZoneSettings("polygons", newPolygons); + function setLocalZoneSettingsPolygons(polygons) { + setLocalZoneSettings({type: localZoneSettings.type, polygons: polygons}); } - function removePolygon(i) { - const newPolygons = zoneSettings.polygons.filter((_, index) => index !== i); - modifyZoneSettings("polygons", newPolygons); + function addZone(polygon) { + setLocalZoneSettingsPolygons([...localZoneSettings.polygons, {id: getNewPolygonName(), polygon: polygon, duration: defaultDuration}]); } - function updateDuration(i, duration) { - const newPolygons = zoneSettings.polygons.map((zone, index) => index === i ? {id: zone.id, polygon: zone.polygon, duration: duration} : zone); - modifyZoneSettings("polygons", newPolygons); + function removeZone(i) { + setLocalZoneSettingsPolygons(localZoneSettings.polygons.filter((_, index) => index !== i)); } - function handleSettingsSubmit() { - applyZoneSettings(); + function updateDuration(id, duration) { + setLocalZoneSettingsPolygons(localZoneSettings.polygons.map(zone => zone.id === id ? {...zone, duration: duration} : zone)); + } + + function handleSubmit() { + applyLocalZoneSettings(); applyLocalOutOfZoneDelay(); } + + function customStringToInt(e) { + return parseInt(e, 10) || null; + } return (
-
- - - -
-
-
-

Reduction order

+ {localZoneSettings.type == ZoneTypes.POLYGON && <> +
+ + +
- modifyZoneSettings("polygons", polygons)}> - { (zone, i) => -
-

Zone {i+1}

-
- updateDuration(i, parseInt(e.target.value, 10))}/> +
+
+

Reduction order

+
+ + { (zone) => +
+

Zone {zone.id}

+
+ updateDuration(zone.id, customStringToInt(e.target.value))}/> +
+ } +
+
+

Timeout

+
+ setLocalOutOfZoneDelay(customStringToInt(e.target.value))}/>
- } - -
-

Timeout

-
- setLocalOutOfZoneDelay(parseInt(e.target.value, 10))} /> +
+
+ Apply
-
- Apply -
-
+ }
); } diff --git a/traque-front/app/admin/parameters/components/teamManager.jsx b/traque-front/app/admin/parameters/components/teamManager.jsx index bddd933..470f330 100644 --- a/traque-front/app/admin/parameters/components/teamManager.jsx +++ b/traque-front/app/admin/parameters/components/teamManager.jsx @@ -1,31 +1,61 @@ +import { useState } from "react"; import { ReorderList } from '@/components/list'; import useAdmin from '@/hook/useAdmin'; +import useLocalVariable from "@/hook/useLocalVariable"; +import { TextInput } from "@/components/input"; +import { Section } from "@/components/section"; function TeamManagerItem({ team }) { const { captureTeam, removeTeam } = useAdmin(); return ( -
-
-

{team.name}

-
-

{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}

- captureTeam(team.id)} /> - removeTeam(team.id)} /> -
+
+

{team.name}

+
+

{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}

+ captureTeam(team.id)} /> + removeTeam(team.id)} />
); } export default function TeamManager() { - const { teams, reorderTeams } = useAdmin(); + const { teams, addTeam, reorderTeams, sendPositionDelay, updateSettings } = useAdmin(); + const [teamName, setTeamName] = useState(''); + const [localSendPositionDelay, setLocalSendPositionDelay, applyLocalSendPositionDelay] = useLocalVariable(sendPositionDelay, (e) => updateSettings({sendPositionDelay: e})); + + function handleTeamSubmit(e) { + e.preventDefault(); + if (teamName !== "") { + addTeam(teamName); + setTeamName("") + } + } return ( - reorderTeams(teams.map(team => team.id))}> - {(team) => ( - - )} - +
+
+
+ setTeamName(e.target.value)} type="text" className="w-full h-full p-4 ring-1 ring-inset ring-gray-300" /> +
+
+ +
+
+
+ reorderTeams(teams.map(team => team.id))}> + {(team) => ( + + )} + +
+
+

Interval between position updates

+
+ setLocalSendPositionDelay(parseInt(e.target.value, 10))} onBlur={applyLocalSendPositionDelay} /> +
+
+
); } diff --git a/traque-front/app/admin/parameters/page.js b/traque-front/app/admin/parameters/page.js index b221eb7..f5dc215 100644 --- a/traque-front/app/admin/parameters/page.js +++ b/traque-front/app/admin/parameters/page.js @@ -2,87 +2,66 @@ import { useState } from "react"; import dynamic from "next/dynamic"; import Link from "next/link"; -import { TextInput } from "@/components/input"; -import { Section } from "@/components/section"; -import { CustomButton } from "@/components/button"; import { useAdminConnexion } from "@/context/adminConnexionContext"; -import useAdmin from '@/hook/useAdmin'; import Messages from "./components/messages"; import TeamManager from './components/teamManager'; -import useLocalVariable from "@/hook/useLocalVariable"; -import { ZoneTypes } from "@/util/types"; -import { defaultZoneSettings } from "@/util/configurations"; +import PlayingZoneSelector from "./components/playingZoneSelector"; // Imported at runtime and not at compile time -const CircleZoneSelector = dynamic(() => import('./components/circleZoneSelector'), { ssr: false }); -const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false }); +const PlacementZoneSelector = dynamic(() => import('./components/placementZoneSelector'), { ssr: false }); + +const Tabs = { + PLACEMENT_ZONES: "placement_zones", + PLAYING_ZONES: "playing_zones", +} + +function ParametersTitle() { + return ( +
+ + + +

Paramètres

+
+ ); +} + +function TabButton({title, onClick, isSelected}) { + const grayStyle = "bg-gray-300 hover:bg-gray-400"; + const blueStyle = "bg-custom-light-blue"; + + return ( +
+

{title}

+
+ ); +} export default function ConfigurationPage() { const { useProtect } = useAdminConnexion(); - const {zoneSettings, sendPositionDelay, updateSettings, addTeam} = useAdmin(); - const [teamName, setTeamName] = useState(''); - const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e})); - const [localSendPositionDelay, setLocalSendPositionDelay, applyLocalSendPositionDelay] = useLocalVariable(sendPositionDelay, (e) => updateSettings({sendPositionDelay: e})); + const [currentTab, setCurrentTab] = useState(Tabs.PLACEMENT_ZONES); useProtect(); - function modifyLocalZoneSettings(key, value) { - setLocalZoneSettings(prev => ({...prev, [key]: value})); - } - - function handleChangeZoneType() { - setLocalZoneSettings(localZoneSettings.type == ZoneTypes.CIRCLE ? defaultZoneSettings.polygon : defaultZoneSettings.circle) - } - - function handleTeamSubmit(e) { - e.preventDefault(); - if (teamName !== "") { - addTeam(teamName); - setTeamName("") - } - } - return (
-
- - - -

Paramètres

-
+ -
-
-
- setTeamName(e.target.value)} type="text" className="w-full h-full p-4 ring-1 ring-inset ring-gray-300" /> -
-
- -
-
-
- -
-
-

Interval between position updates

-
- setLocalSendPositionDelay(parseInt(e.target.value, 10))} onBlur={applyLocalSendPositionDelay} /> -
-
-
+
-
-
- {localZoneSettings && Change zone type} +
+
+ setCurrentTab(Tabs.PLACEMENT_ZONES)} isSelected={currentTab == Tabs.PLACEMENT_ZONES}/> + setCurrentTab(Tabs.PLAYING_ZONES)} isSelected={currentTab == Tabs.PLAYING_ZONES}/>
-
- {localZoneSettings && localZoneSettings.type == ZoneTypes.CIRCLE && - - } - {localZoneSettings && localZoneSettings.type == ZoneTypes.POLYGON && - - } +
+ { currentTab == Tabs.PLAYING_ZONES && + + } + { currentTab == Tabs.PLACEMENT_ZONES && + + }
diff --git a/traque-front/components/layer.jsx b/traque-front/components/layer.jsx index f8197db..a874341 100644 --- a/traque-front/components/layer.jsx +++ b/traque-front/components/layer.jsx @@ -9,7 +9,7 @@ export function Node({pos, nodeSize = 5, color = 'black'}) { ); } -export function LabeledPolygon({polygon, number, color = 'black', opacity = '0.5', border = 3, iconSize = 24, iconColor = 'white'}) { +export function LabeledPolygon({polygon, label, color = 'black', opacity = '0.5', border = 3, iconSize = 24, iconColor = 'white'}) { const length = polygon.length; if (length < 3) return null; @@ -26,7 +26,7 @@ export function LabeledPolygon({polygon, number, color = 'black', opacity = '0.5 // Idea : take the mean point of the largest connected subpolygon const meanPoint = {lat: sum.lat / length, lng: sum.lng / length} - const numberIcon = L.divIcon({ + const labelIcon = L.divIcon({ html: `
${number}
`, - className: 'custom-number-icon', + ">${label}
`, + className: 'custom-label-icon', iconSize: [iconSize, iconSize], iconAnchor: [iconSize / 2, iconSize / 2] }); return (<> - + ); } diff --git a/traque-front/components/list.jsx b/traque-front/components/list.jsx index b8216cf..38cf43d 100644 --- a/traque-front/components/list.jsx +++ b/traque-front/components/list.jsx @@ -1,13 +1,19 @@ import { useEffect, useState } from 'react'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; -export function List({array, children}) { +export function List({array, selectedId, onSelected, children}) { + const canSelect = selectedId !== undefined + const cursor = () => canSelect ? " cursor-pointer" : ""; + const outline = (id) => canSelect && id === selectedId ? " outline outline-4 outline-black" : ""; + return (
    {array.map((elem, i) => (
  • - {children(elem, i)} +
    canSelect && onSelected(elem.id)}> + {children(elem, i)} +
  • ))} @@ -48,7 +54,7 @@ export function ReorderList({droppableId, array, setArray, children}) {
  • {provided => ( -
    +
    {children(elem, i)}
    diff --git a/traque-front/hook/useAdmin.jsx b/traque-front/hook/useAdmin.jsx index 0f140f2..6f7d577 100644 --- a/traque-front/hook/useAdmin.jsx +++ b/traque-front/hook/useAdmin.jsx @@ -27,6 +27,10 @@ export default function useAdmin() { adminSocket.emit("capture_team", teamId); } + function placementTeam(teamId, placementZone) { + adminSocket.emit("placement_team", teamId, placementZone); + } + function changeState(state) { adminSocket.emit("change_state", state); } @@ -35,5 +39,5 @@ export default function useAdmin() { adminSocket.emit("update_settings", settings); } - return { ...adminContext, getTeam, reorderTeams, addTeam, removeTeam, captureTeam, changeState, updateSettings }; + return { ...adminContext, getTeam, reorderTeams, addTeam, removeTeam, captureTeam, placementTeam, changeState, updateSettings }; } diff --git a/traque-front/hook/useMapCircleDraw.jsx b/traque-front/hook/useCircleDraw.jsx similarity index 60% rename from traque-front/hook/useMapCircleDraw.jsx rename to traque-front/hook/useCircleDraw.jsx index 7d23caa..b2e57ae 100644 --- a/traque-front/hook/useMapCircleDraw.jsx +++ b/traque-front/hook/useCircleDraw.jsx @@ -1,16 +1,16 @@ "use client"; import { useEffect, useState } from "react"; -export default function useMapCircleDraw(area, setArea) { +export default function useMapCircleDraw(circle, setCircle) { const [drawing, setDrawing] = useState(false); - const [center, setCenter] = useState(area?.center || null); - const [radius, setRadius] = useState(area?.radius || null); + const [center, setCenter] = useState(circle?.center || null); + const [radius, setRadius] = useState(circle?.radius || null); useEffect(() => { setDrawing(false); - setCenter(area?.center || null); - setRadius(area?.radius || null); - }, [area]) + setCenter(circle?.center || null); + setRadius(circle?.radius || null); + }, [circle]) function handleLeftClick(e) { if (!drawing) { @@ -19,17 +19,17 @@ export default function useMapCircleDraw(area, setArea) { setDrawing(true); } else { setDrawing(false); - setArea({center, radius}); + setCircle({center, radius}); } } function handleRightClick(e) { if (drawing) { setDrawing(false); - setCenter(area?.center || null); - setRadius(area?.radius || null); + setCenter(circle?.center || null); + setRadius(circle?.radius || null); } else { - setArea(null); + setCircle(null); } } diff --git a/traque-front/hook/useMultipleCircleDraw.jsx b/traque-front/hook/useMultipleCircleDraw.jsx new file mode 100644 index 0000000..0fe3569 --- /dev/null +++ b/traque-front/hook/useMultipleCircleDraw.jsx @@ -0,0 +1,39 @@ +"use client"; + +export default function useMultipleCircleDraw(circles, addCircle, removeCircle, radius) { + + function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) { + const degToRad = (deg) => deg * (Math.PI / 180); + var R = 6371; // Radius of the earth in km + var dLat = degToRad(lat2 - lat1); + var dLon = degToRad(lon2 - lon1); + var a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2) + ; + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; // Distance in km + return d * 1000; + } + + function isBaddlyPlaced(latlng) { + return circles.some(circle => getDistanceFromLatLon(latlng, circle.center) < 2 * circle.radius); + } + + function getCircleFromLatlng(latlng) { + return circles.find(circle => getDistanceFromLatLon(latlng, circle.center) < circle.radius); + } + + function handleLeftClick(e) { + if (isBaddlyPlaced(e.latlng)) return; + addCircle(e.latlng, radius); + } + + function handleRightClick(e) { + const circle = getCircleFromLatlng(e.latlng); + if (circle) removeCircle(circle.id); + } + + return { handleLeftClick, handleRightClick }; +} diff --git a/traque-front/hook/useMapPolygonDraw.jsx b/traque-front/hook/usePolygonDraw.jsx similarity index 95% rename from traque-front/hook/useMapPolygonDraw.jsx rename to traque-front/hook/usePolygonDraw.jsx index be168b7..926106a 100644 --- a/traque-front/hook/useMapPolygonDraw.jsx +++ b/traque-front/hook/usePolygonDraw.jsx @@ -94,15 +94,10 @@ export default function useMapPolygonDraw(polygons, addPolygon, removePolygon) { } return sum > 0; }; - - function getZoneIndex(latlng) { + + function getPolygonIndex(latlng) { // Return the index of the polygon where latlng is according to isInPolygon - for (let iPolygon = 0; iPolygon < polygons.length; iPolygon++) { - if (isInPolygon(latlng, polygons[iPolygon])) { - return iPolygon; - } - } - return -1; + return polygons.findIndex(polygon => isInPolygon(latlng, polygon)); } function getEventLatLng(e) { @@ -142,7 +137,7 @@ export default function useMapPolygonDraw(polygons, addPolygon, removePolygon) { // If it is the first node if (!isDrawing()) { // If the point is not in an existing polygon - if (getZoneIndex(latlng) == -1) { + if (getPolygonIndex(latlng) == -1) { setCurrentPolygon([latlng]); } @@ -172,7 +167,7 @@ export default function useMapPolygonDraw(polygons, addPolygon, removePolygon) { // Is the new point making the current polygon intersect with itself ? if (isIntersecting([latlng, currentPolygon[length-1]], currentPolygon, false)) return; // Is the new point inside a polygon ? - if (getZoneIndex(latlng) != -1) return; + if (getPolygonIndex(latlng) != -1) return; // Is the new point making the current polygon intersect with another polygon ? for (const polygon of polygons) { // Strict intersection @@ -200,7 +195,7 @@ export default function useMapPolygonDraw(polygons, addPolygon, removePolygon) { setCurrentPolygon([]); // If not isDrawing, remove the clicked polygon } else { - const i = getZoneIndex(e.latlng); + const i = getPolygonIndex(e.latlng); if (i != -1) removePolygon(i); } }