Number of zones
setReductionCount(e.target.value)}>
diff --git a/traque-front/app/admin/parameters/components/polygonZoneMap.jsx b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx
similarity index 56%
rename from traque-front/app/admin/parameters/components/polygonZoneMap.jsx
rename to traque-front/app/admin/parameters/components/polygonZoneSelector.jsx
index c1b3382..74fe16e 100644
--- a/traque-front/app/admin/parameters/components/polygonZoneMap.jsx
+++ b/traque-front/app/admin/parameters/components/polygonZoneSelector.jsx
@@ -1,16 +1,14 @@
import { useEffect, useState } from "react";
-import { MapContainer, TileLayer, Polyline, Polygon, CircleMarker } from "react-leaflet";
+import { Polyline, Polygon, CircleMarker, Marker } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { GreenButton } from "@/components/button";
-import { TextInput } from "@/components/textInput";
-import { MapPan, MapEventListener } from "@/components/mapUtils";
+import { ReorderList } from "@/components/list";
+import { CustomMapContainer, MapEventListener } from "@/components/map";
+import { TextInput } from "@/components/input";
import useAdmin from "@/hook/useAdmin";
-import useLocation from "@/hook/useLocation";
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
-const DEFAULT_ZOOM = 14;
-
-function PolygonDrawings({ polygons, addPolygon, removePolygon }) {
+function Drawings({ polygons, addPolygon, removePolygon }) {
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
const nodeSize = 5; // px
const lineThickness = 3; // px
@@ -45,110 +43,120 @@ function PolygonDrawings({ polygons, addPolygon, removePolygon }) {
}
}
- function DrawPolygon({polygon}) {
+ function DrawPolygon({polygon, number}) {
const length = polygon.length;
- if (length > 2) {
- return (
+ 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) => )}
+ {polygons.map((polygon, i) => )}
{highlightNodes.map((node, i) => )}
);
}
-function PolygonZonePicker({ polygons, addPolygon, removePolygon, ...props }) {
- const location = useLocation(Infinity);
-
- return (
-
- );
-}
-
-export default function PolygonZoneMap() {
+export default function PolygonZoneSelector() {
const defaultDuration = 10;
+ const [zones, setZones] = useState([]);
const [polygons, setPolygons] = useState([]);
- const [durations, setDurations] = useState([]);
const {zoneSettings, changeZoneSettings} = useAdmin();
const {penaltySettings, changePenaltySettings} = useAdmin();
const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState("");
+ useEffect(() => {
+ setPolygons(zones.map((zone) => zone.polygon));
+ }, [zones])
+
useEffect(() => {
if (zoneSettings) {
- setPolygons(zoneSettings.polygons);
- setDurations(zoneSettings.durations);
+ setZones(zoneSettings.map((zone) => ({id: idFromPolygon(zone.polygon), polygon: zone.polygon, duration: zone.duration})));
}
if (penaltySettings) {
setAllowedTimeOutOfZone(penaltySettings.allowedTimeOutOfZone.toString());
}
}, [zoneSettings, 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) {
- // Polygons
- setPolygons([...polygons, polygon]);
- // Durations
- setDurations([...durations, defaultDuration]);
+ setZones([...zones, {id: idFromPolygon(polygon), polygon: polygon, duration: defaultDuration}]);
}
function removePolygon(i) {
- // Polygons
- const newPolygons = [...polygons];
- newPolygons.splice(i, 1);
- setPolygons(newPolygons);
- // Durations
- const newDurations = [...durations];
- newDurations.splice(i, 1);
- setDurations(newDurations);
+ setZones(zones.filter((_, index) => index !== i));
}
function updateDuration(i, duration) {
- const newDurations = [...durations];
- newDurations[i] = duration;
- setDurations(newDurations);
+ setZones(zones.map((zone, index) => index === i ? {id: zone.id, polygon: zone.polygon, duration: duration} : zone));
}
function handleSettingsSubmit() {
- const newSettings = {polygons: polygons, durations: durations};
- changeZoneSettings(newSettings);
+ changeZoneSettings(zones);
changePenaltySettings({allowedTimeOutOfZone: Number(allowedTimeOutOfZone)});
}
return (
-
-
+
+
+
+
Reduction order
-
- {durations.map((duration, i) => (
- -
+
+ { (zone, i) =>
+
Zone {i+1}
- updateDuration(i, e.target.value)}/>
+ updateDuration(i, e.target.value)}/>
-
- ))}
-
+
+ }
+
Timeout
diff --git a/traque-front/app/admin/parameters/components/teamManager.jsx b/traque-front/app/admin/parameters/components/teamManager.jsx
index bc39377..709ee16 100644
--- a/traque-front/app/admin/parameters/components/teamManager.jsx
+++ b/traque-front/app/admin/parameters/components/teamManager.jsx
@@ -1,15 +1,7 @@
-import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
-import { List } from '@/components/list';
+import { ReorderList } from '@/components/list';
import useAdmin from '@/hook/useAdmin';
-function reorder(list, startIndex, endIndex) {
- const result = Array.from(list);
- const [removed] = result.splice(startIndex, 1);
- result.splice(endIndex, 0, removed);
- return result;
-};
-
-function TeamManagerItem({ team, index }) {
+function TeamManagerItem({ team }) {
const { updateTeam, removeTeam } = useAdmin();
function handleRemove() {
@@ -17,47 +9,27 @@ function TeamManagerItem({ team, index }) {
}
return (
-
- {provided => (
-
-
-
{team.name}
-
-
{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}
-

updateTeam(team.id, { captured: !team.captured })} />
-

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

updateTeam(team.id, { captured: !team.captured })} />
+
- )}
-
+
+
);
}
export default function TeamManager() {
const { teams, reorderTeams } = useAdmin();
-
- function onDragEnd(result) {
- if (!result.destination) return;
- if (result.destination.index === result.source.index) return;
- const newTeams = reorder(teams, result.source.index, result.destination.index);
- reorderTeams(newTeams);
- }
return (
-
-
- {provided => (
-
-
- {(team, i) => (
-
- )}
-
- {provided.placeholder}
-
- )}
-
-
+
+ {(team) => (
+
+ )}
+
);
}
diff --git a/traque-front/app/admin/parameters/page.js b/traque-front/app/admin/parameters/page.js
index d5b0f5f..11fe72a 100644
--- a/traque-front/app/admin/parameters/page.js
+++ b/traque-front/app/admin/parameters/page.js
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
import dynamic from "next/dynamic";
import Link from "next/link";
-import { TextInput } from "@/components/textInput";
+import { TextInput } from "@/components/input";
import { Section } from "@/components/section";
import { useAdminConnexion } from "@/context/adminConnexionContext";
import useAdmin from '@/hook/useAdmin';
@@ -10,13 +10,20 @@ import Messages from "./components/messages";
import TeamManager from './components/teamManager';
// Imported at runtime and not at compile time
-const ZoneSelector = dynamic(() => import('./components/polygonZoneMap'), { ssr: false });
+const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false });
+const CircleZoneSelector = dynamic(() => import('./components/circleZoneSelector'), { ssr: false });
-export default function AdminPage() {
+const zoneSelectors = {
+ circle: "circle",
+ polygon: "polygon"
+}
+
+export default function ConfigurationPage() {
const {penaltySettings, changePenaltySettings, addTeam} = useAdmin();
const { useProtect } = useAdminConnexion();
const [allowedTimeBetweenUpdates, setAllowedTimeBetweenUpdates] = useState("");
const [teamName, setTeamName] = useState('');
+ const [zoneSelector, setZoneSelector] = useState(zoneSelectors.polygon);
useProtect();
@@ -71,7 +78,8 @@ export default function AdminPage() {
-
+ {zoneSelector == zoneSelectors.circle &&
}
+ {zoneSelector == zoneSelectors.polygon &&
}
);
diff --git a/traque-front/app/team/components/loginForm.jsx b/traque-front/app/team/components/loginForm.jsx
index da40b9e..6688c8a 100644
--- a/traque-front/app/team/components/loginForm.jsx
+++ b/traque-front/app/team/components/loginForm.jsx
@@ -1,6 +1,6 @@
import { useState } from "react";
import { BlueButton } from "@/components/button";
-import { TextInput } from "@/components/textInput";
+import { TextInput } from "@/components/input";
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
const [value, setValue] = useState("");
diff --git a/traque-front/app/team/track/components/actionDrawer.jsx b/traque-front/app/team/track/components/actionDrawer.jsx
index 9fe90dd..04491fb 100644
--- a/traque-front/app/team/track/components/actionDrawer.jsx
+++ b/traque-front/app/team/track/components/actionDrawer.jsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"
import { BlueButton, GreenButton } from "@/components/button";
-import { TextInput } from "@/components/textInput";
+import { TextInput } from "@/components/input";
import useTeamConnexion from "@/context/teamConnexionContext";
import useGame from "@/hook/useGame";
import EnemyTeamModal from "./enemyTeamModal";
diff --git a/traque-front/components/textInput.jsx b/traque-front/components/input.jsx
similarity index 100%
rename from traque-front/components/textInput.jsx
rename to traque-front/components/input.jsx
diff --git a/traque-front/components/list.jsx b/traque-front/components/list.jsx
index 8f26c0d..40fc9f1 100644
--- a/traque-front/components/list.jsx
+++ b/traque-front/components/list.jsx
@@ -1,14 +1,67 @@
+import { useEffect, useState } from 'react';
+import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
+
export function List({array, children}) {
- // The elements of array have to be identified by a field id
+ // TODO : change key
return (
-
-
+
+
{array.map((elem, i) => (
- -
+
-
{children(elem, i)}
+
))}
);
}
+
+export function ReorderList({droppableId, array, setArray, children}) {
+ const [arrayLocal, setArrayLocal] = useState(array);
+
+ useEffect(() => {
+ setArrayLocal(array);
+ }, [array])
+
+ function reorder(list, startIndex, endIndex) {
+ const result = Array.from(list);
+ const [removed] = result.splice(startIndex, 1);
+ result.splice(endIndex, 0, removed);
+ return result;
+ };
+
+ function onDragEnd(result) {
+ if (!result.destination) return;
+ if (result.destination.index === result.source.index) return;
+ const newArray = reorder(array, result.source.index, result.destination.index);
+ setArrayLocal(newArray);
+ setArray(newArray);
+ }
+
+ return (
+
+
+ {provided => (
+
+
+ {arrayLocal.map((elem, i) => (
+ -
+
+ {provided => (
+
+ {children(elem, i)}
+
+
+ )}
+
+
+ ))}
+
+ {provided.placeholder}
+
+ )}
+
+
+ );
+}
diff --git a/traque-front/components/mapUtils.jsx b/traque-front/components/map.jsx
similarity index 52%
rename from traque-front/components/mapUtils.jsx
rename to traque-front/components/map.jsx
index 9a1c0d7..21130da 100644
--- a/traque-front/components/mapUtils.jsx
+++ b/traque-front/components/map.jsx
@@ -1,17 +1,28 @@
import { useEffect, useState } from "react";
-import { useMap } from "react-leaflet";
+import { MapContainer, TileLayer, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
-export function MapPan(props) {
+const DEFAULT_ZOOM = 14;
+
+const mapStyles = {
+ default: {
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ attribution: '© OpenStreetMap'
+ },
+ satellite: {
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
+ attribution: 'Tiles © Esri'
+ },
+}
+
+export function MapPan({center, zoom}) {
const map = useMap();
- const [initialized, setInitialized] = useState(false);
useEffect(() => {
- if (!initialized && props.center) {
- map.flyTo(props.center, props.zoom, { animate: false });
- setInitialized(true)
+ if (center, zoom) {
+ map.flyTo(center, zoom, { animate: false });
}
- }, [props.center]);
+ }, [center, zoom]);
return null;
}
@@ -21,6 +32,8 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
// Handle the mouse click left
useEffect(() => {
+ if (!onLeftClick) return;
+
let moved = false;
let downButton = null;
@@ -55,6 +68,7 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
// Handle the right click
useEffect(() => {
+ if (!onRightClick) return;
const handleMouseDown = (e) => {
if (e.originalEvent.button == 2) {
@@ -71,6 +85,8 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
// Handle the mouse move
useEffect(() => {
+ if (!onMouseMove) return;
+
map.on('mousemove', onMouseMove);
return () => {
@@ -85,4 +101,42 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
container.addEventListener('contextmenu', preventContextMenu);
return () => container.removeEventListener('contextmenu', preventContextMenu);
}, []);
+
+ return null;
+}
+
+export function CustomMapContainer({mapStyle, children}) {
+ const [location, setLocation] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!navigator.geolocation) {
+ console.log('Geolocation not supported');
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ (pos) => {
+ setLocation([pos.coords.latitude, pos.coords.longitude]);
+ setLoading(false);
+ },
+ (err) => console.log("Error :", err),
+ {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ maximumAge: 0
+ }
+ );
+ }, []);
+
+ if (loading) {
+ return
+ }
+
+ return (
+
+
+ {children}
+
+ )
}
diff --git a/traque-front/hook/useLocation.jsx b/traque-front/hook/useLocation.jsx
index 9e4e816..acc51c8 100644
--- a/traque-front/hook/useLocation.jsx
+++ b/traque-front/hook/useLocation.jsx
@@ -2,23 +2,37 @@
import { useEffect, useState } from "react";
export default function useLocation(interval) {
- const [location, setLocation] = useState();
+ const [location, setLocation] = useState(null);
useEffect(() => {
- function update() {
- navigator.geolocation.getCurrentPosition(
- (position) => {
- setLocation([position.coords.latitude, position.coords.longitude]);
- if(interval != Infinity) {
- setTimeout(update, interval);
- }
- },
- () => { },
- { enableHighAccuracy: true, timeout: Infinity, maximumAge: 0 }
- );
+ if (!navigator.geolocation) {
+ console.log('Geolocation not supported');
+ return;
+ }
+
+ if (interval < 1000 || interval == Infinity) {
+ console.log('Localisation interval no supported');
+ return;
}
- update();
+ const watchId = navigator.geolocation.watchPosition(
+ (pos) => {
+ setLocation({
+ lat: pos.coords.latitude,
+ lng: pos.coords.longitude,
+ accuracy: pos.coords.accuracy,
+ timestamp: pos.timestamp
+ });
+ },
+ (err) => console.log("Error :", err),
+ {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ maximumAge: interval,
+ }
+ );
+
+ return () => navigator.geolocation.clearWatch(watchId);
}, []);
return location;