From 245fcf0dab31b845f757ff3e71602d5885e651f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:45:00 +0000 Subject: [PATCH 01/22] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/uv-pre-commit: 0.5.5 → 0.5.7](https://github.com/astral-sh/uv-pre-commit/compare/0.5.5...0.5.7) - [github.com/commitizen-tools/commitizen: v4.0.0 → v4.1.0](https://github.com/commitizen-tools/commitizen/compare/v4.0.0...v4.1.0) - [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.2) - [github.com/pycontribs/mirrors-prettier: v3.3.3 → v3.4.2](https://github.com/pycontribs/mirrors-prettier/compare/v3.3.3...v3.4.2) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 457e1429..5f182d17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -79,7 +79,7 @@ repos: # Deps: ensure Python uv lockfile is up to date - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.5.5 + rev: 0.5.7 hooks: - id: uv-lock files: src/backend/pyproject.toml @@ -87,7 +87,7 @@ repos: # Versioning: Commit messages & changelog - repo: https://github.com/commitizen-tools/commitizen - rev: v4.0.0 + rev: v4.1.0 hooks: - id: commitizen stages: [commit-msg] @@ -95,7 +95,7 @@ repos: # Lint / autoformat: Python code - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: "v0.8.1" + rev: "v0.8.2" hooks: # Run the linter - id: ruff @@ -107,7 +107,7 @@ repos: # Autoformat: YAML, JSON, Markdown, etc. - repo: https://github.com/pycontribs/mirrors-prettier - rev: v3.3.3 + rev: v3.4.2 hooks: - id: prettier args: From 7725889e71f271f9670d7c92c5f8a53692143886 Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 11:06:03 +0545 Subject: [PATCH 02/22] feat: update rotation funcitons --- .../DroneOperatorTask/MapSection/index.tsx | 283 +++++++++++------- 1 file changed, 170 insertions(+), 113 deletions(-) diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index 76420d8e..3109dc70 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -1,3 +1,7 @@ +/* eslint-disable no-unused-vars */ +/* 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 marker from '@Assets/images/marker.png'; @@ -37,13 +41,11 @@ 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 { getLastCoordinates } from '@Utils/index'; import GetCoordinatesOnClick from './GetCoordinatesOnClick'; import ShowInfo from './ShowInfo'; @@ -51,15 +53,12 @@ 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 queryClient = useQueryClient(); @@ -67,6 +66,8 @@ const MapSection = ({ className }: { className?: string }) => { 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: { @@ -85,6 +86,12 @@ const MapSection = ({ className }: { className?: string }) => { mapRef.current = map; }, [map, isMapLoaded]); + function setVisibilityOfLayers(layerIds: string[], visibility: string) { + layerIds.forEach(layerId => { + map?.setLayoutProperty(layerId, 'visibility', visibility); + }); + } + const { data: taskDataPolygon, // isFetching: isProjectDataFetching, @@ -253,49 +260,60 @@ 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'}`, ); + // 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', + // `${!showTakeOffPoint ? 'visible' : 'none'}`, + // ); setShowTakeOffPoint(!showTakeOffPoint); } const rotateLayerGeoJSON = ( layerIds: string[], rotationDegreeParam: number, + baseLayerIds: string[], ) => { if (!mapRef.current) return; - layerIds.forEach(layerId => { - const source = mapRef.current?.getSource(layerId); + baseLayerIds.forEach((baseLayerId, index) => { + const source = mapRef.current?.getSource(baseLayerId); + const sourceToRotate = mapRef.current?.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; + const rotatedGeoJSON = rotateGeoJSON( + // @ts-ignore + baseGeoData, + rotationDegreeParam, + ); + if (sourceToRotate && sourceToRotate instanceof GeoJSONSource) { + // @ts-ignore + sourceToRotate.setData(rotatedGeoJSON); + } } }); }; @@ -321,81 +339,122 @@ 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 || []); + 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); + }; - const calculatedAngleFromCoordinates = calculateAngle( - [originalCoordinates[0], originalCoordinates[1]], - [event.lngLat?.lng, event.lngLat?.lat], - [centroidCoordinates.lng, centroidCoordinates.lat], - ); - // Update rotation continuously while dragging - setRotationDegree(calculatedAngleFromCoordinates); + // Calculate the distance of the first and second coordinates from the center + const distance1 = calculateDistance(coord1, center); + const distance2 = calculateDistance(coord2, center); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Return the nearest coordinate + return distance1 <= distance2 ? 'first' : 'second'; + } + + 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], + centeroidRef.current || [0, 0], + ); + let indexToReplace = 0; + if (nearestCoordinate === 'second') { + indexToReplace = coordinates.length - 1; + } + features[0].geometry.coordinates[indexToReplace] = coordinate; + source.setData({ features, ...restGeoData }); + } + if (layerId.type === 'Points') { + const nearestPoint = findNearestCoordinate( + coordinates, + features[features.length - 1].geometry.coordinates, + centeroidRef.current || [0, 0], + ); + let pointIndexToReplace = 0; + if (nearestPoint === 'second') { + pointIndexToReplace = features.length - 1; + } + features[pointIndexToReplace].geometry.coordinates = coordinate; + const updatedFeatures = features.map((featurex: any) => { + if (pointIndexToReplace === 0) return featurex; + return { + ...featurex, + properties: { + ...featurex.properties, + heading: -90, + }, + }; + }); + source.setData({ features: updatedFeatures, ...restGeoData }); + } + // console.log(modifiedGeoJson, 'modifiedGeoJson'); } }); - - 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', + ]); + updateLayerCoordinates( + [ + { id: 'waypoint-line', type: 'MultiString' }, + { id: 'waypoint-points', type: 'Points' }, + ], + centeroidRef.current || [0, 0], + ); + + return; } - }, []); + rotateLayerGeoJSON( + ['rotated-waypoint-line', 'rotated-waypoint-points'], + rotationAngle, + ['waypoint-line', 'waypoint-points'], + ); + // 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 ( <> @@ -436,9 +495,6 @@ const MapSection = ({ className }: { className?: string }) => { 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 */} { imageLayerOptions={{ filter: ['==', 'index', 0], }} - onDrag={handleDrag} - onDragEnd={handleDragEnd} /> )} @@ -619,7 +666,7 @@ const MapSection = ({ className }: { className?: string }) => {
-
+
+
+ )} +
+ ); +} + +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(null); + 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); + // 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); + 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-new'); + const popupBtn = document.getElementById('popup-button'); + + const handleCloseBtnClick = () => { + popup.remove(); + onClose?.(); + setProperties(null); + }; + + 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; From 56befcdc2d42c8a115c3c2947523677226256c36 Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 17:29:26 +0545 Subject: [PATCH 06/22] feat: add rotattionCue component --- .../components/common/RotationCue/index.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/frontend/src/components/common/RotationCue/index.tsx 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..b8855161 --- /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: any) => { + const { clientX, clientY } = event; + const circle = event.target.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: any) => { + if (!dragging) return; + + const { clientX, clientY } = event; + const circle = event.target.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; From 75c4e7c9619d71e44868766ce7aa46885becf1e8 Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 17:29:56 +0545 Subject: [PATCH 07/22] feat: optimize flightplan rotation --- .../DroneOperatorTask/MapSection/index.tsx | 72 +++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index 8c493865..14e835d9 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -222,8 +222,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, @@ -275,6 +278,7 @@ const MapSection = ({ className }: { className?: string }) => { layerIds: string[], rotationDegreeParam: number, baseLayerIds: string[], + excludeFirstFeature?: boolean, ) => { if (!map || !isMapLoaded) return; @@ -284,14 +288,51 @@ const MapSection = ({ className }: { className?: string }) => { if (source && source instanceof GeoJSONSource) { const baseGeoData = source._data; - const rotatedGeoJSON = rotateGeoJSON( - // @ts-ignore - baseGeoData, - rotationDegreeParam, - ); - if (sourceToRotate && sourceToRotate instanceof GeoJSONSource) { - // @ts-ignore - sourceToRotate.setData(rotatedGeoJSON); + 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); + } } } }); @@ -386,10 +427,12 @@ const MapSection = ({ className }: { className?: string }) => { useEffect(() => { if (!dragging) { - rotateLayerGeoJSON(['waypoint-line', 'waypoint-points'], rotationAngle, [ - 'waypoint-line', - 'waypoint-points', - ]); + rotateLayerGeoJSON( + ['waypoint-line', 'waypoint-points'], + rotationAngle, + ['waypoint-line', 'waypoint-points'], + false, + ); updateLayerCoordinates( [ { id: 'waypoint-line', type: 'MultiString' }, @@ -404,6 +447,7 @@ const MapSection = ({ className }: { className?: string }) => { ['rotated-waypoint-line', 'rotated-waypoint-points'], rotationAngle, ['waypoint-line', 'waypoint-points'], + true, ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [rotationAngle, dragging]); From 6441699d5654e1aa5f920595b8159561892f1559 Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 17:30:11 +0545 Subject: [PATCH 08/22] feat: add layer ids constants --- src/frontend/src/constants/droneOperator.ts | 8 ++++++++ 1 file changed, 8 insertions(+) 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', +]; From 2957cfd605130eba1f74a43c85ce9b7713af562c Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 17:31:55 +0545 Subject: [PATCH 09/22] refactor: remove unused vars --- .../components/DroneOperatorTask/MapSection/index.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index 14e835d9..b4ba7d29 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ /* eslint-disable no-param-reassign */ /* eslint-disable no-underscore-dangle */ /* eslint-disable prefer-destructuring */ @@ -45,11 +44,7 @@ import { useGetProjectsDetailQuery } from '@Api/projects'; import { toast } from 'react-toastify'; import RotatingCircle from '@Components/common/RotationCue'; import { mapLayerIDs } from '@Constants/droneOperator'; -import { - findNearestCoordinate, - getLastCoordinates, - swapFirstAndLast, -} from '@Utils/index'; +import { findNearestCoordinate, swapFirstAndLast } from '@Utils/index'; import GetCoordinatesOnClick from './GetCoordinatesOnClick'; import ShowInfo from './ShowInfo'; @@ -414,9 +409,9 @@ const MapSection = ({ className }: { className?: string }) => { }); } features[pointIndexToReplace].geometry.coordinates = coordinate; - let rotatedFeatures = features; + const rotatedFeatures = features; if (pointIndexToReplace !== 0) { - rotatedFeatures = swapFirstAndLast(rotatedFeatures); + swapFirstAndLast(rotatedFeatures); features.pop(); } source.setData({ features, ...restGeoData }); From d28cfac703021df92c44e1e7f1b57e6235d77905 Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 17:32:31 +0545 Subject: [PATCH 10/22] feat: add function to swap first and last coordinate and fucntion to get last coordinate --- src/frontend/src/utils/index.ts | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/frontend/src/utils/index.ts b/src/frontend/src/utils/index.ts index c39b93a3..ae05e4e6 100644 --- a/src/frontend/src/utils/index.ts +++ b/src/frontend/src/utils/index.ts @@ -213,3 +213,88 @@ export function calculateCentroidFromCoordinates(coordinates: any[]) { // Convert the results to degrees return [RadtoDegrees(lat), RadtoDegrees(lon)]; } + +type Coordinate = [number, number]; +type GeoJSONFeature = { + type: 'Feature'; + geometry: { + type: 'LineString' | 'Polygon'; + coordinates: Coordinate[] | Coordinate[][]; + }; + properties: Record; +}; + +export function swapFirstAndLastCoordinate( + geojson: GeoJSONFeature, +): GeoJSONFeature { + // Clone the GeoJSON object to avoid mutation + const updatedGeoJSON: GeoJSONFeature = { + ...geojson, + geometry: { ...geojson.geometry }, + }; + + if (geojson.geometry.type === 'LineString') { + const coordinates = [...(geojson.geometry.coordinates as Coordinate[])]; + if (coordinates.length < 2) { + throw new Error('LineString must have at least two coordinates to swap.'); + } + // Swap the first and last coordinates + [coordinates[0], coordinates[coordinates.length - 1]] = [ + coordinates[coordinates.length - 1], + coordinates[0], + ]; + updatedGeoJSON.geometry.coordinates = coordinates; + } else if (geojson.geometry.type === 'Polygon') { + const coordinates = [...(geojson.geometry.coordinates as Coordinate[][])]; + const firstRing = coordinates[0]; + if (!firstRing || firstRing.length < 2) { + throw new Error( + 'Polygon must have at least two coordinates in the first ring to swap.', + ); + } + // Swap the first and last coordinates of the first ring + [firstRing[0], firstRing[firstRing.length - 1]] = [ + firstRing[firstRing.length - 1], + firstRing[0], + ]; + updatedGeoJSON.geometry.coordinates = coordinates; + } else { + throw new Error( + 'Unsupported geometry type. Only LineString and Polygon are supported.', + ); + } + + return updatedGeoJSON; +} + +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; +} From c1e67436ce1084a892b5808985afa1c826ce94fa Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 17:38:48 +0545 Subject: [PATCH 11/22] style: add responsive attributes to rotation cue --- .../src/components/DroneOperatorTask/MapSection/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index b4ba7d29..39a1ecaf 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -716,7 +716,7 @@ const MapSection = ({ className }: { className?: string }) => { /> )} {isRotationEnabled && ( -
+
Date: Tue, 10 Dec 2024 17:46:21 +0545 Subject: [PATCH 12/22] refactor: update swapFirstAndLastCoordinate --- src/frontend/src/utils/index.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/frontend/src/utils/index.ts b/src/frontend/src/utils/index.ts index ae05e4e6..79981520 100644 --- a/src/frontend/src/utils/index.ts +++ b/src/frontend/src/utils/index.ts @@ -235,33 +235,13 @@ export function swapFirstAndLastCoordinate( if (geojson.geometry.type === 'LineString') { const coordinates = [...(geojson.geometry.coordinates as Coordinate[])]; - if (coordinates.length < 2) { - throw new Error('LineString must have at least two coordinates to swap.'); - } + // Swap the first and last coordinates [coordinates[0], coordinates[coordinates.length - 1]] = [ coordinates[coordinates.length - 1], coordinates[0], ]; updatedGeoJSON.geometry.coordinates = coordinates; - } else if (geojson.geometry.type === 'Polygon') { - const coordinates = [...(geojson.geometry.coordinates as Coordinate[][])]; - const firstRing = coordinates[0]; - if (!firstRing || firstRing.length < 2) { - throw new Error( - 'Polygon must have at least two coordinates in the first ring to swap.', - ); - } - // Swap the first and last coordinates of the first ring - [firstRing[0], firstRing[firstRing.length - 1]] = [ - firstRing[firstRing.length - 1], - firstRing[0], - ]; - updatedGeoJSON.geometry.coordinates = coordinates; - } else { - throw new Error( - 'Unsupported geometry type. Only LineString and Polygon are supported.', - ); } return updatedGeoJSON; From cd9fde0ed5a80760586ef7b94ebb295df7f8680c Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Tue, 10 Dec 2024 17:47:59 +0545 Subject: [PATCH 13/22] refactor: remove swapFirstAndLast coordinate function --- src/frontend/src/utils/index.ts | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/src/frontend/src/utils/index.ts b/src/frontend/src/utils/index.ts index 79981520..bf63d50d 100644 --- a/src/frontend/src/utils/index.ts +++ b/src/frontend/src/utils/index.ts @@ -214,39 +214,6 @@ export function calculateCentroidFromCoordinates(coordinates: any[]) { return [RadtoDegrees(lat), RadtoDegrees(lon)]; } -type Coordinate = [number, number]; -type GeoJSONFeature = { - type: 'Feature'; - geometry: { - type: 'LineString' | 'Polygon'; - coordinates: Coordinate[] | Coordinate[][]; - }; - properties: Record; -}; - -export function swapFirstAndLastCoordinate( - geojson: GeoJSONFeature, -): GeoJSONFeature { - // Clone the GeoJSON object to avoid mutation - const updatedGeoJSON: GeoJSONFeature = { - ...geojson, - geometry: { ...geojson.geometry }, - }; - - if (geojson.geometry.type === 'LineString') { - const coordinates = [...(geojson.geometry.coordinates as Coordinate[])]; - - // Swap the first and last coordinates - [coordinates[0], coordinates[coordinates.length - 1]] = [ - coordinates[coordinates.length - 1], - coordinates[0], - ]; - updatedGeoJSON.geometry.coordinates = coordinates; - } - - return updatedGeoJSON; -} - export function findNearestCoordinate( coord1: number[], coord2: number[], From 858af1136d54110e1d3a5cd8f3fb86e60e5cd09e Mon Sep 17 00:00:00 2001 From: Pradip Thapa Date: Wed, 11 Dec 2024 09:31:24 +0545 Subject: [PATCH 14/22] fix: update the flight plan time & updated waypoints conuts if user terrian follow (#390) --- src/backend/app/projects/project_routes.py | 32 +++++++++++- src/backend/app/utils.py | 51 +++++++++++++++++++- src/backend/app/waypoints/waypoint_routes.py | 11 +++-- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index a3872075..e6799b32 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -1,5 +1,6 @@ import json import os +import shutil import uuid from typing import Annotated, Optional from uuid import UUID @@ -39,7 +40,7 @@ ) from app.users import user_schemas from minio.deleteobjects import DeleteObject -from drone_flightplan import waypoints +from drone_flightplan import waypoints, add_elevation_from_dem router = APIRouter( prefix=f"{settings.API_PREFIX}/projects", @@ -668,6 +669,7 @@ async def get_project_waypoints_counts( meters: float = 100, project_geojson: UploadFile = File(...), is_terrain_follow: bool = False, + dem: UploadFile = File(None), user_data: AuthUser = Depends(login_required), ): """ @@ -708,6 +710,32 @@ async def get_project_waypoints_counts( generate_3d=generate_3d, take_off_point=None, ) + + # Handle terrain-following logic if a DEM is provided + points_with_elevation = points + if is_terrain_follow and dem: + temp_dir = f"/tmp/{uuid.uuid4()}" + try: + os.makedirs(temp_dir, exist_ok=True) + dem_path = os.path.join(temp_dir, "dem.tif") + outfile_with_elevation = os.path.join( + temp_dir, "output_file_with_elevation.geojson" + ) + + with open(dem_path, "wb") as dem_file: + dem_file.write(await dem.read()) + + add_elevation_from_dem(dem_path, points, outfile_with_elevation) + + with open(outfile_with_elevation, "r") as inpointsfile: + points_with_elevation = inpointsfile.read() + except Exception as e: + log.error(f"Error processing DEM: {e}") + + finally: + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + return { - "avg_no_of_waypoints": len(json.loads(points)["features"]), + "avg_no_of_waypoints": len(json.loads(points_with_elevation)["features"]), } diff --git a/src/backend/app/utils.py b/src/backend/app/utils.py index bcc54154..41212919 100644 --- a/src/backend/app/utils.py +++ b/src/backend/app/utils.py @@ -5,7 +5,7 @@ import json import base64 from datetime import datetime, timezone -from typing import Optional, Union, Any +from typing import Dict, Optional, Union, Any from geojson_pydantic import Feature, MultiPolygon, Polygon from geojson_pydantic import FeatureCollection as FeatCol from geoalchemy2 import WKBElement @@ -21,6 +21,9 @@ from email.mime.text import MIMEText from email.utils import formataddr from aiosmtplib import send as send_email +from shapely.geometry import Point +from shapely.ops import transform +from pyproj import Transformer log = logging.getLogger(__name__) @@ -551,3 +554,49 @@ async def send_project_approval_email_to_regulator( subject="Project Review Request for Drone Operations Approval", html_content=html_content, ) + + +def calculate_flight_time_from_placemarks(placemarks: Dict) -> Dict: + """ + Calculate the total and average flight time based on placemarks and dynamically format the output. + + Args: + placemarks (Dict): GeoJSON-like data structure with flight plan. + + Returns: + Dict: Contains formatted total flight time and segment times. + """ + total_time = 0 + features = placemarks["features"] + transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True) + for i in range(1, len(features)): + # Extract current and previous coordinates + prev_coords = features[i - 1]["geometry"]["coordinates"][:2] + curr_coords = features[i]["geometry"]["coordinates"][:2] + speed = features[i]["properties"]["speed"] # Speed in m/s + + # Create Shapely Points and transform to planar coordinates for distance calculation + prev_point = Point(transform(transformer.transform, Point(prev_coords))) + curr_point = Point(transform(transformer.transform, Point(curr_coords))) + + # Calculate distance (meters) and time (seconds) + distance = prev_point.distance(curr_point) + segment_time = distance / speed + total_time += segment_time + + # Dynamically format the total flight time + hours = int(total_time // 3600) + minutes = int((total_time % 3600) // 60) + seconds = round(total_time % 60, 2) + + if total_time < 60: + formatted_time = f"{seconds} seconds" + elif total_time < 3600: + formatted_time = f"{minutes} minutes {seconds:.2f} seconds" + else: + formatted_time = f"{hours} hours {minutes} minutes {seconds:.2f} seconds" + + return { + "total_flight_time": formatted_time, + "total_flight_time_seconds": round(total_time, 2), + } diff --git a/src/backend/app/waypoints/waypoint_routes.py b/src/backend/app/waypoints/waypoint_routes.py index fab6f6ba..4613e08c 100644 --- a/src/backend/app/waypoints/waypoint_routes.py +++ b/src/backend/app/waypoints/waypoint_routes.py @@ -18,9 +18,11 @@ get_take_off_point_from_db, update_take_off_point_in_db, ) -from app.waypoints.waypoint_logic import check_point_within_buffer +from app.waypoints.waypoint_logic import ( + check_point_within_buffer, +) from app.db import database -from app.utils import merge_multipolygon +from app.utils import calculate_flight_time_from_placemarks, merge_multipolygon from app.s3 import get_file_from_bucket from typing import Annotated from psycopg import Connection @@ -28,6 +30,7 @@ from shapely.geometry import shape from app.waypoints import waypoint_schemas + # Constant to convert gsd to Altitude above ground level GSD_to_AGL_CONST = 29.7 # For DJI Mini 4 Pro @@ -146,8 +149,8 @@ async def get_task_waypoint( return FileResponse( kmz_file, media_type="application/zip", filename="flight_plan.kmz" ) - - return placemarks + flight_data = calculate_flight_time_from_placemarks(placemarks) + return {"results": placemarks, "flight_data": flight_data} @router.post("/") From 85bd95f43d7b835da7fe1d57b3155b31d0737475 Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Wed, 11 Dec 2024 11:11:16 +0545 Subject: [PATCH 15/22] feat: update api for taskDataPolygon --- .../DroneOperatorTask/MapSection/index.tsx | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index 39a1ecaf..616acfc1 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -2,7 +2,11 @@ /* 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'; @@ -40,7 +44,6 @@ 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 { useGetProjectsDetailQuery } from '@Api/projects'; import { toast } from 'react-toastify'; import RotatingCircle from '@Components/common/RotationCue'; import { mapLayerIDs } from '@Constants/droneOperator'; @@ -87,24 +90,34 @@ const MapSection = ({ className }: { className?: string }) => { 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, @@ -479,7 +492,7 @@ const MapSection = ({ className }: { className?: string }) => { - {taskWayPoints && !isProjectDataFetching && ( + {taskWayPoints && !taskDataPolygonIsFetching && ( <> {/* render line */} { 'fill-opacity': 0.6, }, }} - // layerOptions={getLayerOptionsByStatus( - // taskStatusObj?.[`${task?.id}`], - // )} - // hasImage={ - // taskStatusObj?.[`${task?.id}`] === 'LOCKED_FOR_MAPPING' || false - // } - // image={lock} /> {newTakeOffPoint === 'place_on_map' && ( From 4e84a1dea4f67f01d59161e436c9b326186311ce Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Wed, 11 Dec 2024 11:11:37 +0545 Subject: [PATCH 16/22] refactor: update popup close fn --- .../common/MapLibreComponents/NewAsyncPopup/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/common/MapLibreComponents/NewAsyncPopup/index.tsx b/src/frontend/src/components/common/MapLibreComponents/NewAsyncPopup/index.tsx index 152b1ea7..3e88f5b3 100644 --- a/src/frontend/src/components/common/MapLibreComponents/NewAsyncPopup/index.tsx +++ b/src/frontend/src/components/common/MapLibreComponents/NewAsyncPopup/index.tsx @@ -48,7 +48,7 @@ function PopupUIComponent({

{title}

)} ( }, [map, openPopupFor, popupCoordinate]); useEffect(() => { - const closeBtn = document.getElementById('popup-close-button-new'); + const closeBtn = document.getElementById('popup-close-button'); const popupBtn = document.getElementById('popup-button'); const handleCloseBtnClick = () => { popup.remove(); onClose?.(); setProperties(null); + setIsPopupOpen(false); }; const handlePopupBtnClick = () => { From de31906b4c2aee4629b824fbc5b4f7cb9de8a87c Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Wed, 11 Dec 2024 11:12:13 +0545 Subject: [PATCH 17/22] refactor: update type for mouse Events --- src/frontend/src/components/common/RotationCue/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/components/common/RotationCue/index.tsx b/src/frontend/src/components/common/RotationCue/index.tsx index b8855161..1eba21f5 100644 --- a/src/frontend/src/components/common/RotationCue/index.tsx +++ b/src/frontend/src/components/common/RotationCue/index.tsx @@ -15,9 +15,9 @@ const RotationCue = ({ }: RotationCueProps) => { const [startAngle, setStartAngle] = useState(0); - const handleMouseDown = (event: any) => { + const handleMouseDown = (event: React.MouseEvent) => { const { clientX, clientY } = event; - const circle = event.target.getBoundingClientRect(); + const circle = (event.target as HTMLElement).getBoundingClientRect(); const centerX = circle.left + circle.width / 2; const centerY = circle.top + circle.height / 2; @@ -27,11 +27,11 @@ const RotationCue = ({ setDragging(true); }; - const handleMouseMove = (event: any) => { + const handleMouseMove = (event: React.MouseEvent) => { if (!dragging) return; const { clientX, clientY } = event; - const circle = event.target.getBoundingClientRect(); + const circle = (event.target as HTMLElement).getBoundingClientRect(); const centerX = circle.left + circle.width / 2; const centerY = circle.top + circle.height / 2; From 9a44f58bc6dc89d5dabfdf93f0e766c44a10621d Mon Sep 17 00:00:00 2001 From: Niraj Adhikari <41701707+nrjadkry@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:24:29 +0545 Subject: [PATCH 18/22] fix: calculate bbox for the drone image (#393) --- src/backend/app/gcp/gcp_crud.py | 101 +++++++++++++++++++++++++++--- src/backend/app/gcp/gcp_routes.py | 6 +- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/src/backend/app/gcp/gcp_crud.py b/src/backend/app/gcp/gcp_crud.py index e376c035..02e5f6ac 100644 --- a/src/backend/app/gcp/gcp_crud.py +++ b/src/backend/app/gcp/gcp_crud.py @@ -6,6 +6,7 @@ from app.s3 import get_presigned_url from app.waypoints import waypoint_schemas from app.config import settings +from pyproj import Transformer async def calculate_bounding_box( @@ -103,7 +104,9 @@ async def find_matching_images_that_contains_point(bounding_boxes, gps_coordinat return matching_images -async def calculate_bbox_from_images_file(images_json_url: str): +async def calculate_bbox_from_images_file( + images_json_url: str, fov_degree: float, altitude: float +): """ Create bounding boxes for all images from a presigned JSON file URL. """ @@ -116,16 +119,12 @@ async def calculate_bbox_from_images_file(images_json_url: str): filename = image["filename"] lat = image["latitude"] lon = image["longitude"] - altitude = image["altitude"] width = image["width"] height = image["height"] - focal_ratio = image["focal_ratio"] - fnumber = image["fnumber"] - # Calculate the bounding box - bbox = await calculate_bounding_box( - lat, lon, width, height, focal_ratio, fnumber, altitude - ) + aspect_ratio = width / height + + bbox = await calc_bbox(lat, lon, altitude, fov_degree, aspect_ratio) bounding_boxes[filename] = bbox return bounding_boxes @@ -158,6 +157,82 @@ async def calculate_image_footprint( return width, height +async def calc_bbox(lat, long, altitude, fov_degree, aspect_ratio): + # Define the bounding box coordinates in EPSG:3857 + # Update offset function to work with EPSG:3857 coordinates + async def offset_coordinates_3857(x, y, dx, dy): + """ + Calculate new coordinates in EPSG:3857 given distance offsets. + """ + new_x = x + dx + new_y = y + dy + return new_x, new_y + + # Initialize transformer for WGS84 to EPSG:3857 and vice versa + wgs84_to_3857 = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True) + epsg_3857_to_wgs84 = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True) + + # Convert centroid coordinates to EPSG:3857 + centroid_3857 = wgs84_to_3857.transform(long, lat) + + # Calculate the width and height of the image footprint + footprint_height, footprint_width = await calculate_image_footprint( + altitude, fov_degree, aspect_ratio + ) + + # Calculate half-width and half-height in meters (same as before) + half_width = footprint_width / 2 + half_height = footprint_height / 2 + + # Calculate the four corners in EPSG:3857 + top_left_3857 = await offset_coordinates_3857( + centroid_3857[0], centroid_3857[1], -half_width, half_height + ) + top_right_3857 = await offset_coordinates_3857( + centroid_3857[0], centroid_3857[1], half_width, half_height + ) + bottom_right_3857 = await offset_coordinates_3857( + centroid_3857[0], centroid_3857[1], half_width, -half_height + ) + bottom_left_3857 = await offset_coordinates_3857( + centroid_3857[0], centroid_3857[1], -half_width, -half_height + ) + + # Convert corners back to WGS84 + top_left_wgs84 = epsg_3857_to_wgs84.transform(top_left_3857[0], top_left_3857[1]) + top_right_wgs84 = epsg_3857_to_wgs84.transform(top_right_3857[0], top_right_3857[1]) + bottom_right_wgs84 = epsg_3857_to_wgs84.transform( + bottom_right_3857[0], bottom_right_3857[1] + ) + bottom_left_wgs84 = epsg_3857_to_wgs84.transform( + bottom_left_3857[0], bottom_left_3857[1] + ) + + # Extract longitude and latitude values + longitudes = [ + top_left_wgs84[0], + top_right_wgs84[0], + bottom_right_wgs84[0], + bottom_left_wgs84[0], + ] + latitudes = [ + top_left_wgs84[1], + top_right_wgs84[1], + bottom_right_wgs84[1], + bottom_left_wgs84[1], + ] + + # Calculate the bounding box: [min_longitude, min_latitude, max_longitude, max_latitude] + bbox = [ + min(longitudes), # min_longitude + min(latitudes), # min_latitude + max(longitudes), # max_longitude + max(latitudes), # max_latitude + ] + + return bbox + + def find_images_with_coordinate( bounding_boxes: Dict[str, Tuple[float, float, float, float]], gps_coordinate: Tuple[float, float], @@ -200,7 +275,11 @@ def find_images_with_coordinate( async def process_images_for_point( - project_id: uuid.UUID, task_id: uuid.UUID, point: waypoint_schemas.PointField + project_id: uuid.UUID, + task_id: uuid.UUID, + point: waypoint_schemas.PointField, + fov_degree: float, + altitude: float, ) -> List[str]: """ Process images to find those containing a specific point and return their pre-signed URLs. @@ -221,7 +300,9 @@ async def process_images_for_point( s3_images_json_url = get_presigned_url(settings.S3_BUCKET_NAME, s3_images_json_path) # Fetch bounding boxes from the `images.json` file - bbox_list = await calculate_bbox_from_images_file(s3_images_json_url) + bbox_list = await calculate_bbox_from_images_file( + s3_images_json_url, fov_degree, altitude + ) # Extract the longitude and latitude of the point point_tuple = (point.longitude, point.latitude) diff --git a/src/backend/app/gcp/gcp_routes.py b/src/backend/app/gcp/gcp_routes.py index 8dc29c66..3bf1d1f1 100644 --- a/src/backend/app/gcp/gcp_routes.py +++ b/src/backend/app/gcp/gcp_routes.py @@ -20,4 +20,8 @@ async def find_images( point: waypoint_schemas.PointField = None, ) -> List[str]: """Find images that contain a specified point.""" - return await gcp_crud.process_images_for_point(project_id, task_id, point) + fov_degree = 82.1 # For DJI Mini 4 Pro + altitude = 100 # TODO: Get this from db + return await gcp_crud.process_images_for_point( + project_id, task_id, point, fov_degree, altitude + ) From 0b8c888be558d57cf3797d119719c4fe01b1d125 Mon Sep 17 00:00:00 2001 From: Bijay Rauniyar Date: Wed, 11 Dec 2024 11:27:13 +0545 Subject: [PATCH 19/22] refactor: update taskWayPointsData data --- .../src/components/DroneOperatorTask/MapSection/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index 616acfc1..fb6eb1c4 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -123,9 +123,9 @@ const MapSection = ({ className }: { className?: string }) => { projectId as string, taskId as string, { - select: (data: any) => { + select: ({ data }: any) => { const modifiedTaskWayPointsData = { - geojsonListOfPoint: data.data, + geojsonListOfPoint: data.results, geojsonAsLineString: { type: 'FeatureCollection', features: [ @@ -135,7 +135,7 @@ const MapSection = ({ className }: { className?: string }) => { geometry: { type: 'LineString', // get all coordinates - coordinates: coordAll(data.data), + coordinates: coordAll(data.results), }, }, ], @@ -658,7 +658,7 @@ const MapSection = ({ className }: { className?: string }) => {
-
+