diff --git a/cypress.config.mjs b/cypress.config.mjs index 9ff4c8898..040ce93d9 100644 --- a/cypress.config.mjs +++ b/cypress.config.mjs @@ -11,6 +11,7 @@ export default defineConfig({ defaultCommandTimeout: 5000, requestTimeout: 5000, numTestsKeptInMemory: 2, + watchForFileChanges: false, // Prevent auto run on file changes retries: { runMode: 3, diff --git a/package-lock.json b/package-lock.json index 5373ecee3..2534f369f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/vue-fontawesome": "^3.0.6", "@geoblocks/cesium-compass": "^0.5.0", - "@geoblocks/mapfishprint": "^0.2.13", + "@geoblocks/mapfishprint": "^0.2.14", "@geoblocks/ol-maplibre-layer": "^0.1.3", "@ivanv/vue-collapse-transition": "^1.0.2", "@mapbox/togeojson": "^0.16.2", @@ -41,6 +41,7 @@ "hammerjs": "^2.0.8", "jquery": "^3.7.1", "liang-barsky": "^1.0.5", + "lodash": "^4.17.21", "maplibre-gl": "^4.1.2", "ol": "^9.1.0", "pako": "^2.1.0", @@ -52,7 +53,7 @@ "vue": "^3.4.21", "vue-chartjs": "^5.3.0", "vue-i18n": "^9.11.0", - "vue-router": "^4.3.0", + "vue-router": "^4.3.2", "vue3-social-sharing": "^1.0.3", "vuex": "^4.1.0" }, @@ -806,9 +807,9 @@ } }, "node_modules/@geoblocks/mapfishprint": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/@geoblocks/mapfishprint/-/mapfishprint-0.2.13.tgz", - "integrity": "sha512-y2WHuXoezDtfiVsNVb98YY/CYjOiIBGaCX7Jdx4FbVyT0yP3QKzoPupOfF+Evnu30o7u2lNNjPozF0/aQtfKbw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/@geoblocks/mapfishprint/-/mapfishprint-0.2.14.tgz", + "integrity": "sha512-WuTZcxvGsKbFIvoGZGAqFacK7PZU2mQAFd/KmdsYmjIoo9UFImktO0CJtdtyYamUXJEBY/D+oihZ4x8wd5QHFA==", "optionalDependencies": { "@geoblocks/print": "0.7.8" }, @@ -6201,8 +6202,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -9645,9 +9645,9 @@ } }, "node_modules/vue-router": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz", - "integrity": "sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz", + "integrity": "sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==", "dependencies": { "@vue/devtools-api": "^6.5.1" }, diff --git a/package.json b/package.json index 24563aa65..232377ab8 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/vue-fontawesome": "^3.0.6", "@geoblocks/cesium-compass": "^0.5.0", - "@geoblocks/mapfishprint": "^0.2.13", + "@geoblocks/mapfishprint": "^0.2.14", "@geoblocks/ol-maplibre-layer": "^0.1.3", "@ivanv/vue-collapse-transition": "^1.0.2", "@mapbox/togeojson": "^0.16.2", @@ -68,6 +68,7 @@ "hammerjs": "^2.0.8", "jquery": "^3.7.1", "liang-barsky": "^1.0.5", + "lodash": "^4.17.21", "maplibre-gl": "^4.1.2", "ol": "^9.1.0", "pako": "^2.1.0", @@ -79,7 +80,7 @@ "vue": "^3.4.21", "vue-chartjs": "^5.3.0", "vue-i18n": "^9.11.0", - "vue-router": "^4.3.0", + "vue-router": "^4.3.2", "vue3-social-sharing": "^1.0.3", "vuex": "^4.1.0" }, diff --git a/src/api/layers/AbstractLayer.class.js b/src/api/layers/AbstractLayer.class.js index 395dabd0c..0f2eb8be0 100644 --- a/src/api/layers/AbstractLayer.class.js +++ b/src/api/layers/AbstractLayer.class.js @@ -69,6 +69,8 @@ export default class AbstractLayer { * is from another (external) source. Default is `false` * @param {boolean} [layerData.isLoading=false] Set to true if some parts of the layer (e.g. * metadata) are still loading. Default is `false` + * @param {LayerTimeConfig | null} [layerData.timeConfig=null] Time series config (if + * available). Default is `null` * @throws InvalidLayerDataError if no `layerData` is given, or if `layerData.name` or * `layerData.type` or `layer.baseUrl` aren't valid */ @@ -90,6 +92,7 @@ export default class AbstractLayer { hasLegend = false, isExternal = false, isLoading = false, + timeConfig = null, } = layerData if (name === null) { throw new InvalidLayerDataError('Missing layer name', layerData) @@ -120,6 +123,8 @@ export default class AbstractLayer { this.hasLegend = hasLegend this.errorKey = null this.hasError = false + this.timeConfig = timeConfig + this.hasMultipleTimestamps = this.timeConfig?.timeEntries?.length > 1 } clone() { diff --git a/src/api/layers/ExternalLayer.class.js b/src/api/layers/ExternalLayer.class.js index 7ae465737..3543ce7f4 100644 --- a/src/api/layers/ExternalLayer.class.js +++ b/src/api/layers/ExternalLayer.class.js @@ -94,6 +94,11 @@ export default class ExternalLayer extends AbstractLayer { * @param {ExternalLayerGetFeatureInfoCapability | null} [externalLayerData.getFeatureInfoCapability=null] * Configuration describing how to request this layer's server to get feature information. * Default is `null` + * @param {LayerTimeConfig | null} [externalLayerData.timeConfig=null] Time series config (if + * available). Default is `null` + * @param {Number} [externalWmtsData.currentYear=null] Current year of the time series config to + * use. This parameter is needed as it is set in the URL while the timeConfig parameter is not + * yet available and parse later on from the GetCapabilities. Default is `null` * @throws InvalidLayerDataError if no `externalLayerData` is given or if it is invalid */ constructor(externalLayerData) { @@ -115,6 +120,8 @@ export default class ExternalLayer extends AbstractLayer { availableProjections = [], hasTooltip = false, getFeatureInfoCapability = null, + timeConfig = null, + currentYear = null, } = externalLayerData // keeping this checks, even though it will be checked again by the super constructor, because we use the baseUrl // to build our call to the super constructor (with a URL construction, which could raise an error if baseUrl is @@ -135,6 +142,7 @@ export default class ExternalLayer extends AbstractLayer { isLoading, hasDescription: abstract?.length > 0 || legends?.length > 0, hasLegend: legends?.length > 0, + timeConfig, }) this.abstract = abstract this.extent = extent @@ -148,5 +156,9 @@ export default class ExternalLayer extends AbstractLayer { this.availableProjections.push(WGS84) } this.getFeatureInfoCapability = getFeatureInfoCapability + this.currentYear = currentYear + if (currentYear && this.timeConfig) { + this.timeConfig.updateCurrentTimeEntry(this.timeConfig.getTimeEntryForYear(currentYear)) + } } } diff --git a/src/api/layers/ExternalWMTSLayer.class.js b/src/api/layers/ExternalWMTSLayer.class.js index e1ff75171..8e60fab44 100644 --- a/src/api/layers/ExternalWMTSLayer.class.js +++ b/src/api/layers/ExternalWMTSLayer.class.js @@ -2,6 +2,57 @@ import ExternalLayer from '@/api/layers/ExternalLayer.class' import { InvalidLayerDataError } from '@/api/layers/InvalidLayerData.error' import LayerTypes from '@/api/layers/LayerTypes.enum' +/** + * @readonly + * @enum {String} + */ +export const WMTSEncodingTypes = { + KVP: 'KVP', + REST: 'REST', +} + +/** + * WMTS TileMatrixSet + * + * @class + */ +export class TileMatrixSet { + /** + * @param {String} id Identifier of the tile matrix set (see WMTS OGC spec) + * @param {CoordinateSystem} projection Coordinate system supported by the Tile Matrix Set + * @param {any} tileMatrix TileMatrix from GetCapabilities (see WMTS OGC spec) + */ + constructor(id, projection, tileMatrix) { + this.id = id + this.projection = projection + this.tileMatrix = tileMatrix + } +} + +/** + * A WMTS Layer dimension + * + * See WMTS OGC Spec + * + * @class + */ +export class WMTSDimension { + /** + * @param {String} id Dimension identifier + * @param {String} dft Dimension default value + * @param {[String]} values All dimension values + * @param {Boolean} [optionals.current] Boolean flag if the dimension support current (see WMTS + * OGC spec) + */ + constructor(id, dft, values, optionals = {}) { + const { current = false } = optionals + this.id = id + this.default = dft + this.values = values + this.current = current + } +} + /** * Metadata for an external WMTS layer, that will be defined through a GetCapabilities.xml endpoint * (and a layer ID) @@ -37,6 +88,20 @@ export default class ExternalWMTSLayer extends ExternalLayer { * @param {CoordinateSystem[]} [externalWmtsData.availableProjections=[]] All projection that * can be used to request this layer. Default is `[]` * @param {ol/WMTS/Options} [externalWmtsData.options] WMTS Get Capabilities options + * @param {String} [externalWmtsData.getTileEncoding=REST] WMTS Get Tile encoding (KVP or REST). + * Default is `REST` + * @param {String | null} [externalWmtsData.urlTemplate=''] WMTS Get Tile url template for REST + * encoding. Default is `''` + * @param {String} [externalWmtsData.style='default'] WMTS layer style. Default is `'default'` + * @param {[TileMatrixSet]} [externalWmtsData.tileMatrixSets=[]] WMTS tile matrix sets + * identifiers. Default is `[]` + * @param {[WMTSDimension]} [externalWmtsData.dimensions=[]] WMTS tile dimensions. Default is + * `[]` + * @param {LayerTimeConfig | null} [externalLayerData.timeConfig=null] Time series config (if + * available). Default is `null` + * @param {Number} [externalWmtsData.currentYear=null] Current year of the time series config to + * use. This parameter is needed as it is set in the URL while the timeConfig parameter is not + * yet available and parse later on from the GetCapabilities. Default is `null` * @throws InvalidLayerDataError if no `externalWmtsData` is given or if it is invalid */ constructor(externalWmtsData) { @@ -56,6 +121,13 @@ export default class ExternalWMTSLayer extends ExternalLayer { isLoading = true, availableProjections = [], options = null, + getTileEncoding = WMTSEncodingTypes.REST, + urlTemplate = '', + style = 'default', + tileMatrixSets = [], + dimensions = [], + timeConfig = null, + currentYear = null, } = externalWmtsData super({ name, @@ -71,7 +143,14 @@ export default class ExternalWMTSLayer extends ExternalLayer { legends, isLoading, availableProjections, + timeConfig, + currentYear, }) this.options = options + this.getTileEncoding = getTileEncoding + this.urlTemplate = urlTemplate + this.style = style + this.tileMatrixSets = tileMatrixSets + this.dimensions = dimensions } } diff --git a/src/api/layers/GeoAdminLayer.class.js b/src/api/layers/GeoAdminLayer.class.js index 2685d076a..f8961e408 100644 --- a/src/api/layers/GeoAdminLayer.class.js +++ b/src/api/layers/GeoAdminLayer.class.js @@ -113,14 +113,13 @@ export default class GeoAdminLayer extends AbstractLayer { isLoading, hasDescription, hasLegend, + timeConfig, }) this.technicalName = technicalName this.isBackground = isBackground this.isHighlightable = isHighlightable this.topics = topics this.isSpecificFor3D = id.toLowerCase().endsWith('_3d') - this.timeConfig = timeConfig - this.hasMultipleTimestamps = this.timeConfig?.timeEntries?.length > 1 this.searchable = searchable } diff --git a/src/api/layers/LayerTimeConfig.class.js b/src/api/layers/LayerTimeConfig.class.js index 7c6d61122..66e3aeff2 100644 --- a/src/api/layers/LayerTimeConfig.class.js +++ b/src/api/layers/LayerTimeConfig.class.js @@ -98,7 +98,7 @@ export default class LayerTimeConfig { * @returns {LayerTimeConfigEntry | null} */ getTimeEntryForYear(year) { - return this.timeEntries.find((entry) => entry.year === year) + return this.timeEntries.find((entry) => entry.year === year) ?? null } /** @@ -106,7 +106,7 @@ export default class LayerTimeConfig { * @returns {LayerTimeConfigEntry | null} */ getTimeEntryForTimestamp(timestamp) { - return this.timeEntries.find((entry) => entry.timestamp === timestamp) + return this.timeEntries.find((entry) => entry.timestamp === timestamp) ?? null } clone() { diff --git a/src/api/layers/LayerTimeConfigEntry.class.js b/src/api/layers/LayerTimeConfigEntry.class.js index 8bf6513b4..8b439c9aa 100644 --- a/src/api/layers/LayerTimeConfigEntry.class.js +++ b/src/api/layers/LayerTimeConfigEntry.class.js @@ -1,3 +1,5 @@ +import { isTimestampYYYYMMDD } from '@/utils/numberUtils' + /** * Year we are using to describe the timestamp "all data" for WMS (and also for WMTS as there is no * equivalent for that in the norm) @@ -34,7 +36,7 @@ export const CURRENT_YEAR_WMTS_TIMESTAMP = 'current' * to Vue reactivity engine. */ export default class LayerTimeConfigEntry { - /** @param {String} timestamp A full timestamp as YYYYYMMDD */ + /** @param {String} timestamp A full timestamp as YYYYYMMDD or ISO 8601 format */ constructor(timestamp) { this.timestamp = timestamp if ( @@ -43,7 +45,16 @@ export default class LayerTimeConfigEntry { ) { this.year = YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA } else { - this.year = parseInt(timestamp.substring(0, 4)) + if (isTimestampYYYYMMDD(timestamp)) { + this.year = parseInt(timestamp.substring(0, 4)) + } else { + const date = new Date(timestamp) + if (!isNaN(date)) { + this.year = date.getFullYear() + } else { + this.year = null + } + } } } } diff --git a/src/api/layers/WMSCapabilitiesParser.class.js b/src/api/layers/WMSCapabilitiesParser.class.js index fa7eb8bf1..eec31ac0e 100644 --- a/src/api/layers/WMSCapabilitiesParser.class.js +++ b/src/api/layers/WMSCapabilitiesParser.class.js @@ -105,13 +105,23 @@ export default class WMSCapabilitiesParser { * * @param {string} layerId Layer ID of the layer to retrieve * @param {CoordinateSystem} projection Projection currently used by the application - * @param {number} opacity - * @param {boolean} visible - * @param {boolean} ignoreError Don't throw exception in case of error, but return a default - * value or null + * @param {number} [opacity=1] Default is `1` + * @param {boolean} [visible=true] Default is `true` + * @param {Number | null} [currentYear=null] Current year to select for the time config. Only + * needed when a time config is present a year is pre-selected in the url parameter. Default + * is `null` + * @param {boolean} [ignoreError=true] Don't throw exception in case of error, but return a + * default value or null. Default is `true` * @returns {ExternalWMSLayer | null} ExternalWMSLayer object or nul in case of error */ - getExternalLayerObject(layerId, projection, opacity = 1, visible = true, ignoreError = true) { + getExternalLayerObject( + layerId, + projection, + opacity = 1, + visible = true, + currentYear = null, + ignoreError = true + ) { const { layer, parents } = this.findLayer(layerId) if (!layer) { const msg = `No WMS layer ${layerId} found in Capabilities ${this.originUrl.toString()}` @@ -127,6 +137,7 @@ export default class WMSCapabilitiesParser { projection, opacity, visible, + currentYear, ignoreError ) } @@ -155,7 +166,15 @@ export default class WMSCapabilitiesParser { ).filter((layer) => !!layer) } - _getExternalLayerObject(layer, parents, projection, opacity, visible, ignoreError) { + _getExternalLayerObject( + layer, + parents, + projection, + opacity, + visible, + currentYear, + ignoreError + ) { const { layerId, title, @@ -200,6 +219,7 @@ export default class WMSCapabilitiesParser { isLoading: false, availableProjections, getFeatureInfoCapability: this.getFeatureInfoCapability(ignoreError), + currentYear, }) } return new ExternalWMSLayer({ @@ -218,6 +238,7 @@ export default class WMSCapabilitiesParser { availableProjections, hasTooltip: queryable, getFeatureInfoCapability: this.getFeatureInfoCapability(ignoreError), + currentYear, }) } diff --git a/src/api/layers/WMTSCapabilitiesParser.class.js b/src/api/layers/WMTSCapabilitiesParser.class.js index 888f02321..6ee18769c 100644 --- a/src/api/layers/WMTSCapabilitiesParser.class.js +++ b/src/api/layers/WMTSCapabilitiesParser.class.js @@ -4,8 +4,14 @@ import proj4 from 'proj4' import { LayerAttribution } from '@/api/layers/AbstractLayer.class' import { LayerLegend } from '@/api/layers/ExternalLayer.class' -import ExternalWMTSLayer from '@/api/layers/ExternalWMTSLayer.class' +import ExternalWMTSLayer, { + TileMatrixSet, + WMTSDimension, + WMTSEncodingTypes, +} from '@/api/layers/ExternalWMTSLayer.class' import { CapabilitiesError } from '@/api/layers/layers-external.api' +import LayerTimeConfig from '@/api/layers/LayerTimeConfig.class' +import LayerTimeConfigEntry from '@/api/layers/LayerTimeConfigEntry.class' import allCoordinateSystems, { WGS84 } from '@/utils/coordinates/coordinateSystems' import log from '@/utils/logging' @@ -61,13 +67,23 @@ export default class WMTSCapabilitiesParser { * * @param {string} layerId WMTS Capabilities layer ID to retrieve * @param {CoordinateSystem} projection Projection currently used by the application - * @param {number} opacity - * @param {boolean} visible - * @param {boolean} ignoreError Don't throw exception in case of error, but return a default - * value or null + * @param {number} [opacity=1] Default is `1` + * @param {boolean} [visible=true] Default is `true` + * @param {Number | null} [currentYear=null] Current year to select for the time config. Only + * needed when a time config is present a year is pre-selected in the url parameter. Default + * is `null` + * @param {boolean} [ignoreError=true] Don't throw exception in case of error, but return a + * default value or null. Default is `true` * @returns {ExternalWMTSLayer | null} ExternalWMTSLayer object */ - getExternalLayerObject(layerId, projection, opacity = 1, visible = true, ignoreError = true) { + getExternalLayerObject( + layerId, + projection, + opacity = 1, + visible = true, + currentYear = null, + ignoreError = true + ) { const layer = this.findLayer(layerId) if (!layer) { const msg = `No WMTS layer ${layerId} found in Capabilities ${this.originUrl.toString()}` @@ -77,7 +93,14 @@ export default class WMTSCapabilitiesParser { } return null } - return this._getExternalLayerObject(layer, projection, opacity, visible, ignoreError) + return this._getExternalLayerObject( + layer, + projection, + opacity, + visible, + currentYear, + ignoreError + ) } /** @@ -86,17 +109,33 @@ export default class WMTSCapabilitiesParser { * @param {CoordinateSystem} projection Projection currently used by the application * @param {number} opacity * @param {boolean} visible + * @param {Number | null} [currentYear=null] Current year to select for the time config. Only + * needed when a time config is present a year is pre-selected in the url parameter. Default + * is `null` * @param {boolean} ignoreError Don't throw exception in case of error, but return a default * value or null * @returns {[ExternalWMTSLayer]} List of ExternalWMTSLayer objects */ - getAllExternalLayerObjects(projection, opacity = 1, visible = true, ignoreError = true) { + getAllExternalLayerObjects( + projection, + opacity = 1, + visible = true, + currentYear = null, + ignoreError = true + ) { return this.Contents.Layer.map((layer) => - this._getExternalLayerObject(layer, projection, opacity, visible, ignoreError) + this._getExternalLayerObject( + layer, + projection, + opacity, + visible, + currentYear, + ignoreError + ) ).filter((layer) => !!layer) } - _getExternalLayerObject(layer, projection, opacity, visible, ignoreError) { + _getExternalLayerObject(layer, projection, opacity, visible, currentYear, ignoreError) { const attributes = this._getLayerAttributes(layer, projection, ignoreError) if (!attributes) { @@ -121,6 +160,12 @@ export default class WMTSCapabilitiesParser { isLoading: false, availableProjections: attributes.availableProjections, options, + getTileEncoding: attributes.getTileEncoding, + urlTemplate: attributes.urlTemplate, + tileMatrixSets: attributes.tileMatrixSets, + dimensions: attributes.dimensions, + timeConfig: this._getTimeConfig(attributes.layerId, attributes.dimensions), + currentYear, }) } @@ -154,6 +199,11 @@ export default class WMTSCapabilitiesParser { extent: this._getLayerExtent(layerId, layer, projection), legends: this._getLegends(layerId, layer), availableProjections: this._getAvailableProjections(layerId, layer, ignoreError), + getTileEncoding: this._getTileEncoding(), + urlTemplate: this._getUrlTemplate(layerId, layer), + style: this._getStyle(layerId, layer), + tileMatrixSets: this._getTileMatrixSets(layerId, layer), + dimensions: this._getDimensions(layerId, layer), } } @@ -184,9 +234,17 @@ export default class WMTSCapabilitiesParser { ) // Take the available projections from the tile matrix set - availableProjections.push( - parseCrs(this._findTileMatrixSetFromLinks(layer.TileMatrixSetLink)?.SupportedCRS) - ) + const tileMatrixSetCrs = this._findTileMatrixSetFromLinks( + layer.TileMatrixSetLink + )?.SupportedCRS + if (tileMatrixSetCrs) { + const tileMatrixSetProjection = parseCrs(tileMatrixSetCrs) + if (!tileMatrixSetProjection) { + log.warn(`CRS ${tileMatrixSetCrs} no supported by application or invalid`) + } else { + availableProjections.push(tileMatrixSetProjection) + } + } // Remove duplicates availableProjections = [...new Set(availableProjections)] @@ -295,4 +353,53 @@ export default class WMTSCapabilitiesParser { ) .flat() } + + _getTileEncoding() { + return ( + this.OperationsMetadata?.GetTile?.DCP?.HTTP?.Get[0]?.Constraint[0]?.AllowedValues + ?.Value[0] ?? WMTSEncodingTypes.REST + ) + } + + _getUrlTemplate(layerId, layer) { + return layer.ResourceURL[0]?.template ?? '' + } + + _getStyle(layerId, layer) { + // Based on the spec at least one style should be available + return layer.Style[0].Identifier + } + + _getTileMatrixSets(layerId, layer) { + // Based on the spec at least one TileMatrixSetLink should be available + const ids = layer.TileMatrixSetLink.map((link) => link.TileMatrixSet) + return this.Contents.TileMatrixSet.filter((set) => ids.includes(set.Identifier)) + .map((set) => { + const projection = parseCrs(set.SupportedCRS) + if (!projection) { + log.warn( + `Invalid or non supported CRS ${set.SupportedCRS} in TileMatrixSet ${set.Identifier} for layer ${layerId}}` + ) + return null + } + return new TileMatrixSet(set.Identifier, projection, set.TileMatrix) + }) + .filter((set) => !!set) + } + + _getDimensions(layerId, layer) { + return ( + layer.Dimension?.map((d) => new WMTSDimension(d.Identifier, d.Default, d.Value)) ?? [] + ) + } + + _getTimeConfig(layerId, dimensions) { + const timeDimension = dimensions.find((d) => d.id.toLowerCase() === 'time') + if (!timeDimension) { + return null + } + const timeEntries = + timeDimension.values?.map((value) => new LayerTimeConfigEntry(value)) ?? [] + return new LayerTimeConfig(timeDimension.default ?? null, timeEntries) + } } diff --git a/src/api/layers/__tests__/WMTSCapabitliesParser.class.spec.js b/src/api/layers/__tests__/WMTSCapabitliesParser.class.spec.js index 3432e0f22..6ebc00624 100644 --- a/src/api/layers/__tests__/WMTSCapabitliesParser.class.spec.js +++ b/src/api/layers/__tests__/WMTSCapabitliesParser.class.spec.js @@ -219,4 +219,75 @@ describe('WMTSCapabilitiesParser of wmts-ogc-sample.xml', () => { ) expect(layer.legends[0].format).toBe('image/png') }) + it('Parse layer time dimension in format YYYYMMDD', () => { + let layer = capabilities.getExternalLayerObject('BlueMarbleSecondGenerationAG', WGS84) + expect(layer.id).toBe('BlueMarbleSecondGenerationAG') + expect(layer.timeConfig).to.be.not.null.and.not.undefined + expect(layer.timeConfig.hasTimestamp('20110805')).to.be.true + expect(layer.timeConfig.hasTimestamp('20081024')).to.be.true + expect(layer.timeConfig.hasTimestamp('20081023')).to.be.false + + expect(layer.timeConfig.getTimeEntryForYear(2008)).to.be.not.null.and.not.undefined + expect(layer.timeConfig.getTimeEntryForYear(2005)).to.be.null + + expect(layer.timeConfig.currentTimeEntry).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimeEntry.timestamp).toBe('20110805') + expect(layer.timeConfig.currentTimeEntry.year).toBe(2011) + expect(layer.timeConfig.currentYear).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentYear).toBe(2011) + expect(layer.timeConfig.currentTimestamp).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimestamp).toBe('20110805') + }) + it('Parse layer time dimension in format ISO format YYYY-MM-DD', () => { + let layer = capabilities.getExternalLayerObject('BlueMarbleThirdGenerationZH', WGS84) + expect(layer.id).toBe('BlueMarbleThirdGenerationZH') + expect(layer.timeConfig).to.be.not.null.and.not.undefined + expect(layer.timeConfig.hasTimestamp('2011-08-05')).to.be.true + expect(layer.timeConfig.hasTimestamp('2008-10-24')).to.be.true + expect(layer.timeConfig.hasTimestamp('2008-10-23')).to.be.false + + expect(layer.timeConfig.getTimeEntryForYear(2008)).to.be.not.null.and.not.undefined + expect(layer.timeConfig.getTimeEntryForYear(2005)).to.be.null + + expect(layer.timeConfig.currentTimeEntry).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimeEntry.timestamp).toBe('2011-08-05') + expect(layer.timeConfig.currentTimeEntry.year).toBe(2011) + expect(layer.timeConfig.currentYear).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentYear).toBe(2011) + expect(layer.timeConfig.currentTimestamp).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimestamp).toBe('2011-08-05') + }) + it('Parse layer time dimension in format full ISO format YYYY-MM-DDTHH:mm:ss.sssZ', () => { + let layer = capabilities.getExternalLayerObject('BlueMarbleFourthGenerationJU', WGS84) + expect(layer.id).toBe('BlueMarbleFourthGenerationJU') + expect(layer.timeConfig).to.be.not.null.and.not.undefined + expect(layer.timeConfig.hasTimestamp('2011-08-05T01:20:34.345Z')).to.be.true + expect(layer.timeConfig.hasTimestamp('2008-10-24T01:20:34.345Z')).to.be.true + expect(layer.timeConfig.hasTimestamp('2008-10-23T01:20:34.345Z')).to.be.false + + expect(layer.timeConfig.getTimeEntryForYear(2008)).to.be.not.null.and.not.undefined + expect(layer.timeConfig.getTimeEntryForYear(2005)).to.be.null + + expect(layer.timeConfig.currentTimeEntry).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimeEntry.timestamp).toBe('2011-08-05T01:20:34.345Z') + expect(layer.timeConfig.currentTimeEntry.year).toBe(2011) + expect(layer.timeConfig.currentYear).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentYear).toBe(2011) + expect(layer.timeConfig.currentTimestamp).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimestamp).toBe('2011-08-05T01:20:34.345Z') + }) + it('Parse layer time dimension in unknown format', () => { + let layer = capabilities.getExternalLayerObject('BlueMarbleFifthGenerationGE', WGS84) + expect(layer.id).toBe('BlueMarbleFifthGenerationGE') + expect(layer.timeConfig).to.be.not.null.and.not.undefined + expect(layer.timeConfig.hasTimestamp('Time A')).to.be.true + expect(layer.timeConfig.hasTimestamp('Time B')).to.be.true + + expect(layer.timeConfig.currentTimeEntry).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimeEntry.timestamp).toBe('Time A') + expect(layer.timeConfig.currentTimeEntry.year).to.be.null + expect(layer.timeConfig.currentYear).to.be.null + expect(layer.timeConfig.currentTimestamp).to.be.not.null.and.not.undefined + expect(layer.timeConfig.currentTimestamp).toBe('Time A') + }) }) diff --git a/src/api/layers/__tests__/wmts-ogc-sample.xml b/src/api/layers/__tests__/wmts-ogc-sample.xml index 0cb809b38..c719491f7 100644 --- a/src/api/layers/__tests__/wmts-ogc-sample.xml +++ b/src/api/layers/__tests__/wmts-ogc-sample.xml @@ -6,8 +6,7 @@ xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0.0/wmtsGetCapabilities_response.xsd" -> + xsi:schemaLocation="http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd"> Web Map Tile Service Service that constrains the map @@ -98,6 +97,12 @@ image/jpeg image/gif + + Time + 20110805 + 20110805 + 20081024 + BigWorldPixel @@ -117,21 +122,15 @@ resourceType="FeatureInfo" template="http://www.example.com/wmts/coastlines/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}/{J}/{I}.xml" /> - - Time - 20110805 - 20110805 - 20081024 - Blue Marble Second Generation - AG Blue Marble Second Generation Canton Aargau Product + BlueMarbleSecondGenerationAG 7.8 47.09 8.47 47.64 - BlueMarbleSecondGenerationAG image/jpeg image/gif + + Time + 20110805 + 20110805 + 20081024 + BigWorldPixel @@ -166,12 +171,6 @@ resourceType="FeatureInfo" template="http://www.example.com/wmts/coastlines/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}/{J}/{I}.xml" /> - - Time - 20110805 - 20110805 - 20081024 - BlueMarbleThirdGenerationZH @@ -195,6 +194,12 @@ image/jpeg image/gif + + Time + 2011-08-05 + 2011-08-05 + 2008-10-24 + BigWorldPixel @@ -208,12 +213,6 @@ resourceType="FeatureInfo" template="http://www.example.com/wmts/coastlines/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}/{J}/{I}.xml" /> - - Time - 20110805 - 20110805 - 20081024 - Blue Marble Fourth Generation - JU @@ -234,6 +233,12 @@ image/jpeg image/gif + + Time + 2011-08-05T01:20:34.345Z + 2011-08-05T01:20:34.345Z + 2008-10-24T01:20:34.345Z + JURA @@ -247,16 +252,11 @@ resourceType="FeatureInfo" template="http://www.example.com/wmts/coastlines/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}/{J}/{I}.xml" /> - - Time - 20110805 - 20110805 - 20081024 - Blue Marble Fifth Generation - GE Blue Marble Fifth Generation Canton Geneva Product + BlueMarbleFifthGenerationGE 5.95 46.12 6.33 46.32 @@ -269,7 +269,6 @@ 2484928.06 1108705.32 2514614.27 1130449.26 - BlueMarbleFifthGenerationGE image/jpeg image/gif + + Time + Time A + Time A + Time B + BigWorldPixel @@ -304,12 +309,6 @@ resourceType="FeatureInfo" template="http://www.example.com/wmts/coastlines/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}/{J}/{I}.xml" /> - - Time - 20110805 - 20110805 - 20081024 - @@ -503,11 +502,11 @@ BigWorldPixel - urn:ogc:def:crs:OGC:1.3:CRS84 -180 -65 180 65 + urn:ogc:def:crs:OGC:1.3:CRS84 urn:ogc:def:wkss:OGC:1.0:GlobalCRS84Pixel 10000m @@ -588,11 +587,11 @@ JURA - urn:ogc:def:crs:OGC:1.3:CRS84 6.81 47.12 7.56 47.55 + urn:ogc:def:crs:OGC:1.3:CRS84 1e6 1e6 diff --git a/src/api/print.api.js b/src/api/print.api.js index 223bb1df8..c58c7bef9 100644 --- a/src/api/print.api.js +++ b/src/api/print.api.js @@ -6,6 +6,7 @@ import { requestReport, } from '@geoblocks/mapfishprint' import axios from 'axios' +import { Circle } from 'ol/style' import { API_BASE_URL, API_SERVICES_BASE_URL, WMS_BASE_URL } from '@/config' import i18n from '@/modules/i18n' @@ -88,6 +89,16 @@ class GeoAdminCustomizer extends BaseCustomizer { symbolizer.pointRadius = adjustWidth(symbolizer.pointRadius, this.printResolution) symbolizer.strokeWidth = adjustWidth(symbolizer.strokeWidth, this.printResolution) symbolizer.haloRadius = adjustWidth(symbolizer.haloRadius, this.printResolution) + // Ideally this should be done in the geoblocks/mapfishprint + // but it's quite complex to handle all the cases + try { + const fontFamily = symbolizer.fontFamily.split(' ') + symbolizer.fontWeight = fontFamily[0] + symbolizer.fontSize = parseInt(fontFamily[1]) + symbolizer.fontFamily = fontFamily[2].toUpperCase() + } catch (error) { + // Keep the font family as it is + } } /** @@ -100,12 +111,32 @@ class GeoAdminCustomizer extends BaseCustomizer { */ // eslint-disable-next-line no-unused-vars point(layerState, symbolizer, image) { - symbolizer.graphicWidth = adjustWidth(symbolizer.graphicWidth, this.printResolution) - symbolizer.graphicXOffset = adjustWidth(symbolizer.graphicXOffset, this.printResolution) - symbolizer.graphicYOffset = adjustWidth(symbolizer.graphicYOffset, this.printResolution) - // Handling the case where we need to print a circle in the end of measurement lines - // It's not rendered in the OpenLayers (opacity == 0.0) but it's needed to be rendered in the print + const scale = image.getScaleArray()[0] + let size = null + let anchor = null + + // We need to resize the image to match the old geoadmin + if (symbolizer.externalGraphic) { + size = image.getSize() + anchor = image.getAnchor() + } else if (image instanceof Circle) { + const radius = image.getRadius() + const width = adjustWidth(2 * radius, this.printResolution) + size = [width, width] + anchor = [width / 2, width / 2] + } + + if (anchor) { + symbolizer.graphicXOffset = adjustWidth(-anchor[0] * scale, this.printResolution) + symbolizer.graphicYOffset = adjustWidth(-anchor[1] * scale, this.printResolution) + } + if (size) { + symbolizer.graphicWidth = adjustWidth(size[0] * scale, this.printResolution) + } + if (symbolizer.fillOpacity === 0.0 && symbolizer.fillColor === '#ff0000') { + // Handling the case where we need to print a circle in the end of measurement lines + // It's not rendered in the OpenLayers (opacity == 0.0) but it's needed to be rendered in the print symbolizer.fillOpacity = 1 } } @@ -316,7 +347,7 @@ async function transformOlMapToPrintParams(olMap, config) { const encodedMap = await encoder.encodeMap({ map: olMap, scale, - printResolution: dpi, + printResolution: olMap.getView().getResolution(), dpi: dpi, customizer: customizer, }) diff --git a/src/modules/i18n/locales/de.json b/src/modules/i18n/locales/de.json index b905e563e..22cfc6fe8 100644 --- a/src/modules/i18n/locales/de.json +++ b/src/modules/i18n/locales/de.json @@ -484,6 +484,10 @@ "time_hide": "Deaktivieren der Anzeige von Daten-Zeitständen.", "time_select_year": "Wählen Sie ein Jahr aus", "time_show": "Aktivieren der Anzeige von Daten-Zeitständen.", + "time_slider_legend_tippy_intro": "Der Zeitschieber zeigt verschiedene Farben an, je nachdem, ob die Karten Daten für das ausgewählte Jahr enthalten oder nicht.", + "time_slider_legend_tippy_full_data": "Vollständige Daten", + "time_slider_legend_tippy_no_data": "Teildaten", + "time_slider_legend_tippy_partial_data": "Keine Daten", "title": "Titel", "tooltip": "Tooltip", "topic_are_tooltip": "Bundesamt für Raumentwicklung", @@ -638,7 +642,7 @@ "loading_error_network_failure": "Laden fehlgeschlagen, auf die Datei kann nicht zugegriffen werden", "loading_file": "Laden...", "invalid_kml_gpx_file_error": "Ungültige Datei, nur KML- oder GPX-Dateien werden unterstützt", - "import_maps": "Karten Importieren", + "import_maps": "Karten importieren", "import_maps_tooltip": "Importieren Sie externe WMTS- oder WMS-Datenquellen", "unsupported_content_type": "Nicht unterstützter Antwortinhaltstyp", "invalid_wms_capabilities": "Ungültige WMS-Capabilities-Daten", @@ -666,11 +670,15 @@ "media_disclaimer": "Enthält Inhalte von Drittanbietern", "drawing_attached": "Zeichnung als Anhang hinzugefügt", "switch_tooltip_attached_to_feature": "Tooltip an Geometrie anhängen", - "switch_tooltip_floating": "Eine schwebende Tooltip verwenden", + "switch_tooltip_floating": "Einen schwebenden Tooltip verwenden", "field_required": "Dieses Feld ist erforderlich", "invalid_email": "ungültige E-Mail", "no_email": "Das E-Mail-Feld ist erforderlich", "file_unsupported_format": "Dieses Dateiformat wird nicht unterstützt. Nur die folgenden Formate sind erlaubt: ALLOWED_FORMATS", "form_invalid": "Das Formular ist ungültig", - "file_imported_success": "Datei erfolgreich importiert" + "file_imported_success": "Datei erfolgreich importiert", + "3d_labels": "Geografische Namen", + "3d_vegetation": "Vegetationen", + "3d_constructions": "Gebäude und Konstruktionen", + "webmapviewer_live_disclaimer": "Grosse Veränderungen stehen bevor, erfahren Sie mehr" } diff --git a/src/modules/i18n/locales/en.json b/src/modules/i18n/locales/en.json index 88d70fd53..5a72d7edf 100644 --- a/src/modules/i18n/locales/en.json +++ b/src/modules/i18n/locales/en.json @@ -484,6 +484,10 @@ "time_hide": "Disable representation of data time stamps.", "time_select_year": "Select a year", "time_show": "Enable representation of data time stamps.", + "time_slider_legend_tippy_intro": "The time slider displays different colors depending on whether the maps have data for the year or not.", + "time_slider_legend_tippy_full_data": "Complete data", + "time_slider_legend_tippy_no_data": "No data.", + "time_slider_legend_tippy_partial_data": "Partial data", "title": "Title", "tooltip": "Tooltip", "topic_are_tooltip": "Federal Office for Spatial Development ", @@ -661,7 +665,7 @@ "operation_aborted": "Operation aborted", "outside_valid_year_range": "The entered year is outside ot the available range:", "linkedin_tooltip": "Share this map on LinkedIn", - "share_map_title": "Link to the geoportal of Swiss Confederation", + "share_map_title": "Link to the geoportal of the Swiss Confederation", "link_description": "Link description (optional)", "media_disclaimer": "Contains third party content", "drawing_attached": "Drawing added as attachment", @@ -669,8 +673,12 @@ "switch_tooltip_floating": "Use a floating tooltip", "field_required": "This field is required", "invalid_email": "Invalid email", - "no_email": "Email field is required", + "no_email": "The e-mail field is required", "file_unsupported_format": "This file format is not supported. Only the following formats are allowed: ALLOWED_FORMATS", "form_invalid": "Form is invalid", - "file_imported_success": "File successfully imported" + "file_imported_success": "File successfully imported", + "3d_labels": "Geographic names", + "3d_vegetation": "Vegetations", + "3d_constructions": "Buildings and constructions", + "webmapviewer_live_disclaimer": "Big changes are coming soon, learn more" } diff --git a/src/modules/i18n/locales/fr.json b/src/modules/i18n/locales/fr.json index e1ee6ab6c..b849f5f3c 100644 --- a/src/modules/i18n/locales/fr.json +++ b/src/modules/i18n/locales/fr.json @@ -484,6 +484,10 @@ "time_hide": "Désactiver l'outil de représentation historique des données.", "time_select_year": "Choisissez une année", "time_show": "Activer l'outil de représentation historique des données.", + "time_slider_legend_tippy_intro": "Le curseur de temps affiche différentes couleurs selon si les cartes temporelles disposent de données ou non.", + "time_slider_legend_tippy_full_data": "Données complètes", + "time_slider_legend_tippy_no_data": "Aucune donnée", + "time_slider_legend_tippy_partial_data": "Données partielles", "title": "Titre", "tooltip": "Tooltip", "topic_are_tooltip": "Office fédéral du développement territorial ", @@ -672,5 +676,9 @@ "no_email": "Le champ email est requis", "file_unsupported_format": "Ce format de fichier n'est pas pris en charge. Seuls les formats suivants sont autorisés : ALLOWED_FORMATS", "form_invalid": "Le formulaire est invalide", - "file_imported_success": "Fichier importé avec succès" + "file_imported_success": "Fichier importé avec succès", + "3d_labels": "Noms géographiques", + "3d_vegetation": "Végétations", + "3d_constructions": "Bâtiments et constructions", + "webmapviewer_live_disclaimer": "De grands changements sont à venir, en savoir plus" } diff --git a/src/modules/i18n/locales/it.json b/src/modules/i18n/locales/it.json index 2b046948b..1a3fd4ba2 100644 --- a/src/modules/i18n/locales/it.json +++ b/src/modules/i18n/locales/it.json @@ -482,8 +482,12 @@ "time_bt_disabled_tooltip": "La visualizzazione storica dei dati è possibile solo con i layer storicizzati. Aggiungere un layer storicizzato per utilizzare questa funzione", "time_current": "Attuale", "time_hide": "Disattivare la visualizzazione storica dei dati", - "time_select_year": "Scegliete un anno", + "time_select_year": "Scelga un anno", "time_show": "Attivare la visualizzazione storica dei dati", + "time_slider_legend_tippy_intro": "Il cursore del tempo mostra colori diversi a seconda che gli strati temporali abbiano o no dati per l'anno selezionato.", + "time_slider_legend_tippy_full_data": "Dati completi", + "time_slider_legend_tippy_no_data": "Nessun dato", + "time_slider_legend_tippy_partial_data": "Dati parziali", "title": "Titolo", "tooltip": "Tooltip", "topic_are_tooltip": "Ufficio federale dello sviluppo territoriale ", @@ -668,9 +672,13 @@ "switch_tooltip_attached_to_feature": "Collegare il tooltip alla geometria", "switch_tooltip_floating": "Utilizzare un tooltip fluttuante", "field_required": "Questo campo è obbligatorio", - "invalid_email": "e-mail non valide", + "invalid_email": "e-mail non valido", "no_email": "Il campo email è obbligatorio", "file_unsupported_format": "Questo formato file non è supportato. Sono ammessi solo i seguenti formati: ALLOWED_FORMATS", "form_invalid": "Il modulo non è valido", - "file_imported_success": "File importato con successo" + "file_imported_success": "File importato con successo", + "3d_labels": "Nomi geografici", + "3d_vegetation": "Vegetazione", + "3d_constructions": "Edifici e costruzioni", + "webmapviewer_live_disclaimer": "Grandi cambiamenti in arrivo, per saperne di più" } diff --git a/src/modules/i18n/locales/rm.json b/src/modules/i18n/locales/rm.json index d1238c872..e49897b6e 100644 --- a/src/modules/i18n/locales/rm.json +++ b/src/modules/i18n/locales/rm.json @@ -482,6 +482,10 @@ "time_hide": "Deactivar la visualisaziun istorica da las datas.", "time_select_year": "Selecziunais in onn", "time_show": "Activar la visualisaziun istorica da las datas.", + "time_slider_legend_tippy_intro": "Il temp slider mussa differentas colurs, tut tenor sche las stresas dal temp han datas per l'onn tschernì u betg.", + "time_slider_legend_tippy_full_data": "Datas entiras", + "time_slider_legend_tippy_no_data": "Naginas datas", + "time_slider_legend_tippy_partial_data": "Datas parzialas", "title": "Titel", "tooltip": "Tooltip", "topic_are_tooltip": "Uffizi federal da planisaziun dal territori", @@ -670,5 +674,9 @@ "no_email": "Il champ da posta eletronicala è necessari", "file_unsupported_format": "Quest format da datoteca na vegn betg sustegnì. Sun ils seguents formats èn permess: ALLOWED_FORMATS", "form_invalid": "Il formular è invalid", - "file_imported_success": "Datoteca importada cun success" + "file_imported_success": "Datoteca importada cun success", + "3d_labels": "Numns geografics", + "3d_vegetation": "Vegietaziun", + "3d_constructions": "Edifizis ed installaziuns", + "webmapviewer_live_disclaimer": "Grondas midadas vegnan prest, emprender dapli" } diff --git a/src/modules/map/components/cesium/CesiumWMTSLayer.vue b/src/modules/map/components/cesium/CesiumWMTSLayer.vue index 3e0b8f355..0af18ce44 100644 --- a/src/modules/map/components/cesium/CesiumWMTSLayer.vue +++ b/src/modules/map/components/cesium/CesiumWMTSLayer.vue @@ -5,13 +5,21 @@ diff --git a/src/modules/map/components/openlayers/utils/printConstants.js b/src/modules/map/components/openlayers/utils/printConstants.js deleted file mode 100644 index b83e1b842..000000000 --- a/src/modules/map/components/openlayers/utils/printConstants.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * The name of the OpenLayer layer that is used to render the print area. - * - * @type {String} - */ -export const PRINT_AREA_LAYER_ID = 'printAreaLayer' diff --git a/src/modules/map/components/openlayers/utils/usePrint.composable.js b/src/modules/map/components/openlayers/utils/usePrint.composable.js index 2e060d7a7..438b52265 100644 --- a/src/modules/map/components/openlayers/utils/usePrint.composable.js +++ b/src/modules/map/components/openlayers/utils/usePrint.composable.js @@ -6,8 +6,6 @@ import { getGenerateQRCodeUrl } from '@/api/qrcode.api.js' import { createShortLink } from '@/api/shortlink.api.js' import log from '@/utils/logging' -import { PRINT_AREA_LAYER_ID } from './printConstants' - const dispatcher = { dispatcher: 'usePrint.composable' } /** @enum */ @@ -81,7 +79,6 @@ export function usePrint(map) { lang: store.state.i18n.lang, printGrid: printGrid, projection: store.state.position.projection, - excludedLayerIDs: [PRINT_AREA_LAYER_ID], dpi: store.getters.selectedDPI, }) currentJobReference.value = printJob.ref diff --git a/src/modules/map/components/openlayers/utils/usePrintAreaRenderer.composable.js b/src/modules/map/components/openlayers/utils/usePrintAreaRenderer.composable.js index 0149b30e1..30b0cb336 100644 --- a/src/modules/map/components/openlayers/utils/usePrintAreaRenderer.composable.js +++ b/src/modules/map/components/openlayers/utils/usePrintAreaRenderer.composable.js @@ -1,51 +1,12 @@ -import { Feature } from 'ol' -import { Polygon } from 'ol/geom' import * as olHas from 'ol/has' -import VectorLayer from 'ol/layer/Vector' -import VectorSource from 'ol/source/Vector' -import { Fill, Style } from 'ol/style' +import { getRenderPixel } from 'ol/render' import { computed, watch } from 'vue' import { useStore } from 'vuex' import log from '@/utils/logging' -import { PRINT_AREA_LAYER_ID } from './printConstants' - const dispatcher = { dispatcher: 'print-area-renderer.composable' } -function createWorldPolygon() { - // Create a polygon feature covering the whole world in EPSG:4326 - const worldPolygon = new Feature({ - geometry: new Polygon([ - [ - [-180, -90], // Bottom-left corner - [180, -90], // Bottom-right corner - [180, 90], // Top-right corner - [-180, 90], // Top-left corner - [-180, -90], // Bottom-left corner - ], - ]).transform('EPSG:4326', 'EPSG:3857'), - }) - - // Define a transparent style for the polygon - const transparentStyle = new Style({ - fill: new Fill({ - color: 'rgba(255, 255, 255, 0)', - }), - }) - - // Create a VectorLayer outside the map creation - const vectorLayer = new VectorLayer({ - source: new VectorSource({ - features: [worldPolygon], - }), - style: transparentStyle, - id: PRINT_AREA_LAYER_ID, - zIndex: Infinity, // Make sure the print area is always on top - }) - return vectorLayer -} - export default function usePrintAreaRenderer(map) { const store = useStore() @@ -53,7 +14,6 @@ export default function usePrintAreaRenderer(map) { const POINTS_PER_INCH = 72 // PostScript points 1/72" const MM_PER_INCHES = 25.4 const UNITS_RATIO = 39.37 // inches per meter - let worldPolygon = null let printRectangle = [] const isActive = computed(() => store.state.print.printSectionShown) @@ -63,7 +23,6 @@ export default function usePrintAreaRenderer(map) { const mapWidth = computed(() => store.state.ui.width) // Same here for simplicity we take the screen size minus the header size for the map size const mapHeight = computed(() => store.state.ui.height - store.state.ui.headerHeight) - const headerHeight = computed(() => store.state.ui.headerHeight) watch(isActive, (newValue) => { if (newValue) { @@ -74,13 +33,9 @@ export default function usePrintAreaRenderer(map) { }) function activatePrintArea() { - if (!worldPolygon) { - worldPolygon = createWorldPolygon() - } - map.addLayer(worldPolygon) deregister = [ - worldPolygon.on('prerender', handlePreRender), - worldPolygon.on('postrender', handlePostRender), + map.getAllLayers()[0].on('prerender', handlePreRender), + map.getAllLayers()[0].on('postrender', handlePostRender), watch(printLayoutSize, async () => { await store.dispatch('setSelectedScale', { scale: getOptimalScale(), @@ -104,7 +59,6 @@ export default function usePrintAreaRenderer(map) { } function deactivatePrintArea() { - map.removeLayer(worldPolygon) while (deregister.length > 0) { const item = deregister.pop() if (typeof item === 'function') { @@ -141,11 +95,9 @@ export default function usePrintAreaRenderer(map) { ] const minx = center[0] - w / 2 - // here we move the center down due to the header that overlap the map - const miny = center[1] - h / 2 + headerHeight.value + const miny = center[1] - h / 2 const maxx = center[0] + w / 2 - // here we move the center down due to the header that overlap the map - const maxy = center[1] + h / 2 + headerHeight.value + const maxy = center[1] + h / 2 return [minx, miny, maxx, maxy] } @@ -170,20 +122,19 @@ export default function usePrintAreaRenderer(map) { context.save() context.beginPath() + // Outside polygon, must be clockwise - context.moveTo(0, 0) - context.lineTo(width, 0) - context.lineTo(width, height) - context.lineTo(0, height) - context.lineTo(0, 0) - context.closePath() + context.moveTo(...getRenderPixel(event, [0, 0])) + context.lineTo(...getRenderPixel(event, [width, 0])) + context.lineTo(...getRenderPixel(event, [width, height])) + context.lineTo(...getRenderPixel(event, [0, height])) + + // Inner polygon, must be counter-clockwise + context.moveTo(...getRenderPixel(event, [minx, miny])) + context.lineTo(...getRenderPixel(event, [minx, maxy])) + context.lineTo(...getRenderPixel(event, [maxx, maxy])) + context.lineTo(...getRenderPixel(event, [maxx, miny])) - // Inner polygon,must be counter-clockwise - context.moveTo(minx, miny) - context.lineTo(minx, maxy) - context.lineTo(maxx, maxy) - context.lineTo(maxx, miny) - context.lineTo(minx, miny) context.closePath() context.fillStyle = 'rgba(0, 5, 25, 0.75)' diff --git a/src/modules/map/components/toolbox/TimeSlider.vue b/src/modules/map/components/toolbox/TimeSlider.vue index 41995deef..4518757d1 100644 --- a/src/modules/map/components/toolbox/TimeSlider.vue +++ b/src/modules/map/components/toolbox/TimeSlider.vue @@ -1,6 +1,6 @@ diff --git a/src/modules/menu/components/3d/MenuThreeD.vue b/src/modules/menu/components/3d/MenuThreeD.vue new file mode 100644 index 000000000..65cce7c30 --- /dev/null +++ b/src/modules/menu/components/3d/MenuThreeD.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/modules/menu/components/LayerCatalogueItem.vue b/src/modules/menu/components/LayerCatalogueItem.vue index 5f6213de8..74c913aa9 100644 --- a/src/modules/menu/components/LayerCatalogueItem.vue +++ b/src/modules/menu/components/LayerCatalogueItem.vue @@ -234,7 +234,12 @@ function containsLayer(layers, searchText) {