Map resize correction + player focus + cleaning

This commit is contained in:
Sebastien Riviere
2025-09-04 18:58:41 +02:00
parent 31c43f6361
commit d0a3351f55
12 changed files with 132 additions and 116 deletions

View File

@@ -33,11 +33,11 @@
- [x] Ajouter timer du rétrécissement des zones. - [x] Ajouter timer du rétrécissement des zones.
- [x] Pouvoir changer les paramètres du jeu pendant une partie. - [x] Pouvoir changer les paramètres du jeu pendant une partie.
- [x] Implémenter les wireframes - [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 - [ ] Pouvoir faire pause dans la partie
- [ ] Voir les traces et évènements des teams - [ ] Voir les traces et évènements des teams
- [ ] Voir l'incertitude de position 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 - [ ] Refaire les flèches de chasse sur la map
- [ ] Mettre en évidence le menu paramètre (configuration) - [ ] Mettre en évidence le menu paramètre (configuration)
- [ ] Afficher un feedback quand un paramètre est sauvegardé - [ ] Afficher un feedback quand un paramètre est sauvegardé

View File

@@ -1,9 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Marker, Tooltip, Polyline, Polygon, Circle } from "react-leaflet"; import { Marker, Tooltip, Polyline, Polygon, Circle } from "react-leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { CustomMapContainer } from "@/components/map"; import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
import useAdmin from "@/hook/useAdmin"; 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({ const positionIcon = new L.Icon({
iconUrl: '/icons/marker/blue.png', iconUrl: '/icons/marker/blue.png',
@@ -13,12 +14,7 @@ const positionIcon = new L.Icon({
shadowSize: [30, 30], shadowSize: [30, 30],
}); });
const zoneTypes = { export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
circle: "circle",
polygon: "polygon"
}
export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin(); const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null); 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; if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null;
switch (zoneType) { switch (zoneType) {
case zoneTypes.circle: case ZoneTypes.CIRCLE:
return ( return (
<div> <div>
{ zoneExtremities.begin && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} color="red" fillColor="red" />} { 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" />} { zoneExtremities.end && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} color="green" fillColor="green" />}
</div> </div>
); );
case zoneTypes.polygon: case ZoneTypes.POLYGON:
return ( return (
<div> <div>
{ zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />} { 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'> <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>}
<CustomMapContainer mapStyle={mapStyle}> <CustomMapContainer mapStyle={mapStyle}>
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
<MapEventListener onDragStart={() => setIsFocusing(false)}/>
<Zones/> <Zones/>
{teams.map((team) => team.currentLocation && !team.captured && {teams.map((team) => team.currentLocation && !team.captured &&
<div> <>
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}> <Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>} {showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
</Marker> </Marker>
{showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>} {showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>}
</div> </>
)} )}
</CustomMapContainer> </CustomMapContainer>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useAdmin from "@/hook/useAdmin"; import useAdmin from "@/hook/useAdmin";
import { GameState } from '@/util/gameState'; import { getStatus } from '@/util/functions';
function DotLine({ label, value }) { function DotLine({ label, value }) {
return ( 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 }) { export default function TeamSidePanel({ selectedTeamId, onClose }) {
const { getTeam, startDate, gameState } = useAdmin(); const { getTeam, startDate, gameState } = useAdmin();
const [imgSrc, setImgSrc] = useState(""); const [imgSrc, setImgSrc] = useState("");

View File

@@ -1,28 +1,6 @@
import { List } from '@/components/list'; import { List } from '@/components/list';
import useAdmin from '@/hook/useAdmin'; import useAdmin from '@/hook/useAdmin';
import { GameState } from '@/util/gameState'; import { getStatus } from '@/util/functions';
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;
}
}
function TeamViewerItem({ team, itemSelected, onSelected }) { function TeamViewerItem({ team, itemSelected, onSelected }) {
const { gameState } = useAdmin(); const { gameState } = useAdmin();

View File

@@ -4,16 +4,18 @@ 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("");
function handleSubmit(e) { function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
setValue(""); setValue("");
onSubmit(value); onSubmit(value);
} }
return ( return (
<form className="bg-white shadow-md max-w mx-auto p-5 mx-10 flex flex-col space-y-4" onSubmit={handleSubmit}> <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> <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"/> <TextInput inputMode="numeric" placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} name="team-id"/>
<BlueButton type="submit">{buttonText}</BlueButton> <BlueButton type="submit">{buttonText}</BlueButton>
</form> </form>
) );
} }

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import React, { useState } from 'react'; import { useState } from 'react';
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
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";
import { GameState } from "@/util/gameState"; import { GameState } from "@/util/types";
import { mapStyles } from '@/util/configurations';
import TeamSidePanel from "./components/teamSidePanel"; import TeamSidePanel from "./components/teamSidePanel";
import TeamViewer from './components/teamViewer'; import TeamViewer from './components/teamViewer';
import { MapButton, ControlButton } from './components/buttons'; import { MapButton, ControlButton } from './components/buttons';
@@ -13,33 +14,25 @@ import { MapButton, ControlButton } from './components/buttons';
// Imported at runtime and not at compile time // Imported at runtime and not at compile time
const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false }); const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
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 default function AdminPage() { export default function AdminPage() {
const { useProtect } = useAdminConnexion(); const { useProtect } = useAdminConnexion();
const [selectedTeamId, setSelectedTeamId] = useState(null); const [selectedTeamId, setSelectedTeamId] = useState(null);
const { changeState } = useAdmin(); const { changeState, getTeam } = useAdmin();
const [mapStyle, setMapStyle] = useState(mapStyles.default); const [mapStyle, setMapStyle] = useState(mapStyles.default);
const [showZones, setShowZones] = useState(true); const [showZones, setShowZones] = useState(true);
const [showNames, setShowNames] = useState(true); const [showNames, setShowNames] = useState(true);
const [showArrows, setShowArrows] = useState(false); const [showArrows, setShowArrows] = useState(false);
const [isFocusing, setIsFocusing] = useState(true);
useProtect(); useProtect();
function onSelected(id) { function onSelected(id) {
if (selectedTeamId === id) { if (selectedTeamId == id && (!getTeam(id)?.currentLocation || isFocusing)) {
setSelectedTeamId(null); setSelectedTeamId(null);
setIsFocusing(false);
} else { } else {
setSelectedTeamId(id); 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='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 flex flex-row gap-3">
<div className="flex-1 h-full"> <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> </div>
{selectedTeamId && {selectedTeamId &&
<div className="w-3/12 h-full"> <div className="w-3/12 h-full">

View File

@@ -10,18 +10,12 @@ import useAdmin from '@/hook/useAdmin';
import Messages from "./components/messages"; import Messages from "./components/messages";
import TeamManager from './components/teamManager'; import TeamManager from './components/teamManager';
import useLocalVariable from "@/hook/useLocalVariable"; import useLocalVariable from "@/hook/useLocalVariable";
import { ZoneTypes } from "@/util/types";
import { defaultZoneSettings } from "@/util/configurations";
// Imported at runtime and not at compile time // 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 CircleZoneSelector = dynamic(() => import('./components/circleZoneSelector'), { ssr: false });
const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { 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: []}
export default function ConfigurationPage() { export default function ConfigurationPage() {
const { useProtect } = useAdminConnexion(); const { useProtect } = useAdminConnexion();
@@ -34,10 +28,10 @@ export default function ConfigurationPage() {
function modifyLocalZoneSettings(key, value) { function modifyLocalZoneSettings(key, value) {
setLocalZoneSettings(prev => ({...prev, [key]: value})); setLocalZoneSettings(prev => ({...prev, [key]: value}));
}; }
function handleChangeZoneType() { function handleChangeZoneType() {
setLocalZoneSettings(localZoneSettings.type == zoneTypes.circle ? defaultPolygonSettings : defaultCircleSettings) setLocalZoneSettings(localZoneSettings.type == ZoneTypes.CIRCLE ? defaultZoneSettings.polygon : defaultZoneSettings.circle)
} }
function handleTeamSubmit(e) { function handleTeamSubmit(e) {
@@ -83,10 +77,10 @@ export default function ConfigurationPage() {
{localZoneSettings && <BlueButton onClick={handleChangeZoneType}>Change zone type</BlueButton>} {localZoneSettings && <BlueButton onClick={handleChangeZoneType}>Change zone type</BlueButton>}
</div> </div>
<div className="w-full flex-1"> <div className="w-full flex-1">
{localZoneSettings && localZoneSettings.type == zoneTypes.circle && {localZoneSettings && localZoneSettings.type == ZoneTypes.CIRCLE &&
<CircleZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/> <CircleZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/>
} }
{localZoneSettings && localZoneSettings.type == zoneTypes.polygon && {localZoneSettings && localZoneSettings.type == ZoneTypes.POLYGON &&
<PolygonZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/> <PolygonZoneSelector zoneSettings={localZoneSettings} modifyZoneSettings={modifyLocalZoneSettings} applyZoneSettings={applyLocalZoneSettings}/>
} }
</div> </div>

View File

@@ -1,33 +1,21 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { MapContainer, TileLayer, useMap } from "react-leaflet"; import { MapContainer, TileLayer, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { mapLocations, mapZooms, mapStyles } from "@/util/configurations";
const DEFAULT_ZOOM = 14; export function MapPan({center, zoom, animate=false}) {
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();
useEffect(() => { useEffect(() => {
if (center, zoom) { if (center && zoom) {
map.flyTo(center, zoom, { animate: false }); map.flyTo(center, zoom, { animate: animate });
} }
}, [center, zoom]); }, [center, zoom]);
return null; return null;
} }
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) { export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDragStart }) {
const map = useMap(); const map = useMap();
// Handle the mouse click left // Handle the mouse click left
@@ -93,6 +81,17 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
map.off('mousemove', onMouseMove); map.off('mousemove', onMouseMove);
} }
}, [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 // Prevent right click context menu
useEffect(() => { useEffect(() => {
@@ -105,9 +104,23 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
return null; 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}) { export function CustomMapContainer({mapStyle, children}) {
const [location, setLocation] = useState(null); const [location, setLocation] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
if (!navigator.geolocation) { if (!navigator.geolocation) {
@@ -118,7 +131,6 @@ export function CustomMapContainer({mapStyle, children}) {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos) => { (pos) => {
setLocation([pos.coords.latitude, pos.coords.longitude]); setLocation([pos.coords.latitude, pos.coords.longitude]);
setLoading(false);
}, },
(err) => console.log("Error :", err), (err) => console.log("Error :", err),
{ {
@@ -129,13 +141,11 @@ export function CustomMapContainer({mapStyle, children}) {
); );
}, []); }, []);
if (loading) {
return <div className="w-full h-full"/>
}
return ( 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}/> <TileLayer url={(mapStyle || mapStyles.default).url} attribution={(mapStyle || mapStyles.default).attribution}/>
<MapPan center={location} zoom={mapZooms.high}/>
<MapResizeWatcher/>
{children} {children}
</MapContainer> </MapContainer>
) )

View File

@@ -2,7 +2,7 @@
import { createContext, useContext, useMemo, useState } from "react"; import { createContext, useContext, useMemo, useState } from "react";
import { useSocket } from "./socketContext"; import { useSocket } from "./socketContext";
import useSocketListener from "@/hook/useSocketListener"; import useSocketListener from "@/hook/useSocketListener";
import { GameState } from "@/util/gameState"; import { GameState } from "@/util/types";
const adminContext = createContext(); const adminContext = createContext();

View 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: '&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 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" },
}

View 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;
}
}

View File

@@ -3,4 +3,9 @@ export const GameState = {
PLACEMENT: "placement", PLACEMENT: "placement",
PLAYING: "playing", PLAYING: "playing",
FINISHED: "finished" FINISHED: "finished"
} }
export const ZoneTypes = {
CIRCLE: "circle",
POLYGON: "polygon"
}