Skip to content

Commit

Permalink
PB-1028 : do not load a COG while importing it, only read its metadata
Browse files Browse the repository at this point in the history
renaming the layer type from GeoTIFF (which we shouldn't support much, too big of a risk of users loading very large files and crashing the app because of lack of memory) and clarifying that we support COG (Cloud Optimized GeoTIFF)

Adding a dedicated parser for COG files

improvements made during this work package :
 - read the no data value from the COG metadata (instead of arbitrarily setting it to zero)
 - re-load COG extent if the layer was added at startup through a URL param
  • Loading branch information
pakb committed Oct 11, 2024
1 parent 8dec5c6 commit 03aa99b
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,41 @@ 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
* 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 {
export default class CloudOptimizedGeoTIFFLayer 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).
* @param {String} cogConfig.fileSource The URL to access the COG data.
* @param {Boolean} [cogConfig.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
* @param {Number} [cogConfig.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} [cogConfig.data=null] Data/content of the COG file, as a string.
* Default is `null`
* @param {Number | null} [cogConfig.noDataValue] Which value will be describing the absence of
* data in this COG. Will be used to create transparency whenever this value is present.
* @param {[Number, Number, Number, Number] | null} [cogConfig.extent] The extent of this COG.
* @throws InvalidLayerDataError if no `cogConfig` is given or if it is invalid
*/
constructor(geoTIFFConfig) {
if (!geoTIFFConfig) {
throw new InvalidLayerDataError('Missing GeoTIFF layer data', geoTIFFConfig)
constructor(cogConfig) {
if (!cogConfig) {
throw new InvalidLayerDataError('Missing COG layer data', cogConfig)
}
const { fileSource = null, visible = true, opacity = 1.0, data = null } = geoTIFFConfig
const {
fileSource = null,
visible = true,
opacity = 1.0,
data = null,
noDataValue = null,
extent = null,
} = cogConfig
if (fileSource === null) {
throw new InvalidLayerDataError('Missing GeoTIFF file source', geoTIFFConfig)
throw new InvalidLayerDataError('Missing COG file source', cogConfig)
}
const isLocalFile = !fileSource.startsWith('http')
const attributionName = isLocalFile ? fileSource : new URL(fileSource).hostname
Expand All @@ -41,7 +50,7 @@ export default class GeoTIFFLayer extends AbstractLayer {
super({
name: fileName,
id: fileSource,
type: LayerTypes.GEOTIFF,
type: LayerTypes.COG,
baseUrl: fileSource,
opacity: opacity ?? 1.0,
visible: visible ?? true,
Expand All @@ -52,7 +61,8 @@ export default class GeoTIFFLayer extends AbstractLayer {
})
this.isLocalFile = isLocalFile
this.fileSource = fileSource
this.isLoading = false
this.data = data
this.noDataValue = noDataValue
this.extent = extent
}
}
2 changes: 1 addition & 1 deletion src/api/layers/LayerTypes.enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ const LayerTypes = {
GPX: 'GPX',
VECTOR: 'VECTOR',
GROUP: 'GROUP',
GEOTIFF: 'GEOTIFF',
COG: 'COG',
}
export default LayerTypes
8 changes: 4 additions & 4 deletions src/modules/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "Datei hier ablegen (KML, KMZ, GPX, GeoTIFF)",
"drop_me_here": "Datei hier ablegen (KML, KMZ, GPX, COG)",
"duplicate_layer": "Karte duplizieren",
"east": "Ost",
"ech": "Geokatalog",
Expand Down Expand Up @@ -293,8 +293,8 @@
"import": "Importieren",
"import_file": "Datei importieren",
"import_file_succeeded": "Erfolg",
"import_file_tooltip": "Importieren eine externe KML, KMZ, GPX oder GeoTIFF-Datei",
"import_file_url_placeholder": "GPX KML KMZ GeoTIFF URL",
"import_file_tooltip": "Importieren eine externe KML, KMZ, GPX oder COG-Datei",
"import_file_url_placeholder": "GPX KML KMZ COG URL",
"import_kml": "KML Import",
"import_maps": "Karten importieren",
"import_maps_tooltip": "Importieren Sie externe WMTS- oder WMS-Datenquellen",
Expand All @@ -309,7 +309,7 @@
"inspire_service_link_label": "geo.admin.ch",
"invalid_email": "ungültige E-Mail",
"invalid_file": "Datei ungültig",
"invalid_import_file_error": "Ungültige Datei, nur KML, KMZ, GPX oder GeoTIFF-Dateien werden unterstützt",
"invalid_import_file_error": "Ungültige Datei, nur KML, KMZ, GPX oder COG-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",
Expand Down
8 changes: 4 additions & 4 deletions src/modules/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 file here (KML, KMZ, GPX, GeoTIFF)",
"drop_me_here": "Drop file here (KML, KMZ, GPX, COG)",
"duplicate_layer": "Duplicate map",
"east": "East",
"ech": "Geocatalog",
Expand Down Expand Up @@ -293,8 +293,8 @@
"import": "Import",
"import_file": "Import file",
"import_file_succeeded": "Success",
"import_file_tooltip": "Import an external KML, KMZ, GPX or GeoTIFF file",
"import_file_url_placeholder": "GPX KML KMZ GeoTIFF URL",
"import_file_tooltip": "Import an external KML, KMZ, GPX or COG file",
"import_file_url_placeholder": "GPX KML KMZ COG URL",
"import_kml": "KML import",
"import_maps": "Import maps",
"import_maps_tooltip": "Import external WMTS WMS sources",
Expand All @@ -309,7 +309,7 @@
"inspire_service_link_label": "geo.admin.ch",
"invalid_email": "Invalid email",
"invalid_file": "Invalid file",
"invalid_import_file_error": "Invalid file, only KML, KMZ, GPX or GeoTIFF file are supported",
"invalid_import_file_error": "Invalid file, only KML, KMZ, GPX or COG file are supported",
"invalid_url": "URL is not valid.",
"invalid_wms_capabilities": "Invalid WMS Capabilities",
"invalid_wmts_capabilities": "Invalid WMTS Capabilities",
Expand Down
8 changes: 4 additions & 4 deletions src/modules/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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époser le fichier ici (KML, KMZ, GPX, GeoTIFF)",
"drop_me_here": "Déposer le fichier ici (KML, KMZ, GPX, COG)",
"duplicate_layer": "Duplication de carte",
"east": "Est",
"ech": "Géocatalogue",
Expand Down Expand Up @@ -293,8 +293,8 @@
"import": "Importer",
"import_file": "Importer un fichier",
"import_file_succeeded": "Succès",
"import_file_tooltip": "Importer un fichier KML, KMZ, GPX ou GeoTIFF externe",
"import_file_url_placeholder": "GPX KML KMZ GeoTIFF URL",
"import_file_tooltip": "Importer un fichier KML, KMZ, GPX ou COG externe",
"import_file_url_placeholder": "GPX KML KMZ COG URL",
"import_kml": "KML import",
"import_maps": "Importer des cartes",
"import_maps_tooltip": "Importer des données WMTS WMS externes",
Expand All @@ -309,7 +309,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_import_file_error": "Fichier invalide, seuls les fichiers KML, KMZ, GPX ou GeoTIFF sont pris en charge",
"invalid_import_file_error": "Fichier invalide, seuls les fichiers KML, KMZ, GPX ou COG 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",
Expand Down
8 changes: 4 additions & 4 deletions src/modules/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "Lasciare qui il file (KML, KMZ, GPX, GeoTIFF)",
"drop_me_here": "Lasciare qui il file (KML, KMZ, GPX, COG)",
"duplicate_layer": "Mappa duplicata",
"east": "Est",
"ech": "Geocatalogo",
Expand Down Expand Up @@ -293,8 +293,8 @@
"import": "Importare",
"import_file": "Importare file",
"import_file_succeeded": "Successo",
"import_file_tooltip": "Importa un file KML, KMZ, GPX o GeoTIFF esterno",
"import_file_url_placeholder": "GPX KML KMZ GeoTIFF URL",
"import_file_tooltip": "Importa un file KML, KMZ, GPX o COG esterno",
"import_file_url_placeholder": "GPX KML KMZ COG URL",
"import_kml": "Importare KML",
"import_maps": "Importa mappe",
"import_maps_tooltip": "Importare dati WMTS WMS esterni",
Expand All @@ -309,7 +309,7 @@
"inspire_service_link_label": "geo.admin.ch",
"invalid_email": "e-mail non valido",
"invalid_file": "file non valido",
"invalid_import_file_error": "File non valido, sono supportati solo file KML, KMZ, GPX o GeoTIFF",
"invalid_import_file_error": "File non valido, sono supportati solo file KML, KMZ, GPX o COG",
"invalid_url": "URL non valido",
"invalid_wms_capabilities": "Dati WMS Capabilities non validi",
"invalid_wmts_capabilities": "Dati WMTS Capabilities non validi",
Expand Down
8 changes: 4 additions & 4 deletions src/modules/i18n/locales/rm.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "Dar giu la datoteca (KML, KMZ, GPX, GeoTIFF)",
"drop_me_here": "Dar giu la datoteca (KML, KMZ, GPX, COG)",
"duplicate_layer": "Mapa duplicada",
"east": "ost",
"ech": "Catalog da geodatas",
Expand Down Expand Up @@ -291,8 +291,8 @@
"import": "Importar",
"import_file": "Importar ina datoteca",
"import_file_succeeded": "Success",
"import_file_tooltip": "Importar ina datoteca esterna KML, KMZ, GPX u GeoTIFF",
"import_file_url_placeholder": "GPX KML KMZ GeoTIFF URL",
"import_file_tooltip": "Importar ina datoteca esterna KML, KMZ, GPX u COG",
"import_file_url_placeholder": "GPX KML KMZ COG URL",
"import_kml": "Importar KML",
"import_maps": "Importar charta",
"import_maps_tooltip": "Agiuntar in unitad da datas WMTS WMS externa",
Expand All @@ -307,7 +307,7 @@
"inspire_service_link_label": "geo.admin.ch",
"invalid_email": "ungültige E-Mail",
"invalid_file": "Datotecadad nun vala",
"invalid_import_file_error": "Datotecadad nun vala, èn ancum suttatschadas ils files KML, KMZ, GPX u GeoTIFF.",
"invalid_import_file_error": "Datotecadad nun vala, èn ancum suttatschadas ils files KML, KMZ, GPX u COG.",
"invalid_url": "URL è nunvalid",
"invalid_wms_capabilities": "Dadis WMS Capabilitiesinvalid",
"invalid_wmts_capabilities": "Dadis WMTS Capabilitiesinvalid",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import WebGLTileLayer from 'ol/layer/WebGLTile'
import GeoTIFFSource from 'ol/source/GeoTIFF'
import { computed, inject, toRefs, watch } from 'vue'
import GeoTIFFLayer from '@/api/layers/GeoTIFFLayer.class'
import CloudOptimizedGeoTIFFLayer from '@/api/layers/CloudOptimizedGeoTIFFLayer.class.js'
import useAddLayerToMap from '@/modules/map/components/openlayers/utils/useAddLayerToMap.composable'
const props = defineProps({
geotiffConfig: {
type: GeoTIFFLayer,
type: CloudOptimizedGeoTIFFLayer,
required: true,
},
parentLayerOpacity: {
Expand All @@ -23,11 +23,17 @@ const props = defineProps({
const { geotiffConfig, parentLayerOpacity, zIndex } = toRefs(props)
const olMap = inject('olMap')
const noDataValue = computed(() => geotiffConfig.value.noDataValue ?? 0)
const source = computed(() => {
const base = {
nodata: noDataValue.value,
}
if (geotiffConfig.value.isLocalFile) {
return { blob: geotiffConfig.value.data }
base.blob = geotiffConfig.value.data
} else {
base.url = geotiffConfig.value.fileSource
}
return { url: geotiffConfig.value.fileSource }
return base
})
const opacity = computed(() => parentLayerOpacity.value ?? geotiffConfig.value.opacity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { useStore } from 'vuex'
import AbstractLayer from '@/api/layers/AbstractLayer.class'
import LayerTypes from '@/api/layers/LayerTypes.enum'
import OpenLayersCOGTiffLayer from '@/modules/map/components/openlayers/OpenLayersCOGTiffLayer.vue'
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'
Expand Down Expand Up @@ -130,8 +130,8 @@ function shouldAggregateSubLayerBeVisible(subLayer) {
:parent-layer-opacity="parentLayerOpacity"
:z-index="zIndex"
/>
<OpenLayersGeoTIFF
v-if="layerConfig.type === LayerTypes.GEOTIFF"
<OpenLayersCOGTiffLayer
v-if="layerConfig.type === LayerTypes.COG"
:geotiff-config="layerConfig"
:parent-layer-opacity="parentLayerOpacity"
:z-index="zIndex"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { fromBlob, fromUrl } from 'geotiff'

import CloudOptimizedGeoTIFFLayer from '@/api/layers/CloudOptimizedGeoTIFFLayer.class'
import OutOfBoundsError from '@/modules/menu/components/advancedTools/ImportFile/parser/errors/OutOfBoundsError.error'
import UnknownProjectionError from '@/modules/menu/components/advancedTools/ImportFile/parser/errors/UnknownProjectionError.error'
import FileParser from '@/modules/menu/components/advancedTools/ImportFile/parser/FileParser.class'
import allCoordinateSystems from '@/utils/coordinates/coordinateSystems'
import { flattenExtent, getExtentIntersectionWithCurrentProjection } from '@/utils/extentUtils'

export class CloudOptimizedGeoTIFFParser extends FileParser {
constructor() {
super({
fileExtensions: ['.tif', '.tiff'],
fileContentTypes: [
'image/tiff',
'image/tiff;subtype=geotiff',
'application=geotiff',
'application=geotiff; profile=cloud-optimized',
'image/tiff; application=geotiff; profile=cloud-optimized',
],
})
}

async parseCOGLayer(fileSource, geoTIFFInstance, currentProjection) {
if (!geoTIFFInstance) {
throw false
}
const firstImage = await geoTIFFInstance.getImage()
const imageGeoKey = firstImage.getGeoKeys()?.ProjectedCSTypeGeoKey
const cogProjection = allCoordinateSystems.find(
(coordinateSystem) => coordinateSystem.epsgNumber === imageGeoKey
)
if (!cogProjection) {
throw new UnknownProjectionError(
`Unknown projection found in COG EPSG:${imageGeoKey}`,
`EPSG:${imageGeoKey}`
)
}
const cogExtent = firstImage.getBoundingBox()
const intersection = getExtentIntersectionWithCurrentProjection(
cogExtent,
cogProjection,
currentProjection
)
if (!intersection) {
throw new OutOfBoundsError(`COG is out of bounds of current projection: ${cogExtent}`)
}
return new CloudOptimizedGeoTIFFLayer({
fileSource: this.isLocalFile(fileSource) ? fileSource.name : fileSource,
visible: true,
opacity: 1.0,
data: fileSource,
noDataValue: firstImage.getGDALNoData(),
extent: flattenExtent(intersection),
})
}

async parseUrl(fileUrl, currentProjection, options) {
return this.parseCOGLayer(fileUrl, await fromUrl(fileUrl), currentProjection, options)
}

async parseLocalFile(file, currentProjection) {
return this.parseCOGLayer(file, await fromBlob(file), currentProjection)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import axios from 'axios'

import { getContentThroughServiceProxy } from '@/api/file-proxy.api.js'
import AbstractLayer from '@/api/layers/AbstractLayer.class'
import { CloudOptimizedGeoTIFFParser } from '@/modules/menu/components/advancedTools/ImportFile/parser/CloudOptimizedGeoTIFFParser.class'
import GPXParser from '@/modules/menu/components/advancedTools/ImportFile/parser/GPXParser.class'
import { KMLParser } from '@/modules/menu/components/advancedTools/ImportFile/parser/KMLParser.class'
import KMZParser from '@/modules/menu/components/advancedTools/ImportFile/parser/KMZParser.class'
import log from '@/utils/logging.js'

const allParsers = [new KMZParser(), new KMLParser(), new GPXParser()]
const allParsers = [
new CloudOptimizedGeoTIFFParser(),
new KMZParser(),
new KMLParser(),
new GPXParser(),
]

/**
* @param {Object} config
Expand Down
7 changes: 4 additions & 3 deletions src/router/storeSync/LayerParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import getFeature from '@/api/features/features.api'
import CloudOptimizedGeoTIFFLayer from '@/api/layers/CloudOptimizedGeoTIFFLayer.class.js'
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'
Expand Down Expand Up @@ -76,13 +76,14 @@ 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) {
} else if (parsedLayer.type === LayerTypes.COG) {
// format is GEOTIFF|FILE_URL
if (parsedLayer.baseUrl.startsWith('http')) {
layer = new GeoTIFFLayer({
layer = new CloudOptimizedGeoTIFFLayer({
fileSource: parsedLayer.baseUrl,
visible: parsedLayer.visible,
opacity: parsedLayer.opacity ?? defaultOpacity,
isLoading: false,
})
}
}
Expand Down
Loading

0 comments on commit 03aa99b

Please sign in to comment.