From 0e5763ad70b548b04b698673885c24f1ea47f172 Mon Sep 17 00:00:00 2001 From: Sujit <90745363+suzit-10@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:41:39 +0545 Subject: [PATCH] Update MapLibre and Integrate COG Protocol for Efficient Orthophoto Rendering (#334) * feat: add `COGOrthophotoViewer` component * feat: add `@geomatico/maplibre-cog-protocol` and update `maplibre-gl` version to support COG file * feat: implement `COGOrthophotoViewer` to preview task orthophoto * feat: add s3 endpoint to vite `defineConfig` * feat(orthophoto-preview): add dynamic source with the url prepended with `cog://` to preview orthophoto --------- Co-authored-by: Sujit --- src/frontend/package.json | 9 +- .../ModalContent/TaskOrthophotoPreview.tsx | 86 ++++--------------- .../COGOrthophotoViewer/index.tsx | 49 +++++++++++ src/frontend/vite.config.ts | 1 + 4 files changed, 70 insertions(+), 75 deletions(-) create mode 100644 src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx diff --git a/src/frontend/package.json b/src/frontend/package.json index f07f9e6a..81213b8c 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,8 +1,8 @@ { "dependencies": { + "@geomatico/maplibre-cog-protocol": "^0.3.1", "@mapbox/mapbox-gl-draw": "^1.4.2", "@mapbox/mapbox-gl-draw-static-mode": "^1.0.1", - "mapbox-gl-draw-cut-line-mode": "^1.2.0", "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", @@ -15,10 +15,10 @@ "@turf/area": "^7.0.0", "@turf/bbox": "^7.0.0", "@turf/centroid": "^7.0.0", - "@turf/helpers": "^7.0.0", - "@turf/meta": "^7.0.0", "@turf/flatten": "^7.0.0", + "@turf/helpers": "^7.0.0", "@turf/length": "^7.0.0", + "@turf/meta": "^7.0.0", "autoprefixer": "^10.4.14", "axios": "^1.3.4", "class-variance-authority": "^0.6.1", @@ -30,7 +30,8 @@ "geojson": "^0.5.0", "geojson-validation": "^1.0.2", "html2canvas": "^1.4.1", - "maplibre-gl": "^3.2.0", + "mapbox-gl-draw-cut-line-mode": "^1.2.0", + "maplibre-gl": "^4.7.1", "papaparse": "^5.4.1", "react": "^18.2.0", "react-datepicker": "^7.3.0", diff --git a/src/frontend/src/components/DroneOperatorTask/ModalContent/TaskOrthophotoPreview.tsx b/src/frontend/src/components/DroneOperatorTask/ModalContent/TaskOrthophotoPreview.tsx index 0889a855..ab48fe26 100644 --- a/src/frontend/src/components/DroneOperatorTask/ModalContent/TaskOrthophotoPreview.tsx +++ b/src/frontend/src/components/DroneOperatorTask/ModalContent/TaskOrthophotoPreview.tsx @@ -1,13 +1,14 @@ import BaseLayerSwitcherUI from '@Components/common/BaseLayerSwitcher'; import { useMapLibreGLMap } from '@Components/common/MapLibreComponents'; +import COGOrthophotoViewer from '@Components/common/MapLibreComponents/COGOrthophotoViewer'; import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; import { setSelectedTaskDetailToViewOrthophoto } from '@Store/actions/droneOperatorTask'; import { useTypedSelector } from '@Store/hooks'; -import { LngLatBoundsLike } from 'maplibre-gl'; +import { LngLatBoundsLike, RasterSourceSpecification } from 'maplibre-gl'; import { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -const { BASE_URL } = process.env; +const { S3_ENDPOINT } = process.env; const TaskOrthophotoPreview = () => { const dispatch = useDispatch(); @@ -23,29 +24,19 @@ const TaskOrthophotoPreview = () => { const taskId = pathname?.[4] || taskIdFromRedux; const { map, isMapLoaded } = useMapLibreGLMap({ - containerId: 'dashboard-map', + containerId: 'orthophoto-map', mapOptions: { - zoom: 0, + zoom: 21, center: [0, 0], }, disableRotation: true, }); - const orhtophotoSource: Record = useMemo( + const orthophotoSource: RasterSourceSpecification = useMemo( () => ({ - source: { - type: 'raster', - tiles: [ - `${BASE_URL}/projects/orthophoto/{z}/{x}/{y}.png?project_id=${projectId}&task_id=${taskId}`, - ], - tileSize: 256, - }, - layer: { - id: 'ortho-photo', - type: 'raster', - source: 'ortho-photo', - layout: {}, - }, + type: 'raster', + url: `cog://${S3_ENDPOINT}/dtm-data/projects/${projectId}/${taskId}/orthophoto/odm_orthophoto.tif`, + tileSize: 256, }), [projectId, taskId], @@ -57,58 +48,6 @@ const TaskOrthophotoPreview = () => { map?.fitBounds(bbox as LngLatBoundsLike, { padding: 50, duration: 500 }); }, [map, isMapLoaded, taskOutline]); - useEffect(() => { - if ( - !map || - !isMapLoaded || - !projectId || - !taskId || - !orhtophotoSource || - !taskOutline - ) - return; - - // check if the map view intersects the bbox of task - function isInOrthophotoBoundsAndZoom() { - const bounds = map?.getBounds(); // Get the current map bounds (sw, ne) - const zoom = map?.getZoom(); - if (!bounds || !zoom) return null; - const { bbox } = taskOutline.properties; // tasks bbox - return ( - bounds.getWest() < bbox[2] && - bounds.getEast() > bbox[0] && - bounds.getNorth() > bbox[1] && - bounds.getSouth() < bbox[3] && - zoom > 12 - ); - } - - function addOrthophotoLayerIfInBounds() { - if (!map || !orhtophotoSource) return; - if (isInOrthophotoBoundsAndZoom()) { - if (!map.getSource('ortho-photo')) { - map.addSource('ortho-photo', orhtophotoSource.source); - map.addLayer(orhtophotoSource.layer); - } - } else { - if (map.getLayer('ortho-photo')) { - map.removeLayer('ortho-photo'); - } - if (map.getSource('ortho-photo')) { - map.removeSource('ortho-photo'); - } - } - } - - map.on('moveend', () => { - addOrthophotoLayerIfInBounds(); - }); - - map.on('zoomend', () => { - addOrthophotoLayerIfInBounds(); - }); - }, [map, isMapLoaded, projectId, taskId, orhtophotoSource, taskOutline]); - useEffect(() => { return () => { dispatch(setSelectedTaskDetailToViewOrthophoto(null)); @@ -120,13 +59,18 @@ const TaskOrthophotoPreview = () => { + ); diff --git a/src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx b/src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx new file mode 100644 index 00000000..3a465717 --- /dev/null +++ b/src/frontend/src/components/common/MapLibreComponents/COGOrthophotoViewer/index.tsx @@ -0,0 +1,49 @@ +import { useEffect } from 'react'; +import mapLibre, { RasterSourceSpecification } from 'maplibre-gl'; +import { cogProtocol } from '@geomatico/maplibre-cog-protocol'; +import { MapInstanceType } from '../types'; + +interface IViewOrthophotoProps { + map?: MapInstanceType; + isMapLoaded?: Boolean; + id: string; + source: RasterSourceSpecification; + visibleOnMap?: Boolean; +} + +const COGOrthophotoViewer = ({ + map, + isMapLoaded, + id, + source, + visibleOnMap, +}: IViewOrthophotoProps) => { + useEffect(() => { + if (!map || !isMapLoaded || !source || !visibleOnMap) return; + + // Registers the 'cog' protocol with the mapLibre instance, enabling support for Cloud Optimized GeoTIFF (COG) files + mapLibre?.addProtocol('cog', cogProtocol); + + if (!map.getSource(id)) { + map.addSource(id, source); + map.addLayer({ + id, + source: id, + layout: {}, + ...source, + }); + } + + // eslint-disable-next-line consistent-return + return () => { + if (map?.getSource(id)) { + map?.removeSource(id); + if (map?.getLayer(id)) map?.removeLayer(id); + } + }; + }, [map, isMapLoaded, id, source, visibleOnMap]); + + return null; +}; + +export default COGOrthophotoViewer; diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index 3c16a6c9..6e3900ca 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -46,6 +46,7 @@ export default defineConfig({ API_URL_V1: process.env.API_URL_V1, SITE_NAME: process.env.SITE_NAME, STATIC_BASE_URL: process.env.STATIC_BASE_URL, + S3_ENDPOINT: process.env.S3_ENDPOINT, }, }, server: {