From 7c9ae72a0e67f40ad84381dbe3e75caa4628a56a Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Mon, 16 Sep 2024 07:31:07 +0200 Subject: [PATCH 1/5] PB-1000 : add support for local GeoTIFF import online GeoTIFF requires some work done on service-proxy, as it currently answers with 413 Payload Too Large (obviously...) I've tested with a Dropbox link (hence the use of the proxy). I couldn't get our STAC API to give me a file (through a URL import), it blocks somewhere (maybe on purpose, so that we don't use our STAC API as a web hosting) --- package-lock.json | 1 + package.json | 1 + src/api/layers/GeoTIFFLayer.class.js | 58 +++++++++++++++++++ src/api/layers/LayerTypes.enum.js | 1 + src/modules/i18n/locales/de.json | 2 +- src/modules/i18n/locales/en.json | 2 +- src/modules/i18n/locales/fr.json | 2 +- src/modules/i18n/locales/it.json | 2 +- src/modules/i18n/locales/rm.json | 2 +- .../openlayers/OpenLayersGeoTIFF.vue | 45 ++++++++++++++ .../openlayers/OpenLayersInternalLayer.vue | 7 +++ .../utils/useMapInteractions.composable.js | 10 +++- .../advancedTools/ImportFile/utils.js | 58 ++++++++++++++----- 13 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 src/api/layers/GeoTIFFLayer.class.js create mode 100644 src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue diff --git a/package-lock.json b/package-lock.json index aca1f1504..dbb1fb978 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "file-saver": "^2.0.5", "form-data": "^4.0.0", "geographiclib-geodesic": "^2.1.1", + "geotiff": "^2.1.3", "hammerjs": "^2.0.8", "jquery": "^3.7.1", "jszip": "^3.10.1", diff --git a/package.json b/package.json index 664814691..167232247 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "file-saver": "^2.0.5", "form-data": "^4.0.0", "geographiclib-geodesic": "^2.1.1", + "geotiff": "^2.1.3", "hammerjs": "^2.0.8", "jquery": "^3.7.1", "jszip": "^3.10.1", diff --git a/src/api/layers/GeoTIFFLayer.class.js b/src/api/layers/GeoTIFFLayer.class.js new file mode 100644 index 000000000..79e85f61b --- /dev/null +++ b/src/api/layers/GeoTIFFLayer.class.js @@ -0,0 +1,58 @@ +import AbstractLayer, { LayerAttribution } from '@/api/layers/AbstractLayer.class' +import { InvalidLayerDataError } from '@/api/layers/InvalidLayerData.error' +import LayerTypes from '@/api/layers/LayerTypes.enum' + +/** + * Metadata for an external Cloud-Optimized GeoTIFF layer + * + * @WARNING DON'T USE GETTER AND SETTER ! Instances of this class will be used a Vue 3 reactive + * object which SHOULD BE plain javascript object ! For convenience we use class instances but this + * has some limitations and javascript class getter and setter are not correctly supported which + * introduced subtle bugs. As rule of thumb we should avoid any public methods with side effects on + * properties, properties should change be changed either by the constructor or directly by setting + * them, not through a functions that updates other properties as it can lead to subtle bugs due + * to Vue reactivity engine. + */ +export default class GeoTIFFLayer extends AbstractLayer { + /** + * @param {String} geoTIFFConfig.fileSource The URL to access the GeoTIFF data. + * @param {Boolean} [geoTIFFConfig.visible=true] If the layer is visible on the map (or hidden). + * When `null` is given, then it uses the default value. Default is `true` + * @param {Number} [geoTIFFConfig.opacity=1.0] The opacity of this layer, between 0.0 + * (transparent) and 1.0 (opaque). When `null` is given, then it uses the default value. + * Default is `1.0` + * @param {String | null} [geoTIFFConfig.data=null] Data/content of the GeoTIFF file, as a + * string. Default is `null` + * @throws InvalidLayerDataError if no `geoTIFFConfig` is given or if it is invalid + */ + constructor(geoTIFFConfig) { + if (!geoTIFFConfig) { + throw new InvalidLayerDataError('Missing GeoTIFF layer data', geoTIFFConfig) + } + const { fileSource = null, visible = true, opacity = 1.0, data = null } = geoTIFFConfig + if (fileSource === null) { + throw new InvalidLayerDataError('Missing GeoTIFF file source', geoTIFFConfig) + } + const isLocalFile = !fileSource.startsWith('http') + const attributionName = isLocalFile ? fileSource : new URL(fileSource).hostname + const fileName = isLocalFile + ? fileSource + : fileSource.substring(fileSource.lastIndexOf('/') + 1) + super({ + name: fileName, + id: fileSource, + type: LayerTypes.GEOTIFF, + baseUrl: fileSource, + opacity: opacity ?? 1.0, + visible: visible ?? true, + attributions: [new LayerAttribution(attributionName)], + isExternal: true, + hasDescription: false, + hasLegend: false, + }) + this.isLocalFile = isLocalFile + this.fileSource = fileSource + this.isLoading = !data + this.data = data + } +} diff --git a/src/api/layers/LayerTypes.enum.js b/src/api/layers/LayerTypes.enum.js index ae8cbb4c7..bacf20ae5 100644 --- a/src/api/layers/LayerTypes.enum.js +++ b/src/api/layers/LayerTypes.enum.js @@ -11,5 +11,6 @@ const LayerTypes = { GPX: 'GPX', VECTOR: 'VECTOR', GROUP: 'GROUP', + GEOTIFF: 'GEOTIFF', } export default LayerTypes diff --git a/src/modules/i18n/locales/de.json b/src/modules/i18n/locales/de.json index 23e09e6d3..4fb29f81d 100644 --- a/src/modules/i18n/locales/de.json +++ b/src/modules/i18n/locales/de.json @@ -172,7 +172,7 @@ "drawing_attached": "Zeichnung als Anhang hinzugefügt", "drawing_too_large": "Ihre Zeichnung ist zu gross, entfernen Sie einige Details.", "drop_invalid_url": "URL ist ungültig.", - "drop_me_here": "KML Datei hierhin ziehen", + "drop_me_here": "Datei hier ablegen (KML, GPX, GeoTIFF)", "duplicate_layer": "Karte duplizieren", "east": "Ost", "ech": "Geokatalog", diff --git a/src/modules/i18n/locales/en.json b/src/modules/i18n/locales/en.json index 46145619d..cda1f173f 100644 --- a/src/modules/i18n/locales/en.json +++ b/src/modules/i18n/locales/en.json @@ -172,7 +172,7 @@ "drawing_attached": "Drawing added as attachment", "drawing_too_large": "Your drawing is too large, remove some features", "drop_invalid_url": "URL is not valid.", - "drop_me_here": "Drop KML file here", + "drop_me_here": "Drop file here (KML, GPX, GeoTIFF)", "duplicate_layer": "Duplicate map", "east": "East", "ech": "Geocatalog", diff --git a/src/modules/i18n/locales/fr.json b/src/modules/i18n/locales/fr.json index f8880ef4a..280a5b58e 100644 --- a/src/modules/i18n/locales/fr.json +++ b/src/modules/i18n/locales/fr.json @@ -172,7 +172,7 @@ "drawing_attached": "Dessin ajouté en pièce jointe", "drawing_too_large": "Ton dessin est trop grand, enlève quelques éléments", "drop_invalid_url": "URL non valide.", - "drop_me_here": "Déplacer le fichier KML ici", + "drop_me_here": "Déposer le fichier ici (KML, GPX, GeoTIFF)", "duplicate_layer": "Duplication de carte", "east": "Est", "ech": "Géocatalogue", diff --git a/src/modules/i18n/locales/it.json b/src/modules/i18n/locales/it.json index 43ddc890e..b549d1f1b 100644 --- a/src/modules/i18n/locales/it.json +++ b/src/modules/i18n/locales/it.json @@ -172,7 +172,7 @@ "drawing_attached": "Disegno aggiunto come allegato", "drawing_too_large": "Il suo disegno è troppo grande, rimuova alcuni elementi", "drop_invalid_url": "URL non valido", - "drop_me_here": "Spostare file KML qui", + "drop_me_here": "Lasciare qui il file (KML, GPX, GeoTIFF)", "duplicate_layer": "Mappa duplicata", "east": "Est", "ech": "Geocatalogo", diff --git a/src/modules/i18n/locales/rm.json b/src/modules/i18n/locales/rm.json index 934ddbdcd..eb85360c5 100644 --- a/src/modules/i18n/locales/rm.json +++ b/src/modules/i18n/locales/rm.json @@ -172,7 +172,7 @@ "drawing_attached": "Dissegn è agiuntà sco agiunta.", "drawing_too_large": "Tes dissegn è memia grond, stizza intgins detagls", "drop_invalid_url": "URL è nunvalid", - "drop_me_here": "Trair vi la datoteca KML nà qua", + "drop_me_here": "Dar giu la datoteca (KML, GPX, GeoTIFF)", "duplicate_layer": "Mapa duplicada", "east": "ost", "ech": "Catalog da geodatas", diff --git a/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue b/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue new file mode 100644 index 000000000..5ade51352 --- /dev/null +++ b/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/modules/map/components/openlayers/OpenLayersInternalLayer.vue b/src/modules/map/components/openlayers/OpenLayersInternalLayer.vue index 9b5986eba..7ffc4d073 100644 --- a/src/modules/map/components/openlayers/OpenLayersInternalLayer.vue +++ b/src/modules/map/components/openlayers/OpenLayersInternalLayer.vue @@ -11,6 +11,7 @@ import AbstractLayer from '@/api/layers/AbstractLayer.class' import LayerTypes from '@/api/layers/LayerTypes.enum' import OpenLayersExternalWMTSLayer from '@/modules/map/components/openlayers/OpenLayersExternalWMTSLayer.vue' import OpenLayersGeoJSONLayer from '@/modules/map/components/openlayers/OpenLayersGeoJSONLayer.vue' +import OpenLayersGeoTIFF from '@/modules/map/components/openlayers/OpenLayersGeoTIFF.vue' import OpenLayersGPXLayer from '@/modules/map/components/openlayers/OpenLayersGPXLayer.vue' import OpenLayersKMLLayer from '@/modules/map/components/openlayers/OpenLayersKMLLayer.vue' import OpenLayersVectorLayer from '@/modules/map/components/openlayers/OpenLayersVectorLayer.vue' @@ -129,6 +130,12 @@ function shouldAggregateSubLayerBeVisible(subLayer) { :parent-layer-opacity="parentLayerOpacity" :z-index="zIndex" /> + diff --git a/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js b/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js index ada912a06..63b70dd5e 100644 --- a/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js +++ b/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js @@ -243,6 +243,10 @@ export default function useMapInteractions(map) { mapElement.removeEventListener('dragleave', onDragLeave) } + /** + * @param {File} file + * @returns {Promise} + */ function readFileContent(file) { return new Promise((resolve, reject) => { const reader = new FileReader() @@ -254,10 +258,14 @@ export default function useMapInteractions(map) { }) } + /** + * @param {File} file + * @returns {Promise} + */ async function handleFile(file) { try { const fileContent = await readFileContent(file) - handleFileContent(store, fileContent, file.name) + handleFileContent(store, fileContent, file.name, file) } catch (error) { let errorKey log.error(`Error loading file`, file.name, error) diff --git a/src/modules/menu/components/advancedTools/ImportFile/utils.js b/src/modules/menu/components/advancedTools/ImportFile/utils.js index 8e8d2cfb0..ce990f6b4 100644 --- a/src/modules/menu/components/advancedTools/ImportFile/utils.js +++ b/src/modules/menu/components/advancedTools/ImportFile/utils.js @@ -1,10 +1,12 @@ +import { fromArrayBuffer, GeoTIFF } from 'geotiff' import GPX from 'ol/format/GPX' -import GPXLayer from '@/api/layers/GPXLayer.class.js' +import GeoTIFFLayer from '@/api/layers/GeoTIFFLayer.class' +import GPXLayer from '@/api/layers/GPXLayer.class' import KMLLayer from '@/api/layers/KMLLayer.class' -import { OutOfBoundsError } from '@/utils/coordinates/coordinateUtils' -import { getExtentForProjection } from '@/utils/extentUtils.js' -import { EmptyGPXError, getGpxExtent } from '@/utils/gpxUtils.js' +import { normalizeExtent, OutOfBoundsError } from '@/utils/coordinates/coordinateUtils' +import { getExtentForProjection } from '@/utils/extentUtils' +import { EmptyGPXError, getGpxExtent } from '@/utils/gpxUtils' import { EmptyKMLError, getKmlExtent, unzipKmz } from '@/utils/kmlUtils' import log from '@/utils/logging' import { isZipContent } from '@/utils/utils' @@ -39,31 +41,57 @@ export function isGpx(fileContent) { * @param {OBject} store Vuex store * @param {ArrayBuffer} content Content of the file * @param {string} source Source of the file (either URL or file path) + * @param {File | null} [originalFile] * @returns {ExternalLayer} External layer object */ -export async function handleFileContent(store, content, source) { +export async function handleFileContent(store, content, source, originalFile = null) { let layer = null - let textContent + let parsedContent let linkFiles if (isZipContent(content)) { log.debug(`File content is a zipfile, assume it is a KMZ archive`) const kmz = await unzipKmz(content, source) - textContent = kmz.kml + parsedContent = kmz.kml linkFiles = kmz.files + } else if (source.endsWith('.tif')) { + log.debug(`File content might be a COGTIFF, attempting a parse as such`) + try { + parsedContent = await fromArrayBuffer(content) + } catch (err) { + log.debug('parsing as COGTIFF failed, moving on with other formats', err) + } } else { // If it is not a zip file then we assume is a text file and decode it for further handling - textContent = new TextDecoder('utf-8').decode(content) + parsedContent = new TextDecoder('utf-8').decode(content) } - if (isKml(textContent)) { + if (parsedContent instanceof GeoTIFF) { + layer = new GeoTIFFLayer({ + fileSource: source, + visible: true, + opacity: 1.0, + // For local files : OpenLayers needs a Blob, and not an already parsed GeoTIFF instance + data: originalFile, + }) + store.dispatch('addLayer', { layer, ...dispatcher }) + // looking into the GeoTIFF file to get the extento + const geoTIFFImage = await parsedContent.getImage() + const geoTIFFExtent = geoTIFFImage.getBoundingBox() + if (geoTIFFExtent) { + store.dispatch('zoomToExtent', { + extent: normalizeExtent(geoTIFFExtent), + ...dispatcher, + }) + } + } else if (isKml(parsedContent)) { layer = new KMLLayer({ kmlFileUrl: source, visible: true, opacity: 1.0, adminId: null, - kmlData: textContent, + kmlData: parsedContent, linkFiles, }) - const extent = getKmlExtent(textContent) + const extent = getKmlExtent(parsedContent) if (!extent) { throw new EmptyKMLError() } @@ -78,17 +106,17 @@ export async function handleFileContent(store, content, source) { } else { store.dispatch('addLayer', { layer, ...dispatcher }) } - } else if (isGpx(textContent)) { + } else if (isGpx(parsedContent)) { const gpxParser = new GPX() - const metadata = gpxParser.readMetadata(textContent) + const metadata = gpxParser.readMetadata(parsedContent) layer = new GPXLayer({ gpxFileUrl: source, visible: true, opacity: 1.0, - gpxData: textContent, + gpxData: parsedContent, gpxMetadata: metadata, }) - const extent = getGpxExtent(textContent) + const extent = getGpxExtent(parsedContent) if (!extent) { throw new EmptyGPXError() } From 829205790be35becf657ebfb2af479d71e9c55c7 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Mon, 16 Sep 2024 16:47:27 +0200 Subject: [PATCH 2/5] PB-1000 : switch GeoTIFF detection to a signature based instead of relying only on the file extension, will be more robust, and take into account .tif, .tiff (both as a mix of uppercased) and files without extension that are TIFF underneath --- .../advancedTools/ImportFile/utils.js | 50 ++++++++++--------- src/utils/utils.js | 19 +++++++ 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/modules/menu/components/advancedTools/ImportFile/utils.js b/src/modules/menu/components/advancedTools/ImportFile/utils.js index ce990f6b4..69456acfb 100644 --- a/src/modules/menu/components/advancedTools/ImportFile/utils.js +++ b/src/modules/menu/components/advancedTools/ImportFile/utils.js @@ -1,4 +1,4 @@ -import { fromArrayBuffer, GeoTIFF } from 'geotiff' +import { fromArrayBuffer } from 'geotiff' import GPX from 'ol/format/GPX' import GeoTIFFLayer from '@/api/layers/GeoTIFFLayer.class' @@ -9,7 +9,7 @@ import { getExtentForProjection } from '@/utils/extentUtils' import { EmptyGPXError, getGpxExtent } from '@/utils/gpxUtils' import { EmptyKMLError, getKmlExtent, unzipKmz } from '@/utils/kmlUtils' import log from '@/utils/logging' -import { isZipContent } from '@/utils/utils' +import { isTiffContent, isZipContent } from '@/utils/utils' const dispatcher = { dispatcher: 'ImportFile/utils' } @@ -53,36 +53,38 @@ export async function handleFileContent(store, content, source, originalFile = n const kmz = await unzipKmz(content, source) parsedContent = kmz.kml linkFiles = kmz.files - } else if (source.endsWith('.tif')) { - log.debug(`File content might be a COGTIFF, attempting a parse as such`) + } else if (isTiffContent(content)) { + log.debug(`File content might be a GeoTIFF, attempting a parse as such`) try { parsedContent = await fromArrayBuffer(content) + layer = new GeoTIFFLayer({ + fileSource: source, + visible: true, + opacity: 1.0, + // For local files : OpenLayers needs a Blob, and not an already parsed GeoTIFF instance + data: originalFile, + }) + store.dispatch('addLayer', { layer, ...dispatcher }) + // looking into the GeoTIFF file to get the extent + const geoTIFFImage = await parsedContent.getImage() + const geoTIFFExtent = geoTIFFImage.getBoundingBox() + if (geoTIFFExtent) { + store.dispatch('zoomToExtent', { + extent: normalizeExtent(geoTIFFExtent), + ...dispatcher, + }) + } + // we are done here, so to not run the parsed content part below, we stop the function here + return layer } catch (err) { - log.debug('parsing as COGTIFF failed, moving on with other formats', err) + log.debug('parsing as GeoTIFF failed', err) + throw new Error(`Could not parse GeoTIFF from ${source}`) } } else { // If it is not a zip file then we assume is a text file and decode it for further handling parsedContent = new TextDecoder('utf-8').decode(content) } - if (parsedContent instanceof GeoTIFF) { - layer = new GeoTIFFLayer({ - fileSource: source, - visible: true, - opacity: 1.0, - // For local files : OpenLayers needs a Blob, and not an already parsed GeoTIFF instance - data: originalFile, - }) - store.dispatch('addLayer', { layer, ...dispatcher }) - // looking into the GeoTIFF file to get the extento - const geoTIFFImage = await parsedContent.getImage() - const geoTIFFExtent = geoTIFFImage.getBoundingBox() - if (geoTIFFExtent) { - store.dispatch('zoomToExtent', { - extent: normalizeExtent(geoTIFFExtent), - ...dispatcher, - }) - } - } else if (isKml(parsedContent)) { + if (isKml(parsedContent)) { layer = new KMLLayer({ kmlFileUrl: source, visible: true, diff --git a/src/utils/utils.js b/src/utils/utils.js index 1748f9e30..a28e7a9b2 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -278,3 +278,22 @@ export function isZipContent(content) { } return true } + +/** + * Check if the input is a TIFF content or not + * + * @param {ArrayBuffer} content + * @returns {boolean} Return true if the content is a TIFF content + */ +export function isTiffContent(content) { + // Check the first 4 bytes for the TIFF file signature + // there are two signatures that can be found here https://en.wikipedia.org/wiki/TIFF + const tiffSignatures = [ + [0x49, 0x49, 0x2a, 0x00], + [0x4d, 0x4d, 0x00, 0x2a], + ] + const contentSignature = new Uint8Array(content.slice(0, 4)) + return tiffSignatures.some((signature) => + signature.every((byte, index) => byte === contentSignature[index]) + ) +} From 4c8927bf70d029ed5f5b4f53bc018ddc687f2915 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Thu, 19 Sep 2024 11:18:27 +0200 Subject: [PATCH 3/5] PB-1000 : allow persistence of GeoTIFF layer through URL params adding the new type in the layer parser also fixing loading GeoTIFF through URL in the OL component. removing big-endian signature check, as 99.9% of hardware on the web uses little-endian removing the concept of GeoTIFF layer needing to load, they are either already loaded because local file, or will be loaded through the OpenLayers component (not by us, so no need to keep track of that in the menu) --- src/api/layers/GeoTIFFLayer.class.js | 2 +- src/modules/i18n/locales/de.json | 2 +- src/modules/i18n/locales/en.json | 2 +- src/modules/i18n/locales/fr.json | 2 +- src/modules/i18n/locales/it.json | 2 +- src/modules/i18n/locales/rm.json | 2 +- .../map/components/openlayers/OpenLayersGeoTIFF.vue | 2 +- .../openlayers/utils/useMapInteractions.composable.js | 2 +- .../advancedTools/ImportFile/ImportFileLocalTab.vue | 2 +- .../advancedTools/ImportFile/ImportFileOnlineTab.vue | 2 +- .../menu/components/advancedTools/ImportFile/utils.js | 2 +- src/router/storeSync/LayerParamConfig.class.js | 10 ++++++++++ src/utils/utils.js | 11 +++-------- 13 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/api/layers/GeoTIFFLayer.class.js b/src/api/layers/GeoTIFFLayer.class.js index 79e85f61b..bebc437e2 100644 --- a/src/api/layers/GeoTIFFLayer.class.js +++ b/src/api/layers/GeoTIFFLayer.class.js @@ -52,7 +52,7 @@ export default class GeoTIFFLayer extends AbstractLayer { }) this.isLocalFile = isLocalFile this.fileSource = fileSource - this.isLoading = !data + this.isLoading = false this.data = data } } diff --git a/src/modules/i18n/locales/de.json b/src/modules/i18n/locales/de.json index 4fb29f81d..4d65d6f0d 100644 --- a/src/modules/i18n/locales/de.json +++ b/src/modules/i18n/locales/de.json @@ -308,7 +308,7 @@ "inspire_service_link_label": "geo.admin.ch", "invalid_email": "ungültige E-Mail", "invalid_file": "Datei ungültig", - "invalid_kml_gpx_file_error": "Ungültige Datei, nur KML- oder GPX-Dateien werden unterstützt", + "invalid_import_file_error": "Ungültige Datei, nur KML, GPX oder GeoTIFF-Dateien werden unterstützt", "invalid_url": "URL ist ungültig.", "invalid_wms_capabilities": "Ungültige WMS-Capabilities-Daten", "invalid_wmts_capabilities": "Ungültige WMTS-Capabilities-Daten", diff --git a/src/modules/i18n/locales/en.json b/src/modules/i18n/locales/en.json index cda1f173f..f992f769f 100644 --- a/src/modules/i18n/locales/en.json +++ b/src/modules/i18n/locales/en.json @@ -308,7 +308,7 @@ "inspire_service_link_label": "geo.admin.ch", "invalid_email": "Invalid email", "invalid_file": "Invalid file", - "invalid_kml_gpx_file_error": "Invalid file, only KML or GPX file are supported", + "invalid_import_file_error": "Invalid file, only KML, GPX or GeoTIFF file are supported", "invalid_url": "URL is not valid.", "invalid_wms_capabilities": "Invalid WMS Capabilities", "invalid_wmts_capabilities": "Invalid WMTS Capabilities", diff --git a/src/modules/i18n/locales/fr.json b/src/modules/i18n/locales/fr.json index 280a5b58e..a43bd665a 100644 --- a/src/modules/i18n/locales/fr.json +++ b/src/modules/i18n/locales/fr.json @@ -308,7 +308,7 @@ "inspire_service_link_label": "geo.admin.ch", "invalid_email": "e-mail invalide", "invalid_file": "Fichier invalide, seuls les fichiers KML ou GPX sont pris en charge", - "invalid_kml_gpx_file_error": "Fichier invalide, seuls les fichiers KML ou GPX sont pris en charge", + "invalid_import_file_error": "Fichier invalide, seuls les fichiers KML, GPX ou GeoTIFF sont pris en charge", "invalid_url": "URL non valide.", "invalid_wms_capabilities": "Données WMS Capabilities invalides", "invalid_wmts_capabilities": "Données WMTS Capabilities invalides", diff --git a/src/modules/i18n/locales/it.json b/src/modules/i18n/locales/it.json index b549d1f1b..18b3957f4 100644 --- a/src/modules/i18n/locales/it.json +++ b/src/modules/i18n/locales/it.json @@ -308,7 +308,7 @@ "inspire_service_link_label": "geo.admin.ch", "invalid_email": "e-mail non valido", "invalid_file": "file non valido", - "invalid_kml_gpx_file_error": "File non valido, sono supportati solo file KML o GPX", + "invalid_import_file_error": "File non valido, sono supportati solo file KML, GPX o GeoTIFF", "invalid_url": "URL non valido", "invalid_wms_capabilities": "Dati WMS Capabilities non validi", "invalid_wmts_capabilities": "Dati WMTS Capabilities non validi", diff --git a/src/modules/i18n/locales/rm.json b/src/modules/i18n/locales/rm.json index eb85360c5..cf47b395f 100644 --- a/src/modules/i18n/locales/rm.json +++ b/src/modules/i18n/locales/rm.json @@ -306,7 +306,7 @@ "inspire_service_link_label": "geo.admin.ch", "invalid_email": "ungültige E-Mail", "invalid_file": "Datotecadad nun vala", - "invalid_kml_gpx_file_error": "Datotecadad nun vala, èn ancum suttatschadas ils files KML u GPX.", + "invalid_import_file_error": "Datotecadad nun vala, èn ancum suttatschadas ils files KML, GPX u GeoTIFF.", "invalid_url": "URL è nunvalid", "invalid_wms_capabilities": "Dadis WMS Capabilitiesinvalid", "invalid_wmts_capabilities": "Dadis WMTS Capabilitiesinvalid", diff --git a/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue b/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue index 5ade51352..cc21b04fa 100644 --- a/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue +++ b/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue @@ -27,7 +27,7 @@ const source = computed(() => { if (geotiffConfig.value.isLocalFile) { return { blob: geotiffConfig.value.data } } - return { source: geotiffConfig.value.fileSource } + return { url: geotiffConfig.value.fileSource } }) const geoTIFFSource = new GeoTIFFSource({ diff --git a/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js b/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js index 63b70dd5e..bc8848e3b 100644 --- a/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js +++ b/src/modules/map/components/openlayers/utils/useMapInteractions.composable.js @@ -274,7 +274,7 @@ export default function useMapInteractions(map) { } else if (error instanceof EmptyKMLError || error instanceof EmptyGPXError) { errorKey = 'kml_gpx_file_empty' } else { - errorKey = 'invalid_kml_gpx_file_error' + errorKey = 'invalid_import_file_error' log.error(`Failed to load file`, error) } store.dispatch('addError', { error: new ErrorMessage(errorKey, null), ...dispatcher }) diff --git a/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue b/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue index a1fa6e9b6..19ba37828 100644 --- a/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue +++ b/src/modules/menu/components/advancedTools/ImportFile/ImportFileLocalTab.vue @@ -52,7 +52,7 @@ async function loadFile() { } else if (error instanceof EmptyKMLError || error instanceof EmptyGPXError) { errorFileLoadingMessage.value = 'kml_gpx_file_empty' } else { - errorFileLoadingMessage.value = 'invalid_kml_gpx_file_error' + errorFileLoadingMessage.value = 'invalid_import_file_error' log.error(`Failed to load file`, error) } } diff --git a/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue b/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue index 763ef5bcd..31ca4b117 100644 --- a/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue +++ b/src/modules/menu/components/advancedTools/ImportFile/ImportFileOnlineTab.vue @@ -102,7 +102,7 @@ async function loadFile() { errorFileLoadingMessage.value = 'kml_gpx_file_empty' } else { log.error(`Failed to parse file from url ${fileUrl.value}`, error) - errorFileLoadingMessage.value = 'invalid_kml_gpx_file_error' + errorFileLoadingMessage.value = 'invalid_import_file_error' } } loading.value = false diff --git a/src/modules/menu/components/advancedTools/ImportFile/utils.js b/src/modules/menu/components/advancedTools/ImportFile/utils.js index 69456acfb..a835cbddf 100644 --- a/src/modules/menu/components/advancedTools/ImportFile/utils.js +++ b/src/modules/menu/components/advancedTools/ImportFile/utils.js @@ -77,7 +77,7 @@ export async function handleFileContent(store, content, source, originalFile = n // we are done here, so to not run the parsed content part below, we stop the function here return layer } catch (err) { - log.debug('parsing as GeoTIFF failed', err) + log.error('parsing as GeoTIFF failed', err) throw new Error(`Could not parse GeoTIFF from ${source}`) } } else { diff --git a/src/router/storeSync/LayerParamConfig.class.js b/src/router/storeSync/LayerParamConfig.class.js index 1bced074f..1a579595a 100644 --- a/src/router/storeSync/LayerParamConfig.class.js +++ b/src/router/storeSync/LayerParamConfig.class.js @@ -2,6 +2,7 @@ import getFeature from '@/api/features/features.api' import ExternalWMSLayer from '@/api/layers/ExternalWMSLayer.class' import ExternalWMTSLayer from '@/api/layers/ExternalWMTSLayer.class' import GeoAdminWMSLayer from '@/api/layers/GeoAdminWMSLayer.class' +import GeoTIFFLayer from '@/api/layers/GeoTIFFLayer.class.js' import GPXLayer from '@/api/layers/GPXLayer.class' import KMLLayer from '@/api/layers/KMLLayer.class' import LayerTypes from '@/api/layers/LayerTypes.enum' @@ -72,6 +73,15 @@ export function createLayerObject(parsedLayer, currentLayer, store, featuresRequ } else { // we can't re-load GPX files loaded through a file import; this GPX file is ignored } + } else if (parsedLayer.type === LayerTypes.GEOTIFF) { + // format is GEOTIFF|FILE_URL + if (parsedLayer.baseUrl.startsWith('http')) { + layer = new GeoTIFFLayer({ + fileSource: parsedLayer.baseUrl, + visible: parsedLayer.visible, + opacity: parsedLayer.opacity ?? defaultOpacity, + }) + } } // format is WMTS|GET_CAPABILITIES_URL|LAYER_ID else if (parsedLayer.type === LayerTypes.WMTS) { diff --git a/src/utils/utils.js b/src/utils/utils.js index a28e7a9b2..62231c9c2 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -287,13 +287,8 @@ export function isZipContent(content) { */ export function isTiffContent(content) { // Check the first 4 bytes for the TIFF file signature - // there are two signatures that can be found here https://en.wikipedia.org/wiki/TIFF - const tiffSignatures = [ - [0x49, 0x49, 0x2a, 0x00], - [0x4d, 0x4d, 0x00, 0x2a], - ] + // signatures can be found here https://en.wikipedia.org/wiki/TIFF + const tiffLittleEndianSignature = [0x49, 0x49, 0x2a, 0x00] const contentSignature = new Uint8Array(content.slice(0, 4)) - return tiffSignatures.some((signature) => - signature.every((byte, index) => byte === contentSignature[index]) - ) + return tiffLittleEndianSignature.every((byte, index) => byte === contentSignature[index]) } From b988b95bb87aece78c5cefa6ab52963eba9779ed Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Thu, 19 Sep 2024 11:45:27 +0200 Subject: [PATCH 4/5] PB-1000 : take GeoTIFF layer opacity into account and also react to changes in the source of the GeoTIFF (if the URL is changed on-the-fly) --- .../openlayers/OpenLayersGeoTIFF.vue | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue b/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue index cc21b04fa..2bb021613 100644 --- a/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue +++ b/src/modules/map/components/openlayers/OpenLayersGeoTIFF.vue @@ -1,7 +1,7 @@