Skip to content

Commit

Permalink
Merge pull request #395 from hotosm/feat/flightplan-rotation
Browse files Browse the repository at this point in the history
Feat/flightplan rotation
  • Loading branch information
subashtiwari1010 authored Dec 12, 2024
2 parents 6b23508 + 266a325 commit a42c5d4
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,6 @@ const ImageMapBox = () => {
'circle-stroke-width': 4,
'circle-stroke-color': 'red',
'circle-stroke-opacity': 1,
'circle-opacity': [
'match',
['get', 'index'],
0,
0,
Number(
// eslint-disable-next-line no-unsafe-optional-chaining
imageFilesGeoJsonData?.features?.length - 1,
),
0,
1,
],
},
}}
zoomToExtent
Expand Down
94 changes: 50 additions & 44 deletions src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import Skeleton from '@Components/RadixComponents/Skeleton';
import rotateGeoJSON from '@Utils/rotateGeojsonData';
import COGOrthophotoViewer from '@Components/common/MapLibreComponents/COGOrthophotoViewer';
import { toast } from 'react-toastify';
import { FlexColumn } from '@Components/common/Layouts';
import RotatingCircle from '@Components/common/RotationCue';
import { mapLayerIDs } from '@Constants/droneOperator';
import { findNearestCoordinate, swapFirstAndLast } from '@Utils/index';
Expand All @@ -69,6 +70,7 @@ const MapSection = ({ className }: { className?: string }) => {
const [isRotationEnabled, setIsRotationEnabled] = useState(false);
const [rotationAngle, setRotationAngle] = useState(0);
const [dragging, setDragging] = useState(false);
const centroidRef = useRef();
const { map, isMapLoaded } = useMapLibreGLMap({
containerId: 'dashboard-map',
mapOptions: {
Expand All @@ -93,8 +95,9 @@ const MapSection = ({ className }: { className?: string }) => {
isFetching: taskDataPolygonIsFetching,
}: Record<string, any> = useGetIndividualTaskQuery(taskId as string, {
select: (projectRes: any) => {
const taskPolygon = projectRes.data.outline;
const { geometry } = taskPolygon;
const taskPolygon = projectRes.data;
centroidRef.current = taskPolygon.centroid.coordinates;
const { geometry } = taskPolygon.outline;
return {
type: 'FeatureCollection',
features: [
Expand Down Expand Up @@ -310,6 +313,7 @@ const MapSection = ({ className }: { className?: string }) => {
: [firstFeature, ...restFeatures],
},
rotationDegreeParam,
centroidRef.current,
);
if (sourceToRotate && sourceToRotate instanceof GeoJSONSource) {
// @ts-ignore
Expand All @@ -336,6 +340,7 @@ const MapSection = ({ className }: { className?: string }) => {
type: 'FeatureCollection',
},
rotationDegreeParam,
centroidRef.current,
);
if (sourceToRotate && sourceToRotate instanceof GeoJSONSource) {
// @ts-ignore
Expand Down Expand Up @@ -623,29 +628,28 @@ const MapSection = ({ className }: { className?: string }) => {
</>
)}

<div className="naxatw-absolute naxatw-bottom-3 naxatw-right-[calc(50%-5.4rem)] naxatw-z-30 naxatw-h-fit lg:naxatw-right-3 lg:naxatw-top-3">
<Button
withLoader
leftIcon="place"
className="naxatw-w-[11.8rem] naxatw-bg-red"
onClick={() => {
if (newTakeOffPoint) {
handleSaveStartingPoint();
} else {
dispatch(toggleModal('update-flight-take-off-point'));
}
}}
isLoading={isUpdatingTakeOffPoint}
>
{newTakeOffPoint
? 'Save Take off Point'
: 'Change Take off Point'}
</Button>
</div>
{isRotationEnabled && (
<div className="naxatw-absolute naxatw-bottom-3 naxatw-right-[calc(50%-5.4rem)] naxatw-z-50 naxatw-h-fit naxatw-cursor-pointer lg:naxatw-right-3 lg:naxatw-top-3">
<Button
withLoader
leftIcon="save"
className="naxatw-w-[10.8rem] naxatw-bg-red"
>
<FlexColumn className="naxatw-gap-1">
<p className="naxatw-leading-3 naxatw-tracking-wide">
Save Rotated Flight Plan
</p>
{/* <p className="naxatw-font-normal naxatw-leading-3">
Rotated: {rotationAngle.toFixed(2)}°
</p> */}
</FlexColumn>
</Button>
</div>
)}
<div className="naxatw-absolute naxatw-left-[0.575rem] naxatw-top-[5.75rem] naxatw-z-30 naxatw-h-fit">
<Button
variant="ghost"
className={`naxatw-grid naxatw-h-[1.85rem] naxatw-place-items-center naxatw-border naxatw-bg-[#F5F5F5] !naxatw-px-[0.315rem] ${isRotationEnabled ? 'has-dropshadow naxatw-border-red' : 'naxatw-border-gray-400'}`}
className={`naxatw-grid naxatw-h-[1.85rem] naxatw-place-items-center naxatw-border !naxatw-px-[0.315rem] ${isRotationEnabled ? 'naxatw-border-red naxatw-bg-[#ffe0e0]' : 'naxatw-border-gray-400 naxatw-bg-[#F5F5F5]'}`}
onClick={() => handleRotationToggle()}
>
<ToolTip
Expand All @@ -661,7 +665,7 @@ const MapSection = ({ className }: { className?: string }) => {
<div className="naxatw-absolute naxatw-left-[0.575rem] naxatw-top-[8.25rem] naxatw-z-30 naxatw-h-fit naxatw-overflow-hidden naxatw-pb-1 naxatw-pr-1">
<Button
variant="ghost"
className={`naxatw-grid naxatw-h-[1.85rem] naxatw-place-items-center naxatw-border naxatw-bg-[#F5F5F5] ${showTakeOffPoint ? 'has-dropshadow naxatw-border-red' : 'naxatw-border-gray-400'} !naxatw-px-[0.315rem]`}
className={`naxatw-grid naxatw-h-[1.85rem] naxatw-place-items-center naxatw-border !naxatw-px-[0.315rem] ${showTakeOffPoint ? 'naxatw-border-red naxatw-bg-[#ffe0e0]' : 'naxatw-border-gray-400 naxatw-bg-[#F5F5F5]'}`}
onClick={() => handleTaskWayPoint()}
>
<ToolTip
Expand All @@ -681,7 +685,7 @@ const MapSection = ({ className }: { className?: string }) => {
<div className="naxatw-absolute naxatw-left-[0.575rem] naxatw-top-[10.75rem] naxatw-z-30 naxatw-h-fit">
<Button
variant="ghost"
className={`naxatw-grid naxatw-h-[1.85rem] naxatw-place-items-center naxatw-border naxatw-bg-[#F5F5F5] !naxatw-px-[0.315rem] ${showOrthoPhotoLayer ? 'has-dropshadow naxatw-border-red' : 'naxatw-border-gray-400'}`}
className={`naxatw-grid naxatw-h-[1.85rem] naxatw-place-items-center naxatw-border !naxatw-px-[0.315rem] ${showOrthoPhotoLayer ? 'naxatw-border-red naxatw-bg-[#ffe0e0]' : 'naxatw-border-gray-400 naxatw-bg-[#F5F5F5]'}`}
onClick={() => handleOtrhophotoLayerView()}
>
<ToolTip
Expand All @@ -695,25 +699,27 @@ const MapSection = ({ className }: { className?: string }) => {
</div>
)
)}
<div className="naxatw-absolute naxatw-bottom-3 naxatw-right-[calc(50%-5.4rem)] naxatw-z-30 naxatw-h-fit lg:naxatw-right-3 lg:naxatw-top-3">
<Button
withLoader
leftIcon="place"
className="naxatw-w-[11.8rem] naxatw-bg-red"
onClick={() => {
if (newTakeOffPoint) {
handleSaveStartingPoint();
} else {
dispatch(toggleModal('update-flight-take-off-point'));
}
}}
isLoading={isUpdatingTakeOffPoint}
>
{newTakeOffPoint
? 'Save Take off Point'
: 'Change Take off Point'}
</Button>
</div>
{!isRotationEnabled && (
<div className="naxatw-absolute naxatw-bottom-3 naxatw-right-[calc(50%-5.4rem)] naxatw-z-30 naxatw-h-fit lg:naxatw-right-3 lg:naxatw-top-3">
<Button
withLoader
leftIcon="place"
className="naxatw-w-[11.8rem] naxatw-bg-red"
onClick={() => {
if (newTakeOffPoint) {
handleSaveStartingPoint();
} else {
dispatch(toggleModal('update-flight-take-off-point'));
}
}}
isLoading={isUpdatingTakeOffPoint}
>
{newTakeOffPoint
? 'Save Take off Point'
: 'Change Take off Point'}
</Button>
</div>
)}

{newTakeOffPoint && (
<VectorLayer
Expand All @@ -729,7 +735,7 @@ const MapSection = ({ className }: { className?: string }) => {
/>
)}
{isRotationEnabled && (
<div className="naxatw-absolute naxatw-bottom-4 naxatw-right-[calc(50%-5.4rem)] naxatw-z-50 lg:naxatw-right-2 lg:naxatw-top-8">
<div className="naxatw-absolute naxatw-bottom-10 naxatw-right-[calc(50%-5.4rem)] naxatw-z-30 lg:naxatw-right-2 lg:naxatw-top-10">
<RotatingCircle
setRotation={setRotationAngle}
rotation={rotationAngle}
Expand Down
117 changes: 92 additions & 25 deletions src/frontend/src/components/common/RotationCue/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,47 @@
/* eslint-disable no-unused-vars */
import React, { useState } from 'react';
import React, { useEffect, useRef, 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 circleRef = useRef<HTMLDivElement>(null);
const [startAngle, setStartAngle] = useState(0);
const radius = 56; // Adjust to match circle size (half of `naxatw-h-28`)

const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
const { clientX, clientY } = event;
const circle = (event.target as HTMLElement).getBoundingClientRect();
const calculateAngle = (
clientX: number,
clientY: number,
circle: DOMRect,
) => {
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;

return degrees;
};

const handleStart = (clientX: number, clientY: number, circle: DOMRect) => {
const degrees = calculateAngle(clientX, clientY, circle);
setStartAngle(degrees - rotation); // Offset for smooth dragging
setDragging(true);
};

const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
const handleMove = (clientX: number, clientY: number, circle: DOMRect) => {
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;
const degrees = calculateAngle(clientX, clientY, circle);

let rotationDelta = degrees - startAngle;
if (rotationDelta < 0) {
Expand All @@ -46,37 +51,99 @@ const RotationCue = ({
setRotation(rotationDelta);
};

const handleMouseUp = () => {
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
const { clientX, clientY } = event;
const circle = (circleRef.current as HTMLElement).getBoundingClientRect();
handleStart(clientX, clientY, circle);
};

const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!dragging) return;

const { clientX, clientY } = event;
const circle = (circleRef.current as HTMLElement).getBoundingClientRect();
handleMove(clientX, clientY, circle);
};

const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
const { clientX, clientY } = event.touches[0];
const circle = (circleRef.current as HTMLElement).getBoundingClientRect();
handleStart(clientX, clientY, circle);
};

const handleTouchMove = (event: React.TouchEvent<HTMLDivElement>) => {
if (!dragging) return;

const { clientX, clientY } = event.touches[0];
const circle = (circleRef.current as HTMLElement).getBoundingClientRect();
handleMove(clientX, clientY, circle);
};

const handleEnd = () => {
setDragging(false);
};
useEffect(() => {
const preventDefault = (e: TouchEvent | WheelEvent) => {
e.preventDefault();
};
if (!dragging) return () => {};

// Disable scroll on mobile devices
document.body.style.overflow = 'hidden';
window.addEventListener('touchmove', preventDefault, { passive: false });
window.addEventListener('wheel', preventDefault, { passive: false });

return () => {
window.removeEventListener('touchmove', preventDefault);
window.removeEventListener('wheel', preventDefault);
document.body.style.overflow = '';
};
}, [dragging]);
// Calculate handle position
const radians = (rotation * Math.PI) / 180;
const handleX = radius + radius * Math.cos(radians);
const handleY = radius + radius * Math.sin(radians);

return (
<div
className="naxatw-flex naxatw-h-48 naxatw-w-48 naxatw-flex-col naxatw-items-center naxatw-justify-center naxatw-bg-transparent"
className="naxatw-relative naxatw-flex naxatw-h-48 naxatw-w-48 naxatw-flex-col naxatw-items-center naxatw-justify-center"
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onMouseUp={handleEnd}
onTouchMove={handleTouchMove}
onTouchEnd={handleEnd}
role="presentation"
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
onDoubleClick={e => e.preventDefault()}
onClick={e => e.preventDefault()}
ref={circleRef}
>
{/* Circle */}
<div
className="naxatw-relative naxatw-flex naxatw-h-28 naxatw-w-28 naxatw-cursor-grab naxatw-items-center naxatw-justify-center naxatw-rounded-full naxatw-border-2 naxatw-border-red naxatw-bg-white"
onMouseDown={handleMouseDown}
className="naxatw-relative naxatw-flex naxatw-h-28 naxatw-w-28 naxatw-items-center naxatw-justify-center naxatw-rounded-full naxatw-border-4 naxatw-border-red naxatw-bg-white naxatw-outline naxatw-outline-4 naxatw-outline-white"
role="presentation"
>
{/* Rotating Line */}
{/* Handle */}
<div
className="naxatw-absolute naxatw-top-3 naxatw-h-10 naxatw-w-1 naxatw-origin-bottom naxatw-bg-red"
className="naxatw-absolute naxatw-h-5 naxatw-w-5 naxatw-cursor-grab naxatw-rounded-full naxatw-bg-red naxatw-outline naxatw-outline-white"
style={{
transform: `rotate(${rotation}deg)`,
left: `${handleX - 14.5}px`, // Offset by half of handle size to center
top: `${handleY - 14.5}px`, // Offset by half of handle size to center
}}
onMouseMove={handleMouseMove}
onMouseUp={handleEnd}
role="presentation"
onMouseDown={handleMouseDown}
onClick={e => e.preventDefault()}
/>

{/* Static Center */}
<p className="naxatw-absolute naxatw-bottom-4 naxatw-select-none naxatw-text-sm">
<p
className="naxatw-absolute naxatw-left-1/2 naxatw-top-1/2 naxatw-translate-x-[-50%] naxatw-translate-y-[-50%] naxatw-select-none naxatw-text-sm"
draggable="false"
>
{rotation.toFixed(2)}
</p>
<div className="naxatw-absolute naxatw-h-4 naxatw-w-4 naxatw-rounded-full naxatw-bg-red" />
</div>
{/* Rotation Display */}
</div>
);
};
Expand Down
13 changes: 11 additions & 2 deletions src/frontend/src/utils/getExifData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ const getExifData = (file: File): Promise<any> => {
const tags = EXIFReader.load(arrayBuffer) as EXIFTags;

const dateTime = tags.DateTime?.description;
const gpsLatitude = tags.GPSLatitude?.description;
const gpsLongitude = tags.GPSLongitude?.description;
let gpsLatitude = tags.GPSLatitude?.description;
const gpsLatitudeRef = tags.GPSLatitudeRef?.value;
let gpsLongitude = tags.GPSLongitude?.description;
const gpsLongitudeRef = tags.GPSLongitudeRef?.value;

if (gpsLatitudeRef[0] === 'S' && gpsLatitude) {
gpsLatitude = -gpsLatitude;
}
if (gpsLongitudeRef[0] === 'W' && gpsLongitude) {
gpsLongitude = -gpsLongitude;
}

if (gpsLatitude && gpsLongitude) {
const latitude = gpsLatitude;
Expand Down

0 comments on commit a42c5d4

Please sign in to comment.