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,
})