diff --git a/secrets.yml b/secrets.yml index 8686424b4..14c89cb5f 100644 --- a/secrets.yml +++ b/secrets.yml @@ -1,3 +1,3 @@ # required in order to access the translation spreadsheet (while running `npm run update:translations`) -GOOGLE_API_KEY: !var infra-gopass-bgdi/web-mapviewer/GOOGLE_API_KEY -CYPRESS_RECORD_KEY: !var infra-gopass-bgdi/web-mapviewer/cypress-key key +GOOGLE_API_KEY: !var /google.com/web-mapviewer/api-key --profile swisstopo-bgdi-builder +CYPRESS_RECORD_KEY: !var /cypress.io/ppbgdi/web-mapviewer/record-key --profile swisstopo-bgdi-builder diff --git a/src/App.vue b/src/App.vue index ce5d23350..b93773ad7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,6 +10,7 @@ import { useI18n } from 'vue-i18n' import { useStore } from 'vuex' import ErrorWindow from '@/utils/components/ErrorWindow.vue' +import WarningWindow from '@/utils/components/WarningWindow.vue' import debounce from '@/utils/debounce' const withOutline = ref(false) @@ -22,6 +23,12 @@ const dispatcher = { dispatcher: 'App.vue' } let debouncedOnResize const errorText = computed(() => store.state.ui.errorText) +const warning = computed(() => { + if (store.state.ui.warnings.size > 0) { + return store.state.ui.warnings.values().next().value + } + return null +}) onMounted(() => { // reading size @@ -58,8 +65,16 @@ function refreshPageTitle() { v-if="errorText" title="error" @close="store.dispatch('setErrorText', { errorText: null, ...dispatcher })" - >
{{ i18n.t(errorText) }}
+
{{ i18n.t(errorText) }}
+ + +
{{ i18n.t(warning.msg, warning.params) }}
+
diff --git a/src/modules/i18n/README.md b/src/modules/i18n/README.md index 66a4a027c..447169999 100644 --- a/src/modules/i18n/README.md +++ b/src/modules/i18n/README.md @@ -1,5 +1,13 @@ # Internationalization (i18n) module +- [Quick guide](#quick-guide) +- [Formatting messages](#formatting-messages) +- [update translations](#update-translations) +- [State properties](#state-properties) + - [Noteworthy mutation](#noteworthy-mutation) + +## Quick guide + Responsible for loading and serving `vue-i18n`. This utils can be accessed by linking the result of `useI18n()` to a local ref (in Composition API) or in-place with the Option API. As we've deactivated the legacy support, it's not possible to use `this.$i18n` anymore, we must now go through the `useI18n()` function to @@ -39,6 +47,12 @@ useI18n().t('a_translation_key') Or if you have multiple call to `t(...)`, you can store the reference given by `useI18n()` at some point (do not store it in `data()`) +## Formatting messages + +Some message might require some formatting from the application to do so you can use the `{placeholder}` notation in the translation key and then use `i18n.t('my_formatted_key', {placeholder: 'my placeholder'})` + +See [i18n guide](https://kazupon.github.io/vue-i18n/guide/formatting.html) + ## update translations See [the main README.md's section on that](../../../README.md#tooling-for-translation-update) diff --git a/src/modules/i18n/locales/de.json b/src/modules/i18n/locales/de.json index 99a546e70..41f954848 100644 --- a/src/modules/i18n/locales/de.json +++ b/src/modules/i18n/locales/de.json @@ -683,5 +683,8 @@ "webmapviewer_live_disclaimer": "Der Kartenviewer wird bald aktualisiert - mach dich startklar", "drawing_too_large": "Ihre Zeichnung ist zu gross, entfernen Sie einige Details.", "3d_unsupported_projection": "Dieser Datensatz (externe Quelle) kann wegen fehlender Unterstützung der Projektion EPSG:4326 nicht in 3D dargestellt werden", - "geoloc_out_of_bounds": "Ihre aktuelle Position liegt ausserhalb der Schweiz und kann deshalb nicht angezeigt werden" + "geoloc_out_of_bounds": "Ihre aktuelle Position liegt ausserhalb der Schweiz und kann deshalb nicht angezeigt werden", + "orient_map_north": "Karte einnorden", + "kml_icon_url_cors_issue": "Die KML-Datei „{layerName}“ enthält Symbol(e) mit der URL {url}, die keine CORS-Unterstützung bietet! Bitte wenden Sie sich an den Administrator dieser URL, um die Unterstützung für CORS hinzuzufügen.", + "warning": "Warnung" } diff --git a/src/modules/i18n/locales/en.json b/src/modules/i18n/locales/en.json index ee279113b..ff0c110b5 100644 --- a/src/modules/i18n/locales/en.json +++ b/src/modules/i18n/locales/en.json @@ -683,5 +683,8 @@ "webmapviewer_live_disclaimer": "The new map viewer is coming soon - get ready", "drawing_too_large": "Your drawing is too large, remove some features", "3d_unsupported_projection": "This map provided by external source can't be displayed in 3d because it doesn't support the projection EPSG:4326", - "geoloc_out_of_bounds": "Your current location is outside of Switzerland and cannot be shown" + "geoloc_out_of_bounds": "Your current location is outside of Switzerland and cannot be shown", + "orient_map_north": "Orient the map", + "kml_icon_url_cors_issue": "The KML \"{layerName}\" contains icon(s) with URL {url} that doesn't support CORS! Please contact the administrator of this URL to add support for CORS.", + "warning": "Warning" } diff --git a/src/modules/i18n/locales/fr.json b/src/modules/i18n/locales/fr.json index bbea285df..be6123f3b 100644 --- a/src/modules/i18n/locales/fr.json +++ b/src/modules/i18n/locales/fr.json @@ -683,5 +683,8 @@ "webmapviewer_live_disclaimer": "Le visualiseur de cartes sera bientôt mis à jour - comment se préparer", "drawing_too_large": "Ton dessin est trop grand, enlève quelques éléments", "3d_unsupported_projection": "La carte ne peut pas être affichées en 3d parce qu'elle ne supporte pas la projection EPSG:4326", - "geoloc_out_of_bounds": "Votre emplacement actuel est en dehors de la Suisse et ne peut pas être affiché" + "geoloc_out_of_bounds": "Votre emplacement actuel est en dehors de la Suisse et ne peut pas être affiché", + "orient_map_north": "Orienter la carte", + "kml_icon_url_cors_issue": "Le KML « {layerName} » contient des icônes avec l'URL {url} qui ne supporte pas CORS ! Veuillez contacter l'administrateur de cette URL pour avoir du support de CORS.", + "warning": "Attention" } diff --git a/src/modules/i18n/locales/it.json b/src/modules/i18n/locales/it.json index 6eb0a5dbd..350c6c367 100644 --- a/src/modules/i18n/locales/it.json +++ b/src/modules/i18n/locales/it.json @@ -683,5 +683,8 @@ "webmapviewer_live_disclaimer": "Il visualizzatore di mappe sarà aggiornato in breve - tenetevi pronti", "drawing_too_large": "Il suo disegno è troppo grande, rimuova alcuni elementi", "3d_unsupported_projection": "La mappa non può essere visualizzata in 3D perché non supporta la proiezione EPSG:4326", - "geoloc_out_of_bounds": "La Sua posizione attuale è al di fuori della Svizzera e non può essere mostrata." + "geoloc_out_of_bounds": "La Sua posizione attuale è al di fuori della Svizzera e non può essere mostrata.", + "orient_map_north": "Orientare la mappa", + "kml_icon_url_cors_issue": "Il KML “{layerName}” contiene icone con l'URL {url} che non supporta CORS! Contattare l'amministratore di questo URL per aggiungere il supporto per CORS.", + "warning": "Avvertenza" } diff --git a/src/modules/i18n/locales/rm.json b/src/modules/i18n/locales/rm.json index 63c155d66..00d8cee24 100644 --- a/src/modules/i18n/locales/rm.json +++ b/src/modules/i18n/locales/rm.json @@ -681,5 +681,8 @@ "webmapviewer_live_disclaimer": "Il visualisader da chartas vegn actualisà proximamain - stai pronts", "drawing_too_large": "Tes dissegn è memia grond, stizza intgins detagls", "3d_unsupported_projection": "La carta na po betg vegnir mussada en 3D perquai ch'ella na sustegna betg la projecziun EPSG:4326", - "geoloc_out_of_bounds": "Voss lieu actual sa chatta ordaifer la Svizra e na po betg vegnir mussà" + "geoloc_out_of_bounds": "Voss lieu actual sa chatta ordaifer la Svizra e na po betg vegnir mussà", + "orient_map_north": "Orientar la carta", + "kml_icon_url_cors_issue": "Il KML \"{layerName}\" cuntegna icona(s) cun URL {url} che na po nagin sustegnair CORS! As drizzai p.pl. a l'administratura da quella URL per agiuntar il sustegn dal CORS.", + "warning": "Avertiment" } diff --git a/src/modules/map/components/openlayers/OpenLayersKMLLayer.vue b/src/modules/map/components/openlayers/OpenLayersKMLLayer.vue index 56cd231ef..2694e9814 100644 --- a/src/modules/map/components/openlayers/OpenLayersKMLLayer.vue +++ b/src/modules/map/components/openlayers/OpenLayersKMLLayer.vue @@ -9,8 +9,9 @@ import { useStore } from 'vuex' import KMLLayer from '@/api/layers/KMLLayer.class' import { IS_TESTING_WITH_CYPRESS } from '@/config' import useAddLayerToMap from '@/modules/map/components/openlayers/utils/useAddLayerToMap.composable' -import { parseKml } from '@/utils/kmlUtils' +import { iconUrlProxyFy, parseKml } from '@/utils/kmlUtils' import log from '@/utils/logging' +import WarningMessage from '@/utils/WarningMessage.class' const dispatcher = { dispatcher: 'OpenLayersKMLLayer.vue' } @@ -39,6 +40,7 @@ const iconsArePresent = computed(() => availableIconSets.value.length > 0) // extracting useful info from what we've linked so far const layerId = computed(() => kmlLayerConfig.value.id) +const layerName = computed(() => kmlLayerConfig.value.name) const opacity = computed(() => parentLayerOpacity.value ?? kmlLayerConfig.value.opacity) const url = computed(() => kmlLayerConfig.value.baseUrl) const kmlData = computed(() => kmlLayerConfig.value.kmlData) @@ -81,6 +83,18 @@ onUnmounted(() => { } }) +function iconUrlProxy(url) { + return iconUrlProxyFy(url, (url) => { + store.dispatch('addWarning', { + warning: new WarningMessage('kml_icon_url_cors_issue', { + layerName: layerName.value, + url: url, + }), + dispatcher: 'kmlUtils.js', + }) + }) +} + function createSourceForProjection() { if (!kmlData.value) { log.debug('no KML data loaded yet, could not create source') @@ -94,7 +108,12 @@ function createSourceForProjection() { new VectorSource({ wrapX: true, projection: projection.value.epsg, - features: parseKml(kmlLayerConfig.value, projection.value, availableIconSets.value), + features: parseKml( + kmlLayerConfig.value, + projection.value, + availableIconSets.value, + iconUrlProxy + ), }) ) log.debug('Openlayer KML layer source created') diff --git a/src/store/modules/ui.store.js b/src/store/modules/ui.store.js index 5778993fd..4941ba8fd 100644 --- a/src/store/modules/ui.store.js +++ b/src/store/modules/ui.store.js @@ -7,6 +7,7 @@ import { WARNING_RIBBON_HOSTNAMES, } from '@/config' import log from '@/utils/logging' +import WarningMessage from '@/utils/WarningMessage.class' const MAP_LOADING_BAR_REQUESTER = 'app-map-loading' @@ -161,6 +162,13 @@ export default { */ errorText: null, + /** + * Set of warnings to display. Each warning must be an object WarningMessage + * + * @type Set(WarningMessage) + */ + warnings: new Set(), + /** * Flag telling if the "Drop file here" overlay will be displayed on top of the map. * @@ -387,6 +395,26 @@ export default { setErrorText({ commit }, { errorText, dispatcher }) { commit('setErrorText', { errorText, dispatcher }) }, + addWarning({ commit, state }, { warning, dispatcher }) { + if (!(warning instanceof WarningMessage)) { + throw new Error( + `Warning ${warning} dispatched by ${dispatcher} is not of type WarningMessage` + ) + } + if (!state.warnings.has(warning)) { + commit('addWarning', { warning, dispatcher }) + } + }, + removeWarning({ commit, state }, { warning, dispatcher }) { + if (!(warning instanceof WarningMessage)) { + throw new Error( + `Warning ${warning} dispatched by ${dispatcher} is not of type WarningMessage` + ) + } + if (state.warnings.has(warning)) { + commit('removeWarning', { warning, dispatcher }) + } + }, setShowDragAndDropOverlay({ commit }, { showDragAndDropOverlay, dispatcher }) { commit('setShowDragAndDropOverlay', { showDragAndDropOverlay, dispatcher }) }, @@ -450,6 +478,8 @@ export default { }, setShowDisclaimer: (state, { showDisclaimer }) => (state.showDisclaimer = showDisclaimer), setErrorText: (state, { errorText }) => (state.errorText = errorText), + addWarning: (state, { warning }) => state.warnings.add(warning), + removeWarning: (state, { warning }) => state.warnings.delete(warning), setShowDragAndDropOverlay: (state, { showDragAndDropOverlay }) => (state.showDragAndDropOverlay = showDragAndDropOverlay), }, diff --git a/src/utils/WarningMessage.class.js b/src/utils/WarningMessage.class.js new file mode 100644 index 000000000..91690bcba --- /dev/null +++ b/src/utils/WarningMessage.class.js @@ -0,0 +1,11 @@ +/** Warning message to display to the user */ +export default class WarningMessage { + /** + * @param {string} msg Translation key message + * @param {any} params Translation params to pass to i18n (used for message formatting) + */ + constructor(msg, params = null) { + this.msg = msg + this.params = params + } +} diff --git a/src/utils/components/ErrorWindow.vue b/src/utils/components/ErrorWindow.vue index 159116d17..27f4d3d93 100644 --- a/src/utils/components/ErrorWindow.vue +++ b/src/utils/components/ErrorWindow.vue @@ -16,11 +16,6 @@ const props = defineProps({ type: Boolean, default: false, }, - /** Add a minimize button in header that will hide/show the body */ - hasMinimize: { - type: Boolean, - default: true, - }, }) const { title, hide } = toRefs(props) @@ -70,6 +65,7 @@ const emit = defineEmits(['close']) diff --git a/src/utils/kmlUtils.js b/src/utils/kmlUtils.js index 0b1e9dcca..ec3b71425 100644 --- a/src/utils/kmlUtils.js +++ b/src/utils/kmlUtils.js @@ -1,3 +1,4 @@ +import axios from 'axios' import { createEmpty as emptyExtent, extend as extendExtent, @@ -10,6 +11,7 @@ import Style from 'ol/style/Style' import EditableFeature, { EditableFeatureTypes } from '@/api/features/EditableFeature.class' import { extractOlFeatureCoordinates } from '@/api/features/features.api' +import { proxifyUrl } from '@/api/file-proxy.api' import { DEFAULT_TITLE_OFFSET, DrawingIcon } from '@/api/icon.api' import { WGS84 } from '@/utils/coordinates/coordinateSystems' import { @@ -46,7 +48,7 @@ export const LEGACY_ICON_XML_SCALE_FACTOR = 1.5 * @returns {string} Return KML name */ export function parseKmlName(content) { - const kml = new KML({ extractStyles: false }) + const kml = new KML({ extractStyles: false, iconUrlFunction: iconUrlProxyFy }) return kml.readName(content) } @@ -58,7 +60,7 @@ export function parseKmlName(content) { * @returns {ol/extent|null} KML layer extent in WGS84 projection or null if the KML has no features */ export function getKmlExtent(content) { - const kml = new KML({ extractStyles: false }) + const kml = new KML({ extractStyles: false, iconUrlFunction: iconUrlProxyFy }) const features = kml.readFeatures(content, { dataProjection: WGS84.epsg, // KML files should always be in WGS84 featureProjection: WGS84.epsg, @@ -449,6 +451,39 @@ export function getEditableFeatureFromKmlFeature(kmlFeature, kmlLayer, available }) } +const nonGeoadminIconUrls = new Set() +export function iconUrlProxyFy(url, corsIssueCallback = null) { + // We only proxyfy URL that are not from our backend. + if (!/^(https:\/\/[^/]*(bgdi\.ch|geo\.admin\.ch)|http:\/\/localhost)/.test(url)) { + const proxyUrl = proxifyUrl(url) + // Only perform the CORS check if we have a callback and it has not yet been done + if (!nonGeoadminIconUrls.has(url) && corsIssueCallback) { + nonGeoadminIconUrls.add(url) + log.warn(`Non geoadmin KML Icon url detected, checking CORS: ${url}`) + // Detected non geoadmin URL, in this case always use the proxy to avoid CORS errors as + // this method is synchrone. + // but still check for CORS support asynchronously to set a user warning if needed. + axios + .head(url, { + timeout: 10 * 1000, + }) + .then((response) => { + log.debug(`KML Icon url ${url} support CORS:`, response) + }) + .catch((error) => { + log.warn(`KML Icon url ${url} do not support CORS`, error) + if (corsIssueCallback) { + corsIssueCallback(url, error) + } + }) + } + + log.debug(`KML icon change url from ${url} to ${proxyUrl}`) + return proxyUrl + } + return url +} + /** * Parses a KML's data into OL Features * @@ -457,9 +492,9 @@ export function getEditableFeatureFromKmlFeature(kmlFeature, kmlLayer, available * @param {DrawingIconSet[]} iconSets Icon sets to use for EditabeFeature deserialization * @returns {ol/Feature[]} List of OL Features */ -export function parseKml(kmlLayer, projection, iconSets) { +export function parseKml(kmlLayer, projection, iconSets, iconUrlProxy = iconUrlProxyFy) { const kmlData = kmlLayer.kmlData - const features = new KML().readFeatures(kmlData, { + const features = new KML({ iconUrlFunction: iconUrlProxy }).readFeatures(kmlData, { dataProjection: WGS84.epsg, // KML files should always be in WGS84 featureProjection: projection.epsg, })