Improvements on maps

This commit is contained in:
Sebastien Riviere
2025-08-31 17:33:18 +02:00
parent e5d90d824e
commit d088253758
13 changed files with 264 additions and 184 deletions

View File

@@ -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) {

View File

@@ -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>
) )
} }

View File

@@ -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("");

View File

@@ -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='&copy; <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>

View File

@@ -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='&copy; <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">

View File

@@ -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>
); );
} }

View File

@@ -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>
); );

View File

@@ -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("");

View File

@@ -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";

View File

@@ -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>
);
}

View File

@@ -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: '&copy; <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 &copy; 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>
)
} }

View File

@@ -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);
}
},
() => { },
{ enableHighAccuracy: true, timeout: Infinity, maximumAge: 0 }
);
} }
update(); if (interval < 1000 || interval == Infinity) {
console.log('Localisation interval no supported');
return;
}
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;