frontend/src/maps/mapMarkers/Markers.tsx

247 lines
8.1 KiB
TypeScript

import { Box,Grid2 as Grid, Theme, Tooltip, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";
import MiniPie from "charts/MiniPie";
import { useMobile } from "hooks";
import { clone } from "lodash";
import { pond } from "protobuf-ts/pond";
import React, { useCallback, useEffect, useState } from "react";
import { Marker } from "react-map-gl/mapbox-legacy";
//interface for markers
export interface MarkerData {
title: string; //displayed in the tooltip
longitude: number;
latitude: number;
colour: string;
centered?: boolean;
details?: string[];
markerIcon?: JSX.Element;
subtype?: number;
graphPercent?: number; //if defined show graph behind markericon
customImage?: JSX.Element; //if defined use image rather than basic marker with icon
customSize?: number; //if the size is different from the normal or mini markers
mini?: boolean; //whether the marker is the small scouting marker or the normal size object marker
visibleLevels?: { min?: number; max?: number }; //zoom levels that determine when the marker is visible
clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void; //function the marker will run when clicked
updateFunc?: (location: pond.Location) => void; //function the marker will run when something changes
}
interface Props {
markerData: MarkerData[];
mapZoomLevel: number;
dragOn?: boolean;
groupSelect?: boolean;
showMarkerTitle?: boolean;
displayDetails?: boolean;
}
const useStyles = makeStyles((theme: Theme) => ({
pin: {
borderRadius: "50rem",
display: "inline-block",
borderBottomRightRadius: "0",
transform: "rotate(45deg)",
cursor: "pointer",
boxShadow: "4px 4px 10px black"
},
details: {
transform: "rotate(45deg)",
background: theme.palette.background.default,
opacity: 0.75,
padding: 5,
borderRadius: 10,
paddingLeft: 40,
clipPath: "polygon(25% 0, 100% 0%, 100% 100%, 25% 100%, 0% 50%)"
},
titleDisplay: {
textAlign: "center",
width: 100,
marginLeft: -25,
marginTop: 10,
backgroundColor: theme.palette.background.default,
borderRadius: 30,
boxShadow: "4px 4px 10px black"
}
}));
export default function Markers(props: Props) {
const { markerData, dragOn, mapZoomLevel, displayDetails, showMarkerTitle, groupSelect } = props;
const classes = useStyles();
const isMobile = useMobile();
const [groupedMarkers, setGroupedMarkers] = useState<Map<number, MarkerData>>(new Map());
const moveGroup = (oldLong: number, oldLat: number, newLong: number, newLat: number) => {
//find the diff of the oldLong (the starting longitude of the marker that was actually dragged) and the newLong
const diffLong = newLong - oldLong;
//find the diff of the oldLat and new Lat
const diffLat = newLat - oldLat;
let gm = clone(groupedMarkers);
gm.forEach(marker => {
if (marker.updateFunc) {
marker.longitude = marker.longitude + diffLong;
marker.latitude = marker.latitude + diffLat;
marker.updateFunc(
pond.Location.create({ longitude: marker.longitude, latitude: marker.latitude })
);
}
});
setGroupedMarkers(gm);
};
//when group select is turned off, clear the grouped markers
useEffect(() => {
if (!groupSelect) {
setGroupedMarkers(new Map());
}
}, [groupSelect]);
const renderMarkers = useCallback(() => {
let markerList: JSX.Element[] = [];
markerData.forEach((marker, index) => {
let markerSize = marker.mini ? 20 : 50;
if (marker.customSize) {
markerSize = marker.customSize;
}
let yOff = 0;
if (!marker.centered) {
yOff = -markerSize * 0.7;
}
let vis = "block";
if (marker.visibleLevels) {
let max = marker.visibleLevels.max;
let min = marker.visibleLevels.min;
if ((max && mapZoomLevel > max) || (min && mapZoomLevel < min)) {
vis = "none";
}
}
let gm = groupedMarkers.get(index);
if (gm) {
marker.longitude = gm.longitude;
marker.latitude = gm.latitude;
}
let border = marker.customImage ? "" : "1px solid white";
if (gm) {
border = "2px solid black";
}
let blah: JSX.Element = (
<Marker
//style={{border: gm ? "2px solid black" : ""}}
key={"marker-" + index}
longitude={marker.longitude}
latitude={marker.latitude}
//offsetTop={yOff}
offset={[0, yOff]}
draggable={dragOn}
onDragEnd={e => {
let newLong = e.lngLat.lng;
let newLat = e.lngLat.lat;
if (groupSelect && groupedMarkers.size > 0) {
moveGroup(marker.longitude, marker.latitude, newLong, newLat);
} else {
if (marker.updateFunc) {
marker.longitude = newLong;
marker.latitude = newLat;
marker.updateFunc(
pond.Location.create({ latitude: e.lngLat.lat, longitude: e.lngLat.lng })
);
}
}
}}>
<Box id={"marker-" + index} />
<Tooltip title={marker.title} placement={"right"} arrow>
<Box
className={!marker.customImage ? classes.pin : undefined}
style={{
display: vis,
border: border,
background: marker.customImage ? "none" : marker.colour,
width: markerSize,
height: markerSize
}}
onPointerUp={e => {
if (!dragOn && marker.clickFunc) {
marker.clickFunc(e, index, isMobile);
} else if (groupSelect) {
let group = clone(groupedMarkers);
group.set(index, marker);
setGroupedMarkers(group);
}
}}>
{marker.graphPercent !== undefined && (
<Box
width={"100%"}
height={"100%"}
style={{ pointerEvents: "none", position: "absolute" }}>
<MiniPie
max={100}
current={marker.graphPercent}
colour={marker.colour}
size={markerSize}
/>
</Box>
)}
{marker.customImage ? (
marker.customImage
) : (
<Box
style={{
transform: "rotate(-45deg)",
width: markerSize * 0.6,
height: markerSize * 0.6,
paddingTop: marker.mini ? "10%" : "30%",
pointerEvents: "none"
}}>
{marker.markerIcon}
</Box>
)}
{marker.details && displayDetails && (
<Box
className={classes.details}
style={{
position: "absolute",
left: markerSize / 2,
top: markerSize / 2,
width: 200,
marginTop: 80
}}>
<Grid container direction="column" wrap="nowrap">
{marker.details.map((detail, i) => (
<Grid key={i}>
<Typography noWrap style={{ fontSize: 10, fontWeight: 750, height: 15 }}>
{detail}
</Typography>
</Grid>
))}
</Grid>
</Box>
)}
</Box>
</Tooltip>
{showMarkerTitle && <Box className={classes.titleDisplay}>{marker.title}</Box>}
</Marker>
);
markerList.push(<React.Fragment key={index}>{blah}</React.Fragment>);
});
return markerList;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
markerData,
classes,
displayDetails,
dragOn,
groupSelect,
groupedMarkers,
isMobile,
showMarkerTitle,
mapZoomLevel
]);
return <React.Fragment>{renderMarkers()}</React.Fragment>;
}