diff --git a/src/frontend/src/assets/css/index.css b/src/frontend/src/assets/css/index.css index 4c004fc3..bfb1cfd1 100644 --- a/src/frontend/src/assets/css/index.css +++ b/src/frontend/src/assets/css/index.css @@ -61,6 +61,12 @@ body { .scrollbar-images-grid::-webkit-scrollbar-thumb { background: transparent; } + +.has-dropshadow { + box-shadow: + 2px 2px 5px rgb(255, 0, 0), + 2px 2px 3px rgba(255, 0, 0, 0.692); +} @keyframes pulse-animation { 0% { box-shadow: 0 0 0 0 rgba(0, 70, 119, 0.5); diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx index 9140b743..2d8b9fe7 100644 --- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx @@ -86,6 +86,14 @@ const DescriptionBox = () => { }, }); + const { data: flightTimeData }: any = useGetTaskWaypointQuery( + projectId as string, + taskId as string, + { + select: ({ data }: any) => data.flight_data, + }, + ); + const { data: taskDescription }: Record = useGetIndividualTaskQuery(taskId as string, { enabled: !!taskWayPoints, @@ -126,7 +134,11 @@ const DescriptionBox = () => { { name: 'Total points', value: taskWayPoints?.length }, { name: 'Est. flight time', - value: taskData?.flight_time || null, + value: flightTimeData?.total_flight_time || null, + }, + { + name: 'Est. flight time in seconds', + value: flightTimeData?.total_flight_time_seconds || null, }, ], }, diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index 76420d8e..fb6eb1c4 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -1,16 +1,23 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable prefer-destructuring */ /* eslint-disable react/no-array-index-key */ -import { useGetTaskAssetsInfo, useGetTaskWaypointQuery } from '@Api/tasks'; +import { + useGetIndividualTaskQuery, + useGetTaskAssetsInfo, + useGetTaskWaypointQuery, +} from '@Api/tasks'; import marker from '@Assets/images/marker.png'; import right from '@Assets/images/rightArrow.png'; import BaseLayerSwitcherUI from '@Components/common/BaseLayerSwitcher'; import { useMapLibreGLMap } from '@Components/common/MapLibreComponents'; -import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup'; import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer'; import LocateUser from '@Components/common/MapLibreComponents/LocateUser'; import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; import { GeojsonType } from '@Components/common/MapLibreComponents/types'; import { Button } from '@Components/RadixComponents/Button'; import { postTaskWaypoint } from '@Services/tasks'; +import AsyncPopup from '@Components/common/MapLibreComponents/NewAsyncPopup'; import { toggleModal } from '@Store/actions/common'; import { setSelectedTakeOffPoint, @@ -37,13 +44,10 @@ import ToolTip from '@Components/RadixComponents/ToolTip'; import Skeleton from '@Components/RadixComponents/Skeleton'; import rotateGeoJSON from '@Utils/rotateGeojsonData'; import COGOrthophotoViewer from '@Components/common/MapLibreComponents/COGOrthophotoViewer'; -import { - calculateAngle, - calculateCentroid, - // calculateCentroidFromCoordinates, -} from '@Utils/index'; -import { useGetProjectsDetailQuery } from '@Api/projects'; import { toast } from 'react-toastify'; +import RotatingCircle from '@Components/common/RotationCue'; +import { mapLayerIDs } from '@Constants/droneOperator'; +import { findNearestCoordinate, swapFirstAndLast } from '@Utils/index'; import GetCoordinatesOnClick from './GetCoordinatesOnClick'; import ShowInfo from './ShowInfo'; @@ -51,22 +55,20 @@ const { COG_URL } = process.env; const MapSection = ({ className }: { className?: string }) => { const dispatch = useDispatch(); - const [rotationDegree, setRotationDegree] = useState(0); const bboxRef = useRef(); - const mapRef = useRef(); - const rotatedTaskWayPointsRef = useRef>(); const [taskWayPoints, setTaskWayPoints] = useState | null>(); - const draggingRef = useRef(false); const { projectId, taskId } = useParams(); - const centeroidRef = useRef<[number, number]>(); + const takeOffPointRef = useRef<[number, number]>(); const queryClient = useQueryClient(); const [popupData, setPopupData] = useState>({}); const [showOrthoPhotoLayer, setShowOrthoPhotoLayer] = useState(true); const [showTakeOffPoint, setShowTakeOffPoint] = useState(true); const [isRotationEnabled, setIsRotationEnabled] = useState(false); + const [rotationAngle, setRotationAngle] = useState(0); + const [dragging, setDragging] = useState(false); const { map, isMapLoaded } = useMapLibreGLMap({ containerId: 'dashboard-map', mapOptions: { @@ -80,39 +82,50 @@ const MapSection = ({ className }: { className?: string }) => { state => state.droneOperatorTask.selectedTakeOffPoint, ); - useEffect(() => { - if (!map || !isMapLoaded) return; - mapRef.current = map; - }, [map, isMapLoaded]); + function setVisibilityOfLayers(layerIds: string[], visibility: string) { + layerIds.forEach(layerId => { + map?.setLayoutProperty(layerId, 'visibility', visibility); + }); + } const { data: taskDataPolygon, - // isFetching: isProjectDataFetching, - }: Record = useGetProjectsDetailQuery(projectId as string, { + isFetching: taskDataPolygonIsFetching, + }: Record = useGetIndividualTaskQuery(taskId as string, { select: (projectRes: any) => { - const taskPolygon = projectRes.data.tasks.find( - (task: Record) => task.id === taskId, - ); - const { geometry } = taskPolygon.outline; + const taskPolygon = projectRes.data.outline; + const { geometry } = taskPolygon; return { type: 'FeatureCollection', features: [ { type: 'Feature', - geometry, + geometry: { + type: 'Polygon', + coordinates: geometry.coordinates, + }, properties: {}, }, ], }; }, + onSuccess: () => { + if (map) { + const layers = map.getStyle().layers; + if (layers && layers.length > 0) { + const firstLayerId = layers[4].id; // Get the first layer + map.moveLayer('task-polygon-layer', firstLayerId); // Move the layer before the first layer + } + } + }, }); const { data: taskWayPointsData }: any = useGetTaskWaypointQuery( projectId as string, taskId as string, { - select: (data: any) => { + select: ({ data }: any) => { const modifiedTaskWayPointsData = { - geojsonListOfPoint: data.data, + geojsonListOfPoint: data.results, geojsonAsLineString: { type: 'FeatureCollection', features: [ @@ -122,13 +135,13 @@ const MapSection = ({ className }: { className?: string }) => { geometry: { type: 'LineString', // get all coordinates - coordinates: coordAll(data.data), + coordinates: coordAll(data.results), }, }, ], }, }; - centeroidRef.current = + takeOffPointRef.current = modifiedTaskWayPointsData?.geojsonListOfPoint?.features[0]?.geometry?.coordinates; return modifiedTaskWayPointsData; }, @@ -155,7 +168,6 @@ const MapSection = ({ className }: { className?: string }) => { // zoom to task (waypoint) useEffect(() => { if (!taskWayPoints?.geojsonAsLineString || !isMapLoaded || !map) return; - const { geojsonAsLineString } = taskWayPoints; let bbox = null; // calculate bbox with with updated take-off point if (newTakeOffPoint && newTakeOffPoint !== 'place_on_map') { @@ -163,19 +175,26 @@ const MapSection = ({ className }: { className?: string }) => { type: 'FeatureCollection', features: [ // @ts-ignore - ...geojsonAsLineString.features, + ...taskDataPolygon.features, newTakeOffPoint, ], }; bbox = getBbox(combinedFeatures); } else { - bbox = getBbox(geojsonAsLineString as FeatureCollection); + bbox = getBbox(taskDataPolygon as FeatureCollection); } bboxRef.current = bbox; if (!isRotationEnabled) { - map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25, duration: 500 }); + map?.fitBounds(bbox as LngLatBoundsLike, { padding: 105, duration: 500 }); } - }, [map, taskWayPoints, newTakeOffPoint, isMapLoaded, isRotationEnabled]); + }, [ + map, + taskWayPoints, + taskDataPolygon, + newTakeOffPoint, + isMapLoaded, + isRotationEnabled, + ]); const getPopupUI = useCallback(() => { return ( @@ -211,8 +230,11 @@ const MapSection = ({ className }: { className?: string }) => { }, [popupData]); const handleSaveStartingPoint = () => { - const { geometry } = newTakeOffPoint as Record; - const [lng, lat] = geometry.coordinates; + const { geometry: startingPonyGeometry } = newTakeOffPoint as Record< + string, + any + >; + const [lng, lat] = startingPonyGeometry.coordinates; postWaypoint({ projectId, taskId, @@ -253,29 +275,8 @@ const MapSection = ({ className }: { className?: string }) => { function handleTaskWayPoint() { if (!map || !isMapLoaded) return; - map.setLayoutProperty( - 'waypoint-points-layer', - 'visibility', - `${!showTakeOffPoint ? 'visible' : 'none'}`, - ); - map.setLayoutProperty( - 'waypoint-points-image-layer', - 'visibility', - `${!showTakeOffPoint ? 'visible' : 'none'}`, - ); - map.setLayoutProperty( - 'waypoint-line-layer', - 'visibility', - `${!showTakeOffPoint ? 'visible' : 'none'}`, - ); - map.setLayoutProperty( - 'waypoint-points-image-image/logo', - 'visibility', - `${!showTakeOffPoint ? 'visible' : 'none'}`, - ); - map.setLayoutProperty( - 'waypoint-line-image/logo', - 'visibility', + setVisibilityOfLayers( + mapLayerIDs, `${!showTakeOffPoint ? 'visible' : 'none'}`, ); setShowTakeOffPoint(!showTakeOffPoint); @@ -284,18 +285,63 @@ const MapSection = ({ className }: { className?: string }) => { const rotateLayerGeoJSON = ( layerIds: string[], rotationDegreeParam: number, + baseLayerIds: string[], + excludeFirstFeature?: boolean, ) => { - if (!mapRef.current) return; + if (!map || !isMapLoaded) return; - layerIds.forEach(layerId => { - const source = mapRef.current?.getSource(layerId); + baseLayerIds.forEach((baseLayerId, index) => { + const source = map?.getSource(baseLayerId); + const sourceToRotate = map?.getSource(layerIds[index]); if (source && source instanceof GeoJSONSource) { - // eslint-disable-next-line no-underscore-dangle - const geojsonData = source._data; - // @ts-ignore - const rotatedGeoJSON = rotateGeoJSON(geojsonData, rotationDegreeParam); - source.setData(rotatedGeoJSON); + const baseGeoData = source._data; + if (!baseGeoData) return; + const [firstFeature, ...restFeatures] = ( + baseGeoData as Record + ).features; + if (firstFeature.geometry.type === 'Point') { + const pointRotatedGeoJson = rotateGeoJSON( + // @ts-ignore + { + ...(baseGeoData as object), + features: excludeFirstFeature + ? restFeatures + : [firstFeature, ...restFeatures], + }, + rotationDegreeParam, + ); + if (sourceToRotate && sourceToRotate instanceof GeoJSONSource) { + // @ts-ignore + sourceToRotate.setData(pointRotatedGeoJson); + } + } + if (firstFeature.geometry.type === 'LineString') { + const [firstCoordinate, ...restCoordinates] = + firstFeature.geometry.coordinates; + const rotatedGeoJson = rotateGeoJSON( + { + features: [ + // @ts-ignore + { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: excludeFirstFeature + ? restCoordinates + : [firstCoordinate, ...restCoordinates], + }, + }, + ], + type: 'FeatureCollection', + }, + rotationDegreeParam, + ); + if (sourceToRotate && sourceToRotate instanceof GeoJSONSource) { + // @ts-ignore + sourceToRotate.setData(rotatedGeoJson); + } + } } }); }; @@ -321,90 +367,121 @@ const MapSection = ({ className }: { className?: string }) => { }; }, [taskWayPointsData]); - useEffect(() => { - if (!rotatedTaskWayPoints) return; - rotatedTaskWayPointsRef.current = rotatedTaskWayPoints; - }, [rotatedTaskWayPoints]); - - // function that handles drag and calculates rotation - const handleDrag = useCallback((event: any) => { - if (!draggingRef.current) { - draggingRef.current = true; - } - const { originalCoordinates } = event; - const centroidCoordinates = calculateCentroid(bboxRef.current || []); - - const calculatedAngleFromCoordinates = calculateAngle( - [originalCoordinates[0], originalCoordinates[1]], - [event.lngLat?.lng, event.lngLat?.lat], - [centroidCoordinates.lng, centroidCoordinates.lat], - ); - // Update rotation continuously while dragging - setRotationDegree(calculatedAngleFromCoordinates); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + function updateLayerCoordinates( + layerIds: Record[], + coordinate: [number, number], + ) { + // Iterate over the array of layer IDs + if (!map || !isMapLoaded) return; + layerIds.forEach(layerId => { + // Check if the layer is of type 'symbol' (or any other type) + const source = map.getSource(layerId.id); // Get the source of the layer - function getGeoJSONDataFromMap(sourceIds: string[]) { - const geojsonData: Record = {}; + // Update the feature on the map - if (!mapRef.current) return geojsonData; - sourceIds.forEach(sourceId => { - const source = mapRef.current?.getSource(sourceId); if (source && source instanceof GeoJSONSource) { - // eslint-disable-next-line no-underscore-dangle - geojsonData[sourceId] = source._data; + const geoJsonData = source._data; + // @ts-ignore + const { features, ...restGeoData } = geoJsonData; + const coordinates = features[0].geometry.coordinates; + if (layerId.type === 'MultiString') { + const nearestCoordinate = findNearestCoordinate( + coordinates[0], + coordinates[coordinates.length - 1], + takeOffPointRef.current || [0, 0], + ); + let indexToReplace = 0; + if (nearestCoordinate === 'second') { + indexToReplace = coordinates.length; + } + features[0].geometry.coordinates[indexToReplace] = coordinate; + const updatedLineStringData = features[0].geometry.coordinates; + if (indexToReplace !== 0) { + updatedLineStringData.reverse().pop(); + } + source.setData({ features, ...restGeoData }); + } + if (layerId.type === 'Points') { + const nearestPoint = findNearestCoordinate( + coordinates, + features[features.length - 1].geometry.coordinates, + takeOffPointRef.current || [0, 0], + ); + let pointIndexToReplace = 0; + if (nearestPoint === 'second') { + pointIndexToReplace = features.length; + } + if (pointIndexToReplace !== 0) { + features.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [], + }, + properties: {}, + }); + } + features[pointIndexToReplace].geometry.coordinates = coordinate; + const rotatedFeatures = features; + if (pointIndexToReplace !== 0) { + swapFirstAndLast(rotatedFeatures); + features.pop(); + } + source.setData({ features, ...restGeoData }); + } } }); - - return geojsonData; } - // function to handle end of drag - const handleDragEnd = useCallback(() => { - const dataRetrieved = getGeoJSONDataFromMap([ - 'rotated-waypoint-line', - 'rotated-waypoint-points', - 'rotated-waypoint-points-image', - ]); - const newTaskGeoJson = { - geojsonAsLineString: dataRetrieved['rotated-waypoint-line'], - geojsonListOfPoint: dataRetrieved['rotated-waypoint-points'], - }; - setTaskWayPoints(newTaskGeoJson); - if (draggingRef.current) { - draggingRef.current = false; // Reset dragging state + useEffect(() => { + if (!dragging) { + rotateLayerGeoJSON( + ['waypoint-line', 'waypoint-points'], + rotationAngle, + ['waypoint-line', 'waypoint-points'], + false, + ); + updateLayerCoordinates( + [ + { id: 'waypoint-line', type: 'MultiString' }, + { id: 'waypoint-points', type: 'Points' }, + ], + takeOffPointRef.current || [0, 0], + ); + + return; } - }, []); + rotateLayerGeoJSON( + ['rotated-waypoint-line', 'rotated-waypoint-points'], + rotationAngle, + ['waypoint-line', 'waypoint-points'], + true, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rotationAngle, dragging]); function handleRotationToggle() { if (!map || !isMapLoaded) return; setIsRotationEnabled(!isRotationEnabled); - if (!isRotationEnabled) { - map.dragPan.disable(); - } else { - map.dragPan.enable(); - } } + useEffect(() => { + if (!map || !isMapLoaded) return; - // Call the function to update rotation for each layer - rotateLayerGeoJSON( - [ - 'rotated-waypoint-line', - 'rotated-waypoint-points', - 'rotated-waypoint-points-image', - ], - rotationDegree, - ); + if (!dragging) { + setVisibilityOfLayers(mapLayerIDs, 'visible'); + return; + } + setVisibilityOfLayers(mapLayerIDs, 'none'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dragging, isMapLoaded, map]); return ( <>
{ - {taskWayPoints && ( + {taskWayPoints && !taskDataPolygonIsFetching && ( <> {/* render line */} { image={right} symbolPlacement="line" iconAnchor="center" - onDrag={handleDrag} - onDragEnd={handleDragEnd} - needDragEvent={isRotationEnabled} /> {/* render points */} { ], }, }} - onDrag={handleDrag} - onDragEnd={handleDragEnd} - needDragEvent={isRotationEnabled} /> {/* render image and only if index is 0 */} { imageLayerOptions={{ filter: ['==', 'index', 0], }} - onDrag={handleDrag} - onDragEnd={handleDragEnd} /> )} - {isRotationEnabled && draggingRef.current && ( + {isRotationEnabled && dragging && ( <> {/* render line */} { ], }, }} - onDrag={handleDrag} - onDragEnd={handleDragEnd} - /> - {/* render image and only if index is 0 */} - )} -
-
+
+
+ )} +
+ ); +} + +const AsyncPopup = forwardRef( + ( + { + map, + fetchPopupData, + popupUI, + title, + handleBtnClick, + isLoading = false, + onClose, + hideButton = false, + getCoordOnProperties = false, + showPopup = (_clickedFeature: Record) => true, + openPopupFor, + popupCoordinate, + }: IAsyncPopup, + ref, + ) => { + const [properties, setProperties] = useState | null>( + null, + ); + const [coordinates, setCoordinates] = useState(); + const [isPopupOpen, setIsPopupOpen] = useState(false); + + useEffect(() => { + if (!map) return; + function displayPopup(e: MapMouseEvent): void { + if (!map) return; + const features = map.queryRenderedFeatures(e.point); + const clickedFeature = features?.[0]; + + // in case of popup rendering conditionally + if (!showPopup(clickedFeature)) return; + + if (!clickedFeature) return; + setProperties( + getCoordOnProperties + ? { + ...clickedFeature.properties, + layer: clickedFeature.source, + coordinates: e.lngLat, + } + : { + ...clickedFeature.properties, + layer: clickedFeature.source, + }, + ); + + setCoordinates(e.lngLat as unknown as number[]); + // popup.setLngLat(e.lngLat); + } + map.on('click', displayPopup); + }, [map, getCoordOnProperties, showPopup]); + + useEffect(() => { + if (!map || !properties) return; + fetchPopupData?.(properties); + }, [map, properties]); // eslint-disable-line + + useEffect(() => { + if (!map || !properties || !popupUI) return; + const htmlString = renderToString( + void} + />, + ); + popup.setHTML(htmlString).addTo(map); + popup.setLngLat(coordinates as LngLatLike); + setIsPopupOpen(true); + }, [ + handleBtnClick, + hideButton, + isLoading, + map, + onClose, + popupUI, + properties, + title, + coordinates, + ]); + + useEffect(() => { + if (!map || !openPopupFor || !popupCoordinate) return; + setProperties(openPopupFor); + setCoordinates(popupCoordinate); + }, [map, openPopupFor, popupCoordinate]); + + useEffect(() => { + const closeBtn = document.getElementById('popup-close-button'); + const popupBtn = document.getElementById('popup-button'); + + const handleCloseBtnClick = () => { + popup.remove(); + onClose?.(); + setProperties(null); + setIsPopupOpen(false); + }; + + const handlePopupBtnClick = () => { + if (!properties) return; + handleBtnClick?.(properties); + }; + + closeBtn?.addEventListener('click', handleCloseBtnClick); + popupBtn?.addEventListener('click', handlePopupBtnClick); + + return () => { + closeBtn?.removeEventListener('click', handleCloseBtnClick); + popupBtn?.removeEventListener('click', handlePopupBtnClick); + }; + }, [onClose, isPopupOpen, properties, handleBtnClick]); + + if (!properties) return
; + return null; + }, +); +export default AsyncPopup; diff --git a/src/frontend/src/components/common/RotationCue/index.tsx b/src/frontend/src/components/common/RotationCue/index.tsx new file mode 100644 index 00000000..1eba21f5 --- /dev/null +++ b/src/frontend/src/components/common/RotationCue/index.tsx @@ -0,0 +1,84 @@ +/* eslint-disable no-unused-vars */ +import React, { useState } from 'react'; + +type RotationCueProps = { + setRotation: (rotation: number) => void; + rotation: number; + dragging: boolean; + setDragging: (dragging: boolean) => void; +}; +const RotationCue = ({ + setRotation, + rotation, + setDragging, + dragging, +}: RotationCueProps) => { + const [startAngle, setStartAngle] = useState(0); + + const handleMouseDown = (event: React.MouseEvent) => { + const { clientX, clientY } = event; + const circle = (event.target as HTMLElement).getBoundingClientRect(); + const centerX = circle.left + circle.width / 2; + const centerY = circle.top + circle.height / 2; + + const radians = Math.atan2(clientY - centerY, clientX - centerX); + const degrees = (radians * (180 / Math.PI) + 360) % 360; + setStartAngle(degrees - rotation); // Offset for smooth dragging + setDragging(true); + }; + + const handleMouseMove = (event: React.MouseEvent) => { + if (!dragging) return; + + const { clientX, clientY } = event; + const circle = (event.target as HTMLElement).getBoundingClientRect(); + const centerX = circle.left + circle.width / 2; + const centerY = circle.top + circle.height / 2; + + const radians = Math.atan2(clientY - centerY, clientX - centerX); + const degrees = (radians * (180 / Math.PI) + 360) % 360; + + let rotationDelta = degrees - startAngle; + if (rotationDelta < 0) { + rotationDelta += 360; + } + + setRotation(rotationDelta); + }; + + const handleMouseUp = () => { + setDragging(false); + }; + + return ( +
+
+ {/* Rotating Line */} +
+ + {/* Static Center */} +

+ {rotation.toFixed(2)} +

+
+
+
+ ); +}; + +export default RotationCue; diff --git a/src/frontend/src/constants/droneOperator.ts b/src/frontend/src/constants/droneOperator.ts index 8624cc25..6a70bac7 100644 --- a/src/frontend/src/constants/droneOperator.ts +++ b/src/frontend/src/constants/droneOperator.ts @@ -29,3 +29,11 @@ export const descriptionData = [ ], ]; export const descriptionTitle = ['Task Description', 'Flight Parameters']; + +export const mapLayerIDs = [ + 'waypoint-points-layer', + 'waypoint-points-image-layer', + 'waypoint-line-layer', + 'waypoint-points-image-image/logo', + 'waypoint-line-image/logo', +]; diff --git a/src/frontend/src/utils/index.ts b/src/frontend/src/utils/index.ts index c39b93a3..bf63d50d 100644 --- a/src/frontend/src/utils/index.ts +++ b/src/frontend/src/utils/index.ts @@ -213,3 +213,35 @@ export function calculateCentroidFromCoordinates(coordinates: any[]) { // Convert the results to degrees return [RadtoDegrees(lat), RadtoDegrees(lon)]; } + +export function findNearestCoordinate( + coord1: number[], + coord2: number[], + center: number[], +) { + // Function to calculate distance between two points + const calculateDistance = (point1: number[], point2: number[]) => { + const xDiff = point2[0] - point1[0]; + const yDiff = point2[1] - point1[1]; + return Math.sqrt(xDiff * xDiff + yDiff * yDiff); + }; + + // Calculate the distance of the first and second coordinates from the center + const distance1 = calculateDistance(coord1, center); + const distance2 = calculateDistance(coord2, center); + + // Return the nearest coordinate + return distance1 <= distance2 ? 'first' : 'second'; +} + +export function swapFirstAndLast(arr: T[]): T[] { + if (arr.length < 2) { + throw new Error('Array must have at least two elements to swap.'); + } + + // Swap the first and last elements using destructuring + // eslint-disable-next-line no-param-reassign + [arr[0], arr[arr.length - 1]] = [arr[arr.length - 1], arr[0]]; + + return arr; +}