mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 02:10:18 +01:00
Map resize correction + player focus + cleaning
This commit is contained in:
@@ -33,11 +33,11 @@
|
||||
- [x] Ajouter timer du rétrécissement des zones.
|
||||
- [x] Pouvoir changer les paramètres du jeu pendant une partie.
|
||||
- [x] Implémenter les wireframes
|
||||
- [ ] Ajouter une région par défaut si pas de position
|
||||
- [x] Ajouter une région par défaut si pas de position
|
||||
- [ ] Pouvoir faire pause dans la partie
|
||||
- [ ] Voir les traces et évènements des teams
|
||||
- [ ] Voir l'incertitude de position des teams
|
||||
- [ ] Focus une team cliquée
|
||||
- [x] Focus une team cliquée
|
||||
- [ ] Refaire les flèches de chasse sur la map
|
||||
- [ ] Mettre en évidence le menu paramètre (configuration)
|
||||
- [ ] Afficher un feedback quand un paramètre est sauvegardé
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Marker, Tooltip, Polyline, Polygon, Circle } from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { CustomMapContainer } from "@/components/map";
|
||||
import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
|
||||
import useAdmin from "@/hook/useAdmin";
|
||||
import { GameState } from "@/util/gameState";
|
||||
import { GameState, ZoneTypes } from "@/util/types";
|
||||
import { mapZooms } from "@/util/configurations";
|
||||
|
||||
const positionIcon = new L.Icon({
|
||||
iconUrl: '/icons/marker/blue.png',
|
||||
@@ -13,12 +14,7 @@ const positionIcon = new L.Icon({
|
||||
shadowSize: [30, 30],
|
||||
});
|
||||
|
||||
const zoneTypes = {
|
||||
circle: "circle",
|
||||
polygon: "polygon"
|
||||
}
|
||||
|
||||
export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
||||
export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
|
||||
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
||||
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
||||
|
||||
@@ -58,14 +54,14 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
||||
if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null;
|
||||
|
||||
switch (zoneType) {
|
||||
case zoneTypes.circle:
|
||||
case ZoneTypes.CIRCLE:
|
||||
return (
|
||||
<div>
|
||||
{ zoneExtremities.begin && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} color="red" fillColor="red" />}
|
||||
{ zoneExtremities.end && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} color="green" fillColor="green" />}
|
||||
</div>
|
||||
);
|
||||
case zoneTypes.polygon:
|
||||
case ZoneTypes.POLYGON:
|
||||
return (
|
||||
<div>
|
||||
{ zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
||||
@@ -81,14 +77,16 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
||||
<div className='h-full w-full flex flex-col'>
|
||||
{gameState == GameState.PLAYING && <p>{`Next zone in : ${formatTime(timeLeftNextZone)}`}</p>}
|
||||
<CustomMapContainer mapStyle={mapStyle}>
|
||||
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
|
||||
<MapEventListener onDragStart={() => setIsFocusing(false)}/>
|
||||
<Zones/>
|
||||
{teams.map((team) => team.currentLocation && !team.captured &&
|
||||
<div>
|
||||
<>
|
||||
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
||||
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
|
||||
</Marker>
|
||||
{showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CustomMapContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { env } from 'next-runtime-env';
|
||||
import { useEffect, useState } from "react";
|
||||
import useAdmin from "@/hook/useAdmin";
|
||||
import { GameState } from '@/util/gameState';
|
||||
import { getStatus } from '@/util/functions';
|
||||
|
||||
function DotLine({ label, value }) {
|
||||
return (
|
||||
@@ -27,28 +27,6 @@ function IconValue({ color, icon, value }) {
|
||||
);
|
||||
}
|
||||
|
||||
const TEAM_STATUS = {
|
||||
playing: { label: "En jeu", color: "text-custom-green" },
|
||||
captured: { label: "Capturée", color: "text-custom-red" },
|
||||
outofzone: { label: "Hors zone", color: "text-custom-orange" },
|
||||
ready: { label: "Placée", color: "text-custom-green" },
|
||||
notready: { label: "Non placée", color: "text-custom-red" },
|
||||
waiting: { label: "En attente", color: "text-custom-grey" },
|
||||
};
|
||||
|
||||
function getStatus(team, gamestate) {
|
||||
switch (gamestate) {
|
||||
case GameState.SETUP:
|
||||
return TEAM_STATUS.waiting;
|
||||
case GameState.PLACEMENT:
|
||||
return team.ready ? TEAM_STATUS.ready : TEAM_STATUS.notready;
|
||||
case GameState.PLAYING:
|
||||
return team.captured ? TEAM_STATUS.captured : team.outofzone ? TEAM_STATUS.outofzone : TEAM_STATUS.playing;
|
||||
case GameState.FINISHED:
|
||||
return team.captured ? TEAM_STATUS.captured : TEAM_STATUS.playing;
|
||||
}
|
||||
}
|
||||
|
||||
export default function TeamSidePanel({ selectedTeamId, onClose }) {
|
||||
const { getTeam, startDate, gameState } = useAdmin();
|
||||
const [imgSrc, setImgSrc] = useState("");
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
import { List } from '@/components/list';
|
||||
import useAdmin from '@/hook/useAdmin';
|
||||
import { GameState } from '@/util/gameState';
|
||||
|
||||
const TEAM_STATUS = {
|
||||
playing: { label: "En jeu", color: "text-custom-green" },
|
||||
captured: { label: "Capturée", color: "text-custom-red" },
|
||||
outofzone: { label: "Hors zone", color: "text-custom-orange" },
|
||||
ready: { label: "Placée", color: "text-custom-green" },
|
||||
notready: { label: "Non placée", color: "text-custom-red" },
|
||||
waiting: { label: "En attente", color: "text-custom-grey" },
|
||||
};
|
||||
|
||||
function getStatus(team, gamestate) {
|
||||
switch (gamestate) {
|
||||
case GameState.SETUP:
|
||||
return TEAM_STATUS.waiting;
|
||||
case GameState.PLACEMENT:
|
||||
return team.ready ? TEAM_STATUS.ready : TEAM_STATUS.notready;
|
||||
case GameState.PLAYING:
|
||||
return team.captured ? TEAM_STATUS.captured : team.outofzone ? TEAM_STATUS.outofzone : TEAM_STATUS.playing;
|
||||
case GameState.FINISHED:
|
||||
return team.captured ? TEAM_STATUS.captured : TEAM_STATUS.playing;
|
||||
}
|
||||
}
|
||||
import { getStatus } from '@/util/functions';
|
||||
|
||||
function TeamViewerItem({ team, itemSelected, onSelected }) {
|
||||
const { gameState } = useAdmin();
|
||||
|
||||
@@ -4,16 +4,18 @@ import { TextInput } from "@/components/input";
|
||||
|
||||
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setValue("");
|
||||
onSubmit(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="bg-white shadow-md max-w mx-auto p-5 mx-10 flex flex-col space-y-4" onSubmit={handleSubmit}>
|
||||
<h1 className="text-2xl font-bold text-center text-gray-700">{title}</h1>
|
||||
<TextInput inputMode="numeric" placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} name="team-id"/>
|
||||
<BlueButton type="submit">{buttonText}</BlueButton>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { Section } from "@/components/section";
|
||||
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
||||
import useAdmin from "@/hook/useAdmin";
|
||||
import { GameState } from "@/util/gameState";
|
||||
import { GameState } from "@/util/types";
|
||||
import { mapStyles } from '@/util/configurations';
|
||||
import TeamSidePanel from "./components/teamSidePanel";
|
||||
import TeamViewer from './components/teamViewer';
|
||||
import { MapButton, ControlButton } from './components/buttons';
|
||||
@@ -13,33 +14,25 @@ import { MapButton, ControlButton } from './components/buttons';
|
||||
// Imported at runtime and not at compile time
|
||||
const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
|
||||
|
||||
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 default function AdminPage() {
|
||||
const { useProtect } = useAdminConnexion();
|
||||
const [selectedTeamId, setSelectedTeamId] = useState(null);
|
||||
const { changeState } = useAdmin();
|
||||
const { changeState, getTeam } = useAdmin();
|
||||
const [mapStyle, setMapStyle] = useState(mapStyles.default);
|
||||
const [showZones, setShowZones] = useState(true);
|
||||
const [showNames, setShowNames] = useState(true);
|
||||
const [showArrows, setShowArrows] = useState(false);
|
||||
const [isFocusing, setIsFocusing] = useState(true);
|
||||
|
||||
useProtect();
|
||||
|
||||
function onSelected(id) {
|
||||
if (selectedTeamId === id) {
|
||||
if (selectedTeamId == id && (!getTeam(id)?.currentLocation || isFocusing)) {
|
||||
setSelectedTeamId(null);
|
||||
setIsFocusing(false);
|
||||
} else {
|
||||
setSelectedTeamId(id);
|
||||
setIsFocusing(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +75,15 @@ export default function AdminPage() {
|
||||
<div className='grow flex-1 flex flex-col bg-white p-3 gap-3 shadow-2xl'>
|
||||
<div className="flex-1 flex flex-row gap-3">
|
||||
<div className="flex-1 h-full">
|
||||
<LiveMap mapStyle={mapStyle} showZones={showZones} showNames={showNames} showArrows={showArrows}/>
|
||||
<LiveMap
|
||||
selectedTeamId={selectedTeamId}
|
||||
isFocusing={isFocusing}
|
||||
setIsFocusing={setIsFocusing}
|
||||
mapStyle={mapStyle}
|
||||
showZones={showZones}
|
||||
showNames={showNames}
|
||||
showArrows={showArrows}
|
||||
/>
|
||||
</div>
|
||||
{selectedTeamId &&
|
||||
<div className="w-3/12 h-full">
|
||||
|
||||
@@ -10,18 +10,12 @@ import useAdmin from '@/hook/useAdmin';
|
||||
import Messages from "./components/messages";
|
||||
import TeamManager from './components/teamManager';
|
||||
import useLocalVariable from "@/hook/useLocalVariable";
|
||||
import { ZoneTypes } from "@/util/types";
|
||||
import { defaultZoneSettings } from "@/util/configurations";
|
||||
|
||||
// Imported at runtime and not at compile time
|
||||
const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false });
|
||||
const CircleZoneSelector = dynamic(() => import('./components/circleZoneSelector'), { ssr: false });
|
||||
|
||||
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: []}
|
||||
const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false });
|
||||
|
||||
export default function ConfigurationPage() {
|
||||
const { useProtect } = useAdminConnexion();
|
||||
@@ -34,10 +28,10 @@ export default function ConfigurationPage() {
|
||||
|
||||
function modifyLocalZoneSettings(key, value) {
|
||||
setLocalZoneSettings(prev => ({...prev, [key]: value}));
|
||||
};
|
||||
}
|
||||
|
||||
function handleChangeZoneType() {
|
||||
setLocalZoneSettings(localZoneSettings.type == zoneTypes.circle ? defaultPolygonSettings : defaultCircleSettings)
|
||||
setLocalZoneSettings(localZoneSettings.type == ZoneTypes.CIRCLE ? defaultZoneSettings.polygon : defaultZoneSettings.circle)
|
||||
}
|
||||
|
||||
function handleTeamSubmit(e) {
|
||||
@@ -83,10 +77,10 @@ export default function ConfigurationPage() {
|
||||
{localZoneSettings && <BlueButton onClick={handleChangeZoneType}>Change zone type</BlueButton>}
|
||||
</div>
|
||||
<div className="w-full flex-1">
|
||||
{localZoneSettings && localZoneSettings.type == zoneTypes.circle &&
|
||||
{localZoneSettings && localZoneSettings.type == ZoneTypes.CIRCLE &&
|
||||
<CircleZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/>
|
||||
}
|
||||
{localZoneSettings && localZoneSettings.type == zoneTypes.polygon &&
|
||||
{localZoneSettings && localZoneSettings.type == ZoneTypes.POLYGON &&
|
||||
<PolygonZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { MapContainer, TileLayer, useMap } from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { mapLocations, mapZooms, mapStyles } from "@/util/configurations";
|
||||
|
||||
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}) {
|
||||
export function MapPan({center, zoom, animate=false}) {
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
if (center, zoom) {
|
||||
map.flyTo(center, zoom, { animate: false });
|
||||
if (center && zoom) {
|
||||
map.flyTo(center, zoom, { animate: animate });
|
||||
}
|
||||
}, [center, zoom]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
||||
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDragStart }) {
|
||||
const map = useMap();
|
||||
|
||||
// Handle the mouse click left
|
||||
@@ -93,6 +81,17 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
||||
map.off('mousemove', onMouseMove);
|
||||
}
|
||||
}, [onMouseMove]);
|
||||
|
||||
// Handle the drag start
|
||||
useEffect(() => {
|
||||
if (!onDragStart) return;
|
||||
|
||||
map.on('dragstart', onDragStart);
|
||||
|
||||
return () => {
|
||||
map.off('dragstart', onDragStart);
|
||||
}
|
||||
}, [onDragStart]);
|
||||
|
||||
// Prevent right click context menu
|
||||
useEffect(() => {
|
||||
@@ -105,9 +104,23 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function MapResizeWatcher() {
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
map.invalidateSize();
|
||||
});
|
||||
observer.observe(map.getContainer());
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [map]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CustomMapContainer({mapStyle, children}) {
|
||||
const [location, setLocation] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.geolocation) {
|
||||
@@ -118,7 +131,6 @@ export function CustomMapContainer({mapStyle, children}) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
setLocation([pos.coords.latitude, pos.coords.longitude]);
|
||||
setLoading(false);
|
||||
},
|
||||
(err) => console.log("Error :", err),
|
||||
{
|
||||
@@ -129,13 +141,11 @@ export function CustomMapContainer({mapStyle, children}) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div className="w-full h-full"/>
|
||||
}
|
||||
|
||||
return (
|
||||
<MapContainer className='w-full h-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
||||
<MapContainer className='w-full h-full' center={mapLocations.paris} zoom={mapZooms.low} scrollWheelZoom={true}>
|
||||
<TileLayer url={(mapStyle || mapStyles.default).url} attribution={(mapStyle || mapStyles.default).attribution}/>
|
||||
<MapPan center={location} zoom={mapZooms.high}/>
|
||||
<MapResizeWatcher/>
|
||||
{children}
|
||||
</MapContainer>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
import { useSocket } from "./socketContext";
|
||||
import useSocketListener from "@/hook/useSocketListener";
|
||||
import { GameState } from "@/util/gameState";
|
||||
import { GameState } from "@/util/types";
|
||||
|
||||
const adminContext = createContext();
|
||||
|
||||
|
||||
35
traque-front/util/configurations.js
Normal file
35
traque-front/util/configurations.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ZoneTypes } from "./types";
|
||||
|
||||
export const mapLocations = {
|
||||
paris: [48.86, 2.33]
|
||||
}
|
||||
|
||||
export const mapZooms = {
|
||||
low: 4,
|
||||
high: 15,
|
||||
}
|
||||
|
||||
export 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 const defaultZoneSettings = {
|
||||
circle: {type: ZoneTypes.CIRCLE, min: null, max: null, reductionCount: 4, duration: 10},
|
||||
polygon: {type: ZoneTypes.POLYGON, polygons: []}
|
||||
}
|
||||
|
||||
export const teamStatus = {
|
||||
playing: { label: "En jeu", color: "text-custom-green" },
|
||||
captured: { label: "Capturée", color: "text-custom-red" },
|
||||
outofzone: { label: "Hors zone", color: "text-custom-orange" },
|
||||
ready: { label: "Placée", color: "text-custom-green" },
|
||||
notready: { label: "Non placée", color: "text-custom-red" },
|
||||
waiting: { label: "En attente", color: "text-custom-grey" },
|
||||
}
|
||||
15
traque-front/util/functions.js
Normal file
15
traque-front/util/functions.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { GameState } from './types';
|
||||
import { teamStatus } from './configurations';
|
||||
|
||||
export function getStatus(team, gamestate) {
|
||||
switch (gamestate) {
|
||||
case GameState.SETUP:
|
||||
return teamStatus.waiting;
|
||||
case GameState.PLACEMENT:
|
||||
return team.ready ? teamStatus.ready : teamStatus.notready;
|
||||
case GameState.PLAYING:
|
||||
return team.captured ? teamStatus.captured : team.outofzone ? teamStatus.outofzone : teamStatus.playing;
|
||||
case GameState.FINISHED:
|
||||
return team.captured ? teamStatus.captured : teamStatus.playing;
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,9 @@ export const GameState = {
|
||||
PLACEMENT: "placement",
|
||||
PLAYING: "playing",
|
||||
FINISHED: "finished"
|
||||
}
|
||||
}
|
||||
|
||||
export const ZoneTypes = {
|
||||
CIRCLE: "circle",
|
||||
POLYGON: "polygon"
|
||||
}
|
||||
Reference in New Issue
Block a user