diff --git a/.env.example b/.env.example index 0ded43ef..815265be 100644 --- a/.env.example +++ b/.env.example @@ -47,6 +47,7 @@ SITE_NAME=${SITE_NAME:-"DTM-Drone Tasking Manager"} BASE_URL=${BASE_URL:-http://localhost:8000/api} API_URL_V1=${API_URL_V1:-http://localhost:8000/api} NODE_ODM_URL=${NODE_ODM_URL:-http://odm-api:3000} +COG_URL=${COG_URL} ### ODM ### 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..902068be 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 { COG_URL } = 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://${COG_URL}/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/Projects/MapSection/VectorLayerWithCluster.tsx b/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx index 2295a2b2..59e5f04e 100644 --- a/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx +++ b/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx @@ -68,20 +68,18 @@ export default function VectorLayerWithCluster({ }); // inspect a cluster on click - map.on('click', 'clusters', (e: any) => { + map.on('click', 'clusters', async (e: any) => { const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'], }); const clusterId = features[0].properties.cluster_id; - map + const zoom = await map .getSource(sourceId) - .getClusterExpansionZoom(clusterId, (err: any, zoom: any) => { - if (err) return; - map.easeTo({ - center: features[0].geometry.coordinates, - zoom, - }); - }); + .getClusterExpansionZoom(clusterId); + map.easeTo({ + center: features[0].geometry.coordinates, + zoom, + }); }); map.on('mouseenter', 'clusters', () => { 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/src/components/common/MapLibreComponents/Layers/VectorLayer.ts b/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts index f0e8f239..f50170eb 100644 --- a/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts +++ b/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts @@ -56,11 +56,19 @@ export default function VectorLayer({ }); if (hasImage) { - map.loadImage(image, (error, img: any) => { - if (error) throw error; - // Add the loaded image to the style's sprite with the ID 'kitten'. - map.addImage(imageId, img); + // map.loadImage(image, (error, img: any) => { + // if (error) throw error; + // // Add the loaded image to the style's sprite with the ID 'kitten'. + // map.addImage(imageId, img); + // }); + + // changes on map libre 4 + map.loadImage(image).then(({ data }) => { + if (!map.hasImage(imageId)) { + map.addImage(imageId, data); + } }); + map.addLayer({ id: imageId, type: 'symbol', diff --git a/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts b/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts index 77bda231..d875e112 100644 --- a/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts +++ b/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts @@ -126,6 +126,7 @@ export default function useDrawTool({ const lastCoords = coordinates[coordinates.length - 1]; map.addSource('line-start-point', { type: 'geojson', + // @ts-ignore data: { type: 'Feature', geometry: { @@ -136,6 +137,7 @@ export default function useDrawTool({ }); map.addSource('line-end-point', { type: 'geojson', + // @ts-ignore data: { type: 'Feature', geometry: { @@ -177,11 +179,10 @@ export default function useDrawTool({ const featureCollection = draw.getAll(); const { geometry } = featureCollection.features[0]; if (!lineStringTypes.includes(geometry.type)) return () => {}; - map.loadImage(DirectionArrow, (err, image) => { - if (err) return; + map.loadImage(DirectionArrow).then(({ data }) => { if (map.getLayer('arrowId')) return; // @ts-ignore - map.addImage('arrow', image); + map.addImage('arrow', data); map.addLayer({ id: 'arrowId', type: 'symbol', @@ -209,7 +210,7 @@ export default function useDrawTool({ useEffect(() => { if (!map || !drawMode?.includes('draw') || isDrawLayerAdded) return () => {}; - const handleMouseMove = (e: any) => { + const handleMouseMove = () => { // map.getCanvas().style.cursor = 'crosshair'; map.getCanvas().style.cursor = ''; // const description = 'Click to start drawing shape'; diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index 3c16a6c9..b96e6fc8 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, + COG_URL: process.env.COG_URL, }, }, server: {