diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 639fdb8..7da7dd7 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -1,7 +1,7 @@ import { Waypoint } from "../types/Waypoint"; import api from "./api"; - -// TODO: Implement new endpoint logic +import { AxiosResponse } from "axios"; +import { deserializeWaypoints, serializeWaypointForGCOM } from "./formatters"; export const armDrone = async (arm: boolean) => { return await api.post("/drone/arm", { arm }); @@ -11,10 +11,6 @@ export const takeoffDrone = async (altitude?: number) => { return await api.post("/drone/takeoff", { altitude }); }; -export const postWaypointsToDrone = async (waypoints: Waypoint[]) => { - return await api.post("/drone/queue", waypoints); -}; - export const getGCOM = async (): Promise => { return (await api.get("/drone/queue")) as Waypoint[]; }; @@ -22,3 +18,27 @@ export const getGCOM = async (): Promise => { export const getRoute = async (): Promise => { return (await api.get("/route")) as Waypoint[]; }; + +export const getWaypointsQuery = async (): Promise => { + const response = await api.get("/waypoint"); + const waypoints = deserializeWaypoints(response.data); + return waypoints; +}; + +export const createWaypointQuery = async (waypoint: Waypoint): Promise => { + const serializedWaypoint = serializeWaypointForGCOM(waypoint); + return api.post("/waypoint/", serializedWaypoint); +}; + +export const updateWaypointQuery = async (waypoint: Waypoint): Promise => { + const serializedWaypoint = serializeWaypointForGCOM(waypoint); + return api.patch(`/waypoint/${waypoint.id}/`, serializedWaypoint); +}; + +export const deleteWaypointQuery = async (id: string): Promise => { + return api.delete(`/waypoint/${id}/`); +}; + +export const reorderWaypointsQuery = async (waypointIds: string[]): Promise => { + return api.post("/waypoint/reorder", waypointIds); +}; diff --git a/src/api/formatters.ts b/src/api/formatters.ts new file mode 100644 index 0000000..55b2c67 --- /dev/null +++ b/src/api/formatters.ts @@ -0,0 +1,39 @@ +import { Waypoint } from "../types/Waypoint"; + +export const serializeWaypointForGCOM = (waypoint: Waypoint) => { + return { + name: waypoint.name, + latitude: waypoint.lat, + longitude: waypoint.long, + altitude: waypoint.alt, + radius: waypoint.radius, + order: waypoint.order, + route: 1, + }; +}; + +export const deserializeWaypoints = ( + waypoints: Array<{ + id: string; + name: string; + latitude: number; + longitude: number; + altitude: number; + radius: number; + pass_radius: number; + pass_option: number; + order: number; + route: number; + }>, +): Waypoint[] => { + return waypoints + .sort((a, b) => a.order - b.order) + .map((wp) => ({ + id: wp.id, + name: wp.name, + lat: wp.latitude, + long: wp.longitude, + alt: wp.altitude, + radius: wp.radius, + })); +}; diff --git a/src/components/Map/WaypointCreationMap.tsx b/src/components/Map/WaypointCreationMap.tsx index 07ea515..be48ed8 100644 --- a/src/components/Map/WaypointCreationMap.tsx +++ b/src/components/Map/WaypointCreationMap.tsx @@ -1,19 +1,15 @@ // Honestly its just easier to have this in a separate file rather than having it be in with MapView.tsx -import { Place } from "@mui/icons-material"; +import { AddHomeWork, Place } from "@mui/icons-material"; import { Fragment, useState } from "react"; import { Layer, LayerProps, Map, MapLayerMouseEvent, Marker, Source } from "react-map-gl/maplibre"; -import { - addToQueuedWaypoints, - editWaypointAtIndex, - selectMapCenterCoords, - selectQueuedWaypoints, -} from "../../store/slices/appSlice"; +import { selectMapCenterCoords, selectWaypoints } from "../../store/slices/appSlice"; import { useAppDispatch, useAppSelector } from "../../store/store"; import WaypointItem from "../WaypointItem"; import { roundTo } from "../../utils/routeTo"; import { Box } from "@mui/material"; -import { WaypointEditState } from "../../types/Waypoint"; +import { Waypoint, WaypointEditState } from "../../types/Waypoint"; +import { useWaypoints } from "../../utils/useWaypoints"; type DraggedMarker = { long: number; @@ -23,20 +19,27 @@ type DraggedMarker = { type CreationMapProps = { handleDelete: (index: number) => void; - handleEdit: (index: number) => void; - editState: WaypointEditState; + startEditing: (index: number) => void; + submitWaypoint: (wp: Waypoint) => void; + setEditingCoords: (coords: { lat: number; long: number }) => void; + editingIndex: number; }; -export default function WaypointCreationMap({ handleDelete, handleEdit, editState }: CreationMapProps) { +export default function WaypointCreationMap({ + handleDelete, + startEditing, + editingIndex, + setEditingCoords, + submitWaypoint, +}: CreationMapProps) { const coords = useAppSelector(selectMapCenterCoords); - const clientWPQueue = useAppSelector(selectQueuedWaypoints); - const dispatch = useAppDispatch(); - const [selectedWaypoints, setSelectedWaypoints] = useState(clientWPQueue.map(() => false)); + const { waypoints } = useWaypoints(); + const [selectedWaypoints, setSelectedWaypoints] = useState(waypoints?.map(() => false) || []); const [draggedMarkerData, setDraggedMarkerData] = useState(null); const routeData: GeoJSON.GeoJSON = { type: "LineString", - coordinates: clientWPQueue.map((waypoint) => [waypoint.long, waypoint.lat]), + coordinates: waypoints?.map((waypoint) => [waypoint.long, waypoint.lat]) || [], }; const routeStyle: LayerProps = { id: "mps-route", @@ -49,13 +52,15 @@ export default function WaypointCreationMap({ handleDelete, handleEdit, editStat const createNewWaypoint = (event: MapLayerMouseEvent) => { if (event.originalEvent.detail !== 2) return; - dispatch( - addToQueuedWaypoints({ - id: "-1", - lat: roundTo(event.lngLat.lat, 7), - long: roundTo(event.lngLat.lng, 7), - }), - ); + const wp = { + lat: roundTo(event.lngLat.lat, 7), + long: roundTo(event.lngLat.lng, 7), + alt: 0, + radius: 123123, + name: "New Waypoint", + id: "-1", + }; + submitWaypoint(wp); setSelectedWaypoints((prev) => [...prev, false]); }; @@ -77,7 +82,7 @@ export default function WaypointCreationMap({ handleDelete, handleEdit, editStat mapStyle={ window.navigator.onLine ? "https://api.maptiler.com/maps/basic-v2/style.json?key=ioE7W2lCif3DO9oj1YJh" - : "./src/mapStyles/osmbright.json" + : "http://localhost:8000/api/map-tiles/osmbright" } onClick={createNewWaypoint} doubleClickZoom={false} @@ -85,7 +90,7 @@ export default function WaypointCreationMap({ handleDelete, handleEdit, editStat minHeight: "500px", }} > - {clientWPQueue.map((waypoint, i) => { + {waypoints?.map((waypoint, i) => { return ( { + startEditing(i); + }} onDrag={(e) => { + setEditingCoords({ + lat: e.lngLat.lat, + long: e.lngLat.lng, + }); setDraggedMarkerData({ long: e.lngLat.lng, lat: e.lngLat.lat, @@ -102,16 +114,12 @@ export default function WaypointCreationMap({ handleDelete, handleEdit, editStat }); }} onDragEnd={() => { - dispatch( - editWaypointAtIndex({ - index: i, - waypoint: { - ...waypoint, - lat: roundTo(draggedMarkerData!.lat, 7), - long: roundTo(draggedMarkerData!.long, 7), - }, - }), - ); + const wp = { + ...waypoint, + lat: roundTo(draggedMarkerData!.lat, 7), + long: roundTo(draggedMarkerData!.long, 7), + }; + submitWaypoint(wp); setDraggedMarkerData(null); }} latitude={ @@ -172,7 +180,7 @@ export default function WaypointCreationMap({ handleDelete, handleEdit, editStat width: "220px", top: "10px", border: "4px solid", - borderColor: i === editState.index ? "primary.main" : "transparent", + borderColor: i === editingIndex ? "primary.main" : "transparent", }} waypoint={waypoint} handleDelete={() => { @@ -182,7 +190,6 @@ export default function WaypointCreationMap({ handleDelete, handleEdit, editStat return prev; }); }} - handleEdit={() => handleEdit(i)} /> )} diff --git a/src/components/WaypointItem.tsx b/src/components/WaypointItem.tsx index a142d5a..dffca46 100644 --- a/src/components/WaypointItem.tsx +++ b/src/components/WaypointItem.tsx @@ -64,71 +64,6 @@ export default function WaypointItem({ waypoint, sx, handleDelete, handleEdit }: {waypoint.alt} - {waypoint.command && ( - - Command{" "} - theme.palette.primary.main, - }} - > - {waypoint.command} - - - )} - {waypoint.param1 && ( - - Param1{" "} - theme.palette.primary.main, - }} - > - {waypoint.param1} - - - )} - {waypoint.param2 && ( - - Param2{" "} - theme.palette.primary.main, - }} - > - {waypoint.param2} - - - )} - {waypoint.param3 && ( - - Param3{" "} - theme.palette.primary.main, - }} - > - {waypoint.param3} - - - )} - {waypoint.param4 && ( - - Param4{" "} - theme.palette.primary.main, - }} - > - {waypoint.param4} - - - )} {waypoint.remarks && Remarks -- {waypoint.remarks}} ); diff --git a/src/components/WaypointStatus/WaypointForm.tsx b/src/components/WaypointStatus/WaypointForm.tsx index a20c58c..5265027 100644 --- a/src/components/WaypointStatus/WaypointForm.tsx +++ b/src/components/WaypointStatus/WaypointForm.tsx @@ -1,16 +1,19 @@ import { Button, Grid, TextField, Typography } from "@mui/material"; import { useEffect, useState } from "react"; -import { WaypointEditState } from "../../types/Waypoint"; -import { useAppDispatch } from "../../store/store"; -import { addToQueuedWaypoints, editWaypointAtIndex } from "../../store/slices/appSlice"; +import { Waypoint, WaypointEditState } from "../../types/Waypoint"; +import { useAppDispatch, useAppSelector } from "../../store/store"; +import { openSnackbar, selectAutoClearWaypoints } from "../../store/slices/appSlice"; import { FormErrors, FormKeys, FormState } from "../../types/WaypointForm"; import parseWaypointForm from "../../utils/parseWaypointForm"; +import { useWaypoints } from "../../utils/useWaypoints"; // TODO: Needs a bit of cleaning up, im sure there are better logical flows for this form. type WaypointFormProps = { - editState: WaypointEditState; - clearEditState: () => void; + isEditing: boolean; + cancelEditing: () => void; + submitForm: (waypoint: Waypoint) => void; + initialEditingState?: Waypoint; }; const defaultFormState: FormState = { @@ -20,15 +23,11 @@ const defaultFormState: FormState = { name: "", radius: "", remarks: "", - command: "", - param1: "", - param2: "", - param3: "", - param4: "", }; -export default function WaypointForm({ editState, clearEditState }: WaypointFormProps) { +export default function WaypointForm({ isEditing, cancelEditing, submitForm, initialEditingState }: WaypointFormProps) { const dispatch = useAppDispatch(); + const autoClearWaypoints = useAppSelector(selectAutoClearWaypoints); const [formState, setFormState] = useState(defaultFormState); const [formErrors, setFormErrors] = useState({ @@ -38,25 +37,20 @@ export default function WaypointForm({ editState, clearEditState }: WaypointForm }); useEffect(() => { - if (editState.waypoint) { + if (isEditing) { // TODO: bit ugly, could be improved in the future. setFormState({ - lat: editState.waypoint.lat ? String(editState.waypoint.lat) : "", - long: editState.waypoint.long ? String(editState.waypoint.long) : "", - alt: editState.waypoint.alt ? String(editState.waypoint.alt) : "", - name: editState.waypoint.name ?? "No Name", - radius: editState.waypoint.radius ? String(editState.waypoint.radius) : "", - remarks: editState.waypoint.remarks ?? "", - command: editState.waypoint.command ?? "", - param1: editState.waypoint.param1 ? String(editState.waypoint.param1) : "", - param2: editState.waypoint.param2 ? String(editState.waypoint.param2) : "", - param3: editState.waypoint.param3 ? String(editState.waypoint.param3) : "", - param4: editState.waypoint.param4 ? String(editState.waypoint.param4) : "", + lat: initialEditingState?.lat ? String(initialEditingState.lat) : "", + long: initialEditingState?.long ? String(initialEditingState.long) : "", + alt: initialEditingState?.alt ? String(initialEditingState.alt) : "", + name: initialEditingState?.name ?? "No Name", + radius: initialEditingState?.radius ? String(initialEditingState.radius) : "", + remarks: initialEditingState?.remarks ?? "", }); } else { setFormState(defaultFormState); } - }, [editState.index]); + }, [isEditing, initialEditingState]); const checkReqFields = (keys: FormKeys[]): boolean => { // TODO: This function (and FormError) can be updated so that it also checks Lat/Long are within correct bounds. @@ -87,33 +81,26 @@ export default function WaypointForm({ editState, clearEditState }: WaypointForm }); }; - const handleFormSubmit = () => { - if (checkReqFields(["lat", "long", "alt"])) { - const waypoint = parseWaypointForm(formState); - dispatch(addToQueuedWaypoints(waypoint)); - } - }; + const handleFormSubmit = async () => { + try { + if (!checkReqFields(["lat", "long", "alt"])) return; - const cancelEditing = () => { - clearEditState(); - setFormState(defaultFormState); - }; + const waypoint = parseWaypointForm(formState); + submitForm(waypoint); - const handleFinishEditing = () => { - const waypoint = parseWaypointForm(formState); - dispatch( - editWaypointAtIndex({ - index: editState.index, - waypoint, - }), - ); - cancelEditing(); + if (isEditing) { + cancelEditing(); + } + } catch (error) { + const message = (error as Error).message; + dispatch(openSnackbar(message)); + } }; return ( - {editState.waypoint ? "Edit" : "Create"} Waypoint + {isEditing ? "Edit" : "Create"} Waypoint - + - - - - - - - - - - - - - - - - - - - {editState.waypoint ? ( + {isEditing ? ( <> - diff --git a/src/components/WaypointStatusCard.tsx b/src/components/WaypointStatusCard.tsx index e91f157..03869a3 100644 --- a/src/components/WaypointStatusCard.tsx +++ b/src/components/WaypointStatusCard.tsx @@ -1,60 +1,48 @@ import { Box, Button, Grid, Modal, Paper, Stack, Typography } from "@mui/material"; import { useState } from "react"; -import { postWaypointsToDrone } from "../api/endpoints"; +import { getWaypointsQuery } from "../api/endpoints"; import { - clearQueuedWaypoints, openSnackbar, - removeOneFromWaypoints, selectAutoClearWaypoints, selectMapViewOpen, - selectQueuedWaypoints, + selectWaypoints, setMapViewOpen, } from "../store/slices/appSlice"; import { useAppDispatch, useAppSelector } from "../store/store"; -import { WaypointEditState } from "../types/Waypoint"; +import { Waypoint, WaypointEditState } from "../types/Waypoint"; import InfoCard from "./InfoCard"; import WaypointCreationMap from "./Map/WaypointCreationMap"; import WaypointItem from "./WaypointItem"; import WaypointForm from "./WaypointStatus/WaypointForm"; +import { useWaypoints } from "../utils/useWaypoints"; export default function WaypointStatusCard() { const dispatch = useAppDispatch(); - const waypointQueue = useAppSelector(selectQueuedWaypoints); - const autoClearWaypoints = useAppSelector(selectAutoClearWaypoints); const mapViewOpen = useAppSelector(selectMapViewOpen); const [modalOpen, setModalOpen] = useState(false); + const { waypoints, createWaypoint, deleteWaypoint, editWaypoint, clearWaypoints } = useWaypoints(); const [editState, setEditState] = useState({ index: -1, waypoint: undefined, }); - - const handlePost = async () => { - if (waypointQueue.length === 0) { - return; - } - try { - await postWaypointsToDrone(waypointQueue); - if (autoClearWaypoints) { - dispatch(clearQueuedWaypoints()); - } - } catch (error) { - const message = (error as Error).message; - dispatch(openSnackbar(message)); - } - }; + const isEditing = editState.waypoint != undefined; const handleDeleteWaypoint = (index: number) => { - dispatch(removeOneFromWaypoints(index)); - clearEditState(); + const waypointId = waypoints?.[index].id; + if (waypointId) { + deleteWaypoint(waypointId); + clearEditState(); + } }; - const handleEditWaypoint = (index: number) => { + // Handles editing a waypoint + // Used by both waypoint map and list + const startWaypointEditing = (index: number) => { setEditState({ index, - waypoint: waypointQueue[index], + waypoint: waypoints?.[index], }); }; - const clearEditState = () => { setEditState({ index: -1, @@ -62,6 +50,18 @@ export default function WaypointStatusCard() { }); }; + // This could be for editing an existing waypoint or creating a new one + // Depending on whether isEditing is true + const handleSubmitWaypointForm = (waypoint: Waypoint) => { + if (isEditing) { + waypoint.id = editState.waypoint!.id; + editWaypoint(waypoint); + clearEditState(); + } else { + createWaypoint(waypoint); + } + }; + const rightButtons = ( {mapViewOpen ? "List View" : "Map View"} - ); @@ -97,10 +94,21 @@ export default function WaypointStatusCard() { {mapViewOpen ? ( { + setEditState((curr) => ({ + ...curr, + waypoint: curr.waypoint && { + ...curr.waypoint, + lat, + long, + }, + })); + }} /> - ) : waypointQueue.length === 0 ? ( + ) : waypoints?.length === 0 ? ( - {waypointQueue.map((waypoint, index) => { - return ( - handleDeleteWaypoint(index)} - handleEdit={() => handleEditWaypoint(index)} - /> - ); - })} + {!waypoints ? ( + + Loading waypoints... + + ) : ( + waypoints.map((waypoint, index) => { + return ( + handleDeleteWaypoint(index)} + handleEdit={() => startWaypointEditing(index)} + /> + ); + }) + )} )} - - - @@ -174,7 +195,7 @@ export default function WaypointStatusCard() { variant="contained" color="error" onClick={() => { - dispatch(clearQueuedWaypoints()); + clearWaypoints(); setModalOpen(false); }} > diff --git a/src/store/slices/appSlice.ts b/src/store/slices/appSlice.ts index 2bfc91f..4ad4e7a 100644 --- a/src/store/slices/appSlice.ts +++ b/src/store/slices/appSlice.ts @@ -8,7 +8,7 @@ import { defaultCoords } from "../../utils/coords"; // REDUX SLICE type AppState = { - queuedWaypoints: Waypoint[]; + waypoints: Waypoint[]; preferredTheme: "light" | "dark"; globalSnackbar: { message: string; @@ -25,7 +25,7 @@ type AppState = { const initialState: AppState = localStorage.getItem("appSlice") ? JSON.parse(localStorage.getItem("appSlice")!) : { - queuedWaypoints: [], + waypoints: [], preferredTheme: "dark", globalSnackbar: { message: "", @@ -43,20 +43,8 @@ const appSlice = createSlice({ name: "app", initialState, reducers: { - addToQueuedWaypoints: (state, action: PayloadAction) => { - state.queuedWaypoints.push(action.payload); - }, - setQueuedWaypoints: (state, action: PayloadAction) => { - state.queuedWaypoints = action.payload; - }, - clearQueuedWaypoints: (state) => { - state.queuedWaypoints = []; - }, - removeOneFromWaypoints: (state, action: PayloadAction) => { - state.queuedWaypoints.splice(action.payload, 1); - }, - editWaypointAtIndex: (state, action: PayloadAction<{ index: number; waypoint: Waypoint }>) => { - state.queuedWaypoints[action.payload.index] = action.payload.waypoint; + setWaypoints: (state, action: PayloadAction) => { + state.waypoints = action.payload; }, setPreferredTheme: (state, action: PayloadAction<"light" | "dark">) => { state.preferredTheme = action.payload; @@ -103,11 +91,7 @@ const appSlice = createSlice({ }); export const { - addToQueuedWaypoints, - setQueuedWaypoints, - clearQueuedWaypoints, - removeOneFromWaypoints, - editWaypointAtIndex, + setWaypoints, setPreferredTheme, openSnackbar, closeSnackbar, @@ -123,7 +107,7 @@ export const { export const selectAppSlice = (state: RootState) => state.app; -export const selectQueuedWaypoints = (state: RootState) => state.app.queuedWaypoints; +export const selectWaypoints = (state: RootState) => state.app.waypoints; export const selectPreferredTheme = (state: RootState) => state.app.preferredTheme; export const selectSnackbar = (state: RootState) => state.app.globalSnackbar; export const selectSocketStatus = (state: RootState) => state.app.telemetrySockets; diff --git a/src/types/Waypoint.ts b/src/types/Waypoint.ts index 6b0d790..f97a5fb 100644 --- a/src/types/Waypoint.ts +++ b/src/types/Waypoint.ts @@ -18,11 +18,7 @@ export type Waypoint = { alt?: number; radius?: number; remarks?: string; - command?: string; - param1?: number; - param2?: number; - param3?: number; - param4?: number; + order?: number; }; export type WaypointEditState = { diff --git a/src/types/WaypointForm.ts b/src/types/WaypointForm.ts index 4438a01..3e36b14 100644 --- a/src/types/WaypointForm.ts +++ b/src/types/WaypointForm.ts @@ -1,6 +1,6 @@ import { Waypoint } from "./Waypoint"; -export type FormState = Record, string>; +export type FormState = Record, string>; export type FormErrors = { lat: boolean; diff --git a/src/utils/parseWaypointForm.ts b/src/utils/parseWaypointForm.ts index 3e45677..7fce50e 100644 --- a/src/utils/parseWaypointForm.ts +++ b/src/utils/parseWaypointForm.ts @@ -7,18 +7,22 @@ const parseOptionalFloat = (field: string) => { }; export default function parseWaypointForm(formState: FormState): Waypoint { + const lat = parseFloat(formState.lat); + const long = parseFloat(formState.long); + + if (lat > 90 || lat < -90 || long > 180 || long < -180) { + throw new Error( + "Invalid latitude/longitude values. Must be between -90 and 90 for latitude and -180 and 180 for longitude.", + ); + } + return { - lat: parseFloat(formState.lat), - long: parseFloat(formState.long), + lat: lat, + long: long, alt: parseOptionalFloat(formState.alt), radius: parseOptionalFloat(formState.radius), name: formState.name.trim(), remarks: formState.remarks.trim(), - command: formState.command.trim(), - param1: parseOptionalFloat(formState.param1), - param2: parseOptionalFloat(formState.param2), - param3: parseOptionalFloat(formState.param3), - param4: parseOptionalFloat(formState.param4), id: "-1", }; } diff --git a/src/utils/useWaypoints.ts b/src/utils/useWaypoints.ts new file mode 100644 index 0000000..dfb5f82 --- /dev/null +++ b/src/utils/useWaypoints.ts @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import { Waypoint } from "../types/Waypoint"; +import { + createWaypointQuery, + deleteWaypointQuery, + getWaypointsQuery, + reorderWaypointsQuery, + updateWaypointQuery, +} from "../api/endpoints"; +import { useAppDispatch, useAppSelector } from "../store/store"; +import { selectWaypoints, setWaypoints as setWaypointsSlice } from "../store/slices/appSlice"; + +export const useWaypoints = () => { + // Null signifies that waypoints have not been fetched yet + const dispatch = useAppDispatch(); + const waypoints = useAppSelector(selectWaypoints); + const setWaypoints = (waypoints: Waypoint[]) => dispatch(setWaypointsSlice(waypoints)); + + const fetchAndSetWaypoints = async () => setWaypoints(await getWaypointsQuery()); + useEffect(() => { + fetchAndSetWaypoints(); + }, []); + + const createWaypoint = async (waypoint: Waypoint) => { + if (waypoints !== null) { + waypoint.order = waypoint.order || waypoints.length; + + const response = await createWaypointQuery(waypoint); + // Set the waypoint's id to the one generated by the server + waypoint.id = response.data.id; + + setWaypoints([...waypoints, waypoint]); + } + }; + + const deleteWaypoint = async (id: string) => { + await deleteWaypointQuery(id); + setWaypoints(waypoints?.filter((waypoint) => waypoint.id !== id) || []); + }; + + const editWaypoint = async (waypoint: Waypoint) => { + // Update needs to be instant so that changes update on the map when we drag + // Before the server responds + // So if stuff goes wrong, we roll the changes back + const waypointSnapshot = [...waypoints]; + try { + setWaypoints(waypoints?.map((w) => (w.id === waypoint.id ? waypoint : w)) || []); + await updateWaypointQuery(waypoint); + } catch { + setWaypoints(waypointSnapshot); + } + }; + + const clearWaypoints = async () => { + const waypointSnapshot = [...waypoints]; + try { + for (const waypoint of waypoints) { + await deleteWaypointQuery(waypoint.id); + } + setWaypoints([]); + } catch (error) { + console.error(error); + setWaypoints(waypointSnapshot); + } + }; + + const reorderWaypoints = async (waypointIds: string[]) => { + await reorderWaypointsQuery(waypointIds); + + // Reorders waypoints by id + setWaypoints( + waypointIds.reduce((acc, id) => { + const wp = waypoints?.find((w) => w.id === id); + if (!wp) return acc; + + acc.push(wp); + return acc; + }, [] as Waypoint[]), + ); + }; + + return { waypoints, createWaypoint, deleteWaypoint, editWaypoint, reorderWaypoints, clearWaypoints }; +};