diff --git a/Gruntfile.js b/Gruntfile.js index 41145842b..c7c25443c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -25,10 +25,11 @@ module.exports = function(grunt) { 'dist/mapml.js': ['<%= rollup.main.dest %>'], 'dist/web-map.js': ['src/web-map.js'], 'dist/mapml-viewer.js': ['src/mapml-viewer.js'], - 'dist/DOMTokenList.js': ['src/mapml/utils/DOMTokenList.js'], 'dist/map-caption.js': ['src/map-caption.js'], 'dist/map-feature.js': ['src/map-feature.js'], 'dist/map-extent.js': ['src/map-extent.js'], + 'dist/map-input.js': ['src/map-input.js'], + 'dist/map-link.js': ['src/map-link.js'], 'dist/map-area.js': ['src/map-area.js'], 'dist/layer.js': ['src/layer.js'], 'dist/leaflet.js': ['dist/leaflet-src.js', @@ -159,7 +160,7 @@ module.exports = function(grunt) { { expand: true, src: ['dist/*'], - dest: '../web-map-doc' + dest: '../web-map-doc/static' } ] } diff --git a/index.html b/index.html index a1919378b..7040693c5 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@ /* Responsive map. */ max-width: 100%; - /* Full viewport. */ + /* Full viewport. */ width: 100%; height: 100%; @@ -72,56 +72,64 @@ - - - - - - - - Click me! - - - - - -75.8242035 45.3526278 -75.6793213 45.4572409 -75.5680847 45.4692806 -75.6092834 45.4215881 -75.5756378 45.3810901 -75.7946777 45.3120804 - - - - - - - - - - - - Point 1 - - - -75.6978285 45.4202251 - - - - - - Point 2 - - - -75.7002854 45.4199465 - - - - - - point 3 - - - -75.6984723 45.4179207 - - - - - - + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 7b89c3847..4ae3721dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@maps4html/web-map-custom-element", - "version": "0.11.0", + "version": "0.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@maps4html/web-map-custom-element", - "version": "0.11.0", + "version": "0.12.0", "license": "W3C", "devDependencies": { - "@playwright/test": "^1.35.1", + "@playwright/test": "^1.39.0", "diff": "^5.1.0", "express": "^4.17.1", "grunt": "^1.4.0", @@ -30,7 +30,7 @@ "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", "path": "^0.12.7", - "playwright": "^1.35.1", + "playwright": "^1.39.0", "proj4": "^2.6.2", "proj4leaflet": "^1.0.2", "rollup": "^2.23.1" @@ -1513,22 +1513,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", - "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "dev": true, "dependencies": { - "@types/node": "*", - "playwright-core": "1.35.1" + "playwright": "1.39.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/@sideway/address": { @@ -9740,25 +9736,27 @@ } }, "node_modules/playwright": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", - "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", "dev": true, - "hasInstallScript": true, "dependencies": { - "playwright-core": "1.35.1" + "playwright-core": "1.39.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", - "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -13446,14 +13444,12 @@ } }, "@playwright/test": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", - "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "dev": true, "requires": { - "@types/node": "*", - "fsevents": "2.3.2", - "playwright-core": "1.35.1" + "playwright": "1.39.0" } }, "@sideway/address": { @@ -19810,18 +19806,19 @@ } }, "playwright": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", - "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", "dev": true, "requires": { - "playwright-core": "1.35.1" + "fsevents": "2.3.2", + "playwright-core": "1.39.0" } }, "playwright-core": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", - "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", "dev": true }, "posix-character-classes": { diff --git a/package.json b/package.json index 717ff2fbf..eb9e9cdf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@maps4html/web-map-custom-element", - "version": "0.11.0", + "version": "0.12.0", "description": "web-map customized built-in HTML or custom ", "keywords": [ "mapml-viewer", @@ -32,10 +32,11 @@ } ], "scripts": { - "test": "npx playwright test --retries=3", + "test": "npx playwright test --retries=3 --workers=1", "jest": "jest --verbose --noStackTrace" }, "devDependencies": { + "@playwright/test": "^1.39.0", "diff": "^5.1.0", "express": "^4.17.1", "grunt": "^1.4.0", @@ -56,8 +57,7 @@ "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", "path": "^0.12.7", - "@playwright/test": "^1.35.1", - "playwright": "^1.35.1", + "playwright": "^1.39.0", "proj4": "^2.6.2", "proj4leaflet": "^1.0.2", "rollup": "^2.23.1" diff --git a/src/layer.js b/src/layer.js index e792f090d..59c492d0a 100644 --- a/src/layer.js +++ b/src/layer.js @@ -49,7 +49,7 @@ export class MapLayer extends HTMLElement { get opacity() { // use ?? since 0 is falsy, || would return rhs in that case - return this._opacity ?? this.getAttribute('opacity'); + return +(this._opacity ?? this.getAttribute('opacity')); } set opacity(val) { @@ -57,18 +57,28 @@ export class MapLayer extends HTMLElement { this.setAttribute('opacity', val); } + get extent() { + // calculate the bounds of all content, return it. + if (!this._layer.bounds) { + this._layer._calculateBounds(); + } + return Object.assign( + M._convertAndFormatPCRS( + this._layer.bounds, + this._layer._properties.crs, + this._layer._properties.projection + ), + { zoom: this._layer.zoomBounds } + ); + } + constructor() { // Always call super first in constructor 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 - - // 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; this._onRemove(); } @@ -86,6 +96,7 @@ export class MapLayer extends HTMLElement { this._layerControl.removeLayer(this._layer); } delete this._layer; + delete this._fetchError; if (this.shadowRoot) { this.shadowRoot.innerHTML = ''; @@ -94,6 +105,10 @@ export class MapLayer extends HTMLElement { connectedCallback() { if (this.hasAttribute('data-moving')) return; + this._createLayerControlHTML = M._createLayerControlHTML.bind(this); + // this._opacity is used to record the current opacity value (with or without updates), + // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 + this._opacity = +(this.getAttribute('opacity') || 1.0); const doConnected = this._onAdd.bind(this); this.parentElement .whenReady() @@ -158,11 +173,13 @@ export class MapLayer extends HTMLElement { opacity: this.opacity } ); + this._createLayerControlHTML(); this._attachedToMap(); this._validateDisabled(); resolve(); }) .catch((error) => { + this._fetchError = true; console.log('Error fetching layer content' + error); }); } else { @@ -173,6 +190,7 @@ export class MapLayer extends HTMLElement { mapprojection: this.parentElement.projection, opacity: this.opacity }); + this._createLayerControlHTML(); this._attachedToMap(); this._validateDisabled(); resolve(); @@ -211,17 +229,11 @@ export class MapLayer extends HTMLElement { }); // 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) - this._layer.fire('attached'); 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', this._validateDisabled, this); // toggle the this.disabled attribute depending on whether the layer // is: same prj as map, within view/zoom of map @@ -260,19 +272,28 @@ export class MapLayer extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'label': - this.whenReady().then(() => { - this._layer.setName(newValue); - }); + this.whenReady() + .then(() => { + this._layer.setName(newValue); + }) + .catch((e) => { + console.log(e); + }); break; case 'checked': - if (this._layer) { - 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.whenReady() + .then(() => { + if (typeof newValue === 'string') { + this.parentElement._map.addLayer(this._layer); + } else { + this.parentElement._map.removeLayer(this._layer); + } + this._layerControlCheckbox.checked = this.checked; + this.dispatchEvent(new CustomEvent('map-change')); + }) + .catch((e) => { + console.log(e); + }); break; case 'hidden': var map = this.parentElement && this.parentElement._map; @@ -306,12 +327,18 @@ export class MapLayer extends HTMLElement { } } _validateDisabled() { + // setTimeout is necessary to make the validateDisabled happen later than the moveend operations etc., + // to ensure that the validated result is correct setTimeout(() => { let layer = this._layer, map = layer?._map; if (map) { - let count = 0, - total = 0, + // prerequisite: no inline and remote mapml elements exists at the same time + const mapExtents = this.shadowRoot + ? this.shadowRoot.querySelectorAll('map-extent') + : this.querySelectorAll('map-extent'); + let disabledExtentCount = 0, + totalExtentCount = 0, layerTypes = [ '_staticTileLayer', '_imageLayer', @@ -321,62 +348,71 @@ export class MapLayer extends HTMLElement { if (layer.validProjection) { for (let j = 0; j < layerTypes.length; j++) { let type = layerTypes[j]; - if (this.checked && layer[type]) { - if (type === '_templatedLayer') { - for (let i = 0; i < layer._properties._mapExtents.length; i++) { - for ( - let j = 0; - j < - layer._properties._mapExtents[i].templatedLayer._templates - .length; - j++ - ) { - if ( - layer._properties._mapExtents[i].templatedLayer - ._templates[j].rel === 'query' - ) - continue; - total++; - layer._properties._mapExtents[i].removeAttribute( - 'disabled' - ); - layer._properties._mapExtents[i].disabled = false; - if ( - !layer._properties._mapExtents[i].templatedLayer - ._templates[j].layer.isVisible - ) { - count++; - layer._properties._mapExtents[i].setAttribute( - 'disabled', - '' - ); - layer._properties._mapExtents[i].disabled = true; - } - } + if (this.checked) { + if (type === '_templatedLayer' && mapExtents.length > 0) { + for (let i = 0; i < mapExtents.length; i++) { + totalExtentCount++; + if (mapExtents[i]._validateDisabled()) disabledExtentCount++; } - } else { - total++; - if (!layer[type].isVisible) count++; + } else if (layer[type]) { + // not a templated layer + totalExtentCount++; + if (!layer[type].isVisible) disabledExtentCount++; } } } } else { - count = 1; - total = 1; + disabledExtentCount = 1; + totalExtentCount = 1; } - - if (count === total && count !== 0) { - this.setAttribute('disabled', ''); //set a disabled attribute on the layer element + // if all extents are not visible / disabled, set layer to disabled + if ( + disabledExtentCount === totalExtentCount && + disabledExtentCount !== 0 + ) { + this.setAttribute('disabled', ''); this.disabled = true; } else { - //might be better not to disable the layer controls, might want to deselect layer even when its out of bounds this.removeAttribute('disabled'); this.disabled = false; } - map.fire('validate'); + this.toggleLayerControlDisabled(); } }, 0); } + + // disable/italicize layer control elements based on the layer-.disabled property + toggleLayerControlDisabled() { + let input = this._layerControlCheckbox, + label = this._layerControlLabel, + opacityControl = this._opacityControl, + opacitySlider = this._opacitySlider, + styleControl = this._styles; + if (this.disabled) { + input.disabled = true; + opacitySlider.disabled = true; + label.style.fontStyle = 'italic'; + opacityControl.style.fontStyle = 'italic'; + if (styleControl) { + styleControl.style.fontStyle = 'italic'; + styleControl.querySelectorAll('input').forEach((i) => { + i.disabled = true; + }); + } + } else { + input.disabled = false; + opacitySlider.disabled = false; + label.style.fontStyle = 'normal'; + opacityControl.style.fontStyle = 'normal'; + if (styleControl) { + styleControl.style.fontStyle = 'normal'; + styleControl.querySelectorAll('input').forEach((i) => { + i.disabled = false; + }); + } + } + } + getOuterHTML() { let tempElement = this.cloneNode(true); @@ -419,29 +455,23 @@ export class MapLayer extends HTMLElement { return outerLayer; } - _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); - } - } zoomTo() { - if (!this.extent) return; - let map = this.parentElement._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)); + this.whenElemsReady().then(() => { + let map = this.parentElement._map, + extent = this.extent, + tL = extent.topLeft.pcrs, + bR = 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 = extent.zoom.maxZoom, + minZoom = extent.zoom.minZoom; + map.setView(center, M.getMaxZoom(layerBounds, map, minZoom, maxZoom), { + animate: false + }); }); } mapml2geojson(options = {}) { @@ -467,7 +497,7 @@ export class MapLayer extends HTMLElement { whenReady() { return new Promise((resolve, reject) => { let interval, failureTimer; - if (this._layer) { + if (this._layer && (!this.src || this.shadowRoot?.childNodes.length)) { resolve(); } else { let layerElement = this; @@ -475,10 +505,17 @@ export class MapLayer extends HTMLElement { failureTimer = setTimeout(layerNotDefined, 5000); } function testForLayer(layerElement) { - if (layerElement._layer) { + if ( + layerElement._layer && + (!layerElement.src || layerElement.shadowRoot?.childNodes.length) + ) { clearInterval(interval); clearTimeout(failureTimer); resolve(); + } else if (layerElement._fetchError) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Error fetching layer content'); } } function layerNotDefined() { @@ -488,4 +525,16 @@ export class MapLayer extends HTMLElement { } }); } + // check if all child elements are ready + whenElemsReady() { + let elemsReady = []; + let target = this.shadowRoot || this; + for (let elem of [ + ...target.querySelectorAll('map-extent'), + ...target.querySelectorAll('map-feature') + ]) { + elemsReady.push(elem.whenReady()); + } + return Promise.allSettled(elemsReady); + } } diff --git a/src/map-extent.js b/src/map-extent.js index 5808586a5..2fbacd51a 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -1,6 +1,7 @@ +/* global M */ export class MapExtent extends HTMLElement { static get observedAttributes() { - return ['units', 'checked', 'label', 'opacity']; + return ['units', 'checked', 'label', 'opacity', 'hidden']; } get units() { return this.getAttribute('units'); @@ -24,7 +25,9 @@ export class MapExtent extends HTMLElement { } } get label() { - return this.hasAttribute('label') ? this.getAttribute('label') : ''; + return this.hasAttribute('label') + ? this.getAttribute('label') + : M.options.locale.dfExtent; } set label(val) { if (val) { @@ -32,66 +35,554 @@ export class MapExtent extends HTMLElement { } } get opacity() { - return this._opacity; + // use ?? since 0 is falsy, || would return rhs in that case + return +(this._opacity ?? this.getAttribute('opacity')); } set opacity(val) { if (+val > 1 || +val < 0) return; this.setAttribute('opacity', val); } + get hidden() { + return this.hasAttribute('hidden'); + } + + set hidden(val) { + if (val) { + this.setAttribute('hidden', ''); + } else { + this.removeAttribute('hidden'); + } + } attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'units': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'label': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'checked': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'opacity': - if (oldValue !== newValue) { - // handle side effects + this.whenReady() + .then(() => { + switch (name) { + case 'units': + if (oldValue !== newValue) { + // handle side effects + } + break; + case 'label': + if (oldValue !== newValue) { + this._layerControlHTML.querySelector( + '.mapml-layer-item-name' + ).innerHTML = newValue || M.options.locale.dfExtent; + } + break; + case 'checked': + this._handleChange(); + this._calculateBounds(); + this._layerControlCheckbox.checked = newValue !== null; + break; + case 'opacity': + if (oldValue !== newValue) { + this._opacity = newValue; + if (this._templatedLayer) + this._templatedLayer.changeOpacity(newValue); + } + break; + case 'hidden': + if (oldValue !== newValue) { + let extentsRootFieldset = + this.parentLayer._propertiesGroupAnatomy; + let position = Array.from( + this.parentNode.querySelectorAll('map-extent:not([hidden])') + ).indexOf(this); + if (newValue !== null) { + // remove from layer control (hide from user) + this._layerControlHTML.remove(); + } else { + // insert the extent fieldset into the layer control container in + // the calculated position + if (position === 0) { + extentsRootFieldset.insertAdjacentElement( + 'afterbegin', + this._layerControlHTML + ); + } else if (position > 0) { + this.parentNode + .querySelectorAll('map-extent:not([hidden])') + [position - 1]._layerControlHTML.insertAdjacentElement( + 'afterend', + this._layerControlHTML + ); + } + } + this._validateLayerControlContainerHidden(); + } + break; } - break; - } + }) + .catch((reason) => { + console.log( + reason, + `\nin mapExtent.attributeChangeCallback when changing attribute ${name}` + ); + }); } constructor() { // Always call super first in constructor super(); } - connectedCallback() { + async connectedCallback() { + // this.parentNode.host returns the layer- element when parentNode is + // the shadow root + this._createLayerControlExtentHTML = + M._createLayerControlExtentHTML.bind(this); + this.parentLayer = + this.parentNode.nodeName.toUpperCase() === 'LAYER-' + ? this.parentNode + : this.parentNode.host; + if ( + this.hasAttribute('data-moving') || + this.parentLayer.hasAttribute('data-moving') + ) + return; 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; - parentLayer - .whenReady() - .then(() => { - this._layer = parentLayer._layer; - }) - .catch(() => { - throw new Error('Layer never became ready'); - }); + await this.parentLayer.whenReady(); + // when projection is changed, the parent layer-._layer is created (so whenReady is fulfilled) but then removed, + // then the map-extent disconnectedCallback will be triggered by layer-._onRemove() (clear the shadowRoot) + // even before connectedCallback is finished + // in this case, the microtasks triggered by the fulfillment of the removed MapMLLayer should be stopped as well + // !this.isConnected <=> the disconnectedCallback has run before + if (!this.isConnected) return; + this._layer = this.parentLayer._layer; + this._map = this._layer._map; + // reset the layer extent + delete this.parentLayer.bounds; + this._templateVars = this._initTemplateVars( + // read map-meta[name=extent] from shadowroot or layer- + // querySelector / querySelectorAll on layer- cannot get elements inside its shadowroot + this.parentLayer.shadowRoot + ? this.parentLayer.shadowRoot.querySelector( + 'map-extent > map-meta[name=extent]' + ) || + this.parentLayer.shadowRoot.querySelector('map-meta[name=extent]') + : this.parentLayer.querySelector( + 'map-extent > map-meta[name=extent]' + ) || this.parentLayer.querySelector('map-meta[name=extent]'), + this.units, + this._layer._content, + this._layer.getBase(), + this.units === this._layer.options.mapprojection + ); + this._changeHandler = this._handleChange.bind(this); + this.parentLayer.addEventListener('map-change', this._changeHandler); + // this._opacity is used to record the current opacity value (with or without updates), + // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 + this._opacity = +(this.getAttribute('opacity') || 1.0); + this._templatedLayer = M.templatedLayer(this._templateVars, { + pane: this._layer._container, + opacity: this.opacity, + _leafletLayer: this._layer, + crs: this._layer._properties.crs, + extentZIndex: Array.from( + this.parentLayer.querySelectorAll('map-extent') + ).indexOf(this), + // when a migrates from a remote mapml file and attaches to the shadow of + // this._properties._mapExtents[i] refers to the in remote mapml + extentEl: this._DOMnode || this + }); + // this._layerControlHTML is the fieldset for the extent in the LayerControl + this._layerControlHTML = this._createLayerControlExtentHTML(); + if (!this.hidden) + this._layer.addExtentToLayerControl(this._layerControlHTML); + this._validateLayerControlContainerHidden(); + if (this._templatedLayer._queries) { + if (!this._layer._properties._queries) + this._layer._properties._queries = []; + this._layer._properties._queries = + this._layer._properties._queries.concat(this._templatedLayer._queries); + } + this._calculateBounds(); + } + getLayerControlHTML() { + return this._layerControlHTML; + } + _projectionMatch() { + return ( + this.units.toUpperCase() === + this._layer.options.mapprojection.toUpperCase() + ); + } + _validateDisabled() { + if (!this._templatedLayer) return; + const noTemplateVisible = () => { + let totalTemplateCount = this._templatedLayer._templates.length, + disabledTemplateCount = 0; + for (let j = 0; j < this._templatedLayer._templates.length; j++) { + if (this._templatedLayer._templates[j].rel === 'query') { + continue; + } + if (!this._templatedLayer._templates[j].layer.isVisible) { + disabledTemplateCount++; + } + } + return disabledTemplateCount === totalTemplateCount; + }; + if (!this._projectionMatch() || noTemplateVisible()) { + this.setAttribute('disabled', ''); + this.disabled = true; + } else { + this.removeAttribute('disabled'); + this.disabled = false; + } + this.toggleLayerControlDisabled(); + return this.disabled; + } + + // disable/italicize layer control elements based on the map-extent.disabled property + toggleLayerControlDisabled() { + let input = this._layerControlCheckbox, + label = this._layerControlLabel, // access to the label for the specific map-extent + opacityControl = this._opacityControl, + opacitySlider = this._opacitySlider, + selectDetails = this._selectdetails; + if (this.disabled) { + // update the status of layerControl + input.disabled = true; + opacitySlider.disabled = true; + label.style.fontStyle = 'italic'; + opacityControl.style.fontStyle = 'italic'; + if (selectDetails) { + selectDetails.forEach((i) => { + i.querySelectorAll('select').forEach((j) => { + j.disabled = true; + j.style.fontStyle = 'italic'; + }); + i.style.fontStyle = 'italic'; + }); + } + } else { + input.disabled = false; + opacitySlider.disabled = false; + label.style.fontStyle = 'normal'; + opacityControl.style.fontStyle = 'normal'; + if (selectDetails) { + selectDetails.forEach((i) => { + i.querySelectorAll('select').forEach((j) => { + j.disabled = false; + j.style.fontStyle = 'normal'; + }); + i.style.fontStyle = 'normal'; + }); + } + } } - disconnectedCallback() {} + + _initTemplateVars(metaExtent, projection, mapml, base, projectionMatch) { + function transcribe(element) { + var select = document.createElement('select'); + var elementAttrNames = element.getAttributeNames(); + + for (let i = 0; i < elementAttrNames.length; i++) { + select.setAttribute( + elementAttrNames[i], + element.getAttribute(elementAttrNames[i]) + ); + } + + var options = element.children; + + for (let i = 0; i < options.length; i++) { + var option = document.createElement('option'); + var optionAttrNames = options[i].getAttributeNames(); + + for (let j = 0; j < optionAttrNames.length; j++) { + option.setAttribute( + optionAttrNames[j], + options[i].getAttribute(optionAttrNames[j]) + ); + } + + option.innerHTML = options[i].innerHTML; + select.appendChild(option); + } + return select; + } + var templateVars = []; + // set up the URL template and associated inputs (which yield variable values when processed) + var tlist = this.querySelectorAll( + 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' + ), + varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), + zoomInput = this.querySelector('map-input[type="zoom" i]'), + includesZoom = false, + boundsFallback = {}; + + boundsFallback.zoom = 0; + if (metaExtent) { + let content = M._metaContentToObject(metaExtent.getAttribute('content')), + cs; + + boundsFallback.zoom = content.zoom || boundsFallback.zoom; + + let metaKeys = Object.keys(content); + for (let i = 0; i < metaKeys.length; i++) { + if (!metaKeys[i].includes('zoom')) { + cs = M.axisToCS(metaKeys[i].split('-')[2]); + break; + } + } + let axes = M.csToAxes(cs); + boundsFallback.bounds = M.boundsToPCRSBounds( + L.bounds( + L.point( + +content[`top-left-${axes[0]}`], + +content[`top-left-${axes[1]}`] + ), + L.point( + +content[`bottom-right-${axes[0]}`], + +content[`bottom-right-${axes[1]}`] + ) + ), + boundsFallback.zoom, + projection, + cs + ); + } else { + // for custom projections, M[projection] may not be loaded, so uses M['OSMTILE'] as backup, this code will need to get rerun once projection is changed and M[projection] is available + // TODO: This is a temporary fix, _initTemplateVars (or processinitialextent) should not be called when projection of the layer and map do not match, this should be called/reinitialized once the layer projection matches with the map projection + let fallbackProjection = M[projection] || M.OSMTILE; + boundsFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; + } + + for (var i = 0; i < tlist.length; i++) { + var t = tlist[i], + template = t.getAttribute('tref'); + t.zoomInput = zoomInput; + if (!template) { + template = M.BLANK_TT_TREF; + let blankInputs = mapml.querySelectorAll('map-input'); + for (let i of blankInputs) { + template += `{${i.getAttribute('name')}}`; + } + } + + var v, + title = t.hasAttribute('title') + ? t.getAttribute('title') + : 'Query this layer', + vcount = template.match(varNamesRe), + trel = + !t.hasAttribute('rel') || + t.getAttribute('rel').toLowerCase() === 'tile' + ? 'tile' + : t.getAttribute('rel').toLowerCase(), + ttype = !t.hasAttribute('type') + ? 'image/*' + : t.getAttribute('type').toLowerCase(), + inputs = [], + tms = t && t.hasAttribute('tms'); + var zoomBounds = mapml.querySelector('map-meta[name=zoom]') + ? M._metaContentToObject( + mapml.querySelector('map-meta[name=zoom]').getAttribute('content') + ) + : undefined; + while ((v = varNamesRe.exec(template)) !== null) { + var varName = v[1], + inp = this.querySelector( + 'map-input[name=' + varName + '],map-select[name=' + varName + ']' + ); + if (inp) { + if ( + inp.hasAttribute('type') && + inp.getAttribute('type') === 'location' && + (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && + inp.hasAttribute('axis') && + !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) + ) { + if ( + zoomInput && + template.includes(`{${zoomInput.getAttribute('name')}}`) + ) { + zoomInput.setAttribute('value', boundsFallback.zoom); + } + let axis = inp.getAttribute('axis'), + axisBounds = M.convertPCRSBounds( + boundsFallback.bounds, + boundsFallback.zoom, + projection, + M.axisToCS(axis) + ); + inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); + inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); + } + + inputs.push(inp); + includesZoom = + includesZoom || + (inp.hasAttribute('type') && + inp.getAttribute('type').toLowerCase() === 'zoom'); + if (inp.tagName.toLowerCase() === 'map-select') { + // use a throwaway div to parse the input from MapML into HTML + var div = document.createElement('div'); + div.insertAdjacentHTML('afterbegin', inp.outerHTML); + // parse + inp.htmlselect = div.querySelector('map-select'); + inp.htmlselect = transcribe(inp.htmlselect); + + // this goes into the layer control, so add a listener + L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); + + if (!this._userInputs) { + this._userInputs = []; + } + this._userInputs.push(inp.htmlselect); + } + // TODO: if this is an input@type=location + // get the TCRS min,max attribute values at the identified zoom level + // save this information as properties of the mapExtent, + // perhaps as a bounds object so that it can be easily used + // later by the layer control to determine when to enable + // disable the layer for drawing. + } else { + console.log( + 'input with name=' + + varName + + ' not found for template variable of same name' + ); + // no match found, template won't be used + break; + } + } + if ( + (template && vcount.length === inputs.length) || + template === M.BLANK_TT_TREF + ) { + if (trel === 'query') { + this._layer.queryable = true; + } + if (!includesZoom && zoomInput) { + inputs.push(zoomInput); + } + let step = zoomInput ? zoomInput.getAttribute('step') : 1; + if (!step || step === '0' || isNaN(step)) step = 1; + // template has a matching input for every variable reference {varref} + templateVars.push({ + template: decodeURI(new URL(template, base)), + linkEl: t, + title: title, + rel: trel, + type: ttype, + values: inputs, + zoomBounds: zoomBounds, + boundsFallbackPCRS: { bounds: boundsFallback.bounds }, + projectionMatch: projectionMatch, + projection: this.units || M.FALLBACK_PROJECTION, + tms: tms, + step: step + }); + } + } + return templateVars; + } + redraw() { + this._templatedLayer.redraw(); + } + + _handleChange() { + // if the parent layer- is checked, add _templatedLayer to map if map-extent is checked, otherwise remove it + if (this.checked && this.parentLayer.checked && !this.disabled) { + this._templatedLayer.addTo(this._layer._map); + this._templatedLayer.setZIndex( + Array.from(this.parentLayer.querySelectorAll('map-extent')).indexOf( + this + ) + ); + } else { + this._map.removeLayer(this._templatedLayer); + } + // change the checkbox in the layer control to match map-extent.checked + // doesn't trigger the event handler because it's not user-caused AFAICT + } + _validateLayerControlContainerHidden() { + let extentsFieldset = this.parentLayer._propertiesGroupAnatomy; + let nodeToSearch = this.parentLayer.shadowRoot || this.parentLayer; + if ( + nodeToSearch.querySelectorAll('map-extent:not([hidden])').length === 0 + ) { + extentsFieldset.setAttribute('hidden', ''); + } else { + extentsFieldset.removeAttribute('hidden'); + } + } + disconnectedCallback() { + // in case of projection change, the disconnectedcallback will be triggered by removing layer-._layer even before + // map-extent.connectedcallback is finished (because it will wait for the layer- to be ready) + // !this._templatedLayer <=> this.connectedCallback has not yet been finished before disconnectedCallback is triggered + if ( + this.hasAttribute('data-moving') || + this.parentLayer.hasAttribute('data-moving') || + !this._templatedLayer + ) + return; + this._validateLayerControlContainerHidden(); + // remove layer control for map-extent from layer control DOM + this._layerControlHTML.remove(); + this._map.removeLayer(this._templatedLayer); + this.parentLayer.removeEventListener('map-change', this._changeHandler); + delete this._templatedLayer; + delete this.parentLayer.bounds; + } + _calculateBounds() { + let bounds = null, + zoomMax = 0, + zoomMin = 0, + maxNativeZoom = 0, + minNativeZoom = 0; + // bounds should be able to be calculated unconditionally, not depend on map-extent.checked + for (let j = 0; j < this._templateVars.length; j++) { + let inputData = M._extractInputBounds(this._templateVars[j]); + this._templateVars[j].tempExtentBounds = inputData.bounds; + this._templateVars[j].extentZoomBounds = inputData.zoomBounds; + if (!bounds) { + bounds = this._templateVars[j].tempExtentBounds; + zoomMax = this._templateVars[j].extentZoomBounds.maxZoom; + zoomMin = this._templateVars[j].extentZoomBounds.minZoom; + maxNativeZoom = this._templateVars[j].extentZoomBounds.maxNativeZoom; + minNativeZoom = this._templateVars[j].extentZoomBounds.minNativeZoom; + } else { + bounds.extend(this._templateVars[j].tempExtentBounds.min); + bounds.extend(this._templateVars[j].tempExtentBounds.max); + zoomMax = Math.max( + zoomMax, + this._templateVars[j].extentZoomBounds.maxZoom + ); + zoomMin = Math.min( + zoomMin, + this._templateVars[j].extentZoomBounds.minZoom + ); + maxNativeZoom = Math.max( + maxNativeZoom, + this._templateVars[j].extentZoomBounds.maxNativeZoom + ); + minNativeZoom = Math.min( + minNativeZoom, + this._templateVars[j].extentZoomBounds.minNativeZoom + ); + } + } + // cannot be named as layerBounds if we decide to keep the debugoverlay logic + this._templatedLayer.bounds = bounds; + this._templatedLayer.zoomBounds = { + minZoom: zoomMin, + maxZoom: zoomMax, + maxNativeZoom, + minNativeZoom + }; + } + whenReady() { return new Promise((resolve, reject) => { let interval, failureTimer; - if (this._layer) { + if (this._templatedLayer) { resolve(); } else { let extentElement = this; @@ -99,10 +590,14 @@ export class MapExtent extends HTMLElement { failureTimer = setTimeout(extentNotDefined, 10000); } function testForExtent(extentElement) { - if (extentElement._layer) { + if (extentElement._templatedLayer) { clearInterval(interval); clearTimeout(failureTimer); resolve(); + } else if (!extentElement.isConnected) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('map-extent was disconnected while waiting to be ready'); } } function extentNotDefined() { diff --git a/src/map-feature.js b/src/map-feature.js index 9f00a74f7..097040132 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -100,38 +100,55 @@ export class MapFeature extends HTMLElement { } 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; - } - // re-render feature if there is any observed change - this._reRender(); + // if the features are connected to the remote mapml + if (this.closest('mapml-')) return; + this._parentEl = + this.parentNode.nodeName.toUpperCase() === 'LAYER-' || + this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT' + ? this.parentNode + : this.parentNode.host; + this._parentEl.whenReady().then(() => { + this._layer = this._parentEl._layer; + delete this._parentEl.bounds; + if ( + this._layer._layerEl.hasAttribute('data-moving') || + this._parentEl.hasAttribute('data-moving') + ) + return; + // 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; } - }); - this._observer.observe(this, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - characterData: true + // 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._reRender(); + delete this._parentEl.bounds; + } + }); + this._observer.observe(this, { + childList: true, + subtree: true, + attributes: true, + attributeOldValue: true, + characterData: true + }); }); } @@ -140,6 +157,7 @@ export class MapFeature extends HTMLElement { if (this._layer._layerEl.hasAttribute('data-moving')) return; this._removeFeature(); this._observer.disconnect(); + delete this._parentEl.bounds; } _reRender() { @@ -155,7 +173,6 @@ export class MapFeature extends HTMLElement { .addTo(this._map); placeholder.replaceWith(this._featureGroup.options.group); // TODO: getBounds() should dynamically update the layerBounds and zoomBounds - this._layer._setLayerElExtent(); delete this._getFeatureExtent; this._setUpEvents(); } @@ -178,7 +195,6 @@ export class MapFeature extends HTMLElement { 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 = M.getZoomBounds( this._layer._content, @@ -196,18 +212,11 @@ export class MapFeature extends HTMLElement { } _addFeature() { - this._parentEl = - this.parentNode.nodeName.toUpperCase() === 'LAYER-' || - this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT' - ? this.parentNode - : this.parentNode.host; - this._parentEl.whenReady().then(() => { let parentLayer = this._parentEl.nodeName.toUpperCase() === 'LAYER-' ? this._parentEl : this._parentEl.parentElement || this._parentEl.parentNode.host; - this._layer = parentLayer._layer; this._map = this._layer._map; let mapmlvectors = this._layer._mapmlvectors; // "synchronize" the event handlers between map-feature and @@ -239,12 +248,6 @@ export class MapFeature extends HTMLElement { } } - // Number of features that are being displayed on the map - let renderedFeatureCount = Object.keys(mapmlvectors._layers).length; - // 0 because feature could be hidden by the min/max attr., 1 so as other features are added, _setLayerElExtent() is not run multiple times - if (renderedFeatureCount === 1 || renderedFeatureCount === 0) { - this._layer._setLayerElExtent(); - } this._setUpEvents(); }); } diff --git a/src/map-input.js b/src/map-input.js index bd92e01b1..bb3472829 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -1,5 +1,4 @@ -import { MapLink } from './map-link.js'; - +/* global M */ export class MapInput extends HTMLElement { static get observedAttributes() { return [ @@ -33,7 +32,7 @@ export class MapInput extends HTMLElement { } } get value() { - return this.getAttribute('value'); + return this.input.getValue(); } set value(val) { if (val) { @@ -73,7 +72,26 @@ export class MapInput extends HTMLElement { } } get min() { - return this.getAttribute('min'); + if ( + this.type === 'height' || + this.type === 'width' || + this.type === 'hidden' + ) { + return null; + } + if (this.getAttribute('min')) { + return this.getAttribute('min'); + } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this._layer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).min; + } else { + // fallback map min + return this._layer._layerEl.extent.zoom.minZoom.toString(); + } } set min(val) { if (val) { @@ -81,7 +99,26 @@ export class MapInput extends HTMLElement { } } get max() { - return this.getAttribute('max'); + if ( + this.type === 'height' || + this.type === 'width' || + this.type === 'hidden' + ) { + return null; + } + if (this.getAttribute('max')) { + return this.getAttribute('max'); + } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this._layer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).max; + } else { + // fallback map max + return this._layer._layerEl.extent.zoom.maxZoom.toString(); + } } set max(val) { if (val) { @@ -89,7 +126,11 @@ export class MapInput extends HTMLElement { } } get step() { - return this.getAttribute('step'); + if (this.type !== 'zoom') { + return null; + } else { + return this.getAttribute('step') || '1'; + } } set step(val) { if (val) { @@ -97,64 +138,197 @@ export class MapInput extends HTMLElement { } } attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'name': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'type': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'value': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'axis': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'units': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'position': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'rel': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'min': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'max': - if (oldValue !== newValue) { - // handle side effects + this.whenReady() + .then(() => { + switch (name) { + case 'name': + if (oldValue !== newValue) { + // update associated class value on attribute change + if (oldValue !== null) { + this.input.name = newValue; + } + } + break; + case 'type': + if (oldValue !== newValue) { + // handle side effects + // not allowed to change 'type' + } + break; + case 'value': + if (oldValue !== newValue) { + if (oldValue !== null) { + this.input.value = newValue; + } else { + this.initialValue = newValue; + } + } + break; + case 'axis': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.axis = newValue; + } + break; + case 'units': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.units = newValue; + } + break; + case 'position': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.position = newValue; + } + break; + case 'rel': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.rel = newValue; + } + break; + case 'min': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.min = newValue; + } + break; + case 'max': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.max = newValue; + } + break; + case 'step': + if (oldValue !== newValue && this.input) { + // handle side effects + this.input.step = newValue; + } + break; } - break; - case 'step': - if (oldValue !== newValue) { - // handle side effects - } - break; - } + }) + .catch((reason) => { + console.log( + reason, + `\nin mapInput.attributeChangeCallback when changing attribute ${name}` + ); + }); } constructor() { // Always call super first in constructor super(); } - connectedCallback() {} + connectedCallback() { + this.parentElement + .whenReady() + .then(() => { + if (this.parentElement.nodeName === 'MAP-EXTENT') { + this._layer = this.parentElement._layer; + } + switch (this.type) { + case 'zoom': + // input will store the input Class specific to the input type + this.input = new M.ZoomInput( + this.name, + this.min, + this.max, + this.initialValue, + this.step, + this._layer + ); + break; + case 'location': + // input will store the input Class specific to the input type + this.input = new M.LocationInput( + this.name, + this.position, + this.axis, + this.units, + this.min, + this.max, + this.rel, + this._layer + ); + break; + case 'width': + // input will store the input Class specific to the input type + this.input = new M.WidthInput(this.name, this._layer); + break; + case 'height': + // input will store the input Class specific to the input type + this.input = new M.HeightInput(this.name, this._layer); + break; + case 'hidden': + // input will store the input Class specific to the input type + this.input = new M.HiddenInput(this.name, this.initialValue); + break; + } + }) + .catch((reason) => { + console.log(reason, '\nin mapInput.connectedCallback'); + }); + } disconnectedCallback() {} + + //https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity + checkValidity() { + if (this.input.validateInput()) { + return true; + } else { + const evt = new Event('invalid', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(evt); + return false; + } + } + + //https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity + reportValidity() { + if (this.input.validateInput()) { + return true; + } else { + const evt = new Event('invalid', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(evt); + //if the event isn't canceled reports the problem to the user. + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-reportvalidity-dev + console.log("Input type='" + this.type + "' is not valid!"); + return false; + } + } + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this.input) { + resolve(); + } else { + let inputElement = this; + interval = setInterval(testForInput, 300, inputElement); + failureTimer = setTimeout(inputNotDefined, 10000); + } + function testForInput(inputElement) { + if (inputElement.input) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } else if (!inputElement.isConnected) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('map-input was disconnected while waiting to be ready'); + } + } + function inputNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for input to be ready'); + } + }); + } } -window.customElements.define('map-input', MapInput); diff --git a/src/map-link.js b/src/map-link.js index 969bf7a41..2ebf0ed70 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -149,5 +149,30 @@ export class MapLink extends HTMLElement { } connectedCallback() {} disconnectedCallback() {} + + // Resolve the templated URL with info from the sibling map-input's + resolve() { + if (this.tref) { + let obj = {}; + const inputs = this.parentElement.querySelectorAll('map-input'); + if (this.rel === 'image') { + // image/map + for (let i = 0; i < inputs.length; i++) { + const inp = inputs[i]; + obj[inp.name] = inp.value; + } + console.log(obj); // DEBUGGING + return L.Util.template(this.tref, obj); + } else if (this.rel === 'tile') { + // TODO. Need to get tile coords from moveend + // should be done/called from the TemplatedTilelayer.js file + return obj; + } else if (this.rel === 'query') { + // TODO. Need to get the click coords from click event + // should be done/called from the templatedlayer.js file + } else if (this.rel === 'features') { + // TODO. + } + } + } } -window.customElements.define('map-link', MapLink); diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 4fe1e7ebe..cdb0e0100 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -1,10 +1,11 @@ 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'; import { MapFeature } from './map-feature.js'; import { MapExtent } from './map-extent.js'; +import { MapInput } from './map-input.js'; +import { MapLink } from './map-link.js'; export class MapViewer extends HTMLElement { static get observedAttributes() { @@ -44,21 +45,21 @@ 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); } 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); } get lat() { - return this.hasAttribute('lat') ? this.getAttribute('lat') : '0'; + return +(this.hasAttribute('lat') ? this.getAttribute('lat') : 0); } set lat(val) { if (val) { @@ -66,7 +67,7 @@ export class MapViewer extends HTMLElement { } } get lon() { - return this.hasAttribute('lon') ? this.getAttribute('lon') : '0'; + return +(this.hasAttribute('lon') ? this.getAttribute('lon') : 0); } set lon(val) { if (val) { @@ -90,7 +91,7 @@ export class MapViewer extends HTMLElement { } } 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); @@ -145,7 +146,7 @@ export class MapViewer extends HTMLElement { .then(() => { this._initShadowRoot(); - this._controlsList = new DOMTokenList( + this._controlsList = new M.DOMTokenList( this.getAttribute('controlslist'), this, 'controlslist', @@ -357,6 +358,7 @@ export class MapViewer extends HTMLElement { // level in the crs by changing the zoom level of the map when // you set the map crs. So, we save the current view for use below // when all the layers' reconnections have settled. + // leaflet doesn't like this: https://github.com/Leaflet/Leaflet/issues/2553 this._map.options.crs = M[newValue]; this._map.options.projection = newValue; let layersReady = []; @@ -371,9 +373,13 @@ export class MapViewer extends HTMLElement { // use the saved map location to ensure it is correct after // changing the map CRS. Specifically affects projection // upgrades, e.g. https://maps4html.org/experiments/custom-projections/BNG/ + // see leaflet bug: https://github.com/Leaflet/Leaflet/issues/2553 this.zoomTo(lat, lon, zoom); - this._resetHistory(); - this._map.announceMovement.enable(); + if (M.options.announceMovement) + this._map.announceMovement.enable(); + this.querySelectorAll('layer-').forEach((layer) => { + layer.dispatchEvent(new CustomEvent('map-change')); + }); }); } }; @@ -383,6 +389,14 @@ export class MapViewer extends HTMLElement { connect(); resolve(); }).then(() => { + if (this._map && this._map.options.projection !== oldValue) { + // this awful hack is brought to you by a leaflet bug/ feature request + // https://github.com/Leaflet/Leaflet/issues/2553 + this.zoomTo(this.lat, this.lon, this.zoom + 1); + this.zoomTo(this.lat, this.lon, this.zoom - 1); + // this doesn't completely work either + this._resetHistory(); + } if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); }); } @@ -1148,7 +1162,7 @@ export class MapViewer extends HTMLElement { if (M[t.projection.toUpperCase()]) return t.projection.toUpperCase(); let tileSize = [256, 512, 1024, 2048, 4096].includes(t.tilesize) ? t.tilesize - : 256; + : M.TILE_SIZE; M[t.projection] = new L.Proj.CRS(t.projection, t.proj4string, { origin: t.origin, @@ -1344,10 +1358,11 @@ export class MapViewer extends HTMLElement { } }); } - async whenLayersReady() { + whenLayersReady() { let layersReady = []; + // check if all the children elements (map-extent, map-feature) of all layer- are ready for (let layer of [...this.layers]) { - layersReady.push(layer.whenReady()); + layersReady.push(layer.whenElemsReady()); } return Promise.allSettled(layersReady); } @@ -1389,3 +1404,5 @@ window.customElements.define('layer-', MapLayer); window.customElements.define('map-caption', MapCaption); window.customElements.define('map-feature', MapFeature); window.customElements.define('map-extent', MapExtent); +window.customElements.define('map-input', MapInput); +window.customElements.define('map-link', MapLink); diff --git a/src/mapml.css b/src/mapml.css index eaa5990e3..3509685f1 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -52,6 +52,14 @@ } } +/* for extents that don't overlap the viewport, the extent and it's layer +control fieldset become disabled. The descendant fieldsets also become +disabled (but they / their descendant content are not explicitly italicized by +the browser)' */ +.leaflet-control-layers-overlays fieldset:disabled span.mapml-layer-item-name { + font-style: italic; +} + /* Generic class for seamless buttons */ .mapml-button { background-color: transparent; @@ -855,7 +863,7 @@ label.mapml-layer-item-toggle { padding-inline-start: 0; } -.mapml-layer-item-settings .mapml-layer-extent .mapml-layer-item-opacity { +.mapml-layer-item-settings .mapml-layer-extent .mapml-layer-item-details { padding-inline-start: 1.6rem; } diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index 2f5404070..b524fd353 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -27,8 +27,6 @@ export var LayerControl = L.Control.Layers.extend({ }, 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], @@ -43,7 +41,6 @@ export var LayerControl = L.Control.Layers.extend({ this ); this._update(); - //this._validateExtents(); if (this._layers.length < 1 && !this._map._showControls) { this._container.setAttribute('hidden', ''); } else { @@ -52,18 +49,12 @@ export var LayerControl = L.Control.Layers.extend({ 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); - } }, addOrUpdateOverlay: function (layer, name) { var alreadyThere = false; @@ -90,49 +81,8 @@ export var LayerControl = L.Control.Layers.extend({ 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'; - } - // check if an extent is disabled and disable it - if ( - this._layers[i].layer._properties && - this._layers[i].layer._properties._mapExtents - ) { - for ( - let j = 0; - j < this._layers[i].layer._properties._mapExtents.length; - j++ - ) { - let input = - this._layers[i].layer._properties._mapExtents[j].extentAnatomy, - label = input.getElementsByClassName('mapml-layer-item-name')[0]; - if ( - this._layers[i].layer._properties._mapExtents[j].disabled && - this._layers[i].layer._properties._mapExtents[j].checked - ) { - label.style.fontStyle = 'italic'; - input.disabled = true; - } else { - label.style.fontStyle = 'normal'; - input.disabled = false; - } - } - } - } - }, + + _checkDisabledLayers: function () {}, // focus the first layer in the layer control when enter is pressed _focusFirstLayer: function (e) { @@ -152,7 +102,7 @@ export var LayerControl = L.Control.Layers.extend({ return range.min <= zoom && zoom <= range.max; }, _addItem: function (obj) { - var layercontrols = obj.layer.getLayerUserControlsHTML(); + var layercontrols = obj.layer._layerEl._layerControlHTML; // the input is required by Leaflet... obj.input = layercontrols.querySelector( 'input.leaflet-control-layers-selector' @@ -161,7 +111,6 @@ export var LayerControl = L.Control.Layers.extend({ this._layerControlInputs.push(obj.input); obj.input.layerId = L.stamp(obj.layer); - L.DomEvent.on(obj.input, 'click', this._onInputClick, this); this._overlaysList.appendChild(layercontrols); return layercontrols; }, diff --git a/src/mapml/elementSupport/extents/createLayerControlForExtent.js b/src/mapml/elementSupport/extents/createLayerControlForExtent.js new file mode 100644 index 000000000..80c76df89 --- /dev/null +++ b/src/mapml/elementSupport/extents/createLayerControlForExtent.js @@ -0,0 +1,282 @@ +export var createLayerControlExtentHTML = function () { + var extent = L.DomUtil.create('fieldset', 'mapml-layer-extent'), + extentProperties = L.DomUtil.create( + 'div', + 'mapml-layer-item-properties', + extent + ), + extentSettings = L.DomUtil.create( + 'div', + 'mapml-layer-item-settings', + extent + ), + extentLabel = L.DomUtil.create( + 'label', + 'mapml-layer-item-toggle', + extentProperties + ), + input = L.DomUtil.create('input'), + svgExtentControlIcon = L.SVG.create('svg'), + extentControlPath1 = L.SVG.create('path'), + extentControlPath2 = L.SVG.create('path'), + extentNameIcon = L.DomUtil.create('span'), + extentItemControls = L.DomUtil.create( + 'div', + 'mapml-layer-item-controls', + extentProperties + ), + opacityControl = L.DomUtil.create( + 'details', + 'mapml-layer-item-details mapml-control-layers', + extentSettings + ), + extentOpacitySummary = L.DomUtil.create('summary', '', opacityControl), + mapEl = this.parentLayer.parentNode, + layerEl = this.parentLayer, + opacity = L.DomUtil.create('input', '', opacityControl); + extentSettings.hidden = true; + extent.setAttribute('aria-grabbed', 'false'); + + // append the svg paths + svgExtentControlIcon.setAttribute('viewBox', '0 0 24 24'); + svgExtentControlIcon.setAttribute('height', '22'); + svgExtentControlIcon.setAttribute('width', '22'); + extentControlPath1.setAttribute('d', 'M0 0h24v24H0z'); + extentControlPath1.setAttribute('fill', 'none'); + extentControlPath2.setAttribute( + 'd', + 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' + ); + svgExtentControlIcon.appendChild(extentControlPath1); + svgExtentControlIcon.appendChild(extentControlPath2); + + if (this._userInputs) { + var frag = document.createDocumentFragment(); + var templates = this._templateVars; + if (templates) { + this._selectdetails = []; + for (var i = 0; i < templates.length; i++) { + var template = templates[i]; + for (var j = 0; j < template.values.length; j++) { + var mapmlInput = template.values[j], + id = '#' + mapmlInput.getAttribute('id'); + // don't add it again if it is referenced > once + if ( + mapmlInput.tagName.toLowerCase() === 'map-select' && + !frag.querySelector(id) + ) { + // generate a
+ var selectdetails = L.DomUtil.create( + 'details', + 'mapml-layer-item-details mapml-control-layers', + frag + ), + selectsummary = L.DomUtil.create('summary'), + selectSummaryLabel = L.DomUtil.create('label'); + selectSummaryLabel.innerText = mapmlInput.getAttribute('name'); + selectSummaryLabel.setAttribute( + 'for', + mapmlInput.getAttribute('id') + ); + selectsummary.appendChild(selectSummaryLabel); + selectdetails.appendChild(selectsummary); + selectdetails.appendChild(mapmlInput.htmlselect); + this._selectdetails.push(selectdetails); + } + } + } + } + extentSettings.appendChild(frag); + } + + let removeExtentButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-remove-control', + extentItemControls + ); + removeExtentButton.type = 'button'; + removeExtentButton.title = 'Remove Sub Layer'; + removeExtentButton.innerHTML = ""; + removeExtentButton.classList.add('mapml-button'); + removeExtentButton.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.remove(); + }); + + let extentsettingsButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-settings-control', + extentItemControls + ); + extentsettingsButton.type = 'button'; + extentsettingsButton.title = 'Extent Settings'; + extentsettingsButton.setAttribute('aria-expanded', false); + extentsettingsButton.classList.add('mapml-button'); + L.DomEvent.on( + extentsettingsButton, + 'click', + (e) => { + if (extentSettings.hidden === true) { + extentsettingsButton.setAttribute('aria-expanded', true); + extentSettings.hidden = false; + } else { + extentsettingsButton.setAttribute('aria-expanded', false); + extentSettings.hidden = true; + } + }, + this + ); + + extentNameIcon.setAttribute('aria-hidden', true); + extentLabel.appendChild(input); + extentsettingsButton.appendChild(extentNameIcon); + extentNameIcon.appendChild(svgExtentControlIcon); + extentOpacitySummary.innerText = 'Opacity'; + extentOpacitySummary.id = + 'mapml-extent-item-opacity-' + L.stamp(extentOpacitySummary); + opacity.setAttribute('type', 'range'); + opacity.setAttribute('min', '0'); + opacity.setAttribute('max', '1.0'); + opacity.setAttribute('step', '0.1'); + opacity.setAttribute( + 'aria-labelledby', + 'mapml-extent-item-opacity-' + L.stamp(extentOpacitySummary) + ); + const changeOpacity = function (e) { + if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { + this._templatedLayer.changeOpacity(e.target.value); + } + }; + opacity.setAttribute('value', this.opacity); + opacity.value = this._templatedLayer._container.style.opacity || '1.0'; + opacity.addEventListener('change', changeOpacity.bind(this)); + + var extentItemNameSpan = L.DomUtil.create( + 'span', + 'mapml-layer-item-name', + extentLabel + ); + input.type = 'checkbox'; + input.defaultChecked = this.checked; + extentItemNameSpan.innerHTML = this.label; + const changeCheck = function () { + this.checked = !this.checked; + }; + // save for later access by API + this._layerControlCheckbox = input; + input.addEventListener('change', changeCheck.bind(this)); + extentItemNameSpan.id = + 'mapml-extent-item-name-{' + L.stamp(extentItemNameSpan) + '}'; + extent.setAttribute('aria-labelledby', extentItemNameSpan.id); + extentItemNameSpan.extent = this; + + extent.ontouchstart = extent.onmousedown = (downEvent) => { + if ( + (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && + downEvent.target.tagName.toLowerCase() !== 'input') || + downEvent.target.tagName.toLowerCase() === 'label' + ) { + downEvent.stopPropagation(); + downEvent = + downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent; + + let control = extent, + controls = extent.parentNode, + moving = false, + yPos = downEvent.clientY, + originalPosition = Array.from( + extent.parentElement.querySelectorAll('fieldset') + ).indexOf(extent); + + document.body.ontouchmove = document.body.onmousemove = (moveEvent) => { + moveEvent.preventDefault(); + moveEvent = + moveEvent instanceof TouchEvent ? moveEvent.touches[0] : moveEvent; + + // Fixes flickering by only moving element when there is enough space + let offset = moveEvent.clientY - yPos; + moving = Math.abs(offset) > 15 || moving; + if ( + (controls && !moving) || + (controls && controls.childElementCount <= 1) || + controls.getBoundingClientRect().top > + control.getBoundingClientRect().bottom || + controls.getBoundingClientRect().bottom < + control.getBoundingClientRect().top + ) { + return; + } + + controls.classList.add('mapml-draggable'); + control.style.transform = 'translateY(' + offset + 'px)'; + control.style.pointerEvents = 'none'; + + let x = moveEvent.clientX, + y = moveEvent.clientY, + root = + mapEl.tagName === 'MAPML-VIEWER' + ? mapEl.shadowRoot + : mapEl.querySelector('.mapml-web-map').shadowRoot, + elementAt = root.elementFromPoint(x, y), + swapControl = + !elementAt || !elementAt.closest('fieldset') + ? control + : elementAt.closest('fieldset'); + + swapControl = + Math.abs(offset) <= swapControl.offsetHeight ? control : swapControl; + + control.setAttribute('aria-grabbed', 'true'); + control.setAttribute('aria-dropeffect', 'move'); + if (swapControl && controls === swapControl.parentNode) { + swapControl = + swapControl !== control.nextSibling + ? swapControl + : swapControl.nextSibling; + if (control !== swapControl) { + yPos = moveEvent.clientY; + control.style.transform = null; + } + controls.insertBefore(control, swapControl); + } + }; + + document.body.ontouchend = document.body.onmouseup = () => { + let newPosition = Array.from( + extent.parentElement.querySelectorAll('fieldset') + ).indexOf(extent); + control.setAttribute('aria-grabbed', 'false'); + control.removeAttribute('aria-dropeffect'); + control.style.pointerEvents = null; + control.style.transform = null; + if (originalPosition !== newPosition) { + let controlsElems = controls.children, + zIndex = 0; + for (let c of controlsElems) { + let extentEl = c.querySelector('span').extent; + + extentEl.setAttribute('data-moving', ''); + layerEl.insertAdjacentElement('beforeend', extentEl); + extentEl.removeAttribute('data-moving'); + + extentEl.extentZIndex = zIndex; + extentEl._templatedLayer.setZIndex(zIndex); + zIndex++; + } + } + controls.classList.remove('mapml-draggable'); + document.body.ontouchmove = + document.body.onmousemove = + document.body.ontouchend = + document.body.onmouseup = + null; + }; + } + }; + this._extentRootFieldset = extent; + this._opacitySlider = opacity; + this._opacityControl = opacityControl; + this._layerControlLabel = extentLabel; + return extent; +}; diff --git a/src/mapml/elementSupport/inputs/heightInput.js b/src/mapml/elementSupport/inputs/heightInput.js new file mode 100644 index 000000000..3629b0ddd --- /dev/null +++ b/src/mapml/elementSupport/inputs/heightInput.js @@ -0,0 +1,18 @@ +export class HeightInput { + constructor(name, layer) { + this.name = name; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + return true; + } + + getValue() { + return this.layer._map.getSize().y; + } +} diff --git a/src/mapml/elementSupport/inputs/hiddenInput.js b/src/mapml/elementSupport/inputs/hiddenInput.js new file mode 100644 index 000000000..0919ef90f --- /dev/null +++ b/src/mapml/elementSupport/inputs/hiddenInput.js @@ -0,0 +1,19 @@ +export class HiddenInput { + constructor(name, value) { + this.name = name; + this.value = value; + } + + validateInput() { + // name is required + // value is required + if (!this.name || !this.value) { + return false; + } + return true; + } + + getValue() { + return this.value; + } +} diff --git a/src/mapml/elementSupport/inputs/locationInput.js b/src/mapml/elementSupport/inputs/locationInput.js new file mode 100644 index 000000000..fb4e7b880 --- /dev/null +++ b/src/mapml/elementSupport/inputs/locationInput.js @@ -0,0 +1,116 @@ +export class LocationInput { + constructor(name, position, axis, units, min, max, rel, layer) { + this.name = name; + this.position = position; + this.axis = axis; + // if unit/cs not present, find it + if (!units && axis && !['i', 'j'].includes(axis)) { + this.units = M.axisToCS(axis).toLowerCase(); + } else { + this.units = units; // cs + } + this.min = min; + this.max = max; + this.rel = rel; + this.layer = layer; + } + + validateInput() { + // name is required + // axis is required + if (!this.name || !this.axis) { + return false; + } + // cs/units is only required when the axis is i/j. To differentiate between the units/cs + if ( + (this.axis === 'i' || this.axis === 'j') && + !['map', 'tile'].includes(this.units) + ) { + return false; + } + // check if axis match the units/cs + if (this.units) { + let axisCS = M.axisToCS(this.axis); + if ( + typeof axisCS === 'string' && + axisCS.toUpperCase() !== this.units.toUpperCase() + ) { + return false; + } + } + // position is not required, will default to top-left + // min max fallbacks, map-meta -> projection + // rel not required, default is image/extent + return true; + } + + _TCRSToPCRS(coords, zoom) { + // TCRS pixel point to Projected CRS point (in meters, presumably) + var map = this.layer._map, + crs = map.options.crs, + loc = crs.transformation.untransform(coords, crs.scale(zoom)); + return loc; + } + + getValue(zoom = undefined, bounds = undefined) { + // units = cs + // + if (zoom === undefined) zoom = this.layer._map.getZoom(); + if (bounds === undefined) bounds = this.layer._map.getPixelBounds(); + + if (this.units === 'pcrs' || this.units === 'gcrs') { + switch (this.axis) { + case 'longitude': + case 'easting': + if (this.position) { + if (this.position.match(/.*?-left/i)) { + return this._TCRSToPCRS(bounds.min, zoom).x; + } else if (this.position.match(/.*?-right/i)) { + return this._TCRSToPCRS(bounds.max, zoom).x; + } + } else { + // position is not required, will default to top-left + return this._TCRSToPCRS(bounds.min, zoom).x; + } + break; + case 'latitude': + case 'northing': + if (this.position) { + if (this.position.match(/top-.*?/i)) { + return this._TCRSToPCRS(bounds.min, zoom).y; + } else if (this.position.match(/bottom-.*?/i)) { + return this._TCRSToPCRS(bounds.max, zoom).y; + } + } else { + // position is not required, will default to top-left + return this._TCRSToPCRS(bounds.min, zoom).y; + } + break; + } + } else if (this.units === 'tilematrix') { + // Value is retrieved from the createTile method of TemplatedTileLayer, on move end. + // Different values for each tile when filling in the map tiles on the map. + // Currently storing all x,y,z within one object, + // TODO: change return value as needed based on usage by map-input + // https://github.com/Leaflet/Leaflet/blob/6994baf25f267db1c8b720c28a61e0700d0aa0e8/src/layer/tile/GridLayer.js#L652 + const center = this.layer._map.getCenter(); + const templatedTileLayer = this.layer._templatedLayer._templates[0].layer; + const pixelBounds = templatedTileLayer._getTiledPixelBounds(center); + const tileRange = templatedTileLayer._pxBoundsToTileRange(pixelBounds); + let obj = []; + for (let j = tileRange.min.y; j <= tileRange.max.y; j++) { + for (let i = tileRange.min.x; i <= tileRange.max.x; i++) { + const coords = new L.Point(i, j); + coords.z = templatedTileLayer._tileZoom; + obj.push(coords); + } + } + return obj; + } else if (this.units === 'tile' || this.units === 'map') { + // used for query handler on map enter or click. + // mapi, tilei, mapj, tilej used for query handling, value is derived from the mouse click event + // or center of the map when used with keyboard. + } + return; + } +} diff --git a/src/mapml/elementSupport/inputs/widthInput.js b/src/mapml/elementSupport/inputs/widthInput.js new file mode 100644 index 000000000..756f72ffe --- /dev/null +++ b/src/mapml/elementSupport/inputs/widthInput.js @@ -0,0 +1,18 @@ +export class WidthInput { + constructor(name, layer) { + this.name = name; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + return true; + } + + getValue() { + return this.layer._map.getSize().x; + } +} diff --git a/src/mapml/elementSupport/inputs/zoomInput.js b/src/mapml/elementSupport/inputs/zoomInput.js new file mode 100644 index 000000000..5ac511675 --- /dev/null +++ b/src/mapml/elementSupport/inputs/zoomInput.js @@ -0,0 +1,26 @@ +export class ZoomInput { + constructor(name, min, max, value, step, layer) { + this.name = name; + this.min = min; + this.max = max; + this.value = value; + this.step = step; + this.layer = layer; + } + + validateInput() { + // name is required + if (!this.name) { + return false; + } + // min and max can not be present + // fallback would be layer's meta, -> projection min, max + // don't need value, map-meta max value, -> fallback is max zoom of projection + // don't need step, defaults to 1 + return true; + } + + getValue() { + return this.layer._map.options.mapEl.zoom; + } +} diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js new file mode 100644 index 000000000..e25bb6f5c --- /dev/null +++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js @@ -0,0 +1,305 @@ +export var createLayerControlHTML = function () { + var fieldset = L.DomUtil.create('fieldset', 'mapml-layer-item'), + input = L.DomUtil.create('input'), + layerItemName = L.DomUtil.create('span', 'mapml-layer-item-name'), + settingsButtonNameIcon = L.DomUtil.create('span'), + layerItemProperty = L.DomUtil.create( + 'div', + 'mapml-layer-item-properties', + fieldset + ), + layerItemSettings = L.DomUtil.create( + 'div', + 'mapml-layer-item-settings', + fieldset + ), + itemToggleLabel = L.DomUtil.create( + 'label', + 'mapml-layer-item-toggle', + layerItemProperty + ), + layerItemControls = L.DomUtil.create( + 'div', + 'mapml-layer-item-controls', + layerItemProperty + ), + opacityControl = L.DomUtil.create( + 'details', + 'mapml-layer-item-opacity mapml-control-layers', + layerItemSettings + ), + opacity = L.DomUtil.create('input'), + opacityControlSummary = L.DomUtil.create('summary'), + svgSettingsControlIcon = L.SVG.create('svg'), + settingsControlPath1 = L.SVG.create('path'), + settingsControlPath2 = L.SVG.create('path'), + extentsFieldset = L.DomUtil.create( + 'fieldset', + 'mapml-layer-grouped-extents' + ), + mapEl = this.parentNode; + + // append the paths in svg for the remove layer and toggle icons + svgSettingsControlIcon.setAttribute('viewBox', '0 0 24 24'); + svgSettingsControlIcon.setAttribute('height', '22'); + svgSettingsControlIcon.setAttribute('width', '22'); + svgSettingsControlIcon.setAttribute('fill', 'currentColor'); + settingsControlPath1.setAttribute('d', 'M0 0h24v24H0z'); + settingsControlPath1.setAttribute('fill', 'none'); + settingsControlPath2.setAttribute( + 'd', + 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' + ); + svgSettingsControlIcon.appendChild(settingsControlPath1); + svgSettingsControlIcon.appendChild(settingsControlPath2); + + layerItemSettings.hidden = true; + settingsButtonNameIcon.setAttribute('aria-hidden', true); + + let removeControlButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-remove-control', + layerItemControls + ); + removeControlButton.type = 'button'; + removeControlButton.title = 'Remove Layer'; + removeControlButton.innerHTML = ""; + removeControlButton.classList.add('mapml-button'); + L.DomEvent.on(removeControlButton, 'click', L.DomEvent.stop); + L.DomEvent.on( + removeControlButton, + 'click', + (e) => { + let fieldset = 0, + elem, + root; + root = + mapEl.tagName === 'MAPML-VIEWER' + ? mapEl.shadowRoot + : mapEl.querySelector('.mapml-web-map').shadowRoot; + if ( + e.target.closest('fieldset').nextElementSibling && + !e.target.closest('fieldset').nextElementSibling.disbaled + ) { + elem = e.target.closest('fieldset').previousElementSibling; + while (elem) { + fieldset += 2; // find the next layer menu item + elem = elem.previousElementSibling; + } + } else { + // focus on the link + elem = 'link'; + } + mapEl.removeChild( + e.target.closest('fieldset').querySelector('span').layer._layerEl + ); + elem = elem + ? root.querySelector('.leaflet-control-attribution').firstElementChild + : (elem = root.querySelectorAll('input')[fieldset]); + elem.focus(); + }, + this._layer + ); + + let itemSettingControlButton = L.DomUtil.create( + 'button', + 'mapml-layer-item-settings-control', + layerItemControls + ); + itemSettingControlButton.type = 'button'; + itemSettingControlButton.title = 'Layer Settings'; + itemSettingControlButton.setAttribute('aria-expanded', false); + itemSettingControlButton.classList.add('mapml-button'); + L.DomEvent.on( + itemSettingControlButton, + 'click', + (e) => { + let layerControl = this._layer._layerEl._layerControl._container; + if (!layerControl._isExpanded && e.pointerType === 'touch') { + layerControl._isExpanded = true; + return; + } + if (layerItemSettings.hidden === true) { + itemSettingControlButton.setAttribute('aria-expanded', true); + layerItemSettings.hidden = false; + } else { + itemSettingControlButton.setAttribute('aria-expanded', false); + layerItemSettings.hidden = true; + } + }, + this._layer + ); + + input.defaultChecked = this.checked; + input.type = 'checkbox'; + input.setAttribute('class', 'leaflet-control-layers-selector'); + layerItemName.layer = this._layer; + const changeCheck = function () { + this.checked = !this.checked; + }; + input.addEventListener('change', changeCheck.bind(this)); + if (this._layer._legendUrl) { + var legendLink = document.createElement('a'); + legendLink.text = ' ' + this._layer._title; + legendLink.href = this._layer._legendUrl; + legendLink.target = '_blank'; + legendLink.draggable = false; + layerItemName.appendChild(legendLink); + } else { + layerItemName.innerHTML = this._layer._title; + } + layerItemName.id = 'mapml-layer-item-name-{' + L.stamp(layerItemName) + '}'; + opacityControlSummary.innerText = 'Opacity'; + opacityControlSummary.id = + 'mapml-layer-item-opacity-' + L.stamp(opacityControlSummary); + opacityControl.appendChild(opacityControlSummary); + opacityControl.appendChild(opacity); + opacity.setAttribute('type', 'range'); + opacity.setAttribute('min', '0'); + opacity.setAttribute('max', '1.0'); + opacity.setAttribute('value', this._layer._container.style.opacity || '1.0'); + opacity.setAttribute('step', '0.1'); + opacity.setAttribute( + 'aria-labelledby', + 'mapml-layer-item-opacity-' + L.stamp(opacityControlSummary) + ); + + const changeOpacity = function (e) { + if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { + this._layer.changeOpacity(e.target.value); + } + }; + opacity.value = this._layer._container.style.opacity || '1.0'; + opacity.addEventListener('change', changeOpacity.bind(this)); + + fieldset.setAttribute('aria-grabbed', 'false'); + fieldset.setAttribute('aria-labelledby', layerItemName.id); + + fieldset.ontouchstart = fieldset.onmousedown = (downEvent) => { + if ( + (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && + downEvent.target.tagName.toLowerCase() !== 'input') || + downEvent.target.tagName.toLowerCase() === 'label' + ) { + downEvent = + downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent; + let control = fieldset, + controls = fieldset.parentNode, + moving = false, + yPos = downEvent.clientY, + originalPosition = Array.from( + controls.querySelectorAll('fieldset') + ).indexOf(fieldset); + + document.body.ontouchmove = document.body.onmousemove = (moveEvent) => { + moveEvent.preventDefault(); + moveEvent = + moveEvent instanceof TouchEvent ? moveEvent.touches[0] : moveEvent; + + // Fixes flickering by only moving element when there is enough space + let offset = moveEvent.clientY - yPos; + moving = Math.abs(offset) > 15 || moving; + if ( + (controls && !moving) || + (controls && controls.childElementCount <= 1) || + controls.getBoundingClientRect().top > + control.getBoundingClientRect().bottom || + controls.getBoundingClientRect().bottom < + control.getBoundingClientRect().top + ) { + return; + } + + controls.classList.add('mapml-draggable'); + control.style.transform = 'translateY(' + offset + 'px)'; + control.style.pointerEvents = 'none'; + + let x = moveEvent.clientX, + y = moveEvent.clientY, + root = + mapEl.tagName === 'MAPML-VIEWER' + ? mapEl.shadowRoot + : mapEl.querySelector('.mapml-web-map').shadowRoot, + elementAt = root.elementFromPoint(x, y), + swapControl = + !elementAt || !elementAt.closest('fieldset') + ? control + : elementAt.closest('fieldset'); + + swapControl = + Math.abs(offset) <= swapControl.offsetHeight ? control : swapControl; + + control.setAttribute('aria-grabbed', 'true'); + control.setAttribute('aria-dropeffect', 'move'); + if (swapControl && controls === swapControl.parentNode) { + swapControl = + swapControl !== control.nextSibling + ? swapControl + : swapControl.nextSibling; + if (control !== swapControl) { + yPos = moveEvent.clientY; + control.style.transform = null; + } + controls.insertBefore(control, swapControl); + } + }; + + document.body.ontouchend = document.body.onmouseup = () => { + let newPosition = Array.from( + controls.querySelectorAll('fieldset') + ).indexOf(fieldset); + control.setAttribute('aria-grabbed', 'false'); + control.removeAttribute('aria-dropeffect'); + control.style.pointerEvents = null; + control.style.transform = null; + if (originalPosition !== newPosition) { + let controlsElems = controls.children, + zIndex = 1; + // re-order layer elements DOM order + for (let c of controlsElems) { + let layerEl = c.querySelector('span').layer._layerEl; + layerEl.setAttribute('data-moving', ''); + mapEl.insertAdjacentElement('beforeend', layerEl); + layerEl.removeAttribute('data-moving'); + } + // update zIndex of all layer- elements + let layers = mapEl.querySelectorAll('layer-'); + for (let i = 0; i < layers.length; i++) { + let layer = layers[i]._layer; + if (layer.options.zIndex !== zIndex) { + layer.setZIndex(zIndex); + } + zIndex++; + } + } + controls.classList.remove('mapml-draggable'); + document.body.ontouchmove = + document.body.onmousemove = + document.body.onmouseup = + null; + }; + } + }; + + itemToggleLabel.appendChild(input); + itemToggleLabel.appendChild(layerItemName); + itemSettingControlButton.appendChild(settingsButtonNameIcon); + settingsButtonNameIcon.appendChild(svgSettingsControlIcon); + + if (this._layer._styles) { + layerItemSettings.appendChild(this._layer._styles); + } + + this._layerControlCheckbox = input; + this._layerControlLabel = itemToggleLabel; + this._opacityControl = opacityControl; + this._opacitySlider = opacity; + this._layerControlHTML = fieldset; + this._layerItemSettingsHTML = layerItemSettings; + this._propertiesGroupAnatomy = extentsFieldset; + this._styles = this._layer._styles; + extentsFieldset.setAttribute('aria-label', 'Sublayers'); + extentsFieldset.setAttribute('hidden', ''); + layerItemSettings.appendChild(extentsFieldset); + return this._layerControlHTML; +}; diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js index 6edda8e82..921a216c3 100644 --- a/src/mapml/handlers/AnnounceMovement.js +++ b/src/mapml/handlers/AnnounceMovement.js @@ -121,22 +121,27 @@ export var AnnounceMovement = L.Handler.extend({ 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); + totalBounds: function (e) { + // don't bother with non-MapMLLayer layers... + if (!e.layer._layerEl) return; + let map = this.options.mapEl; + map.whenLayersReady().then(() => { + let layers = map.querySelectorAll('layer-'); + let bounds; + for (let i = 0; i < layers.length; i++) { + // the _layer may no longer exist if this is invoked by layerremove + if (layers[i]._layer) { + let extent = layers[i].extent; + if (bounds && extent) { + bounds.extend(M.extentToBounds(extent, 'pcrs')); + } else if (extent) { + bounds = M.extentToBounds(extent, 'pcrs'); + } } - bounds.extend(this._layers[i].layerBounds.min); - bounds.extend(this._layers[i].layerBounds.max); } - }); - this.totalLayerBounds = bounds; + this.totalLayerBounds = bounds; + }); }, dragged: function () { diff --git a/src/mapml/index.js b/src/mapml/index.js index cd05e6b5a..dbfac8fa4 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -80,6 +80,16 @@ import { featureIndexOverlay, FeatureIndexOverlay } from './layers/FeatureIndexOverlay'; +// element support +import { createLayerControlExtentHTML } from './elementSupport/extents/createLayerControlForExtent'; +import { createLayerControlHTML } from './elementSupport/layers/createLayerControlForLayer'; +import { ZoomInput } from './elementSupport/inputs/zoomInput'; +import { HiddenInput } from './elementSupport/inputs/hiddenInput'; +import { WidthInput } from './elementSupport/inputs/widthInput'; +import { HeightInput } from './elementSupport/inputs/heightInput'; +import { LocationInput } from './elementSupport/inputs/locationInput'; + +import { DOMTokenList } from './utils/DOMTokenList'; /* global L, Node */ (function (window, document, undefined) { @@ -805,6 +815,7 @@ import { M.axisToXY = Util.axisToXY; M.csToAxes = Util.csToAxes; M._convertAndFormatPCRS = Util._convertAndFormatPCRS; + M.extentToBounds = Util.extentToBounds; M.axisToCS = Util.axisToCS; M._parseNumber = Util._parseNumber; M._extractInputBounds = Util._extractInputBounds; @@ -900,4 +911,21 @@ import { M.Geometry = Geometry; M.geometry = geometry; + + // element support + M._createLayerControlExtentHTML = createLayerControlExtentHTML; + M._createLayerControlHTML = createLayerControlHTML; + M.ZoomInput = ZoomInput; + M.HiddenInput = HiddenInput; + M.WidthInput = WidthInput; + M.HeightInput = HeightInput; + M.LocationInput = LocationInput; + + // constants + M.TILE_SIZE = 256; + M.FALLBACK_PROJECTION = 'OSMTILE'; + M.FALLBACK_CS = 'TILEMATRIX'; + M.BLANK_TT_TREF = 'mapmltemplatedtileplaceholder'; + + M.DOMTokenList = DOMTokenList; })(window, document); diff --git a/src/mapml/layers/DebugOverlay.js b/src/mapml/layers/DebugOverlay.js index 2c089135b..049eab823 100644 --- a/src/mapml/layers/DebugOverlay.js +++ b/src/mapml/layers/DebugOverlay.js @@ -199,7 +199,8 @@ export var DebugVectors = L.LayerGroup.extend({ map.options.crs.scale(0) ); this._centerVector = L.circle(map.options.crs.pointToLatLng(center, 0), { - radius: 250 + radius: 250, + className: 'mapml-debug-vectors projection-centre' }); this._centerVector.bindTooltip('Projection Center'); @@ -210,63 +211,92 @@ export var DebugVectors = L.LayerGroup.extend({ }, _addBounds: function (map) { - let id = Object.keys(map._layers), - layers = map._layers, - colors = ['#FF5733', '#8DFF33', '#3397FF', '#E433FF', '#F3FF33'], - j = 0; - - this.addLayer(this._centerVector); - - for (let i of id) { - if (layers[i].layerBounds || layers[i].extentBounds) { - let boundsArray; - if (layers[i].layerBounds) { - boundsArray = [ - layers[i].layerBounds.min, - L.point(layers[i].layerBounds.max.x, layers[i].layerBounds.min.y), - layers[i].layerBounds.max, - L.point(layers[i].layerBounds.min.x, layers[i].layerBounds.max.y) - ]; - } else { - boundsArray = [ - layers[i].extentBounds.min, - L.point(layers[i].extentBounds.max.x, layers[i].extentBounds.min.y), - layers[i].extentBounds.max, - L.point(layers[i].extentBounds.min.x, layers[i].extentBounds.max.y) - ]; - } - let boundsRect = projectedExtent(boundsArray, { - color: colors[j % colors.length], - weight: 2, - opacity: 1, - fillOpacity: 0.01, - fill: true - }); - if (layers[i].options._leafletLayer) - boundsRect.bindTooltip(layers[i].options._leafletLayer._title, { - sticky: true + // to delay the addBounds to wait for the layer.extentbounds / layer.layerbounds to be ready when the layer- checked attribute is changed + setTimeout(() => { + let id = Object.keys(map._layers), + layers = map._layers, + colors = ['#FF5733', '#8DFF33', '#3397FF', '#E433FF', '#F3FF33'], + j = 0; + + this.addLayer(this._centerVector); + + for (let i of id) { + if (layers[i].layerBounds || layers[i].extentBounds) { + let boundsArray; + if (layers[i].layerBounds) { + boundsArray = [ + layers[i].layerBounds.min, + L.point(layers[i].layerBounds.max.x, layers[i].layerBounds.min.y), + layers[i].layerBounds.max, + L.point(layers[i].layerBounds.min.x, layers[i].layerBounds.max.y) + ]; + } else { + boundsArray = [ + layers[i].extentBounds.min, + L.point( + layers[i].extentBounds.max.x, + layers[i].extentBounds.min.y + ), + layers[i].extentBounds.max, + L.point( + layers[i].extentBounds.min.x, + layers[i].extentBounds.max.y + ) + ]; + } + + // boundsTestTag adds the value of from the element + // if it exists. this simplifies debugging because the svg path will be + // tagged with the layer it came from + let boundsTestTag = + layers[i].extentBounds && + layers[i].options.extentEl.parentLayer.hasAttribute('data-testid') + ? layers[i].options.extentEl.parentLayer.getAttribute( + 'data-testid' + ) + : layers[i].layerBounds && + layers[i].options._leafletLayer._layerEl.hasAttribute( + 'data-testid' + ) + ? layers[i].options._leafletLayer._layerEl.getAttribute( + 'data-testid' + ) + : ''; + let boundsRect = projectedExtent(boundsArray, { + className: this.options.className.concat(' ', boundsTestTag), + color: colors[j % colors.length], + weight: 2, + opacity: 1, + fillOpacity: 0.01, + fill: true }); - this.addLayer(boundsRect); - j++; + if (layers[i].options._leafletLayer) + boundsRect.bindTooltip(layers[i].options._leafletLayer._title, { + sticky: true + }); + this.addLayer(boundsRect); + j++; + } } - } - if (map.totalLayerBounds) { - let totalBoundsArray = [ - map.totalLayerBounds.min, - L.point(map.totalLayerBounds.max.x, map.totalLayerBounds.min.y), - map.totalLayerBounds.max, - L.point(map.totalLayerBounds.min.x, map.totalLayerBounds.max.y) - ]; - - let totalBounds = projectedExtent(totalBoundsArray, { - color: '#808080', - weight: 5, - opacity: 0.5, - fill: false - }); - this.addLayer(totalBounds); - } + if (map.totalLayerBounds) { + let totalBoundsArray = [ + map.totalLayerBounds.min, + L.point(map.totalLayerBounds.max.x, map.totalLayerBounds.min.y), + map.totalLayerBounds.max, + L.point(map.totalLayerBounds.min.x, map.totalLayerBounds.max.y) + ]; + + let totalBounds = projectedExtent(totalBoundsArray, { + className: 'mapml-debug-vectors mapml-total-bounds', + color: '#808080', + weight: 5, + opacity: 0.5, + fill: false + }); + this.addLayer(totalBounds); + } + }, 0); }, _mapLayerUpdate: function (e) { diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index f37d90942..1fc30d0a9 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -1,5 +1,3 @@ -import { FALLBACK_CS, FALLBACK_PROJECTION } from '../utils/Constants'; - export var FeatureLayer = L.FeatureGroup.extend({ /* * M.MapML turns any MapML feature data into a Leaflet layer. Based on L.GeoJSON. diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 0287021c6..809b76f62 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1,7 +1,3 @@ -/* global M */ - -import { FALLBACK_PROJECTION, BLANK_TT_TREF } from '../utils/Constants'; - export var MapMLLayer = L.Layer.extend({ // zIndex has to be set, for the case where the layer is added to the // map before the layercontrol is used to control it (where autoZindex is used) @@ -53,7 +49,6 @@ export var MapMLLayer = L.Layer.extend({ // established by metadata in the content, we should use map properties // to set the extent, but the map won't be available until the // element is attached to the element, wait for that to happen. - this.on('attached', this._validateExtent, this); // weirdness. options is actually undefined here, despite the hardcoded // options above. If you use this.options, you see the options defined // above. Not going to change this, but failing to understand ATM. @@ -78,56 +73,11 @@ export var MapMLLayer = L.Layer.extend({ this._container.style.zIndex = this.options.zIndex; } }, - // remove all the extents before removing the layer from the map - _removeExtents: function (map) { - if (this._properties._mapExtents) { - for (let i = 0; i < this._properties._mapExtents.length; i++) { - if (this._properties._mapExtents[i].templatedLayer) { - map.removeLayer(this._properties._mapExtents[i].templatedLayer); - } - } - } - if (this._properties._queries) { - delete this._properties._queries; - } - }, - _changeOpacity: function (e) { - if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { - this.changeOpacity(e.target.value); - } - }, changeOpacity: function (opacity) { this._container.style.opacity = opacity; this._layerEl._opacity = opacity; - if (this.opacityEl) this.opacityEl.value = opacity; - }, - _changeExtentOpacity: function (e) { - if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { - this.templatedLayer.changeOpacity(e.target.value); - this._templateVars.opacity = e.target.value; - } - }, - _changeExtent: function (e, extentEl) { - if (e.target.checked) { - extentEl.checked = true; - if (this._layerEl.checked) { - extentEl.templatedLayer = M.templatedLayer(extentEl._templateVars, { - pane: this._container, - opacity: extentEl._templateVars.opacity, - _leafletLayer: this, - crs: extentEl.crs, - extentZIndex: extentEl.extentZIndex, - extentEl: extentEl._DOMnode || extentEl - }).addTo(this._map); - extentEl.templatedLayer.setZIndex(); - this._setLayerElExtent(); - } - } else { - L.DomEvent.stopPropagation(e); - extentEl.checked = false; - if (this._layerEl.checked) this._map.removeLayer(extentEl.templatedLayer); - this._setLayerElExtent(); - } + if (this._layerEl._opacitySlider) + this._layerEl._opacitySlider.value = opacity; }, titleIsReadOnly() { return !!this._titleIsReadOnly; @@ -138,8 +88,9 @@ export var MapMLLayer = L.Layer.extend({ // can be used if (!this.titleIsReadOnly()) { this._title = newName; - this._mapmlLayerItem.querySelector('.mapml-layer-item-name').innerHTML = - newName; + this._layerEl._layerControlHTML.querySelector( + '.mapml-layer-item-name' + ).innerHTML = newName; } }, getName() { @@ -147,6 +98,7 @@ export var MapMLLayer = L.Layer.extend({ }, onAdd: function (map) { + // probably don't need it except for layer context menu usage if (this._properties && !this._validProjection(map)) { this.validProjection = false; return; @@ -166,95 +118,13 @@ export var MapMLLayer = L.Layer.extend({ map.addLayer(this._staticTileLayer); } - const createAndAdd = createAndAddTemplatedLayers.bind(this); - // if the extent has been initialized and received, update the map, - if ( - this._properties && - this._properties._mapExtents && - this._properties._mapExtents[0]._templateVars - ) { - createAndAdd(); - } - this._setLayerElExtent(); - this.setZIndex(this.options.zIndex); this.getPane().appendChild(this._container); - setTimeout(() => { - map.fire('checkdisabled'); - }, 0); map.on('popupopen', this._attachSkipButtons, this); - - function createAndAddTemplatedLayers() { - if (this._properties && this._properties._mapExtents) { - for (let i = 0; i < this._properties._mapExtents.length; i++) { - if ( - this._properties._mapExtents[i]._templateVars && - this._properties._mapExtents[i].checked - ) { - if (!this._properties._mapExtents[i].extentZIndex) - this._properties._mapExtents[i].extentZIndex = i; - this._templatedLayer = M.templatedLayer( - this._properties._mapExtents[i]._templateVars, - { - pane: this._container, - opacity: this._properties._mapExtents[i]._templateVars.opacity, - _leafletLayer: this, - crs: this._properties.crs, - extentZIndex: this._properties._mapExtents[i].extentZIndex, - // when a migrates from a remote mapml file and attaches to the shadow of - // this._properties._mapExtents[i] refers to the in remote mapml - extentEl: - this._properties._mapExtents[i]._DOMnode || - this._properties._mapExtents[i] - } - ).addTo(map); - this._properties._mapExtents[i].templatedLayer = - this._templatedLayer; - if (this._templatedLayer._queries) { - if (!this._properties._queries) this._properties._queries = []; - this._properties._queries = this._properties._queries.concat( - this._templatedLayer._queries - ); - } - } - if (this._properties._mapExtents[i].hasAttribute('opacity')) { - let opacity = - this._properties._mapExtents[i].getAttribute('opacity'); - this._properties._mapExtents[i].templatedLayer.changeOpacity( - opacity - ); - } - } - } - } - }, - - _validProjection: function (map) { - let noLayer = false; - if (this._properties && this._properties._mapExtents) { - for (let i = 0; i < this._properties._mapExtents.length; i++) { - if (this._properties._mapExtents[i]._templateVars) { - for (let template of this._properties._mapExtents[i]._templateVars) - if ( - !template.projectionMatch && - template.projection !== map.options.projection - ) { - noLayer = true; // if there's a single template where projections don't match, set noLayer to true - break; - } - } - } - } - return !(noLayer || this.getProjection() !== map.options.projection); }, - //sets the elements .bounds property - _setLayerElExtent: function () { + _calculateBounds: function () { let bounds, - zoomMax, - zoomMin, - maxNativeZoom, - minNativeZoom, zoomBounds = { minZoom: 0, maxZoom: 0, @@ -267,140 +137,107 @@ export var MapMLLayer = L.Layer.extend({ '_mapmlvectors', '_templatedLayer' ]; + const mapExtents = this._layerEl.querySelectorAll('map-extent').length + ? this._layerEl.querySelectorAll('map-extent') + : this._layerEl.shadowRoot + ? this._layerEl.shadowRoot.querySelectorAll('map-extent') + : []; layerTypes.forEach((type) => { - if (this[type]) { - if (type === '_templatedLayer') { - for (let i = 0; i < this._properties._mapExtents.length; i++) { - for ( - let j = 0; - j < this._properties._mapExtents[i]._templateVars.length; - j++ - ) { - let inputData = M._extractInputBounds( - this._properties._mapExtents[i]._templateVars[j] - ); - this._properties._mapExtents[i]._templateVars[ - j - ].tempExtentBounds = inputData.bounds; - this._properties._mapExtents[i]._templateVars[ - j - ].extentZoomBounds = inputData.zoomBounds; - } - } - for (let i = 0; i < this._properties._mapExtents.length; i++) { - if (this._properties._mapExtents[i].checked) { - for ( - let j = 0; - j < this._properties._mapExtents[i]._templateVars.length; - j++ - ) { - if (!bounds) { - bounds = - this._properties._mapExtents[i]._templateVars[j] - .tempExtentBounds; - zoomMax = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxZoom; - zoomMin = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minZoom; - maxNativeZoom = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxNativeZoom; - minNativeZoom = - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minNativeZoom; - } else { - bounds.extend( - this._properties._mapExtents[i]._templateVars[j] - .tempExtentBounds.min - ); - bounds.extend( - this._properties._mapExtents[i]._templateVars[j] - .tempExtentBounds.max - ); - zoomMax = Math.max( - zoomMax, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxZoom - ); - zoomMin = Math.min( - zoomMin, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minZoom - ); - maxNativeZoom = Math.max( - maxNativeZoom, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.maxNativeZoom - ); - minNativeZoom = Math.min( - minNativeZoom, - this._properties._mapExtents[i]._templateVars[j] - .extentZoomBounds.minNativeZoom - ); - } - } - } - } - zoomBounds.minZoom = zoomMin; - zoomBounds.maxZoom = zoomMax; - zoomBounds.minNativeZoom = minNativeZoom; - zoomBounds.maxNativeZoom = maxNativeZoom; - this._properties.zoomBounds = zoomBounds; - this._properties.layerBounds = bounds; - // assign each template the layer and zoom bounds - for (let i = 0; i < this._properties._mapExtents.length; i++) { - this._properties._mapExtents[i].templatedLayer.layerBounds = bounds; - this._properties._mapExtents[i].templatedLayer.zoomBounds = - zoomBounds; - } - } else if (type === '_staticTileLayer') { - if (this[type].layerBounds) { + if (type === '_templatedLayer' && mapExtents.length) { + let zoomMax = zoomBounds.maxZoom, + zoomMin = zoomBounds.minZoom, + maxNativeZoom = zoomBounds.maxNativeZoom, + minNativeZoom = zoomBounds.minNativeZoom; + for (let i = 0; i < mapExtents.length; i++) { + if (mapExtents[i]._templatedLayer.bounds) { + let templatedLayer = mapExtents[i]._templatedLayer; if (!bounds) { - bounds = this[type].layerBounds; - zoomBounds = this[type].zoomBounds; + bounds = templatedLayer.bounds; + zoomBounds = templatedLayer.zoomBounds; } else { - bounds.extend(this[type].layerBounds.min); - bounds.extend(this[type].layerBounds.max); + bounds.extend(templatedLayer.bounds.min); + bounds.extend(templatedLayer.bounds.max); + zoomMax = Math.max(zoomMax, templatedLayer.zoomBounds.maxZoom); + zoomMin = Math.min(zoomMin, templatedLayer.zoomBounds.minZoom); + maxNativeZoom = Math.max( + maxNativeZoom, + templatedLayer.zoomBounds.maxNativeZoom + ); + minNativeZoom = Math.min( + minNativeZoom, + templatedLayer.zoomBounds.minNativeZoom + ); + zoomBounds.minZoom = zoomMin; + zoomBounds.maxZoom = zoomMax; + zoomBounds.minNativeZoom = minNativeZoom; + zoomBounds.maxNativeZoom = maxNativeZoom; } } - } else if (type === '_imageLayer') { - if (this[type].layerBounds) { - if (!bounds) { - bounds = this[type].layerBounds; - zoomBounds = this[type].zoomBounds; - } else { - bounds.extend(this[type].layerBounds.min); - bounds.extend(this[type].layerBounds.max); - } + } + } else if (type === '_staticTileLayer' && this._staticTileLayer) { + if (this[type].layerBounds) { + if (!bounds) { + bounds = this[type].layerBounds; + zoomBounds = this[type].zoomBounds; + } else { + bounds.extend(this[type].layerBounds.min); + bounds.extend(this[type].layerBounds.max); } - } else if (type === '_mapmlvectors') { - if (this[type].layerBounds) { - if (!bounds) { - bounds = this[type].layerBounds; - zoomBounds = this[type].zoomBounds; - } else { - bounds.extend(this[type].layerBounds.min); - bounds.extend(this[type].layerBounds.max); - } + } + } else if (type === '_imageLayer' && this._imageLayer) { + if (this[type].layerBounds) { + if (!bounds) { + bounds = this[type].layerBounds; + zoomBounds = this[type].zoomBounds; + } else { + bounds.extend(this[type].layerBounds.min); + bounds.extend(this[type].layerBounds.max); + } + } + } else if ( + // only process extent if mapmlvectors is not empty + type === '_mapmlvectors' && + this._mapmlvectors && + Object.keys(this[type]._layers).length !== 0 + ) { + if (this[type].layerBounds) { + if (!bounds) { + bounds = this[type].layerBounds; + zoomBounds = this[type].zoomBounds; + } else { + bounds.extend(this[type].layerBounds.min); + bounds.extend(this[type].layerBounds.max); } } } }); if (bounds) { //assigns the formatted extent object to .extent and spreads the zoom ranges to .extent also - this._layerEl.extent = Object.assign( - M._convertAndFormatPCRS( - bounds, - this._properties.crs, - this._properties.projection - ), - { zoom: zoomBounds } - ); + this.bounds = bounds; + this.zoomBounds = zoomBounds; } }, + _validProjection: function (map) { + const mapExtents = this._layerEl.querySelectorAll('map-extent'); + let noLayer = false; + if (this._properties && mapExtents.length > 0) { + for (let i = 0; i < mapExtents.length; i++) { + if (mapExtents[i]._templateVars) { + for (let template of mapExtents[i]._templateVars) + if ( + !template.projectionMatch && + template.projection !== map.options.projection + ) { + noLayer = true; // if there's a single template where projections don't match, set noLayer to true + break; + } + } + } + } + return !(noLayer || this.getProjection() !== map.options.projection); + }, + addTo: function (map) { map.addLayer(this); return this; @@ -408,16 +245,6 @@ export var MapMLLayer = L.Layer.extend({ getEvents: function () { return { zoomanim: this._onZoomAnim }; }, - redraw: function () { - // for now, only redraw templated layers. - if (this._properties._mapExtents) { - for (let i = 0; i < this._properties._mapExtents.length; i++) { - if (this._properties._mapExtents[i].templatedLayer) { - this._properties._mapExtents[i].templatedLayer.redraw(); - } - } - } - }, _onZoomAnim: function (e) { // this callback will be invoked AFTER has been removed // but due to the characteristic of JavaScript, the context (this pointer) can still be used @@ -426,12 +253,15 @@ export var MapMLLayer = L.Layer.extend({ return; } // get the min and max zooms from all extents + const layerEl = this._layerEl, + // prerequisite: no inline and remote mapml elements exists at the same time + mapExtents = layerEl.shadowRoot + ? layerEl.shadowRoot.querySelectorAll('map-extent') + : layerEl.querySelectorAll('map-extent'); var toZoom = e.zoom, zoom = - this._properties && this._properties._mapExtents - ? this._properties._mapExtents[0].querySelector( - 'map-input[type=zoom]' - ) + mapExtents.length > 0 + ? mapExtents[0].querySelector('map-input[type=zoom]') : null, min = zoom && zoom.hasAttribute('min') @@ -442,10 +272,8 @@ export var MapMLLayer = L.Layer.extend({ ? parseInt(zoom.getAttribute('max')) : this._map.getMaxZoom(); if (zoom) { - for (let i = 1; i < this._properties._mapExtents.length; i++) { - zoom = this._properties._mapExtents[i].querySelector( - 'map-input[type=zoom]' - ); + for (let i = 1; i < mapExtents.length; i++) { + zoom = mapExtents[i].querySelector('map-input[type=zoom]'); if (zoom && zoom.hasAttribute('min')) { min = Math.min(parseInt(zoom.getAttribute('min')), min); } @@ -474,369 +302,31 @@ export var MapMLLayer = L.Layer.extend({ this._layerEl.src = this._properties.zoomout; } } - if (this._templatedLayer && canZoom) { - // get the new extent - //this._initExtent(); - } }, onRemove: function (map) { L.DomUtil.remove(this._container); if (this._staticTileLayer) map.removeLayer(this._staticTileLayer); if (this._mapmlvectors) map.removeLayer(this._mapmlvectors); if (this._imageLayer) map.removeLayer(this._imageLayer); - if (this._properties && this._properties._mapExtents) - this._removeExtents(map); - - map.fire('checkdisabled'); map.off('popupopen', this._attachSkipButtons); }, getAttribution: function () { return this.options.attribution; }, - getLayerUserControlsHTML: function () { - return this._mapmlLayerItem - ? this._mapmlLayerItem - : this._createLayerControlHTML(); + getBase: function () { + return new URL( + this._content.querySelector('map-base') + ? this._content.querySelector('map-base').getAttribute('href') + : this._content.nodeName === 'LAYER-' + ? this._content.baseURI + : this._href, + this._href + ).href; }, - _createLayerControlHTML: function () { - if (!this._mapmlLayerItem) { - var fieldset = L.DomUtil.create('fieldset', 'mapml-layer-item'), - input = L.DomUtil.create('input'), - layerItemName = L.DomUtil.create('span', 'mapml-layer-item-name'), - settingsButtonNameIcon = L.DomUtil.create('span'), - layerItemProperty = L.DomUtil.create( - 'div', - 'mapml-layer-item-properties', - fieldset - ), - layerItemSettings = L.DomUtil.create( - 'div', - 'mapml-layer-item-settings', - fieldset - ), - itemToggleLabel = L.DomUtil.create( - 'label', - 'mapml-layer-item-toggle', - layerItemProperty - ), - layerItemControls = L.DomUtil.create( - 'div', - 'mapml-layer-item-controls', - layerItemProperty - ), - opacityControl = L.DomUtil.create( - 'details', - 'mapml-layer-item-opacity mapml-control-layers', - layerItemSettings - ), - opacity = L.DomUtil.create('input'), - opacityControlSummary = L.DomUtil.create('summary'), - svgSettingsControlIcon = L.SVG.create('svg'), - settingsControlPath1 = L.SVG.create('path'), - settingsControlPath2 = L.SVG.create('path'), - extentsFieldset = L.DomUtil.create( - 'fieldset', - 'mapml-layer-grouped-extents' - ), - mapEl = this._layerEl.parentNode; - this.opacityEl = opacity; - this._mapmlLayerItem = fieldset; - - // append the paths in svg for the remove layer and toggle icons - svgSettingsControlIcon.setAttribute('viewBox', '0 0 24 24'); - svgSettingsControlIcon.setAttribute('height', '22'); - svgSettingsControlIcon.setAttribute('width', '22'); - svgSettingsControlIcon.setAttribute('fill', 'currentColor'); - settingsControlPath1.setAttribute('d', 'M0 0h24v24H0z'); - settingsControlPath1.setAttribute('fill', 'none'); - settingsControlPath2.setAttribute( - 'd', - 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' - ); - svgSettingsControlIcon.appendChild(settingsControlPath1); - svgSettingsControlIcon.appendChild(settingsControlPath2); - - layerItemSettings.hidden = true; - settingsButtonNameIcon.setAttribute('aria-hidden', true); - - let removeControlButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-remove-control', - layerItemControls - ); - removeControlButton.type = 'button'; - removeControlButton.title = 'Remove Layer'; - removeControlButton.innerHTML = - ""; - removeControlButton.classList.add('mapml-button'); - //L.DomEvent.disableClickPropagation(removeControlButton); - L.DomEvent.on(removeControlButton, 'click', L.DomEvent.stop); - L.DomEvent.on( - removeControlButton, - 'click', - (e) => { - let fieldset = 0, - elem, - root; - root = - mapEl.tagName === 'MAPML-VIEWER' - ? mapEl.shadowRoot - : mapEl.querySelector('.mapml-web-map').shadowRoot; - if ( - e.target.closest('fieldset').nextElementSibling && - !e.target.closest('fieldset').nextElementSibling.disbaled - ) { - elem = e.target.closest('fieldset').previousElementSibling; - while (elem) { - fieldset += 2; // find the next layer menu item - elem = elem.previousElementSibling; - } - } else { - // focus on the link - elem = 'link'; - } - mapEl.removeChild( - e.target.closest('fieldset').querySelector('span').layer._layerEl - ); - elem = elem - ? root.querySelector('.leaflet-control-attribution') - .firstElementChild - : (elem = root.querySelectorAll('input')[fieldset]); - elem.focus(); - }, - this - ); - - let itemSettingControlButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-settings-control', - layerItemControls - ); - itemSettingControlButton.type = 'button'; - itemSettingControlButton.title = 'Layer Settings'; - itemSettingControlButton.setAttribute('aria-expanded', false); - itemSettingControlButton.classList.add('mapml-button'); - L.DomEvent.on( - itemSettingControlButton, - 'click', - (e) => { - let layerControl = this._layerEl._layerControl._container; - if (!layerControl._isExpanded && e.pointerType === 'touch') { - layerControl._isExpanded = true; - return; - } - if (layerItemSettings.hidden === true) { - itemSettingControlButton.setAttribute('aria-expanded', true); - layerItemSettings.hidden = false; - } else { - itemSettingControlButton.setAttribute('aria-expanded', false); - layerItemSettings.hidden = true; - } - }, - this - ); - - input.defaultChecked = this._map ? true : false; - input.type = 'checkbox'; - input.setAttribute('class', 'leaflet-control-layers-selector'); - layerItemName.layer = this; - - if (this._legendUrl) { - var legendLink = document.createElement('a'); - legendLink.text = ' ' + this._title; - legendLink.href = this._legendUrl; - legendLink.target = '_blank'; - legendLink.draggable = false; - layerItemName.appendChild(legendLink); - } else { - layerItemName.innerHTML = this._title; - } - layerItemName.id = - 'mapml-layer-item-name-{' + L.stamp(layerItemName) + '}'; - opacityControlSummary.innerText = 'Opacity'; - opacityControlSummary.id = - 'mapml-layer-item-opacity-' + L.stamp(opacityControlSummary); - opacityControl.appendChild(opacityControlSummary); - opacityControl.appendChild(opacity); - opacity.setAttribute('type', 'range'); - opacity.setAttribute('min', '0'); - opacity.setAttribute('max', '1.0'); - opacity.setAttribute('value', this._container.style.opacity || '1.0'); - opacity.setAttribute('step', '0.1'); - opacity.setAttribute('aria-labelledby', opacityControlSummary.id); - opacity.value = this._container.style.opacity || '1.0'; - - fieldset.setAttribute('aria-grabbed', 'false'); - fieldset.setAttribute('aria-labelledby', layerItemName.id); - - fieldset.ontouchstart = fieldset.onmousedown = (downEvent) => { - if ( - (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && - downEvent.target.tagName.toLowerCase() !== 'input') || - downEvent.target.tagName.toLowerCase() === 'label' - ) { - downEvent = - downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent; - let control = fieldset, - controls = fieldset.parentNode, - moving = false, - yPos = downEvent.clientY; - - document.body.ontouchmove = document.body.onmousemove = ( - moveEvent - ) => { - moveEvent.preventDefault(); - moveEvent = - moveEvent instanceof TouchEvent - ? moveEvent.touches[0] - : moveEvent; - - // Fixes flickering by only moving element when there is enough space - let offset = moveEvent.clientY - yPos; - moving = Math.abs(offset) > 5 || moving; - if ( - (controls && !moving) || - (controls && controls.childElementCount <= 1) || - controls.getBoundingClientRect().top > - control.getBoundingClientRect().bottom || - controls.getBoundingClientRect().bottom < - control.getBoundingClientRect().top - ) { - return; - } - - controls.classList.add('mapml-draggable'); - control.style.transform = 'translateY(' + offset + 'px)'; - control.style.pointerEvents = 'none'; - - let x = moveEvent.clientX, - y = moveEvent.clientY, - root = - mapEl.tagName === 'MAPML-VIEWER' - ? mapEl.shadowRoot - : mapEl.querySelector('.mapml-web-map').shadowRoot, - elementAt = root.elementFromPoint(x, y), - swapControl = - !elementAt || !elementAt.closest('fieldset') - ? control - : elementAt.closest('fieldset'); - - swapControl = - Math.abs(offset) <= swapControl.offsetHeight - ? control - : swapControl; - - control.setAttribute('aria-grabbed', 'true'); - control.setAttribute('aria-dropeffect', 'move'); - if (swapControl && controls === swapControl.parentNode) { - swapControl = - swapControl !== control.nextSibling - ? swapControl - : swapControl.nextSibling; - if (control !== swapControl) { - yPos = moveEvent.clientY; - control.style.transform = null; - } - controls.insertBefore(control, swapControl); - } - }; - - document.body.ontouchend = document.body.onmouseup = () => { - control.setAttribute('aria-grabbed', 'false'); - control.removeAttribute('aria-dropeffect'); - control.style.pointerEvents = null; - control.style.transform = null; - let controlsElems = controls.children, - zIndex = 1; - // re-order layer elements DOM order - for (let c of controlsElems) { - let layerEl = c.querySelector('span').layer._layerEl; - layerEl.setAttribute('data-moving', ''); - mapEl.insertAdjacentElement('beforeend', layerEl); - layerEl.removeAttribute('data-moving'); - } - // update zIndex of all layer- elements - let layers = mapEl.querySelectorAll('layer-'); - for (let i = 0; i < layers.length; i++) { - let layer = layers[i]._layer; - if (layer.options.zIndex !== zIndex) { - layer.setZIndex(zIndex); - } - zIndex++; - } - controls.classList.remove('mapml-draggable'); - document.body.ontouchmove = - document.body.onmousemove = - document.body.onmouseup = - null; - }; - } - }; - - L.DomEvent.on(opacity, 'change', this._changeOpacity, this); - - itemToggleLabel.appendChild(input); - itemToggleLabel.appendChild(layerItemName); - itemSettingControlButton.appendChild(settingsButtonNameIcon); - settingsButtonNameIcon.appendChild(svgSettingsControlIcon); - - if (this._styles) { - layerItemSettings.appendChild(this._styles); - } - - if (this._userInputs) { - var frag = document.createDocumentFragment(); - var templates = this._properties._templateVars; - if (templates) { - for (var i = 0; i < templates.length; i++) { - var template = templates[i]; - for (var j = 0; j < template.values.length; j++) { - var mapmlInput = template.values[j], - id = '#' + mapmlInput.getAttribute('id'); - // don't add it again if it is referenced > once - if ( - mapmlInput.tagName.toLowerCase() === 'map-select' && - !frag.querySelector(id) - ) { - // generate a
- var selectdetails = L.DomUtil.create( - 'details', - 'mapml-layer-item-time mapml-control-layers', - frag - ), - selectsummary = L.DomUtil.create('summary'), - selectSummaryLabel = L.DomUtil.create('label'); - selectSummaryLabel.innerText = mapmlInput.getAttribute('name'); - selectSummaryLabel.setAttribute( - 'for', - mapmlInput.getAttribute('id') - ); - selectsummary.appendChild(selectSummaryLabel); - selectdetails.appendChild(selectsummary); - selectdetails.appendChild(mapmlInput.htmlselect); - } - } - } - } - layerItemSettings.appendChild(frag); - } - - // if there are extents, add them to the layer control - if (this._properties && this._properties._mapExtents) { - var allHidden = true; - this._layerItemSettingsHTML = layerItemSettings; - this._propertiesGroupAnatomy = extentsFieldset; - extentsFieldset.setAttribute('aria-label', 'Sublayers'); - for (let j = 0; j < this._properties._mapExtents.length; j++) { - extentsFieldset.appendChild( - this._properties._mapExtents[j].extentAnatomy - ); - if (!this._properties._mapExtents[j].hidden) allHidden = false; - } - if (!allHidden) layerItemSettings.appendChild(extentsFieldset); - } - } - return this._mapmlLayerItem; + addExtentToLayerControl: function (contents) { + this._layerEl._propertiesGroupAnatomy.appendChild(contents); + // remove hidden attribute, if it exists + this._layerEl._propertiesGroupAnatomy.removeAttribute('hidden'); }, _initialize: function (content) { if (!this._href && !content) { @@ -849,21 +339,12 @@ export var MapMLLayer = L.Layer.extend({ // referred to by this._content), we should use that content. _processContent.call(this, content, this._href ? false : true); function _processContent(mapml, local) { - var base = new URL( - mapml.querySelector('map-base') - ? mapml.querySelector('map-base').getAttribute('href') - : local - ? mapml.baseURI - : layer._href, - layer._href - ).href; + var base = layer.getBase(); layer._properties = {}; // sets layer._properties.projection determineLayerProjection(); // requires that layer._properties.projection be set if (selectMatchingAlternateProjection()) return; - // set layer._properties._mapExtents and layer._properties._templateVars - if (layer._properties.crs) processExtents(); layer._styles = getAlternateStyles(); parseLicenseAndLegend(); setLayerTitle(); @@ -872,7 +353,6 @@ export var MapMLLayer = L.Layer.extend({ if (layer._properties.crs) processTiles(); processFeatures(); M._parseStylesheetAsHTML(mapml, base, layer._container); - layer._validateExtent(); copyRemoteContentToShadowRoot(); // update controls if needed based on mapml-viewer controls/controlslist attribute if (layer._layerEl.parentElement) { @@ -913,9 +393,7 @@ export var MapMLLayer = L.Layer.extend({ ); } layer._properties.projection = projection; - if (layer._properties.projection === layer.options.mapprojection) { - layer._properties.crs = M[layer._properties.projection]; - } + layer._properties.crs = M[layer._properties.projection]; } // determine if, where there's no match of the current layer's projection // and that of the map, if there is a linked alternate text/mapml @@ -953,528 +431,6 @@ export var MapMLLayer = L.Layer.extend({ } catch (error) {} return false; } - // initialize layer._properties._mapExtents (and associated/derived/convenience property _templateVars - function processExtents() { - let projectionMatch = - layer._properties.projection === layer.options.mapprojection; - let extents = mapml.querySelectorAll('map-extent[units]'); - if (extents.length === 0) { - return; - } - layer._properties._mapExtents = []; // stores all the map-extent elements in the layer - layer._properties._templateVars = []; // stores all template variables coming from all extents - for (let j = 0; j < extents.length; j++) { - if ( - extents[j].querySelector( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ) - ) { - extents[j]._templateVars = _initTemplateVars.call( - layer, - extents[j], - mapml.querySelector('map-meta[name=extent]'), - layer._properties.projection, - mapml, - base, - projectionMatch - ); - extents[j].extentAnatomy = createLayerControlExtentHTML.call( - layer, - extents[j] - ); - layer._properties._mapExtents.push(extents[j]); - // get rid of layer._properties._templateVars, TBD. - layer._properties._templateVars = - layer._properties._templateVars.concat(extents[j]._templateVars); - } - } - } - function createLayerControlExtentHTML(mapExtent) { - var extent = L.DomUtil.create('fieldset', 'mapml-layer-extent'), - extentProperties = L.DomUtil.create( - 'div', - 'mapml-layer-item-properties', - extent - ), - extentSettings = L.DomUtil.create( - 'div', - 'mapml-layer-item-settings', - extent - ), - extentLabel = L.DomUtil.create( - 'label', - 'mapml-layer-item-toggle', - extentProperties - ), - input = L.DomUtil.create('input'), - svgExtentControlIcon = L.SVG.create('svg'), - extentControlPath1 = L.SVG.create('path'), - extentControlPath2 = L.SVG.create('path'), - extentNameIcon = L.DomUtil.create('span'), - extentItemControls = L.DomUtil.create( - 'div', - 'mapml-layer-item-controls', - extentProperties - ), - opacityControl = L.DomUtil.create( - 'details', - 'mapml-layer-item-opacity', - extentSettings - ), - extentOpacitySummary = L.DomUtil.create( - 'summary', - '', - opacityControl - ), - mapEl = this._layerEl.parentNode, - layerEl = this._layerEl, - opacity = L.DomUtil.create('input', '', opacityControl); - extentSettings.hidden = true; - extent.setAttribute('aria-grabbed', 'false'); - if (!mapExtent.hasAttribute('label')) { - // if a label attribute is not present, set it to hidden in layer control - extent.setAttribute('hidden', ''); - mapExtent.hidden = true; - } - - // append the svg paths - svgExtentControlIcon.setAttribute('viewBox', '0 0 24 24'); - svgExtentControlIcon.setAttribute('height', '22'); - svgExtentControlIcon.setAttribute('width', '22'); - extentControlPath1.setAttribute('d', 'M0 0h24v24H0z'); - extentControlPath1.setAttribute('fill', 'none'); - extentControlPath2.setAttribute( - 'd', - 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' - ); - svgExtentControlIcon.appendChild(extentControlPath1); - svgExtentControlIcon.appendChild(extentControlPath2); - - let removeExtentButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-remove-control', - extentItemControls - ); - removeExtentButton.type = 'button'; - removeExtentButton.title = 'Remove Sub Layer'; - removeExtentButton.innerHTML = - ""; - removeExtentButton.classList.add('mapml-button'); - L.DomEvent.on(removeExtentButton, 'click', L.DomEvent.stop); - L.DomEvent.on( - removeExtentButton, - 'click', - (e) => { - let allRemoved = true; - e.target.checked = false; - mapExtent.removed = true; - mapExtent.checked = false; - if (this._layerEl.checked) this._changeExtent(e, mapExtent); - mapExtent.extentAnatomy.parentNode.removeChild( - mapExtent.extentAnatomy - ); - for (let j = 0; j < this._properties._mapExtents.length; j++) { - if (!this._properties._mapExtents[j].removed) allRemoved = false; - } - if (allRemoved) - this._layerItemSettingsHTML.removeChild( - this._propertiesGroupAnatomy - ); - }, - this - ); - - let extentsettingsButton = L.DomUtil.create( - 'button', - 'mapml-layer-item-settings-control', - extentItemControls - ); - extentsettingsButton.type = 'button'; - extentsettingsButton.title = 'Extent Settings'; - extentsettingsButton.setAttribute('aria-expanded', false); - extentsettingsButton.classList.add('mapml-button'); - L.DomEvent.on( - extentsettingsButton, - 'click', - (e) => { - if (extentSettings.hidden === true) { - extentsettingsButton.setAttribute('aria-expanded', true); - extentSettings.hidden = false; - } else { - extentsettingsButton.setAttribute('aria-expanded', false); - extentSettings.hidden = true; - } - }, - this - ); - - extentNameIcon.setAttribute('aria-hidden', true); - extentLabel.appendChild(input); - extentsettingsButton.appendChild(extentNameIcon); - extentNameIcon.appendChild(svgExtentControlIcon); - extentOpacitySummary.innerText = 'Opacity'; - extentOpacitySummary.id = - 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary); - opacity.setAttribute('type', 'range'); - opacity.setAttribute('min', '0'); - opacity.setAttribute('max', '1.0'); - opacity.setAttribute('step', '0.1'); - opacity.setAttribute( - 'aria-labelledby', - 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary) - ); - let opacityValue = mapExtent.hasAttribute('opacity') - ? mapExtent.getAttribute('opacity') - : '1.0'; - mapExtent._templateVars.opacity = opacityValue; - opacity.setAttribute('value', opacityValue); - opacity.value = opacityValue; - L.DomEvent.on(opacity, 'change', this._changeExtentOpacity, mapExtent); - - var extentItemNameSpan = L.DomUtil.create( - 'span', - 'mapml-layer-item-name', - extentLabel - ); - input.defaultChecked = mapExtent ? true : false; - mapExtent.checked = input.defaultChecked; - input.type = 'checkbox'; - extentItemNameSpan.innerHTML = mapExtent.getAttribute('label'); - L.DomEvent.on(input, 'change', (e) => { - this._changeExtent(e, mapExtent); - }); - extentItemNameSpan.id = - 'mapml-extent-item-name-{' + L.stamp(extentItemNameSpan) + '}'; - extent.setAttribute('aria-labelledby', extentItemNameSpan.id); - extentItemNameSpan.extent = mapExtent; - - extent.ontouchstart = extent.onmousedown = (downEvent) => { - if ( - (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && - downEvent.target.tagName.toLowerCase() !== 'input') || - downEvent.target.tagName.toLowerCase() === 'label' - ) { - downEvent.stopPropagation(); - downEvent = - downEvent instanceof TouchEvent - ? downEvent.touches[0] - : downEvent; - - let control = extent, - controls = extent.parentNode, - moving = false, - yPos = downEvent.clientY; - - document.body.ontouchmove = document.body.onmousemove = ( - moveEvent - ) => { - moveEvent.preventDefault(); - moveEvent = - moveEvent instanceof TouchEvent - ? moveEvent.touches[0] - : moveEvent; - - // Fixes flickering by only moving element when there is enough space - let offset = moveEvent.clientY - yPos; - moving = Math.abs(offset) > 5 || moving; - if ( - (controls && !moving) || - (controls && controls.childElementCount <= 1) || - controls.getBoundingClientRect().top > - control.getBoundingClientRect().bottom || - controls.getBoundingClientRect().bottom < - control.getBoundingClientRect().top - ) { - return; - } - - controls.classList.add('mapml-draggable'); - control.style.transform = 'translateY(' + offset + 'px)'; - control.style.pointerEvents = 'none'; - - let x = moveEvent.clientX, - y = moveEvent.clientY, - root = - mapEl.tagName === 'MAPML-VIEWER' - ? mapEl.shadowRoot - : mapEl.querySelector('.mapml-web-map').shadowRoot, - elementAt = root.elementFromPoint(x, y), - swapControl = - !elementAt || !elementAt.closest('fieldset') - ? control - : elementAt.closest('fieldset'); - - swapControl = - Math.abs(offset) <= swapControl.offsetHeight - ? control - : swapControl; - - control.setAttribute('aria-grabbed', 'true'); - control.setAttribute('aria-dropeffect', 'move'); - if (swapControl && controls === swapControl.parentNode) { - swapControl = - swapControl !== control.nextSibling - ? swapControl - : swapControl.nextSibling; - if (control !== swapControl) { - yPos = moveEvent.clientY; - control.style.transform = null; - } - controls.insertBefore(control, swapControl); - } - }; - - document.body.ontouchend = document.body.onmouseup = () => { - control.setAttribute('aria-grabbed', 'false'); - control.removeAttribute('aria-dropeffect'); - control.style.pointerEvents = null; - control.style.transform = null; - let controlsElems = controls.children, - zIndex = 0; - for (let c of controlsElems) { - let extentEl = c.querySelector('span').extent; - - extentEl.setAttribute('data-moving', ''); - layerEl.insertAdjacentElement('beforeend', extentEl); - extentEl.removeAttribute('data-moving'); - - extentEl.extentZIndex = zIndex; - extentEl.templatedLayer.setZIndex(zIndex); - zIndex++; - } - controls.classList.remove('mapml-draggable'); - document.body.ontouchmove = - document.body.onmousemove = - document.body.ontouchend = - document.body.onmouseup = - null; - }; - } - }; - return extent; - } - function _initTemplateVars( - serverExtent, - metaExtent, - projection, - mapml, - base, - projectionMatch - ) { - var templateVars = []; - // set up the URL template and associated inputs (which yield variable values when processed) - var tlist = serverExtent.querySelectorAll( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ), - varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), - zoomInput = serverExtent.querySelector('map-input[type="zoom" i]'), - includesZoom = false, - extentFallback = {}; - - extentFallback.zoom = 0; - if (metaExtent) { - let content = M._metaContentToObject( - metaExtent.getAttribute('content') - ), - cs; - - extentFallback.zoom = content.zoom || extentFallback.zoom; - - let metaKeys = Object.keys(content); - for (let i = 0; i < metaKeys.length; i++) { - if (!metaKeys[i].includes('zoom')) { - cs = M.axisToCS(metaKeys[i].split('-')[2]); - break; - } - } - let axes = M.csToAxes(cs); - extentFallback.bounds = M.boundsToPCRSBounds( - L.bounds( - L.point( - +content[`top-left-${axes[0]}`], - +content[`top-left-${axes[1]}`] - ), - L.point( - +content[`bottom-right-${axes[0]}`], - +content[`bottom-right-${axes[1]}`] - ) - ), - extentFallback.zoom, - projection, - cs - ); - } else { - // for custom projections, M[projection] may not be loaded, so uses M['OSMTILE'] as backup, this code will need to get rerun once projection is changed and M[projection] is available - // TODO: This is a temporary fix, _initTemplateVars (or processinitialextent) should not be called when projection of the layer and map do not match, this should be called/reinitialized once the layer projection matches with the map projection - let fallbackProjection = M[projection] || M.OSMTILE; - extentFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; - } - - for (var i = 0; i < tlist.length; i++) { - var t = tlist[i], - template = t.getAttribute('tref'); - t.zoomInput = zoomInput; - if (!template) { - template = BLANK_TT_TREF; - let blankInputs = mapml.querySelectorAll('map-input'); - for (let i of blankInputs) { - template += `{${i.getAttribute('name')}}`; - } - } - - var v, - title = t.hasAttribute('title') - ? t.getAttribute('title') - : 'Query this layer', - vcount = template.match(varNamesRe), - trel = - !t.hasAttribute('rel') || - t.getAttribute('rel').toLowerCase() === 'tile' - ? 'tile' - : t.getAttribute('rel').toLowerCase(), - ttype = !t.hasAttribute('type') - ? 'image/*' - : t.getAttribute('type').toLowerCase(), - inputs = [], - tms = t && t.hasAttribute('tms'); - var zoomBounds = mapml.querySelector('map-meta[name=zoom]') - ? M._metaContentToObject( - mapml - .querySelector('map-meta[name=zoom]') - .getAttribute('content') - ) - : undefined; - while ((v = varNamesRe.exec(template)) !== null) { - var varName = v[1], - inp = serverExtent.querySelector( - 'map-input[name=' + - varName + - '],map-select[name=' + - varName + - ']' - ); - if (inp) { - if ( - inp.hasAttribute('type') && - inp.getAttribute('type') === 'location' && - (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && - inp.hasAttribute('axis') && - !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) - ) { - if ( - zoomInput && - template.includes(`{${zoomInput.getAttribute('name')}}`) - ) { - zoomInput.setAttribute('value', extentFallback.zoom); - } - let axis = inp.getAttribute('axis'), - axisBounds = M.convertPCRSBounds( - extentFallback.bounds, - extentFallback.zoom, - projection, - M.axisToCS(axis) - ); - inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); - inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); - } - - inputs.push(inp); - includesZoom = - includesZoom || - (inp.hasAttribute('type') && - inp.getAttribute('type').toLowerCase() === 'zoom'); - if (inp.tagName.toLowerCase() === 'map-select') { - // use a throwaway div to parse the input from MapML into HTML - var div = document.createElement('div'); - div.insertAdjacentHTML('afterbegin', inp.outerHTML); - // parse - inp.htmlselect = div.querySelector('map-select'); - inp.htmlselect = transcribe(inp.htmlselect); - - // this goes into the layer control, so add a listener - L.DomEvent.on(inp.htmlselect, 'change', layer.redraw, layer); - if (!layer._userInputs) { - layer._userInputs = []; - } - layer._userInputs.push(inp.htmlselect); - } - // TODO: if this is an input@type=location - // get the TCRS min,max attribute values at the identified zoom level - // save this information as properties of the serverExtent, - // perhaps as a bounds object so that it can be easily used - // later by the layer control to determine when to enable - // disable the layer for drawing. - } else { - console.log( - 'input with name=' + - varName + - ' not found for template variable of same name' - ); - // no match found, template won't be used - break; - } - } - if ( - (template && vcount.length === inputs.length) || - template === BLANK_TT_TREF - ) { - if (trel === 'query') { - layer.queryable = true; - } - if (!includesZoom && zoomInput) { - inputs.push(zoomInput); - } - let step = zoomInput ? zoomInput.getAttribute('step') : 1; - if (!step || step === '0' || isNaN(step)) step = 1; - // template has a matching input for every variable reference {varref} - templateVars.push({ - template: decodeURI(new URL(template, base)), - linkEl: t, - title: title, - rel: trel, - type: ttype, - values: inputs, - zoomBounds: zoomBounds, - extentPCRSFallback: { bounds: extentFallback.bounds }, - projectionMatch: projectionMatch, - projection: - serverExtent.getAttribute('units') || FALLBACK_PROJECTION, - tms: tms, - step: step - }); - } - } - return templateVars; - } - function transcribe(element) { - var select = document.createElement('select'); - var elementAttrNames = element.getAttributeNames(); - - for (let i = 0; i < elementAttrNames.length; i++) { - select.setAttribute( - elementAttrNames[i], - element.getAttribute(elementAttrNames[i]) - ); - } - - var options = element.children; - - for (let i = 0; i < options.length; i++) { - var option = document.createElement('option'); - var optionAttrNames = options[i].getAttributeNames(); - - for (let j = 0; j < optionAttrNames.length; j++) { - option.setAttribute( - optionAttrNames[j], - options[i].getAttribute(optionAttrNames[j]) - ); - } - - option.innerHTML = options[i].innerHTML; - select.appendChild(option); - } - return select; - } function setZoomInOrOutLinks() { var zoomin = mapml.querySelector('map-link[rel=zoomin]'), zoomout = mapml.querySelector('map-link[rel=zoomout]'); @@ -1579,7 +535,11 @@ export var MapMLLayer = L.Layer.extend({ 'id', 'rad-' + L.stamp(styleOptionInput) ); - styleOptionInput.setAttribute('name', 'styles-' + layer._title); + styleOptionInput.setAttribute( + 'name', + // grouping radio buttons based on parent layer's style + 'styles-' + L.stamp(stylesControl) + ); styleOptionInput.setAttribute( 'value', styleLinks[j].getAttribute('title') @@ -1619,9 +579,6 @@ export var MapMLLayer = L.Layer.extend({ } else if (mapml instanceof Element && mapml.hasAttribute('label')) { layer._title = mapml.getAttribute('label').trim(); } - // _mapmlLayerItem is set to the root element representing this layer - // in the layer control, iff the layer is not 'hidden' - layer._createLayerControlHTML(); } function copyRemoteContentToShadowRoot() { // only run when content is loaded from network, puts features etc @@ -1630,7 +587,12 @@ export var MapMLLayer = L.Layer.extend({ return; } let shadowRoot = layer._layerEl.shadowRoot; - let elements = mapml.children[0].children[1].children; + // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot + let headMeta = + mapml.children[0].children[0].querySelectorAll('map-meta[name]'); + // get the elements inside map-body of remote mapml + let bodyElements = mapml.children[0].children[1].children; + let elements = [...headMeta, ...bodyElements]; if (elements) { let baseURL = mapml.children[0].children[0] .querySelector('map-base') @@ -1694,44 +656,6 @@ export var MapMLLayer = L.Layer.extend({ } } }, - _validateExtent: function () { - // TODO: change so that the _extent bounds are set based on inputs - if (!this._properties || !this._map) { - return; - } - var serverExtent = this._properties._mapExtents - ? this._properties._mapExtents - : [this._properties], - lp; - - // loop through the map-extent elements and assign each one its crs - for (let i = 0; i < serverExtent.length; i++) { - if (!serverExtent[i].querySelector) { - return; - } - if ( - serverExtent[i].querySelector( - '[type=zoom][min=""], [type=zoom][max=""]' - ) - ) { - var zoom = serverExtent[i].querySelector('[type=zoom]'); - zoom.setAttribute('min', this._map.getMinZoom()); - zoom.setAttribute('max', this._map.getMaxZoom()); - } - lp = serverExtent[i].hasAttribute('units') - ? serverExtent[i].getAttribute('units') - : null; - if (lp && M[lp]) { - if (this._properties._mapExtents) - this._properties._mapExtents[i].crs = M[lp]; - else this._properties.crs = M[lp]; - } else { - if (this._properties._mapExtents) - this._properties._mapExtents[i].crs = M.OSMTILE; - else this._properties.crs = M.OSMTILE; - } - } - }, // new getProjection, maybe simpler, but doesn't work... getProjection: function () { if (!this._properties) { @@ -1740,25 +664,24 @@ export var MapMLLayer = L.Layer.extend({ return this._properties.projection; }, getQueryTemplates: function (pcrsClick) { + const mapExtents = this._layerEl.querySelectorAll('map-extent').length + ? this._layerEl.querySelectorAll('map-extent') + : this._layerEl.shadowRoot.querySelectorAll('map-extent'); if (this._properties && this._properties._queries) { var templates = []; // only return queries that are in bounds if ( this._layerEl.checked && !this._layerEl.hidden && - this._mapmlLayerItem + this._layerEl._layerControlHTML ) { - let layerAndExtents = this._mapmlLayerItem.querySelectorAll( + let layerAndExtents = this._layerEl._layerControlHTML.querySelectorAll( '.mapml-layer-item-name' ); for (let i = 0; i < layerAndExtents.length; i++) { - if ( - layerAndExtents[i].extent || - this._properties._mapExtents.length === 1 - ) { + if (layerAndExtents[i].extent || mapExtents.length === 1) { // the layer won't have an .extent property, this is kind of a hack - let extent = - layerAndExtents[i].extent || this._properties._mapExtents[0]; + let extent = layerAndExtents[i].extent || mapExtents[0]; for (let j = 0; j < extent._templateVars.length; j++) { if (extent.checked) { let template = extent._templateVars[j]; @@ -1805,7 +728,11 @@ export var MapMLLayer = L.Layer.extend({ // if the popup is for a static / templated feature, the "zoom to here" link can be attached once the popup opens attachZoomLink.call(popup); } else { - layer = popup._source._templatedLayer; + // getting access to the first map-extent to get access to _templatedLayer to use it's (possibly) generic _previousFeature + _nextFeature methods. + const mapExtent = + popup._source._layerEl.querySelector('map-extent') || + popup._source._layerEl.shadowRoot.querySelector('map-extent'); + layer = mapExtent._templatedLayer; // if the popup is for a query, the "zoom to here" link should be re-attached every time new pagination features are displayed map.on('attachZoomLink', attachZoomLink, popup); } diff --git a/src/mapml/layers/StaticTileLayer.js b/src/mapml/layers/StaticTileLayer.js index 91d66501b..fc9462ac1 100644 --- a/src/mapml/layers/StaticTileLayer.js +++ b/src/mapml/layers/StaticTileLayer.js @@ -1,11 +1,11 @@ export var StaticTileLayer = L.GridLayer.extend({ initialize: function (options) { + L.setOptions(this, options); this.zoomBounds = this._getZoomBounds( options.tileContainer, options.maxZoomBound ); - L.extend(options, this.zoomBounds); - L.setOptions(this, options); + L.extend(this.options, this.zoomBounds); this._groups = this._groupTiles( this.options.tileContainer.getElementsByTagName('map-tile') ); @@ -119,8 +119,11 @@ export var StaticTileLayer = L.GridLayer.extend({ _getZoomBounds: function (container, maxZoomBound) { if (!container) return null; + // should read zoom information from map-meta (of layer-) instead of the zoom attribute value of map-tile let meta = M._metaContentToObject( - container.getElementsByTagName('map-tiles')[0].getAttribute('zoom') + this.options._leafletLayer._layerEl + .querySelector('map-meta[name=zoom]') + .getAttribute('content') ), zoom = {}, tiles = container.getElementsByTagName('map-tile'); @@ -134,14 +137,11 @@ export var StaticTileLayer = L.GridLayer.extend({ zoom.maxNativeZoom = Math.max(zoom.maxNativeZoom, lZoom); } - //hard coded to only natively zoom out 2 levels, any more and too many tiles are going to be loaded in at one time - //lagging the users computer - zoom.minZoom = zoom.minNativeZoom - 2 <= 0 ? 0 : zoom.minNativeZoom - 2; - zoom.maxZoom = maxZoomBound; - if (meta.min) - zoom.minZoom = - +meta.min < zoom.minNativeZoom - 2 ? zoom.minNativeZoom - 2 : +meta.min; - if (meta.max) zoom.maxZoom = +meta.max; + // currently the min and max zoom bounds of staticTileLayer is set based on map-meta + // can be hard coded to only natively zoom out 2 levels, any more and too many tiles are going to be loaded in at one time + // it will avoid lagging the users computer, but it will also cause the bug that the initial zoom level of map is inproperly set + zoom.minZoom = +meta.min || 0; + zoom.maxZoom = +meta.max || maxZoomBound; return zoom; }, diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index ab4e74268..79a216d3f 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -7,7 +7,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this.isVisible = true; this._template = template; this._extentEl = options.extentEl; - this._container = L.DomUtil.create('div', 'leaflet-layer', options.pane); + this._container = L.DomUtil.create('div', 'leaflet-layer'); L.extend(options, this.zoomBounds); L.DomUtil.addClass(this._container, 'mapml-features-container'); delete options.opacity; @@ -23,6 +23,8 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ return events; }, onAdd: function () { + // this causes the layer (this._features) to actually render... + this.options.pane.appendChild(this._container); this._map._addZoomLimit(this); var opacity = this.options.opacity || 1, container = this._container, @@ -49,10 +51,16 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ geometry.bindPopup(c, { autoClose: false, minWidth: 108 }); } }); + } else { + // if this._features exists add the layer back + this._map.addLayer(this._features); } map.fire('moveend'); // TODO: replace with moveend handler for layer and not entire map }, + onRemove: function () { + this._map.removeLayer(this._features); + }, redraw: function () { this._onMoveEnd(); }, @@ -92,9 +100,6 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._map.getCenter(), steppedZoom ); - let url = this._getfeaturesUrl(steppedZoom, scaleBounds); - //No request needed if the current template url is the same as the url to request - if (url === this._url) return; let mapBounds = M.pixelToPCRSBounds( this._map.getPixelBounds(), @@ -106,6 +111,12 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ mapZoom >= this.zoomBounds.minZoom && this.extentBounds.overlaps(mapBounds); + // should set this.isVisible properly BEFORE return, otherwise will cause layer-.validateDisabled not work properly + let url = this._getfeaturesUrl(steppedZoom, scaleBounds); + // No request needed if the current template url is the same as the url to request + if (url === this._url) return; + + // do cleaning up for new request this._features.clearLayers(); // shadow may has not yet attached to for the first-time rendering if (this._extentEl.shadowRoot) { @@ -220,9 +231,6 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._container.style.zIndex = this.options.zIndex; } }, - onRemove: function () { - this._map.removeLayer(this._features); - }, _getfeaturesUrl: function (zoom, bounds) { if (zoom === undefined) zoom = this._map.getZoom(); if (bounds === undefined) bounds = this._map.getPixelBounds(); diff --git a/src/mapml/layers/TemplatedImageLayer.js b/src/mapml/layers/TemplatedImageLayer.js index e98747160..734d53cd2 100644 --- a/src/mapml/layers/TemplatedImageLayer.js +++ b/src/mapml/layers/TemplatedImageLayer.js @@ -1,7 +1,7 @@ export var TemplatedImageLayer = L.Layer.extend({ initialize: function (template, options) { this._template = template; - this._container = L.DomUtil.create('div', 'leaflet-layer', options.pane); + this._container = L.DomUtil.create('div', 'leaflet-layer'); L.DomUtil.addClass(this._container, 'mapml-image-container'); let inputData = M._extractInputBounds(template); this.zoomBounds = inputData.zoomBounds; @@ -22,6 +22,7 @@ export var TemplatedImageLayer = L.Layer.extend({ return events; }, onAdd: function () { + this.options.pane.appendChild(this._container); this._map._addZoomLimit(this); //used to set the zoom limit of the map this.setZIndex(this.options.zIndex); this._onAdd(); @@ -159,9 +160,9 @@ export var TemplatedImageLayer = L.Layer.extend({ } }, onRemove: function (map) { + L.DomUtil.remove(this._container); this._clearLayer(); map._removeZoomLimit(this); - this._container = null; }, getImageUrl: function (pixelBounds, zoom) { var obj = {}; diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index 23923718f..b393d0152 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -2,8 +2,9 @@ export var TemplatedLayer = L.Layer.extend({ initialize: function (templates, options) { this._templates = templates; L.setOptions(this, options); - this._container = L.DomUtil.create('div', 'leaflet-layer', options.pane); - this._container.style.opacity = this.options.opacity; + this._container = L.DomUtil.create('div', 'leaflet-layer'); + this._extentEl = this.options.extentEl; + this.changeOpacity(this.options.opacity); L.DomUtil.addClass(this._container, 'mapml-templatedlayer-container'); for (var i = 0; i < templates.length; i++) { @@ -257,6 +258,8 @@ export var TemplatedLayer = L.Layer.extend({ } }, onAdd: function (map) { + // add to this.options.pane + this.options.pane.appendChild(this._container); for (var i = 0; i < this._templates.length; i++) { if (this._templates[i].rel !== 'query') { map.addLayer(this._templates[i].layer); @@ -308,6 +311,9 @@ export var TemplatedLayer = L.Layer.extend({ changeOpacity: function (opacity) { this._container.style.opacity = opacity; + this._extentEl._opacity = opacity; + if (this._extentEl._opacitySlider) + this._extentEl._opacitySlider.value = opacity; } }); export var templatedLayer = function (templates, options) { diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 6daea770d..c34b77698 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -1,5 +1,3 @@ -import { BLANK_TT_TREF } from '../utils/Constants'; - export var TemplatedTileLayer = L.TileLayer.extend({ // a TemplateTileLayer is similar to a L.TileLayer except its templates are // defined by the