diff --git a/src/api/features/features.api.js b/src/api/features/features.api.js index e9d929842..380bdff97 100644 --- a/src/api/features/features.api.js +++ b/src/api/features/features.api.js @@ -15,8 +15,7 @@ import { import { getApi3BaseUrl } from '@/config/baseUrl.config' import { DEFAULT_FEATURE_COUNT_SINGLE_POINT } from '@/config/map.config' import allCoordinateSystems, { LV95 } from '@/utils/coordinates/coordinateSystems' -import { projExtent } from '@/utils/coordinates/coordinateUtils' -import { createPixelExtentAround } from '@/utils/extentUtils' +import { createPixelExtentAround, projExtent } from '@/utils/extentUtils' import { getGeoJsonFeatureCoordinates, reprojectGeoJsonData } from '@/utils/geoJsonUtils' import log from '@/utils/logging' diff --git a/src/api/file-proxy.api.js b/src/api/file-proxy.api.js index 3251674e3..d1ef9f0bb 100644 --- a/src/api/file-proxy.api.js +++ b/src/api/file-proxy.api.js @@ -1,3 +1,4 @@ +import axios from 'axios' import { isString } from 'lodash' import { getServiceProxyBaseUrl } from '@/config/baseUrl.config' @@ -68,3 +69,14 @@ export function unProxifyUrl(proxifiedUrl) { return proxifiedUrl } + +/** + * @param {String} fileUrl + * @returns {Promise} + */ +export async function getContentThroughServiceProxy(fileUrl) { + const proxifyGetResponse = await axios.get(proxifyUrl(fileUrl), { + responseType: 'arraybuffer', + }) + return proxifyGetResponse.data +} diff --git a/src/api/files.api.js b/src/api/files.api.js index c6d01c5aa..68a37281e 100644 --- a/src/api/files.api.js +++ b/src/api/files.api.js @@ -340,6 +340,7 @@ export function loadKmlData(kmlLayer) { * proxy. * * @param {string} url URL to fetch + * @param {Object} [options] * @param {Number} [options.timeout] How long should the call wait before timing out * @param {string} [options.responseType] Type of data that the server will respond with. Options * are 'arraybuffer', 'document', 'json', 'text', 'stream'. Default is `json` @@ -349,10 +350,10 @@ export async function getFileFromUrl(url, options = {}) { const { timeout = null, responseType = null } = options if (/^https?:\/\/localhost/.test(url) || isInternalUrl(url)) { // don't go through proxy if it is on localhost or the internal server - return axios.get(url, { timeout, responseType }) + return await axios.get(url, { timeout, responseType }) } else if (url.startsWith('http://')) { // HTTP request goes through the proxy - return axios.get(proxifyUrl(url), { timeout, responseType }) + return await axios.get(proxifyUrl(url), { timeout, responseType }) } // For other urls we need to check if they support CORS @@ -374,8 +375,8 @@ export async function getFileFromUrl(url, options = {}) { if (supportCORS) { // Server support CORS - return axios.get(url, { timeout, responseType }) + return await axios.get(url, { timeout, responseType }) } // server don't support CORS use proxy - return axios.get(proxifyUrl(url), { timeout, responseType }) + return await axios.get(proxifyUrl(url), { timeout, responseType }) } diff --git a/src/api/layers/GPXLayer.class.js b/src/api/layers/GPXLayer.class.js index f3217f01a..fc62bf4c8 100644 --- a/src/api/layers/GPXLayer.class.js +++ b/src/api/layers/GPXLayer.class.js @@ -15,6 +15,7 @@ export default class GPXLayer extends AbstractLayer { * @param {GPXMetadata | null} [gpxLayerData.gpxMetadata=null] Metadata of the GPX file. This * object contains all the metadata found in the file itself within the tag. * Default is `null` + * @param {[Number, Number, Number, Number] | null} gpxLayerData.extent * @throws InvalidLayerDataError if no `gpxLayerData` is given or if it is invalid */ constructor(gpxLayerData) { @@ -27,6 +28,7 @@ export default class GPXLayer extends AbstractLayer { opacity = 1.0, gpxData = null, gpxMetadata = null, + extent = null, } = gpxLayerData if (gpxFileUrl === null) { throw new InvalidLayerDataError('Missing GPX file URL', gpxLayerData) @@ -51,5 +53,6 @@ export default class GPXLayer extends AbstractLayer { this.gpxFileUrl = gpxFileUrl this.gpxData = gpxData this.gpxMetadata = gpxMetadata + this.extent = extent } } diff --git a/src/api/layers/KMLLayer.class.js b/src/api/layers/KMLLayer.class.js index 17281ef97..be2cadbf9 100644 --- a/src/api/layers/KMLLayer.class.js +++ b/src/api/layers/KMLLayer.class.js @@ -32,6 +32,7 @@ export default class KMLLayer extends AbstractLayer { * @param {Map} [kmlLayerData.linkFiles=Map()] Map of KML link files. Those * files are usually sent with the kml inside a KMZ archive and can be referenced inside the * KML (e.g. icon, image, ...). Default is `Map()` + * @param {[Number, Number, Number, Number] | null} kmlLayerData.extent * @throws InvalidLayerDataError if no `gpxLayerData` is given or if it is invalid */ constructor(kmlLayerData) { @@ -46,6 +47,7 @@ export default class KMLLayer extends AbstractLayer { kmlData = null, kmlMetadata = null, linkFiles = new Map(), + extent = null, } = kmlLayerData if (kmlFileUrl === null) { throw new InvalidLayerDataError('Missing KML file URL', kmlLayerData) @@ -77,13 +79,14 @@ export default class KMLLayer extends AbstractLayer { this.kmlMetadata = kmlMetadata if (kmlData) { - this.name = parseKmlName(kmlData) + this.name = parseKmlName(kmlData) ?? kmlFileUrl this.isLoading = false } else { this.isLoading = true } this.kmlData = kmlData this.linkFiles = linkFiles + this.extent = extent } /** diff --git a/src/modules/i18n/locales/de.json b/src/modules/i18n/locales/de.json index fd180d1b8..ae2b2986a 100644 --- a/src/modules/i18n/locales/de.json +++ b/src/modules/i18n/locales/de.json @@ -644,6 +644,7 @@ "transparency": "Transparenz", "try_test_viewer": "Probieren Sie test.map.geo.admin.ch aus", "twitter_tooltip": "Twittern Sie diese Karte", + "unknown_projection_error": "Die Datei verwendet eine ununterstützte Projektion {epsg}", "unsupported_content_type": "Nicht unterstützter Antwortinhaltstyp", "upload_failed": "Fehler beim Hochladen!", "upload_succeeded": "Upload OK!", diff --git a/src/modules/i18n/locales/en.json b/src/modules/i18n/locales/en.json index ddafa29b6..594556212 100644 --- a/src/modules/i18n/locales/en.json +++ b/src/modules/i18n/locales/en.json @@ -644,6 +644,7 @@ "transparency": "Transparency", "try_test_viewer": "Try out test.map.geo.admin.ch", "twitter_tooltip": "Tweet this map", + "unknown_projection_error": "File is using an unsupported projection {epsg}", "unsupported_content_type": "Unsupported response content type", "upload_failed": "Upload error!", "upload_succeeded": "Upload OK!", diff --git a/src/modules/i18n/locales/fr.json b/src/modules/i18n/locales/fr.json index bebca11b5..d751324e6 100644 --- a/src/modules/i18n/locales/fr.json +++ b/src/modules/i18n/locales/fr.json @@ -644,6 +644,7 @@ "transparency": "Transparence", "try_test_viewer": "Essayez test.map.geo.admin.ch", "twitter_tooltip": "Tweeter cette carte", + "unknown_projection_error": "Le fichier utilise une projection non prise en charge {epsg}", "unsupported_content_type": "Type de contenu de réponse non pris en charge", "upload_failed": "Erreur d'enregistrement!", "upload_succeeded": "Chargement OK!", diff --git a/src/modules/i18n/locales/it.json b/src/modules/i18n/locales/it.json index edc93aa61..5c4327bb9 100644 --- a/src/modules/i18n/locales/it.json +++ b/src/modules/i18n/locales/it.json @@ -644,6 +644,7 @@ "transparency": "Trasparenza", "try_test_viewer": "Prova test.map.geo.admin.ch", "twitter_tooltip": "Tweet della carta", + "unknown_projection_error": "Il file utilizza una proiezione non supportata {epsg}", "unsupported_content_type": "Tipo di contenuto della risposta non supportato", "upload_failed": "Caricamento fallito!", "upload_succeeded": "Caricamento OK!", diff --git a/src/modules/i18n/locales/rm.json b/src/modules/i18n/locales/rm.json index 740146891..019a1e735 100644 --- a/src/modules/i18n/locales/rm.json +++ b/src/modules/i18n/locales/rm.json @@ -642,6 +642,7 @@ "transparency": "Transparenza", "try_test_viewer": "Empruvai test.map.geo.admin.ch", "twitter_tooltip": "Tschivlottais questa charta", + "unknown_projection_error": "Die Datei verwendet eine ununterstützte Projektion {epsg}", "unsupported_content_type": "Tip da containt da resposta betg sustegnì", "upload_failed": "Errur da chargiar", "upload_succeeded": "Chargiar reussì", diff --git a/src/modules/map/components/footer/MapFooterAttributionList.vue b/src/modules/map/components/footer/MapFooterAttributionList.vue index 99f0a6075..ddb9ca822 100644 --- a/src/modules/map/components/footer/MapFooterAttributionList.vue +++ b/src/modules/map/components/footer/MapFooterAttributionList.vue @@ -6,7 +6,7 @@ v-if="source.hasDataDisclaimer" :source-name="source.name" :complete-disclaimer-on-click="!source.url" - :is-external-data-local="source.isExternalDataLocal" + :is-local-file="source.isLocalFile" > store.state.drawing.drawingOverlay.show) @@ -254,47 +253,6 @@ 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() - reader.onload = (event) => resolve(event.target.result) - reader.onerror = (error) => reject(error) - // The file might be a KMZ file, which is a zip archive. Reading zip archive as text - // is asking for trouble therefore we use ArrayBuffer - reader.readAsArrayBuffer(file) - }) - } - - /** - * @param {File} file - * @returns {Promise} - */ - async function handleFile(file) { - try { - const fileContent = await readFileContent(file) - await handleFileContent(store, fileContent, file.name, file) - } catch (error) { - let errorKey - log.error(`Error loading file`, file.name, error) - if (error instanceof OutOfBoundsError) { - errorKey = 'imported_file_out_of_bounds' - } else if (error instanceof EmptyKMLError || error instanceof EmptyGPXError) { - errorKey = 'kml_gpx_file_empty' - } else { - errorKey = 'invalid_import_file_error' - log.error(`Failed to load file`, error) - } - store.dispatch('addErrors', { - errors: [new ErrorMessage(errorKey, null)], - ...dispatcher, - }) - } - } - function onDragOver(event) { event.preventDefault() store.dispatch('setShowDragAndDropOverlay', { showDragAndDropOverlay: true, ...dispatcher }) @@ -308,19 +266,15 @@ export default function useMapInteractions(map) { }) if (event.dataTransfer.items) { - // Use DataTransferItemList interface to access the file(s) - for (let i = 0; i < event.dataTransfer.items.length; i++) { + for (/** @type {DataTransferItem} */ const item of event.dataTransfer.items) { // If dropped items aren't files, reject them - if (event.dataTransfer.items[i].kind === 'file') { - const file = event.dataTransfer.items[i].getAsFile() - handleFile(file) + if (item.kind === 'file') { + handleFileSource(item.getAsFile()) } } } else { - // Use DataTransfer interface to access the file(s) - for (let i = 0; i < event.dataTransfer.files.length; i++) { - const file = event.dataTransfer.files[i] - handleFile(file) + for (/** @type {File} */ const file of event.dataTransfer.files) { + handleFileSource(file) } } } diff --git a/src/modules/menu/components/activeLayers/MenuActiveLayersListItem.vue b/src/modules/menu/components/activeLayers/MenuActiveLayersListItem.vue index f844b682d..4e9cbb601 100644 --- a/src/modules/menu/components/activeLayers/MenuActiveLayersListItem.vue +++ b/src/modules/menu/components/activeLayers/MenuActiveLayersListItem.vue @@ -15,6 +15,7 @@ import MenuActiveLayersListItemTimeSelector from '@/modules/menu/components/acti import ErrorButton from '@/utils/components/ErrorButton.vue' import TextTruncate from '@/utils/components/TextTruncate.vue' import ThirdPartyDisclaimer from '@/utils/components/ThirdPartyDisclaimer.vue' +import ZoomToExtentButton from '@/utils/components/ZoomToExtentButton.vue' import { useTippyTooltip } from '@/utils/composables/useTippyTooltip' import debounce from '@/utils/debounce' import log from '@/utils/logging' @@ -165,6 +166,7 @@ function duplicateLayer() { @click="onToggleLayerVisibility" >{{ layer.name }} +