Arrows + marker click focus

This commit is contained in:
Sebastien Riviere
2025-09-05 00:11:38 +02:00
parent d0a3351f55
commit 999c9a8b77
8 changed files with 196 additions and 155 deletions

View File

@@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import { Marker, Tooltip, Polyline, Polygon, Circle } from "react-leaflet";
import { Marker, Tooltip, Polygon, Circle } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import 'leaflet-polylinedecorator';
import { Arrow } from "@/components/layer";
import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
import useAdmin from "@/hook/useAdmin";
import { GameState, ZoneTypes } from "@/util/types";
@@ -14,7 +16,7 @@ const positionIcon = new L.Icon({
shadowSize: [30, 30],
});
export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
@@ -40,34 +42,20 @@ export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, map
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
}
function Arrow({pos1, pos2}) {
if (pos1 && pos2) {
return (
<Polyline positions={[pos1, pos2]} pathOptions={{ color: 'black', weight: 3 }}/>
);
} else {
return null;
}
}
function Zones() {
if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null;
switch (zoneType) {
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>
);
return (<>
{ 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" />}
</>);
case ZoneTypes.POLYGON:
return (
<div>
{ zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
{ zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
</div>
);
return (<>
{ zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
{ zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
</>);
default:
return null;
}
@@ -80,14 +68,12 @@ export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, map
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
<MapEventListener onDragStart={() => setIsFocusing(false)}/>
<Zones/>
{teams.map((team) => team.currentLocation && !team.captured &&
<>
<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}/>}
</>
)}
{teams.map((team) => team.currentLocation && !team.captured && <>
<Marker key={team.id} position={team.currentLocation} icon={positionIcon} eventHandlers={{click: () => onSelected(team.id)}}>
{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}/>}
</>)}
</CustomMapContainer>
</div>
)

View File

@@ -77,6 +77,7 @@ export default function AdminPage() {
<div className="flex-1 h-full">
<LiveMap
selectedTeamId={selectedTeamId}
onSelected={onSelected}
isFocusing={isFocusing}
setIsFocusing={setIsFocusing}
mapStyle={mapStyle}

View File

@@ -16,38 +16,16 @@ const EditMode = {
function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
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);
function handleLeftClick(e) {
if (editMode == EditMode.MAX) {
maxLeftClick(e);
} else {
minLeftClick(e);
}
}
function handleRightClick(e) {
if (editMode == EditMode.MAX) {
maxRightClick(e);
} else {
minRightClick(e);
}
}
function handleMouseMove(e) {
if (editMode == EditMode.MAX) {
maxHover(e);
} else {
minHover(e);
}
}
return (
<div>
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove}/>
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="red" fillColor="red" />}
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="blue" fillColor="blue" />}
</div>
);
return (<>
<MapEventListener
onLeftClick={editMode == EditMode.MAX ? maxLeftClick : minLeftClick}
onRightClick={editMode == EditMode.MAX ? maxRightClick : minRightClick}
onMouseMove={editMode == EditMode.MAX ? maxHover : minHover}
/>
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="red" fillColor="red" />}
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="blue" fillColor="blue" />}
</>);
}
export default function CircleZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) {

View File

@@ -1,98 +1,27 @@
import { useEffect, useState } from "react";
import { Polyline, Polygon, CircleMarker, Marker } from "react-leaflet";
import { Polyline, Polygon, Marker } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { GreenButton } from "@/components/button";
import { ReorderList } from "@/components/list";
import { CustomMapContainer, MapEventListener } from "@/components/map";
import { TextInput } from "@/components/input";
import { Node, LabeledPolygon } from "@/components/layer";
import useAdmin from "@/hook/useAdmin";
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
import useLocalVariable from "@/hook/useLocalVariable";
function Drawings({ polygons, addPolygon, removePolygon }) {
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
const nodeSize = 5; // px
const lineThickness = 3; // px
function DrawNode({pos, color}) {
return (
<CircleMarker center={pos} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} />
);
}
function DrawLine({pos1, pos2, color}) {
return (
<Polyline positions={[pos1, pos2]} pathOptions={{ color: color, weight: lineThickness }} />
);
}
function DrawUnfinishedPolygon({polygon}) {
const length = polygon.length;
if (length > 0) {
return (
<div>
<DrawNode pos={polygon[0]} color={"red"} zIndexOffset={1000} />
{polygon.map((_, i) => {
if (i < length-1) {
return <DrawLine key={i} pos1={polygon[i]} pos2={polygon[i+1]} color={"red"} />;
} else {
return null;
}
})}
</div>
);
}
}
function DrawPolygon({polygon, number}) {
const length = polygon.length;
if (length < 3) return null;
const sum = polygon.reduce(
(acc, coord) => ({
lat: acc.lat + coord.lat,
lng: acc.lng + coord.lng
}),
{ lat: 0, lng: 0 }
);
// meanPoint can be out of the polygon
// Idea : take the mean point of the largest connected subpolygon
const meanPoint = {lat: sum.lat / length, lng: sum.lng / length}
const numberIcon = L.divIcon({
html: `<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 }} />
<Marker position={meanPoint} icon={numberIcon} />
</div>
);
}
return (
<div>
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
{polygons.map((polygon, i) => <DrawPolygon key={i} polygon={polygon} number={i+1} />)}
<DrawUnfinishedPolygon polygon={currentPolygon} />
{highlightNodes.map((node, i) => <DrawNode key={i} pos={node} color={"black"} />)}
</div>
);
return (<>
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
{polygons.map((polygon, i) => <LabeledPolygon key={i} polygon={polygon} number={i+1} />)}
{ currentPolygon.length > 0 && <>
<Node pos={currentPolygon[0]} color={"red"} />
<Polyline positions={currentPolygon} pathOptions={{ color: "red", weight: 3 }} />
</>}
{highlightNodes.map((node, i) => <Node key={i} pos={node} color={"black"} />)}
</>);
}
export default function PolygonZoneSelector({zoneSettings, modifyZoneSettings, applyZoneSettings}) {