import { useControlPointCancelTravelMutation, useSetControlPointDestinationMutation, } from "../../api/api"; import backend from "../../api/backend"; import { ControlPoint as ControlPointModel } from "../../api/controlpoint"; import { Icon, LatLng, Point, Marker as LMarker, Polyline as LPolyline, } from "leaflet"; import { Symbol as MilSymbol } from "milsymbol"; import { MutableRefObject, useCallback, useEffect, useRef, useState, } from "react"; import ReactDOMServer from "react-dom/server"; import { Marker, Polyline, Tooltip } from "react-leaflet"; function iconForControlPoint(cp: ControlPointModel) { const symbol = new MilSymbol(cp.sidc, { size: 24, colorMode: "Dark", }); return new Icon({ iconUrl: symbol.toDataURL(), iconAnchor: new Point(symbol.getAnchor().x, symbol.getAnchor().y), }); } function openInfoDialog(controlPoint: ControlPointModel) { backend.post(`/qt/info/control-point/${controlPoint.id}`); } function openNewPackageDialog(controlPoint: ControlPointModel) { backend.post(`/qt/create-package/control-point/${controlPoint.id}`); } interface ControlPointProps { controlPoint: ControlPointModel; } function LocationTooltipText(props: ControlPointProps) { return

{props.controlPoint.name}

; } function metersToNauticalMiles(meters: number) { return meters * 0.000539957; } function formatLatLng(latLng: LatLng) { const lat = latLng.lat.toFixed(2); const lng = latLng.lng.toFixed(2); const ns = latLng.lat >= 0 ? "N" : "S"; const ew = latLng.lng >= 0 ? "E" : "W"; return `${lat}°${ns} ${lng}°${ew}`; } function destinationTooltipText( cp: ControlPointModel, destinationish: LatLng, inRange: boolean ) { const destination = new LatLng(destinationish.lat, destinationish.lng); const distance = metersToNauticalMiles( destination.distanceTo(cp.position) ).toFixed(1); if (!inRange) { return `Out of range (${distance}nm away)`; } const dest = formatLatLng(destination); return `${cp.name} moving ${distance}nm to ${dest} next turn`; } function PrimaryMarker(props: ControlPointProps) { // We can't use normal state to update the marker tooltip or the line points // because if we set any state in the drag event it will re-render the // component and interrupt dragging. Instead, keep refs to the objects and // mutate them directly. // // For the same reason, the path is owned by this component, because updating // sibling state would be messy. Lifting the state into the parent would still // cause this component to redraw. const marker: MutableRefObject = useRef(); const pathLine: MutableRefObject = useRef(); const [hasDestination, setHasDestination] = useState( props.controlPoint.destination != null ); const [pathDestination, setPathDestination] = useState( props.controlPoint.destination ? props.controlPoint.destination : props.controlPoint.position ); const [position, setPosition] = useState( props.controlPoint.destination ? props.controlPoint.destination : props.controlPoint.position ); const setDestination = useCallback((destination: LatLng) => { setPathDestination(destination); setPosition(destination); setHasDestination(true); }, []); const resetDestination = useCallback(() => { setPathDestination(props.controlPoint.position); setPosition(props.controlPoint.position); setHasDestination(false); }, [props.controlPoint.position]); const [putDestination, { isLoading }] = useSetControlPointDestinationMutation(); const [cancelTravel] = useControlPointCancelTravelMutation(); useEffect(() => { marker.current?.setTooltipContent( props.controlPoint.destination ? destinationTooltipText( props.controlPoint, props.controlPoint.destination, true ) : ReactDOMServer.renderToString( ) ); }); return ( <> { if (ref != null) { marker.current = ref; } }} eventHandlers={{ click: () => { if (!hasDestination) { openInfoDialog(props.controlPoint); } }, contextmenu: () => { if (props.controlPoint.destination) { cancelTravel(props.controlPoint.id).then(() => { resetDestination(); }); } else { openNewPackageDialog(props.controlPoint); } }, drag: (event) => { const destination = event.target.getLatLng(); backend .get( `/control-points/${props.controlPoint.id}/destination-in-range?lat=${destination.lat}&lng=${destination.lng}` ) .then((inRange) => { marker.current?.setTooltipContent( destinationTooltipText( props.controlPoint, destination, inRange.data ) ); }); pathLine.current?.setLatLngs([ props.controlPoint.position, destination, ]); }, dragend: async (event) => { const currentPosition = new LatLng( pathDestination.lat, pathDestination.lng, pathDestination.alt ); const destination = event.target.getLatLng(); setDestination(destination); try { await putDestination({ id: props.controlPoint.id, destination: destination, }).unwrap(); } catch (error) { console.error("setDestination failed", error); setDestination(currentPosition); } }, }} > { if (ref != null) { pathLine.current = ref; } }} /> ); } interface SecondaryMarkerProps { controlPoint: ControlPointModel; destination: LatLng | null; } function SecondaryMarker(props: SecondaryMarkerProps) { if (!props.destination) { return <>; } return ( { openInfoDialog(props.controlPoint); }, contextmenu: () => { openNewPackageDialog(props.controlPoint); }, }} > ); } export default function ControlPoint(props: ControlPointProps) { return ( <> ); }