diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/MapBox/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/MapBox/index.tsx index 77b6c94c..9c02aab6 100644 --- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/MapBox/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/MapBox/index.tsx @@ -38,8 +38,6 @@ const ImageMapBox = () => { const [progressBar, setProgressBar] = useState(false); const [loadingWidth, setLoadingWidth] = useState(0); - const [imagesNames, setImagesNames] = useState([]); - const [files, setFiles] = useState([]); const [imageFilesGeoJsonData, setImageFilesGeoJsonData] = useState>(); const [imageFilesLineStringData, setImageFilesLineStringData] = @@ -52,6 +50,13 @@ const ImageMapBox = () => { const filesExifData = useTypedSelector( state => state.droneOperatorTask.filesExifData, ); + const modalState = useTypedSelector(state => state.common.showModal); + + useEffect(() => { + if (!modalState) { + dispatch(setFilesExifData([])); + } + }, [dispatch, modalState]); useEffect(() => { if (filesExifData.length === 0) return; @@ -76,26 +81,22 @@ const ImageMapBox = () => { ], }; setImageFilesLineStringData(imageFilesLineString); - setFiles(filesExifData.map(file => file.file)); - setImagesNames(filesExifData.map(file => file.file.name)); }, [filesExifData]); const { map, isMapLoaded } = useMapLibreGLMap({ containerId: 'image-upload-map', mapOptions: { - zoom: 17, - center: [ - filesExifData[0]?.coordinates.longitude || 84.124, - filesExifData[0]?.coordinates.latitude || 28.9349, - ], - maxZoom: 19, + zoom: 2, + center: [0, 0], + maxZoom: 25, + renderWorldCopies: false, // Prevent rendering copies of the map outside the primary view + refreshExpiredTiles: false, }, disableRotation: true, }); useEffect(() => { if (isMapLoaded && map) { - // Add zoom and rotation controls map.addControl(new NavigationControl(), 'top-right'); // Add attribution control @@ -155,6 +156,7 @@ const ImageMapBox = () => { // urls fromm array of objects is retrieved and stored in value const urls = urlsData.data.map(({ url }: { url: string }) => url); const chunkedUrls = chunkArray(urls, 4); + const files = filesExifData.map(file => file.file); const chunkedFiles = chunkArray(files, 4); // this calls api simultaneously for each chunk of files @@ -184,7 +186,7 @@ const ImageMapBox = () => { const filesData = { expiry: 5, task_id: taskId, - image_name: imagesNames, + image_name: filesExifData.map(file => file.file.name), project_id: projectId, }; mutate(filesData); @@ -207,7 +209,7 @@ const ImageMapBox = () => { { ], }, }} + zoomToExtent /> { ) => { - return feature?.source === 'image-points'; + return feature?.source === 'image-points-map'; }} popupUI={getPopupUI} fetchPopupData={(properties: Record) => { @@ -282,7 +285,7 @@ const ImageMapBox = () => { />

- {files.length} Images Selected + {filesExifData.length} Images Selected

@@ -290,6 +293,7 @@ const ImageMapBox = () => { variant="ghost" className="naxatw-mx-auto naxatw-w-fit naxatw-bg-[#D73F3F] naxatw-text-[#FFFFFF]" onClick={() => handleSubmit()} + disabled={filesExifData.length === 0} > Upload @@ -299,7 +303,7 @@ const ImageMapBox = () => { diff --git a/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts b/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts index f50170eb..9eabdfbd 100644 --- a/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts +++ b/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts @@ -1,6 +1,9 @@ /* eslint-disable no-param-reassign */ import { useEffect, useMemo, useRef } from 'react'; -import { MapMouseEvent } from 'maplibre-gl'; +import { LngLatLike, MapMouseEvent } from 'maplibre-gl'; +import bbox from '@turf/bbox'; +import { toast } from 'react-toastify'; +import { Feature, FeatureCollection } from 'geojson'; // import { v4 as uuidv4 } from 'uuid'; import { IVectorLayer } from '../types'; @@ -18,9 +21,11 @@ export default function VectorLayer({ symbolPlacement = 'point', iconAnchor = 'center', imageLayerOptions, + zoomToExtent = false, }: IVectorLayer) { const sourceId = useMemo(() => id.toString(), [id]); const hasInteractions = useRef(false); + const firstRender = useRef(true); const imageId = `${sourceId}-image/logo`; useEffect(() => { @@ -108,6 +113,53 @@ export default function VectorLayer({ }; }, [map, sourceId]); + useEffect(() => { + if (!map || !geojson || !zoomToExtent) return; + if (!firstRender.current) return; + firstRender.current = false; + + const handleZoom = () => { + if (!map || !geojson || !zoomToExtent) return; + let parsedGeojson: Feature | FeatureCollection; + + // Parse GeoJSON if it's a string + if (typeof geojson === 'string') { + try { + parsedGeojson = JSON.parse(geojson) as Feature | FeatureCollection; + } catch (error) { + toast.error( + 'Invalid GeoJSON string:', + (error as Record)?.message, + ); + return; + } + } else { + parsedGeojson = geojson as Feature | FeatureCollection; + } + const [minLng, minLat, maxLng, maxLat] = bbox(parsedGeojson); + const bounds: [LngLatLike, LngLatLike] = [ + [minLng, minLat], // Southwest corner + [maxLng, maxLat], // Northeast corner + ]; + + // Zoom to the bounds + map.fitBounds(bounds, { + padding: 20, + maxZoom: 14, + zoom: 18, + // animate: false, + duration: 300, + }); + map.off('idle', handleZoom); + }; + + map.on('idle', handleZoom); + // eslint-disable-next-line consistent-return + return () => { + map.off('idle', handleZoom); + }; + }, [map, geojson, zoomToExtent]); + // add select interaction & return properties on feature select useEffect(() => { if (!map || !interactions.includes('feature')) return () => {}; diff --git a/src/frontend/src/components/common/MapLibreComponents/types/index.ts b/src/frontend/src/components/common/MapLibreComponents/types/index.ts index b23fa07b..8201fb50 100644 --- a/src/frontend/src/components/common/MapLibreComponents/types/index.ts +++ b/src/frontend/src/components/common/MapLibreComponents/types/index.ts @@ -60,6 +60,7 @@ export interface IVectorLayer extends ILayer { | 'bottom-left' | 'bottom-right'; imageLayerOptions?: Object; + zoomToExtent?: boolean; } type InteractionsType = 'hover' | 'select'; diff --git a/src/frontend/src/constants/modalContents.tsx b/src/frontend/src/constants/modalContents.tsx index cf6d473a..af02d3a2 100644 --- a/src/frontend/src/constants/modalContents.tsx +++ b/src/frontend/src/constants/modalContents.tsx @@ -47,7 +47,7 @@ export function getModalContent(content: ModalContentsType): ModalReturnType { case 'raw-image-map-preview': return { className: '!naxatw-w-[95vw] md:!naxatw-w-[60vw]', - title: 'Upload Images, GCP, and align.laz', + title: 'Upload Images', content: , }; diff --git a/src/frontend/src/store/slices/droneOperartorTask.ts b/src/frontend/src/store/slices/droneOperartorTask.ts index ad1e92c3..e73a9bee 100644 --- a/src/frontend/src/store/slices/droneOperartorTask.ts +++ b/src/frontend/src/store/slices/droneOperartorTask.ts @@ -4,7 +4,7 @@ import { createSlice } from '@reduxjs/toolkit'; export interface IFilesExifData { file: File; dateTime: string; - coordinates: { longitude: number | null; latitude: number | null }; + coordinates: { longitude: number; latitude: number }; } export interface IDroneOperatorTaskState { secondPage: boolean;