diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..c53976cdc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "overrides": [ + { + "files": ".firebaserc", + "options": { "parser": "json" } + } + ] +} diff --git a/Gruntfile.js b/Gruntfile.js index 16b92aa80..41145842b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -49,8 +49,8 @@ module.exports = function(grunt) { }, // ensure that jshint keeps processing after an error force: true, - esversion: 11 - + esversion: 11, + laxbreak: true // used to fix differences with prettier } }, watch: { @@ -188,6 +188,18 @@ module.exports = function(grunt) { dest: 'dist/mapml.js', src: 'src/mapml/index.js' // Only one source file is permitted } + }, + prettier: { + options: { + // https://prettier.io/docs/en/options.html + progress: true + }, + files: { + src: [ + "src/**/*.js", + "test/**/*.js" + ] + } } }); @@ -198,9 +210,10 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-rollup'); + grunt.loadNpmTasks('grunt-prettier'); - grunt.registerTask('test', ['jshint']); - grunt.registerTask('default', ['clean:dist', 'copy:main', 'copy:images', 'jshint', 'rollup', + grunt.registerTask('format', ['prettier', 'jshint']); + grunt.registerTask('default', ['clean:dist', 'copy:main', 'copy:images', 'format', 'rollup', 'uglify', 'cssmin','clean:tidyup']); grunt.registerTask('experiments',['clean:experiments','default','copy:experiments']); grunt.registerTask('extension',['clean:extension','default','copy:extension']); diff --git a/package-lock.json b/package-lock.json index 86cf1b2fe..35795e90b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "grunt-contrib-uglify": "~5.0.0", "grunt-contrib-watch": "~1.1.0", "grunt-html": "^15.2.2", + "grunt-prettier": "^2.2.0", "grunt-rollup": "^11.3.0", "jest": "^26.6.3", "jest-circus": "^26.6.3", @@ -4191,21 +4192,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "node_modules/fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "deprecated": "\"Please update to latest v2.3 or v2.2\"", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -5087,6 +5073,19 @@ "node": ">= 8" } }, + "node_modules/grunt-prettier": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/grunt-prettier/-/grunt-prettier-2.2.0.tgz", + "integrity": "sha512-kl6+1sYEM7HezxZS0DEFYYpD7JtwsToD8ZK2kDpAd3SvHINbz2iwsghpPsmdGihUJ2FVo8XBIzACmNxBBCytqQ==", + "dev": true, + "dependencies": { + "prettier": "^2.0.5", + "progress": "^2.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/grunt-rollup": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/grunt-rollup/-/grunt-rollup-11.3.0.tgz", @@ -7504,21 +7503,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-runner/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/jest-runner/node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -9784,6 +9768,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -9862,6 +9861,15 @@ "node": ">=8" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proj4": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.6.2.tgz", @@ -15505,13 +15513,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -16190,6 +16191,16 @@ } } }, + "grunt-prettier": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/grunt-prettier/-/grunt-prettier-2.2.0.tgz", + "integrity": "sha512-kl6+1sYEM7HezxZS0DEFYYpD7JtwsToD8ZK2kDpAd3SvHINbz2iwsghpPsmdGihUJ2FVo8XBIzACmNxBBCytqQ==", + "dev": true, + "requires": { + "prettier": "^2.0.5", + "progress": "^2.0.0" + } + }, "grunt-rollup": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/grunt-rollup/-/grunt-rollup-11.3.0.tgz", @@ -18015,14 +18026,6 @@ "jest-message-util": "^27.5.1" } }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true, - "peer": true - }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -19770,6 +19773,12 @@ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true }, + "prettier": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "dev": true + }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -19829,6 +19838,12 @@ "fromentries": "^1.2.0" } }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "proj4": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.6.2.tgz", diff --git a/package.json b/package.json index 012eb29ac..7674dc188 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "grunt-contrib-uglify": "~5.0.0", "grunt-contrib-watch": "~1.1.0", "grunt-html": "^15.2.2", + "grunt-prettier": "^2.2.0", "grunt-rollup": "^11.3.0", "jest": "^26.6.3", "jest-circus": "^26.6.3", diff --git a/src/layer.js b/src/layer.js index ff730c619..7f8d480ef 100644 --- a/src/layer.js +++ b/src/layer.js @@ -1,12 +1,12 @@ -import './leaflet.js'; // a lightly modified version of Leaflet for use as browser module -import './mapml.js'; // modified URI to make the function a property of window scope (possibly a bad thing to do). +import './leaflet.js'; // a lightly modified version of Leaflet for use as browser module +import './mapml.js'; // modified URI to make the function a property of window scope (possibly a bad thing to do). export class MapLayer extends HTMLElement { static get observedAttributes() { return ['src', 'label', 'checked', 'hidden', 'opacity']; } get src() { - return this.hasAttribute('src')?this.getAttribute('src'):''; + return this.hasAttribute('src') ? this.getAttribute('src') : ''; } set src(val) { @@ -15,17 +15,17 @@ export class MapLayer extends HTMLElement { } } get label() { - return this.hasAttribute('label')?this.getAttribute('label'):''; + return this.hasAttribute('label') ? this.getAttribute('label') : ''; } set label(val) { if (val) { - this.setAttribute('label',val); + this.setAttribute('label', val); } } get checked() { return this.hasAttribute('checked'); } - + set checked(val) { if (val) { this.setAttribute('checked', ''); @@ -33,7 +33,7 @@ export class MapLayer extends HTMLElement { this.removeAttribute('checked'); } } - + get hidden() { return this.hasAttribute('hidden'); } @@ -46,28 +46,28 @@ export class MapLayer extends HTMLElement { } } - get opacity(){ + get opacity() { return this._layer._container.style.opacity || this._layer.options.opacity; } set opacity(val) { - if(+val > 1 || +val < 0) return; + if (+val > 1 || +val < 0) return; this._layer.changeOpacity(val); } constructor() { // Always call super first in constructor - super(); + super(); } disconnectedCallback() { // console.log('Custom map element removed from page.'); // if the map-layer node is removed from the dom, the layer should be - // removed from the map and the layer control + // removed from the map and the layer control // this is moved up here so that the layer control doesn't respond // to the layer being removed with the _onLayerChange execution // that is set up in _attached: - if(this.hasAttribute("data-moving")) return; + if (this.hasAttribute('data-moving')) return; this._onRemove(); } @@ -87,70 +87,76 @@ export class MapLayer extends HTMLElement { } connectedCallback() { - if(this.hasAttribute("data-moving")) return; + if (this.hasAttribute('data-moving')) return; this._onAdd(); } _onAdd() { - if(this.getAttribute('src') && !this.shadowRoot) { - this.attachShadow({mode: 'open'}); + if (this.getAttribute('src') && !this.shadowRoot) { + this.attachShadow({ mode: 'open' }); } //creates listener that waits for createmap event, this allows for delayed builds of maps //this allows a safeguard for the case where loading a custom TCRS takes longer than loading mapml-viewer.js/web-map.js - this.parentNode.addEventListener('createmap', ()=>{ - this._ready(); - // if the map has been attached, set this layer up wrt Leaflet map - if (this.parentNode._map) { + this.parentNode.addEventListener( + 'createmap', + () => { + this._ready(); + // if the map has been attached, set this layer up wrt Leaflet map + if (this.parentNode._map) { this._attachedToMap(); - } - if (this._layerControl && !this.hidden) { - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } - }, {once:true}); //listener stops listening after event occurs once + } + if (this._layerControl && !this.hidden) { + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + } + }, + { once: true } + ); //listener stops listening after event occurs once //if map is already created then dispatch createmap event, allowing layer to be built - if(this.parentNode._map)this.parentNode.dispatchEvent(new CustomEvent('createmap')); + if (this.parentNode._map) + this.parentNode.dispatchEvent(new CustomEvent('createmap')); } adoptedCallback() { - // console.log('Custom map element moved to new page.'); + // console.log('Custom map element moved to new page.'); } attributeChangedCallback(name, oldValue, newValue) { - switch(name) { - case 'label': + switch (name) { + case 'label': if (oldValue !== newValue) { - this.dispatchEvent(new CustomEvent('labelchanged', {detail: - {target: this}})); + this.dispatchEvent( + new CustomEvent('labelchanged', { detail: { target: this } }) + ); } - break; - case 'checked': + break; + case 'checked': if (this._layer) { - if (typeof newValue === "string") { + if (typeof newValue === 'string') { this.parentElement._map.addLayer(this._layer); } else { this.parentElement._map.removeLayer(this._layer); } - this.dispatchEvent(new Event("change", { bubbles: true })); + this.dispatchEvent(new Event('change', { bubbles: true })); } - break; + break; case 'hidden': - var map = this.parentElement && this.parentElement._map; - if (map && this.parentElement.controls) { - if (typeof newValue === "string") { - if (this._layer) { - this.parentElement._layerControl.removeLayer(this._layer); - } + var map = this.parentElement && this.parentElement._map; + if (map && this.parentElement.controls) { + if (typeof newValue === 'string') { + if (this._layer) { + this.parentElement._layerControl.removeLayer(this._layer); + } } else { - this._layerControl = this.parentElement._layerControl; - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - this._validateDisabled(); + this._layerControl = this.parentElement._layerControl; + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + this._validateDisabled(); } - } - break; + } + break; case 'opacity': if (oldValue !== newValue && this._layer) { this.opacity = newValue; } - break; + break; case 'src': if (oldValue !== newValue && this._layer) { this._reload(); @@ -176,13 +182,16 @@ export class MapLayer extends HTMLElement { // currently only support a single link, don't care about type, lang etc. // TODO: add support for full LayerLegend object, and > one link. if (this._layer._legendUrl) { - this.legendLinks = - [{ type: 'application/octet-stream', - href: this._layer._legendUrl, - rel: 'legend', - lang: null, - hreflang: null, - sizes: null }]; + this.legendLinks = [ + { + type: 'application/octet-stream', + href: this._layer._legendUrl, + rel: 'legend', + lang: null, + hreflang: null, + sizes: null + } + ]; } if (this._layer._title) { this.label = this._layer._title; @@ -201,77 +210,113 @@ export class MapLayer extends HTMLElement { // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied // to MapML extent as metadata // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event - this.dispatchEvent(new CustomEvent('loadedmetadata', {detail: - {target: this}})); + this.dispatchEvent( + new CustomEvent('loadedmetadata', { detail: { target: this } }) + ); } else { - this.dispatchEvent(new CustomEvent('error', {detail: - {target: this}})); + this.dispatchEvent( + new CustomEvent('error', { detail: { target: this } }) + ); } } _validateDisabled() { setTimeout(() => { - let layer = this._layer, map = layer?._map; + let layer = this._layer, + map = layer?._map; if (map) { - let count = 0, total=0, layerTypes = ["_staticTileLayer","_imageLayer","_mapmlvectors","_templatedLayer"]; - if(layer.validProjection){ - for(let j = 0 ;j { - if (mapLink.hasAttribute('href')) { - mapLink.setAttribute('href', decodeURI((new URL(mapLink.attributes.href.value, this.baseURI ? this.baseURI : document.baseURI)).href)); - } - else if (mapLink.hasAttribute('tref')) { - mapLink.setAttribute('tref', decodeURI((new URL(mapLink.attributes.tref.value, this.baseURI ? this.baseURI : document.baseURI)).href)); + mapLink.setAttribute( + 'href', + decodeURI( + new URL( + mapLink.attributes.href.value, + this.baseURI ? this.baseURI : document.baseURI + ).href + ) + ); + } else if (mapLink.hasAttribute('tref')) { + mapLink.setAttribute( + 'tref', + decodeURI( + new URL( + mapLink.attributes.tref.value, + this.baseURI ? this.baseURI : document.baseURI + ).href + ) + ); } }); } @@ -285,10 +330,10 @@ export class MapLayer extends HTMLElement { _onLayerChange() { if (this._layer._map) { - // can't disable observers, have to set a flag telling it where - // the 'event' comes from: either the api or a user click/tap - // may not be necessary -> this._apiToggleChecked = false; - this.checked = this._layer._map.hasLayer(this._layer); + // can't disable observers, have to set a flag telling it where + // the 'event' comes from: either the api or a user click/tap + // may not be necessary -> this._apiToggleChecked = false; + this.checked = this._layer._map.hasLayer(this._layer); } } _ready() { @@ -299,25 +344,41 @@ export class MapLayer extends HTMLElement { // be parsed from the second parameter here // IE 11 did not have a value for this.baseURI for some reason var base = this.baseURI ? this.baseURI : document.baseURI; - let opacity_value = this.hasAttribute("opacity")?this.getAttribute("opacity"):"1.0"; - this._layer = M.mapMLLayer(this.src ? (new URL(this.src, base)).href: null, this, {mapprojection:this.parentElement._map.options.projection,opacity:opacity_value}); + let opacity_value = this.hasAttribute('opacity') + ? this.getAttribute('opacity') + : '1.0'; + this._layer = M.mapMLLayer( + this.src ? new URL(this.src, base).href : null, + this, + { + mapprojection: this.parentElement._map.options.projection, + opacity: opacity_value + } + ); this._layer.on('extentload', this._onLayerExtentLoad, this); this._setUpEvents(); } _attachedToMap() { // set i to the position of this layer element in the set of layers - var i = 0, position = 1; - for (var nodes = this.parentNode.children;i < nodes.length;i++) { - if (this.parentNode.children[i].nodeName === "LAYER-") { + var i = 0, + position = 1; + for (var nodes = this.parentNode.children; i < nodes.length; i++) { + if (this.parentNode.children[i].nodeName === 'LAYER-') { if (this.parentNode.children[i] === this) { position = i + 1; } else if (this.parentNode.children[i]._layer) { - this.parentNode.children[i]._layer.setZIndex(i+1); + this.parentNode.children[i]._layer.setZIndex(i + 1); } } } - var proj = this.parentNode.projection ? this.parentNode.projection : "OSMTILE"; - L.setOptions(this._layer, {zIndex: position, mapprojection: proj, opacity: window.getComputedStyle(this).opacity}); + var proj = this.parentNode.projection + ? this.parentNode.projection + : 'OSMTILE'; + L.setOptions(this._layer, { + zIndex: position, + mapprojection: proj, + opacity: window.getComputedStyle(this).opacity + }); // make sure the Leaflet layer has a reference to the map this._layer._map = this.parentNode._map; // notify the layer that it is attached to a map (layer._map) @@ -326,12 +387,12 @@ export class MapLayer extends HTMLElement { if (this.checked) { this._layer.addTo(this._layer._map); } - + // add the handler which toggles the 'checked' property based on the // user checking/unchecking the layer from the layer control // this must be done *after* the layer is actually added to the map - this._layer.on('add remove', this._onLayerChange, this); - this._layer.on('add remove extentload', this._validateDisabled, this); + this._layer.on('add remove', this._onLayerChange, this); + this._layer.on('add remove extentload', this._validateDisabled, this); // if controls option is enabled, insert the layer into the overlays array if (this.parentNode._layerControl && !this.hidden) { @@ -340,11 +401,11 @@ export class MapLayer extends HTMLElement { } // toggle the this.disabled attribute depending on whether the layer // is: same prj as map, within view/zoom of map - this._layer._map.on('moveend', this._validateDisabled, this); + this._layer._map.on('moveend', this._validateDisabled, this); this._layer._map.on('checkdisabled', this._validateDisabled, this); // this is necessary to get the layer control to compare the layer // extents with the map extent & zoom, but it needs to be rethought TODO - // for one thing, layers which are checked by the author before + // for one thing, layers which are checked by the author before // adding to the map are displayed despite that they are not visible // See issue #26 // this._layer._map.fire('moveend'); @@ -355,48 +416,68 @@ export class MapLayer extends HTMLElement { } } _setUpEvents() { - this._layer.on('loadstart', - function () { - this.dispatchEvent(new CustomEvent('loadstart', {detail: - {target: this}})); - }, this); - this._layer.on('changestyle', - function(e) { - this.src = e.src; - this.dispatchEvent(new CustomEvent('changestyle', {detail: - {target: this}})); - },this); - this._layer.on('changeprojection', - function(e) { - this.src = e.href; - this.dispatchEvent(new CustomEvent('changeprojection', {detail: - {target: this}})); - },this); + this._layer.on( + 'loadstart', + function () { + this.dispatchEvent( + new CustomEvent('loadstart', { detail: { target: this } }) + ); + }, + this + ); + this._layer.on( + 'changestyle', + function (e) { + this.src = e.src; + this.dispatchEvent( + new CustomEvent('changestyle', { detail: { target: this } }) + ); + }, + this + ); + this._layer.on( + 'changeprojection', + function (e) { + this.src = e.href; + this.dispatchEvent( + new CustomEvent('changeprojection', { detail: { target: this } }) + ); + }, + this + ); } zoomTo() { - if(!this.extent) return; + if (!this.extent) return; let map = this._layer._map, - tL = this.extent.topLeft.pcrs, - bR = this.extent.bottomRight.pcrs, - layerBounds = L.bounds(L.point(tL.horizontal, tL.vertical), L.point(bR.horizontal, bR.vertical)), - center = map.options.crs.unproject(layerBounds.getCenter(true)); + tL = this.extent.topLeft.pcrs, + bR = this.extent.bottomRight.pcrs, + layerBounds = L.bounds( + L.point(tL.horizontal, tL.vertical), + L.point(bR.horizontal, bR.vertical) + ), + center = map.options.crs.unproject(layerBounds.getCenter(true)); - let maxZoom = this.extent.zoom.maxZoom, - minZoom = this.extent.zoom.minZoom; - map.setView(center, M.getMaxZoom(layerBounds, map, minZoom, maxZoom), {animate: false}); + let maxZoom = this.extent.zoom.maxZoom, + minZoom = this.extent.zoom.minZoom; + map.setView(center, M.getMaxZoom(layerBounds, map, minZoom, maxZoom), { + animate: false + }); } - mapml2geojson(options = {}){ + mapml2geojson(options = {}) { return M.mapml2geojson(this, options); } pasteFeature(feature) { - switch(typeof feature) { - case "string": + switch (typeof feature) { + case 'string': feature.trim(); - if (feature.slice(0,12) === "") { - this.insertAdjacentHTML("beforeend", feature); + if ( + feature.slice(0, 12) === '' + ) { + this.insertAdjacentHTML('beforeend', feature); } break; - case "object": + case 'object': if (feature.nodeName.toUpperCase() === 'MAP-FEATURE') { this.appendChild(feature); } diff --git a/src/map-a.js b/src/map-a.js index 38a1260f8..8b2488073 100644 --- a/src/map-a.js +++ b/src/map-a.js @@ -1,75 +1,70 @@ export class MapA extends HTMLElement { - static get observedAttributes() { - return ['href','target','type','inplace']; - } - get href() { - return this.hasAttribute('href') ? this.getAttribute('href') : ""; - } - set href(url) { - this.href = url; - } - get target() { - return this.hasAttribute('target') ? this.getAttribute('target') : ""; - } - set target(val) { - this.setAttribute('target', val); - } - get type() { - return this.hasAttribute('type') ? this.getAttribute('type') : ""; - } - set type(val) { - this.setAttribute('type', val); - } - get inplace() { - return this.hasAttribute('inplace') ? this.getAttribute('inplace') : ""; - } - set inplace(val) { - const hasInplace = Boolean(val); - if (hasInplace) { - this.setAttribute('inpalce', ''); - } else { - this.removeAttribute('inplace'); - } + static get observedAttributes() { + return ['href', 'target', 'type', 'inplace']; + } + get href() { + return this.hasAttribute('href') ? this.getAttribute('href') : ''; + } + set href(url) { + this.href = url; + } + get target() { + return this.hasAttribute('target') ? this.getAttribute('target') : ''; + } + set target(val) { + this.setAttribute('target', val); + } + get type() { + return this.hasAttribute('type') ? this.getAttribute('type') : ''; + } + set type(val) { + this.setAttribute('type', val); + } + get inplace() { + return this.hasAttribute('inplace') ? this.getAttribute('inplace') : ''; + } + set inplace(val) { + const hasInplace = Boolean(val); + if (hasInplace) { + this.setAttribute('inpalce', ''); + } else { + this.removeAttribute('inplace'); } + } - - attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'href': { - if (oldValue !== newValue) { - // handle side effects - } - break; } - case 'target': { - if (oldValue !== newValue) { - // handle side effects - } - break; + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'href': { + if (oldValue !== newValue) { + // handle side effects } - case 'type': { - if (oldValue !== newValue) { - // handle side effects - } - break; + break; + } + case 'target': { + if (oldValue !== newValue) { + // handle side effects } - case 'inplace': { - if (oldValue !== newValue) { - // handle side effects - } - break; + break; + } + case 'type': { + if (oldValue !== newValue) { + // handle side effects } + break; + } + case 'inplace': { + if (oldValue !== newValue) { + // handle side effects + } + break; } } - constructor() { - // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - - } } - window.customElements.define('map-a', MapA); - \ No newline at end of file + constructor() { + // Always call super first in constructor + super(); + } + connectedCallback() {} + disconnectedCallback() {} +} +window.customElements.define('map-a', MapA); diff --git a/src/map-area.js b/src/map-area.js index eab576de6..a50c32af4 100644 --- a/src/map-area.js +++ b/src/map-area.js @@ -1,36 +1,36 @@ -import './leaflet.js'; // a lightly modified version of Leaflet for use as browser module -import './mapml.js'; // refactored URI usage, replaced with URL standard +import './leaflet.js'; // a lightly modified version of Leaflet for use as browser module +import './mapml.js'; // refactored URI usage, replaced with URL standard export class MapArea extends HTMLAreaElement { static get observedAttributes() { - return ['coords','alt','href','shape','rel','type','target']; + return ['coords', 'alt', 'href', 'shape', 'rel', 'type', 'target']; } - // see comments below regarding attributeChangedCallback vs. getter/setter + // see comments below regarding attributeChangedCallback vs. getter/setter // usage. Effectively, the user of the element must use the property, not // the getAttribute/setAttribute/removeAttribute DOM API, because the latter // calls don't result in the getter/setter being called (so you have to use // the getter/setter directly) get alt() { - return this.hasAttribute('alt') ? this.getAttribute('alt') : ""; + return this.hasAttribute('alt') ? this.getAttribute('alt') : ''; } set alt(value) { this.setAttribute('controls', value); } get coords() { - return this.hasAttribute('coords') ? this.getAttribute('coords') : ""; + return this.hasAttribute('coords') ? this.getAttribute('coords') : ''; } set coords(coordinates) { - // what to do. Probably replace the feature with a new one, without changing + // what to do. Probably replace the feature with a new one, without changing // anything else... } get href() { - return this.hasAttribute('href') ? this.getAttribute('href') : ""; + return this.hasAttribute('href') ? this.getAttribute('href') : ''; } set href(url) { this.href = url; } get shape() { - return this.hasAttribute('shape') ? this.getAttribute('shape') : "default"; + return this.hasAttribute('shape') ? this.getAttribute('shape') : 'default'; } set shape(shape) { shape = shape.toLowerCase(); @@ -40,31 +40,29 @@ export class MapArea extends HTMLAreaElement { } } get rel() { - return this.hasAttribute('rel') ? this.getAttribute('rel') : ""; + return this.hasAttribute('rel') ? this.getAttribute('rel') : ''; } set rel(rel) { this.rel = rel; } get type() { - return this.hasAttribute('type') ? this.getAttribute('type') : ""; + return this.hasAttribute('type') ? this.getAttribute('type') : ''; } set type(type) { this.type = type; } get target() { - return this.hasAttribute('target') ? this.getAttribute('target') : ""; + return this.hasAttribute('target') ? this.getAttribute('target') : ''; } constructor() { // Always call super first in constructor super(); } - attributeChangedCallback(name, oldValue, newValue) { - - } + attributeChangedCallback(name, oldValue, newValue) {} connectedCallback() { // if the map has been attached, set this layer up wrt Leaflet map if (this.parentElement._map) { - this._attachedToMap(); + this._attachedToMap(); } } _attachedToMap() { @@ -78,44 +76,51 @@ export class MapArea extends HTMLAreaElement { // the img might have been scaled by CSS. // compute the style properties to be applied to the feature var options = this._styleToPathOptions(window.getComputedStyle(this)), - points = this.coords ? this._coordsToArray(this.coords): null; + points = this.coords ? this._coordsToArray(this.coords) : null; // scale points if the poster exists because responsive areas if (points && this.parentElement.poster) { - var worig = this.parentElement.poster.width, - wresp = this.parentElement.width, - wadjstmnt = (worig - wresp)/2, - horig = this.parentElement.poster.height, - hresp = this.parentElement.height, - hadjstmnt = (horig - hresp)/2; - for (var i = 0; i< points.length;i++) { + var worig = this.parentElement.poster.width, + wresp = this.parentElement.width, + wadjstmnt = (worig - wresp) / 2, + horig = this.parentElement.poster.height, + hresp = this.parentElement.height, + hadjstmnt = (horig - hresp) / 2; + for (var i = 0; i < points.length; i++) { points[i][0] = points[i][0] - wadjstmnt; points[i][1] = points[i][1] - hadjstmnt; } } if (this.shape === 'circle') { - var pixelRadius = parseInt(this.coords.split(",")[2]), - pointOnCirc = L.point(points[0]).add(L.point(0,pixelRadius)), - latLngOnCirc = map.containerPointToLatLng(pointOnCirc), - latLngCenter = map.containerPointToLatLng(points[0]), - radiusInMeters = map.distance(latLngCenter, latLngOnCirc); - this._feature = L.circle(latLngCenter, radiusInMeters, options).addTo(map); + var pixelRadius = parseInt(this.coords.split(',')[2]), + pointOnCirc = L.point(points[0]).add(L.point(0, pixelRadius)), + latLngOnCirc = map.containerPointToLatLng(pointOnCirc), + latLngCenter = map.containerPointToLatLng(points[0]), + radiusInMeters = map.distance(latLngCenter, latLngOnCirc); + this._feature = L.circle(latLngCenter, radiusInMeters, options).addTo( + map + ); } else if (!this.shape || this.shape === 'rect') { - var bounds = L.latLngBounds(map.containerPointToLatLng(points[0]), map.containerPointToLatLng(points[1])); + var bounds = L.latLngBounds( + map.containerPointToLatLng(points[0]), + map.containerPointToLatLng(points[1]) + ); this._feature = L.rectangle(bounds, options).addTo(map); } else if (this.shape === 'poly') { - this._feature = L.polygon(this._pointsToLatLngs(points),options).addTo(map); + this._feature = L.polygon(this._pointsToLatLngs(points), options).addTo( + map + ); } else { // whole initial area of map is a hyperlink - this._feature = L.rectangle(map.getBounds(),options).addTo(map); + this._feature = L.rectangle(map.getBounds(), options).addTo(map); } if (this.alt) { // other Leaflet features are implemented via SVG. SVG displays tooltips // based on the graphics child element. var title = L.SVG.create('title'), - titleText = document.createTextNode(this.alt); - title.appendChild(titleText); - this._feature._path.appendChild(title); + titleText = document.createTextNode(this.alt); + title.appendChild(titleText); + this._feature._path.appendChild(title); } if (this.href) { // conditionally act on click on an area link. If no link it should be an @@ -123,18 +128,30 @@ export class MapArea extends HTMLAreaElement { // implementation, we could actually use an image map replete with area // children which would provide the linking / cursor change behaviours // that are familiar to HTML authors versed in image maps. - this._feature.on('click', function() {if (this.href) {window.open(this.href);}}, this); + this._feature.on( + 'click', + function () { + if (this.href) { + window.open(this.href); + } + }, + this + ); } } - } + } disconnectedCallback() { this._map.removeLayer(this._feature); delete this._feature; } _coordsToArray(containerPoints) { // returns an array of arrays of coordinate pairs _coordsToArray("1,2,3,4") -> [[1,2],[3,4]] - for (var i=1, points = [], coords = containerPoints.split(",");i tags and update to aria-label - let mapcaption = this.parentElement.querySelector('map-caption').textContent; + constructor() { + super(); + } - this.observer = new MutationObserver(() => { - let mapcaptionupdate = this.parentElement.querySelector('map-caption').textContent; + // called when element is inserted into DOM (setup code) + connectedCallback() { + if ( + this.parentElement.nodeName === 'MAPML-VIEWER' || + this.parentElement.nodeName === 'MAP' + ) { + // calls MutationObserver; needed to observe changes to content between tags and update to aria-label + let mapcaption = + this.parentElement.querySelector('map-caption').textContent; - if (mapcaptionupdate !== mapcaption) { - this.parentElement.setAttribute('aria-label', this.parentElement.querySelector('map-caption').textContent); - } - }); + this.observer = new MutationObserver(() => { + let mapcaptionupdate = + this.parentElement.querySelector('map-caption').textContent; - this.observer.observe(this, { - characterData: true, - subtree: true, - attributes: true, - childList: true - }); - - // don't change aria-label if one already exists from user (checks when element is first created) - if (!this.parentElement.hasAttribute('aria-label')) { - const ariaLabel = this.textContent; - this.parentElement.setAttribute('aria-label', ariaLabel); - } + if (mapcaptionupdate !== mapcaption) { + this.parentElement.setAttribute( + 'aria-label', + this.parentElement.querySelector('map-caption').textContent + ); } + }); + + this.observer.observe(this, { + characterData: true, + subtree: true, + attributes: true, + childList: true + }); + + // don't change aria-label if one already exists from user (checks when element is first created) + if (!this.parentElement.hasAttribute('aria-label')) { + const ariaLabel = this.textContent; + this.parentElement.setAttribute('aria-label', ariaLabel); + } } - disconnectedCallback() { - this.observer.disconnect(); - } + } + disconnectedCallback() { + this.observer.disconnect(); + } } diff --git a/src/map-extent.js b/src/map-extent.js index a65056e5e..bb81f376e 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -1,21 +1,21 @@ export class MapExtent extends HTMLElement { static get observedAttributes() { - return ['units','checked','label','opacity']; + return ['units', 'checked', 'label', 'opacity']; } get units() { - return this.getAttribute('units'); + return this.getAttribute('units'); } set units(val) { // built in support for OSMTILE, CBMTILE, WGS84 and APSTILE - if (['OSMTILE','CBMTILE','WGS84','APSTILE'].includes(val)) { - this.setAttribute('units',val); + if (['OSMTILE', 'CBMTILE', 'WGS84', 'APSTILE'].includes(val)) { + this.setAttribute('units', val); } // else need to check with the mapml-viewer element if the custom projection is defined } get checked() { return this.hasAttribute('checked'); } - + set checked(val) { if (val) { this.setAttribute('checked', ''); @@ -24,34 +24,34 @@ export class MapExtent extends HTMLElement { } } get label() { - return this.hasAttribute('label')?this.getAttribute('label'):''; + return this.hasAttribute('label') ? this.getAttribute('label') : ''; } set label(val) { if (val) { - this.setAttribute('label',val); + this.setAttribute('label', val); } } - get opacity(){ + get opacity() { return this._opacity; } set opacity(val) { - if(+val > 1 || +val < 0) return; + if (+val > 1 || +val < 0) return; this.setAttribute('opacity', val); } attributeChangedCallback(name, oldValue, newValue) { - switch(name) { + switch (name) { case 'units': if (oldValue !== newValue) { // handle side effects } break; - case 'label': + case 'label': if (oldValue !== newValue) { // handle side effects } break; - case 'checked': + case 'checked': if (oldValue !== newValue) { // handle side effects } @@ -65,16 +65,22 @@ export class MapExtent extends HTMLElement { } constructor() { // Always call super first in constructor - super(); + super(); } connectedCallback() { - if(this.querySelector('map-link[rel=query], map-link[rel=features]') && !this.shadowRoot) { - this.attachShadow({mode: 'open'}); + if ( + this.querySelector('map-link[rel=query], map-link[rel=features]') && + !this.shadowRoot + ) { + this.attachShadow({ mode: 'open' }); } - let parentLayer = this.parentNode.nodeName.toUpperCase() === "LAYER-" ? this.parentNode : this.parentNode.host; + let parentLayer = + this.parentNode.nodeName.toUpperCase() === 'LAYER-' + ? this.parentNode + : this.parentNode.host; if (!parentLayer._layer) { // for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point, - // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created + // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created // the event will be dispatched after defineCustomProjection > projection setter // should wait until MapMLLayer is built parentLayer.parentNode.addEventListener('createmap', (e) => { @@ -84,7 +90,5 @@ export class MapExtent extends HTMLElement { this._layer = parentLayer._layer; } } - disconnectedCallback() { - - } -} \ No newline at end of file + disconnectedCallback() {} +} diff --git a/src/map-feature.js b/src/map-feature.js index 66939394d..416886b8f 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -1,550 +1,655 @@ export class MapFeature extends HTMLElement { - static get observedAttributes() { - return ['zoom', 'onfocus', 'onclick', 'onblur']; - } + static get observedAttributes() { + return ['zoom', 'onfocus', 'onclick', 'onblur']; + } - get zoom() { - return +(this.hasAttribute("zoom") ? this.getAttribute("zoom") : 0); - } + get zoom() { + return +(this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0); + } - set zoom(val) { - var parsedVal = parseInt(val,10); - if (!isNaN(parsedVal) && (parsedVal >= this.min && parsedVal <= this.max)) { - this.setAttribute('zoom', parsedVal); - } + set zoom(val) { + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal) && parsedVal >= this.min && parsedVal <= this.max) { + this.setAttribute('zoom', parsedVal); } + } - get min() { - // fallback: the minimum zoom bound of layer- element - return +(this.hasAttribute("min") ? this.getAttribute("min") : this._layer._layerEl.extent.zoom.minZoom); - } + get min() { + // fallback: the minimum zoom bound of layer- element + return +(this.hasAttribute('min') + ? this.getAttribute('min') + : this._layer._layerEl.extent.zoom.minZoom); + } - set min(val) { - var parsedVal = parseInt(val,10); - if (!isNaN(parsedVal)) { - if (parsedVal >= this._layer._layerEl.extent.zoom.minZoom && parsedVal <= this._layer._layerEl.extent.zoom.maxZoom) { - this.setAttribute('min', parsedVal); - } else { - this.setAttribute('min', this._layer._layerEl.extent.zoom.minZoom); - } + set min(val) { + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal)) { + if ( + parsedVal >= this._layer._layerEl.extent.zoom.minZoom && + parsedVal <= this._layer._layerEl.extent.zoom.maxZoom + ) { + this.setAttribute('min', parsedVal); + } else { + this.setAttribute('min', this._layer._layerEl.extent.zoom.minZoom); } } + } - get max() { - // fallback: the maximum zoom bound of layer- element - return +(this.hasAttribute("max") ? this.getAttribute("max") : this._layer._layerEl.extent.zoom.maxZoom); - } + get max() { + // fallback: the maximum zoom bound of layer- element + return +(this.hasAttribute('max') + ? this.getAttribute('max') + : this._layer._layerEl.extent.zoom.maxZoom); + } - set max(val) { - var parsedVal = parseInt(val,10); - if (!isNaN(parsedVal)) { - if (parsedVal >= this._layer._layerEl.extent.zoom.minZoom && parsedVal <= this._layer._layerEl.extent.zoom.maxZoom) { - this.setAttribute('max', parsedVal); - } else { - this.setAttribute('max', this._layer._layerEl.extent.zoom.maxZoom); - } + set max(val) { + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal)) { + if ( + parsedVal >= this._layer._layerEl.extent.zoom.minZoom && + parsedVal <= this._layer._layerEl.extent.zoom.maxZoom + ) { + this.setAttribute('max', parsedVal); + } else { + this.setAttribute('max', this._layer._layerEl.extent.zoom.maxZoom); } } + } - get extent() { - if (this.isConnected) { - // if the feature extent is the first time to be calculated or the feature extent is changed (by changing - // the innertext of map-coordinates), then calculate feature extent by invoking the getFeatureExtent function - if (!this._getFeatureExtent) {this._getFeatureExtent = this._memoizeExtent();} - return this._getFeatureExtent(); + get extent() { + if (this.isConnected) { + // if the feature extent is the first time to be calculated or the feature extent is changed (by changing + // the innertext of map-coordinates), then calculate feature extent by invoking the getFeatureExtent function + if (!this._getFeatureExtent) { + this._getFeatureExtent = this._memoizeExtent(); } + return this._getFeatureExtent(); } + } - attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'zoom': { - if (oldValue !== newValue && this._layer) { - let layer = this._layer, + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'zoom': { + if (oldValue !== newValue && this._layer) { + let layer = this._layer, layerEl = layer._layerEl, mapmlvectors = layer._mapmlvectors; - // if the vector layer only has static features, should update zoom bounds when zoom attribute is changed - if (mapmlvectors?._staticFeature) { - this._removeInFeatureList(oldValue); - let native = this._getNativeZoomAndCS(layer._content); - mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds(layerEl.shadowRoot || layerEl, native.zoom); - } - this._removeFeature(); - this._updateFeature(); + // if the vector layer only has static features, should update zoom bounds when zoom attribute is changed + if (mapmlvectors?._staticFeature) { + this._removeInFeatureList(oldValue); + let native = this._getNativeZoomAndCS(layer._content); + mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds( + layerEl.shadowRoot || layerEl, + native.zoom + ); } - break; + this._removeFeature(); + this._updateFeature(); } - case 'onfocus': - case 'onclick': - case 'onblur': - if (this._groupEl) { - // "synchronize" the onevent properties (i.e. onfocus, onclick, onblur) - // between the mapFeature and its associated element - this._groupEl[name] = this[name].bind(this._groupEl); - break; - } + break; } + case 'onfocus': + case 'onclick': + case 'onblur': + if (this._groupEl) { + // "synchronize" the onevent properties (i.e. onfocus, onclick, onblur) + // between the mapFeature and its associated element + this._groupEl[name] = this[name].bind(this._groupEl); + break; + } } + } - constructor() { - // Always call super first in constructor - super(); - } + constructor() { + // Always call super first in constructor + super(); + } - connectedCallback() { - // if mapFeature element is not connected to layer- or layer-'s shadowroot, - // or the parent layer- element has a "data-moving" attribute - if ((this.parentNode.nodeType !== document.DOCUMENT_FRAGMENT_NODE && this.parentNode.nodeName.toLowerCase() !== 'layer-') || - (this.parentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE && this.parentNode.host.hasAttribute('data-moving')) || - (this.parentNode.nodeName.toLowerCase() === 'layer-' && this.parentNode.hasAttribute('data-moving'))) { + connectedCallback() { + // if mapFeature element is not connected to layer- or layer-'s shadowroot, + // or the parent layer- element has a "data-moving" attribute + if ( + (this.parentNode.nodeType !== document.DOCUMENT_FRAGMENT_NODE && + this.parentNode.nodeName.toLowerCase() !== 'layer-') || + (this.parentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE && + this.parentNode.host.hasAttribute('data-moving')) || + (this.parentNode.nodeName.toLowerCase() === 'layer-' && + this.parentNode.hasAttribute('data-moving')) + ) { + return; + } + // set up the map-feature object properties + this._addFeature(); + // use observer to monitor the changes in mapFeature's subtree + // (i.e. map-properties, map-featurecaption, map-coordinates) + this._observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + // the attributes changes of element should be handled by attributeChangedCallback() + if (mutation.type === 'attributes' && mutation.target === this) { return; - } - // set up the map-feature object properties - this._addFeature(); - // use observer to monitor the changes in mapFeature's subtree - // (i.e. map-properties, map-featurecaption, map-coordinates) - this._observer = new MutationObserver((mutationList) => { - for (let mutation of mutationList) { - // the attributes changes of element should be handled by attributeChangedCallback() - if (mutation.type === 'attributes' && mutation.target === this) { - return; - } - // re-render feature if there is any observed change - this._removeFeature(); - this._updateFeature(); } - }); - this._observer.observe(this, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - characterData: true - }); - } - - disconnectedCallback() { - if(this._layer._layerEl.hasAttribute("data-moving")) return; - this._removeFeature(); - this._observer.disconnect(); - } - - _removeFeature() { - // if the el is disconnected - // the el has already got removed at this point - if (this._groupEl?.isConnected) { - this._groupEl.remove(); + // re-render feature if there is any observed change + this._removeFeature(); + this._updateFeature(); } - // if the el has already been disconnected, - // then _map.removeLayer(layerEl._layer) has already been invoked (inside layerEl.disconnectedCallback()) - // this._featureGroup has already got removed at this point - if (this._featureGroup?._map){ - this._featureGroup._map.removeLayer(this._featureGroup); - let mapmlvectors = this._layer._mapmlvectors; - if (mapmlvectors) { - if (mapmlvectors._staticFeature) { - if (mapmlvectors._features[this.zoom]) { - this._removeInFeatureList(this.zoom); - } - let container = this._layer.shadowRoot || this._layer._layerEl; - // update zoom bounds of vector layer - mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds(container, this._getNativeZoomAndCS(this._layer._content).zoom); + }); + this._observer.observe(this, { + childList: true, + subtree: true, + attributes: true, + attributeOldValue: true, + characterData: true + }); + } + + disconnectedCallback() { + if (this._layer._layerEl.hasAttribute('data-moving')) return; + this._removeFeature(); + this._observer.disconnect(); + } + + _removeFeature() { + // if the el is disconnected + // the el has already got removed at this point + if (this._groupEl?.isConnected) { + this._groupEl.remove(); + } + // if the el has already been disconnected, + // then _map.removeLayer(layerEl._layer) has already been invoked (inside layerEl.disconnectedCallback()) + // this._featureGroup has already got removed at this point + if (this._featureGroup?._map) { + this._featureGroup._map.removeLayer(this._featureGroup); + let mapmlvectors = this._layer._mapmlvectors; + if (mapmlvectors) { + if (mapmlvectors._staticFeature) { + if (mapmlvectors._features[this.zoom]) { + this._removeInFeatureList(this.zoom); } - mapmlvectors.options.properties = null; - delete mapmlvectors._layers[this._featureGroup._leaflet_id]; + let container = this._layer.shadowRoot || this._layer._layerEl; + // update zoom bounds of vector layer + mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds( + container, + this._getNativeZoomAndCS(this._layer._content).zoom + ); } + mapmlvectors.options.properties = null; + delete mapmlvectors._layers[this._featureGroup._leaflet_id]; } - delete this._featureGroup; - delete this._groupEl; - // ensure that feature extent can be re-calculated everytime that map-feature element is updated / re-added - if (this._getFeatureExtent) delete this._getFeatureExtent; } + delete this._featureGroup; + delete this._groupEl; + // ensure that feature extent can be re-calculated everytime that map-feature element is updated / re-added + if (this._getFeatureExtent) delete this._getFeatureExtent; + } - _addFeature() { - let parentEl = (this.parentNode.nodeName.toUpperCase() === "LAYER-" || - this.parentNode.nodeName.toUpperCase() === "MAP-EXTENT" ? - this.parentNode : this.parentNode.host); + _addFeature() { + let parentEl = + this.parentNode.nodeName.toUpperCase() === 'LAYER-' || + this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT' + ? this.parentNode + : this.parentNode.host; - // arrow function is not hoisted, define before use - var _attachedToMap = (e) => { - if (!parentEl._layer._map) { - // if the parent layer- el has not yet added to the map (i.e. not yet rendered), wait until it is added - this._layer.once('attached', function () { + // arrow function is not hoisted, define before use + var _attachedToMap = (e) => { + if (!parentEl._layer._map) { + // if the parent layer- el has not yet added to the map (i.e. not yet rendered), wait until it is added + this._layer.once( + 'attached', + function () { this._map = this._layer._map; - }, this); - } else { - this._map = this._layer._map; - } - // "synchronize" the event handlers between map-feature and - if (!this.querySelector('map-geometry')) return; - if (!this._layer._mapmlvectors) { - // if vector layer has not yet created (i.e. the layer- is not yet rendered on the map / layer is empty) - let layerEl = this._layer._layerEl; - this._layer.once('add', this._setUpEvents, this); - if (!layerEl.querySelector('map-extent, map-tile') && !layerEl.hasAttribute('src') && layerEl.querySelectorAll('map-feature').length === 1) { - // if the map-feature is added to an empty layer, fire extentload to create vector layer - // must re-run _initialize of MapMLLayer.js to re-set layer._extent (layer._extent is null for an empty layer) - this._layer._initialize(layerEl); - this._layer.fire('extentload'); - } - return; - } else if (!this._featureGroup) { - // if the map-feature el or its subtree is updated - // this._featureGroup has been free in this._removeFeature() - this._updateFeature(); - } else { - this._setUpEvents(); + }, + this + ); + } else { + this._map = this._layer._map; + } + // "synchronize" the event handlers between map-feature and + if (!this.querySelector('map-geometry')) return; + if (!this._layer._mapmlvectors) { + // if vector layer has not yet created (i.e. the layer- is not yet rendered on the map / layer is empty) + let layerEl = this._layer._layerEl; + this._layer.once('add', this._setUpEvents, this); + if ( + !layerEl.querySelector('map-extent, map-tile') && + !layerEl.hasAttribute('src') && + layerEl.querySelectorAll('map-feature').length === 1 + ) { + // if the map-feature is added to an empty layer, fire extentload to create vector layer + // must re-run _initialize of MapMLLayer.js to re-set layer._extent (layer._extent is null for an empty layer) + this._layer._initialize(layerEl); + this._layer.fire('extentload'); } - }; - - if (!parentEl._layer) { - // for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point, - // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created - // the event will be dispatched after defineCustomProjection > projection setter - // should wait until MapMLLayer is built - let parentLayer = parentEl.nodeName.toUpperCase() === "LAYER-" ? parentEl : (parentEl.parentElement || parentEl.parentNode.host); - parentLayer.parentNode.addEventListener('createmap', (e) => { - this._layer = parentLayer._layer; - _attachedToMap(); - }); + return; + } else if (!this._featureGroup) { + // if the map-feature el or its subtree is updated + // this._featureGroup has been free in this._removeFeature() + this._updateFeature(); } else { - this._layer = parentEl._layer; - _attachedToMap(); + this._setUpEvents(); } + }; + + if (!parentEl._layer) { + // for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point, + // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created + // the event will be dispatched after defineCustomProjection > projection setter + // should wait until MapMLLayer is built + let parentLayer = + parentEl.nodeName.toUpperCase() === 'LAYER-' + ? parentEl + : parentEl.parentElement || parentEl.parentNode.host; + parentLayer.parentNode.addEventListener('createmap', (e) => { + this._layer = parentLayer._layer; + _attachedToMap(); + }); + } else { + this._layer = parentEl._layer; + _attachedToMap(); } + } - _updateFeature() { - let mapmlvectors = this._layer._mapmlvectors; - // if the parent layer has not yet rendered on the map - if (!mapmlvectors) return; - // if the is not removed, then regenerate featureGroup and update the mapmlvectors accordingly - let native = this._getNativeZoomAndCS(this._layer._content); - this._featureGroup = mapmlvectors.addData(this, native.cs, native.zoom); - mapmlvectors._layers[this._featureGroup._leaflet_id] = this._featureGroup; - this._groupEl = this._featureGroup.options.group; - if (mapmlvectors._staticFeature) { - let container = this._layer.shadowRoot || this._layer._layerEl; - // update zoom bounds of vector layer - mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds(container, this._getNativeZoomAndCS(this._layer._content).zoom); - // add feature layers to map - mapmlvectors._resetFeatures(); - // update map's zoom limit - this._map._addZoomLimit(mapmlvectors); - L.extend(mapmlvectors.options, mapmlvectors.zoomBounds); - } - this._setUpEvents(); + _updateFeature() { + let mapmlvectors = this._layer._mapmlvectors; + // if the parent layer has not yet rendered on the map + if (!mapmlvectors) return; + // if the is not removed, then regenerate featureGroup and update the mapmlvectors accordingly + let native = this._getNativeZoomAndCS(this._layer._content); + this._featureGroup = mapmlvectors.addData(this, native.cs, native.zoom); + mapmlvectors._layers[this._featureGroup._leaflet_id] = this._featureGroup; + this._groupEl = this._featureGroup.options.group; + if (mapmlvectors._staticFeature) { + let container = this._layer.shadowRoot || this._layer._layerEl; + // update zoom bounds of vector layer + mapmlvectors.zoomBounds = mapmlvectors._getZoomBounds( + container, + this._getNativeZoomAndCS(this._layer._content).zoom + ); + // add feature layers to map + mapmlvectors._resetFeatures(); + // update map's zoom limit + this._map._addZoomLimit(mapmlvectors); + L.extend(mapmlvectors.options, mapmlvectors.zoomBounds); } + this._setUpEvents(); + } - _setUpEvents() { - ['click','focus','blur'].forEach(name => { - // onevent properties & onevent attributes - if (this[`on${name}`] && typeof this[`on${name}`] === "function") { - this._groupEl[`on${name}`] = this[`on${name}`]; + _setUpEvents() { + ['click', 'focus', 'blur'].forEach((name) => { + // onevent properties & onevent attributes + if (this[`on${name}`] && typeof this[`on${name}`] === 'function') { + this._groupEl[`on${name}`] = this[`on${name}`]; + } + // handle event handlers set via addEventlistener + // for HTMLElement + // when is clicked / focused / blurred + // should dispatch the click / focus / blur event listener on **linked HTMLFeatureElements** + this._groupEl.addEventListener(name, (e) => { + // this === mapFeature as arrow function does not have their own "this" pointer + // store onEvent handler of mapFeature if there is any to ensure that it will not be re-triggered when the cloned mouseevent is dispatched + // so that only the event handlers set on HTMLFeatureElement via addEventListener method will be triggered + const handler = this[`on${name}`]; // a deep copy, var handler will not change when this.onevent is set to null (i.e. store the onevent property) + this[`on${name}`] = null; + if (name === 'click') { + // dispatch a cloned mouseevent to trigger the click event handlers set on HTMLFeatureElement + this.dispatchEvent(new PointerEvent(name, { ...e })); + } else { + this.dispatchEvent(new FocusEvent(name, { ...e })); } - // handle event handlers set via addEventlistener - // for HTMLElement - // when is clicked / focused / blurred - // should dispatch the click / focus / blur event listener on **linked HTMLFeatureElements** - this._groupEl.addEventListener(name, (e) => { - // this === mapFeature as arrow function does not have their own "this" pointer - // store onEvent handler of mapFeature if there is any to ensure that it will not be re-triggered when the cloned mouseevent is dispatched - // so that only the event handlers set on HTMLFeatureElement via addEventListener method will be triggered - const handler = this[`on${name}`]; // a deep copy, var handler will not change when this.onevent is set to null (i.e. store the onevent property) - this[`on${name}`] = null; - if (name === 'click') { - // dispatch a cloned mouseevent to trigger the click event handlers set on HTMLFeatureElement - this.dispatchEvent(new PointerEvent (name, {...e})); - } else { - this.dispatchEvent(new FocusEvent (name, {...e})); - } - this[`on${name}`] = handler; - }); + this[`on${name}`] = handler; }); - } + }); + } - _getNativeZoomAndCS(content) { - // content: referred to if the has inline , or - // referred to remote mapml if the has a src attribute, and the fetched mapml contains - // referred to null otherwise (i.e. has fetched in shadow, the attaches to 's shadow) - let nativeZoom, nativeCS; - if (this._extentEl) { - // feature attaches to extent's shadow - if (this._extentEl.querySelector('map-link[rel=query]')) { - // for query, fallback zoom is the current map zoom level that the query is returned - nativeZoom = this._map.getZoom(); - nativeCS = 'pcrs'; - } else if (this._extentEl.querySelector('map-link[rel=features]')) { - // for templated feature, read fallback from the fetched mapml's map-meta[name=zoom / cs] - nativeZoom = this._extentEl._nativeZoom; - nativeCS = this._extentEl._nativeCS; - } - return {zoom: nativeZoom, cs: nativeCS}; - } else { - // feature attaches to layer- or layer-'s shadow - if (content.nodeType === Node.DOCUMENT_NODE) { - // for features migrated from mapml, read native zoom and cs from the remote mapml - return this._layer._mapmlvectors._getNativeVariables(content); - } else if (content.nodeName.toUpperCase() === "LAYER-") { - // for inline features, read native zoom and cs from inline map-meta - let zoomMeta = this.parentElement.querySelectorAll('map-meta[name=zoom]'), - zoomLength = zoomMeta?.length; - nativeZoom = zoomLength ? +(zoomMeta[zoomLength - 1].getAttribute('content')?.split(',').find(str => str.includes("value"))?.split('=')[1]) : 0; - - let csMeta = this.parentElement.querySelectorAll("map-meta[name=cs]"), - csLength = csMeta?.length; - nativeCS = csLength ? csMeta[csLength - 1].getAttribute('content') : 'pcrs'; - return {zoom: nativeZoom, cs: nativeCS}; - } + _getNativeZoomAndCS(content) { + // content: referred to if the has inline , or + // referred to remote mapml if the has a src attribute, and the fetched mapml contains + // referred to null otherwise (i.e. has fetched in shadow, the attaches to 's shadow) + let nativeZoom, nativeCS; + if (this._extentEl) { + // feature attaches to extent's shadow + if (this._extentEl.querySelector('map-link[rel=query]')) { + // for query, fallback zoom is the current map zoom level that the query is returned + nativeZoom = this._map.getZoom(); + nativeCS = 'pcrs'; + } else if (this._extentEl.querySelector('map-link[rel=features]')) { + // for templated feature, read fallback from the fetched mapml's map-meta[name=zoom / cs] + nativeZoom = this._extentEl._nativeZoom; + nativeCS = this._extentEl._nativeCS; + } + return { zoom: nativeZoom, cs: nativeCS }; + } else { + // feature attaches to layer- or layer-'s shadow + if (content.nodeType === Node.DOCUMENT_NODE) { + // for features migrated from mapml, read native zoom and cs from the remote mapml + return this._layer._mapmlvectors._getNativeVariables(content); + } else if (content.nodeName.toUpperCase() === 'LAYER-') { + // for inline features, read native zoom and cs from inline map-meta + let zoomMeta = this.parentElement.querySelectorAll( + 'map-meta[name=zoom]' + ), + zoomLength = zoomMeta?.length; + nativeZoom = zoomLength + ? +zoomMeta[zoomLength - 1] + .getAttribute('content') + ?.split(',') + .find((str) => str.includes('value')) + ?.split('=')[1] + : 0; + + let csMeta = this.parentElement.querySelectorAll('map-meta[name=cs]'), + csLength = csMeta?.length; + nativeCS = csLength + ? csMeta[csLength - 1].getAttribute('content') + : 'pcrs'; + return { zoom: nativeZoom, cs: nativeCS }; } } + } - // Util functions: - // internal method to calculate the extent of the feature and store it in cache for the first time - // and return cache when feature's extent is repeatedly requested - // for .extent - _memoizeExtent () { - // memoize calculated extent - let extentCache; - return function () { - if (extentCache && this._getFeatureExtent) { - // if the extent has already been calculated and is not updated, return stored extent - return extentCache; - } else { - // calculate feature extent - let map = this._map, - geometry = this.querySelector('map-geometry'), - native = this._getNativeZoomAndCS(this._layer._content), - cs = geometry.getAttribute('cs') || native.cs, - // zoom level that the feature rendered at - zoom = this.zoom || native.zoom, - shapes = geometry.querySelectorAll("map-point, map-linestring, map-polygon, map-multipoint, map-multilinestring"), - bboxExtent = [Infinity, Infinity, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY]; - for (let shape of shapes) { - let coord = shape.querySelectorAll('map-coordinates'); - for (let i = 0; i < coord.length; ++i) { - bboxExtent = _updateExtent(shape, coord[i], bboxExtent); - } - } - let topLeft = L.point(bboxExtent[0], bboxExtent[1]); - let bottomRight = L.point(bboxExtent[2], bboxExtent[3]); - let pcrsBound = M.boundsToPCRSBounds(L.bounds(topLeft, bottomRight), zoom, map.options.projection, cs); - if (shapes.length === 1 && shapes[0].tagName.toUpperCase() === "MAP-POINT") { - let projection = map.options.projection, - maxZoom = this.hasAttribute('max') ? +this.getAttribute('max') : M[projection].options.resolutions.length - 1, - tileCenter = M[projection].options.crs.tile.bounds.getCenter(), - pixel = M[projection].transformation.transform(pcrsBound.min, M[projection].scale(+this.zoom || maxZoom)); - pcrsBound = M.pixelToPCRSBounds(L.bounds(pixel.subtract(tileCenter), pixel.add(tileCenter)), this.zoom || maxZoom, projection); + // Util functions: + // internal method to calculate the extent of the feature and store it in cache for the first time + // and return cache when feature's extent is repeatedly requested + // for .extent + _memoizeExtent() { + // memoize calculated extent + let extentCache; + return function () { + if (extentCache && this._getFeatureExtent) { + // if the extent has already been calculated and is not updated, return stored extent + return extentCache; + } else { + // calculate feature extent + let map = this._map, + geometry = this.querySelector('map-geometry'), + native = this._getNativeZoomAndCS(this._layer._content), + cs = geometry.getAttribute('cs') || native.cs, + // zoom level that the feature rendered at + zoom = this.zoom || native.zoom, + shapes = geometry.querySelectorAll( + 'map-point, map-linestring, map-polygon, map-multipoint, map-multilinestring' + ), + bboxExtent = [ + Infinity, + Infinity, + Number.NEGATIVE_INFINITY, + Number.NEGATIVE_INFINITY + ]; + for (let shape of shapes) { + let coord = shape.querySelectorAll('map-coordinates'); + for (let i = 0; i < coord.length; ++i) { + bboxExtent = _updateExtent(shape, coord[i], bboxExtent); } - let result = M._convertAndFormatPCRS(pcrsBound, map); - // memoize calculated result - extentCache = result; - return result; } - }; - - // update the bboxExtent - function _updateExtent(shape, coord, bboxExtent) { - let data = coord.innerHTML.trim().replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').split(/[<>\ ]/g); - switch (shape.tagName) { - case "MAP-POINT": - bboxExtent = M._updateExtent(bboxExtent, +data[0], +data[1]); - break; - case "MAP-LINESTRING": - case "MAP-POLYGON": - case "MAP-MULTIPOINT": - case "MAP-MULTILINESTRING": - for (let i = 0; i < data.length; i += 2) { - bboxExtent = M._updateExtent(bboxExtent, +data[i], +data[i + 1]); - } - break; - default: - break; + let topLeft = L.point(bboxExtent[0], bboxExtent[1]); + let bottomRight = L.point(bboxExtent[2], bboxExtent[3]); + let pcrsBound = M.boundsToPCRSBounds( + L.bounds(topLeft, bottomRight), + zoom, + map.options.projection, + cs + ); + if ( + shapes.length === 1 && + shapes[0].tagName.toUpperCase() === 'MAP-POINT' + ) { + let projection = map.options.projection, + maxZoom = this.hasAttribute('max') + ? +this.getAttribute('max') + : M[projection].options.resolutions.length - 1, + tileCenter = M[projection].options.crs.tile.bounds.getCenter(), + pixel = M[projection].transformation.transform( + pcrsBound.min, + M[projection].scale(+this.zoom || maxZoom) + ); + pcrsBound = M.pixelToPCRSBounds( + L.bounds(pixel.subtract(tileCenter), pixel.add(tileCenter)), + this.zoom || maxZoom, + projection + ); } - return bboxExtent; + let result = M._convertAndFormatPCRS(pcrsBound, map); + // memoize calculated result + extentCache = result; + return result; } - } + }; - // find and remove the feature from mapmlvectors._features if vector layer only contains static features, helper function - // prevent it from being rendered again when zooming in / out (mapmlvectors.resetFeature() is invoked) - _removeInFeatureList(zoom) { - let mapmlvectors = this._layer._mapmlvectors; - for (let i = 0; i < mapmlvectors._features[zoom].length; ++i) { - let feature = mapmlvectors._features[zoom][i]; - if (feature._leaflet_id === this._featureGroup._leaflet_id) { - mapmlvectors._features[zoom].splice(i, 1); + // update the bboxExtent + function _updateExtent(shape, coord, bboxExtent) { + let data = coord.innerHTML + .trim() + .replace(/<[^>]+>/g, '') + .replace(/\s+/g, ' ') + .split(/[<>\ ]/g); + switch (shape.tagName) { + case 'MAP-POINT': + bboxExtent = M._updateExtent(bboxExtent, +data[0], +data[1]); + break; + case 'MAP-LINESTRING': + case 'MAP-POLYGON': + case 'MAP-MULTIPOINT': + case 'MAP-MULTILINESTRING': + for (let i = 0; i < data.length; i += 2) { + bboxExtent = M._updateExtent(bboxExtent, +data[i], +data[i + 1]); + } + break; + default: break; - } } + return bboxExtent; } + } - getMaxZoom() { - let tL = this.extent.topLeft.pcrs, - bR = this.extent.bottomRight.pcrs, - bound = L.bounds(L.point(tL.horizontal, tL.vertical), L.point(bR.horizontal, bR.vertical)); - let projection = this._map.options.projection, - layerZoomBounds = this._layer._layerEl.extent.zoom, - minZoom = layerZoomBounds.minZoom ? layerZoomBounds.minZoom : 0, - maxZoom = layerZoomBounds.maxZoom ? layerZoomBounds.maxZoom : M[projection].options.resolutions.length - 1; - let newZoom; - if (this.hasAttribute('zoom')) { - // if there is a zoom attribute set to the map-feature, zoom to the zoom attribute value - newZoom = this.zoom; - } else { - // if not, calculate the maximum zoom level that can show the feature completely - newZoom = M.getMaxZoom(bound, this._map, minZoom, maxZoom); - if (this.max < newZoom) { - // if the calculated zoom is greater than the value of max zoom attribute, go with max zoom attribute - newZoom = this.max; - } else if (this.min > newZoom) { - // if the calculated zoom is less than the value of min zoom attribute, go with min zoom attribute - newZoom = this.min; - } - } - // prevent overzooming / underzooming - if (newZoom < minZoom) { - newZoom = minZoom; - } else if (newZoom > maxZoom) { - newZoom = maxZoom; + // find and remove the feature from mapmlvectors._features if vector layer only contains static features, helper function + // prevent it from being rendered again when zooming in / out (mapmlvectors.resetFeature() is invoked) + _removeInFeatureList(zoom) { + let mapmlvectors = this._layer._mapmlvectors; + for (let i = 0; i < mapmlvectors._features[zoom].length; ++i) { + let feature = mapmlvectors._features[zoom][i]; + if (feature._leaflet_id === this._featureGroup._leaflet_id) { + mapmlvectors._features[zoom].splice(i, 1); + break; } + } + } - // should check whether the extent after zooming falls into the templated extent bound - return newZoom; + getMaxZoom() { + let tL = this.extent.topLeft.pcrs, + bR = this.extent.bottomRight.pcrs, + bound = L.bounds( + L.point(tL.horizontal, tL.vertical), + L.point(bR.horizontal, bR.vertical) + ); + let projection = this._map.options.projection, + layerZoomBounds = this._layer._layerEl.extent.zoom, + minZoom = layerZoomBounds.minZoom ? layerZoomBounds.minZoom : 0, + maxZoom = layerZoomBounds.maxZoom + ? layerZoomBounds.maxZoom + : M[projection].options.resolutions.length - 1; + let newZoom; + if (this.hasAttribute('zoom')) { + // if there is a zoom attribute set to the map-feature, zoom to the zoom attribute value + newZoom = this.zoom; + } else { + // if not, calculate the maximum zoom level that can show the feature completely + newZoom = M.getMaxZoom(bound, this._map, minZoom, maxZoom); + if (this.max < newZoom) { + // if the calculated zoom is greater than the value of max zoom attribute, go with max zoom attribute + newZoom = this.max; + } else if (this.min > newZoom) { + // if the calculated zoom is less than the value of min zoom attribute, go with min zoom attribute + newZoom = this.min; + } + } + // prevent overzooming / underzooming + if (newZoom < minZoom) { + newZoom = minZoom; + } else if (newZoom > maxZoom) { + newZoom = maxZoom; } - // internal support for returning a GeoJSON representation of geometry - // The options object can contain the following: - // propertyFunction - function(), A function that maps the features' element to a GeoJSON "properties" member. - // transform - Bool, Transform coordinates to gcrs values, defaults to True - // mapml2geojson: Object -> GeoJSON - mapml2geojson(options) { - let defaults = { - propertyFunction: null, - transform: true - }; - // assign default values for undefined options - options = Object.assign({}, defaults, options); + // should check whether the extent after zooming falls into the templated extent bound + return newZoom; + } - let json = { - type: "Feature", - properties: {}, - geometry: {} + // internal support for returning a GeoJSON representation of geometry + // The options object can contain the following: + // propertyFunction - function(), A function that maps the features' element to a GeoJSON "properties" member. + // transform - Bool, Transform coordinates to gcrs values, defaults to True + // mapml2geojson: Object -> GeoJSON + mapml2geojson(options) { + let defaults = { + propertyFunction: null, + transform: true + }; + // assign default values for undefined options + options = Object.assign({}, defaults, options); + + let json = { + type: 'Feature', + properties: {}, + geometry: {} + }; + let el = this.querySelector('map-properties'); + if (!el) { + json.properties = null; + } else if (typeof options.propertyFunction === 'function') { + json.properties = options.propertyFunction(el); + } else if (el.querySelector('table')) { + // setting properties when table presented + let table = el.querySelector('table').cloneNode(true); + json.properties = M._table2properties(table); + } else { + // when no table present, strip any possible html tags to only get text + json.properties = { + prop0: el.innerHTML.replace(/(<([^>]+)>)/gi, '').replace(/\s/g, '') }; - let el = this.querySelector('map-properties'); - if (!el) { - json.properties = null; - } else if (typeof options.propertyFunction === "function") { - json.properties = options.propertyFunction(el); - } else if (el.querySelector('table')) { - // setting properties when table presented - let table = (el.querySelector('table')).cloneNode(true); - json.properties = M._table2properties(table); - } else { - // when no table present, strip any possible html tags to only get text - json.properties = {prop0: (el.innerHTML).replace(/(<([^>]+)>)/ig, '').replace(/\s/g, '')}; - } + } - // transform to gcrs if options.transform = true (default) - let source = null, dest = null; - if (options.transform) { - source = new proj4.Proj(this._map.options.crs.code); - dest = new proj4.Proj('EPSG:4326'); - if (this._map.options.crs.code === "EPSG:3857" || this._map.options.crs.code === "EPSG:4326") { - options.transform = false; - } + // transform to gcrs if options.transform = true (default) + let source = null, + dest = null; + if (options.transform) { + source = new proj4.Proj(this._map.options.crs.code); + dest = new proj4.Proj('EPSG:4326'); + if ( + this._map.options.crs.code === 'EPSG:3857' || + this._map.options.crs.code === 'EPSG:4326' + ) { + options.transform = false; } + } - let collection = this.querySelector("map-geometry").querySelector("map-geometrycollection"), - shapes = this.querySelector("map-geometry").querySelectorAll("map-point, map-polygon, map-linestring, map-multipoint, map-multipolygon, map-multilinestring"); + let collection = this.querySelector('map-geometry').querySelector( + 'map-geometrycollection' + ), + shapes = this.querySelector('map-geometry').querySelectorAll( + 'map-point, map-polygon, map-linestring, map-multipoint, map-multipolygon, map-multilinestring' + ); - if (collection) { - json.geometry.type = "GeometryCollection"; - json.geometry.geometries = []; - for (let shape of shapes) { - json.geometry.geometries.push(M._geometry2geojson(shape, source, dest, options.transform)); - } - } else { - json.geometry = M._geometry2geojson(shapes[0], source, dest, options.transform); + if (collection) { + json.geometry.type = 'GeometryCollection'; + json.geometry.geometries = []; + for (let shape of shapes) { + json.geometry.geometries.push( + M._geometry2geojson(shape, source, dest, options.transform) + ); } - return json; + } else { + json.geometry = M._geometry2geojson( + shapes[0], + source, + dest, + options.transform + ); } + return json; + } - // a method that simulates a click, or invoking the user-defined click event - // event (optional): a MouseEvent object, can be passed as an argument of the user-defined click event handlers - click(event) { - let g = this._groupEl, - rect = g.getBoundingClientRect(); - if (!event) { - event = new MouseEvent ("click", { - clientX: rect.x + rect.width / 2, - clientY: rect.y + rect.height / 2, - button: 0 - }); + // a method that simulates a click, or invoking the user-defined click event + // event (optional): a MouseEvent object, can be passed as an argument of the user-defined click event handlers + click(event) { + let g = this._groupEl, + rect = g.getBoundingClientRect(); + if (!event) { + event = new MouseEvent('click', { + clientX: rect.x + rect.width / 2, + clientY: rect.y + rect.height / 2, + button: 0 + }); + } + if (typeof this.onclick === 'function') { + this.onclick.call(this._groupEl, event); + return; + } else { + let properties = this.querySelector('map-properties'); + if (g.getAttribute('role') === 'link') { + for (let path of g.children) { + path.mousedown.call(this._featureGroup, event); + path.mouseup.call(this._featureGroup, event); + } } - if (typeof this.onclick === 'function') { - this.onclick.call(this._groupEl, event); - return; - } else { - let properties = this.querySelector('map-properties'); - if (g.getAttribute('role') === 'link') { - for (let path of g.children) { - path.mousedown.call(this._featureGroup, event); - path.mouseup.call(this._featureGroup, event); + // for custom projection, layer- element may disconnect and re-attach to the map after the click + // so check whether map-feature element is still connected before any further operations + if (properties && this.isConnected) { + let featureGroup = this._featureGroup, + shapes = featureGroup._layers; + // close popup if the popup is currently open + for (let id in shapes) { + if (shapes[id].isPopupOpen()) { + shapes[id].closePopup(); } } - // for custom projection, layer- element may disconnect and re-attach to the map after the click - // so check whether map-feature element is still connected before any further operations - if (properties && this.isConnected) { - let featureGroup = this._featureGroup, - shapes = featureGroup._layers; - // close popup if the popup is currently open - for (let id in shapes) { - if (shapes[id].isPopupOpen()) { - shapes[id].closePopup(); - } - } - if (featureGroup.isPopupOpen()) { - featureGroup.closePopup(); - } else { - featureGroup.openPopup(); - } + if (featureGroup.isPopupOpen()) { + featureGroup.closePopup(); + } else { + featureGroup.openPopup(); } } } - - // a method that sets the current focus to the element, or invoking the user-defined focus event - // event (optional): a FocusEvent object, can be passed as an argument of the user-defined focus event handlers - // options (optional): as options parameter for native HTMLelemnt - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus - focus(event, options) { - let g = this._groupEl; - if (typeof this.onfocus === 'function') { - this.onfocus.call(this._groupEl, event); - return; - } else { - g.focus(options); - } - } + } - // a method that makes the element lose focus, or invoking the user-defined blur event - // event (optional): a FocusEvent object, can be passed as an argument of the user-defined blur event handlers - blur(event) { - if (typeof this.onblur === 'function') { - this.onblur.call(this._groupEl, event); - } else if (document.activeElement.shadowRoot?.activeElement === this._groupEl || - document.activeElement.shadowRoot?.activeElement.parentNode === this._groupEl) { - this._groupEl.blur(); - // set focus to the map container - this._map._container.focus(); - } + // a method that sets the current focus to the element, or invoking the user-defined focus event + // event (optional): a FocusEvent object, can be passed as an argument of the user-defined focus event handlers + // options (optional): as options parameter for native HTMLelemnt + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus + focus(event, options) { + let g = this._groupEl; + if (typeof this.onfocus === 'function') { + this.onfocus.call(this._groupEl, event); + return; + } else { + g.focus(options); } + } - // a method that can the viewport to be centred on the feature's extent - zoomTo() { - let extent = this.extent, - map = this._map; - let tL = extent.topLeft.pcrs, - bR = extent.bottomRight.pcrs, - bound = L.bounds(L.point(tL.horizontal, tL.vertical), L.point(bR.horizontal, bR.vertical)), - center = map.options.crs.unproject(bound.getCenter(true)); - map.setView(center, this.getMaxZoom(), {animate: false}); + // a method that makes the element lose focus, or invoking the user-defined blur event + // event (optional): a FocusEvent object, can be passed as an argument of the user-defined blur event handlers + blur(event) { + if (typeof this.onblur === 'function') { + this.onblur.call(this._groupEl, event); + } else if ( + document.activeElement.shadowRoot?.activeElement === this._groupEl || + document.activeElement.shadowRoot?.activeElement.parentNode === + this._groupEl + ) { + this._groupEl.blur(); + // set focus to the map container + this._map._container.focus(); } - } \ No newline at end of file + } + + // a method that can the viewport to be centred on the feature's extent + zoomTo() { + let extent = this.extent, + map = this._map; + let tL = extent.topLeft.pcrs, + bR = extent.bottomRight.pcrs, + bound = L.bounds( + L.point(tL.horizontal, tL.vertical), + L.point(bR.horizontal, bR.vertical) + ), + center = map.options.crs.unproject(bound.getCenter(true)); + map.setView(center, this.getMaxZoom(), { animate: false }); + } +} diff --git a/src/map-geometry.js b/src/map-geometry.js index 270e75d09..e6db1c02f 100644 --- a/src/map-geometry.js +++ b/src/map-geometry.js @@ -6,11 +6,11 @@ export class MapGeometry extends HTMLElement { return this.getAttribute('cs'); } set cs(val) { - if (['tcrs','tilematrix','pcrs','gcrs','map','tile'].includes(val)) { + if (['tcrs', 'tilematrix', 'pcrs', 'gcrs', 'map', 'tile'].includes(val)) { this.setAttribute('cs', val); } } - + attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'cs': { @@ -23,13 +23,9 @@ export class MapGeometry extends HTMLElement { } constructor() { // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - + super(); } + connectedCallback() {} + disconnectedCallback() {} } window.customElements.define('map-geometry', MapGeometry); diff --git a/src/map-input.js b/src/map-input.js index 11b3f136b..bd92e01b1 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -2,7 +2,18 @@ import { MapLink } from './map-link.js'; export class MapInput extends HTMLElement { static get observedAttributes() { - return ['name','type','value','axis','units','position','rel','min','max','step']; + return [ + 'name', + 'type', + 'value', + 'axis', + 'units', + 'position', + 'rel', + 'min', + 'max', + 'step' + ]; } get name() { @@ -141,13 +152,9 @@ export class MapInput extends HTMLElement { } constructor() { // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - + super(); } + connectedCallback() {} + disconnectedCallback() {} } window.customElements.define('map-input', MapInput); diff --git a/src/map-link.js b/src/map-link.js index d7883e85b..969bf7a41 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -1,13 +1,22 @@ export class MapLink extends HTMLElement { static get observedAttributes() { - return ['type','rel','title','href','hreflang','tref','tms','projection']; + return [ + 'type', + 'rel', + 'title', + 'href', + 'hreflang', + 'tref', + 'tms', + 'projection' + ]; } get type() { return this.getAttribute('type'); } set type(val) { // improve this - if (val === "text/mapml" || val.startsWith('image/')) { + if (val === 'text/mapml' || val.startsWith('image/')) { this.setAttribute('type', val); } } @@ -16,7 +25,22 @@ export class MapLink extends HTMLElement { } set rel(val) { // improve this - if (['license','alternate','self','style','tile','image','features','zoomin','zoomout','legend','query','stylesheet'].includes(val)) { + if ( + [ + 'license', + 'alternate', + 'self', + 'style', + 'tile', + 'image', + 'features', + 'zoomin', + 'zoomout', + 'legend', + 'query', + 'stylesheet' + ].includes(val) + ) { this.setAttribute('type', val); } } @@ -69,11 +93,11 @@ export class MapLink extends HTMLElement { } set projection(val) { // improve this - if (['OSMTILE','CBMTILE','WGS84','APSTILE'].includes(val)) { + if (['OSMTILE', 'CBMTILE', 'WGS84', 'APSTILE'].includes(val)) { this.setAttribute('projection', val); } } - + attributeChangedCallback(name, oldValue, newValue) { //['type','rel','title','href','hreflang','tref','tms','projection']; switch (name) { @@ -121,13 +145,9 @@ export class MapLink extends HTMLElement { } constructor() { // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - + super(); } + connectedCallback() {} + disconnectedCallback() {} } window.customElements.define('map-link', MapLink); diff --git a/src/map-meta.js b/src/map-meta.js index 108474c6b..ef3d92344 100644 --- a/src/map-meta.js +++ b/src/map-meta.js @@ -1,12 +1,12 @@ export class MapMeta extends HTMLElement { static get observedAttributes() { - return ['name','content']; + return ['name', 'content']; } get name() { return this.getAttribute('name'); } set name(val) { - if (['projection','extent','cs','zoom'].includes(val)) { + if (['projection', 'extent', 'cs', 'zoom'].includes(val)) { this.setAttribute('name', val); } } @@ -15,12 +15,15 @@ export class MapMeta extends HTMLElement { } set content(val) { // improve this - if (this.name === 'cs' && !['tcrs','tilematrix','pcrs','gcrs','map','tile'].includes(val)) { + if ( + this.name === 'cs' && + !['tcrs', 'tilematrix', 'pcrs', 'gcrs', 'map', 'tile'].includes(val) + ) { return; } this.setAttribute('content', val); } - + attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'name': { @@ -39,13 +42,9 @@ export class MapMeta extends HTMLElement { } constructor() { // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - + super(); } + connectedCallback() {} + disconnectedCallback() {} } window.customElements.define('map-meta', MapMeta); diff --git a/src/map-properties.js b/src/map-properties.js index e39c32791..f3ef7c392 100644 --- a/src/map-properties.js +++ b/src/map-properties.js @@ -1,20 +1,14 @@ export class MapProperties extends HTMLElement { - static get observedAttributes() { - return; - } + static get observedAttributes() { + return; + } - attributeChangedCallback(name, oldValue, newValue) { - } - constructor() { - // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - - } + attributeChangedCallback(name, oldValue, newValue) {} + constructor() { + // Always call super first in constructor + super(); } - window.customElements.define('map-properties', MapProperties); - \ No newline at end of file + connectedCallback() {} + disconnectedCallback() {} +} +window.customElements.define('map-properties', MapProperties); diff --git a/src/map-select.js b/src/map-select.js index 1e26e3d9c..fdb130ceb 100644 --- a/src/map-select.js +++ b/src/map-select.js @@ -1,6 +1,6 @@ export class MapSelect extends HTMLElement { static get observedAttributes() { - return ['name','id']; + return ['name', 'id']; } get name() { return this.getAttribute('name'); @@ -14,7 +14,7 @@ export class MapSelect extends HTMLElement { set id(val) { this.setAttribute('id', val); } - + attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'name': @@ -31,13 +31,9 @@ export class MapSelect extends HTMLElement { } constructor() { // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - + super(); } + connectedCallback() {} + disconnectedCallback() {} } window.customElements.define('map-select', MapSelect); diff --git a/src/map-span.js b/src/map-span.js index 73fc903bd..c9406958d 100644 --- a/src/map-span.js +++ b/src/map-span.js @@ -1,20 +1,14 @@ export class MapSpan extends HTMLElement { - static get observedAttributes() { - return; - } + static get observedAttributes() { + return; + } - attributeChangedCallback(name, oldValue, newValue) { - } - constructor() { - // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - - } + attributeChangedCallback(name, oldValue, newValue) {} + constructor() { + // Always call super first in constructor + super(); } - window.customElements.define('map-span', MapSpan); - \ No newline at end of file + connectedCallback() {} + disconnectedCallback() {} +} +window.customElements.define('map-span', MapSpan); diff --git a/src/map-style.js b/src/map-style.js index fc4b9680b..ac1847638 100644 --- a/src/map-style.js +++ b/src/map-style.js @@ -1,20 +1,14 @@ export class MapStyle extends HTMLElement { - static get observedAttributes() { - return; - } + static get observedAttributes() { + return; + } - attributeChangedCallback(name, oldValue, newValue) { - } - constructor() { - // Always call super first in constructor - super(); - } - connectedCallback() { - - } - disconnectedCallback() { - - } + attributeChangedCallback(name, oldValue, newValue) {} + constructor() { + // Always call super first in constructor + super(); } - window.customElements.define('map-style', MapStyle); - \ No newline at end of file + connectedCallback() {} + disconnectedCallback() {} +} +window.customElements.define('map-style', MapStyle); diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 93b606a5e..1210f4b72 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -1,5 +1,5 @@ -import './leaflet.js'; // bundled with proj4, proj4leaflet, modularized -import './mapml.js'; +import './leaflet.js'; // bundled with proj4, proj4leaflet, modularized +import './mapml.js'; import DOMTokenList from './DOMTokenList.js'; import { MapLayer } from './layer.js'; import { MapCaption } from './map-caption.js'; @@ -8,7 +8,17 @@ import { MapExtent } from './map-extent.js'; export class MapViewer extends HTMLElement { static get observedAttributes() { - return ['lat', 'lon', 'zoom', 'projection', 'width', 'height', 'controls', 'static', 'controlslist']; + return [ + 'lat', + 'lon', + 'zoom', + 'projection', + 'width', + 'height', + 'controls', + 'static', + 'controlslist' + ]; } // see comments below regarding attributeChangedCallback vs. getter/setter // usage. Effectively, the user of the element must use the property, not @@ -21,7 +31,7 @@ export class MapViewer extends HTMLElement { set controls(value) { const hasControls = Boolean(value); if (hasControls) { - this.setAttribute('controls',''); + this.setAttribute('controls', ''); } else { this.removeAttribute('controls'); } @@ -34,90 +44,91 @@ export class MapViewer extends HTMLElement { this.setAttribute('controlslist', value); } get width() { - return (window.getComputedStyle(this).width).replace('px',''); + return window.getComputedStyle(this).width.replace('px', ''); } set width(val) { //img.height or img.width setters change or add the corresponding attributes - this.setAttribute("width", val); + this.setAttribute('width', val); } get height() { - return (window.getComputedStyle(this).height).replace('px',''); + return window.getComputedStyle(this).height.replace('px', ''); } set height(val) { //img.height or img.width setters change or add the corresponding attributes - this.setAttribute("height", val); + this.setAttribute('height', val); } get lat() { - return this.hasAttribute("lat") ? this.getAttribute("lat") : "0"; + return this.hasAttribute('lat') ? this.getAttribute('lat') : '0'; } set lat(val) { if (val) { - this.setAttribute("lat", val); + this.setAttribute('lat', val); } } get lon() { - return this.hasAttribute("lon") ? this.getAttribute("lon") : "0"; + return this.hasAttribute('lon') ? this.getAttribute('lon') : '0'; } set lon(val) { if (val) { - this.setAttribute("lon", val); + this.setAttribute('lon', val); } } get projection() { - return this.hasAttribute("projection") ? this.getAttribute("projection") : ""; + return this.hasAttribute('projection') + ? this.getAttribute('projection') + : ''; } set projection(val) { - if(val && M[val]){ + if (val && M[val]) { this.setAttribute('projection', val); - if (this._map && this._map.options.projection !== val){ + if (this._map && this._map.options.projection !== val) { this._map.options.crs = M[val]; this._map.options.projection = val; - for(let layer of this.querySelectorAll("layer-")){ - layer.removeAttribute("disabled"); + for (let layer of this.querySelectorAll('layer-')) { + layer.removeAttribute('disabled'); let reAttach = this.removeChild(layer); this.appendChild(reAttach); } - if(this._debug) for(let i = 0; i<2;i++) this.toggleDebug(); + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); } else this.dispatchEvent(new CustomEvent('createmap')); - } else throw new Error("Undefined Projection"); + } else throw new Error('Undefined Projection'); } get zoom() { - return this.hasAttribute("zoom") ? this.getAttribute("zoom") : 0; + return this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0; } set zoom(val) { - var parsedVal = parseInt(val,10); - if (!isNaN(parsedVal) && (parsedVal >= 0 && parsedVal <= 25)) { - this.setAttribute('zoom', parsedVal); - } + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal) && parsedVal >= 0 && parsedVal <= 25) { + this.setAttribute('zoom', parsedVal); + } } get layers() { return this.getElementsByTagName('layer-'); } - get extent(){ + get extent() { let map = this._map, pcrsBounds = M.pixelToPCRSBounds( map.getPixelBounds(), map.getZoom(), - map.options.projection); + map.options.projection + ); let formattedExtent = M._convertAndFormatPCRS(pcrsBounds, map); - if(map.getMaxZoom() !== Infinity){ + if (map.getMaxZoom() !== Infinity) { formattedExtent.zoom = { - minZoom:map.getMinZoom(), - maxZoom:map.getMaxZoom() + minZoom: map.getMinZoom(), + maxZoom: map.getMaxZoom() }; } - return (formattedExtent); + return formattedExtent; } get static() { return this.hasAttribute('static'); } set static(value) { const isStatic = Boolean(value); - if (isStatic) - this.setAttribute('static', ''); - else - this.removeAttribute('static'); + if (isStatic) this.setAttribute('static', ''); + else this.removeAttribute('static'); } constructor() { @@ -130,25 +141,36 @@ export class MapViewer extends HTMLElement { this._traversalCall = false; } connectedCallback() { - this._initShadowRoot(); this._controlsList = new DOMTokenList( - this.getAttribute("controlslist"), - this, "controlslist", - ["noreload","nofullscreen","nozoom","nolayer","noscale","geolocation"] + this.getAttribute('controlslist'), + this, + 'controlslist', + [ + 'noreload', + 'nofullscreen', + 'nozoom', + 'nolayer', + 'noscale', + 'geolocation' + ] ); var s = window.getComputedStyle(this), - wpx = s.width, hpx=s.height, - w = this.hasAttribute("width") ? this.getAttribute("width") : parseInt(wpx.replace('px','')), - h = this.hasAttribute("height") ? this.getAttribute("height") : parseInt(hpx.replace('px','')); + wpx = s.width, + hpx = s.height, + w = this.hasAttribute('width') + ? this.getAttribute('width') + : parseInt(wpx.replace('px', '')), + h = this.hasAttribute('height') + ? this.getAttribute('height') + : parseInt(hpx.replace('px', '')); this._changeWidth(w); - this._changeHeight(h); - + this._changeHeight(h); // wait for createmap event before creating leaflet map - // this allows a safeguard for the case where loading a custom TCRS takes + // this allows a safeguard for the case where loading a custom TCRS takes // longer than loading mapml-viewer.js/web-map.js // the REASON we need a synchronous event listener (see comment below) // is because the mapml-viewer element has / can have a size of 0 up until after @@ -156,12 +178,14 @@ export class MapViewer extends HTMLElement { // perhaps a browser rendering cycle?? this.addEventListener('createmap', this._createMap); - let custom = !(["CBMTILE","APSTILE","OSMTILE","WGS84"].includes(this.projection)); + let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( + this.projection + ); // this is worth a read, because dispatchEvent is synchronous // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent // In particular: // "All applicable event handlers are called and return before dispatchEvent() returns." - if (!custom) { + if (!custom) { this.dispatchEvent(new CustomEvent('createmap')); } // https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274 @@ -184,7 +208,7 @@ export class MapViewer extends HTMLElement { let mapcaptionupdate = this.querySelector('map-caption'); if (mapcaptionupdate !== mapcaption) { this.removeAttribute('aria-label'); - } + } }); this.mapCaptionObserver.observe(this, { childList: true @@ -195,47 +219,50 @@ export class MapViewer extends HTMLElement { } _initShadowRoot() { if (!this.shadowRoot) { - this.attachShadow({mode: 'open'}); + this.attachShadow({ mode: 'open' }); } let tmpl = document.createElement('template'); - tmpl.innerHTML = ``; // jshint ignore:line + /* jshint ignore:start */ + tmpl.innerHTML = ``; + /* jshint ignore:end */ let shadowRoot = this.shadowRoot; this._container = document.createElement('div'); - let output = ""; - this._container.insertAdjacentHTML("beforeend", output); + let output = + ""; + this._container.insertAdjacentHTML('beforeend', output); // Set default styles for the map element. let mapDefaultCSS = document.createElement('style'); mapDefaultCSS.innerHTML = - `:host {` + - `all: initial;` + // Reset properties inheritable from html/body, as some inherited styles may cause unexpected issues with the map element's components (https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/140). - `contain: layout size;` + // Contain layout and size calculations within the map element. - `display: inline-block;` + // This together with dimension properties is required so that Leaflet isn't working with a height=0 box by default. - `height: 150px;` + // Provide a "default object size" (https://github.com/Maps4HTML/HTML-Map-Element/issues/31). - `width: 300px;` + - `border-width: 2px;` + // Set a default border for contrast, similar to UA default for iframes. - `border-style: inset;` + - `}` + - `:host([frameborder="0"]) {` + - `border-width: 0;` + - `}` + - `:host([hidden]) {` + - `display: none!important;` + - `}` + - `:host .leaflet-control-container {` + - `visibility: hidden!important;` + // Visibility hack to improve percieved performance (mitigate FOUC) – visibility is unset in mapml.css! (https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/154). - `}`; + `:host {` + + `all: initial;` + // Reset properties inheritable from html/body, as some inherited styles may cause unexpected issues with the map element's components (https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/140). + `contain: layout size;` + // Contain layout and size calculations within the map element. + `display: inline-block;` + // This together with dimension properties is required so that Leaflet isn't working with a height=0 box by default. + `height: 150px;` + // Provide a "default object size" (https://github.com/Maps4HTML/HTML-Map-Element/issues/31). + `width: 300px;` + + `border-width: 2px;` + // Set a default border for contrast, similar to UA default for iframes. + `border-style: inset;` + + `}` + + `:host([frameborder="0"]) {` + + `border-width: 0;` + + `}` + + `:host([hidden]) {` + + `display: none!important;` + + `}` + + `:host .leaflet-control-container {` + + `visibility: hidden!important;` + // Visibility hack to improve percieved performance (mitigate FOUC) – visibility is unset in mapml.css! (https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/154). + `}`; // Hide all (light DOM) children of the map element. let hideElementsCSS = document.createElement('style'); hideElementsCSS.innerHTML = - `mapml-viewer > * {` + - `display: none!important;` + - `}`; + `mapml-viewer > * {` + `display: none!important;` + `}`; this.appendChild(hideElementsCSS); - + // Make the Leaflet container element programmatically identifiable // (https://github.com/Leaflet/Leaflet/issues/7193). this._container.setAttribute('role', 'region'); @@ -244,7 +271,6 @@ export class MapViewer extends HTMLElement { shadowRoot.appendChild(mapDefaultCSS); shadowRoot.appendChild(tmpl.content.cloneNode(true)); shadowRoot.appendChild(this._container); - } _createMap() { if (!this._map) { @@ -272,7 +298,8 @@ export class MapViewer extends HTMLElement { this._toggleControls(); this._crosshair = M.crosshair().addTo(this._map); - if(M.options.featureIndexOverlayOption) this._featureIndexOverlay = M.featureIndexOverlay().addTo(this._map); + if (M.options.featureIndexOverlayOption) + this._featureIndexOverlay = M.featureIndexOverlay().addTo(this._map); this._setUpEvents(); } @@ -280,16 +307,16 @@ export class MapViewer extends HTMLElement { disconnectedCallback() { while (this.shadowRoot.firstChild) { this.shadowRoot.removeChild(this.shadowRoot.firstChild); - } + } delete this._map; this._deleteControls(); } adoptedCallback() { -// console.log('Custom map element moved to new page.'); + // console.log('Custom map element moved to new page.'); } attributeChangedCallback(name, oldValue, newValue) { -// console.log('Attribute: ' + name + ' changed from: '+ oldValue + ' to: '+newValue); + // console.log('Attribute: ' + name + ' changed from: '+ oldValue + ' to: '+newValue); // "Best practice": handle side-effects in this callback // https://developers.google.com/web/fundamentals/web-components/best-practices // https://developers.google.com/web/fundamentals/web-components/best-practices#avoid-reentrancy @@ -306,7 +333,7 @@ export class MapViewer extends HTMLElement { break; ... } */ - switch(name) { + switch (name) { case 'controlslist': if (this._controlsList) { if (this._controlsList.valueSet === false) { @@ -314,58 +341,62 @@ export class MapViewer extends HTMLElement { } this._toggleControls(); } - break; + break; case 'controls': if (oldValue !== null && newValue === null) { this._hideControls(); } else if (oldValue === null && newValue !== null) { this._showControls(); } - break; - case 'height': + break; + case 'height': if (oldValue !== newValue) { this._changeHeight(newValue); } - break; - case 'width': + break; + case 'width': if (oldValue !== newValue) { this._changeWidth(newValue); } - break; + break; case 'static': this._toggleStatic(); - break; + break; } } // Creates All map controls and adds them to the map, when created. _createControls() { let mapSize = this._map.getSize().y, - totalSize = 0; + totalSize = 0; - this._layerControl = M.layerControl(null,{"collapsed": true, mapEl: this}).addTo(this._map); + this._layerControl = M.layerControl(null, { + collapsed: true, + mapEl: this + }).addTo(this._map); let scaleValue = M.options.announceScale; - if (scaleValue === "metric") { - scaleValue = {"metric": true, "imperial": false}; + if (scaleValue === 'metric') { + scaleValue = { metric: true, imperial: false }; } - if (scaleValue === "imperial") { - scaleValue = {"metric": false, "imperial": true}; + if (scaleValue === 'imperial') { + scaleValue = { metric: false, imperial: true }; } - if (!this._scaleBar) this._scaleBar = M.scaleBar(scaleValue).addTo(this._map); + if (!this._scaleBar) + this._scaleBar = M.scaleBar(scaleValue).addTo(this._map); // Only add controls if there is enough top left vertical space - if (!this._zoomControl && (totalSize + 93) <= mapSize){ + if (!this._zoomControl && totalSize + 93 <= mapSize) { totalSize += 93; this._zoomControl = L.control.zoom().addTo(this._map); } - if (!this._reloadButton && (totalSize + 49) <= mapSize){ + if (!this._reloadButton && totalSize + 49 <= mapSize) { totalSize += 49; this._reloadButton = M.reloadButton().addTo(this._map); } - if (!this._fullScreenControl && (totalSize + 49) <= mapSize){ + if (!this._fullScreenControl && totalSize + 49 <= mapSize) { totalSize += 49; this._fullScreenControl = M.fullscreenButton().addTo(this._map); } @@ -374,64 +405,64 @@ export class MapViewer extends HTMLElement { this._geolocationButton = M.geolocationButton().addTo(this._map); } } - + // Sets controls by hiding/unhiding them based on the map attribute _toggleControls() { if (this.controls === false) { this._hideControls(); - this._map.contextMenu.toggleContextMenuItem("Controls", "disabled"); - } else { + this._map.contextMenu.toggleContextMenuItem('Controls', 'disabled'); + } else { this._showControls(); - this._map.contextMenu.toggleContextMenuItem("Controls", "enabled"); + this._map.contextMenu.toggleContextMenuItem('Controls', 'enabled'); } } _hideControls() { - this._setControlsVisibility("fullscreen",true); - this._setControlsVisibility("layercontrol",true); - this._setControlsVisibility("reload",true); - this._setControlsVisibility("zoom",true); - this._setControlsVisibility("geolocation",true); - this._setControlsVisibility("scale",true); + this._setControlsVisibility('fullscreen', true); + this._setControlsVisibility('layercontrol', true); + this._setControlsVisibility('reload', true); + this._setControlsVisibility('zoom', true); + this._setControlsVisibility('geolocation', true); + this._setControlsVisibility('scale', true); } _showControls() { - this._setControlsVisibility("fullscreen",false); - this._setControlsVisibility("layercontrol",false); - this._setControlsVisibility("reload",false); - this._setControlsVisibility("zoom",false); - this._setControlsVisibility("geolocation",true); - this._setControlsVisibility("scale",false); - + this._setControlsVisibility('fullscreen', false); + this._setControlsVisibility('layercontrol', false); + this._setControlsVisibility('reload', false); + this._setControlsVisibility('zoom', false); + this._setControlsVisibility('geolocation', true); + this._setControlsVisibility('scale', false); + // prune the controls shown if necessary // this logic could be embedded in _showControls // but would require being able to iterate the domain of supported tokens // for the controlslist if (this._controlsList) { this._controlsList.forEach((value) => { - switch(value.toLowerCase()) { + switch (value.toLowerCase()) { case 'nofullscreen': - this._setControlsVisibility("fullscreen",true); - break; + this._setControlsVisibility('fullscreen', true); + break; case 'nolayer': - this._setControlsVisibility("layercontrol",true); - break; + this._setControlsVisibility('layercontrol', true); + break; case 'noreload': - this._setControlsVisibility("reload",true); - break; + this._setControlsVisibility('reload', true); + break; case 'nozoom': - this._setControlsVisibility("zoom",true); - break; + this._setControlsVisibility('zoom', true); + break; case 'geolocation': - this._setControlsVisibility("geolocation",false); - break; + this._setControlsVisibility('geolocation', false); + break; case 'noscale': - this._setControlsVisibility("scale",true); - break; + this._setControlsVisibility('scale', true); + break; } }); } if (this._layerControl && this._layerControl._layers.length === 0) { - this._layerControl._container.setAttribute("hidden",""); + this._layerControl._container.setAttribute('hidden', ''); } } @@ -448,55 +479,55 @@ export class MapViewer extends HTMLElement { // for the control element based on the Boolean hide parameter _setControlsVisibility(control, hide) { let container; - switch(control) { - case "zoom": + switch (control) { + case 'zoom': if (this._zoomControl) { container = this._zoomControl._container; } break; - case "reload": + case 'reload': if (this._reloadButton) { container = this._reloadButton._container; } break; - case "fullscreen": + case 'fullscreen': if (this._fullScreenControl) { container = this._fullScreenControl._container; } break; - case "layercontrol": + case 'layercontrol': if (this._layerControl) { container = this._layerControl._container; } break; - case "geolocation": + case 'geolocation': if (this._geolocationButton) { container = this._geolocationButton._container; } break; - case "scale": + case 'scale': if (this._scaleBar) { container = this._scaleBar._container; } - break; + break; } if (container) { if (hide) { // setting the visibility for all the children of the element - [ ...container.children].forEach((childEl) => { - childEl.setAttribute("hidden",""); + [...container.children].forEach((childEl) => { + childEl.setAttribute('hidden', ''); }); - container.setAttribute("hidden",""); + container.setAttribute('hidden', ''); } else { // setting the visibility for all the children of the element - [ ...container.children].forEach((childEl) => { - childEl.removeAttribute("hidden"); + [...container.children].forEach((childEl) => { + childEl.removeAttribute('hidden'); }); - container.removeAttribute("hidden"); + container.removeAttribute('hidden'); } } } - _toggleStatic(){ + _toggleStatic() { const isStatic = this.hasAttribute('static'); if (this._map) { if (isStatic) { @@ -518,179 +549,308 @@ export class MapViewer extends HTMLElement { } } } - + _dropHandler(event) { event.preventDefault(); - let text = event.dataTransfer.getData("text"); + let text = event.dataTransfer.getData('text'); M._pasteLayer(this, text); } _dragoverHandler(event) { event.preventDefault(); - event.dataTransfer.dropEffect = "copy"; + event.dataTransfer.dropEffect = 'copy'; } _removeEvents() { if (this._map) { this._map.off(); - this.removeEventListener("drop", this._dropHandler, false); - this.removeEventListener("dragover", this._dragoverHandler, false); + this.removeEventListener('drop', this._dropHandler, false); + this.removeEventListener('dragover', this._dragoverHandler, false); } } _setUpEvents() { - this.addEventListener("drop", this._dropHandler, false); - this.addEventListener("dragover", this._dragoverHandler, false); - this.addEventListener("change", - function(e) { - if(e.target.tagName === "LAYER-"){ - this.dispatchEvent(new CustomEvent("layerchange", {details:{target: this, originalEvent: e}})); - } - }, false); + this.addEventListener('drop', this._dropHandler, false); + this.addEventListener('dragover', this._dragoverHandler, false); + this.addEventListener( + 'change', + function (e) { + if (e.target.tagName === 'LAYER-') { + this.dispatchEvent( + new CustomEvent('layerchange', { + details: { target: this, originalEvent: e } + }) + ); + } + }, + false + ); this.parentElement.addEventListener('keyup', function (e) { - if(e.keyCode === 9 && document.activeElement.nodeName === "MAPML-VIEWER"){ - document.activeElement.dispatchEvent(new CustomEvent('mapfocused', {detail: - {target: this}})); + if ( + e.keyCode === 9 && + document.activeElement.nodeName === 'MAPML-VIEWER' + ) { + document.activeElement.dispatchEvent( + new CustomEvent('mapfocused', { detail: { target: this } }) + ); } }); - // pasting layer-, links and geojson using Ctrl+V + // pasting layer-, links and geojson using Ctrl+V this.addEventListener('keydown', function (e) { - if(e.keyCode === 86 && e.ctrlKey){ - navigator.clipboard - .readText() - .then( - (layer) => { - M._pasteLayer(this, layer); - }); - // Prevents default spacebar event on all of mapml-viewer - } else if (e.keyCode === 32 && - this.shadowRoot.activeElement.nodeName !== "INPUT") { + if (e.keyCode === 86 && e.ctrlKey) { + navigator.clipboard.readText().then((layer) => { + M._pasteLayer(this, layer); + }); + // Prevents default spacebar event on all of mapml-viewer + } else if ( + e.keyCode === 32 && + this.shadowRoot.activeElement.nodeName !== 'INPUT' + ) { e.preventDefault(); - this._map.fire('keypress', {originalEvent: e}); + this._map.fire('keypress', { originalEvent: e }); } }); this.parentElement.addEventListener('mousedown', function (e) { - if(document.activeElement.nodeName === "MAPML-VIEWER"){ - document.activeElement.dispatchEvent(new CustomEvent('mapfocused', {detail: - {target: this}})); + if (document.activeElement.nodeName === 'MAPML-VIEWER') { + document.activeElement.dispatchEvent( + new CustomEvent('mapfocused', { detail: { target: this } }) + ); } }); - this._map.on('locationfound', + this._map.on( + 'locationfound', function (e) { - this.dispatchEvent(new CustomEvent('maplocationfound', {detail: - {latlng: e.latlng, accuracy: e.accuracy} - })); - },this); - this._map.on('locationerror', + this.dispatchEvent( + new CustomEvent('maplocationfound', { + detail: { latlng: e.latlng, accuracy: e.accuracy } + }) + ); + }, + this + ); + this._map.on( + 'locationerror', function (e) { - this.dispatchEvent(new CustomEvent('locationerror', {detail: - {error:e.message} - })); - },this); - this._map.on('load', + this.dispatchEvent( + new CustomEvent('locationerror', { detail: { error: e.message } }) + ); + }, + this + ); + this._map.on( + 'load', function () { - this.dispatchEvent(new CustomEvent('load', {detail: {target: this}})); - }, this); - this._map.on('preclick', + this.dispatchEvent( + new CustomEvent('load', { detail: { target: this } }) + ); + }, + this + ); + this._map.on( + 'preclick', function (e) { - this.dispatchEvent(new CustomEvent('preclick', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('click', + this.dispatchEvent( + new CustomEvent('preclick', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'click', function (e) { - this.dispatchEvent(new CustomEvent('click', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('dblclick', + this.dispatchEvent( + new CustomEvent('click', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'dblclick', function (e) { - this.dispatchEvent(new CustomEvent('dblclick', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('mousemove', + this.dispatchEvent( + new CustomEvent('dblclick', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'mousemove', function (e) { - this.dispatchEvent(new CustomEvent('mousemove', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('mouseover', + this.dispatchEvent( + new CustomEvent('mousemove', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'mouseover', function (e) { - this.dispatchEvent(new CustomEvent('mouseover', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('mouseout', + this.dispatchEvent( + new CustomEvent('mouseover', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'mouseout', function (e) { - this.dispatchEvent(new CustomEvent('mouseout', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('mousedown', + this.dispatchEvent( + new CustomEvent('mouseout', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'mousedown', function (e) { - this.dispatchEvent(new CustomEvent('mousedown', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - },this); - this._map.on('mouseup', + this.dispatchEvent( + new CustomEvent('mousedown', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'mouseup', function (e) { - this.dispatchEvent(new CustomEvent('mouseup', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('contextmenu', + this.dispatchEvent( + new CustomEvent('mouseup', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'contextmenu', function (e) { - this.dispatchEvent(new CustomEvent('contextmenu', {detail: - {lat: e.latlng.lat, lon: e.latlng.lng, - x: e.containerPoint.x, y: e.containerPoint.y} - })); - }, this); - this._map.on('movestart', + this.dispatchEvent( + new CustomEvent('contextmenu', { + detail: { + lat: e.latlng.lat, + lon: e.latlng.lng, + x: e.containerPoint.x, + y: e.containerPoint.y + } + }) + ); + }, + this + ); + this._map.on( + 'movestart', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('movestart', {detail: - {target: this}})); - }, this); - this._map.on('move', + this.dispatchEvent( + new CustomEvent('movestart', { detail: { target: this } }) + ); + }, + this + ); + this._map.on( + 'move', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('move', {detail: - {target: this}})); - }, this); - this._map.on('moveend', + this.dispatchEvent( + new CustomEvent('move', { detail: { target: this } }) + ); + }, + this + ); + this._map.on( + 'moveend', function () { this._updateMapCenter(); this._addToHistory(); - this.dispatchEvent(new CustomEvent('moveend', {detail: - {target: this}})); - }, this); - this._map.on('zoomstart', + this.dispatchEvent( + new CustomEvent('moveend', { detail: { target: this } }) + ); + }, + this + ); + this._map.on( + 'zoomstart', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('zoomstart', {detail: - {target: this}})); - }, this); - this._map.on('zoom', + this.dispatchEvent( + new CustomEvent('zoomstart', { detail: { target: this } }) + ); + }, + this + ); + this._map.on( + 'zoom', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('zoom', {detail: - {target: this}})); - }, this); - this._map.on('zoomend', + this.dispatchEvent( + new CustomEvent('zoom', { detail: { target: this } }) + ); + }, + this + ); + this._map.on( + 'zoomend', function () { this._updateMapCenter(); - this.dispatchEvent(new CustomEvent('zoomend', {detail: - {target: this}})); - }, this); - this.addEventListener('fullscreenchange', function(event) { + this.dispatchEvent( + new CustomEvent('zoomend', { detail: { target: this } }) + ); + }, + this + ); + this.addEventListener('fullscreenchange', function (event) { if (document.fullscreenElement === null) { // full-screen mode has been exited this._map.contextMenu.setViewFullScreenInnerHTML('view'); @@ -698,8 +858,8 @@ export class MapViewer extends HTMLElement { this._map.contextMenu.setViewFullScreenInnerHTML('exit'); } }); - this.addEventListener('keydown', function(event) { - if (document.activeElement.nodeName === "MAPML-VIEWER") { + this.addEventListener('keydown', function (event) { + if (document.activeElement.nodeName === 'MAPML-VIEWER') { // Check if Ctrl+R is pressed and map is focused if (event.ctrlKey && event.keyCode === 82) { // Prevent default browser behavior @@ -718,7 +878,8 @@ export class MapViewer extends HTMLElement { }); } - locate(options) { //options: https://leafletjs.com/reference.html#locate-options + locate(options) { + //options: https://leafletjs.com/reference.html#locate-options if (this._geolocationButton) { this._geolocationButton.stop(); } @@ -729,35 +890,35 @@ export class MapViewer extends HTMLElement { } this._map.locate(options); } else { - this._map.locate({setView: true, maxZoom: 16}); + this._map.locate({ setView: true, maxZoom: 16 }); } } - toggleDebug(){ - if(this._debug){ + toggleDebug() { + if (this._debug) { this._debug.remove(); this._debug = undefined; } else { this._debug = M.debugOverlay().addTo(this._map); } } - + _changeWidth(width) { if (this._container) { - this._container.style.width = width+"px"; - this.shadowRoot.styleSheets[0].cssRules[0].style.width = width+"px"; + this._container.style.width = width + 'px'; + this.shadowRoot.styleSheets[0].cssRules[0].style.width = width + 'px'; } if (this._map) { - this._map.invalidateSize(false); + this._map.invalidateSize(false); } } _changeHeight(height) { if (this._container) { - this._container.style.height = height+"px"; - this.shadowRoot.styleSheets[0].cssRules[0].style.height = height+"px"; + this._container.style.height = height + 'px'; + this.shadowRoot.styleSheets[0].cssRules[0].style.height = height + 'px'; } if (this._map) { - this._map.invalidateSize(false); + this._map.invalidateSize(false); } } zoomTo(lat, lon, zoom) { @@ -780,17 +941,18 @@ export class MapViewer extends HTMLElement { * Adds to the maps history on moveends * @private */ - _addToHistory(){ - if(this._traversalCall > 0) { // this._traversalCall tracks how many consecutive moveends to ignore from history - this._traversalCall--; // this is useful for ignoring moveends corresponding to back, forward and reload + _addToHistory() { + if (this._traversalCall > 0) { + // this._traversalCall tracks how many consecutive moveends to ignore from history + this._traversalCall--; // this is useful for ignoring moveends corresponding to back, forward and reload return; } let mapLocation = this._map.getPixelBounds().getCenter(); let location = { zoom: this._map.getZoom(), - x:mapLocation.x, - y:mapLocation.y, + x: mapLocation.x, + y: mapLocation.y }; this._historyIndex++; this._history.splice(this._historyIndex, 0, location); @@ -800,48 +962,50 @@ export class MapViewer extends HTMLElement { } if (this._historyIndex === 0) { // when at initial state of map, disable back, forward, and reload items - this._map.contextMenu.toggleContextMenuItem("Back", "disabled"); // back contextmenu item - this._map.contextMenu.toggleContextMenuItem("Forward", "disabled");// forward contextmenu item - this._map.contextMenu.toggleContextMenuItem("Reload", "disabled"); // reload contextmenu item + this._map.contextMenu.toggleContextMenuItem('Back', 'disabled'); // back contextmenu item + this._map.contextMenu.toggleContextMenuItem('Forward', 'disabled'); // forward contextmenu item + this._map.contextMenu.toggleContextMenuItem('Reload', 'disabled'); // reload contextmenu item this._reloadButton?.disable(); } else { - this._map.contextMenu.toggleContextMenuItem("Back", "enabled"); // back contextmenu item - this._map.contextMenu.toggleContextMenuItem("Forward", "disabled");// forward contextmenu item - this._map.contextMenu.toggleContextMenuItem("Reload", "enabled"); // reload contextmenu item + this._map.contextMenu.toggleContextMenuItem('Back', 'enabled'); // back contextmenu item + this._map.contextMenu.toggleContextMenuItem('Forward', 'disabled'); // forward contextmenu item + this._map.contextMenu.toggleContextMenuItem('Reload', 'enabled'); // reload contextmenu item this._reloadButton?.enable(); } } /** * Allow user to move back in history */ - back(){ + back() { let history = this._history; let curr = history[this._historyIndex]; - if(this._historyIndex > 0){ - this._map.contextMenu.toggleContextMenuItem("Forward", "enabled");// forward contextmenu item + if (this._historyIndex > 0) { + this._map.contextMenu.toggleContextMenuItem('Forward', 'enabled'); // forward contextmenu item this._historyIndex--; let prev = history[this._historyIndex]; // Disable back, reload contextmenu item when at the end of history if (this._historyIndex === 0) { - this._map.contextMenu.toggleContextMenuItem("Back", "disabled"); // back contextmenu item - this._map.contextMenu.toggleContextMenuItem("Reload", "disabled"); // reload contextmenu item + this._map.contextMenu.toggleContextMenuItem('Back', 'disabled'); // back contextmenu item + this._map.contextMenu.toggleContextMenuItem('Reload', 'disabled'); // reload contextmenu item this._reloadButton?.disable(); } - if(prev.zoom !== curr.zoom){ - this._traversalCall = 2; // allows the next 2 moveends to be ignored from history + if (prev.zoom !== curr.zoom) { + this._traversalCall = 2; // allows the next 2 moveends to be ignored from history let currScale = this._map.options.crs.scale(curr.zoom); // gets the scale of the current zoom level let prevScale = this._map.options.crs.scale(prev.zoom); // gets the scale of the previous zoom level let scale = currScale / prevScale; // used to convert the previous pixel location to be in terms of the current zoom level - this._map.panBy([((prev.x * scale) - curr.x), ((prev.y * scale) - curr.y)], {animate: false}); + this._map.panBy([prev.x * scale - curr.x, prev.y * scale - curr.y], { + animate: false + }); this._map.setZoom(prev.zoom); } else { this._traversalCall = 1; - this._map.panBy([(prev.x - curr.x), (prev.y - curr.y)]); + this._map.panBy([prev.x - curr.x, prev.y - curr.y]); } } } @@ -849,21 +1013,21 @@ export class MapViewer extends HTMLElement { /** * Allows user to move forward in history */ - forward(){ + forward() { let history = this._history; let curr = history[this._historyIndex]; - if(this._historyIndex < history.length - 1){ - this._map.contextMenu.toggleContextMenuItem("Back", "enabled"); // back contextmenu item - this._map.contextMenu.toggleContextMenuItem("Reload", "enabled"); // reload contextmenu item + if (this._historyIndex < history.length - 1) { + this._map.contextMenu.toggleContextMenuItem('Back', 'enabled'); // back contextmenu item + this._map.contextMenu.toggleContextMenuItem('Reload', 'enabled'); // reload contextmenu item this._reloadButton?.enable(); this._historyIndex++; let next = history[this._historyIndex]; // disable forward contextmenu item, when at the end of forward history if (this._historyIndex + 1 === this._history.length) { - this._map.contextMenu.toggleContextMenuItem("Forward", "disabled"); // forward contextmenu item + this._map.contextMenu.toggleContextMenuItem('Forward', 'disabled'); // forward contextmenu item } - if(next.zoom !== curr.zoom){ + if (next.zoom !== curr.zoom) { this._traversalCall = 2; // allows the next 2 moveends to be ignored from history let currScale = this._map.options.crs.scale(curr.zoom); // gets the scale of the current zoom level @@ -871,11 +1035,13 @@ export class MapViewer extends HTMLElement { let scale = currScale / nextScale; // used to convert the next pixel location to be in terms of the current zoom level - this._map.panBy([((next.x * scale) - curr.x), ((next.y * scale) - curr.y)], {animate: false}); + this._map.panBy([next.x * scale - curr.x, next.y * scale - curr.y], { + animate: false + }); this._map.setZoom(next.zoom); } else { this._traversalCall = 1; - this._map.panBy([(next.x - curr.x), (next.y - curr.y)]); + this._map.panBy([next.x - curr.x, next.y - curr.y]); } } } @@ -883,24 +1049,24 @@ export class MapViewer extends HTMLElement { /** * Allows the user to reload/reset the map's location to it's initial location */ - reload(){ + reload() { let initialLocation = this._history.shift(); let mapLocation = this._map.getPixelBounds().getCenter(); let curr = { zoom: this._map.getZoom(), - x:mapLocation.x, - y:mapLocation.y, + x: mapLocation.x, + y: mapLocation.y }; - this._map.contextMenu.toggleContextMenuItem("Back", "disabled"); // back contextmenu item - this._map.contextMenu.toggleContextMenuItem("Forward", "disabled");// forward contextmenu item - this._map.contextMenu.toggleContextMenuItem("Reload", "disabled"); // reload contextmenu item + this._map.contextMenu.toggleContextMenuItem('Back', 'disabled'); // back contextmenu item + this._map.contextMenu.toggleContextMenuItem('Forward', 'disabled'); // forward contextmenu item + this._map.contextMenu.toggleContextMenuItem('Reload', 'disabled'); // reload contextmenu item this._reloadButton?.disable(); this._history = [initialLocation]; this._historyIndex = 0; - if(initialLocation.zoom !== curr.zoom) { + if (initialLocation.zoom !== curr.zoom) { this._traversalCall = 2; // ignores the next 2 moveend events let currScale = this._map.options.crs.scale(curr.zoom); // gets the scale of the current zoom level @@ -908,31 +1074,49 @@ export class MapViewer extends HTMLElement { let scale = currScale / initScale; - this._map.panBy([((initialLocation.x * scale) - curr.x), ((initialLocation.y * scale) - curr.y)], {animate: false}); + this._map.panBy( + [ + initialLocation.x * scale - curr.x, + initialLocation.y * scale - curr.y + ], + { animate: false } + ); this._map.setZoom(initialLocation.zoom); - } else { // if it's on the same zoom level as the initial location, no need to calculate scales + } else { + // if it's on the same zoom level as the initial location, no need to calculate scales this._traversalCall = 1; - this._map.panBy([(initialLocation.x- curr.x), (initialLocation.y - curr.y)]); + this._map.panBy([initialLocation.x - curr.x, initialLocation.y - curr.y]); } } - _toggleFullScreen(){ + _toggleFullScreen() { this._map.toggleFullscreen(); } - viewSource(){ - let blob = new Blob([this._source],{type:"text/plain"}), - url = URL.createObjectURL(blob); + viewSource() { + let blob = new Blob([this._source], { type: 'text/plain' }), + url = URL.createObjectURL(blob); window.open(url); URL.revokeObjectURL(url); } defineCustomProjection(jsonTemplate) { let t = JSON.parse(jsonTemplate); - if (t === undefined || !t.proj4string || !t.projection || !t.resolutions || !t.origin || !t.bounds) throw new Error('Incomplete TCRS Definition'); - if (t.projection.indexOf(":") >= 0) throw new Error('":" is not permitted in projection name'); + if ( + t === undefined || + !t.proj4string || + !t.projection || + !t.resolutions || + !t.origin || + !t.bounds + ) + throw new Error('Incomplete TCRS Definition'); + if (t.projection.indexOf(':') >= 0) + throw new Error('":" is not permitted in projection name'); if (M[t.projection.toUpperCase()]) return t.projection.toUpperCase(); - let tileSize = [256, 512, 1024, 2048, 4096].includes(t.tilesize)?t.tilesize:256; + let tileSize = [256, 512, 1024, 2048, 4096].includes(t.tilesize) + ? t.tilesize + : 256; M[t.projection] = new L.Proj.CRS(t.projection, t.proj4string, { origin: t.origin, @@ -941,102 +1125,171 @@ export class MapViewer extends HTMLElement { crs: { tcrs: { horizontal: { - name: "x", - min: 0, - max: zoom => (Math.round(M[t.projection].options.bounds.getSize().x / M[t.projection].options.resolutions[zoom])) + name: 'x', + min: 0, + max: (zoom) => + Math.round( + M[t.projection].options.bounds.getSize().x / + M[t.projection].options.resolutions[zoom] + ) }, vertical: { - name: "y", - min:0, - max: zoom => (Math.round(M[t.projection].options.bounds.getSize().y / M[t.projection].options.resolutions[zoom])) + name: 'y', + min: 0, + max: (zoom) => + Math.round( + M[t.projection].options.bounds.getSize().y / + M[t.projection].options.resolutions[zoom] + ) }, - bounds: zoom => L.bounds([M[t.projection].options.crs.tcrs.horizontal.min, - M[t.projection].options.crs.tcrs.vertical.min], - [M[t.projection].options.crs.tcrs.horizontal.max(zoom), - M[t.projection].options.crs.tcrs.vertical.max(zoom)]) + bounds: (zoom) => + L.bounds( + [ + M[t.projection].options.crs.tcrs.horizontal.min, + M[t.projection].options.crs.tcrs.vertical.min + ], + [ + M[t.projection].options.crs.tcrs.horizontal.max(zoom), + M[t.projection].options.crs.tcrs.vertical.max(zoom) + ] + ) }, pcrs: { horizontal: { - name: "easting", - get min() {return M[t.projection].options.bounds.min.x;}, - get max() {return M[t.projection].options.bounds.max.x;} - }, + name: 'easting', + get min() { + return M[t.projection].options.bounds.min.x; + }, + get max() { + return M[t.projection].options.bounds.max.x; + } + }, vertical: { - name: "northing", - get min() {return M[t.projection].options.bounds.min.y;}, - get max() {return M[t.projection].options.bounds.max.y;} + name: 'northing', + get min() { + return M[t.projection].options.bounds.min.y; + }, + get max() { + return M[t.projection].options.bounds.max.y; + } }, - get bounds() {return M[t.projection].options.bounds;} - }, + get bounds() { + return M[t.projection].options.bounds; + } + }, gcrs: { horizontal: { - name: "longitude", + name: 'longitude', // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - get min() {return M[t.projection].unproject(M.OSMTILE.options.bounds.min).lng;}, - get max() {return M[t.projection].unproject(M.OSMTILE.options.bounds.max).lng;} - }, + get min() { + return M[t.projection].unproject(M.OSMTILE.options.bounds.min) + .lng; + }, + get max() { + return M[t.projection].unproject(M.OSMTILE.options.bounds.max) + .lng; + } + }, vertical: { - name: "latitude", + name: 'latitude', // set min/max axis values from EPSG registry area of use, retrieved 2019-07-25 - get min() {return M[t.projection].unproject(M.OSMTILE.options.bounds.min).lat;}, - get max() {return M[t.projection].unproject(M.OSMTILE.options.bounds.max).lat;} + get min() { + return M[t.projection].unproject(M.OSMTILE.options.bounds.min) + .lat; + }, + get max() { + return M[t.projection].unproject(M.OSMTILE.options.bounds.max) + .lat; + } }, - get bounds() {return L.latLngBounds( - [M[t.projection].options.crs.gcrs.vertical.min,M[t.projection].options.crs.gcrs.horizontal.min], - [M[t.projection].options.crs.gcrs.vertical.max,M[t.projection].options.crs.gcrs.horizontal.max]);} + get bounds() { + return L.latLngBounds( + [ + M[t.projection].options.crs.gcrs.vertical.min, + M[t.projection].options.crs.gcrs.horizontal.min + ], + [ + M[t.projection].options.crs.gcrs.vertical.max, + M[t.projection].options.crs.gcrs.horizontal.max + ] + ); + } }, map: { horizontal: { - name: "i", + name: 'i', min: 0, - max: map => map.getSize().x + max: (map) => map.getSize().x }, vertical: { - name: "j", + name: 'j', min: 0, - max: map => map.getSize().y + max: (map) => map.getSize().y }, - bounds: map => L.bounds(L.point([0,0]),map.getSize()) + bounds: (map) => L.bounds(L.point([0, 0]), map.getSize()) }, tile: { horizontal: { - name: "i", + name: 'i', min: 0, - max: tileSize, + max: tileSize }, vertical: { - name: "j", + name: 'j', min: 0, - max: tileSize, + max: tileSize }, - get bounds() {return L.bounds( - [M[t.projection].options.crs.tile.horizontal.min,M[t.projection].options.crs.tile.vertical.min], - [M[t.projection].options.crs.tile.horizontal.max,M[t.projection].options.crs.tile.vertical.max]);} + get bounds() { + return L.bounds( + [ + M[t.projection].options.crs.tile.horizontal.min, + M[t.projection].options.crs.tile.vertical.min + ], + [ + M[t.projection].options.crs.tile.horizontal.max, + M[t.projection].options.crs.tile.vertical.max + ] + ); + } }, tilematrix: { horizontal: { - name: "column", + name: 'column', min: 0, - max: zoom => (Math.round(M[t.projection].options.crs.tcrs.horizontal.max(zoom) / M[t.projection].options.crs.tile.bounds.getSize().x)) + max: (zoom) => + Math.round( + M[t.projection].options.crs.tcrs.horizontal.max(zoom) / + M[t.projection].options.crs.tile.bounds.getSize().x + ) }, vertical: { - name: "row", + name: 'row', min: 0, - max: zoom => (Math.round(M[t.projection].options.crs.tcrs.vertical.max(zoom) / M[t.projection].options.crs.tile.bounds.getSize().y)) + max: (zoom) => + Math.round( + M[t.projection].options.crs.tcrs.vertical.max(zoom) / + M[t.projection].options.crs.tile.bounds.getSize().y + ) }, - bounds: zoom => L.bounds( - [M[t.projection].options.crs.tilematrix.horizontal.min, - M[t.projection].options.crs.tilematrix.vertical.min], - [M[t.projection].options.crs.tilematrix.horizontal.max(zoom), - M[t.projection].options.crs.tilematrix.vertical.max(zoom)]) + bounds: (zoom) => + L.bounds( + [ + M[t.projection].options.crs.tilematrix.horizontal.min, + M[t.projection].options.crs.tilematrix.vertical.min + ], + [ + M[t.projection].options.crs.tilematrix.horizontal.max(zoom), + M[t.projection].options.crs.tilematrix.vertical.max(zoom) + ] + ) } - }, - }); //creates crs using L.Proj + } + }); //creates crs using L.Proj M[t.projection.toUpperCase()] = M[t.projection]; //adds the projection uppercase to global M return t.projection; } - geojson2mapml(json, options = {}){ + geojson2mapml(json, options = {}) { if (options.projection === undefined) { options.projection = this.projection; } @@ -1048,6 +1301,6 @@ export class MapViewer extends HTMLElement { // need to provide options { extends: ... } for custom built-in elements window.customElements.define('mapml-viewer', MapViewer); window.customElements.define('layer-', MapLayer); -window.customElements.define('map-caption',MapCaption); +window.customElements.define('map-caption', MapCaption); window.customElements.define('map-feature', MapFeature); window.customElements.define('map-extent', MapExtent); diff --git a/src/mapml/control/AttributionButton.js b/src/mapml/control/AttributionButton.js index baf0aadc2..f658fff1f 100644 --- a/src/mapml/control/AttributionButton.js +++ b/src/mapml/control/AttributionButton.js @@ -1,73 +1,88 @@ export var AttributionButton = L.Control.Attribution.extend({ - options: { - prefix: 'Maps for HTML Community Group | Slava Ukraini Leaflet ' - }, + options: { + prefix: + 'Maps for HTML Community Group | Slava Ukraini Leaflet ' + }, - onAdd: function (map) { - map.attributionControl = this; - this._container = L.DomUtil.create('details', 'leaflet-control-attribution'); - L.DomEvent.disableClickPropagation(this._container); + onAdd: function (map) { + map.attributionControl = this; + this._container = L.DomUtil.create( + 'details', + 'leaflet-control-attribution' + ); + L.DomEvent.disableClickPropagation(this._container); - for (var i in map._layers) { - if (map._layers[i].getAttribution) { - this.addAttribution(map._layers[i].getAttribution()); - } - } + for (var i in map._layers) { + if (map._layers[i].getAttribution) { + this.addAttribution(map._layers[i].getAttribution()); + } + } - this._update(); + this._update(); - map.on('layeradd', this._addAttribution, this); + map.on('layeradd', this._addAttribution, this); - let dialog = document.createElement("dialog"); - dialog.setAttribute("class", "shortcuts-dialog"); - dialog.setAttribute("autofocus", ""); - dialog.onclick = function (e) { - e.stopPropagation(); - }; - dialog.innerHTML = `${M.options.locale.kbdShortcuts} ` + - `
    ${M.options.locale.kbdMovement}
  • ${M.options.locale.kbdPanUp}
  • ${M.options.locale.kbdPanDown}
  • ${M.options.locale.kbdPanLeft}
  • ${M.options.locale.kbdPanRight}
  • + ${M.options.locale.btnZoomIn}
  • - ${M.options.locale.btnZoomOut}
  • shift + ←/↑/→/↓ 3x ${M.options.locale.kbdPanIncrement}
  • ctrl + ←/↑/→/↓ 0.2x ${M.options.locale.kbdPanIncrement}
  • shift + +/- ${M.options.locale.kbdZoom}
` + - `
    ${M.options.locale.kbdFeature}
  • ←/↑ ${M.options.locale.kbdPrevFeature}
  • →/↓ ${M.options.locale.kbdNextFeature}
`; - map._container.appendChild(dialog); + let dialog = document.createElement('dialog'); + dialog.setAttribute('class', 'shortcuts-dialog'); + dialog.setAttribute('autofocus', ''); + dialog.onclick = function (e) { + e.stopPropagation(); + }; + dialog.innerHTML = + `${M.options.locale.kbdShortcuts} ` + + `
    ${M.options.locale.kbdMovement}
  • ${M.options.locale.kbdPanUp}
  • ${M.options.locale.kbdPanDown}
  • ${M.options.locale.kbdPanLeft}
  • ${M.options.locale.kbdPanRight}
  • + ${M.options.locale.btnZoomIn}
  • - ${M.options.locale.btnZoomOut}
  • shift + ←/↑/→/↓ 3x ${M.options.locale.kbdPanIncrement}
  • ctrl + ←/↑/→/↓ 0.2x ${M.options.locale.kbdPanIncrement}
  • shift + +/- ${M.options.locale.kbdZoom}
` + + `
    ${M.options.locale.kbdFeature}
  • ←/↑ ${M.options.locale.kbdPrevFeature}
  • →/↓ ${M.options.locale.kbdNextFeature}
`; + map._container.appendChild(dialog); - return this._container; - }, + return this._container; + }, - _update: function () { - if (!this._map) { return; } + _update: function () { + if (!this._map) { + return; + } - var attribs = []; + var attribs = []; - for (var i in this._attributions) { - if (this._attributions[i]) { - attribs.push(i); - } - } + for (var i in this._attributions) { + if (this._attributions[i]) { + attribs.push(i); + } + } - var prefixAndAttribs = []; + var prefixAndAttribs = []; - if (this.options.prefix) { - prefixAndAttribs.push(this.options.prefix); - } - if (attribs.length) { - prefixAndAttribs.push(attribs.join(', ')); - } - this._container.innerHTML = `` + '
' + ` | ` + prefixAndAttribs.join(' ') + '
'; - this._container.setAttribute("role","group"); - this._container.setAttribute("aria-label",`${M.options.locale.btnAttribution}`); - } + if (this.options.prefix) { + prefixAndAttribs.push(this.options.prefix); + } + if (attribs.length) { + prefixAndAttribs.push(attribs.join(', ')); + } + this._container.innerHTML = + `` + + '
' + + ` | ` + + prefixAndAttribs.join(' ') + + '
'; + this._container.setAttribute('role', 'group'); + this._container.setAttribute( + 'aria-label', + `${M.options.locale.btnAttribution}` + ); + } }); L.Map.mergeOptions({ - attributionControl: false, - toggleableAttributionControl: true + attributionControl: false, + toggleableAttributionControl: true }); L.Map.addInitHook(function () { - if (this.options.toggleableAttributionControl) { - new AttributionButton().addTo(this); - } + if (this.options.toggleableAttributionControl) { + new AttributionButton().addTo(this); + } }); export var attributionButton = function (options) { - return new AttributionButton(options); + return new AttributionButton(options); }; diff --git a/src/mapml/control/FullscreenButton.js b/src/mapml/control/FullscreenButton.js index d19ca1242..2ade90123 100644 --- a/src/mapml/control/FullscreenButton.js +++ b/src/mapml/control/FullscreenButton.js @@ -1,164 +1,170 @@ export var FullscreenButton = L.Control.extend({ - options: { - position: 'topleft', - title: { - 'false': 'View Fullscreen', - 'true': 'Exit Fullscreen' - } - }, - - onAdd: function (map) { - var container = L.DomUtil.create('div', 'leaflet-control-fullscreen leaflet-bar leaflet-control'); - - this.link = L.DomUtil.create('a', 'leaflet-control-fullscreen-button leaflet-bar-part', container); - this.link.href = '#'; - this.link.setAttribute('role', 'button'); - - this._map = map; - this._map.on('fullscreenchange', this._toggleTitle, this); - this._toggleTitle(); - - L.DomEvent.on(this.link, 'click', this._click, this); - - return container; - }, - - onRemove: function (map) { - map.off('fullscreenchange', this._toggleTitle, this); - }, - - _click: function (e) { - L.DomEvent.stopPropagation(e); - L.DomEvent.preventDefault(e); - this._map.toggleFullscreen(this.options); - }, - - _toggleTitle: function() { - this.link.title = this.options.title[this._map.isFullscreen()]; - } + options: { + position: 'topleft', + title: { + false: 'View Fullscreen', + true: 'Exit Fullscreen' + } + }, + + onAdd: function (map) { + var container = L.DomUtil.create( + 'div', + 'leaflet-control-fullscreen leaflet-bar leaflet-control' + ); + + this.link = L.DomUtil.create( + 'a', + 'leaflet-control-fullscreen-button leaflet-bar-part', + container + ); + this.link.href = '#'; + this.link.setAttribute('role', 'button'); + + this._map = map; + this._map.on('fullscreenchange', this._toggleTitle, this); + this._toggleTitle(); + + L.DomEvent.on(this.link, 'click', this._click, this); + + return container; + }, + + onRemove: function (map) { + map.off('fullscreenchange', this._toggleTitle, this); + }, + + _click: function (e) { + L.DomEvent.stopPropagation(e); + L.DomEvent.preventDefault(e); + this._map.toggleFullscreen(this.options); + }, + + _toggleTitle: function () { + this.link.title = this.options.title[this._map.isFullscreen()]; + } +}); + +L.Map.include({ + isFullscreen: function () { + return this._isFullscreen || false; + }, + + toggleFullscreen: function (options) { + // the element can't contain a shadow root, so we used a child
+ // can contain a shadow root, so return it directly + var mapEl = this.getContainer().getRootNode().host, + container = mapEl.nodeName === 'DIV' ? mapEl.parentElement : mapEl; + if (this.isFullscreen()) { + if (options && options.pseudoFullscreen) { + this._disablePseudoFullscreen(container); + } else if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitCancelFullScreen) { + document.webkitCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else { + this._disablePseudoFullscreen(container); + } + } else { + if (options && options.pseudoFullscreen) { + this._enablePseudoFullscreen(container); + } else if (container.requestFullscreen) { + container.requestFullscreen(); + } else if (container.mozRequestFullScreen) { + container.mozRequestFullScreen(); + } else if (container.webkitRequestFullscreen) { + container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (container.msRequestFullscreen) { + container.msRequestFullscreen(); + } else { + this._enablePseudoFullscreen(container); + } + } + }, + + _enablePseudoFullscreen: function (container) { + L.DomUtil.addClass(container, 'leaflet-pseudo-fullscreen'); + this._setFullscreen(true); + this.fire('fullscreenchange'); + }, + + _disablePseudoFullscreen: function (container) { + L.DomUtil.removeClass(container, 'leaflet-pseudo-fullscreen'); + this._setFullscreen(false); + this.fire('fullscreenchange'); + }, + + _setFullscreen: function (fullscreen) { + this._isFullscreen = fullscreen; + var container = this.getContainer().getRootNode().host; + if (fullscreen) { + L.DomUtil.addClass(container, 'mapml-fullscreen-on'); + } else { + L.DomUtil.removeClass(container, 'mapml-fullscreen-on'); + } + this.invalidateSize(); + }, + + _onFullscreenChange: function (e) { + var fullscreenElement = + document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + document.msFullscreenElement, + mapEl = this.getContainer().getRootNode().host, + container = mapEl.nodeName === 'DIV' ? mapEl.parentElement : mapEl; + + if (fullscreenElement === container && !this._isFullscreen) { + this._setFullscreen(true); + this.fire('fullscreenchange'); + } else if (fullscreenElement !== container && this._isFullscreen) { + this._setFullscreen(false); + this.fire('fullscreenchange'); + } + } +}); + +L.Map.mergeOptions({ + fullscreenControl: false +}); + +L.Map.addInitHook(function () { + if (this.options.fullscreenControl) { + this.fullscreenControl = new FullscreenButton( + this.options.fullscreenControl + ); + this.addControl(this.fullscreenControl); + } + + var fullscreenchange; + + if ('onfullscreenchange' in document) { + fullscreenchange = 'fullscreenchange'; + } else if ('onmozfullscreenchange' in document) { + fullscreenchange = 'mozfullscreenchange'; + } else if ('onwebkitfullscreenchange' in document) { + fullscreenchange = 'webkitfullscreenchange'; + } else if ('onmsfullscreenchange' in document) { + fullscreenchange = 'MSFullscreenChange'; + } + + if (fullscreenchange) { + var onFullscreenChange = L.bind(this._onFullscreenChange, this); + + this.whenReady(function () { + L.DomEvent.on(document, fullscreenchange, onFullscreenChange); }); - L.Map.include({ - isFullscreen: function () { - return this._isFullscreen || false; - }, - - toggleFullscreen: function (options) { - // the element can't contain a shadow root, so we used a child
- // can contain a shadow root, so return it directly - var mapEl = this.getContainer().getRootNode().host, - container = mapEl.nodeName === "DIV" ? mapEl.parentElement : mapEl; - if (this.isFullscreen()) { - if (options && options.pseudoFullscreen) { - this._disablePseudoFullscreen(container); - } else if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitCancelFullScreen) { - document.webkitCancelFullScreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } else { - this._disablePseudoFullscreen(container); - } - } else { - if (options && options.pseudoFullscreen) { - this._enablePseudoFullscreen(container); - } else if (container.requestFullscreen) { - container.requestFullscreen(); - } else if (container.mozRequestFullScreen) { - container.mozRequestFullScreen(); - } else if (container.webkitRequestFullscreen) { - container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); - } else if (container.msRequestFullscreen) { - container.msRequestFullscreen(); - } else { - this._enablePseudoFullscreen(container); - } - } - - }, - - _enablePseudoFullscreen: function (container) { - L.DomUtil.addClass(container, 'leaflet-pseudo-fullscreen'); - this._setFullscreen(true); - this.fire('fullscreenchange'); - }, - - _disablePseudoFullscreen: function (container) { - L.DomUtil.removeClass(container, 'leaflet-pseudo-fullscreen'); - this._setFullscreen(false); - this.fire('fullscreenchange'); - }, - - _setFullscreen: function(fullscreen) { - this._isFullscreen = fullscreen; - var container = this.getContainer().getRootNode().host; - if (fullscreen) { - L.DomUtil.addClass(container, 'mapml-fullscreen-on'); - } else { - L.DomUtil.removeClass(container, 'mapml-fullscreen-on'); - } - this.invalidateSize(); - }, - - _onFullscreenChange: function (e) { - var fullscreenElement = - document.fullscreenElement || - document.mozFullScreenElement || - document.webkitFullscreenElement || - document.msFullscreenElement, - mapEl = this.getContainer().getRootNode().host, - container = mapEl.nodeName === "DIV" ? mapEl.parentElement : mapEl; - - - if (fullscreenElement === container && !this._isFullscreen) { - this._setFullscreen(true); - this.fire('fullscreenchange'); - } else if (fullscreenElement !== container && this._isFullscreen) { - this._setFullscreen(false); - this.fire('fullscreenchange'); - } - } + this.on('unload', function () { + L.DomEvent.off(document, fullscreenchange, onFullscreenChange); }); + } +}); - L.Map.mergeOptions({ - fullscreenControl: false - }); - - L.Map.addInitHook(function () { - if (this.options.fullscreenControl) { - this.fullscreenControl = new FullscreenButton(this.options.fullscreenControl); - this.addControl(this.fullscreenControl); - } - - var fullscreenchange; - - if ('onfullscreenchange' in document) { - fullscreenchange = 'fullscreenchange'; - } else if ('onmozfullscreenchange' in document) { - fullscreenchange = 'mozfullscreenchange'; - } else if ('onwebkitfullscreenchange' in document) { - fullscreenchange = 'webkitfullscreenchange'; - } else if ('onmsfullscreenchange' in document) { - fullscreenchange = 'MSFullscreenChange'; - } - - if (fullscreenchange) { - var onFullscreenChange = L.bind(this._onFullscreenChange, this); - - this.whenReady(function () { - L.DomEvent.on(document, fullscreenchange, onFullscreenChange); - }); - - this.on('unload', function () { - L.DomEvent.off(document, fullscreenchange, onFullscreenChange); - }); - } - }); - - export var fullscreenButton = function (options) { - return new FullscreenButton(options); - }; - +export var fullscreenButton = function (options) { + return new FullscreenButton(options); +}; diff --git a/src/mapml/control/GeolocationButton.js b/src/mapml/control/GeolocationButton.js index 1ba633aa8..56be01a3c 100644 --- a/src/mapml/control/GeolocationButton.js +++ b/src/mapml/control/GeolocationButton.js @@ -1,26 +1,33 @@ export var GeolocationButton = L.Control.extend({ options: { - position: 'bottomright' + position: 'bottomright' }, onAdd: function (map) { - this.locateControl = L.control.locate({ - showPopup: false, - strings: { - title: M.options.locale.btnLocTrackOff - }, - position: this.options.position, - locateOptions: { - maxZoom: 16 - } - }).addTo(map); + this.locateControl = L.control + .locate({ + showPopup: false, + strings: { + title: M.options.locale.btnLocTrackOff + }, + position: this.options.position, + locateOptions: { + maxZoom: 16 + } + }) + .addTo(map); var container = this.locateControl._container; var button = this.locateControl; - var observer = new MutationObserver(function(mutations) { - if (container.classList.contains('active') && container.classList.contains('following')) { + var observer = new MutationObserver(function (mutations) { + if ( + container.classList.contains('active') && + container.classList.contains('following') + ) { container.firstChild.title = M.options.locale.btnLocTrackOn; - button._marker.bindTooltip(M.options.locale.btnMyLocTrackOn,{permanent:true}); + button._marker.bindTooltip(M.options.locale.btnMyLocTrackOn, { + permanent: true + }); } else if (container.classList.contains('active')) { container.firstChild.title = M.options.locale.btnLocTrackLastKnown; button._marker.bindTooltip(M.options.locale.btnMyLastKnownLocTrackOn); @@ -30,7 +37,7 @@ export var GeolocationButton = L.Control.extend({ }); var observerConfig = { attributes: true, attributeFilter: ['class'] }; observer.observe(container, observerConfig); - + return container; }, diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index 4fb40ca92..bd21a3014 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -1,172 +1,213 @@ export var LayerControl = L.Control.Layers.extend({ - /* removes 'base' layers as a concept */ - options: { - autoZIndex: false, - sortLayers: true, - sortFunction: function (layerA, layerB) { - return layerA.options.zIndex < layerB.options.zIndex ? -1 : (layerA.options.zIndex > layerB.options.zIndex ? 1 : 0); - } - }, - initialize: function (overlays, options) { - L.setOptions(this, options); - - // the _layers array contains objects like {layer: layer, name: "name", overlay: true} - // the array index is the id of the layer returned by L.stamp(layer) which I guess is a unique hash - this._layerControlInputs = []; - this._layers = []; - this._lastZIndex = 0; - this._handlingClick = false; + /* removes 'base' layers as a concept */ + options: { + autoZIndex: false, + sortLayers: true, + sortFunction: function (layerA, layerB) { + return layerA.options.zIndex < layerB.options.zIndex + ? -1 + : layerA.options.zIndex > layerB.options.zIndex + ? 1 + : 0; + } + }, + initialize: function (overlays, options) { + L.setOptions(this, options); - for (var i in overlays) { - this._addLayer(overlays[i], i, true); - } - }, - onAdd: function () { - this._initLayout(); - this._map.on('validate', this._validateInput, this); - L.DomEvent.on(this.options.mapEl, "layerchange", this._validateInput, this); - // Adding event on layer control button - L.DomEvent.on(this._container.getElementsByTagName("a")[0], 'keydown', this._focusFirstLayer, this._container); - L.DomEvent.on(this._container, 'contextmenu', this._preventDefaultContextMenu, this); - this._update(); - //this._validateExtents(); - if(this._layers.length < 1 && !this._map._showControls){ - this._container.setAttribute("hidden",""); - } else{ - this._map._showControls = true; - } - return this._container; - }, - onRemove: function (map) { - map.off('validate', this._validateInput, this); - L.DomEvent.off(this._container.getElementsByTagName("a")[0], 'keydown', this._focusFirstLayer, this._container); - // remove layer-registerd event handlers so that if the control is not - // on the map it does not generate layer events - for (var i = 0; i < this._layers.length; i++) { - this._layers[i].layer.off('add remove', this._onLayerChange, this); - this._layers[i].layer.off('extentload', this._validateInput, this); - } - }, - addOrUpdateOverlay: function (layer, name) { - var alreadyThere = false; - for (var i=0;i 0){ - this._container.removeAttribute("hidden"); - this._map._showControls = true; + // the _layers array contains objects like {layer: layer, name: "name", overlay: true} + // the array index is the id of the layer returned by L.stamp(layer) which I guess is a unique hash + this._layerControlInputs = []; + this._layers = []; + this._lastZIndex = 0; + this._handlingClick = false; + + for (var i in overlays) { + this._addLayer(overlays[i], i, true); + } + }, + onAdd: function () { + this._initLayout(); + this._map.on('validate', this._validateInput, this); + L.DomEvent.on(this.options.mapEl, 'layerchange', this._validateInput, this); + // Adding event on layer control button + L.DomEvent.on( + this._container.getElementsByTagName('a')[0], + 'keydown', + this._focusFirstLayer, + this._container + ); + L.DomEvent.on( + this._container, + 'contextmenu', + this._preventDefaultContextMenu, + this + ); + this._update(); + //this._validateExtents(); + if (this._layers.length < 1 && !this._map._showControls) { + this._container.setAttribute('hidden', ''); + } else { + this._map._showControls = true; + } + return this._container; + }, + onRemove: function (map) { + map.off('validate', this._validateInput, this); + L.DomEvent.off( + this._container.getElementsByTagName('a')[0], + 'keydown', + this._focusFirstLayer, + this._container + ); + // remove layer-registerd event handlers so that if the control is not + // on the map it does not generate layer events + for (var i = 0; i < this._layers.length; i++) { + this._layers[i].layer.off('add remove', this._onLayerChange, this); + this._layers[i].layer.off('extentload', this._validateInput, this); + } + }, + addOrUpdateOverlay: function (layer, name) { + var alreadyThere = false; + for (var i = 0; i < this._layers.length; i++) { + if (this._layers[i].layer === layer) { + alreadyThere = true; + this._layers[i].name = name; + // replace the controls with updated controls if necessary. + break; } - return (this._map) ? this._update() : this; - }, - removeLayer: function (layer) { - L.Control.Layers.prototype.removeLayer.call(this, layer); - if(this._layers.length === 0){ - this._container.setAttribute("hidden", ""); + } + if (!alreadyThere) { + this.addOverlay(layer, name); + } + if (this._layers.length > 0) { + this._container.removeAttribute('hidden'); + this._map._showControls = true; + } + return this._map ? this._update() : this; + }, + removeLayer: function (layer) { + L.Control.Layers.prototype.removeLayer.call(this, layer); + if (this._layers.length === 0) { + this._container.setAttribute('hidden', ''); + } + }, + _validateInput: function (e) { + for (let i = 0; i < this._layers.length; i++) { + if (!this._layers[i].input.labels[0]) continue; + let label = this._layers[i].input.labels[0].getElementsByTagName('span'), + input = this._layers[i].input.labels[0].getElementsByTagName('input'); + input[0].checked = this._layers[i].layer._layerEl.checked; + if ( + this._layers[i].layer._layerEl.disabled && + this._layers[i].layer._layerEl.checked + ) { + input[0].closest('fieldset').disabled = true; + label[0].style.fontStyle = 'italic'; + } else { + input[0].closest('fieldset').disabled = false; + label[0].style.fontStyle = 'normal'; } - }, - _validateInput: function (e) { - for (let i = 0; i < this._layers.length; i++) { - if(!this._layers[i].input.labels[0])continue; - let label = this._layers[i].input.labels[0].getElementsByTagName("span"), - input = this._layers[i].input.labels[0].getElementsByTagName("input"); - input[0].checked = this._layers[i].layer._layerEl.checked; - if(this._layers[i].layer._layerEl.disabled && this._layers[i].layer._layerEl.checked){ - input[0].closest("fieldset").disabled = true; - label[0].style.fontStyle = "italic"; - } else { - input[0].closest("fieldset").disabled = false; - label[0].style.fontStyle = "normal"; - } - // check if an extent is disabled and disable it - if(this._layers[i].layer._extent && this._layers[i].layer._extent._mapExtents){ - for(let j = 0; j < this._layers[i].layer._extent._mapExtents.length; j++){ - let input = this._layers[i].layer._extent._mapExtents[j].extentAnatomy, - label = input.getElementsByClassName("mapml-layer-item-name")[0]; - if(this._layers[i].layer._extent._mapExtents[j].disabled && this._layers[i].layer._extent._mapExtents[j].checked){ - label.style.fontStyle = "italic"; - input.disabled = true; - } else { - label.style.fontStyle = "normal"; - input.disabled = false; - } + // check if an extent is disabled and disable it + if ( + this._layers[i].layer._extent && + this._layers[i].layer._extent._mapExtents + ) { + for ( + let j = 0; + j < this._layers[i].layer._extent._mapExtents.length; + j++ + ) { + let input = + this._layers[i].layer._extent._mapExtents[j].extentAnatomy, + label = input.getElementsByClassName('mapml-layer-item-name')[0]; + if ( + this._layers[i].layer._extent._mapExtents[j].disabled && + this._layers[i].layer._extent._mapExtents[j].checked + ) { + label.style.fontStyle = 'italic'; + input.disabled = true; + } else { + label.style.fontStyle = 'normal'; + input.disabled = false; } } } + } + }, - }, + // focus the first layer in the layer control when enter is pressed + _focusFirstLayer: function (e) { + if ( + e.key === 'Enter' && + this.className === + 'leaflet-control-layers leaflet-control leaflet-control-layers-expanded' + ) { + var elem = + this.children[1].children[2].children[0].children[0].children[0] + .children[0]; + if (elem) setTimeout(() => elem.focus(), 0); + } + }, - // focus the first layer in the layer control when enter is pressed - _focusFirstLayer: function(e){ - if (e.key === 'Enter' && this.className === 'leaflet-control-layers leaflet-control leaflet-control-layers-expanded') { - var elem = this.children[1].children[2].children[0].children[0].children[0].children[0]; - if(elem) setTimeout(() => elem.focus(), 0); - } - }, - - _withinZoomBounds: function(zoom, range) { - return range.min <= zoom && zoom <= range.max; - }, - _addItem: function (obj) { - var layercontrols = obj.layer.getLayerUserControlsHTML(); - // the input is required by Leaflet... - obj.input = layercontrols.querySelector('input.leaflet-control-layers-selector'); + _withinZoomBounds: function (zoom, range) { + return range.min <= zoom && zoom <= range.max; + }, + _addItem: function (obj) { + var layercontrols = obj.layer.getLayerUserControlsHTML(); + // the input is required by Leaflet... + obj.input = layercontrols.querySelector( + 'input.leaflet-control-layers-selector' + ); - this._layerControlInputs.push(obj.input); - obj.input.layerId = L.stamp(obj.layer); + this._layerControlInputs.push(obj.input); + obj.input.layerId = L.stamp(obj.layer); - L.DomEvent.on(obj.input, 'click', this._onInputClick, this); - // this is necessary because when there are several layers in the - // layer control, the response to the last one can be a long time - // after the info is first displayed, so we have to go back and - // verify the layer element is not disabled and can have an enabled input. - obj.layer.on('extentload', this._validateInput, this); - this._overlaysList.appendChild(layercontrols); - return layercontrols; - }, + L.DomEvent.on(obj.input, 'click', this._onInputClick, this); + // this is necessary because when there are several layers in the + // layer control, the response to the last one can be a long time + // after the info is first displayed, so we have to go back and + // verify the layer element is not disabled and can have an enabled input. + obj.layer.on('extentload', this._validateInput, this); + this._overlaysList.appendChild(layercontrols); + return layercontrols; + }, - //overrides collapse and conditionally collapses the panel - collapse: function(e){ - if ( - e.target.tagName === "SELECT" || - e.relatedTarget && + //overrides collapse and conditionally collapses the panel + collapse: function (e) { + if ( + e.target.tagName === 'SELECT' || + (e.relatedTarget && e.relatedTarget.parentElement && - ( - e.relatedTarget.className === "mapml-contextmenu mapml-layer-menu" || - e.relatedTarget.parentElement.className === "mapml-contextmenu mapml-layer-menu" - ) || - (this._map && this._map.contextMenu._layerMenu.style.display === "block") - ) return this; + (e.relatedTarget.className === 'mapml-contextmenu mapml-layer-menu' || + e.relatedTarget.parentElement.className === + 'mapml-contextmenu mapml-layer-menu')) || + (this._map && this._map.contextMenu._layerMenu.style.display === 'block') + ) + return this; - L.DomUtil.removeClass(this._container, 'leaflet-control-layers-expanded'); - if (e.originalEvent?.pointerType === 'touch') { - this._container._isExpanded = false; - } - return this; - }, - _preventDefaultContextMenu: function (e) { - let latlng = this._map.mouseEventToLatLng(e); - let containerPoint = this._map.mouseEventToContainerPoint(e); - e.preventDefault(); - // for touch devices, when the layer control is not expanded, - // the layer context menu should not show on map - if (!this._container._isExpanded && e.pointerType === 'touch') { - this._container._isExpanded = true; - return; - } - this._map.fire('contextmenu', - { originalEvent: e, - containerPoint: containerPoint, - latlng: latlng }); + L.DomUtil.removeClass(this._container, 'leaflet-control-layers-expanded'); + if (e.originalEvent?.pointerType === 'touch') { + this._container._isExpanded = false; + } + return this; + }, + _preventDefaultContextMenu: function (e) { + let latlng = this._map.mouseEventToLatLng(e); + let containerPoint = this._map.mouseEventToContainerPoint(e); + e.preventDefault(); + // for touch devices, when the layer control is not expanded, + // the layer context menu should not show on map + if (!this._container._isExpanded && e.pointerType === 'touch') { + this._container._isExpanded = true; + return; } + this._map.fire('contextmenu', { + originalEvent: e, + containerPoint: containerPoint, + latlng: latlng + }); + } }); export var layerControl = function (layers, options) { - return new LayerControl(layers, options); + return new LayerControl(layers, options); }; diff --git a/src/mapml/control/ReloadButton.js b/src/mapml/control/ReloadButton.js index 3ae9217a1..90bbc256f 100644 --- a/src/mapml/control/ReloadButton.js +++ b/src/mapml/control/ReloadButton.js @@ -1,17 +1,17 @@ export var ReloadButton = L.Control.extend({ options: { - position: 'topleft', + position: 'topleft' }, onAdd: function (map) { - let container = L.DomUtil.create("div", "mapml-reload-button leaflet-bar"); + let container = L.DomUtil.create('div', 'mapml-reload-button leaflet-bar'); - let link = L.DomUtil.create("button", "mapml-reload-button", container); + let link = L.DomUtil.create('button', 'mapml-reload-button', container); link.innerHTML = ""; link.title = M.options.locale.cmReload; - link.setAttribute("type", "button"); - link.classList.add("mapml-button"); - link.setAttribute('aria-label', "Reload"); + link.setAttribute('type', 'button'); + link.classList.add('mapml-button'); + link.setAttribute('aria-label', 'Reload'); L.DomEvent.disableClickPropagation(link); L.DomEvent.on(link, 'click', L.DomEvent.stop); @@ -49,12 +49,15 @@ export var ReloadButton = L.Control.extend({ _updateDisabled: function () { setTimeout(() => { - L.DomUtil.removeClass(this._reloadButton, "leaflet-disabled"); - this._reloadButton.setAttribute("aria-disabled", "false"); + L.DomUtil.removeClass(this._reloadButton, 'leaflet-disabled'); + this._reloadButton.setAttribute('aria-disabled', 'false'); - if (this._map && (this._disabled || this._map.options.mapEl._history.length <= 1)) { - L.DomUtil.addClass(this._reloadButton, "leaflet-disabled"); - this._reloadButton.setAttribute("aria-disabled", "true"); + if ( + this._map && + (this._disabled || this._map.options.mapEl._history.length <= 1) + ) { + L.DomUtil.addClass(this._reloadButton, 'leaflet-disabled'); + this._reloadButton.setAttribute('aria-disabled', 'true'); } }, 0); } @@ -62,4 +65,4 @@ export var ReloadButton = L.Control.extend({ export var reloadButton = function (options) { return new ReloadButton(options); -}; \ No newline at end of file +}; diff --git a/src/mapml/control/ScaleBar.js b/src/mapml/control/ScaleBar.js index 93d4f9512..7e099001c 100644 --- a/src/mapml/control/ScaleBar.js +++ b/src/mapml/control/ScaleBar.js @@ -1,92 +1,106 @@ export var ScaleBar = L.Control.Scale.extend({ - options: { - maxWidth: 100, - updateWhenIdle: true, - position: 'bottomleft' - }, - - onAdd: function (map) { - - // create output tag for screenreader to read from - let outputScale = ""; - map._container.insertAdjacentHTML("beforeend", outputScale); - - // initialize _container - this._container = L.DomUtil.create('div','mapml-control-scale'); - let scaleControl = L.Control.Scale.prototype.onAdd.call(this, map); - this._container.appendChild(scaleControl); - this._container.setAttribute('tabindex',0); - this._scaleControl = this; - - // run on load - setTimeout(() => { - this._updateOutput(); - this._focusOutput(); - }, 0); - - // update whenever map is zoomed or dragged - map.on('zoomend moveend', this._updateOutput, this); - - // have screenreader read out everytime the map is focused - this._map._container.addEventListener('focus', () => this._focusOutput()); - - return this._container; - }, - - onRemove: function (map) { - map.off('zoomend moveend', this._updateOutput, this); - }, - - getContainer: function () { - return this._container; - }, - - _pixelsToDistance: function (px, units) { - let dpi = window.devicePixelRatio * 96; // default dpi - if (units === "metric") { - return px / dpi * 2.54; // inches to cm - } - return px / dpi; - }, - - _scaleLength: function (scale) { - let scaleLength = scale.getAttribute("style"); - let finalLength = parseInt(scaleLength.match(/width:\s*(\d+)px/)[1]); - - return finalLength; - - }, - - _focusOutput: function () { + options: { + maxWidth: 100, + updateWhenIdle: true, + position: 'bottomleft' + }, + + onAdd: function (map) { + // create output tag for screenreader to read from + let outputScale = + ""; + map._container.insertAdjacentHTML('beforeend', outputScale); + + // initialize _container + this._container = L.DomUtil.create('div', 'mapml-control-scale'); + let scaleControl = L.Control.Scale.prototype.onAdd.call(this, map); + this._container.appendChild(scaleControl); + this._container.setAttribute('tabindex', 0); + this._scaleControl = this; + + // run on load + setTimeout(() => { + this._updateOutput(); + this._focusOutput(); + }, 0); + + // update whenever map is zoomed or dragged + map.on('zoomend moveend', this._updateOutput, this); + + // have screenreader read out everytime the map is focused + this._map._container.addEventListener('focus', () => this._focusOutput()); + + return this._container; + }, + + onRemove: function (map) { + map.off('zoomend moveend', this._updateOutput, this); + }, + + getContainer: function () { + return this._container; + }, + + _pixelsToDistance: function (px, units) { + let dpi = window.devicePixelRatio * 96; // default dpi + if (units === 'metric') { + return (px / dpi) * 2.54; // inches to cm + } + return px / dpi; + }, + + _scaleLength: function (scale) { + let scaleLength = scale.getAttribute('style'); + let finalLength = parseInt(scaleLength.match(/width:\s*(\d+)px/)[1]); + + return finalLength; + }, + + _focusOutput: function () { + setTimeout(() => { + let outputFocus = this._map._container.querySelector( + '.mapml-screen-reader-output-scale' + ); + outputFocus.textContent = ''; setTimeout(() => { - let outputFocus = this._map._container.querySelector('.mapml-screen-reader-output-scale'); - outputFocus.textContent = ""; - setTimeout(() => { - outputFocus.textContent = this._container.getAttribute('aria-label'); - }, 100); - }, 0); - }, - - _updateOutput: function () { - let output = ""; - let scaleLine = this._scaleControl.getContainer().getElementsByClassName('leaflet-control-scale-line')[0]; - - if (this.options.metric) { - let distance = parseFloat((this._pixelsToDistance(this._scaleLength(scaleLine),"metric")).toFixed(1)); - output = `${distance} centimeters to ${scaleLine.textContent.trim()}`; - output = output.replace(/(\d+)\s*m\b/g, "$1 meters"); - output = output.replace(/ km/g, " kilometers"); - } else { - let distance = parseFloat((this._pixelsToDistance(this._scaleLength(scaleLine),"imperial")).toFixed(1)); - output = `${distance} inches to ${scaleLine.textContent.trim()}`; - output = output.replace(/ft/g, "feet"); - output = output.replace(/mi/g, "miles"); - } - - this._container.setAttribute('aria-label', output); - this._map._container.querySelector(".mapml-screen-reader-output-scale").textContent = output; + outputFocus.textContent = this._container.getAttribute('aria-label'); + }, 100); + }, 0); + }, + + _updateOutput: function () { + let output = ''; + let scaleLine = this._scaleControl + .getContainer() + .getElementsByClassName('leaflet-control-scale-line')[0]; + + if (this.options.metric) { + let distance = parseFloat( + this._pixelsToDistance(this._scaleLength(scaleLine), 'metric').toFixed( + 1 + ) + ); + output = `${distance} centimeters to ${scaleLine.textContent.trim()}`; + output = output.replace(/(\d+)\s*m\b/g, '$1 meters'); + output = output.replace(/ km/g, ' kilometers'); + } else { + let distance = parseFloat( + this._pixelsToDistance( + this._scaleLength(scaleLine), + 'imperial' + ).toFixed(1) + ); + output = `${distance} inches to ${scaleLine.textContent.trim()}`; + output = output.replace(/ft/g, 'feet'); + output = output.replace(/mi/g, 'miles'); } - }); + + this._container.setAttribute('aria-label', output); + this._map._container.querySelector( + '.mapml-screen-reader-output-scale' + ).textContent = output; + } +}); export var scaleBar = function (options) { - return new ScaleBar(options); + return new ScaleBar(options); }; diff --git a/src/mapml/features/feature.js b/src/mapml/features/feature.js index 2ba180a0a..cb501139e 100644 --- a/src/mapml/features/feature.js +++ b/src/mapml/features/feature.js @@ -19,7 +19,6 @@ * ]; */ export var Feature = L.Path.extend({ - /** * Initializes the M.Feature * @param {HTMLElement} markup - The markup representation of the feature @@ -28,14 +27,17 @@ export var Feature = L.Path.extend({ initialize: function (markup, options) { this.type = markup.tagName.toUpperCase(); - if(this.type === "MAP-POINT" || this.type === "MAP-MULTIPOINT") options.fillOpacity = 1; + if (this.type === 'MAP-POINT' || this.type === 'MAP-MULTIPOINT') + options.fillOpacity = 1; - if(options.wrappers.length > 0) + if (options.wrappers.length > 0) options = Object.assign(this._convertWrappers(options.wrappers), options); L.setOptions(this, options); this.group = this.options.group; - this.options.interactive = this.options.link || (this.options.properties && this.options.onEachFeature); + this.options.interactive = + this.options.link || + (this.options.properties && this.options.onEachFeature); this._parts = []; this._markup = markup; @@ -43,7 +45,7 @@ export var Feature = L.Path.extend({ this._convertMarkup(); - if(markup.querySelector('map-span') || markup.querySelector('map-a')){ + if (markup.querySelector('map-span') || markup.querySelector('map-a')) { this._generateOutlinePoints(); } @@ -57,70 +59,100 @@ export var Feature = L.Path.extend({ * @param leafletLayer */ attachLinkHandler: function (elem, link, leafletLayer) { - let dragStart, container = document.createElement('div'), p = document.createElement('p'), hovered = false; + let dragStart, + container = document.createElement('div'), + p = document.createElement('p'), + hovered = false; container.classList.add('mapml-link-preview'); container.appendChild(p); elem.classList.add('map-a'); - if (link.visited) elem.classList.add("map-a-visited"); - elem.mousedown = e => dragStart = {x:e.clientX, y:e.clientY}; + if (link.visited) elem.classList.add('map-a-visited'); + elem.mousedown = (e) => (dragStart = { x: e.clientX, y: e.clientY }); L.DomEvent.on(elem, 'mousedown', elem.mousedown, this); elem.mouseup = (e) => { if (e.button !== 0) return; // don't trigger when button isn't left click - let onTop = true, nextLayer = this.options._leafletLayer._layerEl.nextElementSibling; - while(nextLayer && onTop){ - if(nextLayer.tagName && nextLayer.tagName.toUpperCase() === "LAYER-") + let onTop = true, + nextLayer = this.options._leafletLayer._layerEl.nextElementSibling; + while (nextLayer && onTop) { + if (nextLayer.tagName && nextLayer.tagName.toUpperCase() === 'LAYER-') onTop = !(nextLayer.checked && nextLayer._layer.queryable); nextLayer = nextLayer.nextElementSibling; } - if(onTop && dragStart) { + if (onTop && dragStart) { //M._handleLink gets called twice, once in the target phase on the path element, then in the bubble phase on the g element //Using stopPropagation leaves the mouse in the mousedown state - if(e.eventPhase === Event.BUBBLING_PHASE) return; - let dist = Math.sqrt(Math.pow(dragStart.x - e.clientX, 2) + Math.pow(dragStart.y - e.clientY, 2)); - if (dist <= 5){ + if (e.eventPhase === Event.BUBBLING_PHASE) return; + let dist = Math.sqrt( + Math.pow(dragStart.x - e.clientX, 2) + + Math.pow(dragStart.y - e.clientY, 2) + ); + if (dist <= 5) { link.visited = true; - elem.setAttribute("stroke", "#6c00a2"); - elem.classList.add("map-a-visited"); + elem.setAttribute('stroke', '#6c00a2'); + elem.classList.add('map-a-visited'); M._handleLink(link, leafletLayer); } } }; - L.DomEvent.on(elem, "mouseup", elem.mouseup, this); - L.DomEvent.on(elem, "keypress", (e) => { - L.DomEvent.stop(e); - if(e.keyCode === 13 || e.keyCode === 32) { - link.visited = true; - elem.setAttribute("stroke", "#6c00a2"); - elem.classList.add("map-a-visited"); - M._handleLink(link, leafletLayer); - } - }, this); - L.DomEvent.on(elem, 'mouseenter keyup', (e) => { - if(e.target !== e.currentTarget) return; - hovered = true; - let resolver = document.createElement('a'), mapWidth = this._map.getContainer().clientWidth; - resolver.href = link.url; - p.innerHTML = resolver.href; + L.DomEvent.on(elem, 'mouseup', elem.mouseup, this); + L.DomEvent.on( + elem, + 'keypress', + (e) => { + L.DomEvent.stop(e); + if (e.keyCode === 13 || e.keyCode === 32) { + link.visited = true; + elem.setAttribute('stroke', '#6c00a2'); + elem.classList.add('map-a-visited'); + M._handleLink(link, leafletLayer); + } + }, + this + ); + L.DomEvent.on( + elem, + 'mouseenter keyup', + (e) => { + if (e.target !== e.currentTarget) return; + hovered = true; + let resolver = document.createElement('a'), + mapWidth = this._map.getContainer().clientWidth; + resolver.href = link.url; + p.innerHTML = resolver.href; - this._map.getContainer().appendChild(container); + this._map.getContainer().appendChild(container); - while(p.clientWidth > mapWidth/2){ - p.innerHTML = p.innerHTML.substring(0, p.innerHTML.length - 5) + "..."; - } - setTimeout(()=>{ - if(hovered) p.innerHTML = resolver.href; - }, 1000); - }, this); - L.DomEvent.on(elem, 'mouseout keydown mousedown', (e) => { - if(e.target !== e.currentTarget || !container.parentElement) return; - hovered = false; - this._map.getContainer().removeChild(container); - }, this); - L.DomEvent.on(leafletLayer._map.getContainer(),'mouseout mouseenter click', (e) => { //adds a lot of event handlers - if(!container.parentElement) return; - hovered = false; - this._map.getContainer().removeChild(container); - }, this); + while (p.clientWidth > mapWidth / 2) { + p.innerHTML = + p.innerHTML.substring(0, p.innerHTML.length - 5) + '...'; + } + setTimeout(() => { + if (hovered) p.innerHTML = resolver.href; + }, 1000); + }, + this + ); + L.DomEvent.on( + elem, + 'mouseout keydown mousedown', + (e) => { + if (e.target !== e.currentTarget || !container.parentElement) return; + hovered = false; + this._map.getContainer().removeChild(container); + }, + this + ); + L.DomEvent.on( + leafletLayer._map.getContainer(), + 'mouseout mouseenter click', + (e) => { + //adds a lot of event handlers + if (!container.parentElement) return; + hovered = false; + this._map.getContainer().removeChild(container); + }, + this + ); }, /** @@ -131,7 +163,9 @@ export var Feature = L.Path.extend({ * @private */ _project: function (addedMap, tileOrigin = undefined, zoomingTo = undefined) { - let map = addedMap || this._map, origin = tileOrigin || map.getPixelOrigin(), zoom = zoomingTo === undefined ? map.getZoom() : zoomingTo; + let map = addedMap || this._map, + origin = tileOrigin || map.getPixelOrigin(), + zoom = zoomingTo === undefined ? map.getZoom() : zoomingTo; for (let p of this._parts) { p.pixelRings = this._convertRing(p.rings, map, origin, zoom); for (let subP of p.subrings) { @@ -141,7 +175,9 @@ export var Feature = L.Path.extend({ if (!this._outline) return; this.pixelOutline = []; for (let o of this._outline) { - this.pixelOutline = this.pixelOutline.concat(this._convertRing(o, map, origin, zoom)); + this.pixelOutline = this.pixelOutline.concat( + this._convertRing(o, map, origin, zoom) + ); } }, @@ -156,7 +192,8 @@ export var Feature = L.Path.extend({ */ _convertRing: function (r, map, origin, zoom) { // TODO: Implement Ramer-Douglas-Peucer Algo for simplifying points - let scale = map.options.crs.scale(zoom), parts = []; + let scale = map.options.crs.scale(zoom), + parts = []; for (let sub of r) { let interm = []; for (let p of sub.points) { @@ -183,17 +220,19 @@ export var Feature = L.Path.extend({ * @private */ _convertWrappers: function (elems) { - if(!elems || elems.length === 0) return; - let classList = '', output = {}; - for(let elem of elems){ - if(elem.tagName.toUpperCase() !== "MAP-A" && elem.className){ - classList +=`${elem.className} `; - } else if(!output.link && elem.getAttribute("href")) { + if (!elems || elems.length === 0) return; + let classList = '', + output = {}; + for (let elem of elems) { + if (elem.tagName.toUpperCase() !== 'MAP-A' && elem.className) { + classList += `${elem.className} `; + } else if (!output.link && elem.getAttribute('href')) { let link = {}; - link.url = elem.getAttribute("href"); - if(elem.hasAttribute("target")) link.target = elem.getAttribute("target"); - if(elem.hasAttribute("type")) link.type = elem.getAttribute("type"); - if(elem.hasAttribute("inplace")) link.inPlace = true; + link.url = elem.getAttribute('href'); + if (elem.hasAttribute('target')) + link.target = elem.getAttribute('target'); + if (elem.hasAttribute('type')) link.type = elem.getAttribute('type'); + if (elem.hasAttribute('inplace')) link.inPlace = true; output.link = link; } } @@ -210,26 +249,43 @@ export var Feature = L.Path.extend({ let attr = this._markup.attributes; this.featureAttributes = {}; - if(this.options.link && this._markup.parentElement.tagName.toUpperCase() === "MAP-A" && this._markup.parentElement.parentElement.tagName.toUpperCase() !== "MAP-GEOMETRY") - this.featureAttributes.tabindex = "0"; - for(let i = 0; i < attr.length; i++){ + if ( + this.options.link && + this._markup.parentElement.tagName.toUpperCase() === 'MAP-A' && + this._markup.parentElement.parentElement.tagName.toUpperCase() !== + 'MAP-GEOMETRY' + ) + this.featureAttributes.tabindex = '0'; + for (let i = 0; i < attr.length; i++) { this.featureAttributes[attr[i].name] = attr[i].value; } let first = true; - for (let c of this._markup.querySelectorAll('map-coordinates')) { //loops through the coordinates of the child - let ring = [], subRings = []; - this._coordinateToArrays(c, ring, subRings, this.options.className); //creates an array of pcrs points for the main ring and the subparts - if (!first && this.type === "MAP-POLYGON") { + for (let c of this._markup.querySelectorAll('map-coordinates')) { + //loops through the coordinates of the child + let ring = [], + subRings = []; + this._coordinateToArrays(c, ring, subRings, this.options.className); //creates an array of pcrs points for the main ring and the subparts + if (!first && this.type === 'MAP-POLYGON') { this._parts[0].rings.push(ring[0]); if (subRings.length > 0) this._parts[0].subrings = this._parts[0].subrings.concat(subRings); - } else if (this.type === "MAP-MULTIPOINT") { + } else if (this.type === 'MAP-MULTIPOINT') { for (let point of ring[0].points.concat(subRings)) { - this._parts.push({ rings: [{ points: [point] }], subrings: [], cls:`${point.cls || ""} ${this.options.className || ""}`.trim() }); + this._parts.push({ + rings: [{ points: [point] }], + subrings: [], + cls: `${point.cls || ''} ${this.options.className || ''}`.trim() + }); } } else { - this._parts.push({ rings: ring, subrings: subRings, cls: `${this.featureAttributes.class || ""} ${this.options.className || ""}`.trim() }); + this._parts.push({ + rings: ring, + subrings: subRings, + cls: `${this.featureAttributes.class || ''} ${ + this.options.className || '' + }`.trim() + }); } first = false; } @@ -240,32 +296,49 @@ export var Feature = L.Path.extend({ * @private */ _generateOutlinePoints: function () { - if (this.type === "MAP-MULTIPOINT" || this.type === "MAP-POINT" || this.type === "MAP-LINESTRING" || this.type === "MAP-MULTILINESTRING") return; + if ( + this.type === 'MAP-MULTIPOINT' || + this.type === 'MAP-POINT' || + this.type === 'MAP-LINESTRING' || + this.type === 'MAP-MULTILINESTRING' + ) + return; this._outline = []; for (let coords of this._markup.querySelectorAll('map-coordinates')) { - let nodes = coords.childNodes, cur = 0, tempDiv = document.createElement('div'), nodeLength = nodes.length; - for(let i = 0; i < nodes.length; i++){ - if(nodes[i].textContent.trim().length === 0){ + let nodes = coords.childNodes, + cur = 0, + tempDiv = document.createElement('div'), + nodeLength = nodes.length; + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].textContent.trim().length === 0) { nodes[i].remove(); } } for (let n of nodes) { let line = []; - if (!n.tagName) { //no tagName means it's text content - let c = '', ind = (((cur - 1)%nodes.length) + nodes.length) % nodes.length; // this equation turns Javascript's % to how it behaves in C for example + if (!n.tagName) { + //no tagName means it's text content + let c = '', + ind = (((cur - 1) % nodes.length) + nodes.length) % nodes.length; // this equation turns Javascript's % to how it behaves in C for example if (nodes[ind].tagName) { let prev = nodes[ind].textContent.trim().split(/\s+/); c += `${prev[prev.length - 2]} ${prev[prev.length - 1]} `; } c += n.textContent; - ind = (((cur + 1)%nodes.length) + nodes.length) % nodes.length; // this is equivalent to C/C++'s (cur + 1) % nodes.length + ind = (((cur + 1) % nodes.length) + nodes.length) % nodes.length; // this is equivalent to C/C++'s (cur + 1) % nodes.length if (nodes[ind].tagName) { let next = nodes[ind].textContent.trim().split(/\s+/); c += `${next[0]} ${next[1]} `; } tempDiv.innerHTML = c; - this._coordinateToArrays(tempDiv, line, [], true, this.featureAttributes.class || this.options.className); + this._coordinateToArrays( + tempDiv, + line, + [], + true, + this.featureAttributes.class || this.options.className + ); this._outline.push(line); } cur++; @@ -283,16 +356,37 @@ export var Feature = L.Path.extend({ * @param parents * @private */ - _coordinateToArrays: function (coords, main, subParts, isFirst = true, cls = undefined, parents = []) { + _coordinateToArrays: function ( + coords, + main, + subParts, + isFirst = true, + cls = undefined, + parents = [] + ) { for (let span of coords.children) { - this._coordinateToArrays(span, main, subParts, false, span.getAttribute("class"), parents.concat([span])); + this._coordinateToArrays( + span, + main, + subParts, + false, + span.getAttribute('class'), + parents.concat([span]) + ); } - let noSpan = coords.textContent.replace(/(<([^>]+)>)/ig, ''), - pairs = noSpan.match(/(\S+\s+\S+)/gim), local = [], bounds; + let noSpan = coords.textContent.replace(/(<([^>]+)>)/gi, ''), + pairs = noSpan.match(/(\S+\s+\S+)/gim), + local = [], + bounds; for (let p of pairs) { let numPair = []; p.split(/\s+/gim).forEach(M._parseNumber, numPair); - let point = M.pointToPCRSPoint(L.point(numPair), this.options.zoom, this.options.projection, this.options.nativeCS); + let point = M.pointToPCRSPoint( + L.point(numPair), + this.options.zoom, + this.options.projection, + this.options.nativeCS + ); local.push(point); bounds = bounds ? bounds.extend(point) : L.bounds(point, point); } @@ -305,20 +399,23 @@ export var Feature = L.Path.extend({ if (isFirst) { main.push({ points: local }); } else { - let attrMap = {}, attr = coords.attributes, wrapperAttr = this._convertWrappers(parents); - if(wrapperAttr.link) attrMap.tabindex = "0"; - for(let i = 0; i < attr.length; i++){ - if(attr[i].name === "class") continue; + let attrMap = {}, + attr = coords.attributes, + wrapperAttr = this._convertWrappers(parents); + if (wrapperAttr.link) attrMap.tabindex = '0'; + for (let i = 0; i < attr.length; i++) { + if (attr[i].name === 'class') continue; attrMap[attr[i].name] = attr[i].value; } subParts.unshift({ points: local, center: bounds.getCenter(), - cls: `${cls || ""} ${wrapperAttr.className || ""}`.trim(), + cls: `${cls || ''} ${wrapperAttr.className || ''}`.trim(), attr: attrMap, link: wrapperAttr.link, linkTarget: wrapperAttr.linkTarget, - linkType: wrapperAttr.linkType}); + linkType: wrapperAttr.linkType + }); } }, @@ -353,7 +450,7 @@ export var Feature = L.Path.extend({ getPCRSCenter: function () { return this._bounds.getCenter(); - }, + } }); /** @@ -364,4 +461,4 @@ export var Feature = L.Path.extend({ */ export var feature = function (markup, options) { return new Feature(markup, options); -}; \ No newline at end of file +}; diff --git a/src/mapml/features/featureGroup.js b/src/mapml/features/featureGroup.js index 2ccb119b9..906cc86ae 100644 --- a/src/mapml/features/featureGroup.js +++ b/src/mapml/features/featureGroup.js @@ -1,35 +1,47 @@ export var FeatureGroup = L.FeatureGroup.extend({ - /** * Initialize the feature group * @param {M.Feature[]} layers * @param {Object} options */ initialize: function (layers, options) { - if(options.wrappers && options.wrappers.length > 0) - options = Object.assign(M.Feature.prototype._convertWrappers(options.wrappers), options); + if (options.wrappers && options.wrappers.length > 0) + options = Object.assign( + M.Feature.prototype._convertWrappers(options.wrappers), + options + ); L.LayerGroup.prototype.initialize.call(this, layers, options); this._featureEl = this.options.mapmlFeature; - if((this.options.onEachFeature && this.options.properties) || this.options.link) { - L.DomUtil.addClass(this.options.group, "leaflet-interactive"); + if ( + (this.options.onEachFeature && this.options.properties) || + this.options.link + ) { + L.DomUtil.addClass(this.options.group, 'leaflet-interactive'); let firstLayer = layers[Object.keys(layers)[0]]; - if(layers.length === 1 && firstLayer.options.link) this.options.link = firstLayer.options.link; - if(this.options.link){ - M.Feature.prototype.attachLinkHandler.call(this, this.options.group, this.options.link, this.options._leafletLayer); + if (layers.length === 1 && firstLayer.options.link) + this.options.link = firstLayer.options.link; + if (this.options.link) { + M.Feature.prototype.attachLinkHandler.call( + this, + this.options.group, + this.options.link, + this.options._leafletLayer + ); this.options.group.setAttribute('role', 'link'); } else { - this.options.group.setAttribute("aria-expanded", "false"); + this.options.group.setAttribute('aria-expanded', 'false'); this.options.group.setAttribute('role', 'button'); this.options.onEachFeature(this.options.properties, this); - this.off("click", this._openPopup); + this.off('click', this._openPopup); } } - - L.DomEvent.on(this.options.group, "keyup keydown", this._handleFocus, this); + + L.DomEvent.on(this.options.group, 'keyup keydown', this._handleFocus, this); this.options.group.setAttribute('aria-label', this.options.accessibleTitle); - if(this.options.featureID) this.options.group.setAttribute("data-fid", this.options.featureID); + if (this.options.featureID) + this.options.group.setAttribute('data-fid', this.options.featureID); for (let feature of layers) { feature._groupLayer = this; } @@ -42,16 +54,21 @@ export var FeatureGroup = L.FeatureGroup.extend({ updateInteraction: function () { let map = this._map || this.options._leafletLayer._map; - if(this.options.onEachFeature || this.options.link) - map.featureIndex.addToIndex(this, this.getPCRSCenter(), this.options.group); + if (this.options.onEachFeature || this.options.link) + map.featureIndex.addToIndex( + this, + this.getPCRSCenter(), + this.options.group + ); for (let layerID in this._layers) { let layer = this._layers[layerID]; - for(let part of layer._parts){ - if(layer.featureAttributes && layer.featureAttributes.tabindex) + for (let part of layer._parts) { + if (layer.featureAttributes && layer.featureAttributes.tabindex) map.featureIndex.addToIndex(layer, layer.getPCRSCenter(), part.path); - for(let subPart of part.subrings) { - if(subPart.attr && subPart.attr.tabindex) map.featureIndex.addToIndex(layer, subPart.center, subPart.path); + for (let subPart of part.subrings) { + if (subPart.attr && subPart.attr.tabindex) + map.featureIndex.addToIndex(layer, subPart.center, subPart.path); } } } @@ -65,15 +82,18 @@ export var FeatureGroup = L.FeatureGroup.extend({ * @returns {Boolean} * @private */ - _checkRender: function(zoom, vectorMinZoom, vectorMaxZoom) { + _checkRender: function (zoom, vectorMinZoom, vectorMaxZoom) { let minZoom = this._featureEl.getAttribute('min'), - maxZoom = this._featureEl.getAttribute('max'); + maxZoom = this._featureEl.getAttribute('max'); // if the current map zoom falls below/above the zoom bounds of the vector layer if (zoom > vectorMaxZoom || zoom < vectorMinZoom) return false; // if no min and max attribute present if (minZoom === null && maxZoom === null) return true; // if the current map zoom falls below/above the [min, max] range - if ((minZoom !== null && zoom < +minZoom) || (maxZoom !== null && zoom > +maxZoom)) { + if ( + (minZoom !== null && zoom < +minZoom) || + (maxZoom !== null && zoom > +maxZoom) + ) { return false; } return true; @@ -84,58 +104,83 @@ export var FeatureGroup = L.FeatureGroup.extend({ * @param {L.DOMEvent} e - Event that occurred * @private */ - _handleFocus: function(e) { - // tab, shift, cr, esc, up, left, down, right, - if(([9, 16, 27, 37, 38, 39, 40].includes(e.keyCode)) && e.type === "keydown"){ + _handleFocus: function (e) { + // tab, shift, cr, esc, up, left, down, right, + if ( + [9, 16, 27, 37, 38, 39, 40].includes(e.keyCode) && + e.type === 'keydown' + ) { let index = this._map.featureIndex.currentIndex; // Down/right arrow keys replicate tabbing through the feature index // Up/left arrow keys replicate shift-tabbing through the feature index - if(e.keyCode === 37 || e.keyCode === 38) { + if (e.keyCode === 37 || e.keyCode === 38) { L.DomEvent.stop(e); - this._map.featureIndex.inBoundFeatures[index].path.setAttribute("tabindex", -1); - if(index === 0) { - this._map.featureIndex.inBoundFeatures[this._map.featureIndex.inBoundFeatures.length - 1].path.focus(); - this._map.featureIndex.currentIndex = this._map.featureIndex.inBoundFeatures.length - 1; + this._map.featureIndex.inBoundFeatures[index].path.setAttribute( + 'tabindex', + -1 + ); + if (index === 0) { + this._map.featureIndex.inBoundFeatures[ + this._map.featureIndex.inBoundFeatures.length - 1 + ].path.focus(); + this._map.featureIndex.currentIndex = + this._map.featureIndex.inBoundFeatures.length - 1; } else { this._map.featureIndex.inBoundFeatures[index - 1].path.focus(); this._map.featureIndex.currentIndex--; } } else if (e.keyCode === 39 || e.keyCode === 40) { L.DomEvent.stop(e); - this._map.featureIndex.inBoundFeatures[index].path.setAttribute("tabindex", -1); - if(index === this._map.featureIndex.inBoundFeatures.length - 1) { + this._map.featureIndex.inBoundFeatures[index].path.setAttribute( + 'tabindex', + -1 + ); + if (index === this._map.featureIndex.inBoundFeatures.length - 1) { this._map.featureIndex.inBoundFeatures[0].path.focus(); this._map.featureIndex.currentIndex = 0; } else { this._map.featureIndex.inBoundFeatures[index + 1].path.focus(); this._map.featureIndex.currentIndex++; } - } else if(e.keyCode === 27){ - let shadowRoot = this._map.options.mapEl.shadowRoot ? this._map.options.mapEl.shadowRoot : - this._map.options.mapEl.querySelector(".mapml-web-map").shadowRoot; - if(shadowRoot.activeElement.nodeName !== "g") return; + } else if (e.keyCode === 27) { + let shadowRoot = this._map.options.mapEl.shadowRoot + ? this._map.options.mapEl.shadowRoot + : this._map.options.mapEl.querySelector('.mapml-web-map').shadowRoot; + if (shadowRoot.activeElement.nodeName !== 'g') return; this._map._container.focus(); } else if (e.keyCode === 9) { let obj = this; setTimeout(function () { - obj._map.featureIndex.inBoundFeatures[0].path.setAttribute("tabindex", 0); + obj._map.featureIndex.inBoundFeatures[0].path.setAttribute( + 'tabindex', + 0 + ); }, 0); } - // tab, shift, cr, esc, right, left, up, down, - // 1, 2, 3, 4, 5, 6, 7 (featureIndexOverlay available index items + // tab, shift, cr, esc, right, left, up, down, + // 1, 2, 3, 4, 5, 6, 7 (featureIndexOverlay available index items // [8, 9] being allocated to next, previous menu items). - } else if (!([9, 16, 13, 27, 37, 38, 39, 40, 49, 50, 51, 52, 53, 54, 55].includes(e.keyCode))){ + } else if ( + ![9, 16, 13, 27, 37, 38, 39, 40, 49, 50, 51, 52, 53, 54, 55].includes( + e.keyCode + ) + ) { this._map.featureIndex.currentIndex = 0; this._map.featureIndex.inBoundFeatures[0].path.focus(); } - + // 27 added so that the tooltip opens when dismissing popup with 'esc' key - if(e.target.tagName.toUpperCase() !== "G") return; - if(([9, 13, 16, 37, 38, 39, 40, 49, 50, 51, 52, 53, 54, 55, 27].includes(e.keyCode)) && e.type === "keyup") { + if (e.target.tagName.toUpperCase() !== 'G') return; + if ( + [9, 13, 16, 37, 38, 39, 40, 49, 50, 51, 52, 53, 54, 55, 27].includes( + e.keyCode + ) && + e.type === 'keyup' + ) { this.openTooltip(); - } else if (e.keyCode === 13 || e.keyCode === 32){ + } else if (e.keyCode === 13 || e.keyCode === 32) { this.closeTooltip(); - if(!this.options.link && this.options.onEachFeature){ + if (!this.options.link && this.options.onEachFeature) { L.DomEvent.stop(e); this.openPopup(); } @@ -149,7 +194,7 @@ export var FeatureGroup = L.FeatureGroup.extend({ * @param layer */ addLayer: function (layer) { - if(!layer.options.link && layer.options.interactive) { + if (!layer.options.link && layer.options.interactive) { this.options.onEachFeature(this.options.properties, layer); } L.FeatureGroup.prototype.addLayer.call(this, layer); @@ -160,10 +205,16 @@ export var FeatureGroup = L.FeatureGroup.extend({ * @param e * @private */ - _previousFeature: function(e){ + _previousFeature: function (e) { L.DomEvent.stop(e); - this._map.featureIndex.currentIndex = Math.max(this._map.featureIndex.currentIndex - 1, 0); - let prevFocus = this._map.featureIndex.inBoundFeatures[this._map.featureIndex.currentIndex]; + this._map.featureIndex.currentIndex = Math.max( + this._map.featureIndex.currentIndex - 1, + 0 + ); + let prevFocus = + this._map.featureIndex.inBoundFeatures[ + this._map.featureIndex.currentIndex + ]; prevFocus.path.focus(); this._map.closePopup(); }, @@ -173,17 +224,23 @@ export var FeatureGroup = L.FeatureGroup.extend({ * @param e * @private */ - _nextFeature: function(e){ + _nextFeature: function (e) { L.DomEvent.stop(e); - this._map.featureIndex.currentIndex = Math.min(this._map.featureIndex.currentIndex + 1, this._map.featureIndex.inBoundFeatures.length - 1); - let nextFocus = this._map.featureIndex.inBoundFeatures[this._map.featureIndex.currentIndex]; + this._map.featureIndex.currentIndex = Math.min( + this._map.featureIndex.currentIndex + 1, + this._map.featureIndex.inBoundFeatures.length - 1 + ); + let nextFocus = + this._map.featureIndex.inBoundFeatures[ + this._map.featureIndex.currentIndex + ]; nextFocus.path.focus(); this._map.closePopup(); }, getPCRSCenter: function () { let bounds; - for(let l in this._layers){ + for (let l in this._layers) { let layer = this._layers[l]; if (!bounds) { bounds = L.bounds(layer.getPCRSCenter(), layer.getPCRSCenter()); @@ -192,7 +249,7 @@ export var FeatureGroup = L.FeatureGroup.extend({ } } return bounds.getCenter(); - }, + } }); /** @@ -203,4 +260,4 @@ export var FeatureGroup = L.FeatureGroup.extend({ */ export var featureGroup = function (layers, options) { return new FeatureGroup(layers, options); -}; \ No newline at end of file +}; diff --git a/src/mapml/features/featureRenderer.js b/src/mapml/features/featureRenderer.js index 89a428648..87c25d134 100644 --- a/src/mapml/features/featureRenderer.js +++ b/src/mapml/features/featureRenderer.js @@ -7,7 +7,7 @@ export var FeatureRenderer = L.SVG.extend({ /** * Override method of same name from L.SVG, use the this._container property * to set up the role="none presentation" on featureGroupu container, - * per this recommendation: + * per this recommendation: * https://github.com/Maps4HTML/Web-Map-Custom-Element/pull/471#issuecomment-845192246 * @private overrides ancestor method so that we have a _container to work with */ @@ -16,11 +16,11 @@ export var FeatureRenderer = L.SVG.extend({ // note you have to pass 'this' as the first arg L.SVG.prototype._initContainer.call(this); // knowing that the previous method call creates the this._container, we - // access it and set the role="none presetation" which suppresses the + // access it and set the role="none presetation" which suppresses the // announcement of "Graphic" on each feature focus. this._container.setAttribute('role', 'none presentation'); }, - + /** * Creates all the appropriate path elements for a M.Feature * @param {M.Feature} layer - The M.Feature that needs paths generated @@ -28,29 +28,44 @@ export var FeatureRenderer = L.SVG.extend({ * @private */ _initPath: function (layer, stampLayer = true) { - - if(layer._outline) { + if (layer._outline) { let outlinePath = L.SVG.create('path'); - if (layer.options.className) L.DomUtil.addClass(outlinePath, layer.featureAttributes.class || layer.options.className); + if (layer.options.className) + L.DomUtil.addClass( + outlinePath, + layer.featureAttributes.class || layer.options.className + ); L.DomUtil.addClass(outlinePath, 'mapml-feature-outline'); - outlinePath.style.fill = "none"; + outlinePath.style.fill = 'none'; layer.outlinePath = outlinePath; } //creates the main parts and sub parts paths for (let p of layer._parts) { - if (p.rings){ - this._createPath(p, layer.options.className, layer.featureAttributes['aria-label'], layer.options.interactive, layer.featureAttributes); - if(layer.outlinePath) p.path.style.stroke = "none"; + if (p.rings) { + this._createPath( + p, + layer.options.className, + layer.featureAttributes['aria-label'], + layer.options.interactive, + layer.featureAttributes + ); + if (layer.outlinePath) p.path.style.stroke = 'none'; } if (p.subrings) { for (let r of p.subrings) { - this._createPath(r, layer.options.className, r.attr['aria-label'], (r.link !== undefined), r.attr); + this._createPath( + r, + layer.options.className, + r.attr['aria-label'], + r.link !== undefined, + r.attr + ); } } this._updateStyle(layer); } - if(stampLayer){ + if (stampLayer) { let stamp = L.stamp(layer); this._layers[stamp] = layer; } @@ -65,14 +80,20 @@ export var FeatureRenderer = L.SVG.extend({ * @param {Object} attr - Attributes map * @private */ - _createPath: function (ring, cls, title, interactive = false, attr = undefined) { + _createPath: function ( + ring, + cls, + title, + interactive = false, + attr = undefined + ) { let p = L.SVG.create('path'); ring.path = p; - if(!attr) { + if (!attr) { if (title) p.setAttribute('aria-label', title); } else { - for(let [name, value] of Object.entries(attr)){ - if(name === "id" || name === "tabindex") continue; + for (let [name, value] of Object.entries(attr)) { + if (name === 'id' || name === 'tabindex') continue; p.setAttribute(name, value); } } @@ -92,28 +113,39 @@ export var FeatureRenderer = L.SVG.extend({ * @private */ _addPath: function (layer, container = undefined, interactive = true) { - if (!this._rootGroup && !container) { this._initContainer(); } - let c = container || this._rootGroup, outlineAdded = false; - if(interactive) { + if (!this._rootGroup && !container) { + this._initContainer(); + } + let c = container || this._rootGroup, + outlineAdded = false; + if (interactive) { layer.addInteractiveTarget(layer.group); } for (let p of layer._parts) { - if (p.path) - layer.group.appendChild(p.path); - if (interactive){ - if(layer.options.link) layer.attachLinkHandler(p.path, layer.options.link, layer.options._leafletLayer); + if (p.path) layer.group.appendChild(p.path); + if (interactive) { + if (layer.options.link) + layer.attachLinkHandler( + p.path, + layer.options.link, + layer.options._leafletLayer + ); layer.addInteractiveTarget(p.path); } - if(!outlineAdded && layer.pixelOutline) { + if (!outlineAdded && layer.pixelOutline) { layer.group.appendChild(layer.outlinePath); outlineAdded = true; } for (let subP of p.subrings) { if (subP.path) { - if (subP.link){ - layer.attachLinkHandler(subP.path, subP.link, layer.options._leafletLayer); + if (subP.link) { + layer.attachLinkHandler( + subP.path, + subP.link, + layer.options._leafletLayer + ); layer.addInteractiveTarget(subP.path); } layer.group.appendChild(subP.path); @@ -135,11 +167,10 @@ export var FeatureRenderer = L.SVG.extend({ L.DomUtil.remove(p.path); } for (let subP of p.subrings) { - if (subP.path) - L.DomUtil.remove(subP.path); + if (subP.path) L.DomUtil.remove(subP.path); } } - if(layer.outlinePath) L.DomUtil.remove(layer.outlinePath); + if (layer.outlinePath) L.DomUtil.remove(layer.outlinePath); layer.removeInteractiveTarget(layer.group); L.DomUtil.remove(layer.group); delete this._layers[L.stamp(layer)]; @@ -151,11 +182,18 @@ export var FeatureRenderer = L.SVG.extend({ * @private */ _updateFeature: function (layer) { - if (layer.pixelOutline) this._setPath(layer.outlinePath, this.geometryToPath(layer.pixelOutline, false)); + if (layer.pixelOutline) + this._setPath( + layer.outlinePath, + this.geometryToPath(layer.pixelOutline, false) + ); for (let p of layer._parts) { this._setPath(p.path, this.geometryToPath(p.pixelRings, layer.isClosed)); for (let subP of p.subrings) { - this._setPath(subP.path, this.geometryToPath(subP.pixelSubrings, false)); + this._setPath( + subP.path, + this.geometryToPath(subP.pixelSubrings, false) + ); } } }, @@ -167,7 +205,9 @@ export var FeatureRenderer = L.SVG.extend({ * @private */ _pointToMarker: function (p) { - return `M${p.x} ${p.y} L${p.x - 12.5} ${p.y - 30} C${p.x - 12.5} ${p.y - 50}, ${p.x + 12.5} ${p.y - 50}, ${p.x + 12.5} ${p.y - 30} L${p.x} ${p.y}z`; + return `M${p.x} ${p.y} L${p.x - 12.5} ${p.y - 30} C${p.x - 12.5} ${ + p.y - 50 + }, ${p.x + 12.5} ${p.y - 50}, ${p.x + 12.5} ${p.y - 30} L${p.x} ${p.y}z`; }, /** @@ -182,8 +222,7 @@ export var FeatureRenderer = L.SVG.extend({ this._updatePathStyle(p.path, layer, true); } for (let subP of p.subrings) { - if (subP.path) - this._updatePathStyle(subP.path, layer); + if (subP.path) this._updatePathStyle(subP.path, layer); } } }, @@ -197,9 +236,15 @@ export var FeatureRenderer = L.SVG.extend({ * @private */ _updatePathStyle: function (path, layer, isMain = false, isOutline = false) { - if (!path || !layer) { return; } - let options = layer.options, isClosed = layer.isClosed; - if ((options.stroke && (!isClosed || isOutline)) || (isMain && !layer.outlinePath)) { + if (!path || !layer) { + return; + } + let options = layer.options, + isClosed = layer.isClosed; + if ( + (options.stroke && (!isClosed || isOutline)) || + (isMain && !layer.outlinePath) + ) { path.setAttribute('stroke', options.color); path.setAttribute('stroke-opacity', options.opacity); path.setAttribute('stroke-width', options.weight); @@ -218,17 +263,20 @@ export var FeatureRenderer = L.SVG.extend({ path.removeAttribute('stroke-dashoffset'); } - if (options.link){ - path.setAttribute("stroke", options.link.visited?"#6c00a2":"#0000EE"); - path.setAttribute("stroke-opacity", "1"); - path.setAttribute("stroke-width", "1px"); - path.setAttribute("stroke-dasharray", "none"); + if (options.link) { + path.setAttribute( + 'stroke', + options.link.visited ? '#6c00a2' : '#0000EE' + ); + path.setAttribute('stroke-opacity', '1'); + path.setAttribute('stroke-width', '1px'); + path.setAttribute('stroke-dasharray', 'none'); } } else { path.setAttribute('stroke', 'none'); } - if(isClosed && !isOutline) { + if (isClosed && !isOutline) { if (!options.fill) { path.setAttribute('fill', options.fillColor || options.color); path.setAttribute('fill-opacity', options.fillOpacity); @@ -258,7 +306,13 @@ export var FeatureRenderer = L.SVG.extend({ * @returns {string} */ geometryToPath: function (rings, closed) { - let str = '', i, j, len, len2, points, p; + let str = '', + i, + j, + len, + len2, + points, + p; for (i = 0, len = rings.length; i < len; i++) { points = rings[i]; @@ -272,7 +326,7 @@ export var FeatureRenderer = L.SVG.extend({ str += closed ? 'z' : ''; } return str || 'M0 0'; - }, + } }); /** @@ -282,4 +336,4 @@ export var FeatureRenderer = L.SVG.extend({ */ export var featureRenderer = function (options) { return new FeatureRenderer(options); -}; \ No newline at end of file +}; diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js index a6d9b1232..f0beef557 100644 --- a/src/mapml/handlers/AnnounceMovement.js +++ b/src/mapml/handlers/AnnounceMovement.js @@ -1,130 +1,142 @@ export var AnnounceMovement = L.Handler.extend({ - addHooks: function () { - this._map.on({ - layeradd: this.totalBounds, - layerremove: this.totalBounds, - }); - - this._map.options.mapEl.addEventListener('moveend', this.announceBounds); - this._map.dragging._draggable.addEventListener('dragstart', this.dragged); - this._map.options.mapEl.addEventListener('mapfocused', this.focusAnnouncement); - }, - removeHooks: function () { - this._map.off({ - layeradd: this.totalBounds, - layerremove: this.totalBounds, - }); - - this._map.options.mapEl.removeEventListener('moveend', this.announceBounds); - this._map.dragging._draggable.removeEventListener('dragstart', this.dragged); - this._map.options.mapEl.removeEventListener('mapfocused', this.focusAnnouncement); - }, - - focusAnnouncement: function () { - let mapEl = this; - setTimeout(function (){ - let el = mapEl.querySelector(".mapml-web-map") ? mapEl.querySelector(".mapml-web-map").shadowRoot.querySelector(".leaflet-container") : - mapEl.shadowRoot.querySelector(".leaflet-container"); - - let mapZoom = mapEl._map.getZoom(); - let location = M._gcrsToTileMatrix(mapEl); - let standard = M.options.locale.amZoom + " " + mapZoom; - - if(mapZoom === mapEl._map._layersMaxZoom){ - standard = M.options.locale.amMaxZoom + " " + standard; - } - else if(mapZoom === mapEl._map._layersMinZoom){ - standard = M.options.locale.amMinZoom + " " + standard; - } - - el.setAttribute("aria-roledescription", "region " + standard); - setTimeout(function () { - el.removeAttribute("aria-roledescription"); - }, 2000); - }, 0); - }, - - announceBounds: function () { - if(this._traversalCall > 0){ - return; - } - let mapZoom = this._map.getZoom(); - let mapBounds = M.pixelToPCRSBounds(this._map.getPixelBounds(),mapZoom,this._map.options.projection); - - let visible = true; - if(this._map.totalLayerBounds){ - visible = mapZoom <= this._map._layersMaxZoom && mapZoom >= this._map._layersMinZoom && - this._map.totalLayerBounds.overlaps(mapBounds); - } - - let output = this.querySelector(".mapml-web-map") ? this.querySelector(".mapml-web-map").shadowRoot.querySelector(".mapml-screen-reader-output") : - this.shadowRoot.querySelector(".mapml-screen-reader-output"); - - //GCRS to TileMatrix - let location = M._gcrsToTileMatrix(this); - let standard = M.options.locale.amZoom + " " + mapZoom; - - if(!visible){ - let outOfBoundsPos = this._history[this._historyIndex]; - let inBoundsPos = this._history[this._historyIndex - 1]; - this.back(); - this._history.pop(); - - if(outOfBoundsPos.zoom !== inBoundsPos.zoom){ - output.innerText = M.options.locale.amZoomedOut; - } - else if(this._map.dragging._draggable.wasDragged){ - output.innerText = M.options.locale.amDraggedOut; - } - else if(outOfBoundsPos.x > inBoundsPos.x){ - output.innerText = M.options.locale.amEastBound; - } - else if(outOfBoundsPos.x < inBoundsPos.x){ - output.innerText = M.options.locale.amWestBound; - } - else if(outOfBoundsPos.y < inBoundsPos.y){ - output.innerText = M.options.locale.amNorthBound; - } - else if(outOfBoundsPos.y > inBoundsPos.y){ - output.innerText = M.options.locale.amSouthBound; - } - - } - else{ - let prevZoom = this._history[this._historyIndex - 1] ? this._history[this._historyIndex - 1].zoom : this._history[this._historyIndex].zoom; - if(mapZoom === this._map._layersMaxZoom && mapZoom !== prevZoom){ - output.innerText = M.options.locale.amMaxZoom + " " + standard; - } - else if(mapZoom === this._map._layersMinZoom && mapZoom !== prevZoom){ - output.innerText = M.options.locale.amMinZoom + " " + standard; - } - else { - output.innerText = standard; - } - } - this._map.dragging._draggable.wasDragged = false; - }, - - totalBounds: function () { - let layers = Object.keys(this._layers); - let bounds = L.bounds(); - - layers.forEach(i => { - if(this._layers[i].layerBounds){ - if(!bounds){ - let point = this._layers[i].layerBounds.getCenter(); - bounds = L.bounds(point, point); - } - bounds.extend(this._layers[i].layerBounds.min); - bounds.extend(this._layers[i].layerBounds.max); - } - }); - - this.totalLayerBounds = bounds; - }, - - dragged: function () { - this.wasDragged = true; + addHooks: function () { + this._map.on({ + layeradd: this.totalBounds, + layerremove: this.totalBounds + }); + + this._map.options.mapEl.addEventListener('moveend', this.announceBounds); + this._map.dragging._draggable.addEventListener('dragstart', this.dragged); + this._map.options.mapEl.addEventListener( + 'mapfocused', + this.focusAnnouncement + ); + }, + removeHooks: function () { + this._map.off({ + layeradd: this.totalBounds, + layerremove: this.totalBounds + }); + + this._map.options.mapEl.removeEventListener('moveend', this.announceBounds); + this._map.dragging._draggable.removeEventListener( + 'dragstart', + this.dragged + ); + this._map.options.mapEl.removeEventListener( + 'mapfocused', + this.focusAnnouncement + ); + }, + + focusAnnouncement: function () { + let mapEl = this; + setTimeout(function () { + let el = mapEl.querySelector('.mapml-web-map') + ? mapEl + .querySelector('.mapml-web-map') + .shadowRoot.querySelector('.leaflet-container') + : mapEl.shadowRoot.querySelector('.leaflet-container'); + + let mapZoom = mapEl._map.getZoom(); + let location = M._gcrsToTileMatrix(mapEl); + let standard = M.options.locale.amZoom + ' ' + mapZoom; + + if (mapZoom === mapEl._map._layersMaxZoom) { + standard = M.options.locale.amMaxZoom + ' ' + standard; + } else if (mapZoom === mapEl._map._layersMinZoom) { + standard = M.options.locale.amMinZoom + ' ' + standard; + } + + el.setAttribute('aria-roledescription', 'region ' + standard); + setTimeout(function () { + el.removeAttribute('aria-roledescription'); + }, 2000); + }, 0); + }, + + announceBounds: function () { + if (this._traversalCall > 0) { + return; + } + let mapZoom = this._map.getZoom(); + let mapBounds = M.pixelToPCRSBounds( + this._map.getPixelBounds(), + mapZoom, + this._map.options.projection + ); + + let visible = true; + if (this._map.totalLayerBounds) { + visible = + mapZoom <= this._map._layersMaxZoom && + mapZoom >= this._map._layersMinZoom && + this._map.totalLayerBounds.overlaps(mapBounds); } -}); \ No newline at end of file + let output = this.querySelector('.mapml-web-map') + ? this.querySelector('.mapml-web-map').shadowRoot.querySelector( + '.mapml-screen-reader-output' + ) + : this.shadowRoot.querySelector('.mapml-screen-reader-output'); + + //GCRS to TileMatrix + let location = M._gcrsToTileMatrix(this); + let standard = M.options.locale.amZoom + ' ' + mapZoom; + + if (!visible) { + let outOfBoundsPos = this._history[this._historyIndex]; + let inBoundsPos = this._history[this._historyIndex - 1]; + this.back(); + this._history.pop(); + + if (outOfBoundsPos.zoom !== inBoundsPos.zoom) { + output.innerText = M.options.locale.amZoomedOut; + } else if (this._map.dragging._draggable.wasDragged) { + output.innerText = M.options.locale.amDraggedOut; + } else if (outOfBoundsPos.x > inBoundsPos.x) { + output.innerText = M.options.locale.amEastBound; + } else if (outOfBoundsPos.x < inBoundsPos.x) { + output.innerText = M.options.locale.amWestBound; + } else if (outOfBoundsPos.y < inBoundsPos.y) { + output.innerText = M.options.locale.amNorthBound; + } else if (outOfBoundsPos.y > inBoundsPos.y) { + output.innerText = M.options.locale.amSouthBound; + } + } else { + let prevZoom = this._history[this._historyIndex - 1] + ? this._history[this._historyIndex - 1].zoom + : this._history[this._historyIndex].zoom; + if (mapZoom === this._map._layersMaxZoom && mapZoom !== prevZoom) { + output.innerText = M.options.locale.amMaxZoom + ' ' + standard; + } else if (mapZoom === this._map._layersMinZoom && mapZoom !== prevZoom) { + output.innerText = M.options.locale.amMinZoom + ' ' + standard; + } else { + output.innerText = standard; + } + } + this._map.dragging._draggable.wasDragged = false; + }, + + totalBounds: function () { + let layers = Object.keys(this._layers); + let bounds = L.bounds(); + + layers.forEach((i) => { + if (this._layers[i].layerBounds) { + if (!bounds) { + let point = this._layers[i].layerBounds.getCenter(); + bounds = L.bounds(point, point); + } + bounds.extend(this._layers[i].layerBounds.min); + bounds.extend(this._layers[i].layerBounds.max); + } + }); + + this.totalLayerBounds = bounds; + }, + + dragged: function () { + this.wasDragged = true; + } +}); diff --git a/src/mapml/handlers/ContextMenu.js b/src/mapml/handlers/ContextMenu.js index e405d2df6..57b6254c9 100644 --- a/src/mapml/handlers/ContextMenu.js +++ b/src/mapml/handlers/ContextMenu.js @@ -9,7 +9,11 @@ The above copyright notice and this permission notice shall be included in all c /* global M */ export var ContextMenu = L.Handler.extend({ - _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart', + _touchstart: L.Browser.msPointer + ? 'MSPointerDown' + : L.Browser.pointer + ? 'pointerdown' + : 'touchstart', initialize: function (map) { L.Handler.prototype.initialize.call(this, map); @@ -19,61 +23,62 @@ export var ContextMenu = L.Handler.extend({ //setting the items in the context menu and their callback functions this._items = [ { - text: M.options.locale.cmBack + " (Alt+Left Arrow)", - callback:this._goBack + text: M.options.locale.cmBack + ' (Alt+Left Arrow)', + callback: this._goBack }, { - text: M.options.locale.cmForward + " (Alt+Right Arrow)", - callback:this._goForward + text: M.options.locale.cmForward + ' (Alt+Right Arrow)', + callback: this._goForward }, { - text: M.options.locale.cmReload + " (Ctrl+R)", - callback:this._reload + text: M.options.locale.cmReload + ' (Ctrl+R)', + callback: this._reload }, { - text: M.options.locale.btnFullScreen + " (F)", - callback:this._toggleFullScreen + text: M.options.locale.btnFullScreen + ' (F)', + callback: this._toggleFullScreen }, { - spacer:"-" + spacer: '-' }, { - text: M.options.locale.cmCopyCoords + " (C)", - callback:this._copyCoords, - hideOnSelect:false, - popup:true, - submenu:[ + text: M.options.locale.cmCopyCoords + ' (C)', + callback: this._copyCoords, + hideOnSelect: false, + popup: true, + submenu: [ { text: M.options.locale.cmCopyMapML, - callback:this._copyMapML + callback: this._copyMapML }, { text: M.options.locale.cmCopyExtent, - callback:this._copyExtent + callback: this._copyExtent }, { text: M.options.locale.cmCopyLocation, - callback:this._copyLocation + callback: this._copyLocation } ] }, { - text: M.options.locale.cmPasteLayer + " (P)", - callback:this._paste + text: M.options.locale.cmPasteLayer + ' (P)', + callback: this._paste }, { - spacer:"-" + spacer: '-' }, { - text: M.options.locale.cmToggleControls + " (T)", - callback:this._toggleControls - }, { - text: M.options.locale.cmToggleDebug + " (D)", - callback:this._toggleDebug + text: M.options.locale.cmToggleControls + ' (T)', + callback: this._toggleControls }, { - text: M.options.locale.cmViewSource + " (V)", - callback:this._viewSource + text: M.options.locale.cmToggleDebug + ' (D)', + callback: this._toggleDebug + }, + { + text: M.options.locale.cmViewSource + ' (V)', + callback: this._viewSource } ]; @@ -83,48 +88,59 @@ export var ContextMenu = L.Handler.extend({ this.defLocCS = M.options.defaultLocCoor; this._layerItems = [ { - text: M.options.locale.lmZoomToLayer + " (Z)", - callback:this._zoomToLayer + text: M.options.locale.lmZoomToLayer + ' (Z)', + callback: this._zoomToLayer }, { - text: M.options.locale.lmCopyLayer + " (L)", - callback:this._copyLayer + text: M.options.locale.lmCopyLayer + ' (L)', + callback: this._copyLayer } ]; this._mapMenuVisible = false; this._keyboardEvent = false; - this._container = L.DomUtil.create("div", "mapml-contextmenu", map._container); + this._container = L.DomUtil.create( + 'div', + 'mapml-contextmenu', + map._container + ); this._container.setAttribute('hidden', ''); - + for (let i = 0; i < 6; i++) { this._items[i].el = this._createItem(this._container, this._items[i]); } - - this._coordMenu = L.DomUtil.create("div", "mapml-contextmenu mapml-submenu", this._container); - this._coordMenu.id = "mapml-copy-submenu"; + + this._coordMenu = L.DomUtil.create( + 'div', + 'mapml-contextmenu mapml-submenu', + this._container + ); + this._coordMenu.id = 'mapml-copy-submenu'; this._coordMenu.setAttribute('hidden', ''); - + this._clickEvent = null; - - for(let i =0;i + this.t.innerHTML = `

@@ -152,63 +167,99 @@ export var ContextMenu = L.Handler.extend({ addHooks: function () { var container = this._map.getContainer(); - L.DomEvent - .on(container, 'mouseleave', this._hide, this) - .on(document, 'keydown', this._onKeyDown, this); + L.DomEvent.on(container, 'mouseleave', this._hide, this).on( + document, + 'keydown', + this._onKeyDown, + this + ); if (L.Browser.touch) { L.DomEvent.on(document, this._touchstart, this._hide, this); } - this._map.on({ - contextmenu: this._show, - mousedown: this._hide, - zoomstart: this._hide - }, this); + this._map.on( + { + contextmenu: this._show, + mousedown: this._hide, + zoomstart: this._hide + }, + this + ); }, removeHooks: function () { var container = this._map.getContainer(); - L.DomEvent - .off(container, 'mouseleave', this._hide, this) - .off(document, 'keydown', this._onKeyDown, this); + L.DomEvent.off(container, 'mouseleave', this._hide, this).off( + document, + 'keydown', + this._onKeyDown, + this + ); if (L.Browser.touch) { L.DomEvent.off(document, this._touchstart, this._hide, this); } - this._map.off({ - contextmenu: this._show, - mousedown: this._hide, - zoomstart: this._hide - }, this); + this._map.off( + { + contextmenu: this._show, + mousedown: this._hide, + zoomstart: this._hide + }, + this + ); }, _updateCS: function () { - if (this.defExtCS !== M.options.defaultExtCoor || this.defLocCS !== M.options.defaultLocCoor) { + if ( + this.defExtCS !== M.options.defaultExtCoor || + this.defLocCS !== M.options.defaultLocCoor + ) { this.defExtCS = M.options.defaultExtCoor; this.defLocCS = M.options.defaultLocCoor; } }, _copyExtent: function (e) { - let context = e instanceof KeyboardEvent ? this._map.contextMenu : this.contextMenu, - coord = context.defExtCS? context.defExtCS.toLowerCase() : 'pcrs', - tL = e instanceof KeyboardEvent? this.extent.topLeft[coord] : this.options.mapEl.extent.topLeft[coord], - bR = e instanceof KeyboardEvent? this.extent.bottomRight[coord] : this.options.mapEl.extent.bottomRight[coord], - data = ""; - switch(coord) { + let context = + e instanceof KeyboardEvent ? this._map.contextMenu : this.contextMenu, + coord = context.defExtCS ? context.defExtCS.toLowerCase() : 'pcrs', + tL = + e instanceof KeyboardEvent + ? this.extent.topLeft[coord] + : this.options.mapEl.extent.topLeft[coord], + bR = + e instanceof KeyboardEvent + ? this.extent.bottomRight[coord] + : this.options.mapEl.extent.bottomRight[coord], + data = ''; + switch (coord) { case 'MapML': - default: + default: if (coord === 'pcrs') { - data = ``; + data = ``; } else if (coord === 'gcrs') { data = ``; } else if (coord === 'tcrs') { - data = ``; - } else if (coord === 'tilematrix') { - data = ``; + data = ``; + } else if (coord === 'tilematrix') { + data = ``; } else { console.log('not support'); } @@ -218,38 +269,45 @@ export var ContextMenu = L.Handler.extend({ }, _zoomToLayer: function (e) { - let context = e instanceof KeyboardEvent ? this._map.contextMenu : this.contextMenu; + let context = + e instanceof KeyboardEvent ? this._map.contextMenu : this.contextMenu; context._layerClicked.layer._layerEl.zoomTo(); }, _copyLayer: function (e) { - let context = e instanceof KeyboardEvent ? this._map.contextMenu : this.contextMenu, + let context = + e instanceof KeyboardEvent ? this._map.contextMenu : this.contextMenu, layerElem = context._layerClicked.layer._layerEl; context._copyData(layerElem.getOuterHTML()); }, - _goForward: function(e){ - let mapEl = e instanceof KeyboardEvent?this._map.options.mapEl:this.options.mapEl; + _goForward: function (e) { + let mapEl = + e instanceof KeyboardEvent ? this._map.options.mapEl : this.options.mapEl; mapEl.forward(); }, - _goBack: function(e){ - let mapEl = e instanceof KeyboardEvent?this._map.options.mapEl:this.options.mapEl; + _goBack: function (e) { + let mapEl = + e instanceof KeyboardEvent ? this._map.options.mapEl : this.options.mapEl; mapEl.back(); }, - _reload: function(e){ - let mapEl = e instanceof KeyboardEvent?this._map.options.mapEl:this.options.mapEl; + _reload: function (e) { + let mapEl = + e instanceof KeyboardEvent ? this._map.options.mapEl : this.options.mapEl; mapEl.reload(); }, - _toggleFullScreen: function(e){ - let mapEl = e instanceof KeyboardEvent?this._map.options.mapEl:this.options.mapEl; + _toggleFullScreen: function (e) { + let mapEl = + e instanceof KeyboardEvent ? this._map.options.mapEl : this.options.mapEl; mapEl._toggleFullScreen(); }, - _toggleControls: function(e){ - let mapEl = e instanceof KeyboardEvent?this._map.options.mapEl:this.options.mapEl; + _toggleControls: function (e) { + let mapEl = + e instanceof KeyboardEvent ? this._map.options.mapEl : this.options.mapEl; if (mapEl.controls) { mapEl.controls = false; } else { @@ -257,41 +315,52 @@ export var ContextMenu = L.Handler.extend({ } }, - _copyMapML: function(e){ - let context = e instanceof KeyboardEvent ? this._map.contextMenu : this.contextMenu, - mapEl = e instanceof KeyboardEvent?this._map.options.mapEl:this.options.mapEl; - context._copyData(mapEl.outerHTML.replace(/
.*?<\/div>|