Skip to content

Commit

Permalink
Merge pull request #360 from hotosm/feat/drone-operator-image-upload-ui
Browse files Browse the repository at this point in the history
Feat/drone-operator-image-upload-UI-optimization
  • Loading branch information
subashtiwari1010 authored Nov 28, 2024
2 parents 5510fd8 + 360afab commit 137ce1b
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ const ImageMapBox = () => {

const [progressBar, setProgressBar] = useState(false);
const [loadingWidth, setLoadingWidth] = useState(0);
const [imagesNames, setImagesNames] = useState<string[]>([]);
const [files, setFiles] = useState<File[]>([]);
const [imageFilesGeoJsonData, setImageFilesGeoJsonData] =
useState<Record<string, any>>();
const [imageFilesLineStringData, setImageFilesLineStringData] =
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -207,7 +209,7 @@ const ImageMapBox = () => {
<VectorLayer
map={map as Map}
isMapLoaded={isMapLoaded}
id="image-points"
id="image-points-map"
geojson={imageFilesGeoJsonData as GeojsonType}
visibleOnMap={!!imageFilesGeoJsonData}
interactions={['feature']}
Expand All @@ -233,6 +235,7 @@ const ImageMapBox = () => {
],
},
}}
zoomToExtent
/>
<VectorLayer
map={map as Map}
Expand All @@ -255,7 +258,7 @@ const ImageMapBox = () => {
<AsyncPopup
map={map as Map}
showPopup={(feature: Record<string, any>) => {
return feature?.source === 'image-points';
return feature?.source === 'image-points-map';
}}
popupUI={getPopupUI}
fetchPopupData={(properties: Record<string, any>) => {
Expand All @@ -282,14 +285,15 @@ const ImageMapBox = () => {
/>
</MapContainer>
<p className="naxatw-text-lg naxatw-font-medium">
{files.length} Images Selected
{filesExifData.length} Images Selected
</p>
</div>
<div className="naxatw-mx-auto naxatw-w-fit">
<Button
variant="ghost"
className="naxatw-mx-auto naxatw-w-fit naxatw-bg-[#D73F3F] naxatw-text-[#FFFFFF]"
onClick={() => handleSubmit()}
disabled={filesExifData.length === 0}
>
Upload
</Button>
Expand All @@ -299,7 +303,7 @@ const ImageMapBox = () => {
<FilesUploadingPopOver
show={progressBar}
width={loadingWidth}
filesLength={files.length}
filesLength={filesExifData.length}
uploadedFiles={uploadedFilesNumber.current}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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(() => {
Expand Down Expand Up @@ -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<string, any>)?.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 () => {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface IVectorLayer extends ILayer {
| 'bottom-left'
| 'bottom-right';
imageLayerOptions?: Object;
zoomToExtent?: boolean;
}

type InteractionsType = 'hover' | 'select';
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/constants/modalContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: <ImageMapBox />,
};

Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/store/slices/droneOperartorTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 137ce1b

Please sign in to comment.