Skip to content

Commit

Permalink
Merge pull request #451 from hotosm/feat/overall-orthophoto
Browse files Browse the repository at this point in the history
Feat/Visualize task's and overall project orthophoto, options to start  processing without GCP  and other relative changes
  • Loading branch information
nrjadkry authored Jan 28, 2025
2 parents a2549cb + 1712ea1 commit 3cca30f
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 40 deletions.
6 changes: 4 additions & 2 deletions src/backend/app/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,10 @@ def get_assets_url_for_project(project_id: str):

def get_orthophoto_url_for_project(project_id: str):
"""Get the orthophoto URL for a project."""
project_orthophoto_path = f"dtm-data/projects/{project_id}/orthophoto/odm_orthophoto.tif"
project_orthophoto_path = (
f"dtm-data/projects/{project_id}/orthophoto/odm_orthophoto.tif"
)
s3_download_root = settings.S3_DOWNLOAD_ROOT
if s3_download_root:
return urljoin(s3_download_root, project_orthophoto_path)
return get_presigned_url(settings.S3_BUCKET_NAME, project_orthophoto_path, 3)
return get_presigned_url(settings.S3_BUCKET_NAME, project_orthophoto_path, 3)
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import DataTable from '@Components/common/DataTable';
import Icon from '@Components/common/Icon';
import { toggleModal } from '@Store/actions/common';
import { setSelectedTaskDetailToViewOrthophoto } from '@Store/actions/droneOperatorTask';
import { setProjectState } from '@Store/actions/project';
import { useTypedSelector } from '@Store/hooks';
import { formatString } from '@Utils/index';
import { useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify';

const { COG_URL } = process.env;

const contributionsDataColumns = [
{
header: 'User',
Expand All @@ -29,6 +31,11 @@ const contributionsDataColumns = [
cell: function CellComponent({ row }: any) {
const { original: rowData } = row;
const dispatch = useDispatch();
const { id } = useParams();
const visibleOrthophotoList = useTypedSelector(
state => state.project.visibleOrthophotoList,
);

const handleDownloadResult = () => {
if (!rowData?.assets_url) return;
try {
Expand All @@ -43,14 +50,32 @@ const contributionsDataColumns = [
}
};

const currentOrthophoto = visibleOrthophotoList?.find(
(orthophoto: Record<string, any>) =>
orthophoto?.taskId === rowData.task_id,
);

const handleViewResult = () => {
dispatch(
setSelectedTaskDetailToViewOrthophoto({
outline: rowData?.outline,
taskId: rowData?.task_id,
}),
);
dispatch(toggleModal('task-ortho-photo-preview'));
let newVisibleList: Record<string, any>[] = [];
if (currentOrthophoto) {
newVisibleList = visibleOrthophotoList.filter(
(orthophoto: Record<string, any>) =>
orthophoto?.taskId !== rowData?.task_id,
);
} else {
newVisibleList = [
...visibleOrthophotoList,
{
taskId: rowData.task_id,
source: {
type: 'raster',
url: `cog://${COG_URL}/dtm-data/projects/${id}/${rowData?.task_id}/orthophoto/odm_orthophoto.tif`,
tileSize: 256,
},
},
];
}
dispatch(setProjectState({ visibleOrthophotoList: newVisibleList }));
};

return (
Expand All @@ -63,7 +88,10 @@ const contributionsDataColumns = [
onKeyDown={() => {}}
onClick={() => handleViewResult()}
>
<Icon className="!naxatw-text-icon-sm" name="visibility" />
<Icon
className="!naxatw-text-icon-sm"
name={currentOrthophoto ? 'visibility' : 'visibility_off'}
/>
</div>
</div>
<div
Expand Down
19 changes: 15 additions & 4 deletions src/frontend/src/components/IndividualProject/GcpEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { useEffect, createElement } from 'react';
import { useEffect, createElement, useRef } from 'react';
import '@hotosm/gcp-editor';
import '@hotosm/gcp-editor/style.css';
import { useDispatch } from 'react-redux';
import { setProjectState } from '@Store/actions/project';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { processAllImagery } from '@Services/project';
import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify';

const GcpEditor = ({
cogUrl,
finalButtonText,
// handleProcessingStart,
rawImageUrl,
}: any) => {
const triggeredEvent = useRef(false);
const { id } = useParams();
const dispatch = useDispatch();
const CUSTOM_EVENT: any = 'start-processing-click';
Expand All @@ -23,6 +25,7 @@ const GcpEditor = ({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['project-detail'] });
dispatch(setProjectState({ showGcpEditor: false }));
toast.success('Processing started');
},
});

Expand All @@ -33,16 +36,24 @@ const GcpEditor = ({
startImageProcessing({ projectId: id, gcp_file: gcpFile });
};

const handleProcessingStart = (data: any) => {
if (triggeredEvent.current) return;
startProcessing(data);
triggeredEvent.current = true;
};

useEffect(() => {
document.addEventListener(
CUSTOM_EVENT,
data => {
startProcessing(data);
},
handleProcessingStart,
// When we use the {once: true} option when adding an event listener, the listener will be invoked at most once and immediately removed as soon as the event is invoked.
{ once: true },
);

return () => {
document.removeEventListener(CUSTOM_EVENT, handleProcessingStart);
};

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [CUSTOM_EVENT, dispatch]);

Expand Down
72 changes: 70 additions & 2 deletions src/frontend/src/components/IndividualProject/MapSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { LngLatBoundsLike, Map } from 'maplibre-gl';
import { LngLatBoundsLike, Map, RasterSourceSpecification } from 'maplibre-gl';
import { FeatureCollection } from 'geojson';
import { toast } from 'react-toastify';
import { useGetTaskStatesQuery, useGetUserDetailsQuery } from '@Api/projects';
Expand All @@ -19,10 +19,13 @@ import { useTypedDispatch, useTypedSelector } from '@Store/hooks';
import { useMutation } from '@tanstack/react-query';
import getBbox from '@turf/bbox';
import hasErrorBoundary from '@Utils/hasErrorBoundary';
import COGOrthophotoViewer from '@Components/common/MapLibreComponents/COGOrthophotoViewer';
import {
getLayerOptionsByStatus,
showPrimaryButton,
} from '@Constants/projectDescription';
import { Button } from '@Components/RadixComponents/Button';
import ToolTip from '@Components/RadixComponents/ToolTip';
import Legend from './Legend';

const MapSection = ({ projectData }: { projectData: Record<string, any> }) => {
Expand All @@ -36,6 +39,8 @@ const MapSection = ({ projectData }: { projectData: Record<string, any> }) => {
const [lockedUser, setLockedUser] = useState<Record<string, any> | null>(
null,
);

const [showOverallOrthophoto, setShowOverallOrthophoto] = useState(false);
const { data: userDetails }: Record<string, any> = useGetUserDetailsQuery();

const { map, isMapLoaded } = useMapLibreGLMap({
Expand All @@ -55,6 +60,10 @@ const MapSection = ({ projectData }: { projectData: Record<string, any> }) => {
const taskClickedOnTable = useTypedSelector(
state => state.project.taskClickedOnTable,
);
const visibleTaskOrthophoto = useTypedSelector(
state => state.project.visibleOrthophotoList,
);

const { data: taskStates } = useGetTaskStatesQuery(id as string, {
enabled: !!tasksData,
});
Expand Down Expand Up @@ -166,6 +175,15 @@ const MapSection = ({ projectData }: { projectData: Record<string, any> }) => {
[taskStatusObj, userDetails, projectData],
);

const projectOrthophotoSource: RasterSourceSpecification = useMemo(
() => ({
type: 'raster',
url: `cog://${projectData?.orthophoto_url}`,
tileSize: 256,
}),
[projectData?.orthophoto_url],
);

const handleTaskLockClick = () => {
lockTask({
projectId: id,
Expand All @@ -182,6 +200,15 @@ const MapSection = ({ projectData }: { projectData: Record<string, any> }) => {
});
};

const handleToggleOverallOrthophoto = () => {
map?.setLayoutProperty(
'project-orthophoto',
'visibility',
showOverallOrthophoto ? 'none' : 'visible',
);
setShowOverallOrthophoto(!showOverallOrthophoto);
};

return (
<MapContainer
map={map}
Expand Down Expand Up @@ -258,6 +285,47 @@ const MapSection = ({ projectData }: { projectData: Record<string, any> }) => {
);
})}

{/* visualize tasks orthophoto */}
{visibleTaskOrthophoto?.map(orthophotoDetails => (
<COGOrthophotoViewer
key={orthophotoDetails.taskId}
id={orthophotoDetails.taskId}
source={orthophotoDetails.source}
visibleOnMap
/>
))}
{/* visualize tasks orthophoto end */}

{/* visualize overall project orthophoto */}
{projectData?.orthophoto_url && (
<COGOrthophotoViewer
id="project-orthophoto"
source={projectOrthophotoSource}
visibleOnMap
/>
)}
{/* visualize tasks orthophoto end */}

{/* additional controls */}
<div className="naxatw-absolute naxatw-left-[0.575rem] naxatw-top-[5.75rem] naxatw-z-30 naxatw-flex naxatw-h-fit naxatw-w-fit naxatw-flex-col naxatw-gap-3">
{projectData?.orthophoto_url && (
<Button
variant="ghost"
className={`naxatw-grid naxatw-h-[1.85rem] naxatw-place-items-center naxatw-border !naxatw-px-[0.315rem] ${showOverallOrthophoto ? 'naxatw-border-red naxatw-bg-[#ffe0e0]' : 'naxatw-border-gray-400 naxatw-bg-[#F5F5F5]'}`}
onClick={() => handleToggleOverallOrthophoto()}
>
<ToolTip
name="visibility"
message="Show Orthophoto"
symbolType="material-icons"
iconClassName="!naxatw-text-xl !naxatw-text-black"
className="naxatw-mt-[-4px]"
/>
</Button>
)}
</div>
{/* additional controls */}

<AsyncPopup
map={map as Map}
popupUI={getPopupUI}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import RadioButton from '@Components/common/RadioButton';
import { Button } from '@Components/RadixComponents/Button';
import { startProcessingOptions } from '@Constants/projectDescription';
import { processAllImagery } from '@Services/project';
import { toggleModal } from '@Store/actions/common';
import { setProjectState } from '@Store/actions/project';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { toast } from 'react-toastify';

const ChooseProcessingParameter = () => {
const dispatch = useDispatch();
const queryClient = useQueryClient();
const pathname = window.location.pathname?.split('/');
const projectId = pathname?.[2];

const [value, setValue] = useState('with_gcp');
const { mutate: startImageProcessing } = useMutation({
mutationFn: processAllImagery,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['project-detail'] });
toast.success('Processing started');
},
});

return (
<div>
<p className="naxatw-text-sm naxatw-text-[#7A7676]">
Choose Processing Parameter
</p>
<div className="naxatw-py-1 naxatw-text-gray-700">
<RadioButton
className="!naxatw-text-black"
options={startProcessingOptions}
direction="column"
onChangeData={selectedValue => setValue(selectedValue)}
value={value}
/>
</div>
<div className="naxatw naxatw-flex naxatw-justify-center naxatw-pt-3">
<Button
withLoader
className="naxatw-bg-red"
onClick={() => {
if (value === 'with_gcp') {
dispatch(setProjectState({ showGcpEditor: true }));
} else {
startImageProcessing({ projectId });
}
dispatch(toggleModal());
}}
>
Proceed
</Button>
</div>
</div>
);
};

export default ChooseProcessingParameter;
Loading

0 comments on commit 3cca30f

Please sign in to comment.