mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 02:10:18 +01:00
Improvements on maps
This commit is contained in:
@@ -22,7 +22,7 @@ function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
|||||||
|
|
||||||
/* -------------------------------- Polygon zones -------------------------------- */
|
/* -------------------------------- Polygon zones -------------------------------- */
|
||||||
|
|
||||||
const defaultPolygonSettings = { polygons: [], durations: [] };
|
const defaultPolygonSettings = [];
|
||||||
|
|
||||||
function polygonZone(points, duration) {
|
function polygonZone(points, duration) {
|
||||||
return {
|
return {
|
||||||
@@ -82,15 +82,9 @@ function mergePolygons(poly1, poly2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function polygonSettingsToZones(settings) {
|
function polygonSettingsToZones(settings) {
|
||||||
const { polygons, durations } = settings;
|
|
||||||
const reversedPolygons = polygons.slice().reverse();
|
|
||||||
const reversedDurations = durations.slice().reverse();
|
|
||||||
|
|
||||||
const zones = [];
|
const zones = [];
|
||||||
|
|
||||||
for (let i = 0; i < reversedPolygons.length; i++) {
|
for (const { polygon, duration } of settings.slice().reverse()) {
|
||||||
const polygon =reversedPolygons[i];
|
|
||||||
const duration = reversedDurations[i];
|
|
||||||
const length = zones.length;
|
const length = zones.length;
|
||||||
|
|
||||||
if (length == 0) {
|
if (length == 0) {
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { MapContainer, Marker, TileLayer, Tooltip, Polyline, Polygon } from "react-leaflet";
|
import { Marker, Tooltip, Polyline, Polygon } from "react-leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { MapPan } from "@/components/mapUtils";
|
import { CustomMapContainer } from "@/components/map";
|
||||||
import useLocation from "@/hook/useLocation";
|
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import { GameState } from "@/util/gameState";
|
import { GameState } from "@/util/gameState";
|
||||||
|
|
||||||
const DEFAULT_ZOOM = 14;
|
|
||||||
|
|
||||||
const positionIcon = new L.Icon({
|
const positionIcon = new L.Icon({
|
||||||
iconUrl: '/icons/marker/blue.png',
|
iconUrl: '/icons/marker/blue.png',
|
||||||
iconSize: [30, 30],
|
iconSize: [30, 30],
|
||||||
@@ -17,7 +14,6 @@ const positionIcon = new L.Icon({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
||||||
const location = useLocation(Infinity);
|
|
||||||
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
||||||
const { zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
const { zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
||||||
|
|
||||||
@@ -56,9 +52,7 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
|||||||
return (
|
return (
|
||||||
<div className='h-full w-full flex flex-col'>
|
<div className='h-full w-full flex flex-col'>
|
||||||
{gameState == GameState.PLAYING && <p>{`Next zone in : ${formatTime(timeLeftNextZone)}`}</p>}
|
{gameState == GameState.PLAYING && <p>{`Next zone in : ${formatTime(timeLeftNextZone)}`}</p>}
|
||||||
<MapContainer className='flex-1 w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
<CustomMapContainer mapStyle={mapStyle}>
|
||||||
<TileLayer url={mapStyle.url} attribution={mapStyle.attribution}/>
|
|
||||||
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
|
||||||
{showZones && gameState == GameState.PLAYING && zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
{showZones && gameState == GameState.PLAYING && zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
||||||
{showZones && gameState == GameState.PLAYING && zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
|
{showZones && gameState == GameState.PLAYING && zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
|
||||||
{teams.map((team) => team.currentLocation && !team.captured &&
|
{teams.map((team) => team.currentLocation && !team.captured &&
|
||||||
@@ -67,7 +61,7 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
|||||||
{showArrows && <Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>}
|
{showArrows && <Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>}
|
||||||
</Marker>
|
</Marker>
|
||||||
)}
|
)}
|
||||||
</MapContainer>
|
</CustomMapContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BlueButton } from "@/components/button";
|
import { BlueButton } from "@/components/button";
|
||||||
import { TextInput } from "@/components/textInput";
|
import { TextInput } from "@/components/input";
|
||||||
|
|
||||||
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Circle, MapContainer, TileLayer } from "react-leaflet";
|
import { Circle } from "react-leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { BlueButton, GreenButton, RedButton } from "@/components/button";
|
import { BlueButton, GreenButton, RedButton } from "@/components/button";
|
||||||
import { TextInput } from "@/components/textInput";
|
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
||||||
import { MapPan, MapEventListener } from "@/components/mapUtils";
|
import { TextInput } from "@/components/input";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import useLocation from "@/hook/useLocation";
|
|
||||||
import useMapCircleDraw from "@/hook/useMapCircleDraw";
|
import useMapCircleDraw from "@/hook/useMapCircleDraw";
|
||||||
|
|
||||||
const DEFAULT_ZOOM = 14;
|
|
||||||
const EditMode = {
|
const EditMode = {
|
||||||
MIN: 0,
|
MIN: 0,
|
||||||
MAX: 1
|
MAX: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function CircleDrawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
||||||
const { center: maxCenter, radius: maxRadius, handleLeftClick: maxLeftClick, handleRightClick: maxRightClick, handleMouseMove: maxHover } = useMapCircleDraw(maxZone, setMaxZone);
|
const { center: maxCenter, radius: maxRadius, handleLeftClick: maxLeftClick, handleRightClick: maxRightClick, handleMouseMove: maxHover } = useMapCircleDraw(maxZone, setMaxZone);
|
||||||
const { center: minCenter, radius: minRadius, handleLeftClick: minLeftClick, handleRightClick: minRightClick, handleMouseMove: minHover } = useMapCircleDraw(minZone, setMinZone);
|
const { center: minCenter, radius: minRadius, handleLeftClick: minLeftClick, handleRightClick: minRightClick, handleMouseMove: minHover } = useMapCircleDraw(minZone, setMinZone);
|
||||||
|
|
||||||
@@ -44,31 +42,14 @@ function CircleDrawings({ minZone, setMinZone, maxZone, setMaxZone, editMode })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove}/>
|
||||||
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="blue" fillColor="blue" />}
|
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="blue" fillColor="blue" />}
|
||||||
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="red" fillColor="red" />}
|
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="red" fillColor="red" />}
|
||||||
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CircleZonePicker({ minZone, maxZone, editMode, setMinZone, setMaxZone, ...props }) {
|
export default function CircleZoneSelector() {
|
||||||
const location = useLocation(Infinity);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='h-96'>
|
|
||||||
<MapContainer {...props} className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
/>
|
|
||||||
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
|
||||||
<CircleDrawings minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
|
|
||||||
</MapContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CircleZoneMap() {
|
|
||||||
const [editMode, setEditMode] = useState(EditMode.MIN);
|
const [editMode, setEditMode] = useState(EditMode.MIN);
|
||||||
const [minZone, setMinZone] = useState(null);
|
const [minZone, setMinZone] = useState(null);
|
||||||
const [maxZone, setMaxZone] = useState(null);
|
const [maxZone, setMaxZone] = useState(null);
|
||||||
@@ -85,11 +66,6 @@ export default function CircleZoneMap() {
|
|||||||
}
|
}
|
||||||
}, [zoneSettings]);
|
}, [zoneSettings]);
|
||||||
|
|
||||||
function handleSettingsSubmit() {
|
|
||||||
const newSettings = {min:minZone, max:maxZone, reductionCount: Number(reductionCount), duration: Number(duration)};
|
|
||||||
changeZoneSettings(newSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the user set one zone, switch to the other
|
// When the user set one zone, switch to the other
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(editMode == EditMode.MIN) {
|
if(editMode == EditMode.MIN) {
|
||||||
@@ -100,12 +76,19 @@ export default function CircleZoneMap() {
|
|||||||
|
|
||||||
}, [minZone, maxZone]);
|
}, [minZone, maxZone]);
|
||||||
|
|
||||||
|
function handleSettingsSubmit() {
|
||||||
|
const newSettings = {min:minZone, max:maxZone, reductionCount: Number(reductionCount), duration: Number(duration)};
|
||||||
|
changeZoneSettings(newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
|
<div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
|
||||||
<h2 className="text-2xl">Edit zones</h2>
|
<h2 className="text-2xl">Edit zones</h2>
|
||||||
{editMode == EditMode.MIN && <BlueButton onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</BlueButton>}
|
{editMode == EditMode.MIN && <BlueButton onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</BlueButton>}
|
||||||
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>}
|
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>}
|
||||||
<CircleZonePicker minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
|
<CustomMapContainer>
|
||||||
|
<Drawings minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
|
||||||
|
</CustomMapContainer>
|
||||||
<div>
|
<div>
|
||||||
<p>Number of zones</p>
|
<p>Number of zones</p>
|
||||||
<TextInput value={reductionCount} onChange={(e) => setReductionCount(e.target.value)}></TextInput>
|
<TextInput value={reductionCount} onChange={(e) => setReductionCount(e.target.value)}></TextInput>
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 "leaflet/dist/leaflet.css";
|
||||||
import { GreenButton } from "@/components/button";
|
import { GreenButton } from "@/components/button";
|
||||||
import { TextInput } from "@/components/textInput";
|
import { ReorderList } from "@/components/list";
|
||||||
import { MapPan, MapEventListener } from "@/components/mapUtils";
|
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
||||||
|
import { TextInput } from "@/components/input";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import useLocation from "@/hook/useLocation";
|
|
||||||
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
|
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
|
||||||
|
|
||||||
const DEFAULT_ZOOM = 14;
|
function Drawings({ polygons, addPolygon, removePolygon }) {
|
||||||
|
|
||||||
function PolygonDrawings({ polygons, addPolygon, removePolygon }) {
|
|
||||||
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
|
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
|
||||||
const nodeSize = 5; // px
|
const nodeSize = 5; // px
|
||||||
const lineThickness = 3; // 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;
|
const length = polygon.length;
|
||||||
|
|
||||||
if (length > 2) {
|
if (length < 3) return null;
|
||||||
return (
|
|
||||||
|
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: `<div style="
|
||||||
|
font-size: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 25px;
|
||||||
|
">${number}</div>`,
|
||||||
|
className: 'custom-number-icon',
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 15]
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
<Polygon positions={polygon} pathOptions={{ color: 'black', fillColor: 'black', fillOpacity: '0.5', weight: lineThickness }} />
|
<Polygon positions={polygon} pathOptions={{ color: 'black', fillColor: 'black', fillOpacity: '0.5', weight: lineThickness }} />
|
||||||
);
|
<Marker position={meanPoint} icon={numberIcon} />
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
||||||
{polygons.map((polygon, i) => <DrawPolygon key={i} polygon={polygon} />)}
|
{polygons.map((polygon, i) => <DrawPolygon key={i} polygon={polygon} number={i+1} />)}
|
||||||
<DrawUnfinishedPolygon polygon={currentPolygon} />
|
<DrawUnfinishedPolygon polygon={currentPolygon} />
|
||||||
{highlightNodes.map((node, i) => <DrawNode key={i} pos={node} color={"black"} />)}
|
{highlightNodes.map((node, i) => <DrawNode key={i} pos={node} color={"black"} />)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PolygonZonePicker({ polygons, addPolygon, removePolygon, ...props }) {
|
export default function PolygonZoneSelector() {
|
||||||
const location = useLocation(Infinity);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='h-full'>
|
|
||||||
<MapContainer {...props} className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
/>
|
|
||||||
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
|
||||||
<PolygonDrawings polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
|
|
||||||
</MapContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PolygonZoneMap() {
|
|
||||||
const defaultDuration = 10;
|
const defaultDuration = 10;
|
||||||
|
const [zones, setZones] = useState([]);
|
||||||
const [polygons, setPolygons] = useState([]);
|
const [polygons, setPolygons] = useState([]);
|
||||||
const [durations, setDurations] = useState([]);
|
|
||||||
const {zoneSettings, changeZoneSettings} = useAdmin();
|
const {zoneSettings, changeZoneSettings} = useAdmin();
|
||||||
const {penaltySettings, changePenaltySettings} = useAdmin();
|
const {penaltySettings, changePenaltySettings} = useAdmin();
|
||||||
const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState("");
|
const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPolygons(zones.map((zone) => zone.polygon));
|
||||||
|
}, [zones])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (zoneSettings) {
|
if (zoneSettings) {
|
||||||
setPolygons(zoneSettings.polygons);
|
setZones(zoneSettings.map((zone) => ({id: idFromPolygon(zone.polygon), polygon: zone.polygon, duration: zone.duration})));
|
||||||
setDurations(zoneSettings.durations);
|
|
||||||
}
|
}
|
||||||
if (penaltySettings) {
|
if (penaltySettings) {
|
||||||
setAllowedTimeOutOfZone(penaltySettings.allowedTimeOutOfZone.toString());
|
setAllowedTimeOutOfZone(penaltySettings.allowedTimeOutOfZone.toString());
|
||||||
}
|
}
|
||||||
}, [zoneSettings, penaltySettings]);
|
}, [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) {
|
function addPolygon(polygon) {
|
||||||
// Polygons
|
setZones([...zones, {id: idFromPolygon(polygon), polygon: polygon, duration: defaultDuration}]);
|
||||||
setPolygons([...polygons, polygon]);
|
|
||||||
// Durations
|
|
||||||
setDurations([...durations, defaultDuration]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePolygon(i) {
|
function removePolygon(i) {
|
||||||
// Polygons
|
setZones(zones.filter((_, index) => index !== i));
|
||||||
const newPolygons = [...polygons];
|
|
||||||
newPolygons.splice(i, 1);
|
|
||||||
setPolygons(newPolygons);
|
|
||||||
// Durations
|
|
||||||
const newDurations = [...durations];
|
|
||||||
newDurations.splice(i, 1);
|
|
||||||
setDurations(newDurations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDuration(i, duration) {
|
function updateDuration(i, duration) {
|
||||||
const newDurations = [...durations];
|
setZones(zones.map((zone, index) => index === i ? {id: zone.id, polygon: zone.polygon, duration: duration} : zone));
|
||||||
newDurations[i] = duration;
|
|
||||||
setDurations(newDurations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSettingsSubmit() {
|
function handleSettingsSubmit() {
|
||||||
const newSettings = {polygons: polygons, durations: durations};
|
changeZoneSettings(zones);
|
||||||
changeZoneSettings(newSettings);
|
|
||||||
changePenaltySettings({allowedTimeOutOfZone: Number(allowedTimeOutOfZone)});
|
changePenaltySettings({allowedTimeOutOfZone: Number(allowedTimeOutOfZone)});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full bg-white p-3 gap-3 flex flex-row shadow-2xl'>
|
<div className='h-full w-full bg-white p-3 gap-3 flex flex-row shadow-2xl'>
|
||||||
<div className="h-full w-full">
|
<div className="h-full flex-1">
|
||||||
<PolygonZonePicker polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
|
<CustomMapContainer>
|
||||||
|
<Drawings polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
|
||||||
|
</CustomMapContainer>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-1/6 flex flex-col gap-3">
|
<div className="h-full w-1/6 flex flex-col gap-3">
|
||||||
<div className="w-full text-center">
|
<div className="w-full text-center">
|
||||||
<h2 className="text-xl">Reduction order</h2>
|
<h2 className="text-xl">Reduction order</h2>
|
||||||
</div>
|
</div>
|
||||||
<ul className="w-full h-full bg-gray-300">
|
<ReorderList droppableId="zones-order" array={zones} setArray={setZones}>
|
||||||
{durations.map((duration, i) => (
|
{ (zone, i) =>
|
||||||
<li key={i} className="w-full bg-white flex flex-row gap-2 items-center justify-between p-1">
|
<div className="w-full p-2 bg-white flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Zone {i+1}</p>
|
<p>Zone {i+1}</p>
|
||||||
<div className="w-16 h-10">
|
<div className="w-16 h-10">
|
||||||
<TextInput value={duration} onChange={(e) => updateDuration(i, e.target.value)}/>
|
<TextInput value={zone.duration} onChange={(e) => updateDuration(i, e.target.value)}/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
}
|
||||||
</ul>
|
</ReorderList>
|
||||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Timeout</p>
|
<p>Timeout</p>
|
||||||
<div className="w-16 h-10">
|
<div className="w-16 h-10">
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
import { ReorderList } from '@/components/list';
|
||||||
import { List } from '@/components/list';
|
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
|
|
||||||
function reorder(list, startIndex, endIndex) {
|
function TeamManagerItem({ team }) {
|
||||||
const result = Array.from(list);
|
|
||||||
const [removed] = result.splice(startIndex, 1);
|
|
||||||
result.splice(endIndex, 0, removed);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
function TeamManagerItem({ team, index }) {
|
|
||||||
const { updateTeam, removeTeam } = useAdmin();
|
const { updateTeam, removeTeam } = useAdmin();
|
||||||
|
|
||||||
function handleRemove() {
|
function handleRemove() {
|
||||||
@@ -17,47 +9,27 @@ function TeamManagerItem({ team, index }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={team.id.toString()} index={index}>
|
<div className='w-full p-2 bg-white flex flex-row items-center text-xl gap-3 font-bold'>
|
||||||
{provided => (
|
<div className='flex-1 w-full h-full flex flex-row items-center justify-between'>
|
||||||
<div className='w-full p-2 bg-white flex flex-row items-center text-xl gap-3 font-bold' {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
<p>{team.name}</p>
|
||||||
<div className='flex-1 w-full h-full flex flex-row items-center justify-between'>
|
<div className='flex flex-row items-center justify-between gap-3'>
|
||||||
<p>{team.name}</p>
|
<p>{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}</p>
|
||||||
<div className='flex flex-row items-center justify-between gap-3'>
|
<img src={`/icons/heart/${team.captured ? "grey" : "pink"}.png`} className="w-8 h-8" onClick={() => updateTeam(team.id, { captured: !team.captured })} />
|
||||||
<p>{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}</p>
|
<img src="/icons/trash.png" className="w-8 h-8" onClick={handleRemove} />
|
||||||
<img src={`/icons/heart/${team.captured ? "grey" : "pink"}.png`} className="w-8 h-8" onClick={() => updateTeam(team.id, { captured: !team.captured })} />
|
|
||||||
<img src="/icons/trash.png" className="w-8 h-8" onClick={handleRemove} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</Draggable>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamManager() {
|
export default function TeamManager() {
|
||||||
const { teams, reorderTeams } = useAdmin();
|
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 (
|
return (
|
||||||
<DragDropContext onDragEnd={onDragEnd} >
|
<ReorderList droppableId="team-manager" array={teams} setArray={reorderTeams}>
|
||||||
<Droppable droppableId='team-list'>
|
{(team) => (
|
||||||
{provided => (
|
<TeamManagerItem team={team}/>
|
||||||
<div className='w-full h-full' ref={provided.innerRef} {...provided.droppableProps}>
|
)}
|
||||||
<List array={teams}>
|
</ReorderList>
|
||||||
{(team, i) => (
|
|
||||||
<TeamManagerItem index={i} team={team}/>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
{provided.placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TextInput } from "@/components/textInput";
|
import { TextInput } from "@/components/input";
|
||||||
import { Section } from "@/components/section";
|
import { Section } from "@/components/section";
|
||||||
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
@@ -10,13 +10,20 @@ import Messages from "./components/messages";
|
|||||||
import TeamManager from './components/teamManager';
|
import TeamManager from './components/teamManager';
|
||||||
|
|
||||||
// Imported at runtime and not at compile time
|
// 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 {penaltySettings, changePenaltySettings, addTeam} = useAdmin();
|
||||||
const { useProtect } = useAdminConnexion();
|
const { useProtect } = useAdminConnexion();
|
||||||
const [allowedTimeBetweenUpdates, setAllowedTimeBetweenUpdates] = useState("");
|
const [allowedTimeBetweenUpdates, setAllowedTimeBetweenUpdates] = useState("");
|
||||||
const [teamName, setTeamName] = useState('');
|
const [teamName, setTeamName] = useState('');
|
||||||
|
const [zoneSelector, setZoneSelector] = useState(zoneSelectors.polygon);
|
||||||
|
|
||||||
useProtect();
|
useProtect();
|
||||||
|
|
||||||
@@ -71,7 +78,8 @@ export default function AdminPage() {
|
|||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full flex-1">
|
<div className="h-full flex-1">
|
||||||
<ZoneSelector />
|
{zoneSelector == zoneSelectors.circle && <CircleZoneSelector/>}
|
||||||
|
{zoneSelector == zoneSelectors.polygon && <PolygonZoneSelector/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BlueButton } from "@/components/button";
|
import { BlueButton } from "@/components/button";
|
||||||
import { TextInput } from "@/components/textInput";
|
import { TextInput } from "@/components/input";
|
||||||
|
|
||||||
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { BlueButton, GreenButton } from "@/components/button";
|
import { BlueButton, GreenButton } from "@/components/button";
|
||||||
import { TextInput } from "@/components/textInput";
|
import { TextInput } from "@/components/input";
|
||||||
import useTeamConnexion from "@/context/teamConnexionContext";
|
import useTeamConnexion from "@/context/teamConnexionContext";
|
||||||
import useGame from "@/hook/useGame";
|
import useGame from "@/hook/useGame";
|
||||||
import EnemyTeamModal from "./enemyTeamModal";
|
import EnemyTeamModal from "./enemyTeamModal";
|
||||||
|
|||||||
@@ -1,14 +1,67 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
||||||
|
|
||||||
export function List({array, children}) {
|
export function List({array, children}) {
|
||||||
// The elements of array have to be identified by a field id
|
// TODO : change key
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full bg-gray-300 overflow-y-auto'>
|
<div className='w-full h-full bg-gray-300 overflow-y-scroll'>
|
||||||
<ul className="w-full p-1 divide-y-4 divide-gray-300">
|
<ul className="w-full p-1 pb-0">
|
||||||
{array.map((elem, i) => (
|
{array.map((elem, i) => (
|
||||||
<li className="w-full" key={elem.id}>
|
<li className="w-full" key={elem.id ?? i}>
|
||||||
{children(elem, i)}
|
{children(elem, i)}
|
||||||
|
<div className="w-full h-1"/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<DragDropContext onDragEnd={onDragEnd} >
|
||||||
|
<Droppable droppableId={droppableId}>
|
||||||
|
{provided => (
|
||||||
|
<div className='w-full h-full bg-gray-300 overflow-y-scroll' ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
|
<ul className="w-full p-1 pb-0">
|
||||||
|
{arrayLocal.map((elem, i) => (
|
||||||
|
<li className='w-full' key={elem.id}>
|
||||||
|
<Draggable draggableId={elem.id.toString()} index={i}>
|
||||||
|
{provided => (
|
||||||
|
<div className='w-full' {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
||||||
|
{children(elem, i)}
|
||||||
|
<div className="w-full h-1"/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMap } from "react-leaflet";
|
import { MapContainer, TileLayer, useMap } from "react-leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
},
|
||||||
|
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 map = useMap();
|
||||||
const [initialized, setInitialized] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialized && props.center) {
|
if (center, zoom) {
|
||||||
map.flyTo(props.center, props.zoom, { animate: false });
|
map.flyTo(center, zoom, { animate: false });
|
||||||
setInitialized(true)
|
|
||||||
}
|
}
|
||||||
}, [props.center]);
|
}, [center, zoom]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -21,6 +32,8 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
|||||||
|
|
||||||
// Handle the mouse click left
|
// Handle the mouse click left
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!onLeftClick) return;
|
||||||
|
|
||||||
let moved = false;
|
let moved = false;
|
||||||
let downButton = null;
|
let downButton = null;
|
||||||
|
|
||||||
@@ -55,6 +68,7 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
|||||||
|
|
||||||
// Handle the right click
|
// Handle the right click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!onRightClick) return;
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
if (e.originalEvent.button == 2) {
|
if (e.originalEvent.button == 2) {
|
||||||
@@ -71,6 +85,8 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
|||||||
|
|
||||||
// Handle the mouse move
|
// Handle the mouse move
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!onMouseMove) return;
|
||||||
|
|
||||||
map.on('mousemove', onMouseMove);
|
map.on('mousemove', onMouseMove);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -85,4 +101,42 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
|||||||
container.addEventListener('contextmenu', preventContextMenu);
|
container.addEventListener('contextmenu', preventContextMenu);
|
||||||
return () => container.removeEventListener('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 <div className="w-full h-full"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapContainer className='w-full h-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
||||||
|
<TileLayer url={(mapStyle || mapStyles.default).url} attribution={(mapStyle || mapStyles.default).attribution}/>
|
||||||
|
{children}
|
||||||
|
</MapContainer>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -2,23 +2,37 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function useLocation(interval) {
|
export default function useLocation(interval) {
|
||||||
const [location, setLocation] = useState();
|
const [location, setLocation] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function update() {
|
if (!navigator.geolocation) {
|
||||||
navigator.geolocation.getCurrentPosition(
|
console.log('Geolocation not supported');
|
||||||
(position) => {
|
return;
|
||||||
setLocation([position.coords.latitude, position.coords.longitude]);
|
}
|
||||||
if(interval != Infinity) {
|
|
||||||
setTimeout(update, interval);
|
if (interval < 1000 || interval == Infinity) {
|
||||||
}
|
console.log('Localisation interval no supported');
|
||||||
},
|
return;
|
||||||
() => { },
|
|
||||||
{ enableHighAccuracy: true, timeout: Infinity, maximumAge: 0 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return location;
|
||||||
|
|||||||
Reference in New Issue
Block a user