From f11b331fb590abcbd35466a4fec35315541a1ace Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:31:08 +0545 Subject: [PATCH 01/13] feat: prevent multiple event trigger on gcp editor component --- .../IndividualProject/GcpEditor/index.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx b/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx index 3c47acce..341c4333 100644 --- a/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx +++ b/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx @@ -1,4 +1,4 @@ -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'; @@ -13,6 +13,7 @@ const GcpEditor = ({ // handleProcessingStart, rawImageUrl, }: any) => { + const triggeredEvent = useRef(false); const { id } = useParams(); const dispatch = useDispatch(); const CUSTOM_EVENT: any = 'start-processing-click'; @@ -33,16 +34,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]); From f55b2b68d88c9a41723a9ffd93884141cdad77b6 Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:32:35 +0545 Subject: [PATCH 02/13] feat: add `ChooseProcessingParameter` modal compoennt to choose processing parameter --- .../ChooseProcessingParameter.tsx | 60 +++++++++++++++++++ src/frontend/src/constants/modalContents.tsx | 8 +++ 2 files changed, 68 insertions(+) create mode 100644 src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx diff --git a/src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx b/src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx new file mode 100644 index 00000000..887c8c98 --- /dev/null +++ b/src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx @@ -0,0 +1,60 @@ +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'; + +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'] }); + dispatch(setProjectState({ showGcpEditor: false })); + }, + }); + + return ( +
+

+ Choose Processing Parameter +

+
+ setValue(selectedValue)} + value={value} + /> +
+
+ +
+
+ ); +}; + +export default ChooseProcessingParameter; diff --git a/src/frontend/src/constants/modalContents.tsx b/src/frontend/src/constants/modalContents.tsx index af02d3a2..bbf97431 100644 --- a/src/frontend/src/constants/modalContents.tsx +++ b/src/frontend/src/constants/modalContents.tsx @@ -4,6 +4,7 @@ import ImageBoxPopOver from '@Components/DroneOperatorTask/DescriptionSection/Po import ImageMapBox from '@Components/DroneOperatorTask/DescriptionSection/PopoverBox/MapBox'; import ChooseTakeOffPointOptions from '@Components/DroneOperatorTask/ModalContent/ChooseTakeOffPointOptions'; import TaskOrthophotoPreview from '@Components/DroneOperatorTask/ModalContent/TaskOrthophotoPreview'; +import ChooseProcessingParameter from '@Components/IndividualProject/ModalContent/ChooseProcessingParameter'; import { ReactElement } from 'react'; export type ModalContentsType = @@ -14,6 +15,7 @@ export type ModalContentsType = | 'update-flight-take-off-point' | 'task-ortho-photo-preview' | 'document-preview' + | 'choose-processing-parameter' | null; export type PromptDialogContentsType = 'delete-layer' | null; @@ -36,6 +38,12 @@ export function getModalContent(content: ModalContentsType): ModalReturnType { title: 'Unsaved Changes!', content: , }; + case 'choose-processing-parameter': + return { + className: 'naxatw-w-[92vw] naxatw-max-w-[25rem]', + title: 'Start Processing', + content: , + }; case 'raw-image-preview': return { From 992d0d67a32a89edaaf5c178d8c765142c5dae94 Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:34:44 +0545 Subject: [PATCH 03/13] feat: update process imagery service --- src/frontend/src/services/project.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/services/project.ts b/src/frontend/src/services/project.ts index badad294..a7f423a0 100644 --- a/src/frontend/src/services/project.ts +++ b/src/frontend/src/services/project.ts @@ -15,12 +15,12 @@ export const getRequestedTasks = () => authenticated(api).get('/tasks/requested_tasks/pending'); export const processAllImagery = (data: Record) => { - const { projectId, gcp_file } = data; - return authenticated(api).post( - `/projects/process_all_imagery/${projectId}/`, - { gcp_file }, - ); + const { projectId } = data; + if (data?.gcp_file) { + return authenticated(api).post( + `/projects/process_all_imagery/${projectId}/`, + { gcp_file: data.gcp_file }, + ); + } + return authenticated(api).post(`/projects/process_all_imagery/${projectId}/`); }; - -// export const getAllAssetsUrl = (projectId: string) => -// authenticated(api).get(`/projects/assets/${projectId}/`); From f0f0e948c628e754f0ea2bd8bbb879c235c29c8a Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:39:37 +0545 Subject: [PATCH 04/13] feat: add toas message on processing started --- .../ModalContent/ChooseProcessingParameter.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx b/src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx index 887c8c98..74101340 100644 --- a/src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx +++ b/src/frontend/src/components/IndividualProject/ModalContent/ChooseProcessingParameter.tsx @@ -7,6 +7,7 @@ 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(); @@ -19,7 +20,7 @@ const ChooseProcessingParameter = () => { mutationFn: processAllImagery, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['project-detail'] }); - dispatch(setProjectState({ showGcpEditor: false })); + toast.success('Processing started'); }, }); From 9ad18e334800de2d93a7b5fba82a6281037d4eef Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:40:24 +0545 Subject: [PATCH 05/13] feat: update processing starter parameter on individual project --- src/frontend/src/constants/projectDescription.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/frontend/src/constants/projectDescription.ts b/src/frontend/src/constants/projectDescription.ts index 67509617..94d1c163 100644 --- a/src/frontend/src/constants/projectDescription.ts +++ b/src/frontend/src/constants/projectDescription.ts @@ -106,3 +106,16 @@ export const showPrimaryButton = ( return false; } }; + +export const startProcessingOptions = [ + { + label: 'Start Processing with GCP', + value: 'with_gcp', + name: 'start_processing', + }, + { + label: 'Start Processing without GCP', + value: 'without_gcp', + name: 'start_processing', + }, +]; From b6af60a0b01ace75c705e1fd1e8e943e50a2ed5a Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:41:25 +0545 Subject: [PATCH 06/13] feat: render buttons as per project processing status --- .../Description/DescriptionSection.tsx | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx b/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx index fc804036..89634385 100644 --- a/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx +++ b/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx @@ -1,6 +1,7 @@ +/* eslint-disable no-nested-ternary */ import { useDispatch } from 'react-redux'; import { Button } from '@Components/RadixComponents/Button'; -import { setProjectState } from '@Store/actions/project'; +import { toggleModal } from '@Store/actions/common'; import ApprovalSection from './ApprovalSection'; const DescriptionSection = ({ @@ -68,18 +69,47 @@ const DescriptionSection = ({ {page !== 'project-approval' && - projectData?.image_processing_status !== 'SUCCESS' && ( -
- -
- )} + projectData?.image_processing_status === 'NOT_STARTED' ? ( +
+ +
+ ) : projectData?.image_processing_status === 'SUCCESS' ? ( + <> + ) : projectData?.image_processing_status === 'PROCESSING' ? ( +
+ +
+ ) : ( +
+ +
+ )} + {page === 'project-approval' && projectData?.regulator_approval_status === 'PENDING' && ( From deef7dcf16cddcf6d57ce8353dc7a0748cef8850 Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:41:54 +0545 Subject: [PATCH 07/13] feat: popup toast message on processing started --- .../src/components/IndividualProject/GcpEditor/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx b/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx index 341c4333..3a1f9826 100644 --- a/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx +++ b/src/frontend/src/components/IndividualProject/GcpEditor/index.tsx @@ -6,6 +6,7 @@ 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, @@ -24,6 +25,7 @@ const GcpEditor = ({ onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['project-detail'] }); dispatch(setProjectState({ showGcpEditor: false })); + toast.success('Processing started'); }, }); From 1870c392c1611ca4e532ae5c4db3c9fa5152f827 Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 27 Jan 2025 16:49:01 +0545 Subject: [PATCH 08/13] feat: add key on breadcrumb items --- src/frontend/src/components/common/Breadcrumb/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/src/components/common/Breadcrumb/index.tsx b/src/frontend/src/components/common/Breadcrumb/index.tsx index 760ab399..95671348 100644 --- a/src/frontend/src/components/common/Breadcrumb/index.tsx +++ b/src/frontend/src/components/common/Breadcrumb/index.tsx @@ -16,6 +16,7 @@ const BreadCrumb = ({ data }: IBreadCrumbProps) => { {data.map((breadCrumbItem, index) => ( <>
index < data.length - 1 ? navigate(breadCrumbItem.navLink) From 95770853437d42ab885b41e5882c7ff4f0a8e3f4 Mon Sep 17 00:00:00 2001 From: Sujit Date: Tue, 28 Jan 2025 10:57:36 +0545 Subject: [PATCH 09/13] fix: cannot removed source the layer is using it remove the layer before source --- .../common/MapLibreComponents/COGOrthophotoViewer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx b/src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx index 7584b814..edf71cfd 100644 --- a/src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx +++ b/src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx @@ -52,8 +52,8 @@ const COGOrthophotoViewer = ({ // eslint-disable-next-line consistent-return return () => { if (map?.getSource(id)) { - map?.removeSource(id); if (map?.getLayer(id)) map?.removeLayer(id); + map?.removeSource(id); map.off('idle', handleZoomToGeoTiff); } }; From e169be567baf6372f2dd4acd67df9a0224f0e025 Mon Sep 17 00:00:00 2001 From: Sujit Date: Tue, 28 Jan 2025 11:12:56 +0545 Subject: [PATCH 10/13] feat(individual-project): store visible list of orthophoto sources on redux state --- src/frontend/src/store/slices/project.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frontend/src/store/slices/project.ts b/src/frontend/src/store/slices/project.ts index 46df0586..dd88b5ee 100644 --- a/src/frontend/src/store/slices/project.ts +++ b/src/frontend/src/store/slices/project.ts @@ -10,6 +10,7 @@ export interface ProjectState { taskClickedOnTable: Record | null; showGcpEditor: boolean; gcpData: any; + visibleOrthophotoList: Record[]; } const initialState: ProjectState = { @@ -20,6 +21,7 @@ const initialState: ProjectState = { taskClickedOnTable: null, showGcpEditor: false, gcpData: null, + visibleOrthophotoList: [], }; const setProjectState: CaseReducer< From f4bc618437be8d924f503c2a7e1e61168fd7e12c Mon Sep 17 00:00:00 2001 From: Sujit Date: Tue, 28 Jan 2025 11:14:10 +0545 Subject: [PATCH 11/13] feat: toggle tasks orthophoto visibility from table row --- .../Contributions/TableSection/index.tsx | 48 +++++++++++---- .../IndividualProject/MapSection/index.tsx | 58 +++++++++++++++++++ 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx b/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx index 6164f599..58147091 100644 --- a/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx @@ -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', @@ -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 { @@ -43,14 +50,32 @@ const contributionsDataColumns = [ } }; + const currentOrthophoto = visibleOrthophotoList?.find( + (orthophoto: Record) => + 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[] = []; + if (currentOrthophoto) { + newVisibleList = visibleOrthophotoList.filter( + (orthophoto: Record) => + 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 ( @@ -63,7 +88,10 @@ const contributionsDataColumns = [ onKeyDown={() => {}} onClick={() => handleViewResult()} > - +
}) => { @@ -36,6 +39,9 @@ const MapSection = ({ projectData }: { projectData: Record }) => { const [lockedUser, setLockedUser] = useState | null>( null, ); + + // eslint-disable-next-line no-unused-vars + const [showOverallOrthophoto, setShowOverallOrthophoto] = useState(false); const { data: userDetails }: Record = useGetUserDetailsQuery(); const { map, isMapLoaded } = useMapLibreGLMap({ @@ -55,6 +61,10 @@ const MapSection = ({ projectData }: { projectData: Record }) => { const taskClickedOnTable = useTypedSelector( state => state.project.taskClickedOnTable, ); + const visibleTaskOrthophoto = useTypedSelector( + state => state.project.visibleOrthophotoList, + ); + const { data: taskStates } = useGetTaskStatesQuery(id as string, { enabled: !!tasksData, }); @@ -182,6 +192,15 @@ const MapSection = ({ projectData }: { projectData: Record }) => { }); }; + // const handleToggleOverallOrthophoto = () => { + // map?.setLayoutProperty( + // 'task-orthophoto', + // 'visibility', + // showOverallOrthophoto ? 'none' : 'visible', + // ); + // setShowOverallOrthophoto(!showOverallOrthophoto); + // }; + return ( }) => { ); })} + {/* visualize tasks orthophoto */} + {visibleTaskOrthophoto?.map(orthophotoDetails => ( + + ))} + {/* visualize tasks orthophoto end */} + + {/* visualize overall project orthophoto */} + {/* {visibleTaskOrthophoto?.map(orthophotoDetails => ( + + ))} */} + {/* visualize tasks orthophoto end */} + + {/* additional controls */} +
+ +
+ Date: Tue, 28 Jan 2025 11:33:31 +0545 Subject: [PATCH 12/13] feat(individual-project): visualize and toggle overall project orthophoto --- .../IndividualProject/MapSection/index.tsx | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx index 409e26a7..b0badf3a 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx @@ -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'; @@ -40,7 +40,6 @@ const MapSection = ({ projectData }: { projectData: Record }) => { null, ); - // eslint-disable-next-line no-unused-vars const [showOverallOrthophoto, setShowOverallOrthophoto] = useState(false); const { data: userDetails }: Record = useGetUserDetailsQuery(); @@ -176,6 +175,15 @@ const MapSection = ({ projectData }: { projectData: Record }) => { [taskStatusObj, userDetails, projectData], ); + const projectOrthophotoSource: RasterSourceSpecification = useMemo( + () => ({ + type: 'raster', + url: `cog://${projectData?.orthophoto_url}`, + tileSize: 256, + }), + [projectData?.orthophoto_url], + ); + const handleTaskLockClick = () => { lockTask({ projectId: id, @@ -192,14 +200,14 @@ const MapSection = ({ projectData }: { projectData: Record }) => { }); }; - // const handleToggleOverallOrthophoto = () => { - // map?.setLayoutProperty( - // 'task-orthophoto', - // 'visibility', - // showOverallOrthophoto ? 'none' : 'visible', - // ); - // setShowOverallOrthophoto(!showOverallOrthophoto); - // }; + const handleToggleOverallOrthophoto = () => { + map?.setLayoutProperty( + 'project-orthophoto', + 'visibility', + showOverallOrthophoto ? 'none' : 'visible', + ); + setShowOverallOrthophoto(!showOverallOrthophoto); + }; return ( }) => { {/* visualize tasks orthophoto end */} {/* visualize overall project orthophoto */} - {/* {visibleTaskOrthophoto?.map(orthophotoDetails => ( + {projectData?.orthophoto_url && ( - ))} */} + )} {/* visualize tasks orthophoto end */} {/* additional controls */}
- + {projectData?.orthophoto_url && ( + + )}
+ {/* additional controls */} Date: Tue, 28 Jan 2025 05:56:42 +0000 Subject: [PATCH 13/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/backend/app/s3.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backend/app/s3.py b/src/backend/app/s3.py index 00e0a9dd..9d9509bc 100644 --- a/src/backend/app/s3.py +++ b/src/backend/app/s3.py @@ -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) \ No newline at end of file + return get_presigned_url(settings.S3_BUCKET_NAME, project_orthophoto_path, 3)