From afe61c6669bf391bb21956b63b68f60a79cfa516 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Sat, 30 Nov 2024 10:25:59 +0000 Subject: [PATCH] refactor: remove custom logic for loading offline pmtiles in react frontend --- src/frontend/package.json | 1 + src/frontend/pnpm-lock.yaml | 21 ++++++ src/frontend/src/api/Files.ts | 66 ------------------- src/frontend/src/api/Project.ts | 21 +----- .../src/components/GenerateBasemap.tsx | 4 +- .../LayerSwitcher/LayerSwitchMenu.tsx | 6 +- .../LayerSwitcher/index.js | 45 ++++--------- .../ProjectDetailsV2/MapControlComponent.tsx | 6 +- src/frontend/src/store/slices/ProjectSlice.ts | 4 -- src/frontend/src/store/types/IProject.ts | 1 - src/frontend/src/views/ProjectDetailsV2.tsx | 42 ++---------- 11 files changed, 52 insertions(+), 165 deletions(-) diff --git a/src/frontend/package.json b/src/frontend/package.json index 3b88535d87..225da2fa82 100755 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -87,6 +87,7 @@ "ol": "^8.0.0", "ol-ext": "^4.0.11", "ol-layerswitcher": "^4.1.1", + "ol-pmtiles": "^1.0.2", "pako": "^2.1.0", "pmtiles": "^3.0.6", "qrcode-generator": "^1.4.4", diff --git a/src/frontend/pnpm-lock.yaml b/src/frontend/pnpm-lock.yaml index 41c6f6bb13..21ed459a22 100644 --- a/src/frontend/pnpm-lock.yaml +++ b/src/frontend/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: ol-layerswitcher: specifier: ^4.1.1 version: 4.1.1(ol@8.2.0) + ol-pmtiles: + specifier: ^1.0.2 + version: 1.0.2(ol@8.2.0) pako: specifier: ^2.1.0 version: 2.1.0 @@ -3566,6 +3569,11 @@ packages: peerDependencies: ol: '>=5.0.0' + ol-pmtiles@1.0.2: + resolution: {integrity: sha512-+2itEeTcOk6RWikH2/cWIvv2mFW0empLaCibon65e1kyOEzB+zHIqF3eKa15yyznV8r9K0wfx9S3aG6ceXL0hQ==} + peerDependencies: + ol: '>=9.0.0' + ol@8.2.0: resolution: {integrity: sha512-/m1ddd7Jsp4Kbg+l7+ozR5aKHAZNQOBAoNZ5pM9Jvh4Etkf0WGkXr9qXd7PnhmwiC1Hnc2Toz9XjCzBBvexfXw==} @@ -3676,6 +3684,9 @@ packages: pmtiles@3.0.6: resolution: {integrity: sha512-IdeMETd5lBIDVTLul1HFl0Q7l4KLJjzdxgcp+sN7pYvbipaV7o/0u0HiV06kaFCD0IGEN8KtUHyFZpY30WMflw==} + pmtiles@3.2.1: + resolution: {integrity: sha512-3R4fBwwoli5mw7a6t1IGwOtfmcSAODq6Okz0zkXhS1zi9sz1ssjjIfslwPvcWw5TNhdjNBUg9fgfPLeqZlH6ng==} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -8432,6 +8443,11 @@ snapshots: dependencies: ol: 8.2.0 + ol-pmtiles@1.0.2(ol@8.2.0): + dependencies: + ol: 8.2.0 + pmtiles: 3.2.1 + ol@8.2.0: dependencies: color-rgba: 3.0.0 @@ -8541,6 +8557,11 @@ snapshots: '@types/leaflet': 1.9.8 fflate: 0.8.2 + pmtiles@3.2.1: + dependencies: + '@types/leaflet': 1.9.8 + fflate: 0.8.2 + possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.38): diff --git a/src/frontend/src/api/Files.ts b/src/frontend/src/api/Files.ts index f130f68186..e6866728f2 100755 --- a/src/frontend/src/api/Files.ts +++ b/src/frontend/src/api/Files.ts @@ -55,69 +55,3 @@ export const GetProjectQrCode = ( }, [projectName, odkToken, osmUser]); return { qrcode }; }; - -export async function readFileFromOPFS(filePath: string) { - const opfsRoot = await navigator.storage.getDirectory(); - const directories = filePath.split('/'); - - let currentDirectoryHandle = opfsRoot; - - // Iterate dirs and get directoryHandles - for (const directory of directories.slice(0, -1)) { - console.log(`Reading OPFS dir: ${directory}`); - try { - currentDirectoryHandle = await currentDirectoryHandle.getDirectoryHandle(directory); - } catch { - return null; // Directory doesn't exist - } - } - - // Get file within final directory handle - try { - const filename: any = directories.pop(); - console.log(`Getting OPFS file: ${filename}`); - const fileHandle = await currentDirectoryHandle.getFileHandle(filename); - const fileData = await fileHandle.getFile(); // Read the file - return fileData; - } catch { - return null; // File doesn't exist or error occurred - } -} - -export async function writeBinaryToOPFS(filePath: string, data: any) { - console.log(`Starting write to OPFS file: ${filePath}`); - - const opfsRoot = await navigator.storage.getDirectory(); - - // Split the filePath into directories and filename - const directories = filePath.split('/'); - const filename: any = directories.pop(); - - // Start with the root directory handle - let currentDirectoryHandle = opfsRoot; - - // Iterate over directories and create nested directories - for (const directory of directories) { - console.log(`Creating OPFS dir: ${directory}`); - try { - currentDirectoryHandle = await currentDirectoryHandle.getDirectoryHandle(directory, { create: true }); - } catch (error) { - console.error('Error creating directory:', error); - } - } - - // Create the file handle within the last directory - const fileHandle = await currentDirectoryHandle.getFileHandle(filename, { create: true }); - const writable = await fileHandle.createWritable(); - - // Write data to the writable stream - try { - await writable.write(data); - } catch (error) { - console.log(error); - } - - // Close the writable stream - await writable.close(); - console.log(`Finished write to OPFS file: ${filePath}`); -} diff --git a/src/frontend/src/api/Project.ts b/src/frontend/src/api/Project.ts index 1a91e5570d..8012166c1b 100755 --- a/src/frontend/src/api/Project.ts +++ b/src/frontend/src/api/Project.ts @@ -2,8 +2,6 @@ import { ProjectActions } from '@/store/slices/ProjectSlice'; import { CommonActions } from '@/store/slices/CommonSlice'; import CoreModules from '@/shared/CoreModules'; import { task_state, task_event } from '@/types/enums'; -import { writeBinaryToOPFS } from '@/api/Files'; -import { projectInfoType } from '@/models/project/projectModel'; export const ProjectById = (projectId: string) => { return async (dispatch) => { @@ -169,25 +167,12 @@ export const GenerateProjectTiles = (url: string, projectId: string, data: objec }; }; -export const DownloadTile = (url: string, projectId: string | null) => { +export const DownloadTile = (url: string) => { return async (dispatch) => { dispatch(ProjectActions.SetDownloadTileLoading({ loading: true })); - const getDownloadTile = async (url: string, projectId: string | null) => { + const getDownloadTile = async (url: string) => { try { - if (projectId) { - const response = await CoreModules.axios.get(url, { - responseType: 'arraybuffer', - }); - const tileData = response.data; - // Copy to OPFS filesystem for offline use - const filePath = `${projectId}/all.pmtiles`; - await writeBinaryToOPFS(filePath, tileData); - // Set the OPFS file path to project state - dispatch(ProjectActions.SetProjectOpfsBasemapPath(filePath)); - return; - } - // Open S3 url directly window.open(url); @@ -198,7 +183,7 @@ export const DownloadTile = (url: string, projectId: string | null) => { dispatch(ProjectActions.SetDownloadTileLoading({ loading: false })); } }; - await getDownloadTile(url, projectId); + await getDownloadTile(url); }; }; diff --git a/src/frontend/src/components/GenerateBasemap.tsx b/src/frontend/src/components/GenerateBasemap.tsx index 8faf542f75..07482c063e 100644 --- a/src/frontend/src/components/GenerateBasemap.tsx +++ b/src/frontend/src/components/GenerateBasemap.tsx @@ -31,8 +31,8 @@ const GenerateBasemap = ({ projectInfo }: { projectInfo: Partial { - dispatch(DownloadTile(url, toOpfs ? id : null)); + const downloadBasemap = (url) => { + dispatch(DownloadTile(url)); }; const getTilesList = () => { diff --git a/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/LayerSwitchMenu.tsx b/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/LayerSwitchMenu.tsx index 3f13dc8427..e06acef95f 100644 --- a/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/LayerSwitchMenu.tsx +++ b/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/LayerSwitchMenu.tsx @@ -56,7 +56,7 @@ const LayerCard = ({ layer, changeBaseLayerHandler, active }: layerCardPropType) ); }; -const LayerSwitchMenu = ({ map, pmTileLayerData = null }: { map: any; pmTileLayerData?: any }) => { +const LayerSwitchMenu = ({ map, pmTileLayerUrl = null }: { map: any; pmTileLayerUrl?: any }) => { const { pathname } = useLocation(); const [baseLayers, setBaseLayers] = useState(['OSM', 'Satellite', 'None']); const [hasPMTile, setHasPMTile] = useState(false); @@ -78,10 +78,10 @@ const LayerSwitchMenu = ({ map, pmTileLayerData = null }: { map: any; pmTileLaye }, [projectInfo, pathname, map]); useEffect(() => { - if (!pmTileLayerData || baseLayers.includes('PMTile')) return; + if (!pmTileLayerUrl || baseLayers.includes('PMTile')) return; setHasPMTile(true); setActiveTileLayer('PMTile'); - }, [pmTileLayerData]); + }, [pmTileLayerUrl]); const changeBaseLayer = (baseLayerTitle: string) => { const allLayers = map.getLayers(); diff --git a/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js b/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js index 0d3a823e2c..a5db3bb694 100644 --- a/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js +++ b/src/frontend/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js @@ -10,7 +10,7 @@ import { XYZ } from 'ol/source'; import { useLocation } from 'react-router-dom'; import DataTile from 'ol/source/DataTile.js'; import TileLayer from 'ol/layer/WebGLTile.js'; -import { FileSource, PMTiles } from 'pmtiles'; +import { PMTilesRasterSource } from 'ol-pmtiles'; import windowDimention from '@/hooks/WindowDimension'; import { useAppSelector } from '@/types/reduxTypes'; @@ -134,41 +134,20 @@ const monochromeMidNight = (visible = false) => }), }); -const pmTileLayer = (pmTileLayerData, visible) => { - function loadImage(src) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.addEventListener('load', () => resolve(img)); - img.addEventListener('error', () => reject(new Error('load failed'))); - img.src = src; - }); - } - - const pmTiles = new PMTiles(new FileSource(pmTileLayerData)); - - async function loader(z, x, y) { - const response = await pmTiles.getZxy(z, x, y); - const blob = new Blob([response.data]); - const src = URL.createObjectURL(blob); - const image = await loadImage(src); - URL.revokeObjectURL(src); - return image; - } - return new TileLayer({ +const pmTileLayer = (pmTileLayerUrl, visible = true) => { + return new WebGLTile({ title: `PMTile`, type: 'raster pm tiles', - visible: true, - source: new DataTile({ - loader, - wrapX: true, + visible: visible, + source: new PMTilesRasterSource({ + url: pmTileLayerUrl, + attributions: ['OpenAerialMap'], tileSize: [512, 512], - maxZoom: 22, - attributions: 'Tiles © OpenAerialMap', }), }); }; -const LayerSwitcherControl = ({ map, visible = 'osm', pmTileLayerData = null }) => { +const LayerSwitcherControl = ({ map, visible = 'osm', pmTileLayerUrl = null }) => { const { windowSize } = windowDimention(); const { pathname } = useLocation(); @@ -181,7 +160,7 @@ const LayerSwitcherControl = ({ map, visible = 'osm', pmTileLayerData = null }) // mapboxMap(visible), // mapboxOutdoors(visible), none(visible), - // pmTileLayer(pmTileLayerData, visible), + // pmTileLayer(pmTileLayerUrl, visible), ], }), ); @@ -246,11 +225,11 @@ const LayerSwitcherControl = ({ map, visible = 'osm', pmTileLayerData = null }) }, [map, visible]); useEffect(() => { - if (!pmTileLayerData) { + if (!pmTileLayerUrl) { return; } - const pmTileBaseLayer = pmTileLayer(pmTileLayerData, visible); + const pmTileBaseLayer = pmTileLayer(pmTileLayerUrl, visible); const currentLayers = basemapLayers.getLayers(); currentLayers.push(pmTileBaseLayer); @@ -259,7 +238,7 @@ const LayerSwitcherControl = ({ map, visible = 'osm', pmTileLayerData = null }) return () => { basemapLayers.getLayers().remove(pmTileBaseLayer); }; - }, [pmTileLayerData]); + }, [pmTileLayerUrl]); const location = useLocation(); useEffect(() => {}, [map]); diff --git a/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx b/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx index 4461329eec..439d2717ec 100644 --- a/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx +++ b/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx @@ -15,7 +15,7 @@ import MapLegends from '@/components/MapLegends'; type mapControlComponentType = { map: any; projectName: string; - pmTileLayerData: any; + pmTileLayerUrl: any; }; const btnList = [ @@ -41,7 +41,7 @@ const btnList = [ }, ]; -const MapControlComponent = ({ map, projectName, pmTileLayerData }: mapControlComponentType) => { +const MapControlComponent = ({ map, projectName, pmTileLayerUrl }: mapControlComponentType) => { const { pathname } = useLocation(); const dispatch = CoreModules.useAppDispatch(); const [toggleCurrentLoc, setToggleCurrentLoc] = useState(false); @@ -89,7 +89,7 @@ const MapControlComponent = ({ map, projectName, pmTileLayerData }: mapControlCo ))} - + {/* download options */}
{ const [dataExtractUrl, setDataExtractUrl] = useState(); const [dataExtractExtent, setDataExtractExtent] = useState(null); const [taskBoundariesLayer, setTaskBoundariesLayer] = useState>(null); - // Can pass a File object, or a string URL to be read by PMTiles - const [customBasemapData, setCustomBasemapData] = useState(); + // FIXME currently we have no logic to retrieve the PMTiles for a project and pass + // FIXME as the customBasemapUrl. + // FIXME This should probably be triggered based on project customTmsUrl being set. + // FIXME If set, then we search for the first PMTiles archive available? + const [customBasemapUrl, setcustomBasemapUrl] = useState(); const [viewState, setViewState] = useState('project_info'); const projectId: string = params.id; const defaultTheme = useAppSelector((state) => state.theme.hotTheme); @@ -68,7 +70,6 @@ const ProjectDetailsV2 = () => { const projectDetailsLoading = useAppSelector((state) => state?.project?.projectDetailsLoading); const geolocationStatus = useAppSelector((state) => state.project.geolocationStatus); const taskModalStatus = CoreModules.useAppSelector((state) => state.project.taskModalStatus); - const projectOpfsBasemapPath = useAppSelector((state) => state?.project?.projectOpfsBasemapPath); const authDetails = CoreModules.useAppSelector((state) => state.login.authDetails); const entityOsmMap = useAppSelector((state) => state?.project?.entityOsmMap); @@ -255,32 +256,6 @@ const ProjectDetailsV2 = () => { dispatch(GetEntityInfo(`${import.meta.env.VITE_API_URL}/projects/${projectId}/entities/statuses`)); }, []); - const getPmtilesBasemap = async () => { - if (!projectOpfsBasemapPath) { - return; - } - const opfsPmtilesData = await readFileFromOPFS(projectOpfsBasemapPath); - setCustomBasemapData(opfsPmtilesData); - }; - useEffect(() => { - if (!projectOpfsBasemapPath) { - return; - } - - // Extract project id from projectOpfsBasemapPath - const projectOpfsBasemapPathParts = projectOpfsBasemapPath.split('/'); - const projectOpfsBasemapProjectId = projectOpfsBasemapPathParts[0]; - - // Check if project id from projectOpfsBasemapPath matches current projectId - if (projectOpfsBasemapProjectId !== projectId) { - // If they don't match, set CustomBasemapData to null - setCustomBasemapData(null); - } else { - // If they match, fetch the basemap data - getPmtilesBasemap(); - } - return () => {}; - }, [projectOpfsBasemapPath]); const [showDebugConsole, setShowDebugConsole] = useState(false); return ( @@ -450,10 +425,7 @@ const ProjectDetailsV2 = () => { />
)} - + {taskBoundariesLayer && taskBoundariesLayer?.features?.length > 0 && ( {