diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js index 5bebd34..eb7dc44 100644 --- a/traque-back/admin_socket.js +++ b/traque-back/admin_socket.js @@ -90,9 +90,9 @@ export function initAdminSocketHandler() { } if (!zoneManager.changeSettings(settings)) { socket.emit("error", "Error changing zone"); - socket.emit("zone_settings", zoneManager.settings) + socket.emit("zone_settings", settings) } else { - secureAdminBroadcast("zone_settings", zoneManager.settings) + secureAdminBroadcast("zone_settings", settings) } }) diff --git a/traque-back/zone_manager.js b/traque-back/zone_manager.js index b926553..55444d2 100644 --- a/traque-back/zone_manager.js +++ b/traque-back/zone_manager.js @@ -4,6 +4,11 @@ import { secureAdminBroadcast } from './admin_socket.js'; /* -------------------------------- Useful functions and constants -------------------------------- */ +const zoneTypes = { + circle: "circle", + polygon: "polygon" +} + const EARTH_RADIUS = 6_371_000; // Radius of the earth in m function haversine_distance({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) { @@ -20,9 +25,64 @@ function latlngEqual(latlng1, latlng2, epsilon = 1e-9) { } +/* -------------------------------- Circle zones -------------------------------- */ + +const defaultCircleSettings = {type: zoneTypes.circle, min: null, max: null, reductionCount: 4, duration: 10} + +function circleZone(center, radius, duration) { + return { + center: center, + radius: radius, + duration: duration, + + isInZone(location) { + return haversine_distance(center, location) < this.radius; + } + } +} + +function circleSettingsToZones(settings) { + const {min, max, reductionCount, duration} = settings; + + if (!min || !max) return []; + if (haversine_distance(max.center, min.center) > max.radius - min.radius) return []; + + const zones = [circleZone(max.center, max.radius, duration)]; + const radiusReductionLength = (max.radius - min.radius) / reductionCount; + let center = max.center; + let radius = max.radius; + + for (let i = 1; i < reductionCount; i++) { + radius -= radiusReductionLength; + let new_center = null; + while (!new_center || haversine_distance(new_center, min.center) > radius - min.radius) { + const angle = Math.random() * 2 * Math.PI; + const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS; + const lat0Rad = center.lat * Math.PI / 180; + const lon0Rad = center.lng * Math.PI / 180; + const latRad = Math.asin( + Math.sin(lat0Rad) * Math.cos(angularDistance) + + Math.cos(lat0Rad) * Math.sin(angularDistance) * Math.cos(angle) + ); + + const lonRad = lon0Rad + Math.atan2( + Math.sin(angle) * Math.sin(angularDistance) * Math.cos(lat0Rad), + Math.cos(angularDistance) - Math.sin(lat0Rad) * Math.sin(latRad) + ); + new_center = {lat: latRad * 180 / Math.PI, lng: lonRad * 180 / Math.PI}; + } + center = new_center; + zones.push(circleZone(center, radius, duration)) + } + zones.push(circleZone(min.center, min.radius, 0)); + + return zones; +} + + /* -------------------------------- Polygon zones -------------------------------- */ -const defaultPolygonSettings = []; +const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []} function polygonZone(points, duration) { return { @@ -82,9 +142,11 @@ function mergePolygons(poly1, poly2) { } function polygonSettingsToZones(settings) { + const {polygons} = settings; + const zones = []; - for (const { polygon, duration } of settings.slice().reverse()) { + for (const { polygon, duration } of polygons.slice().reverse()) { const length = zones.length; if (length == 0) { @@ -104,67 +166,13 @@ function polygonSettingsToZones(settings) { } -/* -------------------------------- Circle zones -------------------------------- */ - -const defaultCircleSettings = { min: null, max: null, reductionCount: 4, duration: 1 }; - -function circleZone(center, radius, duration) { - return { - center: center, - radius: radius, - duration: duration, - - isInZone(location) { - return haversine_distance(center, location) < this.radius; - } - } -} - -function circleSettingsToZones(settings) { - const {min, max, reductionCount, duration} = settings; - if (haversine_distance(max.center, min.center) > max.radius - min.radius) { - return null; - } - const zones = [circleZone(max.center, max.radius, duration)]; - const radiusReductionLength = (max.radius - min.radius) / reductionCount; - let center = max.center; - let radius = max.radius; - for (let i = 1; i < reductionCount; i++) { - radius -= radiusReductionLength; - let new_center = null; - while (!new_center || haversine_distance(new_center, min.center) > radius - min.radius) { - const angle = Math.random() * 2 * Math.PI; - const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS; - const lat0Rad = center.lat * Math.PI / 180; - const lon0Rad = center.lng * Math.PI / 180; - const latRad = Math.asin( - Math.sin(lat0Rad) * Math.cos(angularDistance) + - Math.cos(lat0Rad) * Math.sin(angularDistance) * Math.cos(angle) - ); - - const lonRad = lon0Rad + Math.atan2( - Math.sin(angle) * Math.sin(angularDistance) * Math.cos(lat0Rad), - Math.cos(angularDistance) - Math.sin(lat0Rad) * Math.sin(latRad) - ); - new_center = {lat: latRad * 180 / Math.PI, lng: lonRad * 180 / Math.PI}; - } - center = new_center; - zones.push(circleZone(center, radius, duration)) - } - zones.push(circleZone(min.center, min.radius, 0)); - return zones; -} - - /* -------------------------------- Zone manager -------------------------------- */ export default { isRunning: false, zones: [], // A zone has to be connected space that doesn't contain an earth pole currentZone: { id: 0, timeoutId: null, endDate: null }, - zoneType: "polygon", settings: defaultPolygonSettings, - settingsToZones: polygonSettingsToZones, start() { this.isRunning = true; @@ -209,25 +217,18 @@ export default { }, changeSettings(settings) { - const zones = this.settingsToZones(settings); - if (!zones) return false; - this.zones = zones; + switch (settings.type) { + case zoneTypes.circle: + this.zones = circleSettingsToZones(settings); + break; + case zoneTypes.polygon: + this.zones = polygonSettingsToZones(settings); + break; + default: + return; + } this.settings = settings; this.zoneBroadcast(); - return true; - }, - - changeZoneType(type) { - if (this.zoneType == type) return; - if (type == "circle") { - this.zoneType = "circle"; - this.settings = defaultCircleSettings; - this.settingsToZones = circleSettingsToZones; - } else if (type == "polygon") { - this.zoneType = "polygon"; - this.settings = defaultPolygonSettings; - this.settingsToZones = polygonSettingsToZones; - } }, zoneBroadcast() { diff --git a/traque-front/app/admin/components/liveMap.jsx b/traque-front/app/admin/components/liveMap.jsx index 28ac044..321708d 100644 --- a/traque-front/app/admin/components/liveMap.jsx +++ b/traque-front/app/admin/components/liveMap.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Marker, Tooltip, Polyline, Polygon } from "react-leaflet"; +import { Marker, Tooltip, Polyline, Polygon, Circle } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import { CustomMapContainer } from "@/components/map"; import useAdmin from "@/hook/useAdmin"; @@ -13,9 +13,14 @@ const positionIcon = new L.Icon({ shadowSize: [30, 30], }); +const zoneTypes = { + circle: "circle", + polygon: "polygon" +} + export default function LiveMap({mapStyle, showZones, showNames, showArrows}) { + const { zoneSettings, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin(); const [timeLeftNextZone, setTimeLeftNextZone] = useState(null); - const { zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin(); // Remaining time before sending position useEffect(() => { @@ -49,12 +54,34 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) { } } + function Zones() { + if (!(showZones && gameState == GameState.PLAYING && zoneSettings)) return null; + + switch (zoneSettings.type) { + case zoneTypes.circle: + return ( +
+ { zoneExtremities.begin && } + { zoneExtremities.end && } +
+ ); + case zoneTypes.polygon: + return ( +
+ { zoneExtremities.begin && } + { zoneExtremities.end && } +
+ ); + default: + return null; + } + } + return (
{gameState == GameState.PLAYING &&

{`Next zone in : ${formatTime(timeLeftNextZone)}`}

} - {showZones && gameState == GameState.PLAYING && zoneExtremities.begin && } - {showZones && gameState == GameState.PLAYING && zoneExtremities.end && } + {teams.map((team) => team.currentLocation && !team.captured && {showNames && {team.name}} diff --git a/traque-front/app/admin/parameters/components/circleZoneSelector.jsx b/traque-front/app/admin/parameters/components/circleZoneSelector.jsx index 12a4450..5cb0f19 100644 --- a/traque-front/app/admin/parameters/components/circleZoneSelector.jsx +++ b/traque-front/app/admin/parameters/components/circleZoneSelector.jsx @@ -49,55 +49,61 @@ function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) { ); } -export default function CircleZoneSelector() { +export default function CircleZoneSelector({zoneSettings, updateZoneSettings, applyZoneSettings}) { + const {penaltySettings, changePenaltySettings} = useAdmin(); + const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState(""); const [editMode, setEditMode] = useState(EditMode.MIN); - const [minZone, setMinZone] = useState(null); - const [maxZone, setMaxZone] = useState(null); - const [reductionCount, setReductionCount] = useState(""); - const [duration, setDuration] = useState(""); - const {zoneSettings, changeZoneSettings} = useAdmin(); useEffect(() => { - if (zoneSettings) { - setMinZone(zoneSettings.min); - setMaxZone(zoneSettings.max); - setReductionCount(zoneSettings.reductionCount.toString()); - setDuration(zoneSettings.duration.toString()); - } - }, [zoneSettings]); + setEditMode(editMode == EditMode.MIN ? EditMode.MAX : EditMode.MIN); + }, [zoneSettings.min, zoneSettings.max]) - // When the user set one zone, switch to the other useEffect(() => { - if(editMode == EditMode.MIN) { - setEditMode(EditMode.MAX); - } else { - setEditMode(EditMode.MIN); + if (penaltySettings) { + setAllowedTimeOutOfZone(penaltySettings.allowedTimeOutOfZone.toString()); } - - }, [minZone, maxZone]); + }, [penaltySettings]); function handleSettingsSubmit() { - const newSettings = {min:minZone, max:maxZone, reductionCount: Number(reductionCount), duration: Number(duration)}; - changeZoneSettings(newSettings); + console.log(zoneSettings) + applyZoneSettings(); + changePenaltySettings({allowedTimeOutOfZone: Number(allowedTimeOutOfZone)}); } return ( -
-

Edit zones

- {editMode == EditMode.MIN && setEditMode(EditMode.MAX)}>Click to edit first zone} - {editMode == EditMode.MAX && setEditMode(EditMode.MIN)}>Click to edit last zone} - - - -
-

Number of zones

- setReductionCount(e.target.value)}> +
+
+ + updateZoneSettings("min", e)} maxZone={zoneSettings.max} setMaxZone={(e) => updateZoneSettings("max", e)} editMode={editMode} /> +
-
-

Duration of a zone

- setDuration(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} +
+
+

Number

+
+ updateZoneSettings("reductionCount", e.target.value)} /> +
+
+
+

Duration

+
+ updateZoneSettings("duration", e.target.value)} /> +
+
+
+

Timeout

+
+ setAllowedTimeOutOfZone(e.target.value)} /> +
+
+
+ Apply +
- Apply
); } diff --git a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx index 74fe16e..337de98 100644 --- a/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx +++ b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx @@ -94,45 +94,46 @@ function Drawings({ polygons, addPolygon, removePolygon }) { ); } -export default function PolygonZoneSelector() { +export default function PolygonZoneSelector({zoneSettings, updateZoneSettings, applyZoneSettings}) { const defaultDuration = 10; - const [zones, setZones] = useState([]); const [polygons, setPolygons] = useState([]); - const {zoneSettings, changeZoneSettings} = useAdmin(); const {penaltySettings, changePenaltySettings} = useAdmin(); const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState(""); useEffect(() => { - setPolygons(zones.map((zone) => zone.polygon)); - }, [zones]) + if (zoneSettings) { + const newPolygons = zoneSettings.polygons.map((zone) => ({id: idFromPolygon(zone.polygon), polygon: zone.polygon, duration: zone.duration})); + setPolygons(newPolygons.map((zone) => zone.polygon)); + } + }, [zoneSettings]); useEffect(() => { - if (zoneSettings) { - setZones(zoneSettings.map((zone) => ({id: idFromPolygon(zone.polygon), polygon: zone.polygon, duration: zone.duration}))); - } if (penaltySettings) { setAllowedTimeOutOfZone(penaltySettings.allowedTimeOutOfZone.toString()); } - }, [zoneSettings, penaltySettings]); + }, [penaltySettings]); 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 addPolygon(polygon) { - setZones([...zones, {id: idFromPolygon(polygon), polygon: polygon, duration: defaultDuration}]); + const newPolygons = [...zoneSettings.polygons, {id: idFromPolygon(polygon), polygon: polygon, duration: defaultDuration}]; + updateZoneSettings("polygons", newPolygons); } function removePolygon(i) { - setZones(zones.filter((_, index) => index !== i)); + const newPolygons = zoneSettings.polygons.filter((_, index) => index !== i); + updateZoneSettings("polygons", newPolygons); } function updateDuration(i, duration) { - setZones(zones.map((zone, index) => index === i ? {id: zone.id, polygon: zone.polygon, duration: duration} : zone)); + const newPolygons = zoneSettings.polygons.map((zone, index) => index === i ? {id: zone.id, polygon: zone.polygon, duration: duration} : zone); + updateZoneSettings("polygons", newPolygons); } function handleSettingsSubmit() { - changeZoneSettings(zones); + applyZoneSettings(); changePenaltySettings({allowedTimeOutOfZone: Number(allowedTimeOutOfZone)}); } @@ -147,7 +148,7 @@ export default function PolygonZoneSelector() {

Reduction order

- + updateZoneSettings("polygons", polygons)}> { (zone, i) =>

Zone {i+1}

diff --git a/traque-front/app/admin/parameters/page.js b/traque-front/app/admin/parameters/page.js index 11fe72a..e5f30e1 100644 --- a/traque-front/app/admin/parameters/page.js +++ b/traque-front/app/admin/parameters/page.js @@ -4,6 +4,7 @@ import dynamic from "next/dynamic"; import Link from "next/link"; import { TextInput } from "@/components/input"; import { Section } from "@/components/section"; +import { BlueButton } from "@/components/button"; import { useAdminConnexion } from "@/context/adminConnexionContext"; import useAdmin from '@/hook/useAdmin'; import Messages from "./components/messages"; @@ -13,17 +14,20 @@ import TeamManager from './components/teamManager'; const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false }); const CircleZoneSelector = dynamic(() => import('./components/circleZoneSelector'), { ssr: false }); -const zoneSelectors = { +const zoneTypes = { circle: "circle", polygon: "polygon" } +const defaultCircleSettings = {type: zoneTypes.circle, min: null, max: null, reductionCount: 4, duration: 10} +const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []} + export default function ConfigurationPage() { - const {penaltySettings, changePenaltySettings, addTeam} = useAdmin(); + const {zoneSettings, changeZoneSettings, penaltySettings, changePenaltySettings, addTeam} = useAdmin(); const { useProtect } = useAdminConnexion(); const [allowedTimeBetweenUpdates, setAllowedTimeBetweenUpdates] = useState(""); const [teamName, setTeamName] = useState(''); - const [zoneSelector, setZoneSelector] = useState(zoneSelectors.polygon); + const [localZoneSettings, setLocalZoneSettings] = useState(zoneSettings); useProtect(); @@ -32,12 +36,26 @@ export default function ConfigurationPage() { setAllowedTimeBetweenUpdates(penaltySettings.allowedTimeBetweenPositionUpdate.toString()); } }, [penaltySettings]); + + useEffect(() => { + if (zoneSettings) { + setLocalZoneSettings(zoneSettings); + } + }, [zoneSettings]); + + function updateLocalZoneSettings(key, value) { + setLocalZoneSettings(prev => ({...prev, [key]: value})); + }; function applySettings() { if (Number(allowedTimeBetweenUpdates) != penaltySettings.allowedTimeBetweenPositionUpdate) { changePenaltySettings({allowedTimeBetweenPositionUpdate: Number(allowedTimeBetweenUpdates)}); } } + + function handleChangeZoneType() { + setLocalZoneSettings(localZoneSettings.type == zoneTypes.circle ? defaultPolygonSettings : defaultCircleSettings) + } function handleSubmit(e) { e.preventDefault(); @@ -77,9 +95,18 @@ export default function ConfigurationPage() {
-
- {zoneSelector == zoneSelectors.circle && } - {zoneSelector == zoneSelectors.polygon && } +
+
+ {localZoneSettings && Change zone type} +
+
+ {localZoneSettings && localZoneSettings.type == zoneTypes.circle && + changeZoneSettings(localZoneSettings)}/> + } + {localZoneSettings && localZoneSettings.type == zoneTypes.polygon && + changeZoneSettings(localZoneSettings)}/> + } +
); diff --git a/traque-front/components/list.jsx b/traque-front/components/list.jsx index 40fc9f1..c987331 100644 --- a/traque-front/components/list.jsx +++ b/traque-front/components/list.jsx @@ -2,12 +2,11 @@ import { useEffect, useState } from 'react'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; export function List({array, children}) { - // TODO : change key return (
    {array.map((elem, i) => ( -
  • +
  • {children(elem, i)}