mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Support mobile CPs in the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/2039
This commit is contained in:
@@ -6,11 +6,35 @@ import { LatLng } from "leaflet";
|
||||
export const apiSlice = createApi({
|
||||
reducerPath: "api",
|
||||
baseQuery: fetchBaseQuery({ baseUrl: HTTP_URL }),
|
||||
tagTypes: ["ControlPoint"],
|
||||
endpoints: (builder) => ({
|
||||
getCommitBoundaryForFlight: builder.query<LatLng[], string>({
|
||||
query: (flightId) => `flights/${flightId}/commit-boundary`,
|
||||
providesTags: ["ControlPoint"],
|
||||
}),
|
||||
setControlPointDestination: builder.mutation<
|
||||
void,
|
||||
{ id: number; destination: LatLng }
|
||||
>({
|
||||
query: ({ id, destination }) => ({
|
||||
url: `control-points/${id}/destination`,
|
||||
method: "PUT",
|
||||
body: { lat: destination.lat, lng: destination.lng },
|
||||
invalidatesTags: ["ControlPoint"],
|
||||
}),
|
||||
}),
|
||||
controlPointCancelTravel: builder.mutation<void, number>({
|
||||
query: (id) => ({
|
||||
url: `control-points/${id}/cancel-travel`,
|
||||
method: "PUT",
|
||||
invalidatesTags: ["ControlPoint"],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetCommitBoundaryForFlightQuery } = apiSlice;
|
||||
export const {
|
||||
useGetCommitBoundaryForFlightQuery,
|
||||
useSetControlPointDestinationMutation,
|
||||
useControlPointCancelTravelMutation,
|
||||
} = apiSlice;
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import {
|
||||
useControlPointCancelTravelMutation,
|
||||
useSetControlPointDestinationMutation,
|
||||
} from "../../api/api";
|
||||
import backend from "../../api/backend";
|
||||
import { ControlPoint as ControlPointModel } from "../../api/controlpoint";
|
||||
import { Icon, Point } from "leaflet";
|
||||
import {
|
||||
Icon,
|
||||
LatLng,
|
||||
Point,
|
||||
Marker as LMarker,
|
||||
Polyline as LPolyline,
|
||||
} from "leaflet";
|
||||
import { Symbol as MilSymbol } from "milsymbol";
|
||||
import { Marker, Tooltip } from "react-leaflet";
|
||||
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, {
|
||||
@@ -16,11 +34,205 @@ function iconForControlPoint(cp: ControlPointModel) {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export default function ControlPoint(props: ControlPointProps) {
|
||||
function LocationTooltipText(props: ControlPointProps) {
|
||||
return <h3 style={{ margin: 0 }}>{props.controlPoint.name}</h3>;
|
||||
}
|
||||
|
||||
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<LMarker | undefined> = useRef();
|
||||
const pathLine: MutableRefObject<LPolyline | undefined> = useRef();
|
||||
|
||||
const [hasDestination, setHasDestination] = useState<boolean>(
|
||||
props.controlPoint.destination != null
|
||||
);
|
||||
const [pathDestination, setPathDestination] = useState<LatLng>(
|
||||
props.controlPoint.destination
|
||||
? props.controlPoint.destination
|
||||
: props.controlPoint.position
|
||||
);
|
||||
const [position, setPosition] = useState<LatLng>(
|
||||
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(
|
||||
<LocationTooltipText controlPoint={props.controlPoint} />
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Marker
|
||||
position={position}
|
||||
icon={iconForControlPoint(props.controlPoint)}
|
||||
draggable={props.controlPoint.mobile && !isLoading}
|
||||
autoPan
|
||||
// We might draw other markers on top of the CP. The tooltips from the
|
||||
// other markers are helpful so we want to keep them, but make sure the CP
|
||||
// is always the clickable thing.
|
||||
zIndexOffset={1000}
|
||||
opacity={props.controlPoint.destination ? 0.5 : 1}
|
||||
ref={(ref) => {
|
||||
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);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tooltip />
|
||||
</Marker>
|
||||
<Polyline
|
||||
positions={[props.controlPoint.position, pathDestination]}
|
||||
weight={1}
|
||||
color="#80BA80"
|
||||
interactive
|
||||
ref={(ref) => {
|
||||
if (ref != null) {
|
||||
pathLine.current = ref;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface SecondaryMarkerProps {
|
||||
controlPoint: ControlPointModel;
|
||||
destination: LatLng | null;
|
||||
}
|
||||
|
||||
function SecondaryMarker(props: SecondaryMarkerProps) {
|
||||
if (!props.destination) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Marker
|
||||
position={props.controlPoint.position}
|
||||
@@ -31,18 +243,28 @@ export default function ControlPoint(props: ControlPointProps) {
|
||||
zIndexOffset={1000}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
backend.post(`/qt/info/control-point/${props.controlPoint.id}`);
|
||||
openInfoDialog(props.controlPoint);
|
||||
},
|
||||
contextmenu: () => {
|
||||
backend.post(
|
||||
`/qt/create-package/control-point/${props.controlPoint.id}`
|
||||
);
|
||||
openNewPackageDialog(props.controlPoint);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tooltip>
|
||||
<h3 style={{ margin: 0 }}>{props.controlPoint.name}</h3>
|
||||
<LocationTooltipText {...props} />
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ControlPoint(props: ControlPointProps) {
|
||||
return (
|
||||
<>
|
||||
<PrimaryMarker {...props} />
|
||||
<SecondaryMarker
|
||||
controlPoint={props.controlPoint}
|
||||
destination={props.controlPoint.destination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user