diff --git a/app-starter/WguAppTemplate.vue b/app-starter/WguAppTemplate.vue index d6829a01..56515da9 100644 --- a/app-starter/WguAppTemplate.vue +++ b/app-starter/WguAppTemplate.vue @@ -40,8 +40,8 @@ - - + +
diff --git a/docs/map-layer-configuration.md b/docs/map-layer-configuration.md index 4a6dc12e..51a8f113 100644 --- a/docs/map-layer-configuration.md +++ b/docs/map-layer-configuration.md @@ -196,7 +196,19 @@ Additionally every configuration property of [`ol/style/Text`](https://openlayer "offsetY": 15, "align": "center" } - } + }, + "columnMapping": { + "name": "Name", + "email": "Email", + "website": "Website" + }, + "selectStyle": { + "radius": 10, + "strokeColor": "gray", + "strokeWidth": 5, + "fillColor": "rgb(255, 255, 0, 0.2)" + }, + "doAppendSelectStyle": true } ``` diff --git a/docs/module-configuration.md b/docs/module-configuration.md index 19602bb9..ca0eb2d0 100644 --- a/docs/module-configuration.md +++ b/docs/module-configuration.md @@ -20,7 +20,7 @@ The following properties can be applied to all module types: |--------------------|:---------:|---------| | **target** | Where should the button to enable/disable the module be rendered. Valid options are `menu` or `toolbar` | `"target": "menu"` | | **win** | Value to mark if the module has a window as sub component and where to show the module UI elements. Valid options are `floating` and `sidebar`. If the value is omitted, then the module is not associated with a window. | `"win": "floating"` | -| icon | Provide a customized icon for the module. | `"icon": "info"` | +| icon | Provide a customized icon for the module. | `"icon": "md:info"` | | minimizable | Indicates whether the module window can be minimized. Only applies if a module window is present as indicated by the `win` parameter. | `"minimizable": true` | | closable | Indicates whether the module window can be closed by a "X" icon in the window's header bar. Only applies if a module window is present as indicated by the `win` parameter. | `"closable": false` | | backgroundImage | Optional background image for the window header. Only applies if a module window is present as indicated by the `win` parameter. | `"backgroundImage": "static/icon/myImage.png"}` | diff --git a/docs/wegue-configuration.md b/docs/wegue-configuration.md index 0858901b..6c3e5708 100644 --- a/docs/wegue-configuration.md +++ b/docs/wegue-configuration.md @@ -198,11 +198,12 @@ In a Layer configuration a specific tilegrid can be refered to as follows, using { "type": "XYZ", "lid": "brtachtergrondkaart", - "name": "WMTS - Topo Basemap - PDOK", "url": "https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/{z}/{x}/{y}.png", "projection": "EPSG:28992", "tileGridRef": "dutch_rd", - "visible": true + "isBaseLayer": true, + "visible": true, + "crossOrigin": "anonymous" } ``` @@ -364,7 +365,7 @@ The `overviewMap` object supports the following properties: | Property | Meaning | Example | |--------------------|:---------:|---------| -| icon | Provide a customized map icon. Defaults to `zoom_out_map`. | `"icon": "zoom_out_map"` | +| icon | Provide a customized map icon. Defaults to `md:zoom_out_map`. | `"icon": "md:zoom_out_map"` | | visible | Specifies whether the overviewMap appears in open or closed state on application start. Defaults to true. | `"visible": true` | | rotateWithView | Whether the control view should rotate with the main map view. Defaults to true. | `"rotateWithView": true` | | width | Width of the overview map panel in viewport coordinates. Defaults to 164px. | `"width": 164` | @@ -430,7 +431,9 @@ Example configurations can be found in the `app-starter/static` directory. Below "lang": { "supported": { "en": "English", - "de": "Deutsch" + "de": "Deutsch", + "pt": "Portugues", + "fr": "Français" }, "fallback": "en" }, @@ -574,8 +577,30 @@ Example configurations can be found in the `app-starter/static` directory. Below "visible": false, "displayInLayerList": true, "legend": true, - "opacityControl": true + "opacityControl": true, + "crossOrigin": "anonymous" }, + + { + "type": "TILEARCGIS", + "lid": "test_arcgisrest", + "format": "image/jpeg", + "url": "https://cartografia.comune.padova.it/server/rest/services/topo/MapServer", + "params": { + "LAYERS":"show:3,16", + "TRANSPARENT": true + }, + "transparent": true, + "projection": "EPSG:3003", + "attribution": "Comune di padova", + "isBaseLayer": false, + "visible": false, + "displayInLayerList": true, + "legend": false, + "opacityControl": true, + "crossOrigin": "anonymous" + }, + { "type": "IMAGEWMS", "lid": "ahocevar-imagewms", @@ -589,7 +614,8 @@ Example configurations can be found in the `app-starter/static` directory. Below "isBaseLayer": false, "visible": false, "displayInLayerList": true, - "opacityControl": true + "opacityControl": true, + "crossOrigin": "anonymous" }, { "type": "VECTORTILE", @@ -629,13 +655,13 @@ Example configurations can be found in the `app-starter/static` directory. Below "wgu-layerlist": { "target": "menu", "win": "floating", - "icon": "layers", + "icon": "md:layers", "draggable": false }, "wgu-measuretool": { "target": "menu", "win": "floating", - "icon": "photo_size_select_small", + "icon": "md:photo_size_select_small", "draggable": false, "strokeColor": "#c62828", "fillColor": "rgba(198,40,40,0.2)", @@ -647,7 +673,7 @@ Example configurations can be found in the `app-starter/static` directory. Below "wgu-infoclick": { "target": "menu", "win": "floating", - "icon": "info", + "icon": "md:info", "draggable": false, "initPos": { "left": 8, @@ -683,19 +709,19 @@ Example configurations can be found in the `app-starter/static` directory. Below "wgu-helpwin": { "target": "toolbar", "win": "floating", - "icon": "help" + "icon": "md:help" }, "wgu-geolocator": { "target": "toolbar" }, "wgu-themeswitcher": { "target": "toolbar", - "icon": "dark_mode" + "icon": "md:dark_mode" }, "wgu-attributetable": { "target": "menu", "win": "floating", - "icon": "table_chart", + "icon": "md:table_chart", "syncTableMapSelection": true }, "wgu-localeswitcher": { @@ -704,7 +730,8 @@ Example configurations can be found in the `app-starter/static` directory. Below "sample-module": { "target": "toolbar", "win": "floating", - "icon": "star" + "icon": "md:star", + "closable": false } } } diff --git a/package-lock.json b/package-lock.json index 6ba99681..2c8027f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "canvas-record": "^3.1.0", "core-js": "^3.39.0", "ol": "10.2.1", - "portal-vue": "^3.0.0", "proj4": "2.9.0", "tiny-emitter": "^2.1.0", "vue": "^3.5.13", @@ -15753,22 +15752,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/portal-vue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-3.0.0.tgz", - "integrity": "sha512-9eprMxNURLx6ijbcgkWjYNcTWJYu/H8QF8nyAeBzOmk9lKCea01BW1hYBeLkgz+AestmPOvznAEOFmNuO4Adjw==", - "engines": { - "node": ">=14.19" - }, - "peerDependencies": { - "vue": "^3.0.4" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", diff --git a/package.json b/package.json index 5bef6695..9087b42f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "canvas-record": "^3.1.0", "core-js": "^3.39.0", "ol": "10.2.1", - "portal-vue": "^3.0.0", "proj4": "2.9.0", "tiny-emitter": "^2.1.0", "vue": "^3.5.13", diff --git a/src/assets/css/wegue.css b/src/assets/css/wegue.css index 0d65891f..c3488b6d 100644 --- a/src/assets/css/wegue.css +++ b/src/assets/css/wegue.css @@ -88,6 +88,12 @@ html { overflow-y: hidden; } +/* Teleport to overlay the map content from an application module. */ +#wgu-map-teleport * { + pointer-events: auto; + overflow: visible; +} + /* TODO Review ccs colorization. Used to force the on-primary color for solo fields (light theme only). @@ -189,3 +195,54 @@ html { cursor: ew-resize; z-index: 10000; } + +/* Map vuetify utility classes for on-* theme tokens to theme colors. */ +.bg-on-primary { + background-color: rgb(var(--v-theme-on-primary)) !important; +} +.bg-on-secondary { + background-color: rgb(var(--v-theme-on-secondary)) !important; +} +.bg-on-surface { + background-color: rgb(var(--v-theme-on-surface)) !important; +} +.bg-on-background { + background-color: rgb(var(--v-theme-on-background)) !important; +} +.bg-on-error { + background-color: rgb(var(--v-theme-on-error)) !important; +} +.bg-on-warning { + background-color: rgb(var(--v-theme-on-warning)) !important; +} +.bg-on-success { + background-color: rgb(var(--v-theme-on-success)) !important; +} +.bg-on-info { + background-color: rgb(var(--v-theme-on-info)) !important; +} + +.text-on-primary { + color: rgb(var(--v-theme-on-primary)) !important; +} +.text-on-secondary { + color: rgb(var(--v-theme-on-secondary)) !important; +} +.text-on-surface { + color: rgb(var(--v-theme-on-surface)) !important; +} +.text-on-background { + color: rgb(var(--v-theme-on-background)) !important; +} +.text-on-error { + color: rgb(var(--v-theme-on-error)) !important; +} +.text-on-warning { + color: rgb(var(--v-theme-on-warning)) !important; +} +.text-on-success { + color: rgb(var(--v-theme-on-success)) !important; +} +.text-on-info { + color: rgb(var(--v-theme-on-info)) !important; +} \ No newline at end of file diff --git a/src/components/layerlist/LayerLegendImage.vue b/src/components/layerlist/LayerLegendImage.vue index 0fab5836..ab02fcbd 100644 --- a/src/components/layerlist/LayerLegendImage.vue +++ b/src/components/layerlist/LayerLegendImage.vue @@ -48,11 +48,12 @@ export default { * Returns a URL to the layers legend image. */ legendURL () { + const legendUtil = new LayerLegend(this.$appConfig?.legend); const options = { language: this.$i18n.locale, ...this.layer.get('legendOptions') }; - return LayerLegend.getUrl( + return legendUtil.getUrl( this.layer, this.resolution, options, this.layer.get('legendUrl')); } } diff --git a/src/components/localeswitcher/LocaleSwitcher.vue b/src/components/localeswitcher/LocaleSwitcher.vue index 63946db6..27633426 100644 --- a/src/components/localeswitcher/LocaleSwitcher.vue +++ b/src/components/localeswitcher/LocaleSwitcher.vue @@ -8,7 +8,7 @@ - + @@ -117,7 +117,6 @@ export default { toolbarAttr () { return this.backgroundImage ? { - dark: true, flat: true, color: 'transparent' } diff --git a/src/components/ol/HoverController.js b/src/components/ol/HoverController.js index 11d80128..e3a32c24 100644 --- a/src/components/ol/HoverController.js +++ b/src/components/ol/HoverController.js @@ -108,7 +108,7 @@ export default class HoverController { // Acquire features for all layers. map.getLayers().forEach((layer) => { - if (!layer.get('hoverable')) { + if (!layer.get('hoverable') || !layer.isVisible()) { return; } const source = layer.getSource(); diff --git a/src/main.js b/src/main.js index 275b4375..ea6202d8 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,6 @@ import { md } from 'vuetify/iconsets/md'; import { aliases as defaultAliases, mdi } from 'vuetify/iconsets/mdi'; import 'vuetify/styles'; import { createI18nInstance } from './locales/wgu-i18n'; -import PortalVue from 'portal-vue'; import 'roboto-fontface/css/roboto/roboto-fontface.css'; import '@mdi/font/css/materialdesignicons.css'; import 'material-icons/iconfont/material-icons.css'; @@ -209,7 +208,6 @@ const createAppInstance = function (appConfig) { const i18n = createVueI18nInstance(effectiveAppConfig); const app = createApp(WguApp); - app.use(PortalVue); app.use(vuetify); app.use(i18n); diff --git a/src/util/LayerLegend.js b/src/util/LayerLegend.js index f357c701..f47e448c 100644 --- a/src/util/LayerLegend.js +++ b/src/util/LayerLegend.js @@ -6,6 +6,63 @@ import TileWmsSource from 'ol/source/TileWMS'; import ImageWmsSource from 'ol/source/ImageWMS'; import ObjectUtil from './Object'; +class LayerLegend { + /** + * Instantiates a layer legend object with the legend options configured + * in the application context. + * @param {Object} options Application wide legend options. + */ + constructor (options) { + this.options = options; + } + + /** + * Merges the given legend options with the legend options configured + * in the application context. The provided options will take precedence. + * @param {Object} options Optional configuration params. + * @returns {Object} Merged configuration params. + */ + getOptions (options) { + return { + ...this.options, + ...options + }; + } + + /** + * Returns a URL to the layers legend image. + * @param {ol.Layer} layer The layer to produce the legend for. + * @param {Number} resolution Resolution of the legend image. + * @param {Object} options Optional configuration params. + * @param {String} formatUrl A custom format URL. + * @returns {String} Legend URL or undefined if no legend can be produced. + */ + getUrl (layer, resolution, options, formatUrl) { + const opts = this.getOptions(options); + const source = layer.getSource(); + + // If we cannot obtain a source, no legend can be produced. + if (!source) { + return undefined; + } + + // If a formatUrl is provided, the legend is custom. + if (formatUrl) { + return CustomLegend.getUrl(source, resolution, opts, formatUrl); + } + + // For WMS based sources, use the in-built legend URL formatter. + if ( + source instanceof TileWmsSource || + source instanceof ImageWmsSource + ) { + return WMSSourceLegend.getUrl(source, resolution, opts); + } + + return undefined; + } +} + const CustomLegend = { /** * Returns a URL to the legend image. @@ -57,53 +114,4 @@ const WMSSourceLegend = { } } -const LayerLegend = { - /** - * Merges the given legend options with the legend options configured - * in the application context. The provided options will take precedence. - * @param {Object} options Optional configuration params. - * @returns {Object} Merged configuration params. - */ - getOptions (options) { - const appConfig = this.$appConfig; - return { - ...appConfig?.legend, - ...options - }; - }, - - /** - * Returns a URL to the layers legend image. - * @param {ol.Layer} layer The layer to produce the legend for. - * @param {Number} resolution Resolution of the legend image. - * @param {Object} options Optional configuration params. - * @param {String} formatUrl A custom format URL. - * @returns {String} Legend URL or undefined if no legend can be produced. - */ - getUrl (layer, resolution, options, formatUrl) { - const opts = this.getOptions(options); - const source = layer.getSource(); - - // If we cannot obtain a source, no legend can be produced. - if (!source) { - return undefined; - } - - // If a formatUrl is provided, the legend is custom. - if (formatUrl) { - return CustomLegend.getUrl(source, resolution, opts, formatUrl); - } - - // For WMS based sources, use the in-built legend URL formatter. - if ( - source instanceof TileWmsSource || - source instanceof ImageWmsSource - ) { - return WMSSourceLegend.getUrl(source, resolution, opts); - } - - return undefined; - } -} - export default LayerLegend; diff --git a/src/util/Locale.js b/src/util/Locale.js index 0954f322..7684bf24 100644 --- a/src/util/Locale.js +++ b/src/util/Locale.js @@ -60,7 +60,8 @@ const LocaleUtil = { }, /** - * Import vuetify language files from 'node_modules/vuetify/es/locale'. + * Import vuetify language files from 'node_modules/vuetify/locale' + * (either *.js or *.mjs for older Vuetify 3 versions). * * @returns A container with message data. Key is the language code, value contains the messages. */ @@ -68,7 +69,7 @@ const LocaleUtil = { const moduleDefaultExtractor = i => i.default; return LocaleUtil.importLocales( - require.context('vuetify/lib/locale', false, /[a-z0-9-_]+\.mjs$/i), + require.context('vuetify/lib/locale', false, /[a-z0-9-_]+\.m?js$/i), moduleDefaultExtractor ); }, diff --git a/tests/unit/specs/WguAppTemplate.spec.js b/tests/unit/specs/WguAppTemplate.spec.js index dfde1dbb..5cbbbae3 100644 --- a/tests/unit/specs/WguAppTemplate.spec.js +++ b/tests/unit/specs/WguAppTemplate.spec.js @@ -1,6 +1,5 @@ import { shallowMount } from '@vue/test-utils'; import { createI18n } from 'vue-i18n'; -import PortalVue from 'portal-vue'; import i18nMessages from '@/locales/en.json'; import WguAppTpl from 'APP/WguAppTemplate'; @@ -21,7 +20,7 @@ function createWrapper ($appConfig = {}) { mocks: { $appConfig }, - plugins: [i18nInstance, PortalVue] + plugins: [i18nInstance] } }); } diff --git a/tests/unit/specs/components/layerlist/LayerLegendImage.spec.js b/tests/unit/specs/components/layerlist/LayerLegendImage.spec.js index 6d6eddab..519c3c36 100644 --- a/tests/unit/specs/components/layerlist/LayerLegendImage.spec.js +++ b/tests/unit/specs/components/layerlist/LayerLegendImage.spec.js @@ -35,7 +35,7 @@ const moduleProps = { layer: osmLayer }; -function createWrapper (props = moduleProps) { +function createWrapper (props = moduleProps, $appConfig = {}) { const i18nInstance = createI18n({ legacy: false, globalInjection: true, @@ -51,7 +51,10 @@ function createWrapper (props = moduleProps) { props, attachTo: document.body, global: { - plugins: [i18nInstance] + plugins: [i18nInstance], + mocks: { + $appConfig + } } }); } diff --git a/tests/unit/specs/components/ol/HoverController.spec.js b/tests/unit/specs/components/ol/HoverController.spec.js index bc6a0346..69575ca9 100644 --- a/tests/unit/specs/components/ol/HoverController.spec.js +++ b/tests/unit/specs/components/ol/HoverController.spec.js @@ -7,6 +7,7 @@ import VectorSource from 'ol/source/Vector'; import Point from 'ol/geom/Point'; // Test layers and feature +// The isVisible function of layers must be overwritten to prevent crashes. const feat = new Feature({ foo: 'bar', geometry: new Point([0, 0]) @@ -18,12 +19,24 @@ const layer = new VectorLayer({ features: [feat] }) }); +layer.isVisible = () => { return true; }; + const layerNonHoverable = new VectorLayer({ hoverable: false, source: new VectorSource({ features: [feat] }) }); +layerNonHoverable.isVisible = () => { return true; }; + +const layerNotVisible = new VectorLayer({ + hoverable: true, + hoverAttribute: 'foo', + source: new VectorSource({ + features: [feat] + }) +}); +layerNotVisible.isVisible = () => { return false; }; describe('ol/HoverController.js', () => { let comp; @@ -103,6 +116,28 @@ describe('ol/HoverController.js', () => { }, 100); }); + it('does not emit update-overlay event when non visible layer is hit', done => { + // Setup map and overwrite functions to simulate hitting a valid feature. + map.addLayer(layerNotVisible); + map.forEachLayerAtPixel = (pixel, callback, opts) => { + callback(layerNotVisible); + }; + map.getFeaturesAtPixel = (evt, opts) => { + return [feat]; + }; + + let eventEmitted = false; + WguEventBus.$once('wgu-hover-tooltip-update-overlay', (visible, position, data) => { + eventEmitted = true; + }); + comp.onPointerRest({ pixel: [0, 0] }); + + setTimeout(() => { + expect(eventEmitted).to.be.false; + done(); + }, 100); + }); + it('emits update-overlay event to hide overlay when no feature is hit', done => { // Setup map and functions to simulate hitting a valid layer but not a valid feature. map.addLayer(layer); diff --git a/upgrade-notes.md b/upgrade-notes.md index 86357f00..6f3b24fd 100644 --- a/upgrade-notes.md +++ b/upgrade-notes.md @@ -44,6 +44,14 @@ For basic usage, migrating from the `ColorTheme mixin` should be limited to remo > return { map, layers }; Please also note that `onMapBound` and `onMapUnbound` are not fired anymore. If you'd like to work with them as before, you have to implement your own `watcher` on the `map` for that. You can refer to the [OverviewMapPanel component](https://github.com/wegue-oss/wegue/blob/master/src/components/overviewmap/OverviewMapPanel.vue) to get a full example. +- `portal-vue` plugin has been replaced by the inbuilt Vue3 feature [Teleport](https://vuejs.org/guide/built-ins/teleport.html). The anchor element in the AppTemplate is now a `div` with the id `wgu-map-teleport` and the syntax to hook up a teleport template has slightly changed: + ```xml + + ``` Currently, the `Vue migration build` is used instead of the native `Vue 3` build. Because of this, usage of features that have changed or been deprecated in `Vue 3` will emit runtime warnings. This can be really useful while migrating an application and ensure everything was dealt with properly.