From f8e98c06ca736aa50234b8d9022e879cfbf2572d Mon Sep 17 00:00:00 2001 From: AliyanH Date: Fri, 7 Jul 2023 10:22:46 -0400 Subject: [PATCH 01/62] Add default projection (OSMTILE) + remove use of 'createmap' events Add default projection (OSMTILE) + remove use of 'createmap' events --- src/layer.js | 24 +++--------- src/map-extent.js | 12 +----- src/map-feature.js | 23 +++-------- src/mapml-viewer.js | 38 ++++++++++-------- src/mapml/handlers/AnnounceMovement.js | 10 ++++- src/mapml/layers/MapMLLayer.js | 6 ++- test/e2e/core/projectionDefault.html | 26 ++++++++++++ test/e2e/core/projectionDefault.test.js | 50 ++++++++++++++++++++++++ test/e2e/mapml-viewer/customTCRS.test.js | 18 ++++++--- 9 files changed, 134 insertions(+), 73 deletions(-) create mode 100644 test/e2e/core/projectionDefault.html create mode 100644 test/e2e/core/projectionDefault.test.js diff --git a/src/layer.js b/src/layer.js index 2e5f0e428..d37b1a383 100644 --- a/src/layer.js +++ b/src/layer.js @@ -95,25 +95,11 @@ export class MapLayer extends HTMLElement { if (this.getAttribute('src') && !this.shadowRoot) { this.attachShadow({ mode: 'open' }); } - //creates listener that waits for createmap event, this allows for delayed builds of maps - //this allows a safeguard for the case where loading a custom TCRS takes longer than loading mapml-viewer.js/web-map.js - this.parentNode.addEventListener( - 'createmap', - () => { - this._ready(); - // if the map has been attached, set this layer up wrt Leaflet map - if (this.parentNode._map) { - this._attachedToMap(); - } - if (this._layerControl && !this.hidden) { - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } - }, - { once: true } - ); //listener stops listening after event occurs once - //if map is already created then dispatch createmap event, allowing layer to be built - if (this.parentNode._map) - this.parentNode.dispatchEvent(new CustomEvent('createmap')); + this._ready(); + this._attachedToMap(); + if (this._layerControl && !this.hidden) { + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + } } adoptedCallback() { diff --git a/src/map-extent.js b/src/map-extent.js index bb81f376e..9b81dc2bb 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -78,17 +78,7 @@ export class MapExtent extends HTMLElement { this.parentNode.nodeName.toUpperCase() === 'LAYER-' ? this.parentNode : this.parentNode.host; - if (!parentLayer._layer) { - // for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point, - // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created - // the event will be dispatched after defineCustomProjection > projection setter - // should wait until MapMLLayer is built - parentLayer.parentNode.addEventListener('createmap', (e) => { - this._layer = parentLayer._layer; - }); - } else { - this._layer = parentLayer._layer; - } + this._layer = parentLayer._layer; } disconnectedCallback() {} } diff --git a/src/map-feature.js b/src/map-feature.js index f54d94f5a..b5fa81d68 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -220,23 +220,12 @@ export class MapFeature extends HTMLElement { } }; - if (!this._parentEl._layer) { - // for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point, - // because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created - // the event will be dispatched after defineCustomProjection > projection setter - // should wait until MapMLLayer is built - let parentLayer = - this._parentEl.nodeName.toUpperCase() === 'LAYER-' - ? this._parentEl - : this._parentEl.parentElement || this._parentEl.parentNode.host; - parentLayer.parentNode.addEventListener('createmap', (e) => { - this._layer = parentLayer._layer; - _attachedToMap(); - }); - } else { - this._layer = this._parentEl._layer; - _attachedToMap(); - } + let parentLayer = + parentEl.nodeName.toUpperCase() === 'LAYER-' + ? parentEl + : parentEl.parentElement || parentEl.parentNode.host; + this._layer = parentLayer._layer; + _attachedToMap(); } _updateFeature() { diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 1210f4b72..599a1b42d 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -74,23 +74,13 @@ export class MapViewer extends HTMLElement { } } get projection() { - return this.hasAttribute('projection') + return this.hasAttribute('projection') && M[this.getAttribute('projection')] ? this.getAttribute('projection') - : ''; + : 'OSMTILE'; } set projection(val) { if (val && M[val]) { this.setAttribute('projection', val); - if (this._map && this._map.options.projection !== val) { - this._map.options.crs = M[val]; - this._map.options.projection = val; - for (let layer of this.querySelectorAll('layer-')) { - layer.removeAttribute('disabled'); - let reAttach = this.removeChild(layer); - this.appendChild(reAttach); - } - if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - } else this.dispatchEvent(new CustomEvent('createmap')); } else throw new Error('Undefined Projection'); } get zoom() { @@ -176,7 +166,6 @@ export class MapViewer extends HTMLElement { // is because the mapml-viewer element has / can have a size of 0 up until after // something that happens between this point and the event handler executing // perhaps a browser rendering cycle?? - this.addEventListener('createmap', this._createMap); let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( this.projection @@ -185,9 +174,8 @@ export class MapViewer extends HTMLElement { // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent // In particular: // "All applicable event handlers are called and return before dispatchEvent() returns." - if (!custom) { - this.dispatchEvent(new CustomEvent('createmap')); - } + this._createMap(); + // https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274 this.setAttribute('role', 'application'); this._toggleStatic(); @@ -362,6 +350,22 @@ export class MapViewer extends HTMLElement { case 'static': this._toggleStatic(); break; + case 'projection': + if (newValue && M[newValue]) { + if (this._map && this._map.options.projection !== newValue) { + this._map.options.crs = M[newValue]; + this._map.options.projection = newValue; + for (let layer of this.querySelectorAll('layer-')) { + layer.removeAttribute('disabled'); + let reAttach = this.removeChild(layer); + this.appendChild(reAttach); + } + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); + this.zoomTo(this.lat, this.lon, this.zoom); + //this.dispatchEvent(new CustomEvent('projectionchange')); + } + } + break; } } @@ -815,7 +819,7 @@ export class MapViewer extends HTMLElement { this._updateMapCenter(); this._addToHistory(); this.dispatchEvent( - new CustomEvent('moveend', { detail: { target: this } }) + new CustomEvent('map-moveend', { detail: { target: this } }) ); }, this diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js index f0beef557..960efdd3a 100644 --- a/src/mapml/handlers/AnnounceMovement.js +++ b/src/mapml/handlers/AnnounceMovement.js @@ -5,7 +5,10 @@ export var AnnounceMovement = L.Handler.extend({ layerremove: this.totalBounds }); - this._map.options.mapEl.addEventListener('moveend', this.announceBounds); + this._map.options.mapEl.addEventListener( + 'map-moveend', + this.announceBounds + ); this._map.dragging._draggable.addEventListener('dragstart', this.dragged); this._map.options.mapEl.addEventListener( 'mapfocused', @@ -18,7 +21,10 @@ export var AnnounceMovement = L.Handler.extend({ layerremove: this.totalBounds }); - this._map.options.mapEl.removeEventListener('moveend', this.announceBounds); + this._map.options.mapEl.removeEventListener( + 'map-moveend', + this.announceBounds + ); this._map.dragging._draggable.removeEventListener( 'dragstart', this.dragged diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 48cfaa398..5a829cb58 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1260,7 +1260,10 @@ export var MapMLLayer = L.Layer.extend({ cs ); } else { - extentFallback.bounds = M[projection].options.crs.pcrs.bounds; + // 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++) { @@ -1491,6 +1494,7 @@ export var MapMLLayer = L.Layer.extend({ ); return; } else if ( + // when there is only one layer with different projection from the map, change's map's projection to match layer !projectionMatch && layer._map && layer._map.options.mapEl.querySelectorAll('layer-').length === 1 diff --git a/test/e2e/core/projectionDefault.html b/test/e2e/core/projectionDefault.html new file mode 100644 index 000000000..e1edab2dc --- /dev/null +++ b/test/e2e/core/projectionDefault.html @@ -0,0 +1,26 @@ + + + + + + + + Default Projection + + + + + + Rectangle + + + -123.216259390223 55.90361621419453 -123.216259390223 -0.5886925650467134 78.15130158758717 -0.5886925650467134 78.15130158758717 55.90361621419453 -123.216259390223 55.90361621419453 + + + + Rectangle + + + + + \ No newline at end of file diff --git a/test/e2e/core/projectionDefault.test.js b/test/e2e/core/projectionDefault.test.js new file mode 100644 index 000000000..91ebcd856 --- /dev/null +++ b/test/e2e/core/projectionDefault.test.js @@ -0,0 +1,50 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('Playwright Viewer Default Projection', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('projectionDefault.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test.describe('Viewer with no projection attribute', () => { + test('Viewer defaults to OSMTILE', async () => { + const mapProjection = await page.$eval( + 'body > mapml-viewer', + (map) => map.projection + ); + const leafletProjection = await page.$eval( + 'body > mapml-viewer', + (map) => map._map.options.projection + ); + const leafletProjection1 = await page.$eval( + 'body > mapml-viewer', + (map) => map._map.options.crs.code + ); + const projectionAttribute = await page.$eval( + 'body > mapml-viewer', + (map) => map.getAttribute('projection') + ); + expect(mapProjection).toEqual('OSMTILE'); + expect(leafletProjection).toEqual('OSMTILE'); + expect(leafletProjection1).toEqual('EPSG:3857'); + expect(projectionAttribute).toEqual(null); + }); + + test('layer renders', async () => { + const featureSVG = await page.$eval( + 'body > mapml-viewer > layer- > map-feature', + (feature) => feature._groupEl.firstChild.getAttribute('d') + ); + expect(featureSVG).toEqual('M62 27L62 75L206 75L206 27L62 27z'); + }); + }); +}); diff --git a/test/e2e/mapml-viewer/customTCRS.test.js b/test/e2e/mapml-viewer/customTCRS.test.js index 731e68a92..cf0d4a06c 100644 --- a/test/e2e/mapml-viewer/customTCRS.test.js +++ b/test/e2e/mapml-viewer/customTCRS.test.js @@ -18,8 +18,13 @@ test.describe('Playwright Custom TCRS Tests', () => { test('Simple Custom TCRS, tiles load, mismatched layer disabled', async () => { const misMatchedLayerDisabled = await page.$eval( - 'body > mapml-viewer:nth-child(1) > layer-:nth-child(1)', - (layer) => layer.hasAttribute('disabled') + 'body > mapml-viewer:nth-child(1)', + (map) => map.querySelectorAll('layer-')[0].hasAttribute('disabled') + ); + + const matchedLayerEnabled = await page.$eval( + 'body > mapml-viewer:nth-child(1)', + (map) => map.querySelectorAll('layer-')[1].hasAttribute('disabled') ); const tilesLoaded = await page.$eval( @@ -29,6 +34,7 @@ test.describe('Playwright Custom TCRS Tests', () => { expect(tilesLoaded).toEqual(2); expect(misMatchedLayerDisabled).toEqual(true); + expect(matchedLayerEnabled).toEqual(false); }); test('A projection name containing a colon is invalid', async () => { const message = await page.$eval( @@ -39,13 +45,13 @@ test.describe('Playwright Custom TCRS Tests', () => { }); test('Complex Custom TCRS, static features loaded, templated features loaded', async () => { const staticFeatures = await page.$eval( - 'body > mapml-viewer:nth-child(3) > layer-:nth-child(1)', - (layer) => layer.hasAttribute('disabled') + 'body > mapml-viewer:nth-child(3)', + (map) => map.querySelectorAll('layer-')[0].hasAttribute('disabled') ); const templatedFeatures = await page.$eval( - 'body > mapml-viewer:nth-child(3) > layer-:nth-child(2)', - (layer) => layer.hasAttribute('disabled') + 'body > mapml-viewer:nth-child(3)', + (map) => map.querySelectorAll('layer-')[1].hasAttribute('disabled') ); const featureOne = await page.$eval( From 1a7f57e2955c2ab75116be37bd414825fa244103 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 20 Jul 2023 11:03:00 -0400 Subject: [PATCH 02/62] Rename _processInitialExtent to _processContent. Make a few anonymous function declarations to encapsulate and make the whole process a bit understandable. Not finished yet, but it builds. Fix spooky errors due to closure and other unknowns TBD Tune to closure scopes, add comment, rename attachToLayer to copyRemoteContentToShadowRoot Add temporarily named function for dealing with projection matching Tests not working, some problems with custom projections. --- src/map-feature.js | 6 +- src/mapml/layers/MapMLLayer.js | 208 ++++++++++++++++----------------- 2 files changed, 107 insertions(+), 107 deletions(-) diff --git a/src/map-feature.js b/src/map-feature.js index b5fa81d68..16ae1b52f 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -221,9 +221,9 @@ export class MapFeature extends HTMLElement { }; let parentLayer = - parentEl.nodeName.toUpperCase() === 'LAYER-' - ? parentEl - : parentEl.parentElement || parentEl.parentNode.host; + this._parentEl.nodeName.toUpperCase() === 'LAYER-' + ? this._parentEl + : this._parentEl.parentElement || this._parentEl.parentNode.host; this._layer = parentLayer._layer; _attachedToMap(); } diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 5a829cb58..2bbe2fc6a 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1148,11 +1148,11 @@ export var MapMLLayer = L.Layer.extend({ if (this._href) { var xhr = new XMLHttpRequest(); // xhr.withCredentials = true; - _get(this._href, _processInitialExtent); + _get(this._href, _processContent); } else if (content) { // may not set this._extent if it can't be done from the content // (eg a single point) and there's no map to provide a default yet - _processInitialExtent.call(this, content); + _processContent.call(this, content); } function _get(url, fCallback) { xhr.onreadystatechange = function () { @@ -1396,29 +1396,45 @@ export var MapMLLayer = L.Layer.extend({ } return templateVars; } - - function _processInitialExtent(content) { - //TODO: include inline extents + function _processContent(content) { var mapml = this.responseXML || content; + var base = new URL( + mapml.querySelector('map-base') + ? mapml.querySelector('map-base').getAttribute('href') + : mapml.baseURI || this.responseURL, + this.responseURL + ).href; if (mapml.querySelector && mapml.querySelector('map-feature')) layer._content = mapml; if (!this.responseXML && this.responseText) mapml = new DOMParser().parseFromString(this.responseText, 'text/xml'); - - // if everything is ok, continue with the processing - if ( - this.readyState === this.DONE && - mapml.querySelector && - !mapml.querySelector('parsererror') - ) { - // Get layer's title/label - if (mapml.querySelector('map-title')) { - layer._title = mapml.querySelector('map-title').textContent.trim(); - layer._titleIsReadOnly = true; - } else if (mapml instanceof Element && mapml.hasAttribute('label')) { - layer._title = mapml.getAttribute('label').trim(); + if (mapml.querySelector && mapml.querySelector('parsererror')) { + layer.error = 'true'; + throw new Error('Error parsing content'); + } else if (this.readyState === this.DONE && mapml.querySelector) { + setLayerTitle(); + thinkOfAGoodName(); + parseLicenseAndLegend(); + setZoomInOrOutLinks(); + resetTemplatedLayers(); + processTiles(); + M._parseStylesheetAsHTML(mapml, base, layer._container); + getExtentLayerControls(); + layer._styles = getAlternateStyles(); + layer._validateExtent(); + copyRemoteContentToShadowRoot(); + // update controls if needed based on mapml-viewer controls/controlslist attribute + if (layer._layerEl.parentElement) { + // if layer does not have a parent Element, do not need to set Controls + layer._layerEl.parentElement._toggleControls(); } - + layer.fire('extentload', layer, false); + layer._layerEl.dispatchEvent( + new CustomEvent('extentload', { detail: layer, bubbles: true }) + ); + } + // local functions + function thinkOfAGoodName() { var serverExtent = mapml.querySelectorAll('map-extent'), projection, projectionMatch, @@ -1472,13 +1488,7 @@ export var MapMLLayer = L.Layer.extend({ 'map-head map-link[rel=alternate][projection=' + layer.options.mapprojection + ']' - ), - base = new URL( - mapml.querySelector('map-base') - ? mapml.querySelector('map-base').getAttribute('href') - : mapml.baseURI || this.responseURL, - this.responseURL - ).href; + ); if ( !projectionMatch && @@ -1493,14 +1503,6 @@ export var MapMLLayer = L.Layer.extend({ false ); return; - } else if ( - // when there is only one layer with different projection from the map, change's map's projection to match layer - !projectionMatch && - layer._map && - layer._map.options.mapEl.querySelectorAll('layer-').length === 1 - ) { - layer._map.options.mapEl.projection = projection; - return; } else if (!serverMeta) { layer._extent = {}; if (projectionMatch) { @@ -1540,8 +1542,18 @@ export var MapMLLayer = L.Layer.extend({ layer._extent = serverMeta; } } - layer._parseLicenseAndLegend(mapml, layer, projection); - + } + function getExtentLayerControls() { + // add multiple extents + if (layer._extent._mapExtents) { + for (let j = 0; j < layer._extent._mapExtents.length; j++) { + var labelName = layer._extent._mapExtents[j].getAttribute('label'); + var extentElement = layer.getLayerExtentHTML(labelName, j); + layer._extent._mapExtents[j].extentAnatomy = extentElement; + } + } + } + function setZoomInOrOutLinks() { var zoomin = mapml.querySelector('map-link[rel=zoomin]'), zoomout = mapml.querySelector('map-link[rel=zoomout]'); delete layer._extent.zoomin; @@ -1558,6 +1570,8 @@ export var MapMLLayer = L.Layer.extend({ base ).href; } + } + function resetTemplatedLayers() { if (layer._extent._mapExtents) { for (let i = 0; i < layer._extent._mapExtents.length; i++) { if (layer._extent._mapExtents[i].templatedLayer) { @@ -1568,6 +1582,8 @@ export var MapMLLayer = L.Layer.extend({ } } } + } + function processTiles() { if (mapml.querySelector('map-tile')) { var tiles = document.createElement('map-tiles'), zoom = @@ -1585,17 +1601,8 @@ export var MapMLLayer = L.Layer.extend({ } layer._mapmlTileContainer.appendChild(tiles); } - M._parseStylesheetAsHTML(mapml, base, layer._container); - - // add multiple extents - if (layer._extent._mapExtents) { - for (let j = 0; j < layer._extent._mapExtents.length; j++) { - var labelName = layer._extent._mapExtents[j].getAttribute('label'); - var extentElement = layer.getLayerExtentHTML(labelName, j); - layer._extent._mapExtents[j].extentAnatomy = extentElement; - } - } - + } + function getAlternateStyles() { var styleLinks = mapml.querySelectorAll( 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' ); @@ -1622,7 +1629,7 @@ export var MapMLLayer = L.Layer.extend({ 'id', 'rad-' + L.stamp(styleOptionInput) ); - styleOptionInput.setAttribute('name', 'styles-' + this._title); + styleOptionInput.setAttribute('name', 'styles-' + layer._title); styleOptionInput.setAttribute( 'value', styleLinks[j].getAttribute('title') @@ -1652,41 +1659,25 @@ export var MapMLLayer = L.Layer.extend({ ); L.DomEvent.on(styleOptionInput, 'click', changeStyle, layer); } - layer._styles = stylesControl; + return stylesControl; } - - if (layer._map) { - layer._validateExtent(); - // if the layer is checked in the layer control, force the addition - // of the attribution just received - if (layer._map.hasLayer(layer)) { - layer._map.attributionControl.addAttribution( - layer.getAttribution() - ); - } - //layer._map.fire('moveend', layer); - } - } else { - layer.error = true; } - if (this.responseXML) { - _attachToLayer.call(layer); - } - layer.fire('extentload', layer, false); - // update controls if needed based on mapml-viewer controls/controlslist attribute - if (layer._layerEl.parentElement) { - // if layer does not have a parent Element, do not need to set Controls - layer._layerEl.parentElement._toggleControls(); + function setLayerTitle() { + if (mapml.querySelector('map-title')) { + layer._title = mapml.querySelector('map-title').textContent.trim(); + layer._titleIsReadOnly = true; + } else if (mapml instanceof Element && mapml.hasAttribute('label')) { + layer._title = mapml.getAttribute('label').trim(); + } } - layer._layerEl.dispatchEvent( - new CustomEvent('extentload', { detail: layer, bubbles: true }) - ); - } - - function _attachToLayer() { - let mapml = xhr.responseXML, - shadowRoot = this._layerEl.shadowRoot; - if (mapml) { + function copyRemoteContentToShadowRoot() { + // only run when content is loaded from network, puts features etc + // into layer shadow root + if (!xhr) { + return; + } + let mapml = xhr.responseXML, + shadowRoot = layer._layerEl.shadowRoot; let elements = mapml.children[0].children[1].children; if (elements) { let baseURL = mapml.children[0].children[0] @@ -1717,6 +1708,38 @@ export var MapMLLayer = L.Layer.extend({ } } } + function parseLicenseAndLegend() { + var licenseLink = mapml.querySelector('map-link[rel=license]'), + licenseTitle, + licenseUrl, + attText; + if (licenseLink) { + licenseTitle = licenseLink.getAttribute('title'); + licenseUrl = licenseLink.getAttribute('href'); + attText = + '' + + licenseTitle + + ''; + } + L.setOptions(layer, { attribution: attText }); + var legendLink = mapml.querySelector('map-link[rel=legend]'); + if (legendLink) { + layer._legendUrl = legendLink.getAttribute('href'); + } + if (layer._map) { + // if the layer is checked in the layer control, force the addition + // of the attribution just received + if (layer._map.hasLayer(layer)) { + layer._map.attributionControl.addAttribution( + layer.getAttribution() + ); + } + } + } } }, _validateExtent: function () { @@ -1757,7 +1780,7 @@ export var MapMLLayer = L.Layer.extend({ } }, // a layer must share a projection with the map so that all the layers can - // be overlayed in one coordinate space. WGS84 is a 'wildcard', sort of. + // be overlayed in one coordinate space. getProjection: function () { if (!this._extent) { return; @@ -1786,29 +1809,6 @@ export var MapMLLayer = L.Layer.extend({ } return FALLBACK_PROJECTION; }, - _parseLicenseAndLegend: function (xml, layer) { - var licenseLink = xml.querySelector('map-link[rel=license]'), - licenseTitle, - licenseUrl, - attText; - if (licenseLink) { - licenseTitle = licenseLink.getAttribute('title'); - licenseUrl = licenseLink.getAttribute('href'); - attText = - '' + - licenseTitle + - ''; - } - L.setOptions(layer, { attribution: attText }); - var legendLink = xml.querySelector('map-link[rel=legend]'); - if (legendLink) { - layer._legendUrl = legendLink.getAttribute('href'); - } - }, getQueryTemplates: function (pcrsClick) { if (this._extent && this._extent._queries) { var templates = []; From 5a6c4c284129fbe02be95efd2ef8195831deacb8 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 20 Jul 2023 16:19:39 -0400 Subject: [PATCH 03/62] Get rid of resetTemplatedLayers, which was a hold-over from the initial extent era. --- src/mapml/layers/MapMLLayer.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 2bbe2fc6a..e5174c329 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1404,6 +1404,8 @@ export var MapMLLayer = L.Layer.extend({ : mapml.baseURI || this.responseURL, this.responseURL ).href; + // TODO try to remove need for _extent, or rename it to e.g. _private + layer._extent = {}; if (mapml.querySelector && mapml.querySelector('map-feature')) layer._content = mapml; if (!this.responseXML && this.responseText) @@ -1416,7 +1418,6 @@ export var MapMLLayer = L.Layer.extend({ thinkOfAGoodName(); parseLicenseAndLegend(); setZoomInOrOutLinks(); - resetTemplatedLayers(); processTiles(); M._parseStylesheetAsHTML(mapml, base, layer._container); getExtentLayerControls(); @@ -1504,7 +1505,6 @@ export var MapMLLayer = L.Layer.extend({ ); return; } else if (!serverMeta) { - layer._extent = {}; if (projectionMatch) { layer._extent.crs = M[projection]; } @@ -1556,8 +1556,6 @@ export var MapMLLayer = L.Layer.extend({ function setZoomInOrOutLinks() { var zoomin = mapml.querySelector('map-link[rel=zoomin]'), zoomout = mapml.querySelector('map-link[rel=zoomout]'); - delete layer._extent.zoomin; - delete layer._extent.zoomout; if (zoomin) { layer._extent.zoomin = new URL( zoomin.getAttribute('href'), @@ -1571,18 +1569,6 @@ export var MapMLLayer = L.Layer.extend({ ).href; } } - function resetTemplatedLayers() { - if (layer._extent._mapExtents) { - for (let i = 0; i < layer._extent._mapExtents.length; i++) { - if (layer._extent._mapExtents[i].templatedLayer) { - layer._extent._mapExtents[i].templatedLayer.reset( - layer._extent._mapExtents[i]._templateVars, - layer._extent._mapExtents[i].extentZIndex - ); - } - } - } - } function processTiles() { if (mapml.querySelector('map-tile')) { var tiles = document.createElement('map-tiles'), From 3ced6e7ce0a0bd9c3781a5414e6ee423e1337684 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 20 Jul 2023 16:38:20 -0400 Subject: [PATCH 04/62] Refactor: MapMLLayer._extent -> MapMLLayer._properties --- src/layer.js | 25 ++- src/map-feature.js | 2 +- src/mapml/control/LayerControl.js | 12 +- src/mapml/handlers/QueryHandler.js | 2 +- src/mapml/layers/MapMLLayer.js | 291 +++++++++++++++-------------- 5 files changed, 175 insertions(+), 157 deletions(-) diff --git a/src/layer.js b/src/layer.js index d37b1a383..e0b880720 100644 --- a/src/layer.js +++ b/src/layer.js @@ -219,29 +219,34 @@ export class MapLayer extends HTMLElement { let type = layerTypes[j]; if (this.checked && layer[type]) { if (type === '_templatedLayer') { - for (let i = 0; i < layer._extent._mapExtents.length; i++) { + for (let i = 0; i < layer._properties._mapExtents.length; i++) { for ( let j = 0; j < - layer._extent._mapExtents[i].templatedLayer._templates + layer._properties._mapExtents[i].templatedLayer._templates .length; j++ ) { if ( - layer._extent._mapExtents[i].templatedLayer._templates[j] - .rel === 'query' + layer._properties._mapExtents[i].templatedLayer + ._templates[j].rel === 'query' ) continue; total++; - layer._extent._mapExtents[i].removeAttribute('disabled'); - layer._extent._mapExtents[i].disabled = false; + layer._properties._mapExtents[i].removeAttribute( + 'disabled' + ); + layer._properties._mapExtents[i].disabled = false; if ( - !layer._extent._mapExtents[i].templatedLayer._templates[j] - .layer.isVisible + !layer._properties._mapExtents[i].templatedLayer + ._templates[j].layer.isVisible ) { count++; - layer._extent._mapExtents[i].setAttribute('disabled', ''); - layer._extent._mapExtents[i].disabled = true; + layer._properties._mapExtents[i].setAttribute( + 'disabled', + '' + ); + layer._properties._mapExtents[i].disabled = true; } } } diff --git a/src/map-feature.js b/src/map-feature.js index 16ae1b52f..5cb5ee0c1 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -206,7 +206,7 @@ export class MapFeature extends HTMLElement { layerEl.querySelectorAll('map-feature').length === 1 ) { // if the map-feature is added to an empty layer, fire extentload to create vector layer - // must re-run _initialize of MapMLLayer.js to re-set layer._extent (layer._extent is null for an empty layer) + // must re-run _initialize of MapMLLayer.js to re-set layer._properties (layer._properties is null for an empty layer) this._layer._initialize(layerEl); this._layer.fire('extentload'); } diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index bd21a3014..738a96c16 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -109,20 +109,20 @@ export var LayerControl = L.Control.Layers.extend({ } // check if an extent is disabled and disable it if ( - this._layers[i].layer._extent && - this._layers[i].layer._extent._mapExtents + this._layers[i].layer._properties && + this._layers[i].layer._properties._mapExtents ) { for ( let j = 0; - j < this._layers[i].layer._extent._mapExtents.length; + j < this._layers[i].layer._properties._mapExtents.length; j++ ) { let input = - this._layers[i].layer._extent._mapExtents[j].extentAnatomy, + this._layers[i].layer._properties._mapExtents[j].extentAnatomy, label = input.getElementsByClassName('mapml-layer-item-name')[0]; if ( - this._layers[i].layer._extent._mapExtents[j].disabled && - this._layers[i].layer._extent._mapExtents[j].checked + this._layers[i].layer._properties._mapExtents[j].disabled && + this._layers[i].layer._properties._mapExtents[j].checked ) { label.style.fontStyle = 'italic'; input.disabled = true; diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index b0a494503..6753bcc07 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -50,7 +50,7 @@ export var QueryHandler = L.Handler.extend({ _query(e, layer) { var zoom = e.target.getZoom(), map = this._map, - crs = layer._extent.crs, // the crs for each extent would be the same + crs = layer._properties.crs, // the crs for each extent would be the same tileSize = map.options.crs.options.crs.tile.bounds.max.x, container = layer._container, popupOptions = { diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index e5174c329..f93ca7812 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -88,15 +88,15 @@ export var MapMLLayer = L.Layer.extend({ }, // remove all the extents before removing the layer from the map _removeExtents: function (map) { - if (this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i].templatedLayer) { - map.removeLayer(this._extent._mapExtents[i].templatedLayer); + 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._extent._queries) { - delete this._extent._queries; + if (this._properties._queries) { + delete this._properties._queries; } }, _changeOpacity: function (e) { @@ -151,7 +151,7 @@ export var MapMLLayer = L.Layer.extend({ }, onAdd: function (map) { - if (this._extent && !this._validProjection(map)) { + if (this._properties && !this._validProjection(map)) { this.validProjection = false; return; } @@ -248,9 +248,9 @@ export var MapMLLayer = L.Layer.extend({ const createAndAdd = createAndAddTemplatedLayers.bind(this); // if the extent has been initialized and received, update the map, if ( - this._extent && - this._extent._mapExtents && - this._extent._mapExtents[0]._templateVars + this._properties && + this._properties._mapExtents && + this._properties._mapExtents[0]._templateVars ) { createAndAdd(); } else { @@ -276,40 +276,44 @@ export var MapMLLayer = L.Layer.extend({ map.on('popupopen', this._attachSkipButtons, this); function createAndAddTemplatedLayers() { - if (this._extent && this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { + if (this._properties && this._properties._mapExtents) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { if ( - this._extent._mapExtents[i]._templateVars && - this._extent._mapExtents[i].checked + this._properties._mapExtents[i]._templateVars && + this._properties._mapExtents[i].checked ) { - if (!this._extent._mapExtents[i].extentZIndex) - this._extent._mapExtents[i].extentZIndex = i; + if (!this._properties._mapExtents[i].extentZIndex) + this._properties._mapExtents[i].extentZIndex = i; this._templatedLayer = M.templatedLayer( - this._extent._mapExtents[i]._templateVars, + this._properties._mapExtents[i]._templateVars, { pane: this._container, - opacity: this._extent._mapExtents[i]._templateVars.opacity, + opacity: this._properties._mapExtents[i]._templateVars.opacity, _leafletLayer: this, - crs: this._extent.crs, - extentZIndex: this._extent._mapExtents[i].extentZIndex, + 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._extent._mapExtents[i] refers to the in remote mapml + // this._properties._mapExtents[i] refers to the in remote mapml extentEl: - this._extent._mapExtents[i]._DOMnode || - this._extent._mapExtents[i] + this._properties._mapExtents[i]._DOMnode || + this._properties._mapExtents[i] } ).addTo(map); - this._extent._mapExtents[i].templatedLayer = this._templatedLayer; + this._properties._mapExtents[i].templatedLayer = + this._templatedLayer; if (this._templatedLayer._queries) { - if (!this._extent._queries) this._extent._queries = []; - this._extent._queries = this._extent._queries.concat( + if (!this._properties._queries) this._properties._queries = []; + this._properties._queries = this._properties._queries.concat( this._templatedLayer._queries ); } } - if (this._extent._mapExtents[i].hasAttribute('opacity')) { - let opacity = this._extent._mapExtents[i].getAttribute('opacity'); - this._extent._mapExtents[i].templatedLayer.changeOpacity(opacity); + if (this._properties._mapExtents[i].hasAttribute('opacity')) { + let opacity = + this._properties._mapExtents[i].getAttribute('opacity'); + this._properties._mapExtents[i].templatedLayer.changeOpacity( + opacity + ); } } this._setLayerElExtent(); @@ -319,10 +323,10 @@ export var MapMLLayer = L.Layer.extend({ _validProjection: function (map) { let noLayer = false; - if (this._extent && this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i]._templateVars) { - for (let template of this._extent._mapExtents[i]._templateVars) + 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 @@ -360,71 +364,73 @@ export var MapMLLayer = L.Layer.extend({ layerTypes.forEach((type) => { if (this[type]) { if (type === '_templatedLayer') { - for (let i = 0; i < this._extent._mapExtents.length; i++) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { for ( let j = 0; - j < this._extent._mapExtents[i]._templateVars.length; + j < this._properties._mapExtents[i]._templateVars.length; j++ ) { let inputData = M._extractInputBounds( - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] ); - this._extent._mapExtents[i]._templateVars[j].tempExtentBounds = - inputData.bounds; - this._extent._mapExtents[i]._templateVars[j].extentZoomBounds = - inputData.zoomBounds; + this._properties._mapExtents[i]._templateVars[ + j + ].tempExtentBounds = inputData.bounds; + this._properties._mapExtents[i]._templateVars[ + j + ].extentZoomBounds = inputData.zoomBounds; } } - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i].checked) { + for (let i = 0; i < this._properties._mapExtents.length; i++) { + if (this._properties._mapExtents[i].checked) { for ( let j = 0; - j < this._extent._mapExtents[i]._templateVars.length; + j < this._properties._mapExtents[i]._templateVars.length; j++ ) { if (!bounds) { bounds = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .tempExtentBounds; zoomMax = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxZoom; zoomMin = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minZoom; maxNativeZoom = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxNativeZoom; minNativeZoom = - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minNativeZoom; } else { bounds.extend( - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .tempExtentBounds.min ); bounds.extend( - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .tempExtentBounds.max ); zoomMax = Math.max( zoomMax, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxZoom ); zoomMin = Math.min( zoomMin, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minZoom ); maxNativeZoom = Math.max( maxNativeZoom, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.maxNativeZoom ); minNativeZoom = Math.min( minNativeZoom, - this._extent._mapExtents[i]._templateVars[j] + this._properties._mapExtents[i]._templateVars[j] .extentZoomBounds.minNativeZoom ); } @@ -435,12 +441,13 @@ export var MapMLLayer = L.Layer.extend({ zoomBounds.maxZoom = zoomMax; zoomBounds.minNativeZoom = minNativeZoom; zoomBounds.maxNativeZoom = maxNativeZoom; - this._extent.zoomBounds = zoomBounds; - this._extent.layerBounds = bounds; + this._properties.zoomBounds = zoomBounds; + this._properties.layerBounds = bounds; // assign each template the layer and zoom bounds - for (let i = 0; i < this._extent._mapExtents.length; i++) { - this._extent._mapExtents[i].templatedLayer.layerBounds = bounds; - this._extent._mapExtents[i].templatedLayer.zoomBounds = zoomBounds; + 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 (this[type].layerBounds) { @@ -473,10 +480,10 @@ export var MapMLLayer = L.Layer.extend({ }, redraw: function () { // for now, only redraw templated layers. - if (this._extent._mapExtents) { - for (let i = 0; i < this._extent._mapExtents.length; i++) { - if (this._extent._mapExtents[i].templatedLayer) { - this._extent._mapExtents[i].templatedLayer.redraw(); + 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(); } } } @@ -491,8 +498,10 @@ export var MapMLLayer = L.Layer.extend({ // get the min and max zooms from all extents var toZoom = e.zoom, zoom = - this._extent && this._extent._mapExtents - ? this._extent._mapExtents[0].querySelector('map-input[type=zoom]') + this._properties && this._properties._mapExtents + ? this._properties._mapExtents[0].querySelector( + 'map-input[type=zoom]' + ) : null, min = zoom && zoom.hasAttribute('min') @@ -503,8 +512,8 @@ export var MapMLLayer = L.Layer.extend({ ? parseInt(zoom.getAttribute('max')) : this._map.getMaxZoom(); if (zoom) { - for (let i = 1; i < this._extent._mapExtents.length; i++) { - zoom = this._extent._mapExtents[i].querySelector( + for (let i = 1; i < this._properties._mapExtents.length; i++) { + zoom = this._properties._mapExtents[i].querySelector( 'map-input[type=zoom]' ); if (zoom && zoom.hasAttribute('min')) { @@ -516,23 +525,23 @@ export var MapMLLayer = L.Layer.extend({ } } var canZoom = - (toZoom < min && this._extent.zoomout) || - (toZoom > max && this._extent.zoomin); + (toZoom < min && this._properties.zoomout) || + (toZoom > max && this._properties.zoomin); if (!(min <= toZoom && toZoom <= max)) { - if (this._extent.zoomin && toZoom > max) { + if (this._properties.zoomin && toZoom > max) { // this._href is the 'original' url from which this layer came // since we are following a zoom link we will be getting a new // layer almost, resetting child content as appropriate - this._href = this._extent.zoomin; - this._layerEl.src = this._extent.zoomin; + this._href = this._properties.zoomin; + this._layerEl.src = this._properties.zoomin; // this.href is the "public" property. When a dynamic layer is // accessed, this value changes with every new extent received - this.href = this._extent.zoomin; - this._layerEl.src = this._extent.zoomin; - } else if (this._extent.zoomout && toZoom < min) { - this._href = this._extent.zoomout; - this.href = this._extent.zoomout; - this._layerEl.src = this._extent.zoomout; + this.href = this._properties.zoomin; + this._layerEl.src = this._properties.zoomin; + } else if (this._properties.zoomout && toZoom < min) { + this._href = this._properties.zoomout; + this.href = this._properties.zoomout; + this._layerEl.src = this._properties.zoomout; } } if (this._templatedLayer && canZoom) { @@ -545,7 +554,8 @@ export var MapMLLayer = L.Layer.extend({ if (this._staticTileLayer) map.removeLayer(this._staticTileLayer); if (this._mapmlvectors) map.removeLayer(this._mapmlvectors); if (this._imageLayer) map.removeLayer(this._imageLayer); - if (this._extent && this._extent._mapExtents) this._removeExtents(map); + if (this._properties && this._properties._mapExtents) + this._removeExtents(map); map.fire('checkdisabled'); map.off('popupopen', this._attachSkipButtons); @@ -595,7 +605,7 @@ export var MapMLLayer = L.Layer.extend({ if (!labelName) { // if a label attribute is not present, set it to hidden in layer control extent.setAttribute('hidden', ''); - this._extent._mapExtents[i].hidden = true; + this._properties._mapExtents[i].hidden = true; } // append the svg paths @@ -627,18 +637,18 @@ export var MapMLLayer = L.Layer.extend({ (e) => { let allRemoved = true; e.target.checked = false; - this._extent._mapExtents[i].removed = true; - this._extent._mapExtents[i].checked = false; + this._properties._mapExtents[i].removed = true; + this._properties._mapExtents[i].checked = false; if (this._layerEl.checked) - this._changeExtent(e, this._extent._mapExtents[i]); - this._extent._mapExtents[i].extentAnatomy.parentNode.removeChild( - this._extent._mapExtents[i].extentAnatomy + this._changeExtent(e, this._properties._mapExtents[i]); + this._properties._mapExtents[i].extentAnatomy.parentNode.removeChild( + this._properties._mapExtents[i].extentAnatomy ); - for (let j = 0; j < this._extent._mapExtents.length; j++) { - if (!this._extent._mapExtents[j].removed) allRemoved = false; + for (let j = 0; j < this._properties._mapExtents.length; j++) { + if (!this._properties._mapExtents[j].removed) allRemoved = false; } if (allRemoved) - this._layerItemSettingsHTML.removeChild(this._extentGroupAnatomy); + this._layerItemSettingsHTML.removeChild(this._propertiesGroupAnatomy); }, this ); @@ -682,17 +692,17 @@ export var MapMLLayer = L.Layer.extend({ 'aria-labelledby', 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary) ); - let opacityValue = this._extent._mapExtents[i].hasAttribute('opacity') - ? this._extent._mapExtents[i].getAttribute('opacity') + let opacityValue = this._properties._mapExtents[i].hasAttribute('opacity') + ? this._properties._mapExtents[i].getAttribute('opacity') : '1.0'; - this._extent._mapExtents[i]._templateVars.opacity = opacityValue; + this._properties._mapExtents[i]._templateVars.opacity = opacityValue; opacity.setAttribute('value', opacityValue); opacity.value = opacityValue; L.DomEvent.on( opacity, 'change', this._changeExtentOpacity, - this._extent._mapExtents[i] + this._properties._mapExtents[i] ); var extentItemNameSpan = L.DomUtil.create( @@ -700,17 +710,17 @@ export var MapMLLayer = L.Layer.extend({ 'mapml-layer-item-name', extentLabel ); - input.defaultChecked = this._extent._mapExtents[i] ? true : false; - this._extent._mapExtents[i].checked = input.defaultChecked; + input.defaultChecked = this._properties._mapExtents[i] ? true : false; + this._properties._mapExtents[i].checked = input.defaultChecked; input.type = 'checkbox'; extentItemNameSpan.innerHTML = labelName; L.DomEvent.on(input, 'change', (e) => { - this._changeExtent(e, this._extent._mapExtents[i]); + this._changeExtent(e, this._properties._mapExtents[i]); }); extentItemNameSpan.id = 'mapml-extent-item-name-{' + L.stamp(extentItemNameSpan) + '}'; extent.setAttribute('aria-labelledby', extentItemNameSpan.id); - extentItemNameSpan.extent = this._extent._mapExtents[i]; + extentItemNameSpan.extent = this._properties._mapExtents[i]; extent.ontouchstart = extent.onmousedown = (downEvent) => { if ( @@ -1086,7 +1096,7 @@ export var MapMLLayer = L.Layer.extend({ if (this._userInputs) { var frag = document.createDocumentFragment(); - var templates = this._extent._templateVars; + var templates = this._properties._templateVars; if (templates) { for (var i = 0; i < templates.length; i++) { var template = templates[i]; @@ -1122,14 +1132,16 @@ export var MapMLLayer = L.Layer.extend({ } // if there are extents, add them to the layer control - if (this._extent && this._extent._mapExtents) { + if (this._properties && this._properties._mapExtents) { var allHidden = true; this._layerItemSettingsHTML = layerItemSettings; - this._extentGroupAnatomy = extentsFieldset; + this._propertiesGroupAnatomy = extentsFieldset; extentsFieldset.setAttribute('aria-label', 'Sublayers'); - for (let j = 0; j < this._extent._mapExtents.length; j++) { - extentsFieldset.appendChild(this._extent._mapExtents[j].extentAnatomy); - if (!this._extent._mapExtents[j].hidden) allHidden = false; + 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); } @@ -1150,7 +1162,7 @@ export var MapMLLayer = L.Layer.extend({ // xhr.withCredentials = true; _get(this._href, _processContent); } else if (content) { - // may not set this._extent if it can't be done from the content + // may not set this._properties if it can't be done from the content // (eg a single point) and there's no map to provide a default yet _processContent.call(this, content); } @@ -1404,8 +1416,7 @@ export var MapMLLayer = L.Layer.extend({ : mapml.baseURI || this.responseURL, this.responseURL ).href; - // TODO try to remove need for _extent, or rename it to e.g. _private - layer._extent = {}; + layer._properties = {}; if (mapml.querySelector && mapml.querySelector('map-feature')) layer._content = mapml; if (!this.responseXML && this.responseText) @@ -1506,10 +1517,10 @@ export var MapMLLayer = L.Layer.extend({ return; } else if (!serverMeta) { if (projectionMatch) { - layer._extent.crs = M[projection]; + layer._properties.crs = M[projection]; } - layer._extent._mapExtents = []; // stores all the map-extent elements in the layer - layer._extent._templateVars = []; // stores all template variables coming from all extents + 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 < serverExtent.length; j++) { if ( serverExtent[j].querySelector( @@ -1517,7 +1528,7 @@ export var MapMLLayer = L.Layer.extend({ ) && serverExtent[j].hasAttribute('units') ) { - layer._extent._mapExtents.push(serverExtent[j]); + layer._properties._mapExtents.push(serverExtent[j]); projectionMatch = projectionMatch || selectedAlternate; let templateVars = _initTemplateVars.call( layer, @@ -1528,28 +1539,29 @@ export var MapMLLayer = L.Layer.extend({ base, projectionMatch ); - layer._extent._mapExtents[j]._templateVars = templateVars; - layer._extent._templateVars = - layer._extent._templateVars.concat(templateVars); + layer._properties._mapExtents[j]._templateVars = templateVars; + layer._properties._templateVars = + layer._properties._templateVars.concat(templateVars); } } } else { if (typeof serverMeta === 'string') { // when map-meta projection not present for layer - layer._extent = { serverMeta }; + layer._properties = { serverMeta }; } else { // when map-meta projection present for layer - layer._extent = serverMeta; + layer._properties = serverMeta; } } } function getExtentLayerControls() { // add multiple extents - if (layer._extent._mapExtents) { - for (let j = 0; j < layer._extent._mapExtents.length; j++) { - var labelName = layer._extent._mapExtents[j].getAttribute('label'); + if (layer._properties._mapExtents) { + for (let j = 0; j < layer._properties._mapExtents.length; j++) { + var labelName = + layer._properties._mapExtents[j].getAttribute('label'); var extentElement = layer.getLayerExtentHTML(labelName, j); - layer._extent._mapExtents[j].extentAnatomy = extentElement; + layer._properties._mapExtents[j].extentAnatomy = extentElement; } } } @@ -1557,13 +1569,13 @@ export var MapMLLayer = L.Layer.extend({ var zoomin = mapml.querySelector('map-link[rel=zoomin]'), zoomout = mapml.querySelector('map-link[rel=zoomout]'); if (zoomin) { - layer._extent.zoomin = new URL( + layer._properties.zoomin = new URL( zoomin.getAttribute('href'), base ).href; } if (zoomout) { - layer._extent.zoomout = new URL( + layer._properties.zoomout = new URL( zoomout.getAttribute('href'), base ).href; @@ -1730,12 +1742,12 @@ export var MapMLLayer = L.Layer.extend({ }, _validateExtent: function () { // TODO: change so that the _extent bounds are set based on inputs - if (!this._extent || !this._map) { + if (!this._properties || !this._map) { return; } - var serverExtent = this._extent._mapExtents - ? this._extent._mapExtents - : [this._extent], + var serverExtent = this._properties._mapExtents + ? this._properties._mapExtents + : [this._properties], lp; // loop through the map-extent elements and assign each one its crs @@ -1756,24 +1768,25 @@ export var MapMLLayer = L.Layer.extend({ ? serverExtent[i].getAttribute('units') : null; if (lp && M[lp]) { - if (this._extent._mapExtents) this._extent._mapExtents[i].crs = M[lp]; - else this._extent.crs = M[lp]; + if (this._properties._mapExtents) + this._properties._mapExtents[i].crs = M[lp]; + else this._properties.crs = M[lp]; } else { - if (this._extent._mapExtents) - this._extent._mapExtents[i].crs = M.OSMTILE; - else this._extent.crs = M.OSMTILE; + if (this._properties._mapExtents) + this._properties._mapExtents[i].crs = M.OSMTILE; + else this._properties.crs = M.OSMTILE; } } }, // a layer must share a projection with the map so that all the layers can // be overlayed in one coordinate space. getProjection: function () { - if (!this._extent) { + if (!this._properties) { return; } - let extent = this._extent._mapExtents - ? this._extent._mapExtents[0] - : this._extent; // the projections for each extent eould be the same (as) validated in _validProjection, so can use mapExtents[0] + let extent = this._properties._mapExtents + ? this._properties._mapExtents[0] + : this._properties; // the projections for each extent eould be the same (as) validated in _validProjection, so can use mapExtents[0] if (extent.serverMeta) return extent.serverMeta; switch (extent.tagName.toUpperCase()) { case 'MAP-EXTENT': @@ -1796,7 +1809,7 @@ export var MapMLLayer = L.Layer.extend({ return FALLBACK_PROJECTION; }, getQueryTemplates: function (pcrsClick) { - if (this._extent && this._extent._queries) { + if (this._properties && this._properties._queries) { var templates = []; // only return queries that are in bounds if ( @@ -1810,17 +1823,17 @@ export var MapMLLayer = L.Layer.extend({ for (let i = 0; i < layerAndExtents.length; i++) { if ( layerAndExtents[i].extent || - this._extent._mapExtents.length === 1 + this._properties._mapExtents.length === 1 ) { // the layer won't have an .extent property, this is kind of a hack let extent = - layerAndExtents[i].extent || this._extent._mapExtents[0]; + layerAndExtents[i].extent || this._properties._mapExtents[0]; for (let j = 0; j < extent._templateVars.length; j++) { if (extent.checked) { let template = extent._templateVars[j]; - // for each template in the extent, see if it corresponds to one in the this._extent._queries array - for (let k = 0; k < this._extent._queries.length; k++) { - let queryTemplate = this._extent._queries[k]; + // for each template in the extent, see if it corresponds to one in the this._properties._queries array + for (let k = 0; k < this._properties._queries.length; k++) { + let queryTemplate = this._properties._queries[k]; if ( template === queryTemplate && queryTemplate.extentBounds.contains(pcrsClick) From 58088f368f51b660257e170e9c95cff8066a2643 Mon Sep 17 00:00:00 2001 From: prushfor Date: Mon, 24 Jul 2023 09:45:14 -0400 Subject: [PATCH 05/62] Add promise handling of top-level _layer initialization WIP on tests, label, timing More WIP --- src/layer.js | 136 ++-- src/mapml/layers/MapMLLayer.js | 769 ++++++++++--------- src/web-map.js | 44 +- test/e2e/api/domApi-HTMLLayerElement.html | 4 +- test/e2e/api/domApi-HTMLLayerElement.test.js | 10 +- test/e2e/api/domApi-mapml-viewer.test.js | 30 +- test/e2e/api/domApi-web-map.test.js | 29 +- test/e2e/core/drag.test.js | 8 +- test/e2e/mapml-viewer/mapml-viewer.test.js | 13 +- 9 files changed, 521 insertions(+), 522 deletions(-) diff --git a/src/layer.js b/src/layer.js index e0b880720..2b9756e3f 100644 --- a/src/layer.js +++ b/src/layer.js @@ -15,11 +15,13 @@ export class MapLayer extends HTMLElement { } } get label() { - return this.hasAttribute('label') ? this.getAttribute('label') : ''; + if (this._layer) return this._layer.getName(); + else return this.hasAttribute('label') ? this.getAttribute('label') : ''; } set label(val) { if (val) { - this.setAttribute('label', val); + if (this._layer && !this._layer.titleIsReadOnly()) + this.setAttribute('label', val); } } get checked() { @@ -95,11 +97,60 @@ export class MapLayer extends HTMLElement { if (this.getAttribute('src') && !this.shadowRoot) { this.attachShadow({ mode: 'open' }); } - this._ready(); - this._attachedToMap(); - if (this._layerControl && !this.hidden) { - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } + new Promise((resolve, reject) => { + this.addEventListener( + 'extentload', + (event) => { + event.stopPropagation(); + if (event.detail.error) { + reject(); + } else { + resolve(); + } + }, + { once: true } + ); + this.addEventListener( + 'changestyle', + function (e) { + e.stopPropagation(); + this.src = e.detail.src; + }, + { once: true } + ); + this.addEventListener( + 'changeprojection', + function (e) { + e.stopPropagation(); + this.src = e.detail.href; + }, + { once: true } + ); + let base = this.baseURI ? this.baseURI : document.baseURI; + let opacity_value = this.hasAttribute('opacity') + ? this.getAttribute('opacity') + : '1.0'; + this._layer = M.mapMLLayer( + this.src ? new URL(this.src, base).href : null, + this, + { + mapprojection: this.parentElement._map.options.projection, + opacity: opacity_value + } + ); + }) + .then(() => { + this._onLayerExtentLoad(); + this._attachedToMap(); + if (this._layerControl && !this.hidden) { + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + } + }) + .catch((e) => { + this.dispatchEvent( + new CustomEvent('error', { detail: { target: this } }) + ); + }); } adoptedCallback() { @@ -188,18 +239,12 @@ export class MapLayer extends HTMLElement { if (this._layerControl) { this._layerControl.addOrUpdateOverlay(this._layer, this.label); } - if (!this._layer.error) { - // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied - // to MapML extent as metadata - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event - this.dispatchEvent( - new CustomEvent('loadedmetadata', { detail: { target: this } }) - ); - } else { - this.dispatchEvent( - new CustomEvent('error', { detail: { target: this } }) - ); - } + // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied + // to MapML extent as metadata + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event + this.dispatchEvent( + new CustomEvent('loadedmetadata', { detail: { target: this } }) + ); } _validateDisabled() { setTimeout(() => { @@ -323,28 +368,6 @@ export class MapLayer extends HTMLElement { this.checked = this._layer._map.hasLayer(this._layer); } } - _ready() { - // the layer might not be attached to a map - // so we need a way for non-src based layers to establish what their - // zoom range, extent and projection are. meta elements in content to - // allow the author to provide this explicitly are one way, they will - // be parsed from the second parameter here - // IE 11 did not have a value for this.baseURI for some reason - var base = this.baseURI ? this.baseURI : document.baseURI; - let opacity_value = this.hasAttribute('opacity') - ? this.getAttribute('opacity') - : '1.0'; - this._layer = M.mapMLLayer( - this.src ? new URL(this.src, base).href : null, - this, - { - mapprojection: this.parentElement._map.options.projection, - opacity: opacity_value - } - ); - this._layer.on('extentload', this._onLayerExtentLoad, this); - this._setUpEvents(); - } _attachedToMap() { // set i to the position of this layer element in the set of layers var i = 0, @@ -402,37 +425,6 @@ export class MapLayer extends HTMLElement { this._layer.off(); } } - _setUpEvents() { - this._layer.on( - 'loadstart', - function () { - this.dispatchEvent( - new CustomEvent('loadstart', { detail: { target: this } }) - ); - }, - this - ); - this._layer.on( - 'changestyle', - function (e) { - this.src = e.src; - this.dispatchEvent( - new CustomEvent('changestyle', { detail: { target: this } }) - ); - }, - this - ); - this._layer.on( - 'changeprojection', - function (e) { - this.src = e.href; - this.dispatchEvent( - new CustomEvent('changeprojection', { detail: { target: this } }) - ); - }, - this - ); - } zoomTo() { if (!this.extent) return; let map = this._layer._map, diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index f93ca7812..70d0ebee6 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1,3 +1,5 @@ +/* global M */ + import { FALLBACK_PROJECTION, BLANK_TT_TREF } from '../utils/Constants'; export var MapMLLayer = L.Layer.extend({ @@ -63,10 +65,6 @@ export var MapMLLayer = L.Layer.extend({ // above. Not going to change this, but failing to understand ATM. // may revisit some time. this.validProjection = true; - - // _mapmlLayerItem is set to the root element representing this layer - // in the layer control, iff the layer is not 'hidden' - this._mapmlLayerItem = {}; }, setZIndex: function (zIndex) { this.options.zIndex = zIndex; @@ -136,11 +134,14 @@ export var MapMLLayer = L.Layer.extend({ this._setLayerElExtent(); } }, + titleIsReadOnly() { + return !!this._titleIsReadOnly; + }, setName(newName) { // a layer's accessible name is set by the , if present // if it's not available the attribute // can be used - if (!this._titleIsReadOnly) { + if (!this.titleIsReadOnly()) { this._title = newName; this._mapmlLayerItem.querySelector('.mapml-layer-item-name').innerHTML = newName; @@ -821,331 +822,343 @@ export var MapMLLayer = L.Layer.extend({ }; return extent; }, - getLayerUserControlsHTML: 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._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; + return this._mapmlLayerItem + ? this._mapmlLayerItem + : this._createLayerControlHTML(); + }, + _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); - document.body.ontouchmove = document.body.onmousemove = (moveEvent) => { - moveEvent.preventDefault(); - moveEvent = - moveEvent instanceof TouchEvent ? moveEvent.touches[0] : moveEvent; + layerItemSettings.hidden = true; + settingsButtonNameIcon.setAttribute('aria-hidden', true); - // Fixes flickering by only moving element when there is enough space - let offset = moveEvent.clientY - yPos; - moving = Math.abs(offset) > 5 || moving; + 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 ( - (controls && !moving) || - (controls && controls.childElementCount <= 1) || - controls.getBoundingClientRect().top > - control.getBoundingClientRect().bottom || - controls.getBoundingClientRect().bottom < - control.getBoundingClientRect().top + 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 + ); - controls.classList.add('mapml-draggable'); - control.style.transform = 'translateY(' + offset + 'px)'; - control.style.pointerEvents = 'none'; + 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'); - 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') + Math.abs(offset) <= swapControl.offsetHeight ? control - : elementAt.closest('fieldset'); - - swapControl = - Math.abs(offset) <= swapControl.offsetHeight - ? control - : swapControl; + : 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); + } + }; - 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; + 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; + for (let c of controlsElems) { + let layerEl = c.querySelector('span').layer._layerEl; + + layerEl.setAttribute('data-moving', ''); + mapEl.insertAdjacentElement('beforeend', layerEl); + layerEl.removeAttribute('data-moving'); + + layerEl._layer.setZIndex(zIndex); + zIndex++; } - controls.insertBefore(control, swapControl); - } - }; + controls.classList.remove('mapml-draggable'); + document.body.ontouchmove = + document.body.onmousemove = + document.body.onmouseup = + null; + }; + } + }; - 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; - for (let c of controlsElems) { - let layerEl = c.querySelector('span').layer._layerEl; + L.DomEvent.on(opacity, 'change', this._changeOpacity, this); - layerEl.setAttribute('data-moving', ''); - mapEl.insertAdjacentElement('beforeend', layerEl); - layerEl.removeAttribute('data-moving'); + itemToggleLabel.appendChild(input); + itemToggleLabel.appendChild(layerItemName); + itemSettingControlButton.appendChild(settingsButtonNameIcon); + settingsButtonNameIcon.appendChild(svgSettingsControlIcon); - layerEl._layer.setZIndex(zIndex); - zIndex++; - } - controls.classList.remove('mapml-draggable'); - document.body.ontouchmove = - document.body.onmousemove = - document.body.onmouseup = - null; - }; + if (this._styles) { + layerItemSettings.appendChild(this._styles); } - }; - - 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); + 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); } - 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 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); } - if (!allHidden) layerItemSettings.appendChild(extentsFieldset); } - return this._mapmlLayerItem; }, _initialize: function (content) { @@ -1158,38 +1171,32 @@ export var MapMLLayer = L.Layer.extend({ // but there *is* child content of the element (which is copied/ // referred to by this._content), we should use that content. if (this._href) { - var xhr = new XMLHttpRequest(); - // xhr.withCredentials = true; _get(this._href, _processContent); } else if (content) { // may not set this._properties if it can't be done from the content // (eg a single point) and there's no map to provide a default yet - _processContent.call(this, content); + _processContent.call(this, content, true); } function _get(url, fCallback) { - xhr.onreadystatechange = function () { - if (this.readyState === this.DONE) { - if ( - this.status === 400 || - this.status === 404 || - this.status === 500 || - this.status === 406 - ) { - layer.error = true; - layer.fire('extentload', layer, true); - xhr.abort(); + const headers = new Headers(); + headers.append('Accept', 'text/mapml'); + fetch(url, { headers: headers }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); } - } - }; - xhr.onload = fCallback; - xhr.onerror = function () { - layer.error = true; - layer.fire('extentload', layer, true); - }; - xhr.open('GET', url); - xhr.setRequestHeader('Accept', M.mime); - xhr.overrideMimeType('text/xml'); - xhr.send(); + return response.text(); + }) + .then((response) => { + fCallback(response, false); + }) + .catch((response) => { + layer.error = true; + layer._layerEl.dispatchEvent( + new CustomEvent('extentload', { detail: layer }) + ); + console.log(`HTTP error! Status: ${response.message}`); + }); } function transcribe(element) { var select = document.createElement('select'); @@ -1408,43 +1415,52 @@ export var MapMLLayer = L.Layer.extend({ } return templateVars; } - function _processContent(content) { - var mapml = this.responseXML || content; + function _processContent(content, local) { + var mapml = !local + ? new DOMParser().parseFromString(content, 'text/xml') + : content; + if ( + !local && + (mapml.querySelector('parsererror') || !mapml.querySelector('mapml-')) + ) { + layer.error = true; + layer._layerEl.dispatchEvent( + new CustomEvent('extentload', { detail: layer }) + ); + throw new Error('Parser error'); + } var base = new URL( mapml.querySelector('map-base') ? mapml.querySelector('map-base').getAttribute('href') - : mapml.baseURI || this.responseURL, - this.responseURL + : local + ? mapml.baseURI + : layer._href, + layer._href ).href; layer._properties = {}; if (mapml.querySelector && mapml.querySelector('map-feature')) layer._content = mapml; - if (!this.responseXML && this.responseText) - mapml = new DOMParser().parseFromString(this.responseText, 'text/xml'); - if (mapml.querySelector && mapml.querySelector('parsererror')) { - layer.error = 'true'; - throw new Error('Error parsing content'); - } else if (this.readyState === this.DONE && mapml.querySelector) { - setLayerTitle(); - thinkOfAGoodName(); - parseLicenseAndLegend(); - setZoomInOrOutLinks(); - processTiles(); - M._parseStylesheetAsHTML(mapml, base, layer._container); - getExtentLayerControls(); - layer._styles = getAlternateStyles(); - layer._validateExtent(); - copyRemoteContentToShadowRoot(); - // update controls if needed based on mapml-viewer controls/controlslist attribute - if (layer._layerEl.parentElement) { - // if layer does not have a parent Element, do not need to set Controls - layer._layerEl.parentElement._toggleControls(); - } - layer.fire('extentload', layer, false); - layer._layerEl.dispatchEvent( - new CustomEvent('extentload', { detail: layer, bubbles: true }) - ); + if (thinkOfAGoodName()) return; + layer._styles = getAlternateStyles(); + setLayerTitle(); + parseLicenseAndLegend(); + setZoomInOrOutLinks(); + processTiles(); + M._parseStylesheetAsHTML(mapml, base, layer._container); + layer._styles = getAlternateStyles(); + layer._validateExtent(); + copyRemoteContentToShadowRoot(); + // update controls if needed based on mapml-viewer controls/controlslist attribute + if (layer._layerEl.parentElement) { + // if layer does not have a parent Element, do not need to set Controls + layer._layerEl.parentElement._toggleControls(); } + layer.fire('extentload', layer, false); + // need this to enable processing by the element connectedCallback + // processing + layer._layerEl.dispatchEvent( + new CustomEvent('extentload', { detail: layer }) + ); // local functions function thinkOfAGoodName() { var serverExtent = mapml.querySelectorAll('map-extent'), @@ -1507,14 +1523,14 @@ export var MapMLLayer = L.Layer.extend({ selectedAlternate && selectedAlternate.hasAttribute('href') ) { - layer.fire( - 'changeprojection', - { - href: new URL(selectedAlternate.getAttribute('href'), base).href - }, - false + layer._layerEl.dispatchEvent( + new CustomEvent('changeprojection', { + detail: { + href: new URL(selectedAlternate.getAttribute('href'), base).href + } + }) ); - return; + return true; } else if (!serverMeta) { if (projectionMatch) { layer._properties.crs = M[projection]; @@ -1553,8 +1569,6 @@ export var MapMLLayer = L.Layer.extend({ layer._properties = serverMeta; } } - } - function getExtentLayerControls() { // add multiple extents if (layer._properties._mapExtents) { for (let j = 0; j < layer._properties._mapExtents.length; j++) { @@ -1564,6 +1578,7 @@ export var MapMLLayer = L.Layer.extend({ layer._properties._mapExtents[j].extentAnatomy = extentElement; } } + return false; } function setZoomInOrOutLinks() { var zoomin = mapml.querySelector('map-link[rel=zoomin]'), @@ -1610,10 +1625,12 @@ export var MapMLLayer = L.Layer.extend({ stylesControlSummary.innerText = 'Style'; stylesControl.appendChild(stylesControlSummary); var changeStyle = function (e) { - layer.fire( - 'changestyle', - { src: e.target.getAttribute('data-href') }, - false + layer._layerEl.dispatchEvent( + new CustomEvent('changestyle', { + detail: { + src: e.target.getAttribute('data-href') + } + }) ); }; @@ -1667,15 +1684,17 @@ 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 // into layer shadow root - if (!xhr) { + if (local) { return; } - let mapml = xhr.responseXML, - shadowRoot = layer._layerEl.shadowRoot; + let shadowRoot = layer._layerEl.shadowRoot; let elements = mapml.children[0].children[1].children; if (elements) { let baseURL = mapml.children[0].children[0] diff --git a/src/web-map.js b/src/web-map.js index ff03f80ae..ac72b1cbe 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -75,23 +75,13 @@ export class WebMap extends HTMLMapElement { } } get projection() { - return this.hasAttribute('projection') + return this.hasAttribute('projection') && M[this.getAttribute('projection')] ? this.getAttribute('projection') : 'OSMTILE'; } set projection(val) { if (val && M[val]) { this.setAttribute('projection', val); - if (this._map && this._map.options.projection !== val) { - this._map.options.crs = M[val]; - this._map.options.projection = val; - for (let layer of this.querySelectorAll('layer-')) { - layer.removeAttribute('disabled'); - let reAttach = this.removeChild(layer); - this.appendChild(reAttach); - } - if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - } else this.dispatchEvent(new CustomEvent('createmap')); } else throw new Error('Undefined Projection'); } get zoom() { @@ -180,18 +170,16 @@ export class WebMap extends HTMLMapElement { // is because the mapml-viewer element has / can have a size of 0 up until after // something that happens between this point and the event handler executing // perhaps a browser rendering cycle?? - this.addEventListener('createmap', this._createMap); let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( this.projection ); - if (!custom) { - // this is worth a read, because dispatchEvent is synchronous - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent - // In particular: - // "All applicable event handlers are called and return before dispatchEvent() returns." - this.dispatchEvent(new CustomEvent('createmap')); - } + // this is worth a read, because dispatchEvent is synchronous + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent + // In particular: + // "All applicable event handlers are called and return before dispatchEvent() returns." + this._createMap(); + this._toggleStatic(); /* @@ -407,6 +395,22 @@ export class WebMap extends HTMLMapElement { case 'static': this._toggleStatic(); break; + case 'projection': + if (newValue && M[newValue]) { + if (this._map && this._map.options.projection !== newValue) { + this._map.options.crs = M[newValue]; + this._map.options.projection = newValue; + for (let layer of this.querySelectorAll('layer-')) { + layer.removeAttribute('disabled'); + let reAttach = this.removeChild(layer); + this.appendChild(reAttach); + } + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); + this.zoomTo(this.lat, this.lon, this.zoom); + //this.dispatchEvent(new CustomEvent('projectionchange')); + } + } + break; } } @@ -860,7 +864,7 @@ export class WebMap extends HTMLMapElement { this._updateMapCenter(); this._addToHistory(); this.dispatchEvent( - new CustomEvent('moveend', { detail: { target: this } }) + new CustomEvent('map-moveend', { detail: { target: this } }) ); }, this diff --git a/test/e2e/api/domApi-HTMLLayerElement.html b/test/e2e/api/domApi-HTMLLayerElement.html index 2978270b2..967ee926a 100644 --- a/test/e2e/api/domApi-HTMLLayerElement.html +++ b/test/e2e/api/domApi-HTMLLayerElement.html @@ -54,7 +54,7 @@

A Man With Two Hats

localWithTitle = document.querySelector('mapml-viewer').appendChild(localWithTitle); - addEventListener("extentload", (e) => { + addEventListener("loadedmetadata", (e) => { if (e.target === remoteWithTitle) { // setting label should not change the layer name in layer control remoteWithTitle.label = "Unforsettable in every way"; @@ -75,7 +75,7 @@

A Man With Two Hats

localNoTitle.label = "Go ahead, make my day!"; }, 500); } - }); + }, {capture: true}); \ No newline at end of file diff --git a/test/e2e/api/domApi-HTMLLayerElement.test.js b/test/e2e/api/domApi-HTMLLayerElement.test.js index 8c63847c8..eab6c8be7 100644 --- a/test/e2e/api/domApi-HTMLLayerElement.test.js +++ b/test/e2e/api/domApi-HTMLLayerElement.test.js @@ -19,7 +19,9 @@ test.describe('HTMLLayerElement DOM API Tests', () => { let remoteWithTitleLabel = await page.evaluate(() => { return document.querySelector('#remote-with-title').label; }); - expect(remoteWithTitleLabel).toEqual('Unforsettable in every way'); + expect(remoteWithTitleLabel).toEqual( + 'MapML author-controlled name - unsettable' + ); let remoteWithTitleName = await page.evaluate(() => { let layer = document.querySelector('#remote-with-title'); return layer._layer.getName(); @@ -41,12 +43,14 @@ test.describe('HTMLLayerElement DOM API Tests', () => { let localWithTitleLabel = await page.evaluate(() => { return document.querySelector('#local-with-title').label; }); - expect(localWithTitleLabel).toEqual('No dice, buddy!'); + expect(localWithTitleLabel).toEqual( + 'Layer name set via local map-title element - unsettable via HTMLLayerelement.label' + ); let localWithTitleName = await page.evaluate(() => { let layer = document.querySelector('#local-with-title'); return layer._layer.getName(); }); - expect(localWithTitleName).not.toEqual(localWithTitleLabel); + expect(localWithTitleName).toEqual(localWithTitleLabel); // THIS SHOULD NOT BE NECESSARY, BUT IT IS see comment below await page.waitForTimeout(500); diff --git a/test/e2e/api/domApi-mapml-viewer.test.js b/test/e2e/api/domApi-mapml-viewer.test.js index a6ecfc224..2ebf9fc77 100644 --- a/test/e2e/api/domApi-mapml-viewer.test.js +++ b/test/e2e/api/domApi-mapml-viewer.test.js @@ -108,11 +108,8 @@ test.describe('mapml-viewer DOM API Tests', () => { (layer) => document.querySelector('mapml-viewer').appendChild(layer), layerHandle ); - let layerControlHidden = await page.$eval( - 'css=body > mapml-viewer >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(false); + let layerControl = await page.locator('.leaflet-control-layers'); + await expect(layerControl).toBeVisible(); // set the layer's hidden attribute, the layer should be removed from the layer // control (but not the map), which leaves 0 layers in the layer control, which means the @@ -121,11 +118,7 @@ test.describe('mapml-viewer DOM API Tests', () => { (layer) => layer.setAttribute('hidden', ''), layerHandle ); - layerControlHidden = await page.$eval( - 'css=body > mapml-viewer >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); // takes a couple of seconds for the tiles to load @@ -138,6 +131,7 @@ test.describe('mapml-viewer DOM API Tests', () => { }); test('Remove mapml-viewer from DOM, add it back in', async () => { + await page.pause(); // check for error messages in console let errorLogs = []; page.on('pageerror', (err) => { @@ -145,12 +139,15 @@ test.describe('mapml-viewer DOM API Tests', () => { }); // locators avoid flaky tests, allegedly const viewer = await page.locator('mapml-viewer'); - await viewer.evaluate(() => {}); + await viewer.evaluate(() => { + let m = document.querySelector('mapml-viewer'); + document.body.removeChild(m); + document.body.appendChild(m); + }); + await page.waitForTimeout(200); expect( await viewer.evaluate(() => { let m = document.querySelector('mapml-viewer'); - document.body.removeChild(m); - document.body.appendChild(m); let l = m.querySelector('layer-'); return l.label; // the label attribute is ignored if the mapml document has a map-title @@ -365,10 +362,7 @@ test.describe('mapml-viewer DOM API Tests', () => { '.leaflet-top.leaflet-left > .leaflet-control-fullscreen', (div) => div.hidden ); - let layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right > .leaflet-control-layers', - (div) => div.hidden - ); + let layerControl = await page.locator('.leaflet-control-layers'); let scaleHidden = await page.$eval( '.leaflet-bottom.leaflet-left > .mapml-control-scale', (div) => div.hidden @@ -381,7 +375,7 @@ test.describe('mapml-viewer DOM API Tests', () => { expect(zoomHidden).toEqual(true); expect(reloadHidden).toEqual(true); expect(fullscreenHidden).toEqual(true); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); expect(scaleHidden).toEqual(true); }); diff --git a/test/e2e/api/domApi-web-map.test.js b/test/e2e/api/domApi-web-map.test.js index cd906c824..6d5c40877 100644 --- a/test/e2e/api/domApi-web-map.test.js +++ b/test/e2e/api/domApi-web-map.test.js @@ -103,11 +103,8 @@ test.describe('web-map DOM API Tests', () => { (layer) => document.querySelector('map').appendChild(layer), layerHandle ); - let layerControlHidden = await page.$eval( - 'css=body > map >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(false); + let layerControl = await page.locator('.leaflet-control-layers'); + await expect(layerControl).toBeVisible(); // set the layer's hidden attribute, the layer should be removed from the layer // control (but not the map), which leaves 0 layers in the layer control, which means the @@ -116,11 +113,7 @@ test.describe('web-map DOM API Tests', () => { (layer) => layer.setAttribute('hidden', ''), layerHandle ); - layerControlHidden = await page.$eval( - 'css=body > map >> css=div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div', - (elem) => elem.hasAttribute('hidden') - ); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); // takes a couple of seconds for the tiles to load @@ -139,12 +132,15 @@ test.describe('web-map DOM API Tests', () => { }); // locators avoid flaky tests, allegedly const viewer = await page.locator('map'); - await viewer.evaluate(() => {}); + await viewer.evaluate(() => { + let m = document.querySelector('map'); + document.body.removeChild(m); + document.body.appendChild(m); + }); + await page.waitForTimeout(200); expect( await viewer.evaluate(() => { let m = document.querySelector('map'); - document.body.removeChild(m); - document.body.appendChild(m); let l = m.querySelector('layer-'); return l.label; // the label attribute is ignored if the mapml document has a map-title @@ -346,10 +342,7 @@ test.describe('web-map DOM API Tests', () => { '.leaflet-top.leaflet-left > .leaflet-control-fullscreen', (div) => div.hidden ); - let layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right > .leaflet-control-layers', - (div) => div.hidden - ); + let layerControl = await page.locator('.leaflet-control-layers'); let scaleHidden = await page.$eval( '.leaflet-bottom.leaflet-left > .mapml-control-scale', (div) => div.hidden @@ -362,7 +355,7 @@ test.describe('web-map DOM API Tests', () => { expect(zoomHidden).toEqual(true); expect(reloadHidden).toEqual(true); expect(fullscreenHidden).toEqual(true); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); expect(scaleHidden).toEqual(true); }); diff --git a/test/e2e/core/drag.test.js b/test/e2e/core/drag.test.js index 2c5f0f2ed..8a94878fd 100644 --- a/test/e2e/core/drag.test.js +++ b/test/e2e/core/drag.test.js @@ -73,8 +73,8 @@ test.describe('UI Drag&Drop Test', () => { (span) => span.innerText ); const layerIndex = await page.$eval( - '.leaflet-pane.leaflet-overlay-pane > div:nth-child(1)', - (div) => div.style.zIndex + '.leaflet-pane.leaflet-overlay-pane .mapml-templated-tile-container', + (div) => div.parentElement.parentElement.style.zIndex ); const domLayer = await page.$eval( 'body > map > layer-:nth-child(4)', @@ -109,8 +109,8 @@ test.describe('UI Drag&Drop Test', () => { (span) => span.innerText ); const layerIndex = await page.$eval( - '.leaflet-overlay-pane > div:nth-child(2)', - (div) => div.style.zIndex + '.leaflet-overlay-pane .mapml-static-tile-layer', + (div) => div.parentElement.style.zIndex ); const domLayer = await page.$eval( 'map > layer-:nth-child(3)', diff --git a/test/e2e/mapml-viewer/mapml-viewer.test.js b/test/e2e/mapml-viewer/mapml-viewer.test.js index 7c2b40f39..49adbf28c 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.test.js +++ b/test/e2e/mapml-viewer/mapml-viewer.test.js @@ -120,12 +120,9 @@ test.describe('Playwright mapml-viewer Element Tests', () => { await page.$eval('body > mapml-viewer', (layer) => layer.setAttribute('controlslist', 'nolayer') ); + let layerControl = await page.locator('.leaflet-control-layers'); + await expect(layerControl).toBeHidden(); - let layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right', - (div) => div.firstChild.hidden - ); - expect(layerControlHidden).toEqual(true); await page.click('body > mapml-viewer', { button: 'right' }); // toggle controls await page.click('.mapml-contextmenu > button:nth-of-type(6)'); @@ -133,11 +130,7 @@ test.describe('Playwright mapml-viewer Element Tests', () => { // toggle controls await page.click('.mapml-contextmenu > button:nth-of-type(6)'); - layerControlHidden = await page.$eval( - '.leaflet-top.leaflet-right', - (div) => div.firstChild.hidden - ); - expect(layerControlHidden).toEqual(true); + await expect(layerControl).toBeHidden(); }); }); }); From 87ddcae91c5b2d3b7269d01a988b5a37062417d9 Mon Sep 17 00:00:00 2001 From: AliyanH Date: Fri, 28 Jul 2023 07:29:13 -0400 Subject: [PATCH 06/62] update tests to incorporate the load times with the use of the promise method WIP on thinkOfAGoodName --- src/mapml/layers/MapMLLayer.js | 128 ++++++++++++++++++++++++--- test/e2e/core/metaDefault.html | 4 +- test/e2e/core/metaDefault.test.js | 27 +++--- test/e2e/layers/featureLayer.html | 4 +- test/e2e/layers/featureLayer.test.js | 12 +-- 5 files changed, 139 insertions(+), 36 deletions(-) diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 70d0ebee6..9bdb3e372 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1440,7 +1440,13 @@ export var MapMLLayer = L.Layer.extend({ layer._properties = {}; if (mapml.querySelector && mapml.querySelector('map-feature')) layer._content = mapml; - if (thinkOfAGoodName()) return; + // sets layer._properties.projection + determineLayerProjection(); + // requires that layer._properties.projection be set + if (selectMatchingAlternateProjection()) return; + // sets layer._properties._mapExtents and layer._properties._templateVars, if applicable + processExtents(); + // if (thinkOfAGoodName()) return; layer._styles = getAlternateStyles(); setLayerTitle(); parseLicenseAndLegend(); @@ -1461,6 +1467,99 @@ export var MapMLLayer = L.Layer.extend({ layer._layerEl.dispatchEvent( new CustomEvent('extentload', { detail: layer }) ); + // sets layer._properties.projection. Supposed to replace / simplify + // the dependencies on convoluted getProjection() interface, but doesn't quite + // succeed, yet. + function determineLayerProjection() { + layer._properties.projection = FALLBACK_PROJECTION; + if (mapml.querySelector('map-meta[name=projection][content]')) { + layer._propertes.projection = M._metaContentToObject( + mapml + .querySelector('map-meta[name=projection]') + .getAttribute('content') + ).content.toUpperCase(); + } else if (mapml.querySelector('map-extent[units]')) { + const getProjectionFrom = (extents) => { + const projectionMatches = (extent) => { + return ( + extent.attributes.units.value === layer.options.mapprojection + ); + }; + if (extents.every(projectionMatches)) { + return layer.options.mapprojection; + } + }; + layer._properties.projection = getProjectionFrom( + Array.from(mapml.querySelectorAll('map-extent[units]')) + ); + } + } + // 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 + // resource that matches the map's projection + function selectMatchingAlternateProjection() { + let selectedAlternate = + layer._properties.projection !== layer.options.mapprojection && + mapml.querySelector( + 'map-head map-link[rel=alternate][projection=' + + layer.options.mapprojection + + '][href]' + ); + try { + if (selectedAlternate) { + let url = new URL(selectedAlternate.getAttribute('href'), base) + .href; + layer._layerEl.dispatchEvent( + new CustomEvent('changeprojection', { + detail: { + href: url + } + }) + ); + return true; + } + } catch (error) {} + return false; + } + // initialize layer._properties._mapExtents (and associated/derived/convenience property _templateVars + function processExtents() { + let projectionMatch = + layer._properties.projection === layer.options.mapprojection; + if (projectionMatch) { + layer._properties.crs = M[layer._properties.projection]; + } + let extents = mapml.querySelectorAll('map-extent[units]'); + 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 + ); + // re-write layer.getLayerExtentHTML(label, i) + // as local function createExtentLayerControlHTML(extent) ?? + // rename extentAnatomy to extentLayerControlItem or similar... + extents[j].extentAnatomy = layer.getLayerExtentHTML( + extents[j].getAttribute('label'), + j + ); + layer._properties._mapExtents.push(extents[j]); + // possibly get rid of layer._properties._templateVars, TBD. + layer._properties._templateVars = + layer._properties._templateVars.concat(extents[j]._templateVars); + } + } + } // local functions function thinkOfAGoodName() { var serverExtent = mapml.querySelectorAll('map-extent'), @@ -1556,11 +1655,22 @@ export var MapMLLayer = L.Layer.extend({ projectionMatch ); layer._properties._mapExtents[j]._templateVars = templateVars; + let labelName = + layer._properties._mapExtents[j].getAttribute('label'); + layer._properties._mapExtents[j].extentAnatomy = + layer.getLayerExtentHTML(labelName, j); layer._properties._templateVars = layer._properties._templateVars.concat(templateVars); } } } else { + // TODO simplify the interface to layer._properties (projection string) + // by making it an explicit property + // i.e. layer._properties.projection + // so that getProjection can act as an accessor function that + /// doesn't have to deal with a variety of possible formats for the + // property + //NB this typeof is not working, if (typeof serverMeta === 'string') { // when map-meta projection not present for layer layer._properties = { serverMeta }; @@ -1569,15 +1679,6 @@ export var MapMLLayer = L.Layer.extend({ layer._properties = serverMeta; } } - // add multiple extents - if (layer._properties._mapExtents) { - for (let j = 0; j < layer._properties._mapExtents.length; j++) { - var labelName = - layer._properties._mapExtents[j].getAttribute('label'); - var extentElement = layer.getLayerExtentHTML(labelName, j); - layer._properties._mapExtents[j].extentAnatomy = extentElement; - } - } return false; } function setZoomInOrOutLinks() { @@ -1797,6 +1898,13 @@ export var MapMLLayer = L.Layer.extend({ } } }, + // new getProjection, maybe simpler, but doesn't work... + // getProjection: function () { + // if (!this._properties) { + // return; + // } + // return this._properties.projection; + // } // a layer must share a projection with the map so that all the layers can // be overlayed in one coordinate space. getProjection: function () { diff --git a/test/e2e/core/metaDefault.html b/test/e2e/core/metaDefault.html index 81e240a2c..b548cae2f 100644 --- a/test/e2e/core/metaDefault.html +++ b/test/e2e/core/metaDefault.html @@ -34,7 +34,7 @@ tref="http://maps.geogratis.gc.ca/wms/toporama_en?SERVICE=WMS&REQUEST=GetMap&FORMAT=image/jpeg&TRANSPARENT=FALSE&STYLES=&VERSION=1.3.0&LAYERS=WMS-Toporama&WIDTH={w}&HEIGHT={h}&CRS=EPSG:3978&BBOX={xmin},{ymin},{xmax},{ymax}&m4h=t" >
- + @@ -43,7 +43,7 @@ - + diff --git a/test/e2e/core/metaDefault.test.js b/test/e2e/core/metaDefault.test.js index 7a74ff10b..77bf28e95 100644 --- a/test/e2e/core/metaDefault.test.js +++ b/test/e2e/core/metaDefault.test.js @@ -91,17 +91,15 @@ test.describe('Playwright Missing Min Max Attribute, Meta Default Tests', () => ); }); test("Layer with no map-meta's is rendered on map", async () => { - const viewer = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') + await page.waitForTimeout(200); + const layer = await page.evaluateHandle(() => + document.querySelector('layer-[id=defaultMeta]') ); const layerSVG = await ( await page.evaluateHandle( - (map) => - map.shadowRoot - .querySelectorAll('.mapml-layer')[2] - .querySelector('path') - .getAttribute('d'), - viewer + (layer) => + layer._layer._container.querySelector('path').getAttribute('d'), + layer ) ).jsonValue(); expect(layerSVG).toEqual( @@ -109,17 +107,14 @@ test.describe('Playwright Missing Min Max Attribute, Meta Default Tests', () => ); }); test("Fetched layer with no map-meta's is rendered on map", async () => { - const viewer = await page.evaluateHandle(() => - document.querySelector('mapml-viewer') + const layer = await page.evaluateHandle(() => + document.querySelector('layer-[id=defaultMetaFetched]') ); const layerSVG = await ( await page.evaluateHandle( - (map) => - map.shadowRoot - .querySelectorAll('.mapml-layer')[3] - .querySelector('path') - .getAttribute('d'), - viewer + (layer) => + layer._layer._container.querySelector('path').getAttribute('d'), + layer ) ).jsonValue(); expect(layerSVG).toEqual( diff --git a/test/e2e/layers/featureLayer.html b/test/e2e/layers/featureLayer.html index aaa5095ea..b3e1bf21e 100644 --- a/test/e2e/layers/featureLayer.html +++ b/test/e2e/layers/featureLayer.html @@ -133,8 +133,8 @@

Colorado

title="Canada Base Map © Natural Resources Canada">
- - + + diff --git a/test/e2e/layers/featureLayer.test.js b/test/e2e/layers/featureLayer.test.js index d1fa21bf4..c87b13fbb 100644 --- a/test/e2e/layers/featureLayer.test.js +++ b/test/e2e/layers/featureLayer.test.js @@ -45,8 +45,8 @@ test.describe('Playwright featureLayer (Static Features) Layer Tests', () => { test('Loading in retrieved features', async () => { const features = await page.$eval( - 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(3) > div.leaflet-layer.leaflet-pane.mapml-vector-container > svg > g', - (featureGroups) => featureGroups.childNodes.length + 'layer-#US', + (layer) => layer._layer._container.querySelector('svg').firstChild.childElementCount ); expect(features).toEqual(52); }); @@ -121,12 +121,12 @@ test.describe('Playwright featureLayer (Static Features) Layer Tests', () => { }); test('Feature without properties renders & is not interactable', async () => { const feature = await page.$eval( - 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(4) > div.leaflet-layer.leaflet-pane.mapml-vector-container > svg > g > g > path', - (path) => path.getAttribute('d') + 'layer-#inline', + (layer) => layer._layer._container.querySelector('path').getAttribute('d') ); const classList = await page.$eval( - 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(4) > div.leaflet-layer.leaflet-pane.mapml-vector-container > svg > g > g', - (g) => g.getAttribute('class') + 'layer-#inline', + (layer) => layer._layer._container.querySelector('svg').firstChild.firstChild.getAttribute('class') ); expect(feature).toEqual('M74 -173L330 -173L330 83L74 83L74 -173z'); expect(classList).toBeFalsy(); From 358a540d8221f5f3f68ef55a15d157488adba808 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 27 Jul 2023 22:41:29 -0400 Subject: [PATCH 07/62] Rename getLayerExtentHTML to createLayerControlExtentHTML. Change signature of createLayerControlExtentHTML --- src/mapml/layers/MapMLLayer.js | 48 ++++++++++++++-------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 9bdb3e372..dc6a5f19f 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -564,8 +564,7 @@ export var MapMLLayer = L.Layer.extend({ getAttribution: function () { return this.options.attribution; }, - - getLayerExtentHTML: function (labelName, i) { + createLayerControlExtentHTML: function (mapExtent) { var extent = L.DomUtil.create('fieldset', 'mapml-layer-extent'), extentProperties = L.DomUtil.create( 'div', @@ -603,10 +602,10 @@ export var MapMLLayer = L.Layer.extend({ opacity = L.DomUtil.create('input', '', opacityControl); extentSettings.hidden = true; extent.setAttribute('aria-grabbed', 'false'); - if (!labelName) { + if (!mapExtent.hasAttribute('label')) { // if a label attribute is not present, set it to hidden in layer control extent.setAttribute('hidden', ''); - this._properties._mapExtents[i].hidden = true; + mapExtent.hidden = true; } // append the svg paths @@ -638,13 +637,10 @@ export var MapMLLayer = L.Layer.extend({ (e) => { let allRemoved = true; e.target.checked = false; - this._properties._mapExtents[i].removed = true; - this._properties._mapExtents[i].checked = false; - if (this._layerEl.checked) - this._changeExtent(e, this._properties._mapExtents[i]); - this._properties._mapExtents[i].extentAnatomy.parentNode.removeChild( - this._properties._mapExtents[i].extentAnatomy - ); + 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; } @@ -693,35 +689,30 @@ export var MapMLLayer = L.Layer.extend({ 'aria-labelledby', 'mapml-layer-item-opacity-' + L.stamp(extentOpacitySummary) ); - let opacityValue = this._properties._mapExtents[i].hasAttribute('opacity') - ? this._properties._mapExtents[i].getAttribute('opacity') + let opacityValue = mapExtent.hasAttribute('opacity') + ? mapExtent.getAttribute('opacity') : '1.0'; - this._properties._mapExtents[i]._templateVars.opacity = opacityValue; + mapExtent._templateVars.opacity = opacityValue; opacity.setAttribute('value', opacityValue); opacity.value = opacityValue; - L.DomEvent.on( - opacity, - 'change', - this._changeExtentOpacity, - this._properties._mapExtents[i] - ); + L.DomEvent.on(opacity, 'change', this._changeExtentOpacity, mapExtent); var extentItemNameSpan = L.DomUtil.create( 'span', 'mapml-layer-item-name', extentLabel ); - input.defaultChecked = this._properties._mapExtents[i] ? true : false; - this._properties._mapExtents[i].checked = input.defaultChecked; + input.defaultChecked = mapExtent ? true : false; + mapExtent.checked = input.defaultChecked; input.type = 'checkbox'; - extentItemNameSpan.innerHTML = labelName; + extentItemNameSpan.innerHTML = mapExtent.getAttribute('label'); L.DomEvent.on(input, 'change', (e) => { - this._changeExtent(e, this._properties._mapExtents[i]); + this._changeExtent(e, mapExtent); }); extentItemNameSpan.id = 'mapml-extent-item-name-{' + L.stamp(extentItemNameSpan) + '}'; extent.setAttribute('aria-labelledby', extentItemNameSpan.id); - extentItemNameSpan.extent = this._properties._mapExtents[i]; + extentItemNameSpan.extent = mapExtent; extent.ontouchstart = extent.onmousedown = (downEvent) => { if ( @@ -1549,12 +1540,11 @@ export var MapMLLayer = L.Layer.extend({ // re-write layer.getLayerExtentHTML(label, i) // as local function createExtentLayerControlHTML(extent) ?? // rename extentAnatomy to extentLayerControlItem or similar... - extents[j].extentAnatomy = layer.getLayerExtentHTML( - extents[j].getAttribute('label'), - j + extents[j].extentAnatomy = layer.createLayerControlExtentHTML( + extents[j] ); layer._properties._mapExtents.push(extents[j]); - // possibly get rid of layer._properties._templateVars, TBD. + // get rid of layer._properties._templateVars, TBD. layer._properties._templateVars = layer._properties._templateVars.concat(extents[j]._templateVars); } From f1b6c55920214fce7d4cd093bc5a1ddbd0a8b282 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 28 Jul 2023 10:18:48 -0400 Subject: [PATCH 08/62] Get map extents working, remove _initialize call from map-feature. --- src/map-feature.js | 10 ---- src/mapml/layers/MapMLLayer.js | 94 ++++++++++++++++++---------------- 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/src/map-feature.js b/src/map-feature.js index 5cb5ee0c1..f194a310a 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -200,16 +200,6 @@ export class MapFeature extends HTMLElement { // if vector layer has not yet created (i.e. the layer- is not yet rendered on the map / layer is empty) let layerEl = this._layer._layerEl; this._layer.once('add', this._setUpEvents, this); - if ( - !layerEl.querySelector('map-extent, map-tile') && - !layerEl.hasAttribute('src') && - layerEl.querySelectorAll('map-feature').length === 1 - ) { - // if the map-feature is added to an empty layer, fire extentload to create vector layer - // must re-run _initialize of MapMLLayer.js to re-set layer._properties (layer._properties is null for an empty layer) - this._layer._initialize(layerEl); - this._layer.fire('extentload'); - } return; } else if (!this._featureGroup) { // if the map-feature el or its subtree is updated diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index dc6a5f19f..ab89105e2 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1462,13 +1462,14 @@ export var MapMLLayer = L.Layer.extend({ // the dependencies on convoluted getProjection() interface, but doesn't quite // succeed, yet. function determineLayerProjection() { - layer._properties.projection = FALLBACK_PROJECTION; + let projection = layer.options.mapprojection || FALLBACK_PROJECTION; if (mapml.querySelector('map-meta[name=projection][content]')) { - layer._propertes.projection = M._metaContentToObject( - mapml - .querySelector('map-meta[name=projection]') - .getAttribute('content') - ).content.toUpperCase(); + projection = + M._metaContentToObject( + mapml + .querySelector('map-meta[name=projection]') + .getAttribute('content') + ).content.toUpperCase() || projection; } else if (mapml.querySelector('map-extent[units]')) { const getProjectionFrom = (extents) => { const projectionMatches = (extent) => { @@ -1480,9 +1481,16 @@ export var MapMLLayer = L.Layer.extend({ return layer.options.mapprojection; } }; - layer._properties.projection = getProjectionFrom( - Array.from(mapml.querySelectorAll('map-extent[units]')) - ); + projection = + getProjectionFrom( + Array.from(mapml.querySelectorAll('map-extent[units]')) + ) || projection; + } + layer._properties.projection = projection; + let projectionMatch = + layer._properties.projection === layer.options.mapprojection; + if (projectionMatch) { + layer._properties.crs = M[layer._properties.projection]; } } // determine if, where there's no match of the current layer's projection @@ -1516,10 +1524,10 @@ export var MapMLLayer = L.Layer.extend({ function processExtents() { let projectionMatch = layer._properties.projection === layer.options.mapprojection; - if (projectionMatch) { - layer._properties.crs = M[layer._properties.projection]; - } 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++) { @@ -1889,42 +1897,42 @@ export var MapMLLayer = L.Layer.extend({ } }, // new getProjection, maybe simpler, but doesn't work... - // getProjection: function () { - // if (!this._properties) { - // return; - // } - // return this._properties.projection; - // } - // a layer must share a projection with the map so that all the layers can - // be overlayed in one coordinate space. getProjection: function () { if (!this._properties) { return; } - let extent = this._properties._mapExtents - ? this._properties._mapExtents[0] - : this._properties; // the projections for each extent eould be the same (as) validated in _validProjection, so can use mapExtents[0] - if (extent.serverMeta) return extent.serverMeta; - switch (extent.tagName.toUpperCase()) { - case 'MAP-EXTENT': - if (extent.hasAttribute('units')) - return extent.getAttribute('units').toUpperCase(); - break; - case 'MAP-INPUT': - if (extent.hasAttribute('value')) - return extent.getAttribute('value').toUpperCase(); - break; - case 'MAP-META': - if (extent.hasAttribute('content')) - return M._metaContentToObject( - extent.getAttribute('content') - ).content.toUpperCase(); - break; - default: - return FALLBACK_PROJECTION; - } - return FALLBACK_PROJECTION; + return this._properties.projection; }, + // a layer must share a projection with the map so that all the layers can + // be overlayed in one coordinate space. + // getProjection: function () { + // if (!this._properties) { + // return; + // } + // let extent = this._properties._mapExtents + // ? this._properties._mapExtents[0] + // : this._properties; // the projections for each extent eould be the same (as) validated in _validProjection, so can use mapExtents[0] + // if (extent.serverMeta) return extent.serverMeta; + // switch (extent.tagName.toUpperCase()) { + // case 'MAP-EXTENT': + // if (extent.hasAttribute('units')) + // return extent.getAttribute('units').toUpperCase(); + // break; + // case 'MAP-INPUT': + // if (extent.hasAttribute('value')) + // return extent.getAttribute('value').toUpperCase(); + // break; + // case 'MAP-META': + // if (extent.hasAttribute('content')) + // return M._metaContentToObject( + // extent.getAttribute('content') + // ).content.toUpperCase(); + // break; + // default: + // return FALLBACK_PROJECTION; + // } + // return FALLBACK_PROJECTION; + // }, getQueryTemplates: function (pcrsClick) { if (this._properties && this._properties._queries) { var templates = []; From 64d7ce86621426ebf5b30038d62c0bc402b30a55 Mon Sep 17 00:00:00 2001 From: Aliyan Haq <55751566+AliyanH@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:05:14 -0400 Subject: [PATCH 09/62] Fix determineLayerProjection function + prettier formatting Fix broken test mismatchedLayerWithMap test by running _validateDisabled on extentload. --- src/layer.js | 1 + src/mapml/layers/MapMLLayer.js | 20 ++++++++++---------- test/e2e/layers/featureLayer.test.js | 16 +++++++++------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/layer.js b/src/layer.js index 2b9756e3f..b0b9ff52b 100644 --- a/src/layer.js +++ b/src/layer.js @@ -145,6 +145,7 @@ export class MapLayer extends HTMLElement { if (this._layerControl && !this.hidden) { this._layerControl.addOrUpdateOverlay(this._layer, this.label); } + this._validateDisabled(); }) .catch((e) => { this.dispatchEvent( diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index ab89105e2..2cbafda72 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1472,24 +1472,24 @@ export var MapMLLayer = L.Layer.extend({ ).content.toUpperCase() || projection; } else if (mapml.querySelector('map-extent[units]')) { const getProjectionFrom = (extents) => { - const projectionMatches = (extent) => { - return ( - extent.attributes.units.value === layer.options.mapprojection - ); - }; - if (extents.every(projectionMatches)) { - return layer.options.mapprojection; + let extentProj = extents[0].attributes.units.value; + let isMatch = true; + for (let i = 0; i < extents.length; i++) { + if (extentProj !== extents[i].attributes.units.value) { + isMatch = false; + } } + return isMatch ? extentProj : null; }; projection = getProjectionFrom( Array.from(mapml.querySelectorAll('map-extent[units]')) ) || projection; + } else { + // Warn when no map-meta[name=projection] or map-extent[units] are present, as the map's projection is being used. } layer._properties.projection = projection; - let projectionMatch = - layer._properties.projection === layer.options.mapprojection; - if (projectionMatch) { + if (layer._properties.projection === layer.options.mapprojection) { layer._properties.crs = M[layer._properties.projection]; } } diff --git a/test/e2e/layers/featureLayer.test.js b/test/e2e/layers/featureLayer.test.js index c87b13fbb..04bbbfc17 100644 --- a/test/e2e/layers/featureLayer.test.js +++ b/test/e2e/layers/featureLayer.test.js @@ -46,7 +46,9 @@ test.describe('Playwright featureLayer (Static Features) Layer Tests', () => { test('Loading in retrieved features', async () => { const features = await page.$eval( 'layer-#US', - (layer) => layer._layer._container.querySelector('svg').firstChild.childElementCount + (layer) => + layer._layer._container.querySelector('svg').firstChild + .childElementCount ); expect(features).toEqual(52); }); @@ -120,13 +122,13 @@ test.describe('Playwright featureLayer (Static Features) Layer Tests', () => { await context.close(); }); test('Feature without properties renders & is not interactable', async () => { - const feature = await page.$eval( - 'layer-#inline', - (layer) => layer._layer._container.querySelector('path').getAttribute('d') + const feature = await page.$eval('layer-#inline', (layer) => + layer._layer._container.querySelector('path').getAttribute('d') ); - const classList = await page.$eval( - 'layer-#inline', - (layer) => layer._layer._container.querySelector('svg').firstChild.firstChild.getAttribute('class') + const classList = await page.$eval('layer-#inline', (layer) => + layer._layer._container + .querySelector('svg') + .firstChild.firstChild.getAttribute('class') ); expect(feature).toEqual('M74 -173L330 -173L330 83L74 83L74 -173z'); expect(classList).toBeFalsy(); From 08996cdacf0ec8ba303fcd784d1c78340963f5b4 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 28 Jul 2023 16:42:55 -0400 Subject: [PATCH 10/62] Reorganize some top level private member functions into scoped local functions for use in / by _initialize / _processContent --- src/mapml/layers/MapMLLayer.js | 1080 ++++++++++++++------------------ 1 file changed, 475 insertions(+), 605 deletions(-) diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 2cbafda72..6ec568a16 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -564,255 +564,6 @@ export var MapMLLayer = L.Layer.extend({ getAttribution: function () { return this.options.attribution; }, - createLayerControlExtentHTML: function (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; - }, getLayerUserControlsHTML: function () { return this._mapmlLayerItem ? this._mapmlLayerItem @@ -1189,223 +940,6 @@ export var MapMLLayer = L.Layer.extend({ console.log(`HTTP error! Status: ${response.message}`); }); } - 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 _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 _processContent(content, local) { var mapml = !local ? new DOMParser().parseFromString(content, 'text/xml') @@ -1458,11 +992,12 @@ export var MapMLLayer = L.Layer.extend({ layer._layerEl.dispatchEvent( new CustomEvent('extentload', { detail: layer }) ); + // local functions // sets layer._properties.projection. Supposed to replace / simplify // the dependencies on convoluted getProjection() interface, but doesn't quite // succeed, yet. function determineLayerProjection() { - let projection = layer.options.mapprojection || FALLBACK_PROJECTION; + let projection = layer.options.mapprojection; if (mapml.querySelector('map-meta[name=projection][content]')) { projection = M._metaContentToObject( @@ -1486,7 +1021,9 @@ export var MapMLLayer = L.Layer.extend({ Array.from(mapml.querySelectorAll('map-extent[units]')) ) || projection; } else { - // Warn when no map-meta[name=projection] or map-extent[units] are present, as the map's projection is being used. + console.log( + `A projection was not assigned to the '${layer._layerEl.label}' Layer. Please specify a projection for that layer using a map-meta element. See more here - https://maps4html.org/web-map-doc/docs/elements/meta/` + ); } layer._properties.projection = projection; if (layer._properties.projection === layer.options.mapprojection) { @@ -1545,10 +1082,8 @@ export var MapMLLayer = L.Layer.extend({ base, projectionMatch ); - // re-write layer.getLayerExtentHTML(label, i) - // as local function createExtentLayerControlHTML(extent) ?? - // rename extentAnatomy to extentLayerControlItem or similar... - extents[j].extentAnatomy = layer.createLayerControlExtentHTML( + extents[j].extentAnatomy = createLayerControlExtentHTML.call( + layer, extents[j] ); layer._properties._mapExtents.push(extents[j]); @@ -1558,126 +1093,491 @@ export var MapMLLayer = L.Layer.extend({ } } } - // local functions - function thinkOfAGoodName() { - var serverExtent = mapml.querySelectorAll('map-extent'), - projection, - projectionMatch, - serverMeta; - - if (!serverExtent.length) { - serverMeta = mapml.querySelector('map-meta[name=projection]'); + 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; } - // check whether all map-extent elements have the same units - if (serverExtent.length >= 1) { - for (let i = 0; i < serverExtent.length; i++) { - if ( - serverExtent[i].tagName.toLowerCase() === 'map-extent' && - serverExtent[i].hasAttribute('units') - ) { - projection = serverExtent[i].getAttribute('units'); + // 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; } - projectionMatch = - projection && projection === layer.options.mapprojection; - if (!projectionMatch) { - break; + 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; } - } - } else if (serverMeta) { + }, + 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 ( - serverMeta.tagName.toLowerCase() === 'map-meta' && - serverMeta.hasAttribute('content') + (downEvent.target.parentElement.tagName.toLowerCase() === 'label' && + downEvent.target.tagName.toLowerCase() !== 'input') || + downEvent.target.tagName.toLowerCase() === 'label' ) { - projection = M._metaContentToObject( - serverMeta.getAttribute('content') - ).content; - projectionMatch = - projection && projection === layer.options.mapprojection; + 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; + }; } - } else { - // default projection set to parent projection when no map-meta projection element present - projection = layer.options.mapprojection; - projectionMatch = true; - serverMeta = projection; - console.log( - `A projection was not assigned to the '${layer._title}' Layer. Please specify a projection for that layer using a map-meta element. See more here - https://maps4html.org/web-map-doc/docs/elements/meta/` + }; + 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 ); - // TODO: Add a more obvious warning. + } 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; } - var metaExtent = mapml.querySelector('map-meta[name=extent]'), - selectedAlternate = - !projectionMatch && - mapml.querySelector( - 'map-head map-link[rel=alternate][projection=' + - layer.options.mapprojection + - ']' - ); + 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')}}`; + } + } - if ( - !projectionMatch && - selectedAlternate && - selectedAlternate.hasAttribute('href') - ) { - layer._layerEl.dispatchEvent( - new CustomEvent('changeprojection', { - detail: { - href: new URL(selectedAlternate.getAttribute('href'), base).href + 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)]); } - }) - ); - return true; - } else if (!serverMeta) { - if (projectionMatch) { - layer._properties.crs = M[projection]; - } - 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 < serverExtent.length; j++) { - if ( - serverExtent[j].querySelector( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ) && - serverExtent[j].hasAttribute('units') - ) { - layer._properties._mapExtents.push(serverExtent[j]); - projectionMatch = projectionMatch || selectedAlternate; - let templateVars = _initTemplateVars.call( - layer, - serverExtent[j], - metaExtent, - projection, - mapml, - base, - projectionMatch + + 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' ); - layer._properties._mapExtents[j]._templateVars = templateVars; - let labelName = - layer._properties._mapExtents[j].getAttribute('label'); - layer._properties._mapExtents[j].extentAnatomy = - layer.getLayerExtentHTML(labelName, j); - layer._properties._templateVars = - layer._properties._templateVars.concat(templateVars); + // no match found, template won't be used + break; } } - } else { - // TODO simplify the interface to layer._properties (projection string) - // by making it an explicit property - // i.e. layer._properties.projection - // so that getProjection can act as an accessor function that - /// doesn't have to deal with a variety of possible formats for the - // property - //NB this typeof is not working, - if (typeof serverMeta === 'string') { - // when map-meta projection not present for layer - layer._properties = { serverMeta }; - } else { - // when map-meta projection present for layer - layer._properties = serverMeta; + 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 false; + 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]'), @@ -1903,36 +1803,6 @@ export var MapMLLayer = L.Layer.extend({ } return this._properties.projection; }, - // a layer must share a projection with the map so that all the layers can - // be overlayed in one coordinate space. - // getProjection: function () { - // if (!this._properties) { - // return; - // } - // let extent = this._properties._mapExtents - // ? this._properties._mapExtents[0] - // : this._properties; // the projections for each extent eould be the same (as) validated in _validProjection, so can use mapExtents[0] - // if (extent.serverMeta) return extent.serverMeta; - // switch (extent.tagName.toUpperCase()) { - // case 'MAP-EXTENT': - // if (extent.hasAttribute('units')) - // return extent.getAttribute('units').toUpperCase(); - // break; - // case 'MAP-INPUT': - // if (extent.hasAttribute('value')) - // return extent.getAttribute('value').toUpperCase(); - // break; - // case 'MAP-META': - // if (extent.hasAttribute('content')) - // return M._metaContentToObject( - // extent.getAttribute('content') - // ).content.toUpperCase(); - // break; - // default: - // return FALLBACK_PROJECTION; - // } - // return FALLBACK_PROJECTION; - // }, getQueryTemplates: function (pcrsClick) { if (this._properties && this._properties._queries) { var templates = []; From 58889ba08a7fe4da3633ffca8fe9baee3f38f755 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 1 Aug 2023 09:52:09 -0400 Subject: [PATCH 11/62] Use promise rejection for handling events like changeprojection that happen during initialization. --- src/layer.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/layer.js b/src/layer.js index b0b9ff52b..990fec164 100644 --- a/src/layer.js +++ b/src/layer.js @@ -103,7 +103,7 @@ export class MapLayer extends HTMLElement { (event) => { event.stopPropagation(); if (event.detail.error) { - reject(); + reject(event.detail.error); } else { resolve(); } @@ -122,7 +122,7 @@ export class MapLayer extends HTMLElement { 'changeprojection', function (e) { e.stopPropagation(); - this.src = e.detail.href; + reject(e); }, { once: true } ); @@ -148,9 +148,14 @@ export class MapLayer extends HTMLElement { this._validateDisabled(); }) .catch((e) => { - this.dispatchEvent( - new CustomEvent('error', { detail: { target: this } }) - ); + console.log('Entering catch with e=' + e); + if (e.type === 'changeprojection') { + this.src = e.detail.href; + } else { + this.dispatchEvent( + new CustomEvent('error', { detail: { target: this } }) + ); + } }); } From cde6b6b4276c50d92ad74b34e3489876adb05a15 Mon Sep 17 00:00:00 2001 From: AliyanH Date: Mon, 31 Jul 2023 23:38:08 -0400 Subject: [PATCH 12/62] fix ZIndex mismatch bug (TODO for ordering of map-extent) + fix broken test Essentially what we did before lunch / late Wednesday --- src/layer.js | 181 ++++++++++------------- src/mapml-viewer.js | 162 +++++++++++--------- src/mapml/layers/MapMLLayer.js | 26 ++-- test/e2e/mapml-viewer/customTCRS.test.js | 2 +- 4 files changed, 190 insertions(+), 181 deletions(-) diff --git a/src/layer.js b/src/layer.js index 990fec164..6131b0941 100644 --- a/src/layer.js +++ b/src/layer.js @@ -74,14 +74,18 @@ export class MapLayer extends HTMLElement { } _onRemove() { - this._removeEvents(); - if (this._layer._map) { + if (this._layer) { + this._layer.off(); + } + // if this layer has never been connected, it will not have a _layer + if (this._layer && this._layer._map) { this._layer._map.removeLayer(this._layer); } if (this._layerControl && !this.hidden) { this._layerControl.removeLayer(this._layer); } + delete this._layer; if (this.shadowRoot) { this.shadowRoot.innerHTML = ''; @@ -90,7 +94,8 @@ export class MapLayer extends HTMLElement { connectedCallback() { if (this.hasAttribute('data-moving')) return; - this._onAdd(); + const doConnected = this._onAdd.bind(this); + this.parentElement.addEventListener('load', doConnected, { once: true }); } _onAdd() { @@ -134,17 +139,15 @@ export class MapLayer extends HTMLElement { this.src ? new URL(this.src, base).href : null, this, { - mapprojection: this.parentElement._map.options.projection, + mapprojection: this.parentElement.projection, opacity: opacity_value } ); + console.log(this._layer); }) .then(() => { - this._onLayerExtentLoad(); + console.log(this._layer); this._attachedToMap(); - if (this._layerControl && !this.hidden) { - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } this._validateDisabled(); }) .catch((e) => { @@ -158,6 +161,74 @@ export class MapLayer extends HTMLElement { } }); } + _attachedToMap() { + // set i to the position of this layer element in the set of layers + var i = 0, + position = 1; + for (var nodes = this.parentNode.children; i < nodes.length; i++) { + if (this.parentNode.children[i].nodeName === 'LAYER-') { + if (this.parentNode.children[i] === this) { + position = i + 1; + } else if (this.parentNode.children[i]._layer) { + this.parentNode.children[i]._layer.setZIndex(i + 1); + } + } + } + var proj = this.parentNode.projection + ? this.parentNode.projection + : 'OSMTILE'; + L.setOptions(this._layer, { + zIndex: position, + mapprojection: proj, + opacity: window.getComputedStyle(this).opacity + }); + // 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 extentload', this._validateDisabled, this); + // toggle the this.disabled attribute depending on whether the layer + // is: same prj as map, within view/zoom of map + this._layer._map.on('moveend', this._validateDisabled, this); + + // if controls option is enabled, insert the layer into the overlays array + if (this.parentNode._layerControl && !this.hidden) { + this._layerControl = this.parentNode._layerControl; + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + } + + // the mapml document associated to this layer can in theory contain many + // link[@rel=legend] elements with different @type or other attributes; + // currently only support a single link, don't care about type, lang etc. + // TODO: add support for full LayerLegend object, and > one link. + if (this._layer._legendUrl) { + this.legendLinks = [ + { + type: 'application/octet-stream', + href: this._layer._legendUrl, + rel: 'legend', + lang: null, + hreflang: null, + sizes: null + } + ]; + } + // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied + // to MapML extent as metadata + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event + this.dispatchEvent( + new CustomEvent('loadedmetadata', { detail: { target: this } }) + ); + } adoptedCallback() { // console.log('Custom map element moved to new page.'); @@ -215,43 +286,6 @@ export class MapLayer extends HTMLElement { } this.opacity = oldOpacity; } - _onLayerExtentLoad(e) { - // the mapml document associated to this layer can in theory contain many - // link[@rel=legend] elements with different @type or other attributes; - // currently only support a single link, don't care about type, lang etc. - // TODO: add support for full LayerLegend object, and > one link. - if (this._layer._legendUrl) { - this.legendLinks = [ - { - type: 'application/octet-stream', - href: this._layer._legendUrl, - rel: 'legend', - lang: null, - hreflang: null, - sizes: null - } - ]; - } - if (this._layer._title) { - this.label = this._layer._title; - } - // make sure local content layer has the chance to set its extent properly - // which is important for the layer control and the disabled property - if (this._layer._map) { - this._layer.fire('attached', this._layer); - } - // TODO ensure the controls in this._layerControl contain 'live' controls - // which control the layer, not potentially the previous style / src - if (this._layerControl) { - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } - // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied - // to MapML extent as metadata - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event - this.dispatchEvent( - new CustomEvent('loadedmetadata', { detail: { target: this } }) - ); - } _validateDisabled() { setTimeout(() => { let layer = this._layer, @@ -374,63 +408,6 @@ export class MapLayer extends HTMLElement { this.checked = this._layer._map.hasLayer(this._layer); } } - _attachedToMap() { - // set i to the position of this layer element in the set of layers - var i = 0, - position = 1; - for (var nodes = this.parentNode.children; i < nodes.length; i++) { - if (this.parentNode.children[i].nodeName === 'LAYER-') { - if (this.parentNode.children[i] === this) { - position = i + 1; - } else if (this.parentNode.children[i]._layer) { - this.parentNode.children[i]._layer.setZIndex(i + 1); - } - } - } - var proj = this.parentNode.projection - ? this.parentNode.projection - : 'OSMTILE'; - L.setOptions(this._layer, { - zIndex: position, - mapprojection: proj, - opacity: window.getComputedStyle(this).opacity - }); - // 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 extentload', this._validateDisabled, this); - - // if controls option is enabled, insert the layer into the overlays array - if (this.parentNode._layerControl && !this.hidden) { - this._layerControl = this.parentNode._layerControl; - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - } - // toggle the this.disabled attribute depending on whether the layer - // is: same prj as map, within view/zoom of map - this._layer._map.on('moveend', this._validateDisabled, this); - this._layer._map.on('checkdisabled', this._validateDisabled, this); - // this is necessary to get the layer control to compare the layer - // extents with the map extent & zoom, but it needs to be rethought TODO - // for one thing, layers which are checked by the author before - // adding to the map are displayed despite that they are not visible - // See issue #26 - // this._layer._map.fire('moveend'); - } - _removeEvents() { - if (this._layer) { - this._layer.off(); - } - } zoomTo() { if (!this.extent) return; let map = this._layer._map, diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 599a1b42d..463eb5bd9 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -74,12 +74,12 @@ export class MapViewer extends HTMLElement { } } get projection() { - return this.hasAttribute('projection') && M[this.getAttribute('projection')] + return this.hasAttribute('projection') ? this.getAttribute('projection') : 'OSMTILE'; } set projection(val) { - if (val && M[val]) { + if (val) { this.setAttribute('projection', val); } else throw new Error('Undefined Projection'); } @@ -131,79 +131,88 @@ export class MapViewer extends HTMLElement { this._traversalCall = false; } connectedCallback() { - this._initShadowRoot(); + this.whenProjectionDefined(this.projection) + .then(() => { + this._initShadowRoot(); - this._controlsList = new DOMTokenList( - this.getAttribute('controlslist'), - this, - 'controlslist', - [ - 'noreload', - 'nofullscreen', - 'nozoom', - 'nolayer', - 'noscale', - 'geolocation' - ] - ); + this._controlsList = new DOMTokenList( + this.getAttribute('controlslist'), + this, + 'controlslist', + [ + 'noreload', + 'nofullscreen', + 'nozoom', + 'nolayer', + 'noscale', + 'geolocation' + ] + ); - var s = window.getComputedStyle(this), - wpx = s.width, - hpx = s.height, - w = this.hasAttribute('width') - ? this.getAttribute('width') - : parseInt(wpx.replace('px', '')), - h = this.hasAttribute('height') - ? this.getAttribute('height') - : parseInt(hpx.replace('px', '')); - this._changeWidth(w); - this._changeHeight(h); + var s = window.getComputedStyle(this), + wpx = s.width, + hpx = s.height, + w = this.hasAttribute('width') + ? this.getAttribute('width') + : parseInt(wpx.replace('px', '')), + h = this.hasAttribute('height') + ? this.getAttribute('height') + : parseInt(hpx.replace('px', '')); + this._changeWidth(w); + this._changeHeight(h); - // wait for createmap event before creating leaflet map - // this allows a safeguard for the case where loading a custom TCRS takes - // longer than loading mapml-viewer.js/web-map.js - // the REASON we need a synchronous event listener (see comment below) - // is because the mapml-viewer element has / can have a size of 0 up until after - // something that happens between this point and the event handler executing - // perhaps a browser rendering cycle?? + // wait for createmap event before creating leaflet map + // this allows a safeguard for the case where loading a custom TCRS takes + // longer than loading mapml-viewer.js/web-map.js + // the REASON we need a synchronous event listener (see comment below) + // is because the mapml-viewer element has / can have a size of 0 up until after + // something that happens between this point and the event handler executing + // perhaps a browser rendering cycle?? - let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( - this.projection - ); - // this is worth a read, because dispatchEvent is synchronous - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent - // In particular: - // "All applicable event handlers are called and return before dispatchEvent() returns." - this._createMap(); + let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( + this.projection + ); + // this is worth a read, because dispatchEvent is synchronous + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent + // In particular: + // "All applicable event handlers are called and return before dispatchEvent() returns." + this._createMap(); - // https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274 - this.setAttribute('role', 'application'); - this._toggleStatic(); + // https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274 + this.setAttribute('role', 'application'); + this._toggleStatic(); - /* - 1. only deletes aria-label when the last (only remaining) map caption is removed - 2. only deletes aria-label if the aria-label was defined by the map caption element itself - */ + /* + 1. only deletes aria-label when the last (only remaining) map caption is removed + 2. only deletes aria-label if the aria-label was defined by the map caption element itself + */ - let mapcaption = this.querySelector('map-caption'); + let mapcaption = this.querySelector('map-caption'); - if (mapcaption !== null) { - setTimeout(() => { - let ariaupdate = this.getAttribute('aria-label'); + if (mapcaption !== null) { + setTimeout(() => { + let ariaupdate = this.getAttribute('aria-label'); - if (ariaupdate === mapcaption.innerHTML) { - this.mapCaptionObserver = new MutationObserver((m) => { - let mapcaptionupdate = this.querySelector('map-caption'); - if (mapcaptionupdate !== mapcaption) { - this.removeAttribute('aria-label'); + if (ariaupdate === mapcaption.innerHTML) { + this.mapCaptionObserver = new MutationObserver((m) => { + let mapcaptionupdate = this.querySelector('map-caption'); + if (mapcaptionupdate !== mapcaption) { + this.removeAttribute('aria-label'); + } + }); + this.mapCaptionObserver.observe(this, { + childList: true + }); } - }); - this.mapCaptionObserver.observe(this, { - childList: true - }); + }, 0); } - }, 0); - } + this.dispatchEvent( + new CustomEvent('load', { detail: { target: this } }) + ); + }) + .catch(() => { + throw new Error('Projection not defined'); + }); } _initShadowRoot() { if (!this.shadowRoot) { @@ -361,7 +370,10 @@ export class MapViewer extends HTMLElement { this.appendChild(reAttach); } if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - this.zoomTo(this.lat, this.lon, this.zoom); + this.dispatchEvent( + new CustomEvent('load', { detail: { target: this } }) + ); + //this.zoomTo(this.lat, this.lon, this.zoom); //this.dispatchEvent(new CustomEvent('projectionchange')); } } @@ -1292,7 +1304,25 @@ export class MapViewer extends HTMLElement { M[t.projection.toUpperCase()] = M[t.projection]; //adds the projection uppercase to global M return t.projection; } - + async whenProjectionDefined(projection) { + return new Promise((resolve, reject) => { + if (M[projection]) { + resolve(); + } + const interval = setInterval(testForProjection, 300, projection); + function testForProjection() { + if (M[projection]) { + clearInterval(interval); + resolve(); + } + } + function removeTestForProjection() { + clearInterval(interval); + reject(); + } + setTimeout(removeTestForProjection, 10000); + }); + } geojson2mapml(json, options = {}) { if (options.projection === undefined) { options.projection = this.projection; diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 6ec568a16..918c8a0ab 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -50,8 +50,6 @@ export var MapMLLayer = L.Layer.extend({ // hit the service to determine what its extent might be // OR use the extent of the content provided - if (!mapml && content && content.hasAttribute('label')) - this._title = content.getAttribute('label'); this._initialize(mapml ? content : null); // a default extent can't be correctly set without the map to provide @@ -59,7 +57,7 @@ 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); + // 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. @@ -819,14 +817,20 @@ export var MapMLLayer = L.Layer.extend({ 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'); - - layerEl._layer.setZIndex(zIndex); + } + // 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'); @@ -969,24 +973,22 @@ export var MapMLLayer = L.Layer.extend({ determineLayerProjection(); // requires that layer._properties.projection be set if (selectMatchingAlternateProjection()) return; - // sets layer._properties._mapExtents and layer._properties._templateVars, if applicable - processExtents(); - // if (thinkOfAGoodName()) return; + // set layer._properties._mapExtents and layer._properties._templateVars + if (layer._properties.crs) processExtents(); layer._styles = getAlternateStyles(); setLayerTitle(); parseLicenseAndLegend(); setZoomInOrOutLinks(); processTiles(); M._parseStylesheetAsHTML(mapml, base, layer._container); - layer._styles = getAlternateStyles(); - layer._validateExtent(); + // layer._validateExtent(); copyRemoteContentToShadowRoot(); // update controls if needed based on mapml-viewer controls/controlslist attribute if (layer._layerEl.parentElement) { // if layer does not have a parent Element, do not need to set Controls layer._layerEl.parentElement._toggleControls(); } - layer.fire('extentload', layer, false); + // layer.fire('extentload', layer, false); // need this to enable processing by the element connectedCallback // processing layer._layerEl.dispatchEvent( diff --git a/test/e2e/mapml-viewer/customTCRS.test.js b/test/e2e/mapml-viewer/customTCRS.test.js index cf0d4a06c..85eee2c55 100644 --- a/test/e2e/mapml-viewer/customTCRS.test.js +++ b/test/e2e/mapml-viewer/customTCRS.test.js @@ -28,7 +28,7 @@ test.describe('Playwright Custom TCRS Tests', () => { ); const tilesLoaded = await page.$eval( - 'xpath=//html/body/mapml-viewer[1] >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-static-tile-layer > div', + 'body > mapml-viewer:nth-child(1) >> .mapml-static-tile-layer > div', (tileGroup) => tileGroup.getElementsByTagName('map-tile').length ); From 1c778a7d494ea60df6d65d7bf40324c4d30d0065 Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 4 Aug 2023 16:16:37 -0400 Subject: [PATCH 13/62] Get rid of event-based resolution of layer loading by refactor of MapMLLayer per below Refactor MapMLLayer and layer.js so that fetching is done by layer.js Temporarily rename MapMLLayer 'extentload' event to 'foo', so as to avoid collision with 'extentload' event --- src/layer.js | 67 +++++++++++++++--------- src/mapml/control/LayerControl.js | 4 +- src/mapml/layers/MapMLLayer.js | 85 +++++++------------------------ src/mapml/utils/Util.js | 4 +- 4 files changed, 65 insertions(+), 95 deletions(-) diff --git a/src/layer.js b/src/layer.js index 6131b0941..975ff356f 100644 --- a/src/layer.js +++ b/src/layer.js @@ -103,18 +103,6 @@ export class MapLayer extends HTMLElement { this.attachShadow({ mode: 'open' }); } new Promise((resolve, reject) => { - this.addEventListener( - 'extentload', - (event) => { - event.stopPropagation(); - if (event.detail.error) { - reject(event.detail.error); - } else { - resolve(); - } - }, - { once: true } - ); this.addEventListener( 'changestyle', function (e) { @@ -135,18 +123,54 @@ export class MapLayer extends HTMLElement { let opacity_value = this.hasAttribute('opacity') ? this.getAttribute('opacity') : '1.0'; - this._layer = M.mapMLLayer( - this.src ? new URL(this.src, base).href : null, - this, - { + + const headers = new Headers(); + headers.append('Accept', 'text/mapml'); + if (this.src) { + fetch(this.src, { headers: headers }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.text(); + }) + .then((mapml) => { + let content = new DOMParser().parseFromString(mapml, 'text/xml'); + if ( + content.querySelector('parsererror') || + !content.querySelector('mapml-') + ) { + throw new Error('Parser error'); + } + if (this._layer) { + this._onRemove(); + } + this._layer = M.mapMLLayer( + new URL(this.src, base).href, + this, + content, + { + mapprojection: this.parentElement.projection, + opacity: opacity_value + } + ); + resolve(); + }) + .catch((error) => { + console.log('Error fetching layer content' + error); + }); + } else { + if (this._layer) { + this._onRemove(); + } + this._layer = M.mapMLLayer(null, this, null, { mapprojection: this.parentElement.projection, opacity: opacity_value - } - ); - console.log(this._layer); + }); + resolve(); + } }) .then(() => { - console.log(this._layer); this._attachedToMap(); this._validateDisabled(); }) @@ -230,9 +254,6 @@ export class MapLayer extends HTMLElement { ); } - adoptedCallback() { - // console.log('Custom map element moved to new page.'); - } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'label': diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index 738a96c16..acd1322cf 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -63,7 +63,7 @@ export var LayerControl = L.Control.Layers.extend({ // on the map it does not generate layer events for (var i = 0; i < this._layers.length; i++) { this._layers[i].layer.off('add remove', this._onLayerChange, this); - this._layers[i].layer.off('extentload', this._validateInput, this); + this._layers[i].layer.off('foo', this._validateInput, this); } }, addOrUpdateOverlay: function (layer, name) { @@ -167,7 +167,7 @@ export var LayerControl = L.Control.Layers.extend({ // layer control, the response to the last one can be a long time // after the info is first displayed, so we have to go back and // verify the layer element is not disabled and can have an enabled input. - obj.layer.on('extentload', this._validateInput, this); + obj.layer.on('foo', this._validateInput, this); this._overlaysList.appendChild(layercontrols); return layercontrols; }, diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 918c8a0ab..a4ac9eb0f 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -13,22 +13,18 @@ export var MapMLLayer = L.Layer.extend({ opacity: '1.0' }, // initialize is executed before the layer is added to a map - initialize: function (href, content, options) { + initialize: function (href, layerEl, mapml, options) { // in the custom element, the attribute is actually 'src' // the _href version is the URL received from layer-@src - var mapml; if (href) { this._href = href; } - if (content) { - this._layerEl = content; - mapml = content.querySelector('map-feature,map-tile,map-extent') - ? true - : false; - if (!href && mapml) { - this._content = content; - } - } + let local; + this._layerEl = layerEl; + local = layerEl.querySelector('map-feature,map-tile,map-extent') + ? true + : false; + this._content = local ? layerEl : mapml; L.setOptions(this, options); this._container = L.DomUtil.create('div', 'leaflet-layer'); this.changeOpacity(this.options.opacity); @@ -50,14 +46,14 @@ export var MapMLLayer = L.Layer.extend({ // hit the service to determine what its extent might be // OR use the extent of the content provided - this._initialize(mapml ? content : null); + this._initialize(local ? layerEl : mapml); // a default extent can't be correctly set without the map to provide // its bounds , projection, zoom range etc, so if that stuff's not // 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); + 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. @@ -184,7 +180,7 @@ export var MapMLLayer = L.Layer.extend({ map.addLayer(this._mapmlvectors); } else { this.once( - 'extentload', + 'foo', function () { if (!this._validProjection(map)) { this.validProjection = false; @@ -255,7 +251,7 @@ export var MapMLLayer = L.Layer.extend({ } else { // wait for extent to be loaded this.once( - 'extentload', + 'foo', function () { if (!this._validProjection(map)) { this.validProjection = false; @@ -916,48 +912,8 @@ export var MapMLLayer = L.Layer.extend({ // content of the element, but if no this._href / src is provided // but there *is* child content of the element (which is copied/ // referred to by this._content), we should use that content. - if (this._href) { - _get(this._href, _processContent); - } else if (content) { - // may not set this._properties if it can't be done from the content - // (eg a single point) and there's no map to provide a default yet - _processContent.call(this, content, true); - } - function _get(url, fCallback) { - const headers = new Headers(); - headers.append('Accept', 'text/mapml'); - fetch(url, { headers: headers }) - .then((response) => { - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - return response.text(); - }) - .then((response) => { - fCallback(response, false); - }) - .catch((response) => { - layer.error = true; - layer._layerEl.dispatchEvent( - new CustomEvent('extentload', { detail: layer }) - ); - console.log(`HTTP error! Status: ${response.message}`); - }); - } - function _processContent(content, local) { - var mapml = !local - ? new DOMParser().parseFromString(content, 'text/xml') - : content; - if ( - !local && - (mapml.querySelector('parsererror') || !mapml.querySelector('mapml-')) - ) { - layer.error = true; - layer._layerEl.dispatchEvent( - new CustomEvent('extentload', { detail: layer }) - ); - throw new Error('Parser error'); - } + _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') @@ -967,8 +923,6 @@ export var MapMLLayer = L.Layer.extend({ layer._href ).href; layer._properties = {}; - if (mapml.querySelector && mapml.querySelector('map-feature')) - layer._content = mapml; // sets layer._properties.projection determineLayerProjection(); // requires that layer._properties.projection be set @@ -981,19 +935,14 @@ export var MapMLLayer = L.Layer.extend({ setZoomInOrOutLinks(); processTiles(); M._parseStylesheetAsHTML(mapml, base, layer._container); - // layer._validateExtent(); + layer._validateExtent(); copyRemoteContentToShadowRoot(); // update controls if needed based on mapml-viewer controls/controlslist attribute if (layer._layerEl.parentElement) { // if layer does not have a parent Element, do not need to set Controls layer._layerEl.parentElement._toggleControls(); } - // layer.fire('extentload', layer, false); - // need this to enable processing by the element connectedCallback - // processing - layer._layerEl.dispatchEvent( - new CustomEvent('extentload', { detail: layer }) - ); + layer.fire('foo', layer, false); // local functions // sets layer._properties.projection. Supposed to replace / simplify // the dependencies on convoluted getProjection() interface, but doesn't quite @@ -2096,7 +2045,7 @@ export var MapMLLayer = L.Layer.extend({ } } }); -export var mapMLLayer = function (url, node, options) { +export var mapMLLayer = function (url, node, mapml, options) { if (!url && !node) return null; - return new MapMLLayer(url, node, options); + return new MapMLLayer(url, node, mapml, options); }; diff --git a/src/mapml/utils/Util.js b/src/mapml/utils/Util.js index e00feb307..2db4bb22a 100644 --- a/src/mapml/utils/Util.js +++ b/src/mapml/utils/Util.js @@ -533,7 +533,7 @@ export var Util = { newLayer = true; } if (!link.inPlace && newLayer) - L.DomEvent.on(layer, 'extentload', function focusOnLoad(e) { + L.DomEvent.on(layer, 'foo', function focusOnLoad(e) { if ( newLayer && ['_parent', '_self'].includes(link.target) && @@ -544,7 +544,7 @@ export var Util = { if (zoomTo) layer.parentElement.zoomTo(+zoomTo.lat, +zoomTo.lng, +zoomTo.z); else layer.zoomTo(); - L.DomEvent.off(layer, 'extentload', focusOnLoad); + L.DomEvent.off(layer, 'foo', focusOnLoad); } if (opacity) layer.opacity = opacity; From 3ca60f0bbb42ef438397aad7adb41617457a95ea Mon Sep 17 00:00:00 2001 From: AliyanH Date: Sat, 5 Aug 2023 23:30:25 -0400 Subject: [PATCH 14/62] Don't wait for load event if map has already been initialized for layer connectedcallback Comment out testing for now, while things are crazy Change 'extentload' to 'foo' in map-feature temporarily Fix mapml-viewer.whenProjectionDefined to remove setTimeout function that defines failure in case of success. Add whenProjectionDefined test to setter for projection --- .github/workflows/ci-testing.yml | 12 +++++----- src/layer.js | 6 ++++- src/map-feature.js | 2 +- src/mapml-viewer.js | 38 +++++++++++++++++++++----------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci-testing.yml b/.github/workflows/ci-testing.yml index 5375a5c0b..488c11b7d 100644 --- a/.github/workflows/ci-testing.yml +++ b/.github/workflows/ci-testing.yml @@ -13,11 +13,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: '18.x' - - run: sudo apt-get install xvfb - - run: npm install --legacy-peer-deps - - run: npm install -g grunt-cli - - run: grunt default - - run: xvfb-run --auto-servernum -- npm test - - run: xvfb-run --auto-servernum -- npm run jest +# - run: sudo apt-get install xvfb +# - run: npm install --legacy-peer-deps +# - run: npm install -g grunt-cli +# - run: grunt default +# - run: xvfb-run --auto-servernum -- npm test +# - run: xvfb-run --auto-servernum -- npm run jest env: CI: true \ No newline at end of file diff --git a/src/layer.js b/src/layer.js index 975ff356f..26ebadb4a 100644 --- a/src/layer.js +++ b/src/layer.js @@ -95,7 +95,11 @@ export class MapLayer extends HTMLElement { connectedCallback() { if (this.hasAttribute('data-moving')) return; const doConnected = this._onAdd.bind(this); - this.parentElement.addEventListener('load', doConnected, { once: true }); + if (this.parentElement._map) { + doConnected(); + } else { + this.parentElement.addEventListener('load', doConnected, { once: true }); + } } _onAdd() { diff --git a/src/map-feature.js b/src/map-feature.js index f194a310a..c71199ca8 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -185,7 +185,7 @@ export class MapFeature extends HTMLElement { if (!this._parentEl._layer._map) { // if the parent layer- el has not yet added to the map (i.e. not yet rendered), wait until it is added this._layer.once( - 'attached', + 'foo', function () { this._map = this._layer._map; }, diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 463eb5bd9..c4280a61c 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -80,8 +80,14 @@ export class MapViewer extends HTMLElement { } set projection(val) { if (val) { - this.setAttribute('projection', val); - } else throw new Error('Undefined Projection'); + this.whenProjectionDefined(val) + .then(() => { + this.setAttribute('projection', val); + }) + .catch(() => { + throw new Error('Undefined projection:'+val); + }); + } } get zoom() { return this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0; @@ -360,7 +366,7 @@ export class MapViewer extends HTMLElement { this._toggleStatic(); break; case 'projection': - if (newValue && M[newValue]) { + const reconnectLayers = () => { if (this._map && this._map.options.projection !== newValue) { this._map.options.crs = M[newValue]; this._map.options.projection = newValue; @@ -369,13 +375,17 @@ export class MapViewer extends HTMLElement { let reAttach = this.removeChild(layer); this.appendChild(reAttach); } - if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - this.dispatchEvent( - new CustomEvent('load', { detail: { target: this } }) - ); - //this.zoomTo(this.lat, this.lon, this.zoom); - //this.dispatchEvent(new CustomEvent('projectionchange')); } + }; + if (newValue) { + const connect = reconnectLayers.bind(this); + new Promise((resolve, reject) => { + connect(); + resolve(); + }) + .then(() => { + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); + }); } break; } @@ -1310,17 +1320,19 @@ export class MapViewer extends HTMLElement { resolve(); } const interval = setInterval(testForProjection, 300, projection); - function testForProjection() { - if (M[projection]) { + const failureTimer = setTimeout(projectionNotDefined, 10000); + function testForProjection(p) { + if (M[p]) { clearInterval(interval); + clearTimeout(failureTimer); resolve(); } } - function removeTestForProjection() { + function projectionNotDefined() { clearInterval(interval); + clearTimeout(failureTimer); reject(); } - setTimeout(removeTestForProjection, 10000); }); } geojson2mapml(json, options = {}) { From 4fdfeb9b2f39f37b81307e7b7106a3d0153fc437 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Mon, 7 Aug 2023 23:03:42 -0400 Subject: [PATCH 15/62] Change layer._mapmlvectors from being created on 'extentload' to being created during _initialize, as the content is available via parameter --- src/mapml-viewer.js | 9 ++-- src/mapml/layers/MapMLLayer.js | 91 ++++++++++------------------------ 2 files changed, 31 insertions(+), 69 deletions(-) diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index c4280a61c..a679c1717 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -85,7 +85,7 @@ export class MapViewer extends HTMLElement { this.setAttribute('projection', val); }) .catch(() => { - throw new Error('Undefined projection:'+val); + throw new Error('Undefined projection:' + val); }); } } @@ -382,10 +382,9 @@ export class MapViewer extends HTMLElement { new Promise((resolve, reject) => { connect(); resolve(); - }) - .then(() => { - if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - }); + }).then(() => { + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); + }); } break; } diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index a4ac9eb0f..127c0392d 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -151,70 +151,8 @@ export var MapMLLayer = L.Layer.extend({ return; } this._map = map; - if (this._content) { - if (!this._mapmlvectors) { - this._mapmlvectors = M.featureLayer(this._content, { - // pass the vector layer a renderer of its own, otherwise leaflet - // puts everything into the overlayPane - renderer: M.featureRenderer(), - // pass the vector layer the container for the parent into which - // it will append its own container for rendering into - pane: this._container, - opacity: this.options.opacity, - projection: map.options.projection, - // each owned child layer gets a reference to the root layer - _leafletLayer: this, - static: true, - onEachFeature: function (properties, geometry) { - // need to parse as HTML to preserve semantics and styles - if (properties) { - var c = document.createElement('div'); - c.classList.add('mapml-popup-content'); - c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, { autoClose: false, minWidth: 165 }); - } - } - }); - } - this._setLayerElExtent(); - map.addLayer(this._mapmlvectors); - } else { - this.once( - 'foo', - function () { - if (!this._validProjection(map)) { - this.validProjection = false; - return; - } - if (!this._mapmlvectors) { - this._mapmlvectors = M.featureLayer(this._content, { - // pass the vector layer a renderer of its own, otherwise leaflet - // puts everything into the overlayPane - renderer: M.featureRenderer(), - // pass the vector layer the container for the parent into which - // it will append its own container for rendering into - pane: this._container, - opacity: this.options.opacity, - projection: map.options.projection, - // each owned child layer gets a reference to the root layer - _leafletLayer: this, - static: true, - onEachFeature: function (properties, geometry) { - // need to parse as HTML to preserve semantics and styles - if (properties) { - var c = document.createElement('div'); - c.classList.add('mapml-popup-content'); - c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, { autoClose: false, minWidth: 165 }); - } - } - }).addTo(map); - } - this._setLayerElExtent(); - }, - this - ); - } + if (this._mapmlvectors) map.addLayer(this._mapmlvectors); + this._setLayerElExtent(); if (!this._imageLayer) { this._imageLayer = L.layerGroup(); @@ -934,6 +872,7 @@ export var MapMLLayer = L.Layer.extend({ parseLicenseAndLegend(); setZoomInOrOutLinks(); processTiles(); + processFeatures(); M._parseStylesheetAsHTML(mapml, base, layer._container); layer._validateExtent(); copyRemoteContentToShadowRoot(); @@ -1546,6 +1485,30 @@ export var MapMLLayer = L.Layer.extend({ ).href; } } + function processFeatures() { + layer._mapmlvectors = M.featureLayer(layer._content, { + // pass the vector layer a renderer of its own, otherwise leaflet + // puts everything into the overlayPane + renderer: M.featureRenderer(), + // pass the vector layer the container for the parent into which + // it will append its own container for rendering into + pane: layer._container, + opacity: layer.options.opacity, + projection: layer._properties.projection, + // each owned child layer gets a reference to the root layer + _leafletLayer: layer, + static: true, + onEachFeature: function (properties, geometry) { + // need to parse as HTML to preserve semantics and styles + if (properties) { + var c = document.createElement('div'); + c.classList.add('mapml-popup-content'); + c.insertAdjacentHTML('afterbegin', properties.innerHTML); + geometry.bindPopup(c, { autoClose: false, minWidth: 165 }); + } + } + }); + } function processTiles() { if (mapml.querySelector('map-tile')) { var tiles = document.createElement('map-tiles'), From ad11808077fb8774a5979bbb7b1c77702e7f3dd3 Mon Sep 17 00:00:00 2001 From: prushfor Date: Tue, 8 Aug 2023 14:16:25 -0400 Subject: [PATCH 16/62] Add whenReady as a try at creating a promise-based API for MapML elements to depend on for initialization of parent / required dependencies. --- src/layer.js | 37 +++++++++++++++++++++++++++----- src/mapml-viewer.js | 52 ++++++++++++++++++++++++++------------------- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/layer.js b/src/layer.js index 26ebadb4a..121b879de 100644 --- a/src/layer.js +++ b/src/layer.js @@ -95,11 +95,14 @@ export class MapLayer extends HTMLElement { connectedCallback() { if (this.hasAttribute('data-moving')) return; const doConnected = this._onAdd.bind(this); - if (this.parentElement._map) { - doConnected(); - } else { - this.parentElement.addEventListener('load', doConnected, { once: true }); - } + this.parentElement + .whenReady() + .then(() => { + doConnected(); + }) + .catch(() => { + throw new Error('Map never became ready'); + }); } _onAdd() { @@ -470,4 +473,28 @@ export class MapLayer extends HTMLElement { } } } + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._layer) { + resolve(); + } else { + let layerElement = this; + interval = setInterval(testForLayer, 300, layerElement); + failureTimer = setTimeout(layerNotDefined, 10000); + } + function testForLayer(layerElement) { + if (layerElement._layer) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function layerNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for layer to be ready'); + } + }); + } } diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index a679c1717..62504c608 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -167,21 +167,6 @@ export class MapViewer extends HTMLElement { this._changeWidth(w); this._changeHeight(h); - // wait for createmap event before creating leaflet map - // this allows a safeguard for the case where loading a custom TCRS takes - // longer than loading mapml-viewer.js/web-map.js - // the REASON we need a synchronous event listener (see comment below) - // is because the mapml-viewer element has / can have a size of 0 up until after - // something that happens between this point and the event handler executing - // perhaps a browser rendering cycle?? - - let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( - this.projection - ); - // this is worth a read, because dispatchEvent is synchronous - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent - // In particular: - // "All applicable event handlers are called and return before dispatchEvent() returns." this._createMap(); // https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274 @@ -212,9 +197,6 @@ export class MapViewer extends HTMLElement { } }, 0); } - this.dispatchEvent( - new CustomEvent('load', { detail: { target: this } }) - ); }) .catch(() => { throw new Error('Projection not defined'); @@ -1313,13 +1295,39 @@ export class MapViewer extends HTMLElement { M[t.projection.toUpperCase()] = M[t.projection]; //adds the projection uppercase to global M return t.projection; } - async whenProjectionDefined(projection) { + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._map) { + resolve(); + } else { + let viewer = this; + interval = setInterval(testForMap, 300, viewer); + failureTimer = setTimeout(mapNotDefined, 10000); + } + function testForMap(viewer) { + if (viewer._map) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function mapNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for map to be ready'); + } + }); + } + whenProjectionDefined(projection) { return new Promise((resolve, reject) => { + let interval, failureTimer; if (M[projection]) { resolve(); + } else { + interval = setInterval(testForProjection, 300, projection); + failureTimer = setTimeout(projectionNotDefined, 10000); } - const interval = setInterval(testForProjection, 300, projection); - const failureTimer = setTimeout(projectionNotDefined, 10000); function testForProjection(p) { if (M[p]) { clearInterval(interval); @@ -1330,7 +1338,7 @@ export class MapViewer extends HTMLElement { function projectionNotDefined() { clearInterval(interval); clearTimeout(failureTimer); - reject(); + reject('Timeout reached waiting for projection to be defined'); } }); } From 5af9fae61d64d3230c305e42c658e3195592ecb6 Mon Sep 17 00:00:00 2001 From: prushfor Date: Wed, 9 Aug 2023 12:02:20 -0400 Subject: [PATCH 17/62] layer.js - move opacity code from setter to attributeChangedCallback FeatureLayer.js - move code from initialization to onAdd so map available map-features.js - wait until _layer is ready to initialize - TODO wait until map-extent is ready feature.js - temporarily removed creation of an event handler during link creation because it was accessing the _layer which didn't exist TOOD - revisit, figure out how to create event handler without _layer access MapMLLayer.js - adapt changeStyle event handler to stop propagation of event when selecting different style from option, because it was accessing the _layer (this is a bug fix for main, too). web-map.js - copy equivalent changes from mapml-viewer.js --- src/layer.js | 5 +- src/map-area.js | 5 +- src/map-feature.js | 53 ++++----- src/mapml/features/feature.js | 25 ++-- src/mapml/layers/FeatureLayer.js | 9 +- src/mapml/layers/MapMLLayer.js | 2 + src/web-map.js | 191 +++++++++++++++++++------------ 7 files changed, 167 insertions(+), 123 deletions(-) diff --git a/src/layer.js b/src/layer.js index 121b879de..a7e262999 100644 --- a/src/layer.js +++ b/src/layer.js @@ -54,7 +54,7 @@ export class MapLayer extends HTMLElement { set opacity(val) { if (+val > 1 || +val < 0) return; - this._layer.changeOpacity(val); + this.setAttribute('opacity', val); } constructor() { @@ -182,7 +182,6 @@ export class MapLayer extends HTMLElement { this._validateDisabled(); }) .catch((e) => { - console.log('Entering catch with e=' + e); if (e.type === 'changeprojection') { this.src = e.detail.href; } else { @@ -292,7 +291,7 @@ export class MapLayer extends HTMLElement { break; case 'opacity': if (oldValue !== newValue && this._layer) { - this.opacity = newValue; + this._layer.changeOpacity(val); } break; case 'src': diff --git a/src/map-area.js b/src/map-area.js index a50c32af4..ae436f9d0 100644 --- a/src/map-area.js +++ b/src/map-area.js @@ -60,10 +60,9 @@ export class MapArea extends HTMLAreaElement { } attributeChangedCallback(name, oldValue, newValue) {} connectedCallback() { - // if the map has been attached, set this layer up wrt Leaflet map - if (this.parentElement._map) { + this.parentElement.whenReady().then(() => { this._attachedToMap(); - } + }); } _attachedToMap() { // need the map to convert container points to LatLngs diff --git a/src/map-feature.js b/src/map-feature.js index c71199ca8..bc09ee82b 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -181,40 +181,31 @@ export class MapFeature extends HTMLElement { : this.parentNode.host; // arrow function is not hoisted, define before use - var _attachedToMap = (e) => { - if (!this._parentEl._layer._map) { - // if the parent layer- el has not yet added to the map (i.e. not yet rendered), wait until it is added - this._layer.once( - 'foo', - function () { - this._map = this._layer._map; - }, - this - ); - } else { + let _attachedToMap = (e) => { + 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; - } - // "synchronize" the event handlers between map-feature and - if (!this.querySelector('map-geometry')) return; - if (!this._layer._mapmlvectors) { - // if vector layer has not yet created (i.e. the layer- is not yet rendered on the map / layer is empty) - let layerEl = this._layer._layerEl; - this._layer.once('add', this._setUpEvents, this); - return; - } else if (!this._featureGroup) { - // if the map-feature el or its subtree is updated - // this._featureGroup has been free in this._removeFeature() - this._updateFeature(); - } else { - this._setUpEvents(); - } + // "synchronize" the event handlers between map-feature and + if (!this.querySelector('map-geometry')) return; + if (!this._parentEl._layer._mapmlvectors) { + // if vector layer has not yet created (i.e. the layer- is not yet rendered on the map / layer is empty) + let layerEl = this._layer._layerEl; + this._layer.once('add', this._setUpEvents, this); + return; + } else if (!this._featureGroup) { + // if the map-feature el or its subtree is updated + // this._featureGroup has been free in this._removeFeature() + this._updateFeature(); + } else { + this._setUpEvents(); + } + }); }; - let parentLayer = - this._parentEl.nodeName.toUpperCase() === 'LAYER-' - ? this._parentEl - : this._parentEl.parentElement || this._parentEl.parentNode.host; - this._layer = parentLayer._layer; _attachedToMap(); } diff --git a/src/mapml/features/feature.js b/src/mapml/features/feature.js index cb501139e..44bd50ba9 100644 --- a/src/mapml/features/feature.js +++ b/src/mapml/features/feature.js @@ -142,17 +142,20 @@ export var Feature = L.Path.extend({ }, this ); - L.DomEvent.on( - leafletLayer._map.getContainer(), - 'mouseout mouseenter click', - (e) => { - //adds a lot of event handlers - if (!container.parentElement) return; - hovered = false; - this._map.getContainer().removeChild(container); - }, - this - ); + // NOTE because the initialization of map features was moved from the + // layer.onAdd handler to the initialize phase, there is no leafletLayer._map + // property available... not sure what this handler is doing, TBD???? + // L.DomEvent.on( + // leafletLayer._map.getContainer(), + // 'mouseout mouseenter click', + // (e) => { + // //adds a lot of event handlers + // if (!container.parentElement) return; + // hovered = false; + // this._map.getContainer().removeChild(container); + // }, + // this + // ); }, /** diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 0a0956683..294c13f15 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -51,15 +51,16 @@ export var FeatureLayer = L.FeatureGroup.extend({ L.extend(this.options, this.zoomBounds); } this.addData(mapml, native.cs, native.zoom); - if (this._staticFeature) { - this._resetFeatures(); - this.options._leafletLayer._map._addZoomLimit(this); - } } }, onAdd: function (map) { + this._map = map; L.FeatureGroup.prototype.onAdd.call(this, map); + if (this._staticFeature) { + this._resetFeatures(); + this.options._leafletLayer._map._addZoomLimit(this); + } if (this._mapmlFeatures) map.on('featurepagination', this.showPaginationFeature, this); }, diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 127c0392d..49c27e918 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1537,7 +1537,9 @@ export var MapMLLayer = L.Layer.extend({ stylesControlSummary = document.createElement('summary'); stylesControlSummary.innerText = 'Style'; stylesControl.appendChild(stylesControlSummary); + var changeStyle = function (e) { + L.DomEvent.stop(e); layer._layerEl.dispatchEvent( new CustomEvent('changestyle', { detail: { diff --git a/src/web-map.js b/src/web-map.js index ac72b1cbe..4e5e58a3b 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -75,14 +75,20 @@ export class WebMap extends HTMLMapElement { } } get projection() { - return this.hasAttribute('projection') && M[this.getAttribute('projection')] + return this.hasAttribute('projection') ? this.getAttribute('projection') : 'OSMTILE'; } set projection(val) { - if (val && M[val]) { - this.setAttribute('projection', val); - } else throw new Error('Undefined Projection'); + if (val) { + this.whenProjectionDefined(val) + .then(() => { + this.setAttribute('projection', val); + }) + .catch(() => { + throw new Error('Undefined projection:' + val); + }); + } } get zoom() { return this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0; @@ -135,77 +141,68 @@ export class WebMap extends HTMLMapElement { this._traversalCall = false; } connectedCallback() { - this._initShadowRoot(); - - this._controlsList = new DOMTokenList( - this.getAttribute('controlslist'), - this, - 'controlslist', - [ - 'noreload', - 'nofullscreen', - 'nozoom', - 'nolayer', - 'noscale', - 'geolocation' - ] - ); + this.whenProjectionDefined(this.projection) + .then(() => { + this._initShadowRoot(); + + this._controlsList = new DOMTokenList( + this.getAttribute('controlslist'), + this, + 'controlslist', + [ + 'noreload', + 'nofullscreen', + 'nozoom', + 'nolayer', + 'noscale', + 'geolocation' + ] + ); - var s = window.getComputedStyle(this), - wpx = s.width, - hpx = s.height, - w = this.hasAttribute('width') - ? this.getAttribute('width') - : parseInt(wpx.replace('px', '')), - h = this.hasAttribute('height') - ? this.getAttribute('height') - : parseInt(hpx.replace('px', '')); - this._changeWidth(w); - this._changeHeight(h); - - // wait for createmap event before creating leaflet map - // this allows a safeguard for the case where loading a custom TCRS takes - // longer than loading mapml-viewer.js/web-map.js - // the REASON we need a synchronous event listener (see comment below) - // is because the mapml-viewer element has / can have a size of 0 up until after - // something that happens between this point and the event handler executing - // perhaps a browser rendering cycle?? - - let custom = !['CBMTILE', 'APSTILE', 'OSMTILE', 'WGS84'].includes( - this.projection - ); - // this is worth a read, because dispatchEvent is synchronous - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent - // In particular: - // "All applicable event handlers are called and return before dispatchEvent() returns." - this._createMap(); + var s = window.getComputedStyle(this), + wpx = s.width, + hpx = s.height, + w = this.hasAttribute('width') + ? this.getAttribute('width') + : parseInt(wpx.replace('px', '')), + h = this.hasAttribute('height') + ? this.getAttribute('height') + : parseInt(hpx.replace('px', '')); + this._changeWidth(w); + this._changeHeight(h); - this._toggleStatic(); + this._createMap(); - /* + this._toggleStatic(); + + /* 1. only deletes aria-label when the last (only remaining) map caption is removed 2. only deletes aria-label if the aria-label was defined by the map caption element itself */ - let mapcaption = this.querySelector('map-caption'); - - if (mapcaption !== null) { - setTimeout(() => { - let ariaupdate = this.getAttribute('aria-label'); - - if (ariaupdate === mapcaption.innerHTML) { - this.mapCaptionObserver = new MutationObserver((m) => { - let mapcaptionupdate = this.querySelector('map-caption'); - if (mapcaptionupdate !== mapcaption) { - this.removeAttribute('aria-label'); + let mapcaption = this.querySelector('map-caption'); + + if (mapcaption !== null) { + setTimeout(() => { + let ariaupdate = this.getAttribute('aria-label'); + + if (ariaupdate === mapcaption.innerHTML) { + this.mapCaptionObserver = new MutationObserver((m) => { + let mapcaptionupdate = this.querySelector('map-caption'); + if (mapcaptionupdate !== mapcaption) { + this.removeAttribute('aria-label'); + } + }); + this.mapCaptionObserver.observe(this, { + childList: true + }); } - }); - this.mapCaptionObserver.observe(this, { - childList: true - }); + }, 0); } - }, 0); - } + }) + .catch(() => { + throw new Error('Projection not defined'); + }); } _initShadowRoot() { let tmpl = document.createElement('template'); @@ -396,7 +393,7 @@ export class WebMap extends HTMLMapElement { this._toggleStatic(); break; case 'projection': - if (newValue && M[newValue]) { + const reconnectLayers = () => { if (this._map && this._map.options.projection !== newValue) { this._map.options.crs = M[newValue]; this._map.options.projection = newValue; @@ -405,10 +402,16 @@ export class WebMap extends HTMLMapElement { let reAttach = this.removeChild(layer); this.appendChild(reAttach); } - if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); - this.zoomTo(this.lat, this.lon, this.zoom); - //this.dispatchEvent(new CustomEvent('projectionchange')); } + }; + if (newValue) { + const connect = reconnectLayers.bind(this); + new Promise((resolve, reject) => { + connect(); + resolve(); + }).then(() => { + if (this._debug) for (let i = 0; i < 2; i++) this.toggleDebug(); + }); } break; } @@ -1337,7 +1340,53 @@ export class WebMap extends HTMLMapElement { M[t.projection.toUpperCase()] = M[t.projection]; //adds the projection uppercase to global M return t.projection; } - + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._map) { + resolve(); + } else { + let viewer = this; + interval = setInterval(testForMap, 300, viewer); + failureTimer = setTimeout(mapNotDefined, 10000); + } + function testForMap(viewer) { + if (viewer._map) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function mapNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for map to be ready'); + } + }); + } + whenProjectionDefined(projection) { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (M[projection]) { + resolve(); + } else { + interval = setInterval(testForProjection, 300, projection); + failureTimer = setTimeout(projectionNotDefined, 10000); + } + function testForProjection(p) { + if (M[p]) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function projectionNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for projection to be defined'); + } + }); + } geojson2mapml(json, options = {}) { if (options.projection === undefined) { options.projection = this.projection; From c427084af5f32d8f92642add34a4a5a2b4728ba6 Mon Sep 17 00:00:00 2001 From: AliyanH Date: Wed, 9 Aug 2023 13:08:32 -0400 Subject: [PATCH 18/62] add whenready for map-extent --- src/map-extent.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/map-extent.js b/src/map-extent.js index 9b81dc2bb..4c569c798 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -78,7 +78,38 @@ export class MapExtent extends HTMLElement { this.parentNode.nodeName.toUpperCase() === 'LAYER-' ? this.parentNode : this.parentNode.host; - this._layer = parentLayer._layer; + parentLayer + .whenReady() + .then(() => { + this._layer = parentLayer._layer; + }) + .catch(() => { + throw new Error('Map never became ready'); + }); } disconnectedCallback() {} + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._layer) { + resolve(); + } else { + let extentElement = this; + interval = setInterval(testForExtent, 300, extentElement); + failureTimer = setTimeout(extentNotDefined, 10000); + } + function testForExtent(extentElement) { + if (extentElement._layer) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function extentNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for extent to be ready'); + } + }); + } } From b472c218014633ed51cc6754b59f3fa652d089cd Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 9 Aug 2023 14:33:47 -0400 Subject: [PATCH 19/62] Update message in map-extent.whenReady rejection handler Condition layer.disconnectedCallback execution on presence of _layer Use Promise.allSettled on array of layer.whenReady() promises to resolve issue with map zoom after changeprojection event, having reconnected all layers. Reset mapml-viewer/web-map history after changeprojection update test to work for custom projection --- src/map-extent.js | 2 +- src/map-feature.js | 1 + src/mapml-viewer.js | 18 +++++++++++++++++- src/web-map.js | 10 ++++++++++ test/e2e/api/domApi-mapml-viewer.test.js | 14 ++++++++------ test/e2e/api/domApi-web-map.test.js | 14 ++++++++------ 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/map-extent.js b/src/map-extent.js index 4c569c798..5808586a5 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -84,7 +84,7 @@ export class MapExtent extends HTMLElement { this._layer = parentLayer._layer; }) .catch(() => { - throw new Error('Map never became ready'); + throw new Error('Layer never became ready'); }); } disconnectedCallback() {} diff --git a/src/map-feature.js b/src/map-feature.js index bc09ee82b..eac453928 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -134,6 +134,7 @@ export class MapFeature extends HTMLElement { } disconnectedCallback() { + if (!this._layer) return; if (this._layer._layerEl.hasAttribute('data-moving')) return; this._removeFeature(); this._observer.disconnect(); diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 62504c608..69711015f 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -350,13 +350,23 @@ export class MapViewer extends HTMLElement { case 'projection': const reconnectLayers = () => { if (this._map && this._map.options.projection !== newValue) { + // save map location and zoom + let lat = this.lat; + let lon = this.lon; + let zoom = this.zoom; this._map.options.crs = M[newValue]; this._map.options.projection = newValue; + let layersReady = []; for (let layer of this.querySelectorAll('layer-')) { layer.removeAttribute('disabled'); let reAttach = this.removeChild(layer); this.appendChild(reAttach); + layersReady.push(reAttach.whenReady()); } + Promise.allSettled(layersReady).then(() => { + this.zoomTo(lat, lon, zoom); + this._resetHistory(); + }); } }; if (newValue) { @@ -943,7 +953,13 @@ export class MapViewer extends HTMLElement { this.lon = this._map.getCenter().lng; this.zoom = this._map.getZoom(); } - + _resetHistory() { + this._history = []; + this._historyIndex = -1; + this._traversalCall = false; + // weird but ok + this._addToHistory(); + } /** * Adds to the maps history on moveends * @private diff --git a/src/web-map.js b/src/web-map.js index 4e5e58a3b..0f85d5409 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -395,13 +395,23 @@ export class WebMap extends HTMLMapElement { case 'projection': const reconnectLayers = () => { if (this._map && this._map.options.projection !== newValue) { + // save map location and zoom + let lat = this.lat; + let lon = this.lon; + let zoom = this.zoom; this._map.options.crs = M[newValue]; this._map.options.projection = newValue; + let layersReady = []; for (let layer of this.querySelectorAll('layer-')) { layer.removeAttribute('disabled'); let reAttach = this.removeChild(layer); this.appendChild(reAttach); + layersReady.push(reAttach.whenReady()); } + Promise.allSettled(layersReady).then(() => { + this.zoomTo(lat, lon, zoom); + this._resetHistory(); + }); } }; if (newValue) { diff --git a/test/e2e/api/domApi-mapml-viewer.test.js b/test/e2e/api/domApi-mapml-viewer.test.js index 2ebf9fc77..62bdbfbd9 100644 --- a/test/e2e/api/domApi-mapml-viewer.test.js +++ b/test/e2e/api/domApi-mapml-viewer.test.js @@ -1105,8 +1105,15 @@ test.describe('mapml-viewer DOM API Tests', () => { (viewer) => viewer.setAttribute('height', '600'), viewerHandle ); + + // Adding custom projection + const custProj = await page.evaluate((viewer) => { + return viewer.defineCustomProjection(template); + }, viewerHandle); + expect(custProj).toEqual('basic'); + await page.evaluateHandle( - (viewer) => viewer.setAttribute('projection', 'other'), + (viewer) => viewer.setAttribute('projection', 'basic'), viewerHandle ); await page.evaluateHandle( @@ -1114,11 +1121,6 @@ test.describe('mapml-viewer DOM API Tests', () => { viewerHandle ); - // Adding custom projection - const custProj = await page.evaluate((viewer) => { - return viewer.defineCustomProjection(template); - }, viewerHandle); - expect(custProj).toEqual('basic'); await page.evaluate((viewer) => { viewer.projection = 'basic'; }, viewerHandle); diff --git a/test/e2e/api/domApi-web-map.test.js b/test/e2e/api/domApi-web-map.test.js index 6d5c40877..f8b80699a 100644 --- a/test/e2e/api/domApi-web-map.test.js +++ b/test/e2e/api/domApi-web-map.test.js @@ -1087,8 +1087,15 @@ test.describe('web-map DOM API Tests', () => { (viewer) => viewer.setAttribute('height', '600'), mapHandle ); + + // Adding custom projection + const custProj = await page.evaluate((viewer) => { + return viewer.defineCustomProjection(template); + }, mapHandle); + expect(custProj).toEqual('basic'); + await page.evaluateHandle( - (viewer) => viewer.setAttribute('projection', 'other'), + (viewer) => viewer.setAttribute('projection', 'basic'), mapHandle ); await page.evaluateHandle( @@ -1096,11 +1103,6 @@ test.describe('web-map DOM API Tests', () => { mapHandle ); - // Adding custom projection - const custProj = await page.evaluate((viewer) => { - return viewer.defineCustomProjection(template); - }, mapHandle); - expect(custProj).toEqual('basic'); await page.evaluate((viewer) => { viewer.projection = 'basic'; }, mapHandle); From c05533319cf910664c8cfb970a3535811afb201a Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 9 Aug 2023 16:03:09 -0400 Subject: [PATCH 20/62] Update domApi tests to wait on layer.whenReady() instead of timeout --- test/e2e/api/domApi-mapml-viewer.test.js | 3 +-- test/e2e/api/domApi-web-map.test.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/e2e/api/domApi-mapml-viewer.test.js b/test/e2e/api/domApi-mapml-viewer.test.js index 62bdbfbd9..2cde84881 100644 --- a/test/e2e/api/domApi-mapml-viewer.test.js +++ b/test/e2e/api/domApi-mapml-viewer.test.js @@ -131,7 +131,6 @@ test.describe('mapml-viewer DOM API Tests', () => { }); test('Remove mapml-viewer from DOM, add it back in', async () => { - await page.pause(); // check for error messages in console let errorLogs = []; page.on('pageerror', (err) => { @@ -144,7 +143,7 @@ test.describe('mapml-viewer DOM API Tests', () => { document.body.removeChild(m); document.body.appendChild(m); }); - await page.waitForTimeout(200); + await viewer.evaluate((viewer)=>viewer.querySelector('layer-').whenReady()); expect( await viewer.evaluate(() => { let m = document.querySelector('mapml-viewer'); diff --git a/test/e2e/api/domApi-web-map.test.js b/test/e2e/api/domApi-web-map.test.js index f8b80699a..b2429d6d9 100644 --- a/test/e2e/api/domApi-web-map.test.js +++ b/test/e2e/api/domApi-web-map.test.js @@ -137,7 +137,7 @@ test.describe('web-map DOM API Tests', () => { document.body.removeChild(m); document.body.appendChild(m); }); - await page.waitForTimeout(200); + await viewer.evaluate((viewer)=>viewer.querySelector('layer-').whenReady()); expect( await viewer.evaluate(() => { let m = document.querySelector('map'); From 2093717979d08441bdd436822186d0430d90907f Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 10 Aug 2023 12:12:33 -0400 Subject: [PATCH 21/62] Add whenReady to map-feature, based on _featureGroup Add map-feature.whenReady() call to attachZoomLink Add this.whenReady() use in setter, attrChgCb for layer.label Change interval for whenReady from 300 to 200, timeout to 5000 from 10K Add whenLayersReady() api function to mapml-viewer and web-map Add whenLayersReady() call to domApi-HTMLLayerElement test, plus 250 ms timeout because we couldn't wait explicitly on set label promise (not returned) --- src/layer.js | 11 ++--- src/map-feature.js | 24 ++++++++++ src/mapml-viewer.js | 15 +++++-- src/mapml/layers/MapMLLayer.js | 44 +++++++++--------- src/web-map.js | 15 +++++-- test/e2e/api/domApi-HTMLLayerElement.html | 47 +++++++------------- test/e2e/api/domApi-HTMLLayerElement.test.js | 9 +++- test/e2e/api/domApi-mapml-viewer.test.js | 6 ++- test/e2e/api/domApi-web-map.test.js | 7 ++- 9 files changed, 108 insertions(+), 70 deletions(-) diff --git a/src/layer.js b/src/layer.js index a7e262999..71b82f84e 100644 --- a/src/layer.js +++ b/src/layer.js @@ -20,8 +20,7 @@ export class MapLayer extends HTMLElement { } set label(val) { if (val) { - if (this._layer && !this._layer.titleIsReadOnly()) - this.setAttribute('label', val); + this.setAttribute('label', val); } } get checked() { @@ -263,7 +262,9 @@ export class MapLayer extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'label': - this?._layer?.setName(newValue); + this.whenReady().then(() => { + this._layer.setName(newValue); + }); break; case 'checked': if (this._layer) { @@ -479,8 +480,8 @@ export class MapLayer extends HTMLElement { resolve(); } else { let layerElement = this; - interval = setInterval(testForLayer, 300, layerElement); - failureTimer = setTimeout(layerNotDefined, 10000); + interval = setInterval(testForLayer, 200, layerElement); + failureTimer = setTimeout(layerNotDefined, 5000); } function testForLayer(layerElement) { if (layerElement._layer) { diff --git a/src/map-feature.js b/src/map-feature.js index eac453928..170358d81 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -616,4 +616,28 @@ export class MapFeature extends HTMLElement { center = map.options.crs.unproject(bound.getCenter(true)); map.setView(center, this.getMaxZoom(), { animate: false }); } + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this._featureGroup) { + resolve(); + } else { + let featureElement = this; + interval = setInterval(testForFeature, 200, featureElement); + failureTimer = setTimeout(featureNotDefined, 5000); + } + function testForFeature(featureElement) { + if (featureElement._featureGroup) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } + } + function featureNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for feature to be ready'); + } + }); + } } diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 69711015f..dcbd37347 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -1318,8 +1318,8 @@ export class MapViewer extends HTMLElement { resolve(); } else { let viewer = this; - interval = setInterval(testForMap, 300, viewer); - failureTimer = setTimeout(mapNotDefined, 10000); + interval = setInterval(testForMap, 200, viewer); + failureTimer = setTimeout(mapNotDefined, 5000); } function testForMap(viewer) { if (viewer._map) { @@ -1335,14 +1335,21 @@ export class MapViewer extends HTMLElement { } }); } + async whenLayersReady() { + let layersReady = []; + for (let layer of [...this.layers]) { + layersReady.push(layer.whenReady()); + } + return Promise.allSettled(layersReady); + } whenProjectionDefined(projection) { return new Promise((resolve, reject) => { let interval, failureTimer; if (M[projection]) { resolve(); } else { - interval = setInterval(testForProjection, 300, projection); - failureTimer = setTimeout(projectionNotDefined, 10000); + interval = setInterval(testForProjection, 200, projection); + failureTimer = setTimeout(projectionNotDefined, 5000); } function testForProjection(p) { if (M[p]) { diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 49c27e918..db63aa5a7 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1975,26 +1975,30 @@ export var MapMLLayer = L.Layer.extend({ content.querySelector('a.mapml-zoom-link').remove(); } if (!featureEl.querySelector('map-geometry')) return; - let tL = featureEl.extent.topLeft.gcrs, - bR = featureEl.extent.bottomRight.gcrs, - center = L.latLngBounds( - L.latLng(tL.horizontal, tL.vertical), - L.latLng(bR.horizontal, bR.vertical) - ).getCenter(true); - let zoomLink = document.createElement('a'); - zoomLink.href = `#${featureEl.getMaxZoom()},${center.lng},${center.lat}`; - zoomLink.innerHTML = `${M.options.locale.popupZoom}`; - zoomLink.className = 'mapml-zoom-link'; - zoomLink.onclick = zoomLink.onkeydown = function (e) { - if (!(e instanceof MouseEvent) && e.keyCode !== 13) return; - e.preventDefault(); - featureEl.zoomTo(); - featureEl._map.closePopup(); - }; - content.insertBefore( - zoomLink, - content.querySelector('hr.mapml-popup-divider') - ); + featureEl.whenReady().then(() => { + let tL = featureEl.extent.topLeft.gcrs, + bR = featureEl.extent.bottomRight.gcrs, + center = L.latLngBounds( + L.latLng(tL.horizontal, tL.vertical), + L.latLng(bR.horizontal, bR.vertical) + ).getCenter(true); + let zoomLink = document.createElement('a'); + zoomLink.href = `#${featureEl.getMaxZoom()},${center.lng},${ + center.lat + }`; + zoomLink.innerHTML = `${M.options.locale.popupZoom}`; + zoomLink.className = 'mapml-zoom-link'; + zoomLink.onclick = zoomLink.onkeydown = function (e) { + if (!(e instanceof MouseEvent) && e.keyCode !== 13) return; + e.preventDefault(); + featureEl.zoomTo(); + featureEl._map.closePopup(); + }; + content.insertBefore( + zoomLink, + content.querySelector('hr.mapml-popup-divider') + ); + }); } // if popup closes then the focusFeature handler can be removed diff --git a/src/web-map.js b/src/web-map.js index 0f85d5409..0004ee14a 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -1357,8 +1357,8 @@ export class WebMap extends HTMLMapElement { resolve(); } else { let viewer = this; - interval = setInterval(testForMap, 300, viewer); - failureTimer = setTimeout(mapNotDefined, 10000); + interval = setInterval(testForMap, 200, viewer); + failureTimer = setTimeout(mapNotDefined, 5000); } function testForMap(viewer) { if (viewer._map) { @@ -1374,14 +1374,21 @@ export class WebMap extends HTMLMapElement { } }); } + async whenLayersReady() { + let layersReady = []; + for (let layer of [...this.layers]) { + layersReady.push(layer.whenReady()); + } + return Promise.allSettled(layersReady); + } whenProjectionDefined(projection) { return new Promise((resolve, reject) => { let interval, failureTimer; if (M[projection]) { resolve(); } else { - interval = setInterval(testForProjection, 300, projection); - failureTimer = setTimeout(projectionNotDefined, 10000); + interval = setInterval(testForProjection, 200, projection); + failureTimer = setTimeout(projectionNotDefined, 5000); } function testForProjection(p) { if (M[p]) { diff --git a/test/e2e/api/domApi-HTMLLayerElement.html b/test/e2e/api/domApi-HTMLLayerElement.html index 967ee926a..311fae6b4 100644 --- a/test/e2e/api/domApi-HTMLLayerElement.html +++ b/test/e2e/api/domApi-HTMLLayerElement.html @@ -42,40 +42,23 @@

A Man With Two Hats

- \ No newline at end of file diff --git a/test/e2e/api/domApi-HTMLLayerElement.test.js b/test/e2e/api/domApi-HTMLLayerElement.test.js index eab6c8be7..4b2b35b5e 100644 --- a/test/e2e/api/domApi-HTMLLayerElement.test.js +++ b/test/e2e/api/domApi-HTMLLayerElement.test.js @@ -16,6 +16,11 @@ test.describe('HTMLLayerElement DOM API Tests', () => { await context.close(); }); test('Setting HTMLLayerElement.label sets the layer name per spec', async () => { + const viewer = await page.locator('mapml-viewer'); + await viewer.evaluate((viewer) => { + return viewer.whenLayersReady(); + }); + await page.waitForTimeout(200); let remoteWithTitleLabel = await page.evaluate(() => { return document.querySelector('#remote-with-title').label; }); @@ -52,8 +57,8 @@ test.describe('HTMLLayerElement DOM API Tests', () => { }); expect(localWithTitleName).toEqual(localWithTitleLabel); - // THIS SHOULD NOT BE NECESSARY, BUT IT IS see comment below - await page.waitForTimeout(500); + // // THIS SHOULD NOT BE NECESSARY, BUT IT IS see comment below + // await page.waitForTimeout(500); let localNoTitleLabel = await page.evaluate(() => { return document.querySelector('#local-no-title').label; }); diff --git a/test/e2e/api/domApi-mapml-viewer.test.js b/test/e2e/api/domApi-mapml-viewer.test.js index 2cde84881..36b019dfe 100644 --- a/test/e2e/api/domApi-mapml-viewer.test.js +++ b/test/e2e/api/domApi-mapml-viewer.test.js @@ -131,6 +131,7 @@ test.describe('mapml-viewer DOM API Tests', () => { }); test('Remove mapml-viewer from DOM, add it back in', async () => { + await page.pause(); // check for error messages in console let errorLogs = []; page.on('pageerror', (err) => { @@ -143,7 +144,10 @@ test.describe('mapml-viewer DOM API Tests', () => { document.body.removeChild(m); document.body.appendChild(m); }); - await viewer.evaluate((viewer)=>viewer.querySelector('layer-').whenReady()); + await viewer.evaluate((viewer) => + viewer.querySelector('layer-').whenReady() + ); + await page.waitForTimeout(250); expect( await viewer.evaluate(() => { let m = document.querySelector('mapml-viewer'); diff --git a/test/e2e/api/domApi-web-map.test.js b/test/e2e/api/domApi-web-map.test.js index b2429d6d9..65a528912 100644 --- a/test/e2e/api/domApi-web-map.test.js +++ b/test/e2e/api/domApi-web-map.test.js @@ -137,7 +137,10 @@ test.describe('web-map DOM API Tests', () => { document.body.removeChild(m); document.body.appendChild(m); }); - await viewer.evaluate((viewer)=>viewer.querySelector('layer-').whenReady()); + await viewer.evaluate((viewer) => + viewer.querySelector('layer-').whenReady() + ); + await page.waitForTimeout(250); expect( await viewer.evaluate(() => { let m = document.querySelector('map'); @@ -1087,7 +1090,7 @@ test.describe('web-map DOM API Tests', () => { (viewer) => viewer.setAttribute('height', '600'), mapHandle ); - + // Adding custom projection const custProj = await page.evaluate((viewer) => { return viewer.defineCustomProjection(template); From bf6e691e95a9bb675b99e4f6e11966d395805d2a Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 10 Aug 2023 13:42:30 -0400 Subject: [PATCH 22/62] Add await whenLayersReady to CustomProjectionLayers.test.js fixture Add await whenReady to fix some tests. Change locator to use page.getByTestId('firstmap') which is less flaky according to recent playwright docs (than selectors). --- test/e2e/core/mapContextMenu.test.js | 7 ++++--- test/e2e/core/mapContextMenuKeyboard.test.js | 4 +++- test/e2e/core/mapElement.html | 4 ++-- test/e2e/layers/CustomProjectionLayers.test.js | 2 ++ test/e2e/layers/layerOpacityAttribute.test.js | 2 ++ 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/test/e2e/core/mapContextMenu.test.js b/test/e2e/core/mapContextMenu.test.js index 1b20612a6..858d69caf 100644 --- a/test/e2e/core/mapContextMenu.test.js +++ b/test/e2e/core/mapContextMenu.test.js @@ -328,7 +328,8 @@ test.describe('Playwright Map Context Menu Tests', () => { test('Submenu, copy map (MapML)', async () => { await page.reload(); - await page.waitForTimeout(3000); + const map = await page.getByTestId('firstmap'); + await map.evaluate((map)=>map.whenLayersReady()); await page.click('body > map'); await page.keyboard.press('Shift+F10'); await page.keyboard.press('Tab'); @@ -341,9 +342,9 @@ test.describe('Playwright Map Context Menu Tests', () => { 'body > textarea#coord', (text) => text.value ); - const expected = ` + const expected = ` - '; mapEl.insertAdjacentHTML('beforeend', l); - mapEl.lastChild.addEventListener('error', function () { + mapEl.lastElementChild.whenReady().catch(() => { if (mapEl) { // should invoke lifecyle callbacks automatically by removing it from DOM mapEl.removeChild(mapEl.lastChild); diff --git a/test/e2e/mapml-viewer/mapml-viewer.html b/test/e2e/mapml-viewer/mapml-viewer.html index 3e1300ec8..950d36e75 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.html +++ b/test/e2e/mapml-viewer/mapml-viewer.html @@ -27,7 +27,7 @@ - + diff --git a/test/e2e/mapml-viewer/mapml-viewer.test.js b/test/e2e/mapml-viewer/mapml-viewer.test.js index 49adbf28c..610094860 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.test.js +++ b/test/e2e/mapml-viewer/mapml-viewer.test.js @@ -177,13 +177,15 @@ test.describe('Playwright mapml-viewer Element Tests', () => { }); test('Paste Invalid link to map using ctrl+v', async () => { + await page.pause(); await page.click('body > textarea#invalidLink'); await page.keyboard.press('Control+a'); await page.keyboard.press('Control+c'); await page.click('body > mapml-viewer'); await page.keyboard.press('Control+v'); - await page.waitForTimeout(1000); + const viewer = await page.getByTestId('testviewer'); + await viewer.evaluate(viewer => viewer.whenLayersReady()); const layerCount = await page.$eval( 'body > mapml-viewer', (map) => map.layers.length diff --git a/test/e2e/web-map/map.html b/test/e2e/web-map/map.html index 41785165d..f943b4891 100644 --- a/test/e2e/web-map/map.html +++ b/test/e2e/web-map/map.html @@ -27,7 +27,7 @@ - diff --git a/test/e2e/web-map/map.test.js b/test/e2e/web-map/map.test.js index 735b61191..d11f47f95 100644 --- a/test/e2e/web-map/map.test.js +++ b/test/e2e/web-map/map.test.js @@ -66,7 +66,8 @@ test.describe('Playwright web-map Element Tests', () => { await page.click('body > map'); await page.keyboard.press('Control+v'); - await page.waitForTimeout(500); + const viewer = await page.getByTestId('testviewer'); + await viewer.evaluate(viewer => viewer.whenLayersReady()); const layerCount = await page.$eval( 'body > map', (map) => map.layers.length From 950a599aec7223c161a422f7fc5813e74e0fccbc Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 10 Aug 2023 18:02:26 -0400 Subject: [PATCH 25/62] Fix layer opacity attribute changed callback, update tests. Formatting by perttier, todo list Fix viewerContextMenu test --- src/layer.js | 2 +- test/e2e/core/layerAttributes.test.js | 19 +++++++-------- test/e2e/mapml-viewer/mapml-viewer.html | 2 +- test/e2e/mapml-viewer/mapml-viewer.test.js | 2 +- .../mapml-viewer/viewerContextMenu.test.js | 13 ++++++---- test/e2e/web-map/map.test.js | 2 +- todo-tests | 24 +++++++++++++++++++ 7 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 todo-tests diff --git a/src/layer.js b/src/layer.js index 71b82f84e..3ec700111 100644 --- a/src/layer.js +++ b/src/layer.js @@ -292,7 +292,7 @@ export class MapLayer extends HTMLElement { break; case 'opacity': if (oldValue !== newValue && this._layer) { - this._layer.changeOpacity(val); + this._layer.changeOpacity(newValue); } break; case 'src': diff --git a/test/e2e/core/layerAttributes.test.js b/test/e2e/core/layerAttributes.test.js index 326a20559..288a4acd9 100644 --- a/test/e2e/core/layerAttributes.test.js +++ b/test/e2e/core/layerAttributes.test.js @@ -101,22 +101,19 @@ test.describe('Playwright Checked Attribute Tests', () => { test.describe('Opacity setters & getters test', () => { test('Setting opacity', async () => { await page.reload(); - await page.$eval( - 'body > mapml-viewer > layer-', - (layer) => (layer.opacity = 0.4) - ); - let value = await page.$eval( - 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset > div:nth-child(2) > details > input[type=range]', - (input) => input.value + const layer = page.getByTestId('testlayer'); + await layer.evaluate((layer) => layer.whenReady()); + await layer.evaluate((layer) => (layer.opacity = 0.4)); + let value = await layer.evaluate( + (layer) => + layer._layer._mapmlLayerItem.querySelector('input[type=range]').value ); expect(value).toEqual('0.4'); }); test('Getting appropriate opacity', async () => { - let value = await page.$eval( - 'body > mapml-viewer > layer-', - (layer) => layer.opacity - ); + const layer = page.getByTestId('testlayer'); + let value = await layer.evaluate((layer) => layer.opacity); expect(value).toEqual('0.4'); }); }); diff --git a/test/e2e/mapml-viewer/mapml-viewer.html b/test/e2e/mapml-viewer/mapml-viewer.html index 950d36e75..7e3dacf01 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.html +++ b/test/e2e/mapml-viewer/mapml-viewer.html @@ -28,7 +28,7 @@ - + diff --git a/test/e2e/mapml-viewer/mapml-viewer.test.js b/test/e2e/mapml-viewer/mapml-viewer.test.js index 610094860..90aa78df2 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.test.js +++ b/test/e2e/mapml-viewer/mapml-viewer.test.js @@ -185,7 +185,7 @@ test.describe('Playwright mapml-viewer Element Tests', () => { await page.click('body > mapml-viewer'); await page.keyboard.press('Control+v'); const viewer = await page.getByTestId('testviewer'); - await viewer.evaluate(viewer => viewer.whenLayersReady()); + await viewer.evaluate((viewer) => viewer.whenLayersReady()); const layerCount = await page.$eval( 'body > mapml-viewer', (map) => map.layers.length diff --git a/test/e2e/mapml-viewer/viewerContextMenu.test.js b/test/e2e/mapml-viewer/viewerContextMenu.test.js index 6cef96fc0..a6b616c30 100644 --- a/test/e2e/mapml-viewer/viewerContextMenu.test.js +++ b/test/e2e/mapml-viewer/viewerContextMenu.test.js @@ -342,8 +342,11 @@ test.describe('Playwright mapml-viewer Context Menu (and api) Tests', () => { test('Submenu, copy map (MapML)', async () => { await page.reload(); - await page.waitForTimeout(3000); - await page.click('body > mapml-viewer'); + const viewer = page.getByTestId('testviewer'); + // have to wait for whenLayersReady because the extent sprouts implicit attributes + // from properties that are set by default + await viewer.evaluate((viewer) => viewer.whenLayersReady()); + await viewer.click(); await page.keyboard.press('Shift+F10'); await page.keyboard.press('Tab'); await page.keyboard.press('Enter'); @@ -355,9 +358,9 @@ test.describe('Playwright mapml-viewer Context Menu (and api) Tests', () => { 'body > textarea#coord', (text) => text.value ); - const expected = ` - - diff --git a/test/e2e/data/images/toporama_en.jpg b/test/e2e/data/images/toporama_en.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cc48026ced505abf5cf353686d5026aa5eab92b GIT binary patch literal 136389 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 z02�^E|V)rQWBKeV2pFH9?)=sx1SU0v+w^wJ!>epfUEO%7gHRGz4j?tOY%#mB_9 z9z`A3*+QlFU1l`lGt{`WJc1TU|J5`)^AH+b;LI;N*|Dgs5aa(=^Obti%9nMZfx_CcKwrD4M~2E3@uFYt>88l%jtU)j?YREC3Mmh-tx=coCL*dNopNP2 z8riAVCPJbM9FxOa?<$^{SME3o7>ye?>0MLZILB7djeHy_PCqY*hEDViAZQhiR#+G> z((yQJq;#{ysWCnG${Wn=e|se#k=mXfBvbkh7Hwogom3zkyaH}b_$)=;t;sI&q=*5Z zDmio76q^sfw0b~K?fDIgvHHmyO5eIpU*ZE$y5;>m^R??y$~d41Gk7o|#yM1$!mJ|( zx=Xu^RvBEQXK*~al&3r`o#{ET5c#O^N+|KO>StRYRC+L6zSN8_*_Knzbg0>cX36;JKz}LE1M% z%W1ZZt*V@3cq4!dajo}|dXa$;dNW3A41?p3f6(<(O@3DOo%s#&u&x)__7ZeVBg)jxhdAw!K0w^ zC6vh8EcO9g4Ge^QPzQ%}7b;lV2}j6r<9F_pxA&(Zz$(g0e%Uz>EF77ox^bDN*BQB4 zbYpY_D=>ppC1z}k;w71rJQJ0~pJQTBO1qCoH=IO)E#5iZ{Ob0;_`#_zyzamW=}swA zEToZH`Cak4l4Ruv=(Tvh>IVJ8_h#?Rf&~^Hiy6ARYeKEW_tI8JTQvQn)R31&2kIa?ge5Q*zm@|k z<%9@2p%?SQA}8a%Os%vW zpAf+*Wr_GEy8?{?yt&`Efqx$J2l{hBNACwVE6fXK+aA4I54a_Jz#AFT?H)Gh4YQwZ z|71(%YO>F<^22d)}swwUew8;7V7kN@=EC56{KZRfm|uSXXy@ z3~|1HR-{HuR5G!4BwKUNSP5Zig4;1I43?hDR1aWi`0Y;eExQVIJR7}7>sb5s{0aH6 zG~s)G!x~x~*R5*@4Mw!&b(EJ*sw+54Fy7hCK)2*nb@ti^uH)r8a)!S&am{@Ne+szR z+HwDm<^0z*pbGstw?)z&MQe0o(vtNE6CTCW2;8|^oK>^h_@Eq}=W2OPw-eOhJyb^m z#NbM~i}4KaO(mm6Kg*QnvG~uu7;t1`eJ%uGIG6~5XZsEDYCcz|ZRL6@6*jfjR{tS| zF!F$JP5uX{F(Atl>eNKhi-*%m$wy))vQc>|=V*SLREG)2wom}W>HMBJWxEeO@Pg0; zmMv{oLck`R;SYzC0!yjWe~Rv4TDcJJWxZe3o1Fu^@;~Dk4cn5-MQc;Y>Y5AQ^%835 zddpHN+HvFF1U;sr9s;bP@@6QP7O4BC# zmH+|n}NF54W@dYjBhrKs*<$-L(hT9H-ETj!ifc-hQ3HPM*6x@Fe8 zW*5xma&ANg0SN-@>n4Pgg7&yv=V9G%czRup=#b`=6At zO=`PQt*5OwjAxr^sukW)Z^E#*j9H_x1BY+!-)#Pr{2V_$nTt>&g@ZRVdsACYwpW#PJX`_&migE; z9wHS*kp_ZxQ*i()V@!&gC|ONNVOmOdlcKJsuy^;elES5`aFS}rm{0dZ$|g7P6S75p{e_#0bXwv{x8+wCx9ZD>i)@y;RLNKYqnwhJ6%;nkhF zveSqzn#>jEUL%{2k5(ea&SWV!H=A$2f&joAFxqg?O5}7#!Wzl#`CLJnQD}hQoeJ_wNK`i4EeG^3=tpn20|_z77TF$S{K?Rq_Ub-VrkT)fs8TR zuM-DVb_ZK|5xJId+i?PWJKCnmaCzdpLq0UZ;Nc>fOfmYy9Q;lo@zYKSE^#_bsA8&q zzbVC{DTU;L0d5>c6j(^8# zX~&j^k<(-R&Td$|IJ5Na&JE>U8=b|Xs5$D)DIve3O?x>08z3$A_rNF6gi8qNoL_!0 z()|X^xyxp?ysuDA24qq$>kpuNgHHqsq7$vXoanR1Pw{U@M@&DO-44%#Afg~4Aj>~5 z#i?!L$BWb^=6+td4)W#(U9j+5Ykc~sE0wUvP>7FgIGt*~uCP@uze?wLfP4f%bNtKd zzF{i2`+#XVdh5(W^u0*T%gh?yJge5a+ud1cPj$Bz3iutceOsv+C5t$}x~ES=g3e4z zCb++Sc!##em=Rn@Y=gM9N=LFHxwy-OjN(`EQ%LJlw1BzRYQ>T7q)7{RQr9i+bG*+=n360dYQ!{XN5{V} zWTf=6ig#d6nm1fD;K%{S?^xlT?OaeB9*L+^ETNyUqtQWh`^>dJe^QRNpF|Ylh9-3H z6~E|t!5vSNuT_k>useQ5;f*dRuly`&1)FCZ%XvL=3=h_?yiK6zu@m=Q^-J+uA}>;h zvvO@zkyCtEC+CS*%UxGMrEHDLfP}=@`irg8pWwXaU-a^S$1!5gL}FJ=g;Q=vL8UWY zq2R?#)MKqMwOcY<1B(j1I^SaTMGveo%>3t~N)lF^Q$A3p_CP{((h*BhHOT|E(L74n zcL4Mn5&h4&`Nq#bmF~l=);r|lOO8)NQq+Ur94&(b^YV(bhN3vs#2*Hz?up^1=UKXO zD!))$2rF-FXDXcwfTxkzwHFWm_WNJg`R!WMcYI$qSTUQ^#>@;l+Cu;_(F#jF-8Q>B zpA?)pTMKhAa0_LIBw-i-cb*+sp_oZy7PG6^YYVMb`cew(lt3tZICetWLd4nx+qLV# z2L%7gddT|YR~Ii)-~PheXTrkfltmvMxq(m2@m3{+b^&)SS(T@+_G)7ptVu+a_&#OH z%&>FV=(Yd`2oorjRimG1E;^1akI(vZK5LQ{GZY8CnH(MxyVqlB$5UE`BxFxrXvfq8 zHpxxL<#_7t3nL{;mn{(zIlf^X`vu%8H*XiRJ&M+{idFJi>7Z_J412bNd0aqe`(>O& z0dK0bo1`CgvD?pa1|3zw;HVg?i7-XrgDyiq-S60}ZbbFnpKjr@Gi?bC&9TO7b%GhY zdU>z9QAEs0wZEF}J--4KVFShdIDuk*bdFV%K%!%9`TAN7T%miITHY~hhzTD2sijct zHaajV<;kafagN_2?3Dd&hJPXSEl}MY8jS%&_2-CZPPj-0gS2DH_Bgf_wEH^}C-+Nc zAqKmtCF?S**Z3YXcbUDeAPe5l&GH#BX{dkJm5-+_ZT4Srk)oJfDj<$IT6Al(n+t4# zj0}1l=c|7CjFSVCIw&%*+xl=?YvDwc|DK$jE44aRefAP>I9z)V{G~cUau$;j@(5A- zo8GNNelQ4A-bK{flQv&?dUT>EXKrHsjA*=h$X)lT+@`#r@{-lVK$`N9Vw*u}lk;R1 zL&4L|3%7#N;Iya$96xp`Y}<>EDU`Up2)UyXW0vaP6Lsmgwy!$XcV!4;Y=VS;Yz`5se^aNBD>3n4v?vQV7rw{OL^VbdqYk4iRsHNA22ZQ?u@ z$Sd`3{*f8~8`Fw0O;=w@Y^!g^jNG|9_n~r*gv_#LeiZWcMW#LDxlKnRjYS%jKR6HK zuV0WVi7n_vS22$Qw=YT&hBo2H9jQjDy_~CH(OukQ%iMaW)pUXm=}oABZ9|Kq%)VTP z>>S%F9~Ka)p9SYWnac*Ig`YGr@zT4^N^1l-zG$5|K0iSB7gpd;y0Z(fET>uNEmaY5 zh^}yl=8p_VL}`D*dnIinGZfcl$+_Qlge~gf;w>j6lr6a_daqHpX3VsI!&`vobs;y0Bo#G3Tw4XeZCjG@vVG(t> ztpPtk9aemtn}=Q4Trx%kQ4vv#IYwjR+aJCwt2v5(Ab4M&ceiNgNYwNE0LiiXk#ZKP z{F$yIxO8g3j<)WN5J?rmBk@V()MKXs(P|3jE7NHix|Iip`pWpaUA(fvoa_#v;usdf zDj4DVdOCAOS(h2F8#dX9TmwV~L;Ggk!?G-43gw4$tXZWBiQ4?(rD}vH-6*iDwu1nmB z%Y$nZ6ytSE#WkuS?gt2aBt6Vpba9*MgzJyrCQRSV5PtdmWrnR9iMz?j*NPVz(*xBW z*j@zrg{}&ceMfGxF+&Our;Rr zIwORe>HI1Sqlg73)ztuHuIb-juA>n!7vdj4|Q+XTgYMX2v9`Rk1Fy0hx~ zDD|f`8l)Z=s{DES=M{h6fK=_3cfW3~Eot_X+lFuKJNUacCeV+0_dBRZ9(BLf%h)TmJMtR82Y!zF z`~%eR2E}5ID6te>d)}NX8_#j(*vyA3Jnk?wcM9$u{Q>%8Y-oPVTxF6s zl;2MO&&~4Z&>*^7>}z3ZLs75D)=ENZ6uJAVNQZIF_|xa^f6y~X|K$$-FIyTfV226` z)Vq7awQFupdW4=MuKwqUHAoMjZv|@F{V}+S$fqd^_eti~u)V+I2v8;qY9tdf@#JIC z(OHxN&pHby{B6wr{UG~CtwU!6Y7x|zCroR^_QaF%w%{xTQ;snXvo@l8{x*;Omm~L| zH#Aqt*WB*@mh)HR_MxyiLqBnVHDhr;Ui3eiC;s`}{(d5g7Y(ozzAs^(jGm{~*~orD z?(kHQD7hXB=g~0t4VX9y`azN4KOXqqv0`NjujtKVJ#n<{TWX`J_D50&q4R&~jp^He z9mz8 - + diff --git a/test/e2e/layers/multipleExtents.html b/test/e2e/layers/multipleExtents.html index fed6f2032..4731e977b 100644 --- a/test/e2e/layers/multipleExtents.html +++ b/test/e2e/layers/multipleExtents.html @@ -49,7 +49,7 @@ + tref="images/toporama_en.jpg?SERVICE=WMS&REQUEST=GetMap&FORMAT=image/jpeg&TRANSPARENT=FALSE&STYLES=&VERSION=1.3.0&LAYERS=WMS-Toporama&WIDTH={w}&HEIGHT={h}&CRS=EPSG:3978&BBOX={xmin},{ymin},{xmax},{ymax}&m4h=t" > diff --git a/test/e2e/layers/step/imageStep.test.js b/test/e2e/layers/step/imageStep.test.js index 7b723a072..4576ea5a3 100644 --- a/test/e2e/layers/step/imageStep.test.js +++ b/test/e2e/layers/step/imageStep.test.js @@ -9,7 +9,7 @@ test.describe('Templated image layer with step', () => { 1, 0, 0, - 'https://maps.geogratis.gc.ca/wms/toporama_en?SERVICE=WMS&REQUEST=GetMap&FORMAT=image/jpeg&TRANSPARENT=FALSE&STYLES=&VERSION=1.3.0&LAYERS=WMS-Toporama&WIDTH=300&HEIGHT=150&CRS=EPSG:3978&BBOX=', + '/images/toporama_en.jpg', '-5537023.0124460235,-2392385.4881043136,5972375.006350018,3362313.521293707&m4h=t', '', '-968982.6263652518,-107703.83540767431,1412272.136144273,1082923.545847088&m4h=t', diff --git a/test/e2e/layers/step/templatedImageLayerStep.html b/test/e2e/layers/step/templatedImageLayerStep.html index bd4bcb9d4..236419676 100644 --- a/test/e2e/layers/step/templatedImageLayerStep.html +++ b/test/e2e/layers/step/templatedImageLayerStep.html @@ -39,7 +39,7 @@ + tref="/images/toporama_en.jpg?SERVICE=WMS&REQUEST=GetMap&FORMAT=image/jpeg&TRANSPARENT=FALSE&STYLES=&VERSION=1.3.0&LAYERS=WMS-Toporama&WIDTH={w}&HEIGHT={h}&CRS=EPSG:3978&BBOX={xmin},{ymin},{xmax},{ymax}&m4h=t" >
diff --git a/test/e2e/layers/templatedImageLayer.html b/test/e2e/layers/templatedImageLayer.html index 5dbe7df21..ac709a1a7 100644 --- a/test/e2e/layers/templatedImageLayer.html +++ b/test/e2e/layers/templatedImageLayer.html @@ -42,7 +42,7 @@ + tref="images/toporama_en.jpg?WIDTH={w}&HEIGHT={h}&CRS=EPSG:3978&BBOX={xmin},{ymin},{xmax},{ymax}&m4h=t" > diff --git a/test/server.js b/test/server.js index 88bfd261d..061f1065a 100644 --- a/test/server.js +++ b/test/server.js @@ -73,6 +73,7 @@ app.get('/data/noMapMeta', (req, res, next) => { app.use('/data', express.static(path.join(__dirname, 'e2e/data/tiles/cbmt'))); app.use('/data', express.static(path.join(__dirname, 'e2e/data/tiles/wgs84'))); +app.use('/images', express.static(path.join(__dirname, 'e2e/data/images'))); app.use( '/data', express.static(path.join(__dirname, 'e2e/data/tiles/osmtile')) From c4aa613375d518c9df67cfaef9d062b6cdf1471c Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 26 Sep 2023 15:22:57 -0400 Subject: [PATCH 62/62] Fix onZoomEnd bug: only remove affected features based on their zoom and the zoom of the map. --- src/mapml/layers/FeatureLayer.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index c310448f9..f37d90942 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -157,15 +157,10 @@ export var FeatureLayer = L.FeatureGroup.extend({ }, _handleZoomEnd: function (e) { - let mapZoom = this._map.getZoom(); - if ( - this.zoomBounds && - (mapZoom > this.zoomBounds.maxZoom || mapZoom < this.zoomBounds.minZoom) - ) { - this.clearLayers(); - return; + // handle zoom end gets called twice for every zoom, this condition makes it go through once only. + if (this.zoomBounds) { + this._resetFeatures(); } - this._resetFeatures(); }, // remove or add features based on the min max attribute of the features,