From 8ab0e3317988827b20ba530f493a2c4b5bc08d37 Mon Sep 17 00:00:00 2001 From: prushfor Date: Wed, 15 Nov 2023 15:53:45 -0500 Subject: [PATCH 01/46] Sketch out a path for refactoring to create functional map-link --- src/map-extent.js | 207 ---------------------- src/map-link.js | 268 ++++++++++++++++++++++++++++- src/mapml/layers/TemplatedLayer.js | 46 ----- 3 files changed, 267 insertions(+), 254 deletions(-) diff --git a/src/map-extent.js b/src/map-extent.js index 0aa6bdbf1..3545a73bf 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -275,213 +275,6 @@ export class MapExtent extends HTMLElement { } } - _initTemplateVars(metaExtent, projection, mapml, base, projectionMatch) { - function transcribe(element) { - var select = document.createElement('select'); - var elementAttrNames = element.getAttributeNames(); - - for (let i = 0; i < elementAttrNames.length; i++) { - select.setAttribute( - elementAttrNames[i], - element.getAttribute(elementAttrNames[i]) - ); - } - - var options = element.children; - - for (let i = 0; i < options.length; i++) { - var option = document.createElement('option'); - var optionAttrNames = options[i].getAttributeNames(); - - for (let j = 0; j < optionAttrNames.length; j++) { - option.setAttribute( - optionAttrNames[j], - options[i].getAttribute(optionAttrNames[j]) - ); - } - - option.innerHTML = options[i].innerHTML; - select.appendChild(option); - } - return select; - } - var templateVars = []; - // set up the URL template and associated inputs (which yield variable values when processed) - var tlist = this.querySelectorAll( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ), - varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), - zoomInput = this.querySelector('map-input[type="zoom" i]'), - includesZoom = false, - boundsFallback = {}; - - boundsFallback.zoom = 0; - if (metaExtent) { - let content = M._metaContentToObject(metaExtent.getAttribute('content')), - cs; - - boundsFallback.zoom = content.zoom || boundsFallback.zoom; - - let metaKeys = Object.keys(content); - for (let i = 0; i < metaKeys.length; i++) { - if (!metaKeys[i].includes('zoom')) { - cs = M.axisToCS(metaKeys[i].split('-')[2]); - break; - } - } - let axes = M.csToAxes(cs); - boundsFallback.bounds = M.boundsToPCRSBounds( - L.bounds( - L.point( - +content[`top-left-${axes[0]}`], - +content[`top-left-${axes[1]}`] - ), - L.point( - +content[`bottom-right-${axes[0]}`], - +content[`bottom-right-${axes[1]}`] - ) - ), - boundsFallback.zoom, - projection, - cs - ); - } else { - // for custom projections, M[projection] may not be loaded, so uses M['OSMTILE'] as backup, this code will need to get rerun once projection is changed and M[projection] is available - // TODO: This is a temporary fix, _initTemplateVars (or processinitialextent) should not be called when projection of the layer and map do not match, this should be called/reinitialized once the layer projection matches with the map projection - let fallbackProjection = M[projection] || M.OSMTILE; - boundsFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; - } - - for (var i = 0; i < tlist.length; i++) { - var t = tlist[i], - template = t.getAttribute('tref'); - t.zoomInput = zoomInput; - if (!template) { - template = M.BLANK_TT_TREF; - let blankInputs = mapml.querySelectorAll('map-input'); - for (let i of blankInputs) { - template += `{${i.getAttribute('name')}}`; - } - } - - var v, - title = t.hasAttribute('title') - ? t.getAttribute('title') - : 'Query this layer', - vcount = template.match(varNamesRe), - trel = - !t.hasAttribute('rel') || - t.getAttribute('rel').toLowerCase() === 'tile' - ? 'tile' - : t.getAttribute('rel').toLowerCase(), - ttype = !t.hasAttribute('type') - ? 'image/*' - : t.getAttribute('type').toLowerCase(), - inputs = [], - tms = t && t.hasAttribute('tms'); - var zoomBounds = mapml.querySelector('map-meta[name=zoom]') - ? M._metaContentToObject( - mapml.querySelector('map-meta[name=zoom]').getAttribute('content') - ) - : undefined; - while ((v = varNamesRe.exec(template)) !== null) { - var varName = v[1], - inp = this.querySelector( - 'map-input[name=' + varName + '],map-select[name=' + varName + ']' - ); - if (inp) { - if ( - inp.hasAttribute('type') && - inp.getAttribute('type') === 'location' && - (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && - inp.hasAttribute('axis') && - !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) - ) { - if ( - zoomInput && - template.includes(`{${zoomInput.getAttribute('name')}}`) - ) { - zoomInput.setAttribute('value', boundsFallback.zoom); - } - let axis = inp.getAttribute('axis'), - axisBounds = M.convertPCRSBounds( - boundsFallback.bounds, - boundsFallback.zoom, - projection, - M.axisToCS(axis) - ); - inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); - inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); - } - - inputs.push(inp); - includesZoom = - includesZoom || - (inp.hasAttribute('type') && - inp.getAttribute('type').toLowerCase() === 'zoom'); - if (inp.tagName.toLowerCase() === 'map-select') { - // use a throwaway div to parse the input from MapML into HTML - var div = document.createElement('div'); - div.insertAdjacentHTML('afterbegin', inp.outerHTML); - // parse - inp.htmlselect = div.querySelector('map-select'); - inp.htmlselect = transcribe(inp.htmlselect); - - // this goes into the layer control, so add a listener - L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); - - if (!this._userInputs) { - this._userInputs = []; - } - this._userInputs.push(inp.htmlselect); - } - // TODO: if this is an input@type=location - // get the TCRS min,max attribute values at the identified zoom level - // save this information as properties of the mapExtent, - // perhaps as a bounds object so that it can be easily used - // later by the layer control to determine when to enable - // disable the layer for drawing. - } else { - console.log( - 'input with name=' + - varName + - ' not found for template variable of same name' - ); - // no match found, template won't be used - break; - } - } - if ( - (template && vcount.length === inputs.length) || - template === M.BLANK_TT_TREF - ) { - if (trel === 'query') { - this._layer.queryable = true; - } - if (!includesZoom && zoomInput) { - inputs.push(zoomInput); - } - let step = zoomInput ? zoomInput.getAttribute('step') : 1; - if (!step || step === '0' || isNaN(step)) step = 1; - // template has a matching input for every variable reference {varref} - templateVars.push({ - template: decodeURI(new URL(template, base)), - linkEl: t, - title: title, - rel: trel, - type: ttype, - values: inputs, - zoomBounds: zoomBounds, - boundsFallbackPCRS: { bounds: boundsFallback.bounds }, - projectionMatch: projectionMatch, - projection: this.units || M.FALLBACK_PROJECTION, - tms: tms, - step: step - }); - } - } - return templateVars; - } redraw() { this._templatedLayer.redraw(); } diff --git a/src/map-link.js b/src/map-link.js index 2ebf0ed70..e38d4fc42 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -147,8 +147,274 @@ export class MapLink extends HTMLElement { // Always call super first in constructor super(); } - connectedCallback() {} + connectedCallback() { + + // parse tref for variable names + // find sibling map-input with that name, link to them via js reference + // resolve the tref against the appropriate base URL so as to be absolute + // do the above via call to _initTemplateVars or rename thereof + // create the layer type appropriate to the rel value, so long as the + // parsing has gone well + for (var i = 0; i < templates.length; i++) { + if (templates[i].rel === 'tile') { + this.setZIndex(options.extentZIndex); + this._templates[i].layer = M.templatedTileLayer( + templates[i], + L.Util.extend(options, { + errorTileUrl: + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + zIndex: options.extentZIndex, + pane: this._container + }) + ); + } else if (templates[i].rel === 'image') { + this.setZIndex(options.extentZIndex); + this._templates[i].layer = M.templatedImageLayer( + templates[i], + L.Util.extend(options, { + zIndex: options.extentZIndex, + pane: this._container + }) + ); + } else if (templates[i].rel === 'features') { + this.setZIndex(options.extentZIndex); + this._templates[i].layer = M.templatedFeaturesLayer( + templates[i], + L.Util.extend(options, { + zIndex: options.extentZIndex, + pane: this._container + }) + ); + } else if (templates[i].rel === 'query') { + // add template to array of queryies to be added to map and processed + // on click/tap events + this.hasSetBoundsHandler = true; + if (!this._queries) { + this._queries = []; + } + let inputData = M._extractInputBounds(templates[i]); + templates[i].extentBounds = inputData.bounds; + templates[i].zoomBounds = inputData.zoomBounds; + templates[i]._extentEl = this.options.extentEl; + this._queries.push( + L.extend(templates[i], this._setupQueryVars(templates[i])) + ); + } + } + // create the type of templated leaflet layer appropriate to the rel value + // image/map/features = templated(Image/Feature), tile=templatedTile, + this._tempatedTileLayer = M.templatedTile(pane: this.extentElement._leafletLayer._container) + // add to viewer._map dependant on map-extent.checked, layer-.checked + // what else? + } disconnectedCallback() {} + _initTemplateVars(metaExtent, projection, mapml, base, projectionMatch) { + function transcribe(element) { + var select = document.createElement('select'); + var elementAttrNames = element.getAttributeNames(); + + for (let i = 0; i < elementAttrNames.length; i++) { + select.setAttribute( + elementAttrNames[i], + element.getAttribute(elementAttrNames[i]) + ); + } + + var options = element.children; + + for (let i = 0; i < options.length; i++) { + var option = document.createElement('option'); + var optionAttrNames = options[i].getAttributeNames(); + + for (let j = 0; j < optionAttrNames.length; j++) { + option.setAttribute( + optionAttrNames[j], + options[i].getAttribute(optionAttrNames[j]) + ); + } + + option.innerHTML = options[i].innerHTML; + select.appendChild(option); + } + return select; + } + var templateVars = []; + // set up the URL template and associated inputs (which yield variable values when processed) + var tlist = this.querySelectorAll( + 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' + ), + varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), + zoomInput = this.querySelector('map-input[type="zoom" i]'), + includesZoom = false, + boundsFallback = {}; + + boundsFallback.zoom = 0; + if (metaExtent) { + let content = M._metaContentToObject(metaExtent.getAttribute('content')), + cs; + + boundsFallback.zoom = content.zoom || boundsFallback.zoom; + + let metaKeys = Object.keys(content); + for (let i = 0; i < metaKeys.length; i++) { + if (!metaKeys[i].includes('zoom')) { + cs = M.axisToCS(metaKeys[i].split('-')[2]); + break; + } + } + let axes = M.csToAxes(cs); + boundsFallback.bounds = M.boundsToPCRSBounds( + L.bounds( + L.point( + +content[`top-left-${axes[0]}`], + +content[`top-left-${axes[1]}`] + ), + L.point( + +content[`bottom-right-${axes[0]}`], + +content[`bottom-right-${axes[1]}`] + ) + ), + boundsFallback.zoom, + projection, + cs + ); + } else { + // for custom projections, M[projection] may not be loaded, so uses M['OSMTILE'] as backup, this code will need to get rerun once projection is changed and M[projection] is available + // TODO: This is a temporary fix, _initTemplateVars (or processinitialextent) should not be called when projection of the layer and map do not match, this should be called/reinitialized once the layer projection matches with the map projection + let fallbackProjection = M[projection] || M.OSMTILE; + boundsFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; + } + + for (var i = 0; i < tlist.length; i++) { + var t = tlist[i], + template = t.getAttribute('tref'); + t.zoomInput = zoomInput; + if (!template) { + template = M.BLANK_TT_TREF; + let blankInputs = mapml.querySelectorAll('map-input'); + for (let i of blankInputs) { + template += `{${i.getAttribute('name')}}`; + } + } + + var v, + title = t.hasAttribute('title') + ? t.getAttribute('title') + : 'Query this layer', + vcount = template.match(varNamesRe), + trel = + !t.hasAttribute('rel') || + t.getAttribute('rel').toLowerCase() === 'tile' + ? 'tile' + : t.getAttribute('rel').toLowerCase(), + ttype = !t.hasAttribute('type') + ? 'image/*' + : t.getAttribute('type').toLowerCase(), + inputs = [], + tms = t && t.hasAttribute('tms'); + var zoomBounds = mapml.querySelector('map-meta[name=zoom]') + ? M._metaContentToObject( + mapml.querySelector('map-meta[name=zoom]').getAttribute('content') + ) + : undefined; + while ((v = varNamesRe.exec(template)) !== null) { + var varName = v[1], + inp = this.querySelector( + 'map-input[name=' + varName + '],map-select[name=' + varName + ']' + ); + if (inp) { + if ( + inp.hasAttribute('type') && + inp.getAttribute('type') === 'location' && + (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && + inp.hasAttribute('axis') && + !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) + ) { + if ( + zoomInput && + template.includes(`{${zoomInput.getAttribute('name')}}`) + ) { + zoomInput.setAttribute('value', boundsFallback.zoom); + } + let axis = inp.getAttribute('axis'), + axisBounds = M.convertPCRSBounds( + boundsFallback.bounds, + boundsFallback.zoom, + projection, + M.axisToCS(axis) + ); + inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); + inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); + } + + inputs.push(inp); + includesZoom = + includesZoom || + (inp.hasAttribute('type') && + inp.getAttribute('type').toLowerCase() === 'zoom'); + if (inp.tagName.toLowerCase() === 'map-select') { + // use a throwaway div to parse the input from MapML into HTML + var div = document.createElement('div'); + div.insertAdjacentHTML('afterbegin', inp.outerHTML); + // parse + inp.htmlselect = div.querySelector('map-select'); + inp.htmlselect = transcribe(inp.htmlselect); + + // this goes into the layer control, so add a listener + L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); + + if (!this._userInputs) { + this._userInputs = []; + } + this._userInputs.push(inp.htmlselect); + } + // TODO: if this is an input@type=location + // get the TCRS min,max attribute values at the identified zoom level + // save this information as properties of the mapExtent, + // perhaps as a bounds object so that it can be easily used + // later by the layer control to determine when to enable + // disable the layer for drawing. + } else { + console.log( + 'input with name=' + + varName + + ' not found for template variable of same name' + ); + // no match found, template won't be used + break; + } + } + if ( + (template && vcount.length === inputs.length) || + template === M.BLANK_TT_TREF + ) { + if (trel === 'query') { + this._layer.queryable = true; + } + if (!includesZoom && zoomInput) { + inputs.push(zoomInput); + } + let step = zoomInput ? zoomInput.getAttribute('step') : 1; + if (!step || step === '0' || isNaN(step)) step = 1; + // template has a matching input for every variable reference {varref} + templateVars.push({ + template: decodeURI(new URL(template, base)), + linkEl: t, + title: title, + rel: trel, + type: ttype, + values: inputs, + zoomBounds: zoomBounds, + boundsFallbackPCRS: { bounds: boundsFallback.bounds }, + projectionMatch: projectionMatch, + projection: this.units || M.FALLBACK_PROJECTION, + tms: tms, + step: step + }); + } + } + return templateVars; + } // Resolve the templated URL with info from the sibling map-input's resolve() { diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index b393d0152..fb5b08fe5 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -7,52 +7,6 @@ export var TemplatedLayer = L.Layer.extend({ this.changeOpacity(this.options.opacity); L.DomUtil.addClass(this._container, 'mapml-templatedlayer-container'); - for (var i = 0; i < templates.length; i++) { - if (templates[i].rel === 'tile') { - this.setZIndex(options.extentZIndex); - this._templates[i].layer = M.templatedTileLayer( - templates[i], - L.Util.extend(options, { - errorTileUrl: - 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', - zIndex: options.extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'image') { - this.setZIndex(options.extentZIndex); - this._templates[i].layer = M.templatedImageLayer( - templates[i], - L.Util.extend(options, { - zIndex: options.extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'features') { - this.setZIndex(options.extentZIndex); - this._templates[i].layer = M.templatedFeaturesLayer( - templates[i], - L.Util.extend(options, { - zIndex: options.extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'query') { - // add template to array of queryies to be added to map and processed - // on click/tap events - this.hasSetBoundsHandler = true; - if (!this._queries) { - this._queries = []; - } - let inputData = M._extractInputBounds(templates[i]); - templates[i].extentBounds = inputData.bounds; - templates[i].zoomBounds = inputData.zoomBounds; - templates[i]._extentEl = this.options.extentEl; - this._queries.push( - L.extend(templates[i], this._setupQueryVars(templates[i])) - ); - } - } }, getEvents: function () { return { From d07a463f67e580d85fe0a28e2035e4b98591d8ad Mon Sep 17 00:00:00 2001 From: prushfor Date: Wed, 15 Nov 2023 15:55:09 -0500 Subject: [PATCH 02/46] Stop CI action for now --- .github/workflows/ci-testing.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-testing.yml b/.github/workflows/ci-testing.yml index 07c378cf4..1e379c16b 100644 --- a/.github/workflows/ci-testing.yml +++ b/.github/workflows/ci-testing.yml @@ -13,12 +13,12 @@ jobs: uses: actions/setup-node@v3 with: node-version: '18.x' - - run: sudo apt-get install xvfb - - run: npm install --legacy-peer-deps - - run: npx playwright install - - run: npm install -g grunt-cli - - run: grunt default - - run: xvfb-run --auto-servernum -- npm test +# - run: sudo apt-get install xvfb +# - run: npm install --legacy-peer-deps +# - run: npx playwright install +# - 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 From 2693cff82105e3a57790beb4a8c026f2c95f2fa8 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 16 Nov 2023 16:07:31 -0500 Subject: [PATCH 03/46] WIP on map-link: * change map-extent._templatedLayer to map-extent._extentLayer * add map-extent.getMetaExtent() function to return closest map-meta extent * add comments where code should be in map-input for moved code from map-extent etc * get rid of map-link `title` attribute, we think never used * add comment and methods for map-select custom element in layer control * move map-select code from map-extent module to map-select.js --- src/layer.js | 4 +- src/map-extent.js | 58 ++- src/map-input.js | 25 + src/map-link.js | 467 +++++++++--------- src/map-select.js | 89 +++- .../extents/createLayerControlForExtent.js | 44 +- .../elementSupport/inputs/locationInput.js | 2 +- src/mapml/layers/MapMLLayer.js | 12 +- 8 files changed, 404 insertions(+), 297 deletions(-) diff --git a/src/layer.js b/src/layer.js index cfb9629dc..54d30d073 100644 --- a/src/layer.js +++ b/src/layer.js @@ -343,12 +343,12 @@ export class MapLayer extends HTMLElement { '_staticTileLayer', '_imageLayer', '_mapmlvectors', - '_templatedLayer' + '_extentLayer' ]; for (let j = 0; j < layerTypes.length; j++) { let type = layerTypes[j]; if (this.checked) { - if (type === '_templatedLayer' && mapExtents.length > 0) { + if (type === '_extentLayer' && mapExtents.length > 0) { for (let i = 0; i < mapExtents.length; i++) { totalExtentCount++; if (mapExtents[i]._validateDisabled()) disabledExtentCount++; diff --git a/src/map-extent.js b/src/map-extent.js index 3545a73bf..da0e92585 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -4,6 +4,7 @@ export class MapExtent extends HTMLElement { return ['checked', 'label', 'opacity', 'hidden']; } get units() { + // this should fallback to something?? return this.getAttribute('units'); } @@ -72,8 +73,8 @@ export class MapExtent extends HTMLElement { case 'opacity': if (oldValue !== newValue) { this._opacity = newValue; - if (this._templatedLayer) - this._templatedLayer.changeOpacity(newValue); + if (this._extentLayer) + this._extentLayer.changeOpacity(newValue); } break; case 'hidden': @@ -176,7 +177,7 @@ export class MapExtent extends HTMLElement { // this._opacity is used to record the current opacity value (with or without updates), // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 this._opacity = this.opacity || 1.0; - this._templatedLayer = M.templatedLayer(this._templateVars, { + this._extentLayer = M.templatedLayer(this._templateVars, { pane: this._layer._container, opacity: this.opacity, _leafletLayer: this._layer, @@ -193,11 +194,11 @@ export class MapExtent extends HTMLElement { if (!this.hidden) this._layer.addExtentToLayerControl(this._layerControlHTML); this._validateLayerControlContainerHidden(); - if (this._templatedLayer._queries) { + if (this._extentLayer._queries) { if (!this._layer._properties._queries) this._layer._properties._queries = []; this._layer._properties._queries = - this._layer._properties._queries.concat(this._templatedLayer._queries); + this._layer._properties._queries.concat(this._extentLayer._queries); } this._calculateBounds(); } @@ -211,15 +212,15 @@ export class MapExtent extends HTMLElement { ); } _validateDisabled() { - if (!this._templatedLayer) return; + if (!this._extentLayer) return; const noTemplateVisible = () => { - let totalTemplateCount = this._templatedLayer._templates.length, + let totalTemplateCount = this._extentLayer._templates.length, disabledTemplateCount = 0; - for (let j = 0; j < this._templatedLayer._templates.length; j++) { - if (this._templatedLayer._templates[j].rel === 'query') { + for (let j = 0; j < this._extentLayer._templates.length; j++) { + if (this._extentLayer._templates[j].rel === 'query') { continue; } - if (!this._templatedLayer._templates[j].layer.isVisible) { + if (!this._extentLayer._templates[j].layer.isVisible) { disabledTemplateCount++; } } @@ -235,7 +236,16 @@ export class MapExtent extends HTMLElement { this.toggleLayerControlDisabled(); return this.disabled; } - + getMetaExtent() { + return this.parentLayer.shadowRoot + ? this.parentLayer.shadowRoot.querySelector( + 'map-extent > map-meta[name=extent]' + ) || + this.parentLayer.shadowRoot.querySelector('map-meta[name=extent]') + : this.parentLayer.querySelector( + 'map-extent > map-meta[name=extent]' + ) || this.parentLayer.querySelector('map-meta[name=extent]'); + } // disable/italicize layer control elements based on the map-extent.disabled property toggleLayerControlDisabled() { let input = this._layerControlCheckbox, @@ -276,20 +286,20 @@ export class MapExtent extends HTMLElement { } redraw() { - this._templatedLayer.redraw(); + this._extentLayer.redraw(); } _handleChange() { - // if the parent layer- is checked, add _templatedLayer to map if map-extent is checked, otherwise remove it + // if the parent layer- is checked, add _extentLayer to map if map-extent is checked, otherwise remove it if (this.checked && this.parentLayer.checked && !this.disabled) { - this._templatedLayer.addTo(this._layer._map); - this._templatedLayer.setZIndex( + this._extentLayer.addTo(this._layer._map); + this._extentLayer.setZIndex( Array.from(this.parentLayer.querySelectorAll('map-extent')).indexOf( this ) ); } else { - this._map.removeLayer(this._templatedLayer); + this._map.removeLayer(this._extentLayer); } // change the checkbox in the layer control to match map-extent.checked // doesn't trigger the event handler because it's not user-caused AFAICT @@ -308,20 +318,20 @@ export class MapExtent extends HTMLElement { disconnectedCallback() { // in case of projection change, the disconnectedcallback will be triggered by removing layer-._layer even before // map-extent.connectedcallback is finished (because it will wait for the layer- to be ready) - // !this._templatedLayer <=> this.connectedCallback has not yet been finished before disconnectedCallback is triggered + // !this._extentLayer <=> this.connectedCallback has not yet been finished before disconnectedCallback is triggered if ( this.hasAttribute('data-moving') || this.parentLayer.hasAttribute('data-moving') || - !this._templatedLayer + !this._extentLayer ) return; this._validateLayerControlContainerHidden(); // remove layer control for map-extent from layer control DOM this._layerControlHTML.remove(); - this._map.removeLayer(this._templatedLayer); + this._map.removeLayer(this._extentLayer); this.parentLayer.removeEventListener('map-change', this._changeHandler); this.mapEl.removeEventListener('map-projectionchange', this._changeHandler); - delete this._templatedLayer; + delete this._extentLayer; delete this.parentLayer.bounds; } _calculateBounds() { @@ -363,8 +373,8 @@ export class MapExtent extends HTMLElement { } } // cannot be named as layerBounds if we decide to keep the debugoverlay logic - this._templatedLayer.bounds = bounds; - this._templatedLayer.zoomBounds = { + this._extentLayer.bounds = bounds; + this._extentLayer.zoomBounds = { minZoom: zoomMin, maxZoom: zoomMax, maxNativeZoom, @@ -375,7 +385,7 @@ export class MapExtent extends HTMLElement { whenReady() { return new Promise((resolve, reject) => { let interval, failureTimer; - if (this._templatedLayer) { + if (this._extentLayer) { resolve(); } else { let extentElement = this; @@ -383,7 +393,7 @@ export class MapExtent extends HTMLElement { failureTimer = setTimeout(extentNotDefined, 10000); } function testForExtent(extentElement) { - if (extentElement._templatedLayer) { + if (extentElement._extentLayer) { clearInterval(interval); clearTimeout(failureTimer); resolve(); diff --git a/src/map-input.js b/src/map-input.js index bb3472829..f7125a174 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -14,6 +14,31 @@ export class MapInput extends HTMLElement { 'step' ]; } +// sets default values for min,max on zoom and location input +// this stuff should be handled by the default getters on map-input type=location or map-input type=zoom +// if ( +// inp.hasAttribute('type') && +// inp.getAttribute('type') === 'location' && +// (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && +// inp.hasAttribute('axis') && +// !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) +// ) { +// if ( +// zoomInput && +// template.includes(`{${zoomInput.getAttribute('name')}}`) +// ) { +// zoomInput.setAttribute('value', boundsFallback.zoom); +// } +// let axis = inp.getAttribute('axis'), +// axisBounds = M.convertPCRSBounds( +// boundsFallback.bounds, +// boundsFallback.zoom, +// projection, +// M.axisToCS(axis) +// ); +// inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); +// inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); +// } get name() { return this.getAttribute('name'); diff --git a/src/map-link.js b/src/map-link.js index e38d4fc42..c307c20c3 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -3,7 +3,7 @@ export class MapLink extends HTMLElement { return [ 'type', 'rel', - 'title', +// 'title', 'href', 'hreflang', 'tref', @@ -21,6 +21,8 @@ export class MapLink extends HTMLElement { } } get rel() { + // rel value has no default value + // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#:~:text=The%20rel%20attribute%20has%20no%20default%20value. return this.getAttribute('rel'); } set rel(val) { @@ -44,14 +46,14 @@ export class MapLink extends HTMLElement { this.setAttribute('type', val); } } - get title() { - return this.getAttribute('title'); - } - set title(val) { - if (val) { - this.setAttribute('title', val); - } - } +// get title() { +// return this.getAttribute('title'); +// } +// set title(val) { +// if (val) { +// this.setAttribute('title', val); +// } +// } get href() { return this.getAttribute('href'); } @@ -71,6 +73,14 @@ export class MapLink extends HTMLElement { } } get tref() { + // maybe no longer necessary, set up a default value for tref +// if (!template) { +// template = M.BLANK_TT_TREF; +// let blankInputs = mapml.querySelectorAll('map-input'); +// for (let i of blankInputs) { +// template += `{${i.getAttribute('name')}}`; +// } +// } return this.getAttribute('tref'); } set tref(val) { @@ -99,10 +109,16 @@ export class MapLink extends HTMLElement { } attributeChangedCallback(name, oldValue, newValue) { - //['type','rel','title','href','hreflang','tref','tms','projection']; + //['type','rel','href','hreflang','tref','tms','projection']; + // fold to lowercase switch (name) { case 'type': +// ttype = !t.hasAttribute('type') +// ? 'image/*' +// : t.getAttribute('type').toLowerCase(), + if (oldValue !== newValue) { + // default value image/* // handle side effects } break; @@ -111,11 +127,11 @@ export class MapLink extends HTMLElement { // handle side effects } break; - case 'title': - if (oldValue !== newValue) { - // handle side effects - } - break; +// case 'title': +// if (oldValue !== newValue) { +// // handle side effects +// } +// break; case 'href': if (oldValue !== newValue) { // handle side effects @@ -148,226 +164,166 @@ export class MapLink extends HTMLElement { super(); } connectedCallback() { - + switch(this.rel.toLowerCase()) { + // for some cases, require a dependency check + case "tile": + case "image": + case "features": + case "query": + this._createTemplatedLink(); + break; + case "style": + case "self": + case "style self": + case "self style": + this._createSelfOrStyleLink(); + break; + case "zoomin": + case "zoomout": + this._createZoominOrZoomoutLink(); + break; + case "legend": + this._createLegendLink(); + break; + case "stylesheet": + this._createStylesheetLink(); + break; + case "alternate": + this._createAlternateLink(); // add media attribute + break; + case "license": + this._createLicenseLink(); + break; + } + // create the type of templated leaflet layer appropriate to the rel value + // image/map/features = templated(Image/Feature), tile=templatedTile, + // this._tempatedTileLayer = M.templatedTile(pane: this.extentElement._leafletLayer._container) + // add to viewer._map dependant on map-extent.checked, layer-.checked + // what else? + } + + async _createTemplatedLink() { + // conditions check + // the tms and type attributes are optional, may need to be checked in future + this.parentExtent = + this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT' + ? this.parentNode + : this.parentNode.host; + if (!this.tref || !this.parentExtent) return; + await this.parentExtent.whenReady(); // parse tref for variable names // find sibling map-input with that name, link to them via js reference // resolve the tref against the appropriate base URL so as to be absolute // do the above via call to _initTemplateVars or rename thereof // create the layer type appropriate to the rel value, so long as the // parsing has gone well - for (var i = 0; i < templates.length; i++) { - if (templates[i].rel === 'tile') { - this.setZIndex(options.extentZIndex); - this._templates[i].layer = M.templatedTileLayer( - templates[i], - L.Util.extend(options, { - errorTileUrl: - 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', - zIndex: options.extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'image') { - this.setZIndex(options.extentZIndex); - this._templates[i].layer = M.templatedImageLayer( - templates[i], - L.Util.extend(options, { - zIndex: options.extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'features') { - this.setZIndex(options.extentZIndex); - this._templates[i].layer = M.templatedFeaturesLayer( - templates[i], - L.Util.extend(options, { - zIndex: options.extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'query') { - // add template to array of queryies to be added to map and processed - // on click/tap events - this.hasSetBoundsHandler = true; - if (!this._queries) { - this._queries = []; - } - let inputData = M._extractInputBounds(templates[i]); - templates[i].extentBounds = inputData.bounds; - templates[i].zoomBounds = inputData.zoomBounds; - templates[i]._extentEl = this.options.extentEl; - this._queries.push( - L.extend(templates[i], this._setupQueryVars(templates[i])) - ); + this.zIndex = Array.from(this.parentExtent.querySelectorAll('map-link')).indexOf(this); + if (this.rel === 'tile') { + this._templatedLayer = M.templatedTileLayer( + this._templateVars, + L.Util.extend(options, { + errorTileUrl: + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + zIndex: this.zIndex, + pane: this.parentExtent._tempatedLayer._container + }) + ); + } else if (this.rel === 'image') { + this._templatedLayer = M.templatedImageLayer( + this._templateVars, + L.Util.extend(options, { + zIndex: this.zIndex, + pane: this.parentExtent._tempatedLayer._container + }) + ); + } else if (this.rel === 'features') { + this._templatedLayer = M.templatedFeaturesLayer( + this._templateVars, + L.Util.extend(options, { + zIndex: this.zIndex, + pane: this.parentExtent._tempatedLayer._container + }) + ); + } else if (this.rel === 'query') { + // add template to array of queryies to be added to map and processed + // on click/tap events + this.hasSetBoundsHandler = true; + if (!this._queries) { + this._queries = []; } + let inputData = M._extractInputBounds(this); + this.extentBounds = inputData.bounds; + this.zoomBounds = inputData.zoomBounds; + this._extentEl = this.parentExtent; + this._queries.push( + // need to refactor the _setupQueryVars args / migrate it to map-link? + L.extend(this, this.parentExtent._extentLayer._setupQueryVars(this)) + ); } - // create the type of templated leaflet layer appropriate to the rel value - // image/map/features = templated(Image/Feature), tile=templatedTile, - this._tempatedTileLayer = M.templatedTile(pane: this.extentElement._leafletLayer._container) - // add to viewer._map dependant on map-extent.checked, layer-.checked - // what else? } disconnectedCallback() {} - _initTemplateVars(metaExtent, projection, mapml, base, projectionMatch) { - function transcribe(element) { - var select = document.createElement('select'); - var elementAttrNames = element.getAttributeNames(); - - for (let i = 0; i < elementAttrNames.length; i++) { - select.setAttribute( - elementAttrNames[i], - element.getAttribute(elementAttrNames[i]) - ); - } - - var options = element.children; - - for (let i = 0; i < options.length; i++) { - var option = document.createElement('option'); - var optionAttrNames = options[i].getAttributeNames(); - - for (let j = 0; j < optionAttrNames.length; j++) { - option.setAttribute( - optionAttrNames[j], - options[i].getAttribute(optionAttrNames[j]) - ); - } - - option.innerHTML = options[i].innerHTML; - select.appendChild(option); - } - return select; - } - var templateVars = []; + // there's a new function in map-extent.getMetaExtent() which was created + // to be used to pass its return value in here as metaExtent... + _initTemplateVars() { // set up the URL template and associated inputs (which yield variable values when processed) - var tlist = this.querySelectorAll( - 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' - ), - varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), +// var tlist = this.querySelectorAll( +// 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' +// ), + var varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), + // 'this' is parentExtent actually zoomInput = this.querySelector('map-input[type="zoom" i]'), - includesZoom = false, - boundsFallback = {}; - - boundsFallback.zoom = 0; - if (metaExtent) { - let content = M._metaContentToObject(metaExtent.getAttribute('content')), - cs; - - boundsFallback.zoom = content.zoom || boundsFallback.zoom; - - let metaKeys = Object.keys(content); - for (let i = 0; i < metaKeys.length; i++) { - if (!metaKeys[i].includes('zoom')) { - cs = M.axisToCS(metaKeys[i].split('-')[2]); - break; - } - } - let axes = M.csToAxes(cs); - boundsFallback.bounds = M.boundsToPCRSBounds( - L.bounds( - L.point( - +content[`top-left-${axes[0]}`], - +content[`top-left-${axes[1]}`] - ), - L.point( - +content[`bottom-right-${axes[0]}`], - +content[`bottom-right-${axes[1]}`] - ) - ), - boundsFallback.zoom, - projection, - cs - ); - } else { - // for custom projections, M[projection] may not be loaded, so uses M['OSMTILE'] as backup, this code will need to get rerun once projection is changed and M[projection] is available - // TODO: This is a temporary fix, _initTemplateVars (or processinitialextent) should not be called when projection of the layer and map do not match, this should be called/reinitialized once the layer projection matches with the map projection - let fallbackProjection = M[projection] || M.OSMTILE; - boundsFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; - } + includesZoom = false; - for (var i = 0; i < tlist.length; i++) { - var t = tlist[i], - template = t.getAttribute('tref'); - t.zoomInput = zoomInput; - if (!template) { - template = M.BLANK_TT_TREF; - let blankInputs = mapml.querySelectorAll('map-input'); - for (let i of blankInputs) { - template += `{${i.getAttribute('name')}}`; - } - } + var template = this.tref; + this.zoomInput = zoomInput; - 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; + var v, vcount = template.match(varNamesRe), inputs = []; +// set up zoomBounds for link in map-link.extent property + var zoomBounds = this.getZoomBounds(); while ((v = varNamesRe.exec(template)) !== null) { var varName = v[1], - inp = this.querySelector( + inp = this.parentExtent.querySelector( 'map-input[name=' + varName + '],map-select[name=' + varName + ']' ); if (inp) { - if ( - inp.hasAttribute('type') && - inp.getAttribute('type') === 'location' && - (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && - inp.hasAttribute('axis') && - !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) - ) { - if ( - zoomInput && - template.includes(`{${zoomInput.getAttribute('name')}}`) - ) { - zoomInput.setAttribute('value', boundsFallback.zoom); - } - let axis = inp.getAttribute('axis'), - axisBounds = M.convertPCRSBounds( - boundsFallback.bounds, - boundsFallback.zoom, - projection, - M.axisToCS(axis) - ); - inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); - inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); - } +// this stuff should be handled by the default getters on map-input type=location or map-input type=zoom +// if ( +// inp.hasAttribute('type') && +// inp.getAttribute('type') === 'location' && +// (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && +// inp.hasAttribute('axis') && +// !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) +// ) { +// if ( +// zoomInput && +// template.includes(`{${zoomInput.getAttribute('name')}}`) +// ) { +// zoomInput.setAttribute('value', boundsFallback.zoom); +// } +// let axis = inp.getAttribute('axis'), +// axisBounds = M.convertPCRSBounds( +// boundsFallback.bounds, +// boundsFallback.zoom, +// projection, +// M.axisToCS(axis) +// ); +// inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); +// inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); +// } + // this "associates" the input to this map-link inputs.push(inp); + + // I think this means that regardless of whether the tref includes + // a reference to the zoom input, it gets associated to the link + // and used (to specify the native zoom bounds??) for the templated(Tile|Image|Features)Layer includesZoom = includesZoom || (inp.hasAttribute('type') && inp.getAttribute('type').toLowerCase() === 'zoom'); - if (inp.tagName.toLowerCase() === 'map-select') { - // use a throwaway div to parse the input from MapML into HTML - var div = document.createElement('div'); - div.insertAdjacentHTML('afterbegin', inp.outerHTML); - // parse - inp.htmlselect = div.querySelector('map-select'); - inp.htmlselect = transcribe(inp.htmlselect); - - // this goes into the layer control, so add a listener - L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); - - if (!this._userInputs) { - this._userInputs = []; - } - this._userInputs.push(inp.htmlselect); - } + // moved a block to map-select to transcribe the map-select into an + // actual html select for inclusion in the layer control. + // TODO: if this is an input@type=location // get the TCRS min,max attribute values at the identified zoom level // save this information as properties of the mapExtent, @@ -380,13 +336,10 @@ export class MapLink extends HTMLElement { varName + ' not found for template variable of same name' ); - // no match found, template won't be used - break; } } if ( - (template && vcount.length === inputs.length) || - template === M.BLANK_TT_TREF + (template && vcount.length === inputs.length) ) { if (trel === 'query') { this._layer.queryable = true; @@ -397,23 +350,85 @@ export class MapLink extends HTMLElement { 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, + this._templateVars = { + template: decodeURI(new URL(template, this.parentExtent._layer.getBase())), + linkEl: this, + rel: this.rel, + type: this.type, values: inputs, zoomBounds: zoomBounds, - boundsFallbackPCRS: { bounds: boundsFallback.bounds }, - projectionMatch: projectionMatch, - projection: this.units || M.FALLBACK_PROJECTION, - tms: tms, + boundsFallbackPCRS: { bounds: this.getBounds().bounds }, + projection: this.parentExtent.units || M.FALLBACK_PROJECTION, + tms: this.tms, step: step - }); + }; + } + } + getBounds() { + let boundsFallback = {}; + + boundsFallback.zoom = 0; + if (metaExtent) { + let content = M._metaContentToObject(metaExtent.getAttribute('content')), + cs; + + boundsFallback.zoom = content.zoom || boundsFallback.zoom; + + let metaKeys = Object.keys(content); + for (let i = 0; i < metaKeys.length; i++) { + if (!metaKeys[i].includes('zoom')) { + cs = M.axisToCS(metaKeys[i].split('-')[2]); + break; + } } + let axes = M.csToAxes(cs); + boundsFallback.bounds = M.boundsToPCRSBounds( + L.bounds( + L.point( + +content[`top-left-${axes[0]}`], + +content[`top-left-${axes[1]}`] + ), + L.point( + +content[`bottom-right-${axes[0]}`], + +content[`bottom-right-${axes[1]}`] + ) + ), + boundsFallback.zoom, + projection, + cs + ); + } else { + // for custom projections, M[projection] may not be loaded, so uses M['OSMTILE'] as backup, this code will need to get rerun once projection is changed and M[projection] is available + // TODO: This is a temporary fix, _initTemplateVars (or processinitialextent) should not be called when projection of the layer and map do not match, this should be called/reinitialized once the layer projection matches with the map projection + let fallbackProjection = M[projection] || M.OSMTILE; + boundsFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; } - return templateVars; + return boundsFallback; + } + getZoomBounds() { + // should return BOTH min/max(Display)Zoom AND min/maxNativeZoom which + // are options that can be passed to L.GridLayer... + // https://leafletjs.com/reference.html#gridlayer-minzoom + // returns object like this: + // { minZoom: n, maxZoom: n, minNativeZoom: n, maxNativeZoom: n } + // + // native variables should ONLY come from map-input min/max attributes + // BUT they should fall back to map-meta or projection values for min/max (display) zoom + // display zoom variables should be EQUAL to native unless specified differently + // via map-meta name=zoom + // in particular minNativeZoom being > minZoom can be problematic because + // you fetch tiles at larger scales (i.e. many many small tiles) and render + // them at smaller scale (i.e. little postage stamps), which can freez your + // browser and bury a tile cache in requests, getting you banned/blocked + // + // search document from here up, using closest source of zoom bounds info + // following code was the original, and 'mapml' was the layerEl._layer._content + // so not the upward-search suggested above. +// mapml.querySelector('map-meta[name=zoom]') +// ? M._metaContentToObject( +// mapml.querySelector('map-meta[name=zoom]').getAttribute('content') +// ) +// : undefined; } // Resolve the templated URL with info from the sibling map-input's diff --git a/src/map-select.js b/src/map-select.js index fdb130ceb..a10022974 100644 --- a/src/map-select.js +++ b/src/map-select.js @@ -33,7 +33,94 @@ export class MapSelect extends HTMLElement { // Always call super first in constructor super(); } - connectedCallback() {} + connectedCallback() { + // origin of this block was in _initTemplateVars from map-extent, which was + // originally part of MapMLLayer... + // +// if (inp.tagName.toLowerCase() === 'map-select') { +// // use a throwaway div to parse the input from MapML into HTML +// var div = document.createElement('div'); +// div.insertAdjacentHTML('afterbegin', inp.outerHTML); +// // parse +// inp.htmlselect = div.querySelector('map-select'); +// inp.htmlselect = transcribe(inp.htmlselect); +// +// // this goes into the layer control, so add a listener +// L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); +// +// this refers to map-extent in the original +// if (!this._userInputs) { +// this._userInputs = []; +// } +// this._userInputs.push(inp.htmlselect); +// } + } disconnectedCallback() {} + createLayerControlForSelect() { + // cut-pasted from createLayerControlForExtent. TODO Re-write appropriately + var templates = this._templateVars; + if (templates) { + this._selectdetails = []; + for (var i = 0; i < templates.length; i++) { + var template = templates[i]; + for (var j = 0; j < template.values.length; j++) { + var mapmlInput = template.values[j], + id = '#' + mapmlInput.getAttribute('id'); + // don't add it again if it is referenced > once + if ( + mapmlInput.tagName.toLowerCase() === 'map-select' && + !frag.querySelector(id) + ) { + // generate a
+ var selectdetails = L.DomUtil.create( + 'details', + 'mapml-layer-item-details mapml-control-layers', + frag + ), + selectsummary = L.DomUtil.create('summary'), + selectSummaryLabel = L.DomUtil.create('label'); + selectSummaryLabel.innerText = mapmlInput.getAttribute('name'); + selectSummaryLabel.setAttribute( + 'for', + mapmlInput.getAttribute('id') + ); + selectsummary.appendChild(selectSummaryLabel); + selectdetails.appendChild(selectsummary); + selectdetails.appendChild(mapmlInput.htmlselect); + this._selectdetails.push(selectdetails); + } + } + } + } + } + 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; + } } window.customElements.define('map-select', MapSelect); diff --git a/src/mapml/elementSupport/extents/createLayerControlForExtent.js b/src/mapml/elementSupport/extents/createLayerControlForExtent.js index 80c76df89..dab6e2804 100644 --- a/src/mapml/elementSupport/extents/createLayerControlForExtent.js +++ b/src/mapml/elementSupport/extents/createLayerControlForExtent.js @@ -50,41 +50,11 @@ export var createLayerControlExtentHTML = function () { svgExtentControlIcon.appendChild(extentControlPath1); svgExtentControlIcon.appendChild(extentControlPath2); - if (this._userInputs) { + let mapSelects = this.querySelectorAll('map-select'); + if (mapSelects.length) { var frag = document.createDocumentFragment(); - var templates = this._templateVars; - if (templates) { - this._selectdetails = []; - for (var i = 0; i < templates.length; i++) { - var template = templates[i]; - for (var j = 0; j < template.values.length; j++) { - var mapmlInput = template.values[j], - id = '#' + mapmlInput.getAttribute('id'); - // don't add it again if it is referenced > once - if ( - mapmlInput.tagName.toLowerCase() === 'map-select' && - !frag.querySelector(id) - ) { - // generate a
- var selectdetails = L.DomUtil.create( - 'details', - 'mapml-layer-item-details mapml-control-layers', - frag - ), - selectsummary = L.DomUtil.create('summary'), - selectSummaryLabel = L.DomUtil.create('label'); - selectSummaryLabel.innerText = mapmlInput.getAttribute('name'); - selectSummaryLabel.setAttribute( - 'for', - mapmlInput.getAttribute('id') - ); - selectsummary.appendChild(selectSummaryLabel); - selectdetails.appendChild(selectsummary); - selectdetails.appendChild(mapmlInput.htmlselect); - this._selectdetails.push(selectdetails); - } - } - } + for (var i=0;i< mapSelects.length;i++){ + frag.appendChild(mapSelects[i].createLayerControlForSelect()); } extentSettings.appendChild(frag); } @@ -145,11 +115,11 @@ export var createLayerControlExtentHTML = function () { ); const changeOpacity = function (e) { if (e && e.target && e.target.value >= 0 && e.target.value <= 1.0) { - this._templatedLayer.changeOpacity(e.target.value); + this._extentLayer.changeOpacity(e.target.value); } }; opacity.setAttribute('value', this.opacity); - opacity.value = this._templatedLayer._container.style.opacity || '1.0'; + opacity.value = this._extentLayer._container.style.opacity || '1.0'; opacity.addEventListener('change', changeOpacity.bind(this)); var extentItemNameSpan = L.DomUtil.create( @@ -261,7 +231,7 @@ export var createLayerControlExtentHTML = function () { extentEl.removeAttribute('data-moving'); extentEl.extentZIndex = zIndex; - extentEl._templatedLayer.setZIndex(zIndex); + extentEl._extentLayer.setZIndex(zIndex); zIndex++; } } diff --git a/src/mapml/elementSupport/inputs/locationInput.js b/src/mapml/elementSupport/inputs/locationInput.js index fb4e7b880..d35d8ce83 100644 --- a/src/mapml/elementSupport/inputs/locationInput.js +++ b/src/mapml/elementSupport/inputs/locationInput.js @@ -94,7 +94,7 @@ export class LocationInput { // TODO: change return value as needed based on usage by map-input // https://github.com/Leaflet/Leaflet/blob/6994baf25f267db1c8b720c28a61e0700d0aa0e8/src/layer/tile/GridLayer.js#L652 const center = this.layer._map.getCenter(); - const templatedTileLayer = this.layer._templatedLayer._templates[0].layer; + const templatedTileLayer = this.layer._extentLayer._templates[0].layer; const pixelBounds = templatedTileLayer._getTiledPixelBounds(center); const tileRange = templatedTileLayer._pxBoundsToTileRange(pixelBounds); let obj = []; diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 670f5ca54..ad8c8827d 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -119,7 +119,7 @@ export var MapMLLayer = L.Layer.extend({ '_staticTileLayer', '_imageLayer', '_mapmlvectors', - '_templatedLayer' + '_extentLayer' ]; const mapExtents = this._layerEl.querySelectorAll('map-extent').length ? this._layerEl.querySelectorAll('map-extent') @@ -127,14 +127,14 @@ export var MapMLLayer = L.Layer.extend({ ? this._layerEl.shadowRoot.querySelectorAll('map-extent') : []; layerTypes.forEach((type) => { - if (type === '_templatedLayer' && mapExtents.length) { + if (type === '_extentLayer' && mapExtents.length) { let zoomMax = zoomBounds.maxZoom, zoomMin = zoomBounds.minZoom, maxNativeZoom = zoomBounds.maxNativeZoom, minNativeZoom = zoomBounds.minNativeZoom; for (let i = 0; i < mapExtents.length; i++) { - if (mapExtents[i]._templatedLayer.bounds) { - let templatedLayer = mapExtents[i]._templatedLayer; + if (mapExtents[i]._extentLayer.bounds) { + let templatedLayer = mapExtents[i]._extentLayer; if (!bounds) { bounds = templatedLayer.bounds; zoomBounds = templatedLayer.zoomBounds; @@ -692,11 +692,11 @@ export var MapMLLayer = L.Layer.extend({ // if the popup is for a static / templated feature, the "zoom to here" link can be attached once the popup opens attachZoomLink.call(popup); } else { - // getting access to the first map-extent to get access to _templatedLayer to use it's (possibly) generic _previousFeature + _nextFeature methods. + // getting access to the first map-extent to get access to _extentLayer to use it's (possibly) generic _previousFeature + _nextFeature methods. const mapExtent = popup._source._layerEl.querySelector('map-extent') || popup._source._layerEl.shadowRoot.querySelector('map-extent'); - layer = mapExtent._templatedLayer; + layer = mapExtent._extentLayer; // if the popup is for a query, the "zoom to here" link should be re-attached every time new pagination features are displayed map.on('attachZoomLink', attachZoomLink, popup); } From 3f624b14d7210c605574b3a4ed351aeb6d856874 Mon Sep 17 00:00:00 2001 From: Aliyan Haq <55751566+AliyanH@users.noreply.github.com> Date: Fri, 17 Nov 2023 10:01:58 -0500 Subject: [PATCH 04/46] add comments to attributes to associate them with rel --- src/map-link.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/map-link.js b/src/map-link.js index c307c20c3..9d319e7a8 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -113,6 +113,7 @@ export class MapLink extends HTMLElement { // fold to lowercase switch (name) { case 'type': + // rel = tile, features, etc. TBD when it is used // ttype = !t.hasAttribute('type') // ? 'image/*' // : t.getAttribute('type').toLowerCase(), @@ -123,6 +124,7 @@ export class MapLink extends HTMLElement { } break; case 'rel': + // mandatory attribute, no default value if (oldValue !== newValue) { // handle side effects } @@ -133,26 +135,33 @@ export class MapLink extends HTMLElement { // } // break; case 'href': + // rel = license, legend, stylesheet, self, style, self style, style self, zoomin, zoomout if (oldValue !== newValue) { // handle side effects } break; case 'hreflang': + // rel = *all* + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#hreflang + // idea is that we can have multiple map-links with different hreflang, and map-extent chooses a map-link that matches with user's lang. Not a priority. - create an use-case issue? if (oldValue !== newValue) { // handle side effects } break; case 'tref': + // rel = tile, image, features, query if (oldValue !== newValue) { // handle side effects } break; case 'tms': + // rel = tile if (oldValue !== newValue) { // handle side effects } break; case 'projection': + // rel = alternate if (oldValue !== newValue) { // handle side effects } From 79a93603281d3544dd16ec8437bb8d24b72873cf Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 17 Nov 2023 12:49:41 -0500 Subject: [PATCH 05/46] WIP on map-link et al. * remove call to _initTemplateVars from map-extent connectedCallback; * set the M.templatedLayer templateVars param to null in map-extent connectedCallback * work on map-link _initTemplateVars to get partially working * TODO; revisit how the bounds are established for map-link._templateVars * prettier formatting... --- index.html | 49 +-- src/map-extent.js | 36 +- src/map-input.js | 50 +-- src/map-link.js | 312 +++++++++--------- src/map-select.js | 36 +- .../extents/createLayerControlForExtent.js | 2 +- src/mapml/layers/TemplatedLayer.js | 1 - 7 files changed, 215 insertions(+), 271 deletions(-) diff --git a/index.html b/index.html index 7040693c5..a3ddb3fbc 100644 --- a/index.html +++ b/index.html @@ -76,60 +76,13 @@ + - - - diff --git a/src/map-extent.js b/src/map-extent.js index da0e92585..aac65b7bc 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -73,8 +73,7 @@ export class MapExtent extends HTMLElement { case 'opacity': if (oldValue !== newValue) { this._opacity = newValue; - if (this._extentLayer) - this._extentLayer.changeOpacity(newValue); + if (this._extentLayer) this._extentLayer.changeOpacity(newValue); } break; case 'hidden': @@ -155,29 +154,13 @@ export class MapExtent extends HTMLElement { this._map = this._layer._map; // reset the layer extent delete this.parentLayer.bounds; - this._templateVars = this._initTemplateVars( - // read map-meta[name=extent] from shadowroot or layer- - // querySelector / querySelectorAll on layer- cannot get elements inside its shadowroot - this.parentLayer.shadowRoot - ? this.parentLayer.shadowRoot.querySelector( - 'map-extent > map-meta[name=extent]' - ) || - this.parentLayer.shadowRoot.querySelector('map-meta[name=extent]') - : this.parentLayer.querySelector( - 'map-extent > map-meta[name=extent]' - ) || this.parentLayer.querySelector('map-meta[name=extent]'), - this.units, - this._layer._content, - this._layer.getBase(), - this.units === this._layer.options.mapprojection - ); this._changeHandler = this._handleChange.bind(this); this.parentLayer.addEventListener('map-change', this._changeHandler); this.mapEl.addEventListener('map-projectionchange', this._changeHandler); // this._opacity is used to record the current opacity value (with or without updates), // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 this._opacity = this.opacity || 1.0; - this._extentLayer = M.templatedLayer(this._templateVars, { + this._extentLayer = M.templatedLayer(null, { pane: this._layer._container, opacity: this.opacity, _leafletLayer: this._layer, @@ -236,15 +219,14 @@ export class MapExtent extends HTMLElement { this.toggleLayerControlDisabled(); return this.disabled; } - getMetaExtent() { + getMeta(metaName) { + let name = metaName.toLowerCase(); + if (name !== 'extent' && name !== 'zoom') return; return this.parentLayer.shadowRoot - ? this.parentLayer.shadowRoot.querySelector( - 'map-extent > map-meta[name=extent]' - ) || - this.parentLayer.shadowRoot.querySelector('map-meta[name=extent]') - : this.parentLayer.querySelector( - 'map-extent > map-meta[name=extent]' - ) || this.parentLayer.querySelector('map-meta[name=extent]'); + ? this.querySelector(`map-meta[name=${name}]`) || + this.parentLayer.shadowRoot.querySelector(`map-meta[name=${name}]`) + : this.querySelector(`map-meta[name=${name}]`) || + this.parentLayer.querySelector(`map-meta[name=${name}]`); } // disable/italicize layer control elements based on the map-extent.disabled property toggleLayerControlDisabled() { diff --git a/src/map-input.js b/src/map-input.js index f7125a174..c8ef60c9e 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -14,31 +14,31 @@ export class MapInput extends HTMLElement { 'step' ]; } -// sets default values for min,max on zoom and location input -// this stuff should be handled by the default getters on map-input type=location or map-input type=zoom -// if ( -// inp.hasAttribute('type') && -// inp.getAttribute('type') === 'location' && -// (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && -// inp.hasAttribute('axis') && -// !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) -// ) { -// if ( -// zoomInput && -// template.includes(`{${zoomInput.getAttribute('name')}}`) -// ) { -// zoomInput.setAttribute('value', boundsFallback.zoom); -// } -// let axis = inp.getAttribute('axis'), -// axisBounds = M.convertPCRSBounds( -// boundsFallback.bounds, -// boundsFallback.zoom, -// projection, -// M.axisToCS(axis) -// ); -// inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); -// inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); -// } + // sets default values for min,max on zoom and location input + // this stuff should be handled by the default getters on map-input type=location or map-input type=zoom + // if ( + // inp.hasAttribute('type') && + // inp.getAttribute('type') === 'location' && + // (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && + // inp.hasAttribute('axis') && + // !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) + // ) { + // if ( + // zoomInput && + // template.includes(`{${zoomInput.getAttribute('name')}}`) + // ) { + // zoomInput.setAttribute('value', boundsFallback.zoom); + // } + // let axis = inp.getAttribute('axis'), + // axisBounds = M.convertPCRSBounds( + // boundsFallback.bounds, + // boundsFallback.zoom, + // projection, + // M.axisToCS(axis) + // ); + // inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); + // inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); + // } get name() { return this.getAttribute('name'); diff --git a/src/map-link.js b/src/map-link.js index 9d319e7a8..430ba93d2 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -1,9 +1,11 @@ +/* global M */ + export class MapLink extends HTMLElement { static get observedAttributes() { return [ 'type', 'rel', -// 'title', + // 'title', 'href', 'hreflang', 'tref', @@ -21,7 +23,7 @@ export class MapLink extends HTMLElement { } } get rel() { - // rel value has no default value + // rel value has no default value // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#:~:text=The%20rel%20attribute%20has%20no%20default%20value. return this.getAttribute('rel'); } @@ -46,14 +48,14 @@ export class MapLink extends HTMLElement { this.setAttribute('type', val); } } -// get title() { -// return this.getAttribute('title'); -// } -// set title(val) { -// if (val) { -// this.setAttribute('title', val); -// } -// } + // get title() { + // return this.getAttribute('title'); + // } + // set title(val) { + // if (val) { + // this.setAttribute('title', val); + // } + // } get href() { return this.getAttribute('href'); } @@ -74,13 +76,13 @@ export class MapLink extends HTMLElement { } get tref() { // maybe no longer necessary, set up a default value for tref -// if (!template) { -// template = M.BLANK_TT_TREF; -// let blankInputs = mapml.querySelectorAll('map-input'); -// for (let i of blankInputs) { -// template += `{${i.getAttribute('name')}}`; -// } -// } + // if (!template) { + // template = M.BLANK_TT_TREF; + // let blankInputs = mapml.querySelectorAll('map-input'); + // for (let i of blankInputs) { + // template += `{${i.getAttribute('name')}}`; + // } + // } return this.getAttribute('tref'); } set tref(val) { @@ -114,9 +116,9 @@ export class MapLink extends HTMLElement { switch (name) { case 'type': // rel = tile, features, etc. TBD when it is used -// ttype = !t.hasAttribute('type') -// ? 'image/*' -// : t.getAttribute('type').toLowerCase(), + // ttype = !t.hasAttribute('type') + // ? 'image/*' + // : t.getAttribute('type').toLowerCase(), if (oldValue !== newValue) { // default value image/* @@ -127,13 +129,16 @@ export class MapLink extends HTMLElement { // mandatory attribute, no default value if (oldValue !== newValue) { // handle side effects + if (newValue === 'query') { + this.parentExtent.parentLayer._layer.queryable = true; + } } break; -// case 'title': -// if (oldValue !== newValue) { -// // handle side effects -// } -// break; + // case 'title': + // if (oldValue !== newValue) { + // // handle side effects + // } + // break; case 'href': // rel = license, legend, stylesheet, self, style, self style, style self, zoomin, zoomout if (oldValue !== newValue) { @@ -151,7 +156,8 @@ export class MapLink extends HTMLElement { case 'tref': // rel = tile, image, features, query if (oldValue !== newValue) { - // handle side effects + // create or reset the _templateVars property + this._initTemplateVars(); } break; case 'tms': @@ -173,34 +179,34 @@ export class MapLink extends HTMLElement { super(); } connectedCallback() { - switch(this.rel.toLowerCase()) { + switch (this.rel.toLowerCase()) { // for some cases, require a dependency check - case "tile": - case "image": - case "features": - case "query": + case 'tile': + case 'image': + case 'features': + case 'query': this._createTemplatedLink(); break; - case "style": - case "self": - case "style self": - case "self style": + case 'style': + case 'self': + case 'style self': + case 'self style': this._createSelfOrStyleLink(); break; - case "zoomin": - case "zoomout": + case 'zoomin': + case 'zoomout': this._createZoominOrZoomoutLink(); break; - case "legend": + case 'legend': this._createLegendLink(); break; - case "stylesheet": + case 'stylesheet': this._createStylesheetLink(); break; - case "alternate": + case 'alternate': this._createAlternateLink(); // add media attribute break; - case "license": + case 'license': this._createLicenseLink(); break; } @@ -210,7 +216,7 @@ export class MapLink extends HTMLElement { // add to viewer._map dependant on map-extent.checked, layer-.checked // what else? } - + async _createTemplatedLink() { // conditions check // the tms and type attributes are optional, may need to be checked in future @@ -226,7 +232,9 @@ export class MapLink extends HTMLElement { // do the above via call to _initTemplateVars or rename thereof // create the layer type appropriate to the rel value, so long as the // parsing has gone well - this.zIndex = Array.from(this.parentExtent.querySelectorAll('map-link')).indexOf(this); + this.zIndex = Array.from( + this.parentExtent.querySelectorAll('map-link') + ).indexOf(this); if (this.rel === 'tile') { this._templatedLayer = M.templatedTileLayer( this._templateVars, @@ -271,112 +279,89 @@ export class MapLink extends HTMLElement { } } disconnectedCallback() {} - // there's a new function in map-extent.getMetaExtent() which was created + // there's a new function in map-extent.getMeta('extent'|'zoom') which was created // to be used to pass its return value in here as metaExtent... _initTemplateVars() { // set up the URL template and associated inputs (which yield variable values when processed) -// var tlist = this.querySelectorAll( -// 'map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]' -// ), - var varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), - // 'this' is parentExtent actually - zoomInput = this.querySelector('map-input[type="zoom" i]'), - includesZoom = false; + var varNamesRe = new RegExp('(?:{)(.*?)(?:})', 'g'), + zoomInput = this.parentElement.querySelector('map-input[type="zoom" i]'), + includesZoom = false, + linkedZoomInput; - var template = this.tref; - this.zoomInput = zoomInput; + var template = this.tref; + this.zoomInput = zoomInput; - var v, vcount = template.match(varNamesRe), inputs = []; -// set up zoomBounds for link in map-link.extent property - var zoomBounds = this.getZoomBounds(); - while ((v = varNamesRe.exec(template)) !== null) { - var varName = v[1], - inp = this.parentExtent.querySelector( - 'map-input[name=' + varName + '],map-select[name=' + varName + ']' - ); - if (inp) { -// this stuff should be handled by the default getters on map-input type=location or map-input type=zoom -// if ( -// inp.hasAttribute('type') && -// inp.getAttribute('type') === 'location' && -// (!inp.hasAttribute('min') || !inp.hasAttribute('max')) && -// inp.hasAttribute('axis') && -// !['i', 'j'].includes(inp.getAttribute('axis').toLowerCase()) -// ) { -// if ( -// zoomInput && -// template.includes(`{${zoomInput.getAttribute('name')}}`) -// ) { -// zoomInput.setAttribute('value', boundsFallback.zoom); -// } -// let axis = inp.getAttribute('axis'), -// axisBounds = M.convertPCRSBounds( -// boundsFallback.bounds, -// boundsFallback.zoom, -// projection, -// M.axisToCS(axis) -// ); -// inp.setAttribute('min', axisBounds.min[M.axisToXY(axis)]); -// inp.setAttribute('max', axisBounds.max[M.axisToXY(axis)]); -// } + var v, + vcount = template.match(varNamesRe), + inputs = []; + while ((v = varNamesRe.exec(template)) !== null) { + let varName = v[1], + inp = this.parentExtent.querySelector( + 'map-input[name=' + varName + '],map-select[name=' + varName + ']' + ); + if (inp) { + // this "associates" the input to this map-link + inputs.push(inp); - // this "associates" the input to this map-link - inputs.push(inp); - - // I think this means that regardless of whether the tref includes - // a reference to the zoom input, it gets associated to the link - // and used (to specify the native zoom bounds??) for the templated(Tile|Image|Features)Layer - includesZoom = - includesZoom || - (inp.hasAttribute('type') && - inp.getAttribute('type').toLowerCase() === 'zoom'); - // moved a block to map-select to transcribe the map-select into an - // actual html select for inclusion in the layer control. - - // TODO: if this is an input@type=location - // get the TCRS min,max attribute values at the identified zoom level - // save this information as properties of the mapExtent, - // perhaps as a bounds object so that it can be easily used - // later by the layer control to determine when to enable - // disable the layer for drawing. - } else { - console.log( - 'input with name=' + - varName + - ' not found for template variable of same name' - ); + // I think this means that regardless of whether the tref includes + // a reference to the zoom input, it gets associated to the link + // and used (to specify the native zoom bounds??) for the templated(Tile|Image|Features)Layer + if ( + inp.hasAttribute('type') && + inp.getAttribute('type').toLowerCase() === 'zoom' + ) { + linkedZoomInput = inp; } + includesZoom = + includesZoom || + (inp.hasAttribute('type') && + inp.getAttribute('type').toLowerCase() === 'zoom'); + // moved a block to map-select to transcribe the map-select into an + // actual html select for inclusion in the layer control. + + // TODO: if this is an input@type=location + // get the TCRS min,max attribute values at the identified zoom level + // save this information as properties of the mapExtent, + // perhaps as a bounds object so that it can be easily used + // later by the layer control to determine when to enable + // disable the layer for drawing. + } else { + console.log( + 'input with name=' + + varName + + ' not found for template variable of same name' + ); } - if ( - (template && vcount.length === inputs.length) - ) { - if (trel === 'query') { - this._layer.queryable = true; - } - if (!includesZoom && zoomInput) { - inputs.push(zoomInput); - } - let step = zoomInput ? zoomInput.getAttribute('step') : 1; - if (!step || step === '0' || isNaN(step)) step = 1; - // template has a matching input for every variable reference {varref} - this._templateVars = { - template: decodeURI(new URL(template, this.parentExtent._layer.getBase())), - linkEl: this, - rel: this.rel, - type: this.type, - values: inputs, - zoomBounds: zoomBounds, - boundsFallbackPCRS: { bounds: this.getBounds().bounds }, - projection: this.parentExtent.units || M.FALLBACK_PROJECTION, - tms: this.tms, - step: step - }; + } + if (template && vcount.length === inputs.length) { + if (!includesZoom && zoomInput) { + inputs.push(zoomInput); + linkedZoomInput = 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} + this._templateVars = { + template: decodeURI( + new URL(template, this.parentElement._layer.getBase()) + ), + linkEl: this, + rel: this.rel, + type: this.type, + values: inputs, + zoomBounds: this.getZoomBounds(linkedZoomInput), + boundsFallbackPCRS: { bounds: this.getBounds().bounds }, + projection: this.parentElement.units || M.FALLBACK_PROJECTION, + tms: this.tms, + step: step + }; + } } getBounds() { - let boundsFallback = {}; + let boundsFallback = {}; boundsFallback.zoom = 0; + let metaExtent = this.parentElement.getMeta('extent'); if (metaExtent) { let content = M._metaContentToObject(metaExtent.getAttribute('content')), cs; @@ -409,18 +394,21 @@ export class MapLink extends HTMLElement { } 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; + let fallbackProjection = + M[this.parentElement.units || M.FALLBACK_PROJECTION]; boundsFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; } return boundsFallback; } - getZoomBounds() { - // should return BOTH min/max(Display)Zoom AND min/maxNativeZoom which - // are options that can be passed to L.GridLayer... - // https://leafletjs.com/reference.html#gridlayer-minzoom - // returns object like this: - // { minZoom: n, maxZoom: n, minNativeZoom: n, maxNativeZoom: n } - // + /** + * Return BOTH min/max(Display)Zoom AND min/maxNativeZoom which + * are options that can be passed to L.GridLayer... + * https://leafletjs.com/reference.html#gridlayer-minzoom + * + * @param {Object} zoomInput - is an element reference to a map-input[type=zoom] + * @returns {Object} - returns {minZoom: n,maxZoom: n,minNativeZoom: n,maxNativeZoom: n} + */ + getZoomBounds(zoomInput) { // native variables should ONLY come from map-input min/max attributes // BUT they should fall back to map-meta or projection values for min/max (display) zoom // display zoom variables should be EQUAL to native unless specified differently @@ -430,14 +418,36 @@ export class MapLink extends HTMLElement { // them at smaller scale (i.e. little postage stamps), which can freez your // browser and bury a tile cache in requests, getting you banned/blocked // + // minZoom = map-meta name=zoom min || input type=zoom min || projection minZoom + // minNativeZoom = input type=zoom min || minZoom + // maxZoom = map-meta name=zoom max || input type=zoom max || projection maxZoom + // maxNativeZoom = input type=zoom max || maxZoom + + let zoomBounds = {}; // search document from here up, using closest source of zoom bounds info - // following code was the original, and 'mapml' was the layerEl._layer._content - // so not the upward-search suggested above. -// mapml.querySelector('map-meta[name=zoom]') -// ? M._metaContentToObject( -// mapml.querySelector('map-meta[name=zoom]').getAttribute('content') -// ) -// : undefined; + let meta = this.parentElement.getMeta('zoom'); + let metaMin = meta + ? +M._metaContentToObject(meta.getAttribute('content'))?.min + : null; + zoomBounds.minZoom = + metaMin || (zoomInput ? +zoomInput.getAttribute('min') : 0); + zoomBounds.minNativeZoom = zoomInput + ? +zoomInput.getAttribute('min') + : zoomBounds.minZoom; + let metaMax = meta + ? +M._metaContentToObject(meta.getAttribute('content'))?.max + : null; + zoomBounds.maxZoom = + metaMax || + (zoomInput + ? +zoomInput.getAttribute('max') + : M[this.parentElement.units || M.FALLBACK_PROJECTION].options + .resolutions.length - 1); + zoomBounds.maxNativeZoom = zoomInput + ? +zoomInput.getAttribute('max') + : zoomBounds.maxZoom; + + return zoomBounds; } // Resolve the templated URL with info from the sibling map-input's diff --git a/src/map-select.js b/src/map-select.js index a10022974..f0faf9623 100644 --- a/src/map-select.js +++ b/src/map-select.js @@ -36,24 +36,24 @@ export class MapSelect extends HTMLElement { connectedCallback() { // origin of this block was in _initTemplateVars from map-extent, which was // originally part of MapMLLayer... - // -// if (inp.tagName.toLowerCase() === 'map-select') { -// // use a throwaway div to parse the input from MapML into HTML -// var div = document.createElement('div'); -// div.insertAdjacentHTML('afterbegin', inp.outerHTML); -// // parse -// inp.htmlselect = div.querySelector('map-select'); -// inp.htmlselect = transcribe(inp.htmlselect); -// -// // this goes into the layer control, so add a listener -// L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); -// -// this refers to map-extent in the original -// if (!this._userInputs) { -// this._userInputs = []; -// } -// this._userInputs.push(inp.htmlselect); -// } + // + // if (inp.tagName.toLowerCase() === 'map-select') { + // // use a throwaway div to parse the input from MapML into HTML + // var div = document.createElement('div'); + // div.insertAdjacentHTML('afterbegin', inp.outerHTML); + // // parse + // inp.htmlselect = div.querySelector('map-select'); + // inp.htmlselect = transcribe(inp.htmlselect); + // + // // this goes into the layer control, so add a listener + // L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); + // + // this refers to map-extent in the original + // if (!this._userInputs) { + // this._userInputs = []; + // } + // this._userInputs.push(inp.htmlselect); + // } } disconnectedCallback() {} createLayerControlForSelect() { diff --git a/src/mapml/elementSupport/extents/createLayerControlForExtent.js b/src/mapml/elementSupport/extents/createLayerControlForExtent.js index dab6e2804..af5af5485 100644 --- a/src/mapml/elementSupport/extents/createLayerControlForExtent.js +++ b/src/mapml/elementSupport/extents/createLayerControlForExtent.js @@ -53,7 +53,7 @@ export var createLayerControlExtentHTML = function () { let mapSelects = this.querySelectorAll('map-select'); if (mapSelects.length) { var frag = document.createDocumentFragment(); - for (var i=0;i< mapSelects.length;i++){ + for (var i = 0; i < mapSelects.length; i++) { frag.appendChild(mapSelects[i].createLayerControlForSelect()); } extentSettings.appendChild(frag); diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index fb5b08fe5..58c2c33d3 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -6,7 +6,6 @@ export var TemplatedLayer = L.Layer.extend({ this._extentEl = this.options.extentEl; this.changeOpacity(this.options.opacity); L.DomUtil.addClass(this._container, 'mapml-templatedlayer-container'); - }, getEvents: function () { return { From ecd497c132f21bb204db92a2275ef06e05621558 Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 17 Nov 2023 14:58:03 -0500 Subject: [PATCH 06/46] WIP on map-link * add map-link.getBase() function, relies on / supports map-base which should simulate how works for remote content. * update copyRemoteContentToShadowRoot to copy all children of to shadow root. * use parentElement instead of parentExtent in _initTemplateVars, which allows it to not have to wait on connectedCallback to be runnable --- src/map-link.js | 36 +++++++++++++++++++++++----------- src/mapml/layers/MapMLLayer.js | 3 +-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/map-link.js b/src/map-link.js index 430ba93d2..f52835ea8 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -296,7 +296,7 @@ export class MapLink extends HTMLElement { inputs = []; while ((v = varNamesRe.exec(template)) !== null) { let varName = v[1], - inp = this.parentExtent.querySelector( + inp = this.parentElement.querySelector( 'map-input[name=' + varName + '],map-select[name=' + varName + ']' ); if (inp) { @@ -342,9 +342,7 @@ export class MapLink extends HTMLElement { if (!step || step === '0' || isNaN(step)) step = 1; // template has a matching input for every variable reference {varref} this._templateVars = { - template: decodeURI( - new URL(template, this.parentElement._layer.getBase()) - ), + template: decodeURI(new URL(template, this.getBase())), linkEl: this, rel: this.rel, type: this.type, @@ -357,6 +355,22 @@ export class MapLink extends HTMLElement { }; } } + getBase() { + let layer = this.getRootNode().host; + return new URL( + this.getRootNode().querySelector('map-base') && + this.getRootNode() instanceof ShadowRoot + ? this.getRootNode().querySelector('map-base').getAttribute('href') + : /* local content? */ !(this.getRootNode() instanceof ShadowRoot) + ? /* use the baseURI algorithm which takes into account any */ + this.getRootNode().querySelector('map-base')?.getAttribute('href') || + this.baseURI + : /* else use the resolved value */ new URL( + layer.src, + layer.baseURI + ).href + ).href; + } getBounds() { let boundsFallback = {}; @@ -401,12 +415,12 @@ export class MapLink extends HTMLElement { return boundsFallback; } /** - * Return BOTH min/max(Display)Zoom AND min/maxNativeZoom which - * are options that can be passed to L.GridLayer... - * https://leafletjs.com/reference.html#gridlayer-minzoom - * - * @param {Object} zoomInput - is an element reference to a map-input[type=zoom] - * @returns {Object} - returns {minZoom: n,maxZoom: n,minNativeZoom: n,maxNativeZoom: n} + * Return BOTH min/max(Display)Zoom AND min/maxNativeZoom which + * are options that can be passed to L.GridLayer... + * https://leafletjs.com/reference.html#gridlayer-minzoom + * + * @param {Object} zoomInput - is an element reference to a map-input[type=zoom] + * @returns {Object} - returns {minZoom: n,maxZoom: n,minNativeZoom: n,maxNativeZoom: n} */ getZoomBounds(zoomInput) { // native variables should ONLY come from map-input min/max attributes @@ -417,7 +431,7 @@ export class MapLink extends HTMLElement { // you fetch tiles at larger scales (i.e. many many small tiles) and render // them at smaller scale (i.e. little postage stamps), which can freez your // browser and bury a tile cache in requests, getting you banned/blocked - // + // // minZoom = map-meta name=zoom min || input type=zoom min || projection minZoom // minNativeZoom = input type=zoom min || minZoom // maxZoom = map-meta name=zoom max || input type=zoom max || projection maxZoom diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index ad8c8827d..9df5e3e2a 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -552,8 +552,7 @@ export var MapMLLayer = L.Layer.extend({ } let shadowRoot = layer._layerEl.shadowRoot; // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot - let headMeta = - mapml.children[0].children[0].querySelectorAll('map-meta[name]'); + let headMeta = mapml.children[0].children[0].querySelectorAll('*'); // get the elements inside map-body of remote mapml let bodyElements = mapml.children[0].children[1].children; let elements = [...headMeta, ...bodyElements]; From cc132c933e127ea7b0669241ae07f037bfa5a7c5 Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 17 Nov 2023 17:31:32 -0500 Subject: [PATCH 07/46] WIP on map-link: * change TemplatedLayer to extend LayerGroup * remove TemplatedLayer templates parameter * move TemplatedLayer._setupQueryVars to map-link * get crs option for TemplatedLayer from map-extent units attribute * move creation of MapMLLayer._properties._queries array from map-extent into map-link * change map-extent._validateDisabled to iterate over querySelectorAll over child map-links of selected rel values (image,tile,features,query) * delete map-extent.redraw method, corresponding TemplatedLayer.redraw; that was needed to redraw the layer when the map-select change event happened. TODO we need to migrate that to map-select. * create map-link.mapEl property during connectedCallback --- src/map-extent.js | 25 ++-- src/map-link.js | 152 ++++++++++++++++++-- src/map-select.js | 1 + src/mapml/layers/TemplatedLayer.js | 214 +---------------------------- 4 files changed, 155 insertions(+), 237 deletions(-) diff --git a/src/map-extent.js b/src/map-extent.js index aac65b7bc..cd40b1938 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -160,11 +160,11 @@ export class MapExtent extends HTMLElement { // this._opacity is used to record the current opacity value (with or without updates), // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 this._opacity = this.opacity || 1.0; - this._extentLayer = M.templatedLayer(null, { + this._extentLayer = M.templatedLayer({ pane: this._layer._container, opacity: this.opacity, _leafletLayer: this._layer, - crs: this._layer._properties.crs, + crs: M[this.units], extentZIndex: Array.from( this.parentLayer.querySelectorAll('map-extent') ).indexOf(this), @@ -177,12 +177,6 @@ export class MapExtent extends HTMLElement { if (!this.hidden) this._layer.addExtentToLayerControl(this._layerControlHTML); this._validateLayerControlContainerHidden(); - if (this._extentLayer._queries) { - if (!this._layer._properties._queries) - this._layer._properties._queries = []; - this._layer._properties._queries = - this._layer._properties._queries.concat(this._extentLayer._queries); - } this._calculateBounds(); } getLayerControlHTML() { @@ -196,14 +190,17 @@ export class MapExtent extends HTMLElement { } _validateDisabled() { if (!this._extentLayer) return; + let templates = this.querySelectorAll( + 'map-link[rel=image],map-link[rel=tile],map-link[rel=features],map-link[rel=query]' + ); const noTemplateVisible = () => { - let totalTemplateCount = this._extentLayer._templates.length, + let totalTemplateCount = templates.length, disabledTemplateCount = 0; - for (let j = 0; j < this._extentLayer._templates.length; j++) { - if (this._extentLayer._templates[j].rel === 'query') { + for (let j = 0; j < totalTemplateCount; j++) { + if (templates[j].rel === 'query') { continue; } - if (!this._extentLayer._templates[j].layer.isVisible) { + if (!templates[j]._templatedLayer.isVisible) { disabledTemplateCount++; } } @@ -267,10 +264,6 @@ export class MapExtent extends HTMLElement { } } - redraw() { - this._extentLayer.redraw(); - } - _handleChange() { // if the parent layer- is checked, add _extentLayer to map if map-extent is checked, otherwise remove it if (this.checked && this.parentLayer.checked && !this.disabled) { diff --git a/src/map-link.js b/src/map-link.js index f52835ea8..d4a3d5090 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -226,6 +226,7 @@ export class MapLink extends HTMLElement { : this.parentNode.host; if (!this.tref || !this.parentExtent) return; await this.parentExtent.whenReady(); + this.mapEl = this.parentExtent.mapEl; // parse tref for variable names // find sibling map-input with that name, link to them via js reference // resolve the tref against the appropriate base URL so as to be absolute @@ -233,18 +234,18 @@ export class MapLink extends HTMLElement { // create the layer type appropriate to the rel value, so long as the // parsing has gone well this.zIndex = Array.from( - this.parentExtent.querySelectorAll('map-link') + this.parentExtent.querySelectorAll( + 'map-link[rel=image],map-link[rel=tile],map-link[rel=features]' + ) ).indexOf(this); if (this.rel === 'tile') { - this._templatedLayer = M.templatedTileLayer( - this._templateVars, - L.Util.extend(options, { - errorTileUrl: - 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', - zIndex: this.zIndex, - pane: this.parentExtent._tempatedLayer._container - }) - ); + this._templatedLayer = M.templatedTileLayer(this._templateVars, { + crs: M[this.parentExtent.units], + errorTileUrl: + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + zIndex: this.zIndex, + pane: this.parentExtent._extentLayer.getContainer() + }); } else if (this.rel === 'image') { this._templatedLayer = M.templatedImageLayer( this._templateVars, @@ -271,13 +272,138 @@ export class MapLink extends HTMLElement { let inputData = M._extractInputBounds(this); this.extentBounds = inputData.bounds; this.zoomBounds = inputData.zoomBounds; - this._extentEl = this.parentExtent; - this._queries.push( + if (!this.parentExtent._layer._properties._queries) + this.parentExtent._layer._properties._queries = []; + this.parentExtent._layer._properties._queries.push( // need to refactor the _setupQueryVars args / migrate it to map-link? - L.extend(this, this.parentExtent._extentLayer._setupQueryVars(this)) + L.extend(this._templateVars, this._setupQueryVars(this._templateVars)) ); } } + _setupQueryVars(template) { + // process the inputs associated to template and create an object named + // query with member properties as follows: + // {width: 'widthvarname', + // height: 'heightvarname', + // left: 'leftvarname', + // right: 'rightvarname', + // top: 'topvarname', + // bottom: 'bottomvarname' + // i: 'ivarname' + // j: 'jvarname'} + // x: 'xvarname' x being the tcrs x axis + // y: 'yvarname' y being the tcrs y axis + // z: 'zvarname' zoom + // title: link title + + var queryVarNames = { query: {} }, + inputs = template.values; + + for (var i = 0; i < template.values.length; i++) { + var type = inputs[i].getAttribute('type'), + units = inputs[i].getAttribute('units'), + axis = inputs[i].getAttribute('axis'), + name = inputs[i].getAttribute('name'), + position = inputs[i].getAttribute('position'), + rel = inputs[i].getAttribute('rel'), + select = inputs[i].tagName.toLowerCase() === 'map-select'; + if (type === 'width') { + queryVarNames.query.width = name; + } else if (type === 'height') { + queryVarNames.query.height = name; + } else if (type === 'location') { + switch (axis) { + case 'x': + case 'y': + case 'column': + case 'row': + queryVarNames.query[axis] = name; + break; + case 'longitude': + case 'easting': + if (position) { + if (position.match(/.*?-left/i)) { + if (rel === 'pixel') { + queryVarNames.query.pixelleft = name; + } else if (rel === 'tile') { + queryVarNames.query.tileleft = name; + } else { + queryVarNames.query.mapleft = name; + } + } else if (position.match(/.*?-right/i)) { + if (rel === 'pixel') { + queryVarNames.query.pixelright = name; + } else if (rel === 'tile') { + queryVarNames.query.tileright = name; + } else { + queryVarNames.query.mapright = name; + } + } + } else { + queryVarNames.query[axis] = name; + } + break; + case 'latitude': + case 'northing': + if (position) { + if (position.match(/top-.*?/i)) { + if (rel === 'pixel') { + queryVarNames.query.pixeltop = name; + } else if (rel === 'tile') { + queryVarNames.query.tiletop = name; + } else { + queryVarNames.query.maptop = name; + } + } else if (position.match(/bottom-.*?/i)) { + if (rel === 'pixel') { + queryVarNames.query.pixelbottom = name; + } else if (rel === 'tile') { + queryVarNames.query.tilebottom = name; + } else { + queryVarNames.query.mapbottom = name; + } + } + } else { + queryVarNames.query[axis] = name; + } + break; + case 'i': + if (units === 'tile') { + queryVarNames.query.tilei = name; + } else { + queryVarNames.query.mapi = name; + } + break; + case 'j': + if (units === 'tile') { + queryVarNames.query.tilej = name; + } else { + queryVarNames.query.mapj = name; + } + break; + default: + // unsuportted axis value + } + } else if (type === 'zoom') { + // + queryVarNames.query.zoom = name; + } else if (select) { + /*jshint -W104 */ + const parsedselect = inputs[i].htmlselect; + queryVarNames.query[name] = function () { + return parsedselect.value; + }; + } else { + /*jshint -W104 */ + const input = inputs[i]; + queryVarNames.query[name] = function () { + return input.getAttribute('value'); + }; + } + } + return queryVarNames; + } + _validateDisabled() {} disconnectedCallback() {} // there's a new function in map-extent.getMeta('extent'|'zoom') which was created // to be used to pass its return value in here as metaExtent... diff --git a/src/map-select.js b/src/map-select.js index f0faf9623..46b2c16a1 100644 --- a/src/map-select.js +++ b/src/map-select.js @@ -34,6 +34,7 @@ export class MapSelect extends HTMLElement { super(); } connectedCallback() { + // TODO make the layer redraw after map-select change event // origin of this block was in _initTemplateVars from map-extent, which was // originally part of MapMLLayer... // diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index 58c2c33d3..e884b16f3 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -1,6 +1,5 @@ -export var TemplatedLayer = L.Layer.extend({ - initialize: function (templates, options) { - this._templates = templates; +export var TemplatedLayer = L.LayerGroup.extend({ + initialize: function (options) { L.setOptions(this, options); this._container = L.DomUtil.create('div', 'leaflet-layer'); this._extentEl = this.options.extentEl; @@ -12,212 +11,15 @@ export var TemplatedLayer = L.Layer.extend({ zoomstart: this._onZoomStart }; }, - redraw: function () { - this.closePopup(); - for (var i = 0; i < this._templates.length; i++) { - if ( - this._templates[i].rel === 'tile' || - this._templates[i].rel === 'image' || - this._templates[i].rel === 'features' - ) { - this._templates[i].layer.redraw(); - } - } - }, _onZoomStart: function () { this.closePopup(); }, - - _setupQueryVars: function (template) { - // process the inputs associated to template and create an object named - // query with member properties as follows: - // {width: 'widthvarname', - // height: 'heightvarname', - // left: 'leftvarname', - // right: 'rightvarname', - // top: 'topvarname', - // bottom: 'bottomvarname' - // i: 'ivarname' - // j: 'jvarname'} - // x: 'xvarname' x being the tcrs x axis - // y: 'yvarname' y being the tcrs y axis - // z: 'zvarname' zoom - // title: link title - - var queryVarNames = { query: {} }, - inputs = template.values; - - for (var i = 0; i < template.values.length; i++) { - var type = inputs[i].getAttribute('type'), - units = inputs[i].getAttribute('units'), - axis = inputs[i].getAttribute('axis'), - name = inputs[i].getAttribute('name'), - position = inputs[i].getAttribute('position'), - rel = inputs[i].getAttribute('rel'), - select = inputs[i].tagName.toLowerCase() === 'map-select'; - if (type === 'width') { - queryVarNames.query.width = name; - } else if (type === 'height') { - queryVarNames.query.height = name; - } else if (type === 'location') { - switch (axis) { - case 'x': - case 'y': - case 'column': - case 'row': - queryVarNames.query[axis] = name; - break; - case 'longitude': - case 'easting': - if (position) { - if (position.match(/.*?-left/i)) { - if (rel === 'pixel') { - queryVarNames.query.pixelleft = name; - } else if (rel === 'tile') { - queryVarNames.query.tileleft = name; - } else { - queryVarNames.query.mapleft = name; - } - } else if (position.match(/.*?-right/i)) { - if (rel === 'pixel') { - queryVarNames.query.pixelright = name; - } else if (rel === 'tile') { - queryVarNames.query.tileright = name; - } else { - queryVarNames.query.mapright = name; - } - } - } else { - queryVarNames.query[axis] = name; - } - break; - case 'latitude': - case 'northing': - if (position) { - if (position.match(/top-.*?/i)) { - if (rel === 'pixel') { - queryVarNames.query.pixeltop = name; - } else if (rel === 'tile') { - queryVarNames.query.tiletop = name; - } else { - queryVarNames.query.maptop = name; - } - } else if (position.match(/bottom-.*?/i)) { - if (rel === 'pixel') { - queryVarNames.query.pixelbottom = name; - } else if (rel === 'tile') { - queryVarNames.query.tilebottom = name; - } else { - queryVarNames.query.mapbottom = name; - } - } - } else { - queryVarNames.query[axis] = name; - } - break; - case 'i': - if (units === 'tile') { - queryVarNames.query.tilei = name; - } else { - queryVarNames.query.mapi = name; - } - break; - case 'j': - if (units === 'tile') { - queryVarNames.query.tilej = name; - } else { - queryVarNames.query.mapj = name; - } - break; - default: - // unsuportted axis value - } - } else if (type === 'zoom') { - // - queryVarNames.query.zoom = name; - } else if (select) { - /*jshint -W104 */ - const parsedselect = inputs[i].htmlselect; - queryVarNames.query[name] = function () { - return parsedselect.value; - }; - } else { - /*jshint -W104 */ - const input = inputs[i]; - queryVarNames.query[name] = function () { - return input.getAttribute('value'); - }; - } - } - queryVarNames.query.title = template.title; - return queryVarNames; - }, - reset: function (templates, extentZIndex) { - if (!templates) { - return; - } - if (!this._map) { - return; - } - var addToMap = this._map && this._map.hasLayer(this), - old_templates = this._templates; - delete this._queries; - this._map.off('click', null, this); - - this._templates = templates; - for (var i = 0; i < templates.length; i++) { - if (templates[i].rel === 'tile') { - this._templates[i].layer = M.templatedTileLayer( - templates[i], - L.Util.extend(this.options, { - errorTileUrl: - 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', - zIndex: extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'image') { - this._templates[i].layer = M.templatedImageLayer( - templates[i], - L.Util.extend(this.options, { - zIndex: extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'features') { - this._templates[i].layer = M.templatedFeaturesLayer( - templates[i], - L.Util.extend(this.options, { - zIndex: extentZIndex, - pane: this._container - }) - ); - } else if (templates[i].rel === 'query') { - if (!this._queries) { - this._queries = []; - } - this._queries.push( - L.extend(templates[i], this._setupQueryVars(templates[i])) - ); - } - if (addToMap) { - this.onAdd(this._map); - } - } - for (i = 0; i < old_templates.length; i++) { - if (this._map.hasLayer(old_templates[i].layer)) { - this._map.removeLayer(old_templates[i].layer); - } - } + getContainer: function () { + return this._container; }, onAdd: function (map) { // add to this.options.pane this.options.pane.appendChild(this._container); - for (var i = 0; i < this._templates.length; i++) { - if (this._templates[i].rel !== 'query') { - map.addLayer(this._templates[i].layer); - } - } }, //addTo: function(map) { //for(let i = 0; i < this._templates.length; i++){ @@ -269,10 +71,6 @@ export var TemplatedLayer = L.Layer.extend({ this._extentEl._opacitySlider.value = opacity; } }); -export var templatedLayer = function (templates, options) { - // templates is an array of template objects - // a template object contains the template, plus associated elements - // which need to be processed just prior to creating a url from the template - // with the values of the inputs - return new TemplatedLayer(templates, options); +export var templatedLayer = function (options) { + return new TemplatedLayer(options); }; From 8d7d7450099a2ce1cce9fea2450c5994cfe475f2 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Mon, 20 Nov 2023 14:17:23 -0500 Subject: [PATCH 08/46] WIP on map-link: - create map-link.getExtent that incorporates (refactors move + rename of) M._extractInputBounds(...) - create and use map-link.whenReady() for content links. Used by map-extent._calculateBounds when iterating child links. - create private _getZoomBounds(zoomInput) that is used during template / element initialization - create public getZoomBounds() which reads the bounds from the result\ of the above _getZoomBounds(zoomInput), but doesn't require to sift through the inputs for linked/non-linked zoom input that might not exist - simplify boundsFallback field of _templateVars; remove object props, return simple L.Bounds object type - add map-link._validateDisabled function which is used by map-extent._validateDisabled() - refactor map-extent._calculateBounds to use new map-link.getBounds and map-link.getZoomBounds API methods - update TemplatedTileLayer - pass zoomBounds and extentBounds as required options --- src/map-extent.js | 85 +++++++------- src/map-link.js | 151 +++++++++++++++++++++++-- src/mapml/layers/TemplatedTileLayer.js | 6 +- src/mapml/utils/Util.js | 99 ---------------- 4 files changed, 184 insertions(+), 157 deletions(-) diff --git a/src/map-extent.js b/src/map-extent.js index cd40b1938..02aa54240 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -193,28 +193,34 @@ export class MapExtent extends HTMLElement { let templates = this.querySelectorAll( 'map-link[rel=image],map-link[rel=tile],map-link[rel=features],map-link[rel=query]' ); - const noTemplateVisible = () => { - let totalTemplateCount = templates.length, - disabledTemplateCount = 0; - for (let j = 0; j < totalTemplateCount; j++) { - if (templates[j].rel === 'query') { - continue; - } - if (!templates[j]._templatedLayer.isVisible) { - disabledTemplateCount++; + let linksReady = []; + for (let link of [...templates]) { + linksReady.push(link.whenReady()); + } + Promise.allSettled(linksReady).then(() => { + const noTemplateVisible = () => { + let totalTemplateCount = templates.length, + disabledTemplateCount = 0; + for (let j = 0; j < totalTemplateCount; j++) { + if (templates[j].rel === 'query') { + continue; + } + if (!templates[j]._validateDisabled()) { + disabledTemplateCount++; + } } + return disabledTemplateCount === totalTemplateCount; + }; + if (!this._projectionMatch() || noTemplateVisible()) { + this.setAttribute('disabled', ''); + this.disabled = true; + } else { + this.removeAttribute('disabled'); + this.disabled = false; } - return disabledTemplateCount === totalTemplateCount; - }; - if (!this._projectionMatch() || noTemplateVisible()) { - this.setAttribute('disabled', ''); - this.disabled = true; - } else { - this.removeAttribute('disabled'); - this.disabled = false; - } - this.toggleLayerControlDisabled(); - return this.disabled; + this.toggleLayerControlDisabled(); + return this.disabled; + }); } getMeta(metaName) { let name = metaName.toLowerCase(); @@ -314,36 +320,31 @@ export class MapExtent extends HTMLElement { zoomMax = 0, zoomMin = 0, maxNativeZoom = 0, - minNativeZoom = 0; + minNativeZoom = 0, + templates = this.querySelectorAll( + 'map-link[rel=image],map-link[rel=tile],map-link[rel=features],map-link[rel=query]' + ); + // bounds should be able to be calculated unconditionally, not depend on map-extent.checked - for (let j = 0; j < this._templateVars.length; j++) { - let inputData = M._extractInputBounds(this._templateVars[j]); - this._templateVars[j].tempExtentBounds = inputData.bounds; - this._templateVars[j].extentZoomBounds = inputData.zoomBounds; + for (let j = 0; j < templates.length; j++) { if (!bounds) { - bounds = this._templateVars[j].tempExtentBounds; - zoomMax = this._templateVars[j].extentZoomBounds.maxZoom; - zoomMin = this._templateVars[j].extentZoomBounds.minZoom; - maxNativeZoom = this._templateVars[j].extentZoomBounds.maxNativeZoom; - minNativeZoom = this._templateVars[j].extentZoomBounds.minNativeZoom; + bounds = templates[j].getBounds(); + zoomMax = templates[j].getZoomBounds().maxZoom; + zoomMin = templates[j].getZoomBounds().minZoom; + maxNativeZoom = templates[j].getZoomBounds().maxNativeZoom; + minNativeZoom = templates[j].getZoomBounds().minNativeZoom; } else { - bounds.extend(this._templateVars[j].tempExtentBounds.min); - bounds.extend(this._templateVars[j].tempExtentBounds.max); - zoomMax = Math.max( - zoomMax, - this._templateVars[j].extentZoomBounds.maxZoom - ); - zoomMin = Math.min( - zoomMin, - this._templateVars[j].extentZoomBounds.minZoom - ); + bounds.extend(templates[j].getBounds().min); + bounds.extend(templates[j].getBounds().max); + zoomMax = Math.max(zoomMax, templates[j].getZoomBounds().maxZoom); + zoomMin = Math.min(zoomMin, templates[j].getZoomBounds().minZoom); maxNativeZoom = Math.max( maxNativeZoom, - this._templateVars[j].extentZoomBounds.maxNativeZoom + templates[j].getZoomBounds().maxNativeZoom ); minNativeZoom = Math.min( minNativeZoom, - this._templateVars[j].extentZoomBounds.minNativeZoom + templates[j].getZoomBounds().minNativeZoom ); } } diff --git a/src/map-link.js b/src/map-link.js index d4a3d5090..4a537f07a 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -240,6 +240,8 @@ export class MapLink extends HTMLElement { ).indexOf(this); if (this.rel === 'tile') { this._templatedLayer = M.templatedTileLayer(this._templateVars, { + zoomBounds: this.getZoomBounds(), + extentBounds: this.getBounds(), crs: M[this.parentExtent.units], errorTileUrl: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', @@ -403,7 +405,6 @@ export class MapLink extends HTMLElement { } return queryVarNames; } - _validateDisabled() {} disconnectedCallback() {} // there's a new function in map-extent.getMeta('extent'|'zoom') which was created // to be used to pass its return value in here as metaExtent... @@ -473,14 +474,91 @@ export class MapLink extends HTMLElement { rel: this.rel, type: this.type, values: inputs, - zoomBounds: this.getZoomBounds(linkedZoomInput), - boundsFallbackPCRS: { bounds: this.getBounds().bounds }, + zoomBounds: this._getZoomBounds(linkedZoomInput), + boundsFallbackPCRS: this.getFallbackBounds(), projection: this.parentElement.units || M.FALLBACK_PROJECTION, tms: this.tms, step: step }; } } + getZoomBounds() { + return this._templateVars.zoomBounds; + } + getBounds() { + let template = this._templateVars; + //sets variables with their respective fallback values incase content is missing from the template + let inputs = template.values, + projection = template.projection || M.FALLBACK_PROJECTION, + value = 0, + boundsUnit = M.FALLBACK_CS; + let bounds = M[template.projection].options.crs.tilematrix.bounds(0), + defaultMinZoom = 0, + defaultMaxZoom = M[template.projection].options.resolutions.length - 1, + nativeMinZoom = defaultMinZoom, + nativeMaxZoom = defaultMaxZoom; + let locInputs = false, + numberOfAxes = 0; + for (let i = 0; i < inputs.length; i++) { + switch (inputs[i].getAttribute('type')) { + case 'zoom': + nativeMinZoom = +(inputs[i].hasAttribute('min') && + !isNaN(+inputs[i].getAttribute('min')) + ? inputs[i].getAttribute('min') + : defaultMinZoom); + nativeMaxZoom = +(inputs[i].hasAttribute('max') && + !isNaN(+inputs[i].getAttribute('max')) + ? inputs[i].getAttribute('max') + : defaultMaxZoom); + value = +inputs[i].getAttribute('value'); + break; + case 'location': + if (!inputs[i].getAttribute('max') || !inputs[i].getAttribute('min')) + continue; + let max = +inputs[i].getAttribute('max'), + min = +inputs[i].getAttribute('min'); + switch (inputs[i].getAttribute('axis').toLowerCase()) { + case 'x': + case 'longitude': + case 'column': + case 'easting': + boundsUnit = M.axisToCS( + inputs[i].getAttribute('axis').toLowerCase() + ); + bounds.min.x = min; + bounds.max.x = max; + numberOfAxes++; + break; + case 'y': + case 'latitude': + case 'row': + case 'northing': + boundsUnit = M.axisToCS( + inputs[i].getAttribute('axis').toLowerCase() + ); + bounds.min.y = min; + bounds.max.y = max; + numberOfAxes++; + break; + default: + break; + } + break; + default: + } + } + if (numberOfAxes >= 2) { + locInputs = true; + } + if (!locInputs && template.boundsFallbackPCRS) { + bounds = template.boundsFallbackPCRS; + } else if (locInputs) { + bounds = M.boundsToPCRSBounds(bounds, value, projection, boundsUnit); + } else { + bounds = M[template.projection].options.crs.pcrs.bounds; + } + return bounds; + } getBase() { let layer = this.getRootNode().host; return new URL( @@ -497,16 +575,16 @@ export class MapLink extends HTMLElement { ).href ).href; } - getBounds() { - let boundsFallback = {}; + getFallbackBounds() { + let bounds; - boundsFallback.zoom = 0; + let zoom = 0; let metaExtent = this.parentElement.getMeta('extent'); if (metaExtent) { let content = M._metaContentToObject(metaExtent.getAttribute('content')), cs; - boundsFallback.zoom = content.zoom || boundsFallback.zoom; + zoom = content.zoom || zoom; let metaKeys = Object.keys(content); for (let i = 0; i < metaKeys.length; i++) { @@ -516,7 +594,7 @@ export class MapLink extends HTMLElement { } } let axes = M.csToAxes(cs); - boundsFallback.bounds = M.boundsToPCRSBounds( + bounds = M.boundsToPCRSBounds( L.bounds( L.point( +content[`top-left-${axes[0]}`], @@ -527,7 +605,7 @@ export class MapLink extends HTMLElement { +content[`bottom-right-${axes[1]}`] ) ), - boundsFallback.zoom, + zoom, projection, cs ); @@ -536,9 +614,9 @@ export class MapLink extends HTMLElement { // 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[this.parentElement.units || M.FALLBACK_PROJECTION]; - boundsFallback.bounds = fallbackProjection.options.crs.pcrs.bounds; + bounds = fallbackProjection.options.crs.pcrs.bounds; } - return boundsFallback; + return bounds; } /** * Return BOTH min/max(Display)Zoom AND min/maxNativeZoom which @@ -548,7 +626,7 @@ export class MapLink extends HTMLElement { * @param {Object} zoomInput - is an element reference to a map-input[type=zoom] * @returns {Object} - returns {minZoom: n,maxZoom: n,minNativeZoom: n,maxNativeZoom: n} */ - getZoomBounds(zoomInput) { + _getZoomBounds(zoomInput) { // native variables should ONLY come from map-input min/max attributes // BUT they should fall back to map-meta or projection values for min/max (display) zoom // display zoom variables should be EQUAL to native unless specified differently @@ -589,6 +667,9 @@ export class MapLink extends HTMLElement { return zoomBounds; } + _validateDisabled() { + return this._templatedLayer.isVisible; + } // Resolve the templated URL with info from the sibling map-input's resolve() { @@ -615,4 +696,50 @@ export class MapLink extends HTMLElement { } } } + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + switch (this.rel.toLowerCase()) { + // for some cases, require a dependency check + case 'tile': + case 'image': + case 'features': + if (this._templatedLayer) { + resolve(); + } + interval = setInterval(testForContentLink, 300, this); + failureTimer = setTimeout(linkNotDefined, 10000); + break; + case 'query': + case 'style': + case 'self': + case 'style self': + case 'self style': + case 'zoomin': + case 'zoomout': + case 'legend': + case 'stylesheet': + case 'alternate': + case 'license': + resolve(); + break; + } + function testForContentLink(linkElement) { + if (linkElement._linkLayer) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } else if (!linkElement.isConnected) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('map-link was disconnected while waiting to be ready'); + } + } + function linkNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for link to be ready'); + } + }); + } } diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index c34b77698..27f868ee8 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -8,15 +8,13 @@ export var TemplatedTileLayer = L.TileLayer.extend({ initialize: function (template, options) { // _setUpTileTemplateVars needs options.crs, not available unless we set // options first... - let inputData = M._extractInputBounds(template); - this.zoomBounds = inputData.zoomBounds; - this.extentBounds = inputData.bounds; this.isVisible = true; - L.extend(options, this.zoomBounds); options.tms = template.tms; delete options.opacity; L.setOptions(this, options); this._setUpTileTemplateVars(template); + this.zoomBounds = this.options.zoomBounds; + this.extentBounds = this.options.extentBounds; this._template = template; this._initContainer(); diff --git a/src/mapml/utils/Util.js b/src/mapml/utils/Util.js index 340a9c342..f77cfc9c5 100644 --- a/src/mapml/utils/Util.js +++ b/src/mapml/utils/Util.js @@ -108,105 +108,6 @@ export var Util = { } }, - // _extractInputBounds extracts and returns Input Bounds from the provided template - // _extractInputBounds: Object -> {zoomBounds: ..., bounds: ...} - _extractInputBounds: function (template) { - if (!template) return undefined; - - //sets variables with their respective fallback values incase content is missing from the template - let inputs = template.values, - projection = template.projection || M.FALLBACK_PROJECTION, - value = 0, - boundsUnit = M.FALLBACK_CS; - let bounds = this[projection].options.crs.tilematrix.bounds(0), - defaultMinZoom = 0, - defaultMaxZoom = this[projection].options.resolutions.length - 1, - nativeMinZoom = defaultMinZoom, - nativeMaxZoom = defaultMaxZoom; - let locInputs = false, - numberOfAxes = 0; - for (let i = 0; i < inputs.length; i++) { - switch (inputs[i].getAttribute('type')) { - case 'zoom': - nativeMinZoom = +(inputs[i].hasAttribute('min') && - !isNaN(+inputs[i].getAttribute('min')) - ? inputs[i].getAttribute('min') - : defaultMinZoom); - nativeMaxZoom = +(inputs[i].hasAttribute('max') && - !isNaN(+inputs[i].getAttribute('max')) - ? inputs[i].getAttribute('max') - : defaultMaxZoom); - value = +inputs[i].getAttribute('value'); - break; - case 'location': - if (!inputs[i].getAttribute('max') || !inputs[i].getAttribute('min')) - continue; - let max = +inputs[i].getAttribute('max'), - min = +inputs[i].getAttribute('min'); - switch (inputs[i].getAttribute('axis').toLowerCase()) { - case 'x': - case 'longitude': - case 'column': - case 'easting': - boundsUnit = M.axisToCS( - inputs[i].getAttribute('axis').toLowerCase() - ); - bounds.min.x = min; - bounds.max.x = max; - numberOfAxes++; - break; - case 'y': - case 'latitude': - case 'row': - case 'northing': - boundsUnit = M.axisToCS( - inputs[i].getAttribute('axis').toLowerCase() - ); - bounds.min.y = min; - bounds.max.y = max; - numberOfAxes++; - break; - default: - break; - } - break; - default: - } - } - if (numberOfAxes >= 2) { - locInputs = true; - } - // min/maxZoom are copied from - zoom = { - name: name, - min: 0, - max: crs.resolutions.length, - value: crs.resolutions.length - }; - if ( - !isNaN(Number.parseInt(value, 10)) && - Number.parseInt(value, 10) >= zoom.min && - Number.parseInt(value, 10) <= zoom.max - ) { - zoom.value = Number.parseInt(value, 10); - } else { - zoom.value = zoom.max; - } - if ( - !isNaN(Number.parseInt(min, 10)) && - Number.parseInt(min, 10) >= zoom.min && - Number.parseInt(min, 10) <= zoom.max - ) { - zoom.min = Number.parseInt(min, 10); - } - if ( - !isNaN(Number.parseInt(max, 10)) && - Number.parseInt(max, 10) >= zoom.min && - Number.parseInt(max, 10) <= zoom.max - ) { - zoom.max = Number.parseInt(max, 10); - } - template.zoom = zoom; } else if (select) { /*jshint -W104 */ const parsedselect = inputs[i].htmlselect; template.tile[name] = function () { return parsedselect.value; }; - } else { + } else if (type === 'hidden') { // needs to be a const otherwise it gets overwritten /*jshint -W104 */ const input = inputs[i]; @@ -486,7 +454,7 @@ export var TemplatedTileLayer = L.TileLayer.extend({ ); template.pcrs.easting = east; template.pcrs.northing = north; - } else if (col && row && !isNaN(zoom.value)) { + } else if (col && row && !isNaN(template.zoom.initialValue)) { // convert the tile bounds at this zoom to a pcrs bounds, then // go through the zoom min/max and create a tile-based bounds // at each zoom that applies to the col/row values that constrain what tiles @@ -499,7 +467,7 @@ export var TemplatedTileLayer = L.TileLayer.extend({ template.pcrs.bounds = M.boundsToPCRSBounds( L.bounds(L.point([col.min, row.min]), L.point([col.max, row.max])), - zoom.value, + template.zoom.initialValue, this.options.crs, M.axisToCS('column') ); @@ -507,10 +475,6 @@ export var TemplatedTileLayer = L.TileLayer.extend({ template.tilematrix = {}; template.tilematrix.col = col; template.tilematrix.row = row; - } else { - console.log( - 'Unable to determine bounds for tile template: ' + template.template - ); } if (!template.tilematrix) { @@ -524,8 +488,8 @@ export var TemplatedTileLayer = L.TileLayer.extend({ // by first processing the extent to determine the zoom and if none, adding // one and second by copying that zoom into the set of template variable inputs // even if it is not referenced by one of the template's variable references - var zmin = template.zoom ? template.zoom.min : 0, - zmax = template.zoom ? template.zoom.max : crs.resolutions.length; + var zmin = template.zoomBounds.minNativeZoom, + zmax = template.zoomBounds.maxNativeZoom; for (var z = 0; z <= zmax; z++) { template.tilematrix.bounds[z] = z >= zmin From 05e38b479e801e085cd152e90831a6d0be737876 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 22 Nov 2023 09:40:44 -0500 Subject: [PATCH 13/46] Revert deletion of TemplatedTileLayer.options.minZoom,.maxZoom, .minNativeZoom,..maxNativeZoom --- src/mapml/layers/TemplatedTileLayer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 29ac9a296..0d36f351f 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -9,10 +9,12 @@ export var TemplatedTileLayer = L.TileLayer.extend({ // _setUpTileTemplateVars needs options.crs, not available unless we set // options first... options.tms = template.tms; - delete options.opacity; L.setOptions(this, options); this._setUpTileTemplateVars(template); this.zoomBounds = this.options.zoomBounds; + // it's critical to have this.options.minZoom, minNativeZoom, maxZoom, maxNativeZoom + // which we accomplish here. Used by Leaflet Map and GridLayer. + L.extend(options, this.zoomBounds); this.extentBounds = this.options.extentBounds; this._template = template; From a9ccc579a948ab6c53ddcf4e2169f545858016dc Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 22 Nov 2023 10:50:24 -0500 Subject: [PATCH 14/46] Get map-select quasi working (at least, add it to the layer control) --- Gruntfile.js | 1 + src/map-select.js | 110 +++++++++--------- src/mapml-viewer.js | 4 +- .../extents/createLayerControlForExtent.js | 2 +- 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index c7c25443c..efc38e0bc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -29,6 +29,7 @@ module.exports = function(grunt) { 'dist/map-feature.js': ['src/map-feature.js'], 'dist/map-extent.js': ['src/map-extent.js'], 'dist/map-input.js': ['src/map-input.js'], + 'dist/map-select.js': ['src/map-select.js'], 'dist/map-link.js': ['src/map-link.js'], 'dist/map-area.js': ['src/map-area.js'], 'dist/layer.js': ['src/layer.js'], diff --git a/src/map-select.js b/src/map-select.js index 46b2c16a1..19322454c 100644 --- a/src/map-select.js +++ b/src/map-select.js @@ -38,74 +38,43 @@ export class MapSelect extends HTMLElement { // origin of this block was in _initTemplateVars from map-extent, which was // originally part of MapMLLayer... // - // if (inp.tagName.toLowerCase() === 'map-select') { - // // use a throwaway div to parse the input from MapML into HTML - // var div = document.createElement('div'); - // div.insertAdjacentHTML('afterbegin', inp.outerHTML); - // // parse - // inp.htmlselect = div.querySelector('map-select'); - // inp.htmlselect = transcribe(inp.htmlselect); - // - // // this goes into the layer control, so add a listener - // L.DomEvent.on(inp.htmlselect, 'change', this.redraw, this); - // - // this refers to map-extent in the original - // if (!this._userInputs) { - // this._userInputs = []; - // } - // this._userInputs.push(inp.htmlselect); - // } + // use a throwaway div to parse the input from MapML into HTML + this._createLayerControlForSelect(); } disconnectedCallback() {} - createLayerControlForSelect() { + _createLayerControlForSelect() { // cut-pasted from createLayerControlForExtent. TODO Re-write appropriately - var templates = this._templateVars; - if (templates) { - this._selectdetails = []; - for (var i = 0; i < templates.length; i++) { - var template = templates[i]; - for (var j = 0; j < template.values.length; j++) { - var mapmlInput = template.values[j], - id = '#' + mapmlInput.getAttribute('id'); - // don't add it again if it is referenced > once - if ( - mapmlInput.tagName.toLowerCase() === 'map-select' && - !frag.querySelector(id) - ) { - // generate a
- var selectdetails = L.DomUtil.create( - 'details', - 'mapml-layer-item-details mapml-control-layers', - frag - ), - selectsummary = L.DomUtil.create('summary'), - selectSummaryLabel = L.DomUtil.create('label'); - selectSummaryLabel.innerText = mapmlInput.getAttribute('name'); - selectSummaryLabel.setAttribute( - 'for', - mapmlInput.getAttribute('id') - ); - selectsummary.appendChild(selectSummaryLabel); - selectdetails.appendChild(selectsummary); - selectdetails.appendChild(mapmlInput.htmlselect); - this._selectdetails.push(selectdetails); - } - } - } - } + // don't add it again if it is referenced > once + // generate a
+ this.htmlselect = this.transcribe(); + var selectdetails = L.DomUtil.create( + 'details', + 'mapml-layer-item-details mapml-control-layers' + ), + selectsummary = L.DomUtil.create('summary'), + selectSummaryLabel = L.DomUtil.create('label'); + selectSummaryLabel.innerText = this.getAttribute('name'); + selectSummaryLabel.setAttribute('for', this.getAttribute('id')); + selectsummary.appendChild(selectSummaryLabel); + selectdetails.appendChild(selectsummary); + selectdetails.appendChild(this.htmlselect); + this.selectdetails = selectdetails; + // this goes into the layer control, so add a listener to trigger map + // or layer redraw with newly selected value + // this.htmlselect.addEventListener('change', null); } - transcribe(element) { + transcribe() { var select = document.createElement('select'); - var elementAttrNames = element.getAttributeNames(); + var elementAttrNames = this.getAttributeNames(); for (let i = 0; i < elementAttrNames.length; i++) { select.setAttribute( elementAttrNames[i], - element.getAttribute(elementAttrNames[i]) + this.getAttribute(elementAttrNames[i]) ); } - var options = element.children; + var options = this.children; for (let i = 0; i < options.length; i++) { var option = document.createElement('option'); @@ -123,5 +92,32 @@ export class MapSelect extends HTMLElement { } return select; } + whenReady() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this.selectdetails) { + resolve(); + } else { + let selectElement = this; + interval = setInterval(testForSelect, 300, selectElement); + failureTimer = setTimeout(selectNotDefined, 10000); + } + function testForSelect(selectElement) { + if (selectElement.selectdetails) { + clearInterval(interval); + clearTimeout(failureTimer); + resolve(); + } else if (!selectElement.isConnected) { + clearInterval(interval); + clearTimeout(failureTimer); + reject('map-select was disconnected while waiting to be ready'); + } + } + function selectNotDefined() { + clearInterval(interval); + clearTimeout(failureTimer); + reject('Timeout reached waiting for map-select to be ready'); + } + }); + } } -window.customElements.define('map-select', MapSelect); diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 6eb7a2d63..cd9b6ae3c 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -5,6 +5,7 @@ import { MapCaption } from './map-caption.js'; import { MapFeature } from './map-feature.js'; import { MapExtent } from './map-extent.js'; import { MapInput } from './map-input.js'; +import { MapSelect } from './map-select.js'; import { MapLink } from './map-link.js'; export class MapViewer extends HTMLElement { @@ -1407,6 +1408,7 @@ window.customElements.define('mapml-viewer', MapViewer); window.customElements.define('layer-', MapLayer); window.customElements.define('map-caption', MapCaption); window.customElements.define('map-feature', MapFeature); -window.customElements.define('map-extent', MapExtent); window.customElements.define('map-input', MapInput); +window.customElements.define('map-select', MapSelect); +window.customElements.define('map-extent', MapExtent); window.customElements.define('map-link', MapLink); diff --git a/src/mapml/elementSupport/extents/createLayerControlForExtent.js b/src/mapml/elementSupport/extents/createLayerControlForExtent.js index af5af5485..fbb5eb6e0 100644 --- a/src/mapml/elementSupport/extents/createLayerControlForExtent.js +++ b/src/mapml/elementSupport/extents/createLayerControlForExtent.js @@ -54,7 +54,7 @@ export var createLayerControlExtentHTML = function () { if (mapSelects.length) { var frag = document.createDocumentFragment(); for (var i = 0; i < mapSelects.length; i++) { - frag.appendChild(mapSelects[i].createLayerControlForSelect()); + frag.appendChild(mapSelects[i].selectdetails); } extentSettings.appendChild(frag); } From 69750ec6e784ec465cbe285ef00635bce5c36b81 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 22 Nov 2023 14:47:45 -0500 Subject: [PATCH 15/46] WIP on map-link, especially TemplatedFeaturesLayer: - Temporary fix for map-link.getFallbackBounds by adding projection as parameter - TODO review getBounds, getFallbackBounds, integrate them - Get map-select working better - Create remote templated features in the shadow root of map-link (instead of map-extent) - Add TemplatedLayer.redraw function in order to delegate map-select change event handling across templated layers that may share the variable --- src/map-feature.js | 29 ++++++++++++++++------ src/map-link.js | 13 +++++++--- src/map-select.js | 6 ++++- src/mapml/layers/TemplatedFeaturesLayer.js | 18 ++++++++------ src/mapml/layers/TemplatedLayer.js | 5 ++++ 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/map-feature.js b/src/map-feature.js index 097040132..ed9eb4108 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -104,11 +104,11 @@ export class MapFeature extends HTMLElement { if (this.closest('mapml-')) return; this._parentEl = this.parentNode.nodeName.toUpperCase() === 'LAYER-' || - this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT' + this.parentNode.nodeName.toUpperCase() === 'MAP-LINK' ? this.parentNode : this.parentNode.host; this._parentEl.whenReady().then(() => { - this._layer = this._parentEl._layer; + this._layer = this._parentEl._layer || this._parentEl.parentExtent._layer; delete this._parentEl.bounds; if ( this._layer._layerEl.hasAttribute('data-moving') || @@ -282,11 +282,12 @@ export class MapFeature extends HTMLElement { // referred to [map-meta, ...] if it is query // referred to null otherwise (i.e. has fetched in shadow, the attaches to 's shadow) let nativeZoom, nativeCS; - if (this._extentEl) { + if (this._linkEl) { // feature attaches to extent's shadow - if (this._extentEl.querySelector('map-link[rel=query]')) { + let metaZoom, metaCS; + if (this._linkEl.rel === 'query') { + // TODO migrate remote content into shadow root of linkEl... // for query, fallback zoom is the current map zoom level that the query is returned - let metaZoom, metaCS; if (content) { metaZoom = M._metaContentToObject( Array.prototype.filter @@ -305,10 +306,22 @@ export class MapFeature extends HTMLElement { } nativeZoom = metaZoom || this._map.getZoom(); nativeCS = metaCS || 'gcrs'; - } else if (this._extentEl.querySelector('map-link[rel=features]')) { + } else if (this._linkEl.rel === 'features') { // for templated feature, read fallback from the fetched mapml's map-meta[name=zoom / cs] - nativeZoom = this._extentEl._nativeZoom; - nativeCS = this._extentEl._nativeCS; + nativeZoom = this._map.getZoom(); + nativeCS = 'gcrs'; + if (content) { + metaZoom = M._metaContentToObject( + content + .querySelector('map-meta[name=zoom]') + ?.getAttribute('content') + ); + metaCS = M._metaContentToObject( + content.querySelector('map-meta[name=cs]')?.getAttribute('content') + ).content; + } + nativeZoom = metaZoom || this._map.getZoom(); + nativeCS = metaCS || 'gcrs'; } return { zoom: nativeZoom, cs: nativeCS }; } else { diff --git a/src/map-link.js b/src/map-link.js index 0d7dd1b65..214d5d022 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -252,11 +252,14 @@ export class MapLink extends HTMLElement { pane: this.parentExtent._extentLayer.getContainer() }).addTo(this.parentExtent._extentLayer); } else if (this.rel === 'features') { + // map-feature retrieved by link will be stored in shadowRoot owned by link + this.attachShadow({ mode: 'open' }); this._templatedLayer = M.templatedFeaturesLayer(this._templateVars, { zoomBounds: this.getZoomBounds(), extentBounds: this.getBounds(), zIndex: this.zIndex, - pane: this.parentExtent._extentLayer.getContainer() + pane: this.parentExtent._extentLayer.getContainer(), + linkEl: this }).addTo(this.parentExtent._extentLayer); } else if (this.rel === 'query') { // add template to array of queryies to be added to map and processed @@ -265,7 +268,7 @@ export class MapLink extends HTMLElement { if (!this._queries) { this._queries = []; } - this.extentBounds = this.getBounds(this); + this.extentBounds = this.getBounds(); this.zoomBounds = this.getZoomBounds(); if (!this.parentExtent._layer._properties._queries) this.parentExtent._layer._properties._queries = []; @@ -472,7 +475,9 @@ export class MapLink extends HTMLElement { inputsReady: Promise.allSettled(inputsReady), zoom: linkedZoomInput, zoomBounds: this._getZoomBounds(linkedZoomInput), - boundsFallbackPCRS: this.getFallbackBounds(), + boundsFallbackPCRS: this.getFallbackBounds( + this.parentElement.units || M.FALLBACK_PROJECTION + ), // TODO: make map-extent.units fall back automatically projection: this.parentElement.units || M.FALLBACK_PROJECTION, tms: this.tms, @@ -575,7 +580,7 @@ export class MapLink extends HTMLElement { ).href ).href; } - getFallbackBounds() { + getFallbackBounds(projection) { let bounds; let zoom = 0; diff --git a/src/map-select.js b/src/map-select.js index 19322454c..d17a2687d 100644 --- a/src/map-select.js +++ b/src/map-select.js @@ -34,6 +34,7 @@ export class MapSelect extends HTMLElement { super(); } connectedCallback() { + this._extentEl = this.parentElement; // TODO make the layer redraw after map-select change event // origin of this block was in _initTemplateVars from map-extent, which was // originally part of MapMLLayer... @@ -61,7 +62,10 @@ export class MapSelect extends HTMLElement { this.selectdetails = selectdetails; // this goes into the layer control, so add a listener to trigger map // or layer redraw with newly selected value - // this.htmlselect.addEventListener('change', null); + const drawLayers = function () { + this.parentElement._extentLayer.redraw(); + }.bind(this); + this.htmlselect.addEventListener('change', drawLayers); } transcribe() { var select = document.createElement('select'); diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index cfaaa9087..6edc8f855 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -5,8 +5,9 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._container = L.DomUtil.create('div', 'leaflet-layer'); L.DomUtil.addClass(this._container, 'mapml-features-container'); this.zoomBounds = options.zoomBounds; + L.extend(options, this.zoomBounds); this.extentBounds = options.extentBounds; - this._extentEl = options.extentEl; + this._linkEl = options.linkEl; L.setOptions( this, L.extend(options, this._setUpFeaturesTemplateVars(template)) @@ -91,6 +92,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ let current = history[history.length - 1]; let previous = history[history.length - 2] ?? current; let step = this._template.step; + let mapZoom = this._map.getZoom(); let steppedZoom = mapZoom; //If zooming out from one step interval into a lower one or panning, set the stepped zoom if ( @@ -119,8 +121,8 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ // do cleaning up for new request this._features.clearLayers(); // shadow may has not yet attached to for the first-time rendering - if (this._extentEl.shadowRoot) { - this._extentEl.shadowRoot.innerHTML = ''; + if (this._linkEl.shadowRoot) { + this._linkEl.shadowRoot.innerHTML = ''; } this._removeCSS(); //Leave the layers cleared if the layer is not visible @@ -136,7 +138,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ }), parser = new DOMParser(), features = this._features, - extentEl = this._extentEl, + linkEl = this._linkEl, map = this._map, context = this, MAX_PAGES = 10, @@ -158,7 +160,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ : null; url = url ? new URL(url, base).href : null; // TODO if the xml parser barfed but the response is application/geo+json, use the parent addData method - let nativeZoom = (extentEl._nativeZoom = + let nativeZoom = (linkEl._nativeZoom = (mapml.querySelector('map-meta[name=zoom]') && +M._metaContentToObject( mapml @@ -166,7 +168,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ .getAttribute('content') ).value) || 0); - let nativeCS = (extentEl._nativeCS = + let nativeCS = (linkEl._nativeCS = (mapml.querySelector('map-meta[name=cs]') && M._metaContentToObject( mapml @@ -179,8 +181,8 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ // make a clone, prevent the elements from being removed from mapml file // same as _attachToLayer() in MapMLLayer.js for (let el of mapml.querySelector('map-body').children) { - extentEl.shadowRoot.append(el._DOMnode); - el._DOMnode._extentEl = extentEl; + linkEl.shadowRoot.append(el._DOMnode); + el._DOMnode._linkEl = linkEl; } if (url && --limit) { return _pullFeatureFeed(url, limit); diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index 602cda17b..13a2aabd6 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -25,6 +25,11 @@ export var TemplatedLayer = L.LayerGroup.extend({ // add to this.options.pane this.options.pane.appendChild(this._container); }, + redraw: function () { + this.eachLayer(function (layer) { + layer.redraw(); + }); + }, //addTo: function(map) { //for(let i = 0; i < this._templates.length; i++){ // this._templates[0].layer.addTo(map); From 72f4a8a630be713b7da2540760178230fe6c2ffa Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 22 Nov 2023 16:41:06 -0500 Subject: [PATCH 16/46] WIP on map-link, especially map-link[rel=query] - get rid of MapMLLayer.queryable property, replace with dynamic layer-.queryable() method, which looks for map-link[rel=query] and other properties that determine if a layer is queryable - create shadowRoot in linkEl for queried features - remove shadowRoot from extentEl, since it no longer needs it - MapMLLayer.getQueryTemplates radically simplified - map-feature now uses shadow root of map-link, not map-extent --- src/layer.js | 10 +++++- src/map-extent.js | 6 ---- src/map-feature.js | 4 +-- src/map-link.js | 12 ++----- src/mapml/features/path.js | 6 +--- src/mapml/handlers/QueryHandler.js | 12 ++----- src/mapml/layers/Crosshair.js | 2 +- src/mapml/layers/FeatureLayer.js | 10 +++--- src/mapml/layers/MapMLLayer.js | 52 ++++++++++-------------------- 9 files changed, 41 insertions(+), 73 deletions(-) diff --git a/src/layer.js b/src/layer.js index 8ec8dc443..06bdebfa1 100644 --- a/src/layer.js +++ b/src/layer.js @@ -408,7 +408,15 @@ export class MapLayer extends HTMLElement { } } } - + queryable() { + let content = this.shadowRoot ? this.shadowRoot : this; + return ( + content.querySelector('map-extent > map-link[rel=query]') && + this.checked && + this._layer && + !this.hidden + ); + } getOuterHTML() { let tempElement = this.cloneNode(true); diff --git a/src/map-extent.js b/src/map-extent.js index c16f58064..b15af167f 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -133,12 +133,6 @@ export class MapExtent extends HTMLElement { this.parentLayer.hasAttribute('data-moving') ) return; - if ( - this.querySelector('map-link[rel=query], map-link[rel=features]') && - !this.shadowRoot - ) { - this.attachShadow({ mode: 'open' }); - } await this.parentLayer.whenReady(); this.mapEl = this.parentLayer.closest('mapml-viewer,map[is=web-map]'); await this.mapEl.whenProjectionDefined(this.units).catch(() => { diff --git a/src/map-feature.js b/src/map-feature.js index ed9eb4108..1f0684e2c 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -221,7 +221,7 @@ export class MapFeature extends HTMLElement { let mapmlvectors = this._layer._mapmlvectors; // "synchronize" the event handlers between map-feature and if (!this.querySelector('map-geometry')) return; - if (!this._extentEl) { + if (!this._linkEl) { let native = this._getNativeZoomAndCS(this._layer._content); this._featureGroup = mapmlvectors.addData(this, native.cs, native.zoom); if (parentLayer.checked) { @@ -229,7 +229,7 @@ export class MapFeature extends HTMLElement { } mapmlvectors._layers[this._featureGroup._leaflet_id] = this._featureGroup; - if (mapmlvectors._staticFeature && !this._extentEl) { + if (mapmlvectors._staticFeature && !this._linkEl) { // update zoom bounds of vector layer mapmlvectors.zoomBounds = M.getZoomBounds( this._layer._content, diff --git a/src/map-link.js b/src/map-link.js index 214d5d022..22688b4b2 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -130,7 +130,6 @@ export class MapLink extends HTMLElement { if (oldValue !== newValue) { // handle side effects if (newValue === 'query') { - this.parentExtent.parentLayer._layer.queryable = true; } } break; @@ -262,20 +261,15 @@ export class MapLink extends HTMLElement { linkEl: this }).addTo(this.parentExtent._extentLayer); } else if (this.rel === 'query') { + this.attachShadow({ mode: 'open' }); // add template to array of queryies to be added to map and processed // on click/tap events this.hasSetBoundsHandler = true; if (!this._queries) { this._queries = []; } - this.extentBounds = this.getBounds(); - this.zoomBounds = this.getZoomBounds(); - if (!this.parentExtent._layer._properties._queries) - this.parentExtent._layer._properties._queries = []; - this.parentExtent._layer._properties._queries.push( - // need to refactor the _setupQueryVars args / migrate it to map-link? - L.extend(this._templateVars, this._setupQueryVars(this._templateVars)) - ); + L.extend(this._templateVars, this._setupQueryVars(this._templateVars)); + L.extend(this._templateVars, { extentBounds: this.getBounds() }); } } _setupQueryVars(template) { diff --git a/src/mapml/features/path.js b/src/mapml/features/path.js index 00de8204f..401e21fe8 100644 --- a/src/mapml/features/path.js +++ b/src/mapml/features/path.js @@ -75,11 +75,7 @@ export var Path = L.Path.extend({ nextLayer = this.options._leafletLayer._layerEl.nextElementSibling; while (nextLayer && onTop) { if (nextLayer.tagName && nextLayer.tagName.toUpperCase() === 'LAYER-') - onTop = !( - nextLayer.checked && - nextLayer._layer && - nextLayer._layer.queryable - ); + onTop = !nextLayer.queryable(); nextLayer = nextLayer.nextElementSibling; } if (onTop && dragStart) { diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index 15afc219f..5c8d28aa6 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -15,14 +15,8 @@ export var QueryHandler = L.Handler.extend({ var layers = this.options.mapEl.layers; // work backwards in document order (top down) for (var l = layers.length - 1; l >= 0; l--) { - var mapmlLayer = layers[l]._layer; - if ( - layers[l].checked && - mapmlLayer && - mapmlLayer.queryable && - !mapmlLayer._layerEl.hidden - ) { - return mapmlLayer; + if (layers[l].queryable()) { + return layers[l]._layer; } } }, @@ -259,7 +253,7 @@ export var QueryHandler = L.Handler.extend({ if (f.status === 'fulfilled') { // create connection between queried and its parent for (let feature of f.value.features) { - feature._extentEl = f.value.template._extentEl; + feature._linkEl = f.value.template.linkEl; } layer._mapmlFeatures = layer._mapmlFeatures.concat(f.value.features); } diff --git a/src/mapml/layers/Crosshair.js b/src/mapml/layers/Crosshair.js index 67848530b..89d673e1b 100644 --- a/src/mapml/layers/Crosshair.js +++ b/src/mapml/layers/Crosshair.js @@ -73,7 +73,7 @@ export var Crosshair = L.Layer.extend({ let layers = this._map.options.mapEl.layers; if (this._map.isFocused) { for (let layer of layers) { - if (layer.checked && layer._layer.queryable) { + if (layer.queryable()) { return true; } } diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index d43dd37ba..22640af6b 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -124,11 +124,11 @@ export var FeatureLayer = L.FeatureGroup.extend({ let feature = this._mapmlFeatures[e.i]; if (e.type === 'featurepagination') { // remove map-feature only (keep meta's) when paginating - feature._extentEl.shadowRoot.querySelector('map-feature')?.remove(); + feature._linkEl.shadowRoot.querySelector('map-feature')?.remove(); } else { // empty the map-extent shadowRoot // remove the prev / next one and 's from shadow if there is any - feature._extentEl.shadowRoot.replaceChildren(); + feature._linkEl.shadowRoot.replaceChildren(); } this.clearLayers(); feature._featureGroup = this.addData( @@ -139,10 +139,10 @@ export var FeatureLayer = L.FeatureGroup.extend({ // append all map-meta from mapml document if (e.meta) { for (let i = 0; i < e.meta.length; i++) { - feature._extentEl.shadowRoot.appendChild(e.meta[i]); + feature._linkEl.shadowRoot.appendChild(e.meta[i]); } } - feature._extentEl.shadowRoot.appendChild(feature); + feature._linkEl.shadowRoot.appendChild(feature); e.popup._navigationBar.querySelector('p').innerText = e.i + 1 + '/' + this.options._leafletLayer._totalFeatureCount; e.popup._content @@ -157,7 +157,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ function (e) { this.shadowRoot.innerHTML = ''; }, - feature._extentEl + feature._linkEl ); } }, diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 5ef03794f..8eda0ab66 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -606,43 +606,25 @@ export var MapMLLayer = L.Layer.extend({ return this._properties.projection; }, getQueryTemplates: function (pcrsClick) { - const mapExtents = this._layerEl.querySelectorAll('map-extent').length - ? this._layerEl.querySelectorAll('map-extent') - : this._layerEl.shadowRoot.querySelectorAll('map-extent'); - if (this._properties && this._properties._queries) { + const queryLinks = this._layerEl.querySelectorAll( + 'map-extent[checked] map-link[rel=query]' + ).length + ? this._layerEl.querySelectorAll( + 'map-extent[checked] map-link[rel=query]' + ) + : this._layerEl.shadowRoot.querySelectorAll( + 'map-extent[checked] map-link[rel=query]' + ).length + ? this._layerEl.shadowRoot.querySelectorAll( + 'map-extent[checked] map-link[rel=query]' + ) + : null; + if (queryLinks) { var templates = []; - // only return queries that are in bounds - if ( - this._layerEl.checked && - !this._layerEl.hidden && - this._layerEl._layerControlHTML - ) { - let layerAndExtents = this._layerEl._layerControlHTML.querySelectorAll( - '.mapml-layer-item-name' - ); - for (let i = 0; i < layerAndExtents.length; i++) { - if (layerAndExtents[i].extent || mapExtents.length === 1) { - // the layer won't have an .extent property, this is kind of a hack - let extent = layerAndExtents[i].extent || 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._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) - ) { - templates.push(queryTemplate); - } - } - } - } - } - } - return templates; + for (let i = 0; i < queryLinks.length; i++) { + templates.push(queryLinks[i]._templateVars); } + return templates; } }, _attachSkipButtons: function (e) { From d42637ddc620b9316241c8c4a9244895fe287762 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 22 Nov 2023 17:01:20 -0500 Subject: [PATCH 17/46] WIP on map-link - change the order of defines for map-input and map-select, so that they have parentElement.whenReady available to them when booting up. --- src/mapml-viewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index cd9b6ae3c..ff6e1da22 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -1408,7 +1408,7 @@ window.customElements.define('mapml-viewer', MapViewer); window.customElements.define('layer-', MapLayer); window.customElements.define('map-caption', MapCaption); window.customElements.define('map-feature', MapFeature); +window.customElements.define('map-extent', MapExtent); window.customElements.define('map-input', MapInput); window.customElements.define('map-select', MapSelect); -window.customElements.define('map-extent', MapExtent); window.customElements.define('map-link', MapLink); From 03ee57a200ac14698d092dacea3813bee5f77f59 Mon Sep 17 00:00:00 2001 From: prushfor Date: Thu, 23 Nov 2023 09:25:40 -0500 Subject: [PATCH 18/46] WIP on map-link: * delete MapMLLayer._imageContainer --- src/mapml/layers/MapMLLayer.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 8eda0ab66..ed3d53c7d 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -25,12 +25,6 @@ export var MapMLLayer = L.Layer.extend({ this._container = L.DomUtil.create('div', 'leaflet-layer'); this.changeOpacity(this.options.opacity); L.DomUtil.addClass(this._container, 'mapml-layer'); - this._imageContainer = L.DomUtil.create( - 'div', - 'leaflet-layer', - this._container - ); - L.DomUtil.addClass(this._imageContainer, 'mapml-image-container'); // this layer 'owns' a mapmlTileLayer, which is a subclass of L.GridLayer // it 'passes' what tiles to load via the content of this._mapmlTileContainer @@ -89,8 +83,6 @@ export var MapMLLayer = L.Layer.extend({ onAdd: function (map) { this._map = map; if (this._mapmlvectors) map.addLayer(this._mapmlvectors); - // the layer._imageContainer property contains an element in which - // content will be maintained //only add the layer if there are tiles to be rendered if (this._staticTileLayer) { From 4ba7595a16d450959d3d5a4790bb96550cb6f580 Mon Sep 17 00:00:00 2001 From: prushfor Date: Thu, 23 Nov 2023 12:12:22 -0500 Subject: [PATCH 19/46] WIP on map-link: * layer-.queryable only considers a map-link[rel=query] if it is in a layer-[checked] in a map-extent[checked] * update map-link.getBase to use the (resolved) layer-.src as a base against which to resolve any map-base.href, unless it's in local content, in which case it uses the map-link.baseURI value as base to resolve against * restore erroneously removed check for query location to be within bounds of the queryable link * ensure that the TemplatedLayer.onAdd and .onRemove CALL THE INHERITED METHOD OF THE SAME NAME INSTEAD OF SIMPLY OVERRIDING THAT FUNCTION This was the cause of much confusion! --- src/layer.js | 2 +- src/map-link.js | 13 ++++++++++--- src/mapml/layers/MapMLLayer.js | 4 +++- src/mapml/layers/TemplatedLayer.js | 2 ++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/layer.js b/src/layer.js index 06bdebfa1..7d72ef49e 100644 --- a/src/layer.js +++ b/src/layer.js @@ -411,7 +411,7 @@ export class MapLayer extends HTMLElement { queryable() { let content = this.shadowRoot ? this.shadowRoot : this; return ( - content.querySelector('map-extent > map-link[rel=query]') && + content.querySelector('map-extent[checked] > map-link[rel=query]') && this.checked && this._layer && !this.hidden diff --git a/src/map-link.js b/src/map-link.js index 22688b4b2..c046eccf6 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -560,7 +560,8 @@ export class MapLink extends HTMLElement { } getBase() { let layer = this.getRootNode().host; - return new URL( + // + let relativeURL = this.getRootNode().querySelector('map-base') && this.getRootNode() instanceof ShadowRoot ? this.getRootNode().querySelector('map-base').getAttribute('href') @@ -571,8 +572,14 @@ export class MapLink extends HTMLElement { : /* else use the resolved value */ new URL( layer.src, layer.baseURI - ).href - ).href; + ).href; + + // when remote content, use layer.src as base else use baseURI of map-link + let baseURL = + this.getRootNode() instanceof ShadowRoot + ? new URL(layer.src, layer.baseURI).href + : this.baseURI; + return new URL(relativeURL, baseURL).href; } getFallbackBounds(projection) { let bounds; diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index ed3d53c7d..686909409 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -614,7 +614,9 @@ export var MapMLLayer = L.Layer.extend({ if (queryLinks) { var templates = []; for (let i = 0; i < queryLinks.length; i++) { - templates.push(queryLinks[i]._templateVars); + if (queryLinks[i]._templateVars.extentBounds.contains(pcrsClick)) { + templates.push(queryLinks[i]._templateVars); + } } return templates; } diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index 13a2aabd6..dfc47f289 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -22,6 +22,7 @@ export var TemplatedLayer = L.LayerGroup.extend({ return this._container; }, onAdd: function (map) { + L.LayerGroup.prototype.onAdd.call(this, this._map); // add to this.options.pane this.options.pane.appendChild(this._container); }, @@ -51,6 +52,7 @@ export var TemplatedLayer = L.LayerGroup.extend({ } }, onRemove: function () { + L.LayerGroup.prototype.onRemove.call(this, this._map); L.DomUtil.remove(this._container); }, From b4a80da5eea83d8b61eeaeac667b8023b8275ff4 Mon Sep 17 00:00:00 2001 From: prushfor Date: Thu, 23 Nov 2023 15:14:52 -0500 Subject: [PATCH 20/46] WIP on map-link: * add getMapEl(), getLayerEl() API to map-link * add mapEl reference to options for FeatureLayer wherever created * update TemplatedFeaturesLayer, TemplatedTileLayer, TemplatedImageLayer FeatureLayer .isVisible method so that it doesn't require the layer to be on the map when it is run; will use reference to mapEl._map in order to run * update DebugOverlay to use linkEl reference to get layerEl ref --- src/layer.js | 65 ++++++++++++---------- src/map-link.js | 17 ++++-- src/mapml/handlers/QueryHandler.js | 3 +- src/mapml/layers/DebugOverlay.js | 8 +-- src/mapml/layers/FeatureLayer.js | 7 ++- src/mapml/layers/MapMLLayer.js | 1 + src/mapml/layers/TemplatedFeaturesLayer.js | 14 +++-- src/mapml/layers/TemplatedImageLayer.js | 9 ++- src/mapml/layers/TemplatedLayer.js | 2 +- src/mapml/layers/TemplatedTileLayer.js | 19 ++++--- 10 files changed, 85 insertions(+), 60 deletions(-) diff --git a/src/layer.js b/src/layer.js index 7d72ef49e..f28ed1767 100644 --- a/src/layer.js +++ b/src/layer.js @@ -341,38 +341,47 @@ export class MapLayer extends HTMLElement { for (let i = 0; i < mapExtents.length; i++) { extentLinksReady.push(mapExtents[i].whenLinksReady()); } - Promise.allSettled(extentLinksReady).then(() => { - let disabledExtentCount = 0, - totalExtentCount = 0, - layerTypes = ['_staticTileLayer', '_mapmlvectors', '_extentLayer']; - for (let j = 0; j < layerTypes.length; j++) { - let type = layerTypes[j]; - if (this.checked) { - if (type === '_extentLayer' && mapExtents.length > 0) { - for (let i = 0; i < mapExtents.length; i++) { + Promise.allSettled(extentLinksReady) + .then(() => { + let disabledExtentCount = 0, + totalExtentCount = 0, + layerTypes = [ + '_staticTileLayer', + '_mapmlvectors', + '_extentLayer' + ]; + for (let j = 0; j < layerTypes.length; j++) { + let type = layerTypes[j]; + if (this.checked) { + if (type === '_extentLayer' && mapExtents.length > 0) { + for (let i = 0; i < mapExtents.length; i++) { + totalExtentCount++; + if (mapExtents[i]._validateDisabled()) + disabledExtentCount++; + } + } else if (layer[type]) { + // not a templated layer totalExtentCount++; - if (mapExtents[i]._validateDisabled()) disabledExtentCount++; + if (!layer[type].isVisible()) disabledExtentCount++; } - } else if (layer[type]) { - // not a templated layer - totalExtentCount++; - if (!layer[type].isVisible()) disabledExtentCount++; } } - } - // if all extents are not visible / disabled, set layer to disabled - if ( - disabledExtentCount === totalExtentCount && - disabledExtentCount !== 0 - ) { - this.setAttribute('disabled', ''); - this.disabled = true; - } else { - this.removeAttribute('disabled'); - this.disabled = false; - } - this.toggleLayerControlDisabled(); - }); + // if all extents are not visible / disabled, set layer to disabled + if ( + disabledExtentCount === totalExtentCount && + disabledExtentCount !== 0 + ) { + this.setAttribute('disabled', ''); + this.disabled = true; + } else { + this.removeAttribute('disabled'); + this.disabled = false; + } + this.toggleLayerControlDisabled(); + }) + .catch((e) => { + console.log(e); + }); } }, 0); } diff --git a/src/map-link.js b/src/map-link.js index c046eccf6..628b1d094 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -109,6 +109,12 @@ export class MapLink extends HTMLElement { this.setAttribute('projection', val); } } + getMapEl() { + return this.parentElement.mapEl; + } + getLayerEl() { + if (this.parentExtent) return this.parentExtent.parentLayer; + } attributeChangedCallback(name, oldValue, newValue) { //['type','rel','href','hreflang','tref','tms','projection']; @@ -226,7 +232,7 @@ export class MapLink extends HTMLElement { if (!this.tref || !this.parentExtent) return; await this.parentExtent.whenReady(); await this._templateVars.inputsReady; - this.mapEl = this.parentExtent.mapEl; + this.mapEl = this.getMapEl(); // create the layer type appropriate to the rel value this.zIndex = Array.from( this.parentExtent.querySelectorAll( @@ -241,14 +247,16 @@ export class MapLink extends HTMLElement { errorTileUrl: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', zIndex: this.zIndex, - pane: this.parentExtent._extentLayer.getContainer() + pane: this.parentExtent._extentLayer.getContainer(), + linkEl: this }).addTo(this.parentExtent._extentLayer); } else if (this.rel === 'image') { this._templatedLayer = M.templatedImageLayer(this._templateVars, { zoomBounds: this.getZoomBounds(), extentBounds: this.getBounds(), zIndex: this.zIndex, - pane: this.parentExtent._extentLayer.getContainer() + pane: this.parentExtent._extentLayer.getContainer(), + linkEl: this }).addTo(this.parentExtent._extentLayer); } else if (this.rel === 'features') { // map-feature retrieved by link will be stored in shadowRoot owned by link @@ -265,9 +273,6 @@ export class MapLink extends HTMLElement { // add template to array of queryies to be added to map and processed // on click/tap events this.hasSetBoundsHandler = true; - if (!this._queries) { - this._queries = []; - } L.extend(this._templateVars, this._setupQueryVars(this._templateVars)); L.extend(this._templateVars, { extentBounds: this.getBounds() }); } diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index 5c8d28aa6..0c2e0104a 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -278,7 +278,8 @@ export var QueryHandler = L.Handler.extend({ projection: map.options.projection, _leafletLayer: layer, query: true, - static: true + static: true, + mapEl: this.options.mapEl }); f.addTo(map); diff --git a/src/mapml/layers/DebugOverlay.js b/src/mapml/layers/DebugOverlay.js index 049eab823..1f45a321c 100644 --- a/src/mapml/layers/DebugOverlay.js +++ b/src/mapml/layers/DebugOverlay.js @@ -250,10 +250,10 @@ export var DebugVectors = L.LayerGroup.extend({ // tagged with the layer it came from let boundsTestTag = layers[i].extentBounds && - layers[i].options.extentEl.parentLayer.hasAttribute('data-testid') - ? layers[i].options.extentEl.parentLayer.getAttribute( - 'data-testid' - ) + layers[i].options.linkEl.getLayerEl().hasAttribute('data-testid') + ? layers[i].options.linkEl + .getLayerEl() + .getAttribute('data-testid') : layers[i].layerBounds && layers[i].options._leafletLayer._layerEl.hasAttribute( 'data-testid' diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 22640af6b..7b9933369 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -52,6 +52,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ }, isVisible: function () { + let map = this.options.mapEl._map; // if query, isVisible is unconditionally true if (this.options.query) return true; // if the featureLayer is for static features, i.e. it is the mapmlvector layer, @@ -62,7 +63,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ else if (this._staticFeature && Object.keys(this._features).length === 0) { return false; } else { - let mapZoom = this._map.getZoom(), + let mapZoom = map.getZoom(), withinZoom = this.zoomBounds ? mapZoom <= this.zoomBounds.maxZoom && mapZoom >= this.zoomBounds.minZoom @@ -73,9 +74,9 @@ export var FeatureLayer = L.FeatureGroup.extend({ this.layerBounds && this.layerBounds.overlaps( M.pixelToPCRSBounds( - this._map.getPixelBounds(), + map.getPixelBounds(), mapZoom, - this._map.options.projection + map.options.projection ) ) ); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 686909409..02ef7112d 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -400,6 +400,7 @@ export var MapMLLayer = L.Layer.extend({ // each owned child layer gets a reference to the root layer _leafletLayer: layer, static: true, + mapEl: layer._layerEl.parentElement, onEachFeature: function (properties, geometry) { // need to parse as HTML to preserve semantics and styles if (properties) { diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index 6edc8f855..cd6c1dc89 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -15,11 +15,12 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ }, isVisible: function () { - let mapZoom = this._map.getZoom(); + let map = this._linkEl.getMapEl()._map; + let mapZoom = map.getZoom(); let mapBounds = M.pixelToPCRSBounds( - this._map.getPixelBounds(), + map.getPixelBounds(), mapZoom, - this._map.options.projection + map.options.projection ); return ( mapZoom <= this.zoomBounds.maxZoom && @@ -34,13 +35,13 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ }; return events; }, - onAdd: function () { + onAdd: function (map) { + this._map = map; // this causes the layer (this._features) to actually render... this.options.pane.appendChild(this._container); this._map._addZoomLimit(this); var opacity = this.options.opacity || 1, - container = this._container, - map = this._map; + container = this._container; if (!this._features) { this._features = M.featureLayer(null, { // pass the vector layer a renderer of its own, otherwise leaflet @@ -55,6 +56,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ opacity: opacity, projection: map.options.projection, static: true, + mapEl: this._linkEl.getMapEl(), onEachFeature: function (properties, geometry) { // need to parse as HTML to preserve semantics and styles var c = document.createElement('div'); diff --git a/src/mapml/layers/TemplatedImageLayer.js b/src/mapml/layers/TemplatedImageLayer.js index a77e9d40b..6ca292939 100644 --- a/src/mapml/layers/TemplatedImageLayer.js +++ b/src/mapml/layers/TemplatedImageLayer.js @@ -3,6 +3,7 @@ export var TemplatedImageLayer = L.Layer.extend({ this._template = template; this._container = L.DomUtil.create('div', 'leaflet-layer'); L.DomUtil.addClass(this._container, 'mapml-image-container'); + this._linkEl = options.linkEl; this.zoomBounds = options.zoomBounds; this.extentBounds = options.extentBounds; L.setOptions( @@ -18,6 +19,7 @@ export var TemplatedImageLayer = L.Layer.extend({ return events; }, onAdd: function (map) { + this._map = map; // TODO: set this._map by ourselves this.options.pane.appendChild(this._container); map._addZoomLimit(this); //used to set the zoom limit of the map @@ -28,11 +30,12 @@ export var TemplatedImageLayer = L.Layer.extend({ this._onMoveEnd(); }, isVisible: function () { - let mapZoom = this._map.getZoom(); + let map = this._linkEl.getMapEl()._map; + let mapZoom = map.getZoom(); let mapBounds = M.pixelToPCRSBounds( - this._map.getPixelBounds(), + map.getPixelBounds(), mapZoom, - this._map.options.projection + map.options.projection ); return ( mapZoom <= this.zoomBounds.maxZoom && diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index dfc47f289..da20fc398 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -22,7 +22,7 @@ export var TemplatedLayer = L.LayerGroup.extend({ return this._container; }, onAdd: function (map) { - L.LayerGroup.prototype.onAdd.call(this, this._map); + L.LayerGroup.prototype.onAdd.call(this, map); // add to this.options.pane this.options.pane.appendChild(this._container); }, diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 0d36f351f..69a2ade8b 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -15,6 +15,7 @@ export var TemplatedTileLayer = L.TileLayer.extend({ // it's critical to have this.options.minZoom, minNativeZoom, maxZoom, maxNativeZoom // which we accomplish here. Used by Leaflet Map and GridLayer. L.extend(options, this.zoomBounds); + this._linkEl = options.linkEl; this.extentBounds = this.options.extentBounds; this._template = template; @@ -27,10 +28,10 @@ export var TemplatedTileLayer = L.TileLayer.extend({ L.extend(options, { pane: this.options.pane }) ); }, - onAdd: function () { + onAdd: function (map) { this.options.pane.appendChild(this._container); this._map._addZoomLimit(this); - L.TileLayer.prototype.onAdd.call(this, this._map); + L.TileLayer.prototype.onAdd.call(this, map); this._handleMoveEnd(); }, @@ -50,15 +51,16 @@ export var TemplatedTileLayer = L.TileLayer.extend({ }, isVisible: function () { - let mapZoom = this._map.getZoom(); + let map = this._linkEl.getMapEl()._map; + let mapZoom = map.getZoom(); let mapBounds = M.pixelToPCRSBounds( - this._map.getPixelBounds(), + map.getPixelBounds(), mapZoom, - this._map.options.projection + map.options.projection ); return ( - mapZoom <= this.options.maxZoom && - mapZoom >= this.options.minZoom && + mapZoom <= this.zoomBounds.maxZoom && + mapZoom >= this.zoomBounds.minZoom && this.extentBounds.overlaps(mapBounds) ); }, @@ -179,7 +181,8 @@ export var TemplatedTileLayer = L.TileLayer.extend({ static: false, layerBounds: this.extentBounds, zoomBounds: this.zoomBounds, - interactive: false + interactive: false, + mapEl: this._linkEl.getMapEl() }); for (let groupID in tileFeatures._layers) { From 3378b9d005043a8a3b1451791eba7cf1c7e64e96 Mon Sep 17 00:00:00 2001 From: prushfor Date: Thu, 23 Nov 2023 16:01:58 -0500 Subject: [PATCH 21/46] WIP on map-link: * delete map-link._templateVars.boundsFallbackPCRS, unused, problematic * change implementation of map-link.getBounds() to use getFallbackBounds when necessary, which gets those bounds from the CRS. map-link.getBounds() is called during connectedCallback, so usually the CRS is defined by then. * make map-extent.units getter fall back to M.FALLBACK_PROJECTION if no units attribute is present --- src/map-extent.js | 3 +-- src/map-link.js | 25 +++++++++---------------- src/mapml/handlers/QueryHandler.js | 2 +- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/map-extent.js b/src/map-extent.js index b15af167f..b54e6a77e 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -4,8 +4,7 @@ export class MapExtent extends HTMLElement { return ['checked', 'label', 'opacity', 'hidden']; } get units() { - // this should fallback to something?? - return this.getAttribute('units'); + return this.getAttribute('units') || M.FALLBACK_PROJECTION; } get checked() { diff --git a/src/map-link.js b/src/map-link.js index 628b1d094..22a013223 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -474,11 +474,7 @@ export class MapLink extends HTMLElement { inputsReady: Promise.allSettled(inputsReady), zoom: linkedZoomInput, zoomBounds: this._getZoomBounds(linkedZoomInput), - boundsFallbackPCRS: this.getFallbackBounds( - this.parentElement.units || M.FALLBACK_PROJECTION - ), - // TODO: make map-extent.units fall back automatically - projection: this.parentElement.units || M.FALLBACK_PROJECTION, + projection: this.parentElement.units, tms: this.tms, step: step }; @@ -493,12 +489,12 @@ export class MapLink extends HTMLElement { getBounds() { let template = this._templateVars; let inputs = template.values, - projection = template.projection || M.FALLBACK_PROJECTION, + projection = this.parentElement.units, value = 0, boundsUnit = M.FALLBACK_CS; - let bounds = M[template.projection].options.crs.tilematrix.bounds(0), + let bounds = M[projection].options.crs.tilematrix.bounds(0), defaultMinZoom = 0, - defaultMaxZoom = M[template.projection].options.resolutions.length - 1, + defaultMaxZoom = M[projection].options.resolutions.length - 1, nativeMinZoom = defaultMinZoom, nativeMaxZoom = defaultMaxZoom; let locInputs = false, @@ -554,12 +550,12 @@ export class MapLink extends HTMLElement { if (numberOfAxes >= 2) { locInputs = true; } - if (!locInputs && template.boundsFallbackPCRS) { - bounds = template.boundsFallbackPCRS; + if (!locInputs) { + bounds = this.getFallbackBounds(projection); } else if (locInputs) { bounds = M.boundsToPCRSBounds(bounds, value, projection, boundsUnit); } else { - bounds = M[template.projection].options.crs.pcrs.bounds; + bounds = M[projection].options.crs.pcrs.bounds; } return bounds; } @@ -621,11 +617,8 @@ export class MapLink extends HTMLElement { cs ); } else { - // TODO review. Should this.parentElement.units automatically fall back - // i.e. to OSMTILE if not specified? Probably, but it does not currently. - let fallbackProjection = - M[this.parentElement.units || M.FALLBACK_PROJECTION]; - bounds = fallbackProjection.options.crs.pcrs.bounds; + let crs = M[this.parentElement.units]; + bounds = crs.options.crs.pcrs.bounds; } return bounds; } diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index 0c2e0104a..c2a654282 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -279,7 +279,7 @@ export var QueryHandler = L.Handler.extend({ _leafletLayer: layer, query: true, static: true, - mapEl: this.options.mapEl + mapEl: layer.options.mapEl }); f.addTo(map); From bded4d28bc41daf30ee99cc51e028836d35835d0 Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 24 Nov 2023 15:20:50 -0500 Subject: [PATCH 22/46] WIP on map-link: * add 'zoomchangesrc' internal event which can happen during zoom animations and when the layer is added to the map. Like its name suggests, it changes the layer-.src when called upon based on presence of map-link rel=zoom(in|out), changing the source of the layer as required * small change to FeatureLayer.onAdd to use the passed map parameter * get rid of duplicate options.zoomBounds info on Templated(Content)Layer --- src/layer.js | 8 +++++ src/map-extent.js | 23 ++++++-------- src/map-link.js | 6 ++-- src/mapml/layers/FeatureLayer.js | 2 +- src/mapml/layers/MapMLLayer.js | 36 +++++++++------------- src/mapml/layers/TemplatedFeaturesLayer.js | 1 + src/mapml/layers/TemplatedImageLayer.js | 2 ++ src/mapml/layers/TemplatedTileLayer.js | 24 ++++++++++----- 8 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/layer.js b/src/layer.js index f28ed1767..d62228612 100644 --- a/src/layer.js +++ b/src/layer.js @@ -133,6 +133,14 @@ export class MapLayer extends HTMLElement { }, { once: true } ); + this.addEventListener( + 'zoomchangesrc', + function (e) { + e.stopPropagation(); + this.src = e.detail.href; + }, + { once: true } + ); this.addEventListener( 'changeprojection', function (e) { diff --git a/src/map-extent.js b/src/map-extent.js index b54e6a77e..052e9120d 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -316,25 +316,20 @@ export class MapExtent extends HTMLElement { // bounds should be able to be calculated unconditionally, not depend on map-extent.checked for (let j = 0; j < templates.length; j++) { + let zoomBounds = templates[j].getZoomBounds(); if (!bounds) { bounds = templates[j].getBounds(); - zoomMax = templates[j].getZoomBounds().maxZoom; - zoomMin = templates[j].getZoomBounds().minZoom; - maxNativeZoom = templates[j].getZoomBounds().maxNativeZoom; - minNativeZoom = templates[j].getZoomBounds().minNativeZoom; + zoomMax = zoomBounds.maxZoom; + zoomMin = zoomBounds.minZoom; + maxNativeZoom = zoomBounds.maxNativeZoom; + minNativeZoom = zoomBounds.minNativeZoom; } else { bounds.extend(templates[j].getBounds().min); bounds.extend(templates[j].getBounds().max); - zoomMax = Math.max(zoomMax, templates[j].getZoomBounds().maxZoom); - zoomMin = Math.min(zoomMin, templates[j].getZoomBounds().minZoom); - maxNativeZoom = Math.max( - maxNativeZoom, - templates[j].getZoomBounds().maxNativeZoom - ); - minNativeZoom = Math.min( - minNativeZoom, - templates[j].getZoomBounds().minNativeZoom - ); + zoomMax = Math.max(zoomMax, zoomBounds.maxZoom); + zoomMin = Math.min(zoomMin, zoomBounds.minZoom); + maxNativeZoom = Math.max(maxNativeZoom, zoomBounds.maxNativeZoom); + minNativeZoom = Math.min(minNativeZoom, zoomBounds.minNativeZoom); } } // cannot be named as layerBounds if we decide to keep the debugoverlay logic diff --git a/src/map-link.js b/src/map-link.js index 22a013223..35038d7e8 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -473,7 +473,6 @@ export class MapLink extends HTMLElement { values: inputs, inputsReady: Promise.allSettled(inputsReady), zoom: linkedZoomInput, - zoomBounds: this._getZoomBounds(linkedZoomInput), projection: this.parentElement.units, tms: this.tms, step: step @@ -481,7 +480,7 @@ export class MapLink extends HTMLElement { } } getZoomBounds() { - return this._templateVars.zoomBounds; + return this._getZoomBounds(this._templateVars.zoom); } /** * TODO: review getBounds for sanity, also getFallbackBounds, perhaps integrate @@ -663,8 +662,7 @@ export class MapLink extends HTMLElement { metaMax || (zoomInput ? +zoomInput.getAttribute('max') - : M[this.parentElement.units || M.FALLBACK_PROJECTION].options - .resolutions.length - 1); + : M[this.parentElement.units].options.resolutions.length - 1); zoomBounds.maxNativeZoom = zoomInput ? +zoomInput.getAttribute('max') : zoomBounds.maxZoom; diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 7b9933369..523274431 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -88,7 +88,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ L.FeatureGroup.prototype.onAdd.call(this, map); if (this._staticFeature) { this._resetFeatures(); - this.options._leafletLayer._map._addZoomLimit(this); + 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 02ef7112d..4fe8ac128 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -92,6 +92,7 @@ export var MapMLLayer = L.Layer.extend({ this.setZIndex(this.options.zIndex); this.getPane().appendChild(this._container); map.on('popupopen', this._attachSkipButtons, this); + this._validateLayerZoom({ zoom: map.getZoom() }); }, _calculateBounds: function () { @@ -174,14 +175,10 @@ export var MapMLLayer = L.Layer.extend({ } }, - addTo: function (map) { - map.addLayer(this); - return this; - }, getEvents: function () { - return { zoomanim: this._onZoomAnim }; + return { zoomanim: this._validateLayerZoom }; }, - _onZoomAnim: function (e) { + _validateLayerZoom: function (e) { // this callback will be invoked AFTER has been removed // but due to the characteristic of JavaScript, the context (this pointer) can still be used // the easiest way to solve this: @@ -218,24 +215,21 @@ export var MapMLLayer = L.Layer.extend({ } } } - var canZoom = - (toZoom < min && this._properties.zoomout) || - (toZoom > max && this._properties.zoomin); + let targetURL; if (!(min <= toZoom && 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._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._properties.zoomin; - this._layerEl.src = this._properties.zoomin; + targetURL = 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; + targetURL = this._properties.zoomout; + } + if (targetURL) { + this._layerEl.dispatchEvent( + new CustomEvent('zoomchangesrc', { + detail: { + href: targetURL + } + }) + ); } } }, diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index cd6c1dc89..cb0e72780 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -5,6 +5,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._container = L.DomUtil.create('div', 'leaflet-layer'); L.DomUtil.addClass(this._container, 'mapml-features-container'); this.zoomBounds = options.zoomBounds; + delete options.zoomBounds; L.extend(options, this.zoomBounds); this.extentBounds = options.extentBounds; this._linkEl = options.linkEl; diff --git a/src/mapml/layers/TemplatedImageLayer.js b/src/mapml/layers/TemplatedImageLayer.js index 6ca292939..00be7b2be 100644 --- a/src/mapml/layers/TemplatedImageLayer.js +++ b/src/mapml/layers/TemplatedImageLayer.js @@ -5,6 +5,8 @@ export var TemplatedImageLayer = L.Layer.extend({ L.DomUtil.addClass(this._container, 'mapml-image-container'); this._linkEl = options.linkEl; this.zoomBounds = options.zoomBounds; + delete options.zoomBounds; + L.extend(options, this.zoomBounds); this.extentBounds = options.extentBounds; L.setOptions( this, diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 69a2ade8b..da78070d8 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -10,11 +10,19 @@ export var TemplatedTileLayer = L.TileLayer.extend({ // options first... options.tms = template.tms; L.setOptions(this, options); - this._setUpTileTemplateVars(template); - this.zoomBounds = this.options.zoomBounds; // it's critical to have this.options.minZoom, minNativeZoom, maxZoom, maxNativeZoom - // which we accomplish here. Used by Leaflet Map and GridLayer. - L.extend(options, this.zoomBounds); + // because they are used by Leaflet Map and GridLayer, but we + // don't need two copies of that info on our options object, so set the + // .zoomBounds property (which is used externally), then delete the option + // before unpacking the zoomBound object's properties onto this.options.minZ... etc. + let zoomBounds = options.zoomBounds; + this.zoomBounds = zoomBounds; + delete options.zoomBounds; + // unpack object to this.options.minZ... etc where minZ... are the props + // of the this.zoomBounds object: + L.extend(this.options, this.zoomBounds); + // _setup call here relies on this.options.minZ.. etc + this._setUpTileTemplateVars(template); this._linkEl = options.linkEl; this.extentBounds = this.options.extentBounds; @@ -493,8 +501,8 @@ export var TemplatedTileLayer = L.TileLayer.extend({ // by first processing the extent to determine the zoom and if none, adding // one and second by copying that zoom into the set of template variable inputs // even if it is not referenced by one of the template's variable references - var zmin = template.zoomBounds.minNativeZoom, - zmax = template.zoomBounds.maxNativeZoom; + var zmin = this.options.minNativeZoom, + zmax = this.options.maxNativeZoom; for (var z = 0; z <= zmax; z++) { template.tilematrix.bounds[z] = z >= zmin @@ -507,8 +515,8 @@ export var TemplatedTileLayer = L.TileLayer.extend({ }, _clampZoom: function (zoom) { let clamp = L.GridLayer.prototype._clampZoom.call(this, zoom); - if (this._template.step > this.zoomBounds.maxNativeZoom) - this._template.step = this.zoomBounds.maxNativeZoom; + if (this._template.step > this.options.maxNativeZoom) + this._template.step = this.options.maxNativeZoom; if (zoom !== clamp) { zoom = clamp; From f41c37295b27ea83d261d386bc5c42434e63f6e9 Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 24 Nov 2023 17:21:14 -0500 Subject: [PATCH 23/46] WIP on map-link, really not ready except for re-work: * re-write logic of map-link.getMapEl and map-link.getLayerEl * create _createSelfOrStyleLink() function from part of logic from MapMLLayer.getAlternateStyles... the part that applies to a single link (generates an option for it). * re-code / generalize map-link.whenReady so that it works according to the rel value of the link * move MapMLLayer.getAlternateStyles() to layer-.getAlternateStyles(links) * call it from createLayerControlForLayer * try to wrap layer control generation in layer-.connectedCallback in a promise for links being ready when generating the layer control. Formerly, they didn't have to be ready, just the markup had to be available. To be continued... --- src/layer.js | 58 ++++++++++---- src/map-link.js | 80 +++++++++++++++---- .../layers/createLayerControlForLayer.js | 12 ++- src/mapml/layers/MapMLLayer.js | 69 ---------------- 4 files changed, 118 insertions(+), 101 deletions(-) diff --git a/src/layer.js b/src/layer.js index d62228612..a0540bb65 100644 --- a/src/layer.js +++ b/src/layer.js @@ -181,10 +181,6 @@ export class MapLayer extends HTMLElement { opacity: this.opacity } ); - this._createLayerControlHTML(); - this._attachedToMap(); - this._validateDisabled(); - resolve(); }) .catch((error) => { this._fetchError = true; @@ -198,21 +194,36 @@ export class MapLayer extends HTMLElement { mapprojection: this.parentElement.projection, opacity: this.opacity }); + } + return this.shadowRoot; + }) + .then((shadowRoot) => { + let node = shadowRoot ? shadowRoot : this; + let links = node.querySelectorAll( + 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' + ); + let linksReady = []; + for (let link of links) { + linksReady.push(link.whenReady()); + } + return Promise.allSettled(linksReady); + }) + .then(() => { this._createLayerControlHTML(); this._attachedToMap(); this._validateDisabled(); resolve(); - } - }).catch((e) => { - if (e.type === 'changeprojection') { - this.src = e.detail.href; - } else { - console.log(e); - this.dispatchEvent( - new CustomEvent('error', { detail: { target: this } }) - ); - } - }); + }) + .catch((e) => { + if (e.type === 'changeprojection') { + this.src = e.detail.href; + } else { + console.log(e); + this.dispatchEvent( + new CustomEvent('error', { detail: { target: this } }) + ); + } + }); } _attachedToMap() { // set i to the position of this layer element in the set of layers @@ -434,6 +445,23 @@ export class MapLayer extends HTMLElement { !this.hidden ); } + getAlternateStyles(styleLinks) { + if (styleLinks.length > 1) { + var stylesControl = document.createElement('details'), + stylesControlSummary = document.createElement('summary'); + stylesControlSummary.innerText = 'Style'; + stylesControl.appendChild(stylesControlSummary); + + for (var j = 0; j < styleLinks.length; j++) { + stylesControl.appendChild(styleLinks[j].getLayerControlOption()); + L.DomUtil.addClass( + stylesControl, + 'mapml-layer-item-style mapml-control-layers' + ); + } + return stylesControl; + } + } getOuterHTML() { let tempElement = this.cloneNode(true); diff --git a/src/map-link.js b/src/map-link.js index 35038d7e8..3dec306e6 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -110,10 +110,14 @@ export class MapLink extends HTMLElement { } } getMapEl() { - return this.parentElement.mapEl; + return this.getRootNode() instanceof ShadowRoot + ? this.getRootNode().host + : this.closest('mapml-viewer,map[is=web-map]'); } getLayerEl() { - if (this.parentExtent) return this.parentExtent.parentLayer; + return this.getRootNode() instanceof ShadowRoot + ? this.getRootNode().host + : this.closest('layer-'); } attributeChangedCallback(name, oldValue, newValue) { @@ -270,9 +274,6 @@ export class MapLink extends HTMLElement { }).addTo(this.parentExtent._extentLayer); } else if (this.rel === 'query') { this.attachShadow({ mode: 'open' }); - // add template to array of queryies to be added to map and processed - // on click/tap events - this.hasSetBoundsHandler = true; L.extend(this._templateVars, this._setupQueryVars(this._templateVars)); L.extend(this._templateVars, { extentBounds: this.getBounds() }); } @@ -672,6 +673,49 @@ export class MapLink extends HTMLElement { _validateDisabled() { return this._templatedLayer.isVisible(); } + _createSelfOrStyleLink() { + let layerEl = this.getLayerEl(); + const changeStyle = function (e) { + L.DomEvent.stop(e); + layerEl.dispatchEvent( + new CustomEvent('changestyle', { + detail: { + src: e.target.getAttribute('data-href') + } + }) + ); + }; + + let styleOption = document.createElement('div'), + styleOptionInput = styleOption.appendChild( + document.createElement('input') + ); + styleOptionInput.setAttribute('type', 'radio'); + styleOptionInput.setAttribute('id', 'rad-' + L.stamp(styleOptionInput)); + styleOptionInput.setAttribute( + 'name', + // grouping radio buttons based on parent layer's style + 'styles-' + L.stamp(stylesControl) + ); + styleOptionInput.setAttribute('value', this.getAttribute('title')); + styleOptionInput.setAttribute( + 'data-href', + new URL(this.href, this.getBase()).href + ); + var styleOptionLabel = styleOption.appendChild( + document.createElement('label') + ); + styleOptionLabel.setAttribute('for', 'rad-' + L.stamp(styleOptionInput)); + styleOptionLabel.innerText = this.title; + if (this.rel === 'style self' || this.rel === 'self style') { + styleOptionInput.checked = true; + } + this._styleOption = styleOption; + L.DomEvent.on(styleOptionInput, 'click', changeStyle, layer); + } + getLayerControlOption() { + return this._styleOption; + } // Resolve the templated URL with info from the sibling map-input's resolve() { @@ -700,23 +744,23 @@ export class MapLink extends HTMLElement { } whenReady() { return new Promise((resolve, reject) => { - let interval, failureTimer; + let interval, failureTimer, ready; switch (this.rel.toLowerCase()) { // for some cases, require a dependency check case 'tile': case 'image': case 'features': - if (this._templatedLayer) { - resolve(); - } - interval = setInterval(testForContentLink, 300, this); - failureTimer = setTimeout(linkNotDefined, 10000); + ready = '_templatedLayer'; break; - case 'query': case 'style': case 'self': case 'style self': case 'self style': + ready = '_styleOption'; + break; + case 'query': + ready = 'shadowRoot'; + break; case 'zoomin': case 'zoomout': case 'legend': @@ -725,9 +769,17 @@ export class MapLink extends HTMLElement { case 'license': resolve(); break; + default: + resolve(); + break; + } + if (this[ready]) { + resolve(); } - function testForContentLink(linkElement) { - if (linkElement._linkLayer) { + interval = setInterval(testForLinkReady, 300, this); + failureTimer = setTimeout(linkNotDefined, 10000); + function testForLinkReady(linkElement) { + if (linkElement[ready]) { clearInterval(interval); clearTimeout(failureTimer); resolve(); diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js index e25bb6f5c..840af9657 100644 --- a/src/mapml/elementSupport/layers/createLayerControlForLayer.js +++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js @@ -286,8 +286,14 @@ export var createLayerControlHTML = function () { itemSettingControlButton.appendChild(settingsButtonNameIcon); settingsButtonNameIcon.appendChild(svgSettingsControlIcon); - if (this._layer._styles) { - layerItemSettings.appendChild(this._layer._styles); + let mapml = this.shadowRoot ? this.shadowRoot : this; + var styleLinks = mapml.querySelectorAll( + 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' + ); + let styles; + if (styleLinks) { + styles = this.getAlternateStyles(styleLinks); + layerItemSettings.appendChild(this.getAlternateStyles(styleLinks)); } this._layerControlCheckbox = input; @@ -297,7 +303,7 @@ export var createLayerControlHTML = function () { this._layerControlHTML = fieldset; this._layerItemSettingsHTML = layerItemSettings; this._propertiesGroupAnatomy = extentsFieldset; - this._styles = this._layer._styles; + this._styles = styles; extentsFieldset.setAttribute('aria-label', 'Sublayers'); extentsFieldset.setAttribute('hidden', ''); layerItemSettings.appendChild(extentsFieldset); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 4fe8ac128..477726af7 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -274,7 +274,6 @@ export var MapMLLayer = L.Layer.extend({ determineLayerProjection(); // requires that layer._properties.projection be set if (selectMatchingAlternateProjection()) return; - layer._styles = getAlternateStyles(); parseLicenseAndLegend(); setLayerTitle(); setZoomInOrOutLinks(); @@ -434,74 +433,6 @@ export var MapMLLayer = L.Layer.extend({ }); } } - function getAlternateStyles() { - var styleLinks = mapml.querySelectorAll( - 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' - ); - if (styleLinks.length > 1) { - var stylesControl = document.createElement('details'), - 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: { - src: e.target.getAttribute('data-href') - } - }) - ); - }; - - for (var j = 0; j < styleLinks.length; j++) { - var styleOption = document.createElement('div'), - styleOptionInput = styleOption.appendChild( - document.createElement('input') - ); - styleOptionInput.setAttribute('type', 'radio'); - styleOptionInput.setAttribute( - 'id', - 'rad-' + L.stamp(styleOptionInput) - ); - styleOptionInput.setAttribute( - 'name', - // grouping radio buttons based on parent layer's style - 'styles-' + L.stamp(stylesControl) - ); - styleOptionInput.setAttribute( - 'value', - styleLinks[j].getAttribute('title') - ); - styleOptionInput.setAttribute( - 'data-href', - new URL(styleLinks[j].getAttribute('href'), base).href - ); - var styleOptionLabel = styleOption.appendChild( - document.createElement('label') - ); - styleOptionLabel.setAttribute( - 'for', - 'rad-' + L.stamp(styleOptionInput) - ); - styleOptionLabel.innerText = styleLinks[j].getAttribute('title'); - if ( - styleLinks[j].getAttribute('rel') === 'style self' || - styleLinks[j].getAttribute('rel') === 'self style' - ) { - styleOptionInput.checked = true; - } - stylesControl.appendChild(styleOption); - L.DomUtil.addClass( - stylesControl, - 'mapml-layer-item-style mapml-control-layers' - ); - L.DomEvent.on(styleOptionInput, 'click', changeStyle, layer); - } - return stylesControl; - } - } function setLayerTitle() { if (mapml.querySelector('map-title')) { layer._title = mapml.querySelector('map-title').textContent.trim(); From ad8766bc7f6f8ceaa48b41a997fc1afb41003e89 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Mon, 27 Nov 2023 14:28:35 -0500 Subject: [PATCH 24/46] WIP on map-link really really WIP - working on getting layer control HTML to be part of defining whenReady() for layer... tbd, seems never created! --- src/layer.js | 7 ++++++- src/map-extent.js | 7 ++----- .../elementSupport/layers/createLayerControlForLayer.js | 6 ++++++ src/mapml/layers/MapMLLayer.js | 5 ----- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/layer.js b/src/layer.js index a0540bb65..2c51d8b1e 100644 --- a/src/layer.js +++ b/src/layer.js @@ -546,7 +546,11 @@ export class MapLayer extends HTMLElement { whenReady() { return new Promise((resolve, reject) => { let interval, failureTimer; - if (this._layer && (!this.src || this.shadowRoot?.childNodes.length)) { + if ( + this._layer && + this._layerControlHTML && + (!this.src || this.shadowRoot?.childNodes.length) + ) { resolve(); } else { let layerElement = this; @@ -556,6 +560,7 @@ export class MapLayer extends HTMLElement { function testForLayer(layerElement) { if ( layerElement._layer && + layerElement._layerControlHTML && (!layerElement.src || layerElement.shadowRoot?.childNodes.length) ) { clearInterval(interval); diff --git a/src/map-extent.js b/src/map-extent.js index 052e9120d..e318b64b0 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -144,7 +144,7 @@ export class MapExtent extends HTMLElement { // !this.isConnected <=> the disconnectedCallback has run before if (!this.isConnected) return; this._layer = this.parentLayer._layer; - this._map = this._layer._map; + this._map = this.mapEl._map; // reset the layer extent delete this.parentLayer.bounds; this._changeHandler = this._handleChange.bind(this); @@ -167,9 +167,6 @@ export class MapExtent extends HTMLElement { }); // this._layerControlHTML is the fieldset for the extent in the LayerControl this._layerControlHTML = this._createLayerControlExtentHTML(); - if (!this.hidden) - this._layer.addExtentToLayerControl(this._layerControlHTML); - this._validateLayerControlContainerHidden(); this._calculateBounds(); } getLayerControlHTML() { @@ -261,7 +258,7 @@ export class MapExtent extends HTMLElement { _handleChange() { // if the parent layer- is checked, add _extentLayer to map if map-extent is checked, otherwise remove it if (this.checked && this.parentLayer.checked && !this.disabled) { - this._extentLayer.addTo(this._layer._map); + this._extentLayer.addTo(this._map); this._extentLayer.setZIndex( Array.from(this.parentLayer.querySelectorAll('map-extent')).indexOf( this diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js index 840af9657..304808bcf 100644 --- a/src/mapml/elementSupport/layers/createLayerControlForLayer.js +++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js @@ -306,6 +306,12 @@ export var createLayerControlHTML = function () { this._styles = styles; extentsFieldset.setAttribute('aria-label', 'Sublayers'); extentsFieldset.setAttribute('hidden', ''); + let mapExtents = mapml.querySelectorAll('map-extent:not([hidden])'); + for (let i = 0; i < mapExtents.length; i++) { + extentsFieldset.appendChild(mapExtents[i].getLayerControlHTML()); + // if any map-extent is not hidden, the parent fieldset should not be hidden + extentsFieldset.removeAttribute('hidden'); + } layerItemSettings.appendChild(extentsFieldset); return this._layerControlHTML; }; diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 477726af7..ccfc662c1 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -252,11 +252,6 @@ export var MapMLLayer = L.Layer.extend({ this._href ).href; }, - addExtentToLayerControl: function (contents) { - this._layerEl._propertiesGroupAnatomy.appendChild(contents); - // remove hidden attribute, if it exists - this._layerEl._propertiesGroupAnatomy.removeAttribute('hidden'); - }, _initialize: function (content) { if (!this._href && !content) { return; From 8fbc1ac9d8a0bccab840e69708eec0df988bb280 Mon Sep 17 00:00:00 2001 From: yhy0217 Date: Mon, 27 Nov 2023 17:35:59 -0500 Subject: [PATCH 25/46] WIP to fix layer control --- src/layer.js | 3 +-- .../layers/createLayerControlForLayer.js | 13 ++++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/layer.js b/src/layer.js index 2c51d8b1e..53a4569e5 100644 --- a/src/layer.js +++ b/src/layer.js @@ -195,7 +195,7 @@ export class MapLayer extends HTMLElement { opacity: this.opacity }); } - return this.shadowRoot; + resolve(this.shadowRoot); }) .then((shadowRoot) => { let node = shadowRoot ? shadowRoot : this; @@ -212,7 +212,6 @@ export class MapLayer extends HTMLElement { this._createLayerControlHTML(); this._attachedToMap(); this._validateDisabled(); - resolve(); }) .catch((e) => { if (e.type === 'changeprojection') { diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js index 304808bcf..86446d791 100644 --- a/src/mapml/elementSupport/layers/createLayerControlForLayer.js +++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js @@ -1,4 +1,4 @@ -export var createLayerControlHTML = function () { +export var createLayerControlHTML = async function () { var fieldset = L.DomUtil.create('fieldset', 'mapml-layer-item'), input = L.DomUtil.create('input'), layerItemName = L.DomUtil.create('span', 'mapml-layer-item-name'), @@ -293,7 +293,9 @@ export var createLayerControlHTML = function () { let styles; if (styleLinks) { styles = this.getAlternateStyles(styleLinks); - layerItemSettings.appendChild(this.getAlternateStyles(styleLinks)); + if (styles) { + layerItemSettings.appendChild(style); + } } this._layerControlCheckbox = input; @@ -307,11 +309,16 @@ export var createLayerControlHTML = function () { extentsFieldset.setAttribute('aria-label', 'Sublayers'); extentsFieldset.setAttribute('hidden', ''); let mapExtents = mapml.querySelectorAll('map-extent:not([hidden])'); + let mapExtentLayerControls = []; for (let i = 0; i < mapExtents.length; i++) { - extentsFieldset.appendChild(mapExtents[i].getLayerControlHTML()); + mapExtentLayerControls.push(mapExtents[i].whenReady()); // if any map-extent is not hidden, the parent fieldset should not be hidden extentsFieldset.removeAttribute('hidden'); } + await Promise.all(mapExtentLayerControls); + for (let i = 0; i < mapExtents.length; i++) { + extentsFieldset.appendChild(mapExtents[i].getLayerControlHTML()); + } layerItemSettings.appendChild(extentsFieldset); return this._layerControlHTML; }; From 80d4bf923a8a8ec5377169987500888f27c8bd6c Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 28 Nov 2023 08:10:57 -0500 Subject: [PATCH 26/46] WIP on map-link really really WIP - layer.js: probably want to keep: return shadowRoot from then, not resolve it - map-link.js: use L.stamp on just-created div instead of layer, will make it more whenReady-independent, if it works - map-link.js bind changeStyle listener to layer element; use addEventListener instead of L.on - comment out _createLicenseLink() for now, reduce console noise - createLayerControlForLayer.js: correct typo that meant the styles fieldset was not available or being appended to with self style etc links --- src/layer.js | 3 ++- src/map-link.js | 6 +++--- .../elementSupport/layers/createLayerControlForLayer.js | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/layer.js b/src/layer.js index 53a4569e5..2c51d8b1e 100644 --- a/src/layer.js +++ b/src/layer.js @@ -195,7 +195,7 @@ export class MapLayer extends HTMLElement { opacity: this.opacity }); } - resolve(this.shadowRoot); + return this.shadowRoot; }) .then((shadowRoot) => { let node = shadowRoot ? shadowRoot : this; @@ -212,6 +212,7 @@ export class MapLayer extends HTMLElement { this._createLayerControlHTML(); this._attachedToMap(); this._validateDisabled(); + resolve(); }) .catch((e) => { if (e.type === 'changeprojection') { diff --git a/src/map-link.js b/src/map-link.js index 3dec306e6..f71c5a6c2 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -216,7 +216,7 @@ export class MapLink extends HTMLElement { this._createAlternateLink(); // add media attribute break; case 'license': - this._createLicenseLink(); + // this._createLicenseLink(); break; } // create the type of templated leaflet layer appropriate to the rel value @@ -695,7 +695,7 @@ export class MapLink extends HTMLElement { styleOptionInput.setAttribute( 'name', // grouping radio buttons based on parent layer's style - 'styles-' + L.stamp(stylesControl) + 'styles-' + L.stamp(styleOption) ); styleOptionInput.setAttribute('value', this.getAttribute('title')); styleOptionInput.setAttribute( @@ -711,7 +711,7 @@ export class MapLink extends HTMLElement { styleOptionInput.checked = true; } this._styleOption = styleOption; - L.DomEvent.on(styleOptionInput, 'click', changeStyle, layer); + styleOptionInput.addEventListener('click', changeStyle); } getLayerControlOption() { return this._styleOption; diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js index 86446d791..420f4f704 100644 --- a/src/mapml/elementSupport/layers/createLayerControlForLayer.js +++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js @@ -294,7 +294,7 @@ export var createLayerControlHTML = async function () { if (styleLinks) { styles = this.getAlternateStyles(styleLinks); if (styles) { - layerItemSettings.appendChild(style); + layerItemSettings.appendChild(styles); } } From a43cabe34972f6e1887b0482704f5adb5d597db4 Mon Sep 17 00:00:00 2001 From: prushfor Date: Tue, 28 Nov 2023 12:26:19 -0500 Subject: [PATCH 27/46] WIP on map-link - get map-link.getLayerControlOption() working for templated links - make layer-.connectedCallback wait on map-link.whenReady() - add layer-._layerControlHTML to whenReady criteria for readiness --- src/layer.js | 55 ++++++++++++++++++++++++++++--------------------- src/map-link.js | 2 +- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/layer.js b/src/layer.js index 2c51d8b1e..6bb5763be 100644 --- a/src/layer.js +++ b/src/layer.js @@ -181,6 +181,20 @@ export class MapLayer extends HTMLElement { opacity: this.opacity } ); + let node = this.shadowRoot ? this.shadowRoot : this; + let links = node.querySelectorAll( + 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' + ); + let linksReady = []; + for (let link of links) { + linksReady.push(link.whenReady()); + } + Promise.all(linksReady).then(() => { + this._createLayerControlHTML(); + this._attachedToMap(); + this._validateDisabled(); + resolve(); + }); }) .catch((error) => { this._fetchError = true; @@ -194,11 +208,7 @@ export class MapLayer extends HTMLElement { mapprojection: this.parentElement.projection, opacity: this.opacity }); - } - return this.shadowRoot; - }) - .then((shadowRoot) => { - let node = shadowRoot ? shadowRoot : this; + let node = this.shadowRoot ? this.shadowRoot : this; let links = node.querySelectorAll( 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' ); @@ -206,24 +216,23 @@ export class MapLayer extends HTMLElement { for (let link of links) { linksReady.push(link.whenReady()); } - return Promise.allSettled(linksReady); - }) - .then(() => { - this._createLayerControlHTML(); - this._attachedToMap(); - this._validateDisabled(); - resolve(); - }) - .catch((e) => { - if (e.type === 'changeprojection') { - this.src = e.detail.href; - } else { - console.log(e); - this.dispatchEvent( - new CustomEvent('error', { detail: { target: this } }) - ); - } - }); + Promise.all(linksReady).then(() => { + this._createLayerControlHTML(); + this._attachedToMap(); + this._validateDisabled(); + resolve(); + }); + } + }).catch((e) => { + if (e.type === 'changeprojection') { + this.src = e.detail.href; + } else { + console.log(e); + this.dispatchEvent( + new CustomEvent('error', { detail: { target: this } }) + ); + } + }); } _attachedToMap() { // set i to the position of this layer element in the set of layers diff --git a/src/map-link.js b/src/map-link.js index f71c5a6c2..ce39433c1 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -111,7 +111,7 @@ export class MapLink extends HTMLElement { } getMapEl() { return this.getRootNode() instanceof ShadowRoot - ? this.getRootNode().host + ? this.getRootNode().host.closest('mapml-viewer,map[is=web-map]') : this.closest('mapml-viewer,map[is=web-map]'); } getLayerEl() { From 14b1bb0e5d8496eb74290298127808441f71f26e Mon Sep 17 00:00:00 2001 From: prushfor Date: Tue, 28 Nov 2023 17:29:25 -0500 Subject: [PATCH 28/46] WIP on map-link - add extentsReady as a criteria for layer initialization to proceed. the problem encountered is that the `checked` attr. change callback does a layer-.whenReady() which happens much earlier and doesn't take into account the extents' readiness, so some properties of _extentLayer aren't set up when the layer-._layer is added to the map, resulting in console errors. - the MapMLLayer.copyContentToShadowRoot method was setting the href attribute of map-link elements erroneously. Remove that code, replaced the map-link.href getter with a better approach. - remove code from MapMLLayer that sets up the zoomin/zoomout links, move it to map-link href getter, essentially. - change MapMLLayer._validateLayerZoom() to use map-link, and not _properties.zoomin / zoomout - comment out map-link._createZoominOrZoomoutLink() since it isn't necessary (perhaps), being handled by the href getter. --- src/layer.js | 35 +++++++++----- src/map-link.js | 8 +++- src/mapml/layers/MapMLLayer.js | 86 ++++++---------------------------- 3 files changed, 44 insertions(+), 85 deletions(-) diff --git a/src/layer.js b/src/layer.js index 6bb5763be..5887654d8 100644 --- a/src/layer.js +++ b/src/layer.js @@ -191,9 +191,17 @@ export class MapLayer extends HTMLElement { } Promise.all(linksReady).then(() => { this._createLayerControlHTML(); - this._attachedToMap(); - this._validateDisabled(); - resolve(); + + let node = this.shadowRoot; + let extentsReady = []; + for (let extent of node.querySelectorAll('map-extent')) { + extentsReady.push(extent.whenReady()); + } + Promise.allSettled(extentsReady).then(() => { + this._attachedToMap(); + this._validateDisabled(); + resolve(); + }); }); }) .catch((error) => { @@ -208,19 +216,24 @@ export class MapLayer extends HTMLElement { mapprojection: this.parentElement.projection, opacity: this.opacity }); - let node = this.shadowRoot ? this.shadowRoot : this; - let links = node.querySelectorAll( - 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' - ); let linksReady = []; - for (let link of links) { + for (let link of this.querySelectorAll( + 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' + )) { linksReady.push(link.whenReady()); } Promise.all(linksReady).then(() => { this._createLayerControlHTML(); - this._attachedToMap(); - this._validateDisabled(); - resolve(); + + let extentsReady = []; + for (let extent of this.querySelectorAll('map-extent')) { + extentsReady.push(extent.whenReady()); + } + Promise.allSettled(extentsReady).then(() => { + this._attachedToMap(); + this._validateDisabled(); + resolve(); + }); }); } }).catch((e) => { diff --git a/src/map-link.js b/src/map-link.js index ce39433c1..147b2cd09 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -57,7 +57,11 @@ export class MapLink extends HTMLElement { // } // } get href() { - return this.getAttribute('href'); + if (this.hasAttribute('href')) { + return new URL(this.getAttribute('href'), this.getBase()).href; + } else if (this.hasAttribute('tref')) { + return this.resolve(); + } } set href(val) { // improve this @@ -204,7 +208,7 @@ export class MapLink extends HTMLElement { break; case 'zoomin': case 'zoomout': - this._createZoominOrZoomoutLink(); + // this._createZoominOrZoomoutLink(); break; case 'legend': this._createLegendLink(); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index ccfc662c1..6ff683d4f 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -179,48 +179,23 @@ export var MapMLLayer = L.Layer.extend({ return { zoomanim: this._validateLayerZoom }; }, _validateLayerZoom: function (e) { - // this callback will be invoked AFTER has been removed - // but due to the characteristic of JavaScript, the context (this pointer) can still be used - // the easiest way to solve this: - if (!this._map) { - return; - } // get the min and max zooms from all extents - const layerEl = this._layerEl, - // prerequisite: no inline and remote mapml elements exists at the same time - mapExtents = layerEl.shadowRoot - ? layerEl.shadowRoot.querySelectorAll('map-extent') - : layerEl.querySelectorAll('map-extent'); - var toZoom = e.zoom, - zoom = - mapExtents.length > 0 - ? mapExtents[0].querySelector('map-input[type=zoom]') - : null, - min = - zoom && zoom.hasAttribute('min') - ? parseInt(zoom.getAttribute('min')) - : this._map.getMinZoom(), - max = - zoom && zoom.hasAttribute('max') - ? parseInt(zoom.getAttribute('max')) - : this._map.getMaxZoom(); - if (zoom) { - for (let i = 1; i < mapExtents.length; i++) { - zoom = mapExtents[i].querySelector('map-input[type=zoom]'); - if (zoom && zoom.hasAttribute('min')) { - min = Math.min(parseInt(zoom.getAttribute('min')), min); - } - if (zoom && zoom.hasAttribute('max')) { - max = Math.max(parseInt(zoom.getAttribute('max')), max); - } - } - } + const layerEl = this._layerEl; + let toZoom = e.zoom; + let min = layerEl.extent.zoom.minZoom; + let max = layerEl.extent.zoom.maxZoom; + let inLink = layerEl.shadowRoot + ? layerEl.shadowRoot.querySelector('map-link[rel=zoomin]') + : layerEl.querySelector('map-link[rel=zoomin]'), + outLink = layerEl.shadowRoot + ? layerEl.shadowRoot.querySelector('map-link[rel=zoomout]') + : layerEl.querySelector('map-link[rel=zoomout]'); let targetURL; if (!(min <= toZoom && toZoom <= max)) { - if (this._properties.zoomin && toZoom > max) { - targetURL = this._properties.zoomin; - } else if (this._properties.zoomout && toZoom < min) { - targetURL = this._properties.zoomout; + if (inLink && toZoom > max) { + targetURL = inLink.href; + } else if (outLink && toZoom < min) { + targetURL = outLink.href; } if (targetURL) { this._layerEl.dispatchEvent( @@ -271,7 +246,6 @@ export var MapMLLayer = L.Layer.extend({ if (selectMatchingAlternateProjection()) return; parseLicenseAndLegend(); setLayerTitle(); - setZoomInOrOutLinks(); // crs is only set if the layer has the same projection as the map if (layer._properties.crs) processTiles(); processFeatures(); @@ -354,22 +328,6 @@ export var MapMLLayer = L.Layer.extend({ } catch (error) {} return false; } - function setZoomInOrOutLinks() { - var zoomin = mapml.querySelector('map-link[rel=zoomin]'), - zoomout = mapml.querySelector('map-link[rel=zoomout]'); - if (zoomin) { - layer._properties.zoomin = new URL( - zoomin.getAttribute('href'), - base - ).href; - } - if (zoomout) { - layer._properties.zoomout = new URL( - zoomout.getAttribute('href'), - base - ).href; - } - } function processFeatures() { let native = M.getNativeVariables(layer._content); layer._mapmlvectors = M.featureLayer(null, { @@ -457,22 +415,6 @@ export var MapMLLayer = L.Layer.extend({ // which makes the this._content (mapml file) changed and thus affects the later generation process of this._mapmlvectors let node = el.cloneNode(true); el._DOMnode = node; - // resolve relative url - if (node.nodeName === 'map-link') { - let url = node.getAttribute('href') || node.getAttribute('tref'); - // if relative - if ( - url && - (url.indexOf('http://') === 0 || url.indexOf('https://') === 0) - ) { - let resolvedURL = baseURL + url; - if (node.hasAttribute('href')) { - node.setAttribute('href', resolvedURL); - } else { - node.setAttribute('tref', resolvedURL); - } - } - } shadowRoot.appendChild(node); } } From 379f43cc33696201a2033920ef3154618fee603a Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 29 Nov 2023 13:10:57 -0500 Subject: [PATCH 29/46] WIP on map-link really really WIP - move copyRemoteContentToShadowRoot from MapMLLayer to layer.js - remove parentLayer.whenReady() from map-extent.connectedCallback - add parentLayer.whenReady() to map-extent.attributeChangedCallback for `checked` and `hidden` - remove _leafletLayer option from construction of TemplatedLayer - move access to _layer pane from TemplatedLayer initialize option to onAdd - eliminate use of map-extent._DOMnode by map-extent and TemplatedLayer, use only map-extent (this) element itself - refactor map-extent._projectionMatch() to use the mapEl._map.options.projection value instead of that value passed via the MapMLLayer.options.mapprojection - update MapMLLayer.selectMatchingAlternateProjection() to use the version of mapml content that is saved to shadowRoot, which is now sort of unstructured, similar to the way it's expected to be as local content of the element. --- src/layer.js | 100 ++++++++++++++--------------- src/map-extent.js | 83 ++++++++++++++---------- src/map-link.js | 2 +- src/mapml/layers/MapMLLayer.js | 3 +- src/mapml/layers/TemplatedLayer.js | 3 +- 5 files changed, 100 insertions(+), 91 deletions(-) diff --git a/src/layer.js b/src/layer.js index 5887654d8..c13a4e53d 100644 --- a/src/layer.js +++ b/src/layer.js @@ -169,39 +169,30 @@ export class MapLayer extends HTMLElement { ) { throw new Error('Parser error'); } - if (this._layer) { - this._onRemove(); + this.copyRemoteContentToShadowRoot(content.querySelector('mapml-')); + let elements = this.shadowRoot.querySelectorAll('*'); + let elementsReady = []; + for (let i = 0; i < elements.length; i++) { + if (elements[i].whenReady) + elementsReady.push(elements[i].whenReady()); } - this._layer = M.mapMLLayer( - new URL(this.src, base).href, - this, - content, - { - mapprojection: this.parentElement.projection, - opacity: this.opacity + Promise.allSettled(elementsReady).then(() => { + if (this._layer) { + this._onRemove(); } - ); - let node = this.shadowRoot ? this.shadowRoot : this; - let links = node.querySelectorAll( - 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' - ); - let linksReady = []; - for (let link of links) { - linksReady.push(link.whenReady()); - } - Promise.all(linksReady).then(() => { + this._layer = M.mapMLLayer( + new URL(this.src, base).href, + this, + this.shadowRoot, + { + mapprojection: this.parentElement.projection, + opacity: this.opacity + } + ); this._createLayerControlHTML(); - - let node = this.shadowRoot; - let extentsReady = []; - for (let extent of node.querySelectorAll('map-extent')) { - extentsReady.push(extent.whenReady()); - } - Promise.allSettled(extentsReady).then(() => { - this._attachedToMap(); - this._validateDisabled(); - resolve(); - }); + this._attachedToMap(); + this._validateDisabled(); + resolve(); }); }) .catch((error) => { @@ -209,31 +200,24 @@ export class MapLayer extends HTMLElement { 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: this.opacity - }); - let linksReady = []; - for (let link of this.querySelectorAll( - 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' - )) { - linksReady.push(link.whenReady()); + let elements = this.querySelectorAll('*'); + let elementsReady = []; + for (let i = 0; i < elements.length; i++) { + if (elements[i].whenReady) + elementsReady.push(elements[i].whenReady()); } - Promise.all(linksReady).then(() => { - this._createLayerControlHTML(); - - let extentsReady = []; - for (let extent of this.querySelectorAll('map-extent')) { - extentsReady.push(extent.whenReady()); + Promise.allSettled(elementsReady).then(() => { + if (this._layer) { + this._onRemove(); } - Promise.allSettled(extentsReady).then(() => { - this._attachedToMap(); - this._validateDisabled(); - resolve(); + this._layer = M.mapMLLayer(null, this, null, { + mapprojection: this.parentElement.projection, + opacity: this.opacity }); + this._createLayerControlHTML(); + this._attachedToMap(); + this._validateDisabled(); + resolve(); }); } }).catch((e) => { @@ -247,6 +231,18 @@ export class MapLayer extends HTMLElement { } }); } + copyRemoteContentToShadowRoot(mapml) { + let shadowRoot = this.shadowRoot; + // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot + let head = mapml.querySelector('map-head'); + while (head.firstChild) { + shadowRoot.appendChild(head.firstChild); + } + let body = mapml.querySelector('map-body'); + while (body.firstChild) { + shadowRoot.appendChild(body.firstChild); + } + } _attachedToMap() { // set i to the position of this layer element in the set of layers var i = 0, diff --git a/src/map-extent.js b/src/map-extent.js index e318b64b0..a26a07e7a 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -65,9 +65,18 @@ export class MapExtent extends HTMLElement { } break; case 'checked': - this._handleChange(); - this._calculateBounds(); - this._layerControlCheckbox.checked = newValue !== null; + this.parentLayer + .whenReady() + .then(() => { + this._handleChange(); + this._calculateBounds(); + this._layerControlCheckbox.checked = newValue !== null; + }) + .catch(() => { + console.log( + 'Error while waiting on parentLayer for map-extent checked callback' + ); + }); break; case 'opacity': if (oldValue !== newValue) { @@ -77,32 +86,41 @@ export class MapExtent extends HTMLElement { break; case 'hidden': if (oldValue !== newValue) { - let extentsRootFieldset = - this.parentLayer._propertiesGroupAnatomy; - let position = Array.from( - this.parentNode.querySelectorAll('map-extent:not([hidden])') - ).indexOf(this); - if (newValue !== null) { - // remove from layer control (hide from user) - this._layerControlHTML.remove(); - } else { - // insert the extent fieldset into the layer control container in - // the calculated position - if (position === 0) { - extentsRootFieldset.insertAdjacentElement( - 'afterbegin', - this._layerControlHTML + this.parentLayer + .whenReady() + .then(() => { + let extentsRootFieldset = + this.parentLayer._propertiesGroupAnatomy; + let position = Array.from( + this.parentNode.querySelectorAll('map-extent:not([hidden])') + ).indexOf(this); + if (newValue !== null) { + // remove from layer control (hide from user) + this._layerControlHTML.remove(); + } else { + // insert the extent fieldset into the layer control container in + // the calculated position + if (position === 0) { + extentsRootFieldset.insertAdjacentElement( + 'afterbegin', + this._layerControlHTML + ); + } else if (position > 0) { + this.parentNode + .querySelectorAll('map-extent:not([hidden])') + [position - 1]._layerControlHTML.insertAdjacentElement( + 'afterend', + this._layerControlHTML + ); + } + } + this._validateLayerControlContainerHidden(); + }) + .catch(() => { + console.log( + 'Error while waiting on parentLayer for map-extent hidden callback' ); - } else if (position > 0) { - this.parentNode - .querySelectorAll('map-extent:not([hidden])') - [position - 1]._layerControlHTML.insertAdjacentElement( - 'afterend', - this._layerControlHTML - ); - } - } - this._validateLayerControlContainerHidden(); + }); } break; } @@ -132,7 +150,6 @@ export class MapExtent extends HTMLElement { this.parentLayer.hasAttribute('data-moving') ) return; - await this.parentLayer.whenReady(); this.mapEl = this.parentLayer.closest('mapml-viewer,map[is=web-map]'); await this.mapEl.whenProjectionDefined(this.units).catch(() => { throw new Error('Undefined projection:' + this.units); @@ -143,7 +160,6 @@ export class MapExtent extends HTMLElement { // in this case, the microtasks triggered by the fulfillment of the removed MapMLLayer should be stopped as well // !this.isConnected <=> the disconnectedCallback has run before if (!this.isConnected) return; - this._layer = this.parentLayer._layer; this._map = this.mapEl._map; // reset the layer extent delete this.parentLayer.bounds; @@ -154,16 +170,14 @@ export class MapExtent extends HTMLElement { // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 this._opacity = this.opacity || 1.0; this._extentLayer = M.templatedLayer({ - pane: this._layer._container, opacity: this.opacity, - _leafletLayer: this._layer, crs: M[this.units], extentZIndex: Array.from( this.parentLayer.querySelectorAll('map-extent') ).indexOf(this), // when a migrates from a remote mapml file and attaches to the shadow of // this._properties._mapExtents[i] refers to the in remote mapml - extentEl: this._DOMnode || this + extentEl: this }); // this._layerControlHTML is the fieldset for the extent in the LayerControl this._layerControlHTML = this._createLayerControlExtentHTML(); @@ -174,8 +188,7 @@ export class MapExtent extends HTMLElement { } _projectionMatch() { return ( - this.units.toUpperCase() === - this._layer.options.mapprojection.toUpperCase() + this.units.toUpperCase() === this._map.options.projection.toUpperCase() ); } _validateDisabled() { diff --git a/src/map-link.js b/src/map-link.js index 147b2cd09..38c688b17 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -217,7 +217,7 @@ export class MapLink extends HTMLElement { this._createStylesheetLink(); break; case 'alternate': - this._createAlternateLink(); // add media attribute + // this._createAlternateLink(); // add media attribute break; case 'license': // this._createLicenseLink(); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 6ff683d4f..95796f167 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -250,7 +250,6 @@ export var MapMLLayer = L.Layer.extend({ if (layer._properties.crs) processTiles(); processFeatures(); M._parseStylesheetAsHTML(mapml, base, layer._container); - 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 @@ -299,7 +298,7 @@ export var MapMLLayer = L.Layer.extend({ let selectedAlternate = layer._properties.projection !== layer.options.mapprojection && mapml.querySelector( - 'map-head map-link[rel=alternate][projection=' + + 'map-link[rel=alternate][projection=' + layer.options.mapprojection + '][href]' ); diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index da20fc398..13d0b0384 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -24,7 +24,8 @@ export var TemplatedLayer = L.LayerGroup.extend({ onAdd: function (map) { L.LayerGroup.prototype.onAdd.call(this, map); // add to this.options.pane - this.options.pane.appendChild(this._container); + let pane = this.options.extentEl.parentLayer._layer._container; + pane.appendChild(this._container); }, redraw: function () { this.eachLayer(function (layer) { From 6323475e49bf1ab6fc2ba1cf368945736ad1bc22 Mon Sep 17 00:00:00 2001 From: Aliyan Haq <55751566+AliyanH@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:37:55 -0500 Subject: [PATCH 30/46] map-link WIP move attributeChangedCallback select matching alternate projection in fetching of layer.js modify copyRemoteContentToShadowRoot to use documentFragment, to move all elements to shadowRoot at once add getBase + selectMatchingAlternateProjection + getProjection method for layer.js map-input, remove dependency from _layer map-link - add temporary _createAlternateLink(), not being USED QueryHandler.js - replace layer._properties with alternate MapMLLayer - move out getProjection, selectMatchingAlternateProjection(), determineLayerProjection() to layer.js MapMLLayer - get rid of properties object --- cbmtile-cbmt.mapml | 17 ++ index.html | 14 +- msi-cbmtile.mapml | 45 +++++ msi-osmtile.mapml | 47 +++++ msi-tiles-osmtile.mapml | 44 +++++ msi-tiles.mapml | 44 +++++ src/layer.js | 266 ++++++++++++++++++----------- src/map-extent.js | 2 - src/map-input.js | 11 +- src/map-link.js | 27 ++- src/mapml/handlers/QueryHandler.js | 2 +- src/mapml/layers/MapMLLayer.js | 91 +--------- 12 files changed, 412 insertions(+), 198 deletions(-) create mode 100644 cbmtile-cbmt.mapml create mode 100644 msi-cbmtile.mapml create mode 100644 msi-osmtile.mapml create mode 100644 msi-tiles-osmtile.mapml create mode 100644 msi-tiles.mapml diff --git a/cbmtile-cbmt.mapml b/cbmtile-cbmt.mapml new file mode 100644 index 000000000..1f751d7c4 --- /dev/null +++ b/cbmtile-cbmt.mapml @@ -0,0 +1,17 @@ + + + Canada Base Map - Transportation (CBMT) + + + + + + + + + diff --git a/index.html b/index.html index a3ddb3fbc..bcc375e7b 100644 --- a/index.html +++ b/index.html @@ -73,16 +73,10 @@ - - - - - - - - - - + + + + diff --git a/msi-cbmtile.mapml b/msi-cbmtile.mapml new file mode 100644 index 000000000..78480944a --- /dev/null +++ b/msi-cbmtile.mapml @@ -0,0 +1,45 @@ + + + Mean Sea Ice WMS GetMap + + + + + + + + + + + + + + + + + 2000-08-15T00:00:00Z + 2001-08-15T00:00:00Z + 2002-08-15T00:00:00Z + 2003-08-15T00:00:00Z + 2004-08-15T00:00:00Z + 2005-08-15T00:00:00Z + 2006-08-15T00:00:00Z + 2007-08-15T00:00:00Z + 2008-08-15T00:00:00Z + 2009-08-15T00:00:00Z + 2010-08-15T00:00:00Z + 2011-08-15T00:00:00Z + 2012-08-15T00:00:00Z + 2013-08-15T00:00:00Z + 2014-08-15T00:00:00Z + 2015-08-15T00:00:00Z + 2016-08-15T00:00:00Z + 2017-08-15T00:00:00Z + 2018-08-15T00:00:00Z + 2019-08-15T00:00:00Z + 2020-08-15T00:00:00Z + + + + + diff --git a/msi-osmtile.mapml b/msi-osmtile.mapml new file mode 100644 index 000000000..f6f59cf43 --- /dev/null +++ b/msi-osmtile.mapml @@ -0,0 +1,47 @@ + + + Mean Sea Ice WMS GetMap + + + + + + + + + + + + + + + + + + 2000-08-15T00:00:00Z + 2001-08-15T00:00:00Z + 2002-08-15T00:00:00Z + 2003-08-15T00:00:00Z + 2004-08-15T00:00:00Z + 2005-08-15T00:00:00Z + 2006-08-15T00:00:00Z + 2007-08-15T00:00:00Z + 2008-08-15T00:00:00Z + 2009-08-15T00:00:00Z + 2010-08-15T00:00:00Z + 2011-08-15T00:00:00Z + 2012-08-15T00:00:00Z + 2013-08-15T00:00:00Z + 2014-08-15T00:00:00Z + 2015-08-15T00:00:00Z + 2016-08-15T00:00:00Z + 2017-08-15T00:00:00Z + 2018-08-15T00:00:00Z + 2019-08-15T00:00:00Z + 2020-08-15T00:00:00Z + + + + + + diff --git a/msi-tiles-osmtile.mapml b/msi-tiles-osmtile.mapml new file mode 100644 index 000000000..d0ecf4cca --- /dev/null +++ b/msi-tiles-osmtile.mapml @@ -0,0 +1,44 @@ + + + Mean Sea Ice - Tiled WMS GetMap + + + + + + + + + + + + + + + + 2000-08-15T00:00:00Z + 2001-08-15T00:00:00Z + 2002-08-15T00:00:00Z + 2003-08-15T00:00:00Z + 2004-08-15T00:00:00Z + 2005-08-15T00:00:00Z + 2006-08-15T00:00:00Z + 2007-08-15T00:00:00Z + 2008-08-15T00:00:00Z + 2009-08-15T00:00:00Z + 2010-08-15T00:00:00Z + 2011-08-15T00:00:00Z + 2012-08-15T00:00:00Z + 2013-08-15T00:00:00Z + 2014-08-15T00:00:00Z + 2015-08-15T00:00:00Z + 2016-08-15T00:00:00Z + 2017-08-15T00:00:00Z + 2018-08-15T00:00:00Z + 2019-08-15T00:00:00Z + 2020-08-15T00:00:00Z + + + + + diff --git a/msi-tiles.mapml b/msi-tiles.mapml new file mode 100644 index 000000000..0401ba877 --- /dev/null +++ b/msi-tiles.mapml @@ -0,0 +1,44 @@ + + + Mean Sea Ice - Tiled WMS GetMap + + + + + + + + + + + + + + + + 2000-08-15T00:00:00Z + 2001-08-15T00:00:00Z + 2002-08-15T00:00:00Z + 2003-08-15T00:00:00Z + 2004-08-15T00:00:00Z + 2005-08-15T00:00:00Z + 2006-08-15T00:00:00Z + 2007-08-15T00:00:00Z + 2008-08-15T00:00:00Z + 2009-08-15T00:00:00Z + 2010-08-15T00:00:00Z + 2011-08-15T00:00:00Z + 2012-08-15T00:00:00Z + 2013-08-15T00:00:00Z + 2014-08-15T00:00:00Z + 2015-08-15T00:00:00Z + 2016-08-15T00:00:00Z + 2017-08-15T00:00:00Z + 2018-08-15T00:00:00Z + 2019-08-15T00:00:00Z + 2020-08-15T00:00:00Z + + + + + \ No newline at end of file diff --git a/src/layer.js b/src/layer.js index c13a4e53d..ca289c31a 100644 --- a/src/layer.js +++ b/src/layer.js @@ -65,12 +65,71 @@ export class MapLayer extends HTMLElement { return Object.assign( M._convertAndFormatPCRS( this._layer.bounds, - this._layer._properties.crs, - this._layer._properties.projection + M[this.getProjection()], + this.getProjection() ), { zoom: this._layer.zoomBounds } ); } + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'label': + this.whenReady() + .then(() => { + this._layer.setName(newValue); + }) + .catch((e) => { + console.log(e); + }); + break; + case 'checked': + this.whenReady() + .then(() => { + if (typeof newValue === 'string') { + this.parentElement._map.addLayer(this._layer); + } else { + this.parentElement._map.removeLayer(this._layer); + } + this._layerControlCheckbox.checked = this.checked; + this.dispatchEvent(new CustomEvent('map-change')); + }) + .catch((e) => { + console.log(e); + }); + break; + case 'hidden': + var map = this.parentElement && this.parentElement._map; + if (map && this.parentElement.controls) { + if (typeof newValue === 'string') { + if (this._layer) { + this.parentElement._layerControl.removeLayer(this._layer); + } + } else { + this._layerControl = this.parentElement._layerControl; + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + this._validateDisabled(); + } + } + break; + case 'opacity': + if (oldValue !== newValue && this._layer) { + this._opacity = newValue; + this._layer.changeOpacity(newValue); + } + break; + case 'src': + if (oldValue && oldValue !== newValue) { + if (this.isConnected) { + this.removeAttribute('disabled'); + let map = this.parentElement; + let placeholder = document.createElement('span'); + this.insertAdjacentElement('beforebegin', placeholder); + map.removeChild(this); + placeholder.replaceWith(this); + } + } + } + } constructor() { // Always call super first in constructor @@ -141,16 +200,7 @@ export class MapLayer extends HTMLElement { }, { once: true } ); - this.addEventListener( - 'changeprojection', - function (e) { - e.stopPropagation(); - reject(e); - }, - { once: true } - ); let base = this.baseURI ? this.baseURI : document.baseURI; - const headers = new Headers(); headers.append('Accept', 'text/mapml'); if (this.src) { @@ -169,35 +219,44 @@ export class MapLayer extends HTMLElement { ) { throw new Error('Parser error'); } + let alternate = this.selectMatchingAlternateProjection( + content.querySelector('mapml-') + ); + if (alternate) { + throw new Error('changeprojection', { + cause: { href: alternate } + }); + } this.copyRemoteContentToShadowRoot(content.querySelector('mapml-')); + }) + .then(() => { let elements = this.shadowRoot.querySelectorAll('*'); let elementsReady = []; for (let i = 0; i < elements.length; i++) { if (elements[i].whenReady) elementsReady.push(elements[i].whenReady()); } - Promise.allSettled(elementsReady).then(() => { - if (this._layer) { - this._onRemove(); + return Promise.allSettled(elementsReady); + }) + .then(() => { + this._layer = M.mapMLLayer( + new URL(this.src, base).href, + this, + this.shadowRoot, + { + projection: this.getProjection(), + opacity: this.opacity } - this._layer = M.mapMLLayer( - new URL(this.src, base).href, - this, - this.shadowRoot, - { - mapprojection: this.parentElement.projection, - opacity: this.opacity - } - ); - this._createLayerControlHTML(); - this._attachedToMap(); - this._validateDisabled(); - resolve(); - }); + ); + this._createLayerControlHTML(); + this._attachedToMap(); + this._validateDisabled(); + resolve(); }) .catch((error) => { this._fetchError = true; - console.log('Error fetching layer content' + error); + console.log('Error fetching layer content: ' + error); + reject(error); }); } else { let elements = this.querySelectorAll('*'); @@ -211,7 +270,7 @@ export class MapLayer extends HTMLElement { this._onRemove(); } this._layer = M.mapMLLayer(null, this, null, { - mapprojection: this.parentElement.projection, + projection: this.getProjection(), opacity: this.opacity }); this._createLayerControlHTML(); @@ -221,8 +280,9 @@ export class MapLayer extends HTMLElement { }); } }).catch((e) => { - if (e.type === 'changeprojection') { - this.src = e.detail.href; + if (e.message === 'changeprojection') { + console.log('Changing layer src to: ' + e.cause.href); + this.src = e.cause.href; } else { console.log(e); this.dispatchEvent( @@ -231,17 +291,90 @@ export class MapLayer extends HTMLElement { } }); } + + getBase() { + let layer = this.getRootNode().host; + // + let relativeURL = + this.getRootNode().querySelector('map-base') && + this.getRootNode() instanceof ShadowRoot + ? this.getRootNode().querySelector('map-base').getAttribute('href') + : /* local content? */ !(this.getRootNode() instanceof ShadowRoot) + ? /* use the baseURI algorithm which takes into account any */ + this.getRootNode().querySelector('map-base')?.getAttribute('href') || + this.baseURI + : /* else use the resolved value */ new URL( + layer.src, + layer.baseURI + ).href; + + // when remote content, use layer.src as base else use baseURI of map-link + let baseURL = + this.getRootNode() instanceof ShadowRoot + ? new URL(layer.src, layer.baseURI).href + : this.baseURI; + return new URL(relativeURL, baseURL).href; + } + + selectMatchingAlternateProjection(mapml) { + let selectedAlternate = + this.getProjection(mapml) !== this.parentElement.projection && + mapml.querySelector( + 'map-link[rel=alternate][projection=' + + this.parentElement.projection + + '][href]' + ); + + if (selectedAlternate) { + let url = new URL(selectedAlternate.getAttribute('href'), this.getBase()) + .href; + return url; + } + return false; + } + copyRemoteContentToShadowRoot(mapml) { let shadowRoot = this.shadowRoot; // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot - let head = mapml.querySelector('map-head'); - while (head.firstChild) { - shadowRoot.appendChild(head.firstChild); + let frag = document.createDocumentFragment(); + let elements = mapml.querySelectorAll('map-head > *, map-body > *'); + for (let i = 0; i < elements.length; i++) { + frag.appendChild(elements[i]); } - let body = mapml.querySelector('map-body'); - while (body.firstChild) { - shadowRoot.appendChild(body.firstChild); + shadowRoot.appendChild(frag); + } + getProjection(content) { + let mapml = content || this; + let projection = this.parentElement.projection; + mapml = mapml.shadowRoot ? mapml.shadowRoot : mapml; + if (mapml.querySelector('map-meta[name=projection][content]')) { + projection = + M._metaContentToObject( + mapml + .querySelector('map-meta[name=projection]') + .getAttribute('content') + ).content || projection; + } else if (mapml.querySelector('map-extent[units]')) { + const getProjectionFrom = (extents) => { + 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 { + console.log( + `A projection was not assigned to the '${mapml.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/` + ); } + return projection; } _attachedToMap() { // set i to the position of this layer element in the set of layers @@ -306,63 +439,6 @@ export class MapLayer extends HTMLElement { ); } - attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'label': - this.whenReady() - .then(() => { - this._layer.setName(newValue); - }) - .catch((e) => { - console.log(e); - }); - break; - case 'checked': - this.whenReady() - .then(() => { - if (typeof newValue === 'string') { - this.parentElement._map.addLayer(this._layer); - } else { - this.parentElement._map.removeLayer(this._layer); - } - this._layerControlCheckbox.checked = this.checked; - this.dispatchEvent(new CustomEvent('map-change')); - }) - .catch((e) => { - console.log(e); - }); - break; - case 'hidden': - var map = this.parentElement && this.parentElement._map; - if (map && this.parentElement.controls) { - if (typeof newValue === 'string') { - if (this._layer) { - this.parentElement._layerControl.removeLayer(this._layer); - } - } else { - this._layerControl = this.parentElement._layerControl; - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - this._validateDisabled(); - } - } - break; - case 'opacity': - if (oldValue !== newValue && this._layer) { - this._opacity = newValue; - this._layer.changeOpacity(newValue); - } - break; - case 'src': - if (oldValue !== newValue && this._layer) { - this._onRemove(); - if (this.isConnected) { - this._onAdd(); - } - // the original inline content will not be removed - // but has NO EFFECT and works as a fallback - } - } - } _validateDisabled() { // setTimeout is necessary to make the validateDisabled happen later than the moveend operations etc., // to ensure that the validated result is correct diff --git a/src/map-extent.js b/src/map-extent.js index a26a07e7a..3cc2b7ecd 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -175,8 +175,6 @@ export class MapExtent extends HTMLElement { extentZIndex: Array.from( this.parentLayer.querySelectorAll('map-extent') ).indexOf(this), - // when a migrates from a remote mapml file and attaches to the shadow of - // this._properties._mapExtents[i] refers to the in remote mapml extentEl: this }); // this._layerControlHTML is the fieldset for the extent in the LayerControl diff --git a/src/map-input.js b/src/map-input.js index 6c91dfedd..5c985be0a 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -133,16 +133,16 @@ export class MapInput extends HTMLElement { } if (this.getAttribute('max')) { return this.getAttribute('max'); - } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { + } else if (this.getLayerEl().querySelector('map-meta[name=zoom]')) { // fallback map-meta on layer return M._metaContentToObject( - this._layer._layerEl + this.getLayerEl() .querySelector('map-meta[name=zoom]') .getAttribute('content') ).max; } else { // fallback map max - return this._layer._layerEl.extent.zoom.maxZoom.toString(); + return this.getLayerEl().extent.zoom.maxZoom.toString(); } } set max(val) { @@ -162,6 +162,11 @@ export class MapInput extends HTMLElement { this.setAttribute('step', val); } } + getLayerEl() { + return this.getRootNode() instanceof ShadowRoot + ? this.getRootNode().host + : this.closest('layer-'); + } attributeChangedCallback(name, oldValue, newValue) { this.whenReady() .then(() => { diff --git a/src/map-link.js b/src/map-link.js index 38c688b17..27474c6e2 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -211,13 +211,13 @@ export class MapLink extends HTMLElement { // this._createZoominOrZoomoutLink(); break; case 'legend': - this._createLegendLink(); + //this._createLegendLink(); break; case 'stylesheet': this._createStylesheetLink(); break; case 'alternate': - // this._createAlternateLink(); // add media attribute + //this._createAlternateLink(); // add media attribute break; case 'license': // this._createLicenseLink(); @@ -229,6 +229,29 @@ export class MapLink extends HTMLElement { // add to viewer._map dependant on map-extent.checked, layer-.checked // what else? } + _createAlternateLink() { + let selectedAlternate = + this.getLayerEl().getProjection() !== this.getMapEl().projection && + this.projection === this.getMapEl().projection && + this.href; + if (selectedAlternate) { + let url = new URL(this.href, this.getBase()).href; + this.getLayerEl().dispatchEvent( + new CustomEvent('changeprojection', { + detail: { + href: url + } + }) + ); + //if this is the only layer, but the projection doesn't match, + // set the map's projection to that of the layer + } else if ( + this.getLayerEl().getProjection() !== this.getMapEl().projection && + this.getMapEl().layers.length === 1 + ) { + this.getMapEl().projection = this.getLayerEl().getProjection(); + } + } async _createTemplatedLink() { // conditions check diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index c2a654282..d9f2b45e6 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -49,7 +49,7 @@ export var QueryHandler = L.Handler.extend({ _query(e, layer) { var zoom = e.target.getZoom(), map = this._map, - crs = layer._properties.crs, // the crs for each extent would be the same + crs = M[layer.options.projection], // 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 95796f167..f7f7aaa16 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -239,15 +239,10 @@ export var MapMLLayer = L.Layer.extend({ _processContent.call(this, content, this._href ? false : true); function _processContent(mapml, local) { var base = layer.getBase(); - layer._properties = {}; - // sets layer._properties.projection - determineLayerProjection(); - // requires that layer._properties.projection be set - if (selectMatchingAlternateProjection()) return; parseLicenseAndLegend(); setLayerTitle(); // crs is only set if the layer has the same projection as the map - if (layer._properties.crs) processTiles(); + if (M[layer.options.projection]) processTiles(); processFeatures(); M._parseStylesheetAsHTML(mapml, base, layer._container); // update controls if needed based on mapml-viewer controls/controlslist attribute @@ -256,77 +251,9 @@ export var MapMLLayer = L.Layer.extend({ layer._layerEl.parentElement._toggleControls(); } // 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; - if (mapml.querySelector('map-meta[name=projection][content]')) { - projection = - M._metaContentToObject( - mapml - .querySelector('map-meta[name=projection]') - .getAttribute('content') - ).content || projection; - } else if (mapml.querySelector('map-extent[units]')) { - const getProjectionFrom = (extents) => { - 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 { - 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; - layer._properties.crs = M[layer._properties.projection]; - } // determine if, where there's no match of the current layer's projection // and that of the map, if there is a linked alternate text/mapml // resource that matches the map's projection - function selectMatchingAlternateProjection() { - let selectedAlternate = - layer._properties.projection !== layer.options.mapprojection && - mapml.querySelector( - '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; - //if this is the only layer, but the projection doesn't match, - // set the map's projection to that of the layer - } else if ( - layer._properties.projection !== layer.options.mapprojection && - layer._layerEl.parentElement.layers.length === 1 - ) { - layer._layerEl.parentElement.projection = - layer._properties.projection; - return true; - } - } catch (error) {} - return false; - } function processFeatures() { let native = M.getNativeVariables(layer._content); layer._mapmlvectors = M.featureLayer(null, { @@ -337,7 +264,7 @@ export var MapMLLayer = L.Layer.extend({ // it will append its own container for rendering into pane: layer._container, opacity: layer.options.opacity, - projection: layer._properties.projection, + projection: layer.options.projection, // by NOT passing options.extent, we are asking the FeatureLayer // to dynamically update its .layerBounds property as features are // added or removed from it @@ -377,11 +304,12 @@ export var MapMLLayer = L.Layer.extend({ layer._staticTileLayer = M.staticTileLayer({ pane: layer._container, _leafletLayer: layer, - projection: layer._properties.projection, + projection: layer.options.projection, className: 'mapml-static-tile-layer', tileContainer: layer._mapmlTileContainer, - maxZoomBound: layer._properties.crs.options.resolutions.length - 1, - tileSize: layer._properties.crs.options.crs.tile.bounds.max.x + maxZoomBound: + M[layer.options.projection].crs.options.resolutions.length - 1, + tileSize: M[layer.options.projection].options.crs.tile.bounds.max.x }); } } @@ -452,13 +380,6 @@ export var MapMLLayer = L.Layer.extend({ } } }, - // new getProjection, maybe simpler, but doesn't work... - getProjection: function () { - if (!this._properties) { - return; - } - return this._properties.projection; - }, getQueryTemplates: function (pcrsClick) { const queryLinks = this._layerEl.querySelectorAll( 'map-extent[checked] map-link[rel=query]' From 7c1e287fb34c16fe911c862f0eec53ac494d9d31 Mon Sep 17 00:00:00 2001 From: prushfor Date: Thu, 30 Nov 2023 16:59:27 -0500 Subject: [PATCH 31/46] WIP on map-link - refactor promise resolution on mapml-viewer.attributeChangedCallback for `projection`. (To Do: do same to web-map). - remove call to mapml-viewer._updateMapCenter() from _map event listeners for: 'move','movestart','zoom','zoomstart' (To Do: do same to web-map) - remove MapMLLayer.copyRemoteContentToShadowRoot - update map-input getters for min, max - update layer-.extent getter tbd if it breaks tests, but it avoids some problems with the layer not being ready --- src/layer.js | 45 ++++++++-------- src/map-input.js | 86 +++++++++++++++++-------------- src/mapml-viewer.js | 94 ++++++++++++++++------------------ src/mapml/layers/MapMLLayer.js | 25 --------- 4 files changed, 117 insertions(+), 133 deletions(-) diff --git a/src/layer.js b/src/layer.js index ca289c31a..c84d1d263 100644 --- a/src/layer.js +++ b/src/layer.js @@ -59,17 +59,19 @@ export class MapLayer extends HTMLElement { get extent() { // calculate the bounds of all content, return it. - if (!this._layer.bounds) { + if (this._layer && !this._layer.bounds) { this._layer._calculateBounds(); } - return Object.assign( - M._convertAndFormatPCRS( - this._layer.bounds, - M[this.getProjection()], - this.getProjection() - ), - { zoom: this._layer.zoomBounds } - ); + return this._layer + ? Object.assign( + M._convertAndFormatPCRS( + this._layer.bounds, + M[this.getProjection()], + this.getProjection() + ), + { zoom: this._layer.zoomBounds } + ) + : null; } attributeChangedCallback(name, oldValue, newValue) { switch (name) { @@ -143,23 +145,26 @@ export class MapLayer extends HTMLElement { } _onRemove() { - if (this._layer) { - this._layer.off(); + let l = this._layer, + lc = this._layerControl; + delete this._layer; + delete this._layerControl; + if (this.shadowRoot) { + this.shadowRoot.innerHTML = ''; + } + + if (l) { + l.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 (l && l._map) { + l._map.removeLayer(l); } - if (this._layerControl && !this.hidden) { - this._layerControl.removeLayer(this._layer); + if (lc && !this.hidden) { + lc.removeLayer(l); } - delete this._layer; delete this._fetchError; - - if (this.shadowRoot) { - this.shadowRoot.innerHTML = ''; - } } connectedCallback() { diff --git a/src/map-input.js b/src/map-input.js index 5c985be0a..c2e473e5f 100644 --- a/src/map-input.js +++ b/src/map-input.js @@ -97,25 +97,30 @@ export class MapInput extends HTMLElement { } } get min() { - if ( - this.type === 'height' || - this.type === 'width' || - this.type === 'hidden' - ) { - return null; - } - if (this.getAttribute('min')) { - return this.getAttribute('min'); - } else if (this._layer._layerEl.querySelector('map-meta[name=zoom]')) { - // fallback map-meta on layer - return M._metaContentToObject( - this._layer._layerEl - .querySelector('map-meta[name=zoom]') - .getAttribute('content') - ).min; - } else { - // fallback map min - return this._layer._layerEl.extent.zoom.minZoom.toString(); + switch (this.type) { + case 'zoom': + if (this.hasAttribute('min')) { + return this.getAttribute('min'); + // min attribute can apply to: type=location, type=zoom + // for zoom, it should fall back via upward document search: 1) this element, + // 2) map-meta within the parent extent, 3) map-meta within the parent layer, + // or finally 4) the map projection crs min/min + // for location, it should fall back by searching upwards: same as for zoom + } else if (this.parentElement.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this.parentElement + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).min; + } else { + // fallback map min + return this.getLayerEl().extent?.zoom.minZoom.toString(); + } + break; + case 'location': + default: + break; } } set min(val) { @@ -124,25 +129,30 @@ export class MapInput extends HTMLElement { } } get max() { - if ( - this.type === 'height' || - this.type === 'width' || - this.type === 'hidden' - ) { - return null; - } - if (this.getAttribute('max')) { - return this.getAttribute('max'); - } else if (this.getLayerEl().querySelector('map-meta[name=zoom]')) { - // fallback map-meta on layer - return M._metaContentToObject( - this.getLayerEl() - .querySelector('map-meta[name=zoom]') - .getAttribute('content') - ).max; - } else { - // fallback map max - return this.getLayerEl().extent.zoom.maxZoom.toString(); + switch (this.type) { + case 'zoom': + if (this.hasAttribute('max')) { + return this.getAttribute('max'); + // max attribute can apply to: type=location, type=zoom + // for zoom, it should fall back via upward document search: 1) this element, + // 2) map-meta within the parent extent, 3) map-meta within the parent layer, + // or finally 4) the map projection crs min/max + // for location, it should fall back by searching upwards: same as for zoom + } else if (this.parentElement.querySelector('map-meta[name=zoom]')) { + // fallback map-meta on layer + return M._metaContentToObject( + this.parentElement + .querySelector('map-meta[name=zoom]') + .getAttribute('content') + ).max; + } else { + // fallback map max + return this.getLayerEl().extent?.zoom.maxZoom.toString(); + } + break; + case 'location': + default: + break; } } set max(val) { diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index ff6e1da22..d18b2b65e 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -349,51 +349,49 @@ export class MapViewer extends HTMLElement { break; 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; - // saving the lat, lon and zoom is necessary because Leaflet seems - // to try to compensate for the change in the scales for each zoom - // level in the crs by changing the zoom level of the map when - // you set the map crs. So, we save the current view for use below - // when all the layers' reconnections have settled. - // leaflet doesn't like this: https://github.com/Leaflet/Leaflet/issues/2553 - this._map.options.crs = M[newValue]; - this._map.options.projection = newValue; - let layersReady = []; - this._map.announceMovement.disable(); - 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(() => { - // use the saved map location to ensure it is correct after - // changing the map CRS. Specifically affects projection - // upgrades, e.g. https://maps4html.org/experiments/custom-projections/BNG/ - // see leaflet bug: https://github.com/Leaflet/Leaflet/issues/2553 - this.zoomTo(lat, lon, zoom); - if (M.options.announceMovement) - this._map.announceMovement.enable(); - // required to delay until map-extent.disabled is correctly set - // which happens as a result of layer-._validateDisabled() - // which happens so much we have to delay until they calls are - // completed - setTimeout(() => { - this.dispatchEvent(new CustomEvent('map-projectionchange')); - }, 0); - }); + // save map location and zoom + let lat = this.lat; + let lon = this.lon; + let zoom = this.zoom; + // saving the lat, lon and zoom is necessary because Leaflet seems + // to try to compensate for the change in the scales for each zoom + // level in the crs by changing the zoom level of the map when + // you set the map crs. So, we save the current view for use below + // when all the layers' reconnections have settled. + // leaflet doesn't like this: https://github.com/Leaflet/Leaflet/issues/2553 + this._map.options.crs = M[newValue]; + this._map.options.projection = newValue; + let layersReady = []; + this._map.announceMovement.disable(); + for (let layer of this.querySelectorAll('layer-')) { + layer.removeAttribute('disabled'); + let reAttach = this.removeChild(layer); + this.appendChild(reAttach); + layersReady.push(reAttach.whenReady()); } + return Promise.allSettled(layersReady).then(() => { + // use the saved map location to ensure it is correct after + // changing the map CRS. Specifically affects projection + // upgrades, e.g. https://maps4html.org/experiments/custom-projections/BNG/ + // see leaflet bug: https://github.com/Leaflet/Leaflet/issues/2553 + this.zoomTo(lat, lon, zoom); + if (M.options.announceMovement) this._map.announceMovement.enable(); + // required to delay until map-extent.disabled is correctly set + // which happens as a result of layer-._validateDisabled() + // which happens so much we have to delay until they calls are + // completed + setTimeout(() => { + this.dispatchEvent(new CustomEvent('map-projectionchange')); + }, 0); + }); }; - if (newValue) { + if ( + newValue && + this._map && + this._map.options.projection !== newValue + ) { const connect = reconnectLayers.bind(this); - new Promise((resolve, reject) => { - connect(); - resolve(); - }).then(() => { + connect().then(() => { if (this._map && this._map.options.projection !== oldValue) { // this awful hack is brought to you by a leaflet bug/ feature request // https://github.com/Leaflet/Leaflet/issues/2553 @@ -836,7 +834,6 @@ export class MapViewer extends HTMLElement { this._map.on( 'movestart', function () { - this._updateMapCenter(); this.dispatchEvent( new CustomEvent('movestart', { detail: { target: this } }) ); @@ -846,7 +843,6 @@ export class MapViewer extends HTMLElement { this._map.on( 'move', function () { - this._updateMapCenter(); this.dispatchEvent( new CustomEvent('move', { detail: { target: this } }) ); @@ -867,7 +863,6 @@ export class MapViewer extends HTMLElement { this._map.on( 'zoomstart', function () { - this._updateMapCenter(); this.dispatchEvent( new CustomEvent('zoomstart', { detail: { target: this } }) ); @@ -877,7 +872,6 @@ export class MapViewer extends HTMLElement { this._map.on( 'zoom', function () { - this._updateMapCenter(); this.dispatchEvent( new CustomEvent('zoom', { detail: { target: this } }) ); @@ -887,10 +881,10 @@ export class MapViewer extends HTMLElement { this._map.on( 'zoomend', function () { - this._updateMapCenter(); - this.dispatchEvent( - new CustomEvent('zoomend', { detail: { target: this } }) - ); + this._updateMapCenter(); + this.dispatchEvent( + new CustomEvent('zoomend', { detail: { target: this } }) + ); }, this ); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index f7f7aaa16..aea447eb9 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -321,31 +321,6 @@ export var MapMLLayer = L.Layer.extend({ layer._title = mapml.getAttribute('label').trim(); } } - function copyRemoteContentToShadowRoot() { - // only run when content is loaded from network, puts features etc - // into layer shadow root - if (local) { - return; - } - let shadowRoot = layer._layerEl.shadowRoot; - // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot - let headMeta = mapml.children[0].children[0].querySelectorAll('*'); - // get the elements inside map-body of remote mapml - let bodyElements = mapml.children[0].children[1].children; - let elements = [...headMeta, ...bodyElements]; - if (elements) { - let baseURL = mapml.children[0].children[0] - .querySelector('map-base') - ?.getAttribute('href'); - for (let el of elements) { - // if not clone, the elements will be **REMOVED** from mapml file and re-attached to the layer's shadow root - // which makes the this._content (mapml file) changed and thus affects the later generation process of this._mapmlvectors - let node = el.cloneNode(true); - el._DOMnode = node; - shadowRoot.appendChild(node); - } - } - } function parseLicenseAndLegend() { var licenseLink = mapml.querySelector('map-link[rel=license]'), licenseTitle, From 0d77e492a0e41382a562c07564459234e638820d Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 1 Dec 2023 12:29:35 -0500 Subject: [PATCH 32/46] WIP on map-link really really WIP - update layer.-src attributeChangedCallback to not do side effects on first pass through lifecylcle callback for remote layer (with src). - refactor / rename layer-.selectMatchingAlternateProjection to selectAlternateOrChangeProjection. Make it throw when condition(s) detected, use it on both remote **and local** content (new behaviour). --- src/layer.js | 56 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/layer.js b/src/layer.js index c84d1d263..52e845c4d 100644 --- a/src/layer.js +++ b/src/layer.js @@ -120,8 +120,14 @@ export class MapLayer extends HTMLElement { } break; case 'src': - if (oldValue && oldValue !== newValue) { + // in lifecycle, attributeChangedCallback is called before connectedCallback + // connectedCallback is called no matter what you do in here + // need to skip these "side effects" of src change on initial load, + // only execute side effects when transitioning from local to remote + // content + if ((oldValue && oldValue !== newValue) || this.children.length) { if (this.isConnected) { + this.innerHTML = ''; this.removeAttribute('disabled'); let map = this.parentElement; let placeholder = document.createElement('span'); @@ -146,9 +152,13 @@ export class MapLayer extends HTMLElement { _onRemove() { let l = this._layer, - lc = this._layerControl; + lc = this._layerControl, + lchtml = this._layerControlHTML; + // remove properties of layer involved in whenReady() logic delete this._layer; delete this._layerControl; + delete this._layerControlHTML; + delete this._fetchError; if (this.shadowRoot) { this.shadowRoot.innerHTML = ''; } @@ -164,7 +174,6 @@ export class MapLayer extends HTMLElement { if (lc && !this.hidden) { lc.removeLayer(l); } - delete this._fetchError; } connectedCallback() { @@ -224,14 +233,10 @@ export class MapLayer extends HTMLElement { ) { throw new Error('Parser error'); } - let alternate = this.selectMatchingAlternateProjection( + // may throw: + this.selectAlternateOrChangeProjection( content.querySelector('mapml-') ); - if (alternate) { - throw new Error('changeprojection', { - cause: { href: alternate } - }); - } this.copyRemoteContentToShadowRoot(content.querySelector('mapml-')); }) .then(() => { @@ -264,6 +269,9 @@ export class MapLayer extends HTMLElement { reject(error); }); } else { + // may throw: + this.selectAlternateOrChangeProjection(this); + let elements = this.querySelectorAll('*'); let elementsReady = []; for (let i = 0; i < elements.length; i++) { @@ -271,9 +279,6 @@ export class MapLayer extends HTMLElement { elementsReady.push(elements[i].whenReady()); } Promise.allSettled(elementsReady).then(() => { - if (this._layer) { - this._onRemove(); - } this._layer = M.mapMLLayer(null, this, null, { projection: this.getProjection(), opacity: this.opacity @@ -286,8 +291,15 @@ export class MapLayer extends HTMLElement { } }).catch((e) => { if (e.message === 'changeprojection') { - console.log('Changing layer src to: ' + e.cause.href); - this.src = e.cause.href; + if (e.cause.href) { + console.log('Changing layer src to: ' + e.cause.href); + this.src = e.cause.href; + } else if (e.cause.mapprojection) { + console.log( + 'Changing map projection to match layer: ' + e.cause.mapprojection + ); + this.parentElement.projection = e.cause.mapprojection; + } } else { console.log(e); this.dispatchEvent( @@ -321,7 +333,7 @@ export class MapLayer extends HTMLElement { return new URL(relativeURL, baseURL).href; } - selectMatchingAlternateProjection(mapml) { + selectAlternateOrChangeProjection(mapml) { let selectedAlternate = this.getProjection(mapml) !== this.parentElement.projection && mapml.querySelector( @@ -333,9 +345,19 @@ export class MapLayer extends HTMLElement { if (selectedAlternate) { let url = new URL(selectedAlternate.getAttribute('href'), this.getBase()) .href; - return url; + throw new Error('changeprojection', { + cause: { href: url } + }); + } + let contentProjection = this.getProjection(mapml); + if ( + contentProjection !== this.parentElement.projection && + this.parentElement.layers.length === 1 + ) { + throw new Error('changeprojection', { + cause: { mapprojection: contentProjection } + }); } - return false; } copyRemoteContentToShadowRoot(mapml) { From dda7f21a15eb2c6acceabfb2e1b6af25150a193b Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Mon, 4 Dec 2023 17:51:54 -0500 Subject: [PATCH 33/46] WIP on map-link really really WIP - add layer.#hasConnected private so that can avoid attributeChangedCallback calls queued by browser on upgrade etc. Allows to remove individual this.whenReady() calls and consequent microtask queue enlargement - remove use of inducing layer.connectedCallback by the layer.src.attributeChangedCallback - only do the side effects which means calling the functions that layer.src.connectedCallback calls, seems to prevent inadvertent duplication of content in layer.shadowRoot - per advice from HTML Standard, create layer.shadowRoot in constructor as well as initialization of opacity. - unconditionally purge layer.shadowRoot during onRemove - move selectAlternateOrChangeProjection() to after copying content to shadow root, insert into next then in promise chain which waits on all that contents' whenReady() to settle which can only be done there because it needs access to the map-link.getBase() instance function to determine the correct URL to load, which isn't a function until the map-link and all its "sibling" content becomes ready. - add changeprojection messaging to fetch catch block, but don't call it a "_fetchError" because that's confusing (not really a fetch error). Also, the overall promise should fulfill, just not until after changing the src or mapml-viewer.projection as selected. - get rid of copy of getBase() on layer, which was cut-and-pasted from map-link, and can't work on layer because the document context of the map-link is element instance-based and not available to layer. - remove the parameter from layer.getProjection(), make it detect whether the content is remote (.src exists, so .shadowRoot should contain stuff) - change other stuff that relies on layer.shadowRoot to use layer.src to detect if layer *should* have a shadowRoot - make map-link.whenReady() reject if rel=alternate but no href nor projection - attempt to map-link.whenReady() to reject with message when await used on parentExtent.whenReady() and _templateVars.inputsReady --- src/layer.js | 219 ++++++++---------- src/map-extent.js | 7 +- src/map-link.js | 39 ++-- src/mapml-viewer.js | 8 +- .../layers/createLayerControlForLayer.js | 2 +- src/mapml/layers/MapMLLayer.js | 4 +- 6 files changed, 121 insertions(+), 158 deletions(-) diff --git a/src/layer.js b/src/layer.js index 52e845c4d..0b88104b1 100644 --- a/src/layer.js +++ b/src/layer.js @@ -5,6 +5,9 @@ export class MapLayer extends HTMLElement { static get observedAttributes() { return ['src', 'label', 'checked', 'hidden', 'opacity']; } + /* jshint ignore:start */ + #hasConnected; + /* jshint ignore:end */ get src() { return this.hasAttribute('src') ? this.getAttribute('src') : ''; } @@ -74,74 +77,55 @@ export class MapLayer extends HTMLElement { : null; } attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'label': - this.whenReady() - .then(() => { - this._layer.setName(newValue); - }) - .catch((e) => { - console.log(e); - }); - break; - case 'checked': - this.whenReady() - .then(() => { - if (typeof newValue === 'string') { - this.parentElement._map.addLayer(this._layer); - } else { - this.parentElement._map.removeLayer(this._layer); - } - this._layerControlCheckbox.checked = this.checked; - this.dispatchEvent(new CustomEvent('map-change')); - }) - .catch((e) => { - console.log(e); - }); - break; - case 'hidden': - var map = this.parentElement && this.parentElement._map; - if (map && this.parentElement.controls) { + if (this.#hasConnected /* jshint ignore:line */) { + switch (name) { + case 'label': + this._layer.setName(newValue); + break; + case 'checked': if (typeof newValue === 'string') { - if (this._layer) { + this.parentElement._map.addLayer(this._layer); + } else { + this.parentElement._map.removeLayer(this._layer); + } + this._layerControlCheckbox.checked = this.checked; + this.dispatchEvent(new CustomEvent('map-change')); + break; + case 'hidden': + var map = this.parentElement && this.parentElement._map; + if (map && this.parentElement.controls) { + if (typeof newValue === 'string') { this.parentElement._layerControl.removeLayer(this._layer); + } else { + this._layerControl.addOrUpdateOverlay(this._layer, this.label); + this._validateDisabled(); } - } else { - this._layerControl = this.parentElement._layerControl; - this._layerControl.addOrUpdateOverlay(this._layer, this.label); - this._validateDisabled(); } - } - break; - case 'opacity': - if (oldValue !== newValue && this._layer) { - this._opacity = newValue; - this._layer.changeOpacity(newValue); - } - break; - case 'src': - // in lifecycle, attributeChangedCallback is called before connectedCallback - // connectedCallback is called no matter what you do in here - // need to skip these "side effects" of src change on initial load, - // only execute side effects when transitioning from local to remote - // content - if ((oldValue && oldValue !== newValue) || this.children.length) { - if (this.isConnected) { - this.innerHTML = ''; - this.removeAttribute('disabled'); - let map = this.parentElement; - let placeholder = document.createElement('span'); - this.insertAdjacentElement('beforebegin', placeholder); - map.removeChild(this); - placeholder.replaceWith(this); + break; + case 'opacity': + if (oldValue !== newValue && this._layer) { + this._opacity = newValue; + this._layer.changeOpacity(newValue); } - } + break; + case 'src': + if (oldValue !== newValue) { + this._onRemove(); + if (this.isConnected) { + this._onAdd(); + } + } + } } } constructor() { // Always call super first in constructor super(); + // this._opacity is used to record the current opacity value (with or without updates), + // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 + this._opacity = this.opacity || 1.0; + this.attachShadow({ mode: 'open' }); } disconnectedCallback() { // if the map-layer node is removed from the dom, the layer should be @@ -159,9 +143,7 @@ export class MapLayer extends HTMLElement { delete this._layerControl; delete this._layerControlHTML; delete this._fetchError; - if (this.shadowRoot) { - this.shadowRoot.innerHTML = ''; - } + this.shadowRoot.innerHTML = ''; if (l) { l.off(); @@ -178,25 +160,24 @@ export class MapLayer extends HTMLElement { connectedCallback() { if (this.hasAttribute('data-moving')) return; + /* jshint ignore:start */ + this.#hasConnected = true; + /* jshint ignore:end */ this._createLayerControlHTML = M._createLayerControlHTML.bind(this); - // this._opacity is used to record the current opacity value (with or without updates), - // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 - this._opacity = this.opacity || 1.0; const doConnected = this._onAdd.bind(this); + const doRemove = this._onRemove.bind(this); this.parentElement .whenReady() .then(() => { + doRemove(); doConnected(); }) - .catch(() => { - throw new Error('Map never became ready'); + .catch((error) => { + throw new Error('Map never became ready: ' + error); }); } _onAdd() { - if (this.getAttribute('src') && !this.shadowRoot) { - this.attachShadow({ mode: 'open' }); - } new Promise((resolve, reject) => { this.addEventListener( 'changestyle', @@ -233,13 +214,7 @@ export class MapLayer extends HTMLElement { ) { throw new Error('Parser error'); } - // may throw: - this.selectAlternateOrChangeProjection( - content.querySelector('mapml-') - ); this.copyRemoteContentToShadowRoot(content.querySelector('mapml-')); - }) - .then(() => { let elements = this.shadowRoot.querySelectorAll('*'); let elementsReady = []; for (let i = 0; i < elements.length; i++) { @@ -248,6 +223,10 @@ export class MapLayer extends HTMLElement { } return Promise.allSettled(elementsReady); }) + .then(() => { + // may throw: + this.selectAlternateOrChangeProjection(); + }) .then(() => { this._layer = M.mapMLLayer( new URL(this.src, base).href, @@ -264,30 +243,42 @@ export class MapLayer extends HTMLElement { resolve(); }) .catch((error) => { - this._fetchError = true; - console.log('Error fetching layer content: ' + error); + if (error.message === 'changeprojection') { + console.log('Changing projection'); + } else { + this._fetchError = true; + console.log('Error fetching layer content: ' + error); + } reject(error); }); } else { - // may throw: - this.selectAlternateOrChangeProjection(this); - let elements = this.querySelectorAll('*'); let elementsReady = []; for (let i = 0; i < elements.length; i++) { if (elements[i].whenReady) elementsReady.push(elements[i].whenReady()); } - Promise.allSettled(elementsReady).then(() => { - this._layer = M.mapMLLayer(null, this, null, { - projection: this.getProjection(), - opacity: this.opacity + Promise.allSettled(elementsReady) + .then(() => { + // may throw: + this.selectAlternateOrChangeProjection(); + }) + .then(() => { + this._layer = M.mapMLLayer(null, this, null, { + projection: this.getProjection(), + opacity: this.opacity + }); + this._createLayerControlHTML(); + this._attachedToMap(); + this._validateDisabled(); + resolve(); + }) + .catch((error) => { + if (error.message === 'changeprojection') { + console.log('Changing projection'); + } + reject(error); }); - this._createLayerControlHTML(); - this._attachedToMap(); - this._validateDisabled(); - resolve(); - }); } }).catch((e) => { if (e.message === 'changeprojection') { @@ -309,33 +300,10 @@ export class MapLayer extends HTMLElement { }); } - getBase() { - let layer = this.getRootNode().host; - // - let relativeURL = - this.getRootNode().querySelector('map-base') && - this.getRootNode() instanceof ShadowRoot - ? this.getRootNode().querySelector('map-base').getAttribute('href') - : /* local content? */ !(this.getRootNode() instanceof ShadowRoot) - ? /* use the baseURI algorithm which takes into account any */ - this.getRootNode().querySelector('map-base')?.getAttribute('href') || - this.baseURI - : /* else use the resolved value */ new URL( - layer.src, - layer.baseURI - ).href; - - // when remote content, use layer.src as base else use baseURI of map-link - let baseURL = - this.getRootNode() instanceof ShadowRoot - ? new URL(layer.src, layer.baseURI).href - : this.baseURI; - return new URL(relativeURL, baseURL).href; - } - - selectAlternateOrChangeProjection(mapml) { + selectAlternateOrChangeProjection() { + let mapml = this.src ? this.shadowRoot : this; let selectedAlternate = - this.getProjection(mapml) !== this.parentElement.projection && + this.getProjection() !== this.parentElement.projection && mapml.querySelector( 'map-link[rel=alternate][projection=' + this.parentElement.projection + @@ -343,13 +311,15 @@ export class MapLayer extends HTMLElement { ); if (selectedAlternate) { - let url = new URL(selectedAlternate.getAttribute('href'), this.getBase()) - .href; + let url = new URL( + selectedAlternate.getAttribute('href'), + selectedAlternate.getBase() + ).href; throw new Error('changeprojection', { cause: { href: url } }); } - let contentProjection = this.getProjection(mapml); + let contentProjection = this.getProjection(); if ( contentProjection !== this.parentElement.projection && this.parentElement.layers.length === 1 @@ -370,10 +340,13 @@ export class MapLayer extends HTMLElement { } shadowRoot.appendChild(frag); } - getProjection(content) { - let mapml = content || this; + /** + * For "local" content, getProjection will use content of "this" + * For "remote" content, you need to pass the shadowRoot to search through + */ + getProjection() { + let mapml = this.src ? this.shadowRoot : this; let projection = this.parentElement.projection; - mapml = mapml.shadowRoot ? mapml.shadowRoot : mapml; if (mapml.querySelector('map-meta[name=projection][content]')) { projection = M._metaContentToObject( @@ -474,7 +447,7 @@ export class MapLayer extends HTMLElement { map = layer?._map; if (map) { // prerequisite: no inline and remote mapml elements exists at the same time - const mapExtents = this.shadowRoot + const mapExtents = this.src ? this.shadowRoot.querySelectorAll('map-extent') : this.querySelectorAll('map-extent'); let extentLinksReady = []; @@ -558,7 +531,7 @@ export class MapLayer extends HTMLElement { } } queryable() { - let content = this.shadowRoot ? this.shadowRoot : this; + let content = this.src ? this.shadowRoot : this; return ( content.querySelector('map-extent[checked] > map-link[rel=query]') && this.checked && @@ -703,7 +676,7 @@ export class MapLayer extends HTMLElement { // check if all child elements are ready whenElemsReady() { let elemsReady = []; - let target = this.shadowRoot || this; + let target = this.src ? this.shadowRoot : this; for (let elem of [ ...target.querySelectorAll('map-extent'), ...target.querySelectorAll('map-feature') diff --git a/src/map-extent.js b/src/map-extent.js index 3cc2b7ecd..05ed739b4 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -221,7 +221,7 @@ export class MapExtent extends HTMLElement { getMeta(metaName) { let name = metaName.toLowerCase(); if (name !== 'extent' && name !== 'zoom') return; - return this.parentLayer.shadowRoot + return this.parentLayer.src ? this.querySelector(`map-meta[name=${name}]`) || this.parentLayer.shadowRoot.querySelector(`map-meta[name=${name}]`) : this.querySelector(`map-meta[name=${name}]`) || @@ -283,7 +283,10 @@ export class MapExtent extends HTMLElement { } _validateLayerControlContainerHidden() { let extentsFieldset = this.parentLayer._propertiesGroupAnatomy; - let nodeToSearch = this.parentLayer.shadowRoot || this.parentLayer; + let nodeToSearch = this.parentLayer.src + ? this.parentLayer.shadowRoot + : this.parentLayer; + if (!extentsFieldset) return; if ( nodeToSearch.querySelectorAll('map-extent:not([hidden])').length === 0 ) { diff --git a/src/map-link.js b/src/map-link.js index 27474c6e2..de80d2c23 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -217,7 +217,7 @@ export class MapLink extends HTMLElement { this._createStylesheetLink(); break; case 'alternate': - //this._createAlternateLink(); // add media attribute + this._createAlternateLink(); // add media attribute break; case 'license': // this._createLicenseLink(); @@ -229,28 +229,8 @@ export class MapLink extends HTMLElement { // add to viewer._map dependant on map-extent.checked, layer-.checked // what else? } - _createAlternateLink() { - let selectedAlternate = - this.getLayerEl().getProjection() !== this.getMapEl().projection && - this.projection === this.getMapEl().projection && - this.href; - if (selectedAlternate) { - let url = new URL(this.href, this.getBase()).href; - this.getLayerEl().dispatchEvent( - new CustomEvent('changeprojection', { - detail: { - href: url - } - }) - ); - //if this is the only layer, but the projection doesn't match, - // set the map's projection to that of the layer - } else if ( - this.getLayerEl().getProjection() !== this.getMapEl().projection && - this.getMapEl().layers.length === 1 - ) { - this.getMapEl().projection = this.getLayerEl().getProjection(); - } + _createAlternateLink(mapml) { + if (this.href && this.projection) this._alternate = true; } async _createTemplatedLink() { @@ -261,8 +241,13 @@ export class MapLink extends HTMLElement { ? this.parentNode : this.parentNode.host; if (!this.tref || !this.parentExtent) return; - await this.parentExtent.whenReady(); - await this._templateVars.inputsReady; + try { + await this.parentExtent.whenReady(); + await this._templateVars.inputsReady; + } catch (error) { + console.log('Error while creating templated link: ' + error); + return; + } this.mapEl = this.getMapEl(); // create the layer type appropriate to the rel value this.zIndex = Array.from( @@ -788,11 +773,13 @@ export class MapLink extends HTMLElement { case 'query': ready = 'shadowRoot'; break; + case 'alternate': + ready = '_alternate'; + break; case 'zoomin': case 'zoomout': case 'legend': case 'stylesheet': - case 'alternate': case 'license': resolve(); break; diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index d18b2b65e..75ebc8683 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -881,10 +881,10 @@ export class MapViewer extends HTMLElement { this._map.on( 'zoomend', function () { - this._updateMapCenter(); - this.dispatchEvent( - new CustomEvent('zoomend', { detail: { target: this } }) - ); + this._updateMapCenter(); + this.dispatchEvent( + new CustomEvent('zoomend', { detail: { target: this } }) + ); }, this ); diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js index 420f4f704..e80b9a2df 100644 --- a/src/mapml/elementSupport/layers/createLayerControlForLayer.js +++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js @@ -286,7 +286,7 @@ export var createLayerControlHTML = async function () { itemSettingControlButton.appendChild(settingsButtonNameIcon); settingsButtonNameIcon.appendChild(svgSettingsControlIcon); - let mapml = this.shadowRoot ? this.shadowRoot : this; + let mapml = this.src ? this.shadowRoot : this; var styleLinks = mapml.querySelectorAll( 'map-link[rel=style],map-link[rel="self style"],map-link[rel="style self"]' ); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index aea447eb9..005eb9abb 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -184,10 +184,10 @@ export var MapMLLayer = L.Layer.extend({ let toZoom = e.zoom; let min = layerEl.extent.zoom.minZoom; let max = layerEl.extent.zoom.maxZoom; - let inLink = layerEl.shadowRoot + let inLink = layerEl.src ? layerEl.shadowRoot.querySelector('map-link[rel=zoomin]') : layerEl.querySelector('map-link[rel=zoomin]'), - outLink = layerEl.shadowRoot + outLink = layerEl.src ? layerEl.shadowRoot.querySelector('map-link[rel=zoomout]') : layerEl.querySelector('map-link[rel=zoomout]'); let targetURL; From 462b6c51af89c637400c99c4e3ce40884bc1dfb4 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 5 Dec 2023 17:55:45 -0500 Subject: [PATCH 34/46] WIP on map-link: - validate and update map-link.getBounds(), map-link.getFallbackBounds() - add map-link.media attribute, with the idea of prototyping `prefers-map-content` proposed media feature - update layer on one side of connectedCallback to show where handling of map-link.media attribute may go see layer-.checkForPreferredContent() - add and use #hasConnected field to map-extent - tentatively add mapml-viewer._map.options.minZoom, .maxZoom to allow map to use all zoom levels in crs - update TemplatedTileLayer.initialize to remove .options.zoomBounds property which is / would be information triplication (already have TemplatedTileLayer.zoomBounds, .options.minZoom etc, don't need a third) - --- src/layer.js | 3 + src/map-extent.js | 154 ++++++++++----------- src/map-link.js | 184 ++++++++++++++----------- src/mapml-viewer.js | 2 + src/mapml/layers/TemplatedTileLayer.js | 4 +- 5 files changed, 184 insertions(+), 163 deletions(-) diff --git a/src/layer.js b/src/layer.js index 0b88104b1..221ac7cc1 100644 --- a/src/layer.js +++ b/src/layer.js @@ -226,6 +226,9 @@ export class MapLayer extends HTMLElement { .then(() => { // may throw: this.selectAlternateOrChangeProjection(); + }).then(() => { + // may throw + this.checkForPreferredContent(); }) .then(() => { this._layer = M.mapMLLayer( diff --git a/src/map-extent.js b/src/map-extent.js index 05ed739b4..65ca2ff9a 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -3,6 +3,9 @@ export class MapExtent extends HTMLElement { static get observedAttributes() { return ['checked', 'label', 'opacity', 'hidden']; } + /* jshint ignore:start */ + #hasConnected; + /* jshint ignore:end */ get units() { return this.getAttribute('units') || M.FALLBACK_PROJECTION; } @@ -49,98 +52,93 @@ export class MapExtent extends HTMLElement { } } attributeChangedCallback(name, oldValue, newValue) { - this.whenReady() - .then(() => { - switch (name) { - case 'units': - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'label': - if (oldValue !== newValue) { - this._layerControlHTML.querySelector( - '.mapml-layer-item-name' - ).innerHTML = newValue || M.options.locale.dfExtent; - } - break; - case 'checked': + if (this.#hasConnected /* jshint ignore:line */) { + switch (name) { + case 'units': + if (oldValue !== newValue) { + // handle side effects + } + break; + case 'label': + if (oldValue !== newValue) { + this._layerControlHTML.querySelector( + '.mapml-layer-item-name' + ).innerHTML = newValue || M.options.locale.dfExtent; + } + break; + case 'checked': + this.parentLayer + .whenReady() + .then(() => { + this._handleChange(); + this._calculateBounds(); + this._layerControlCheckbox.checked = newValue !== null; + }) + .catch((error) => { + console.log( + 'Error while waiting on parentLayer for map-extent checked callback: ' + + error + ); + }); + break; + case 'opacity': + if (oldValue !== newValue) { + this._opacity = newValue; + if (this._extentLayer) this._extentLayer.changeOpacity(newValue); + } + break; + case 'hidden': + if (oldValue !== newValue) { this.parentLayer .whenReady() .then(() => { - this._handleChange(); - this._calculateBounds(); - this._layerControlCheckbox.checked = newValue !== null; + let extentsRootFieldset = + this.parentLayer._propertiesGroupAnatomy; + let position = Array.from( + this.parentNode.querySelectorAll('map-extent:not([hidden])') + ).indexOf(this); + if (newValue !== null) { + // remove from layer control (hide from user) + this._layerControlHTML.remove(); + } else { + // insert the extent fieldset into the layer control container in + // the calculated position + if (position === 0) { + extentsRootFieldset.insertAdjacentElement( + 'afterbegin', + this._layerControlHTML + ); + } else if (position > 0) { + this.parentNode + .querySelectorAll('map-extent:not([hidden])') + [position - 1]._layerControlHTML.insertAdjacentElement( + 'afterend', + this._layerControlHTML + ); + } + } + this._validateLayerControlContainerHidden(); }) .catch(() => { console.log( - 'Error while waiting on parentLayer for map-extent checked callback' + 'Error while waiting on parentLayer for map-extent hidden callback' ); }); - break; - case 'opacity': - if (oldValue !== newValue) { - this._opacity = newValue; - if (this._extentLayer) this._extentLayer.changeOpacity(newValue); - } - break; - case 'hidden': - if (oldValue !== newValue) { - this.parentLayer - .whenReady() - .then(() => { - let extentsRootFieldset = - this.parentLayer._propertiesGroupAnatomy; - let position = Array.from( - this.parentNode.querySelectorAll('map-extent:not([hidden])') - ).indexOf(this); - if (newValue !== null) { - // remove from layer control (hide from user) - this._layerControlHTML.remove(); - } else { - // insert the extent fieldset into the layer control container in - // the calculated position - if (position === 0) { - extentsRootFieldset.insertAdjacentElement( - 'afterbegin', - this._layerControlHTML - ); - } else if (position > 0) { - this.parentNode - .querySelectorAll('map-extent:not([hidden])') - [position - 1]._layerControlHTML.insertAdjacentElement( - 'afterend', - this._layerControlHTML - ); - } - } - this._validateLayerControlContainerHidden(); - }) - .catch(() => { - console.log( - 'Error while waiting on parentLayer for map-extent hidden callback' - ); - }); - } - break; - } - }) - .catch((reason) => { - console.log( - reason, - `\nin mapExtent.attributeChangeCallback when changing attribute ${name}` - ); - }); + } + break; + } + } } constructor() { // Always call super first in constructor super(); + this._createLayerControlExtentHTML = + M._createLayerControlExtentHTML.bind(this); + this._changeHandler = this._handleChange.bind(this); } async connectedCallback() { // this.parentNode.host returns the layer- element when parentNode is // the shadow root - this._createLayerControlExtentHTML = - M._createLayerControlExtentHTML.bind(this); this.parentLayer = this.parentNode.nodeName.toUpperCase() === 'LAYER-' ? this.parentNode @@ -160,10 +158,12 @@ export class MapExtent extends HTMLElement { // in this case, the microtasks triggered by the fulfillment of the removed MapMLLayer should be stopped as well // !this.isConnected <=> the disconnectedCallback has run before if (!this.isConnected) return; + /* jshint ignore:start */ + this.#hasConnected = true; + /* jshint ignore:end */ this._map = this.mapEl._map; // reset the layer extent delete this.parentLayer.bounds; - this._changeHandler = this._handleChange.bind(this); this.parentLayer.addEventListener('map-change', this._changeHandler); this.mapEl.addEventListener('map-projectionchange', this._changeHandler); // this._opacity is used to record the current opacity value (with or without updates), diff --git a/src/map-link.js b/src/map-link.js index de80d2c23..26de9422e 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -6,6 +6,7 @@ export class MapLink extends HTMLElement { 'type', 'rel', // 'title', + 'media', 'href', 'hreflang', 'tref', @@ -95,6 +96,12 @@ export class MapLink extends HTMLElement { this.setAttribute('tref', val); } } + get media() { + return this.getAttribute('media'); + } + set media(val) { + this.setAttribute('media', val); + } get tms() { return this.hasAttribute('tms'); } @@ -173,6 +180,8 @@ export class MapLink extends HTMLElement { this._initTemplateVars(); } break; + case 'media': + break; case 'tms': // rel = tile if (oldValue !== newValue) { @@ -497,103 +506,87 @@ export class MapLink extends HTMLElement { } /** * TODO: review getBounds for sanity, also getFallbackBounds, perhaps integrate - */ + * there is no other kind of bounds but native.... + * each rectangle must be established and valid and converted to PCRS coordinates... + // "native" bounds = input type=location min max || map-extent/map-meta name=extent min,max || layer-/map-meta name=extent min,max || layer projection min/max + */ getBounds() { let template = this._templateVars; let inputs = template.values, projection = this.parentElement.units, - value = 0, - boundsUnit = M.FALLBACK_CS; + boundsUnit = {}; + boundsUnit.name = M.FALLBACK_CS; let bounds = M[projection].options.crs.tilematrix.bounds(0), - defaultMinZoom = 0, - defaultMaxZoom = M[projection].options.resolutions.length - 1, - nativeMinZoom = defaultMinZoom, - nativeMaxZoom = defaultMaxZoom; - let locInputs = false, - numberOfAxes = 0; + locInputs = false, + numberOfAxes = 0, + horizontalAxis = false, + verticalAxis = false; for (let i = 0; i < inputs.length; i++) { - switch (inputs[i].getAttribute('type')) { - case 'zoom': - nativeMinZoom = +(inputs[i].hasAttribute('min') && - !isNaN(+inputs[i].getAttribute('min')) - ? inputs[i].getAttribute('min') - : defaultMinZoom); - nativeMaxZoom = +(inputs[i].hasAttribute('max') && - !isNaN(+inputs[i].getAttribute('max')) - ? inputs[i].getAttribute('max') - : defaultMaxZoom); - value = +inputs[i].getAttribute('value'); - break; - case 'location': - if (!inputs[i].getAttribute('max') || !inputs[i].getAttribute('min')) - continue; - let max = +inputs[i].getAttribute('max'), - min = +inputs[i].getAttribute('min'); - switch (inputs[i].getAttribute('axis').toLowerCase()) { - case 'x': - case 'longitude': - case 'column': - case 'easting': - boundsUnit = M.axisToCS( - inputs[i].getAttribute('axis').toLowerCase() - ); - bounds.min.x = min; - bounds.max.x = max; - numberOfAxes++; - break; - case 'y': - case 'latitude': - case 'row': - case 'northing': - boundsUnit = M.axisToCS( - inputs[i].getAttribute('axis').toLowerCase() - ); - bounds.min.y = min; - bounds.max.y = max; - numberOfAxes++; - break; - default: - break; - } - break; - default: + if (inputs[i].getAttribute('type') === 'location') { + if (!inputs[i].getAttribute('max') || !inputs[i].getAttribute('min')) + continue; + let max = +inputs[i].getAttribute('max'), + min = +inputs[i].getAttribute('min'); + switch (inputs[i].getAttribute('axis').toLowerCase()) { + case 'x': + case 'longitude': + case 'column': + case 'easting': + boundsUnit.name = M.axisToCS( + inputs[i].getAttribute('axis').toLowerCase() + ); + bounds.min.x = min; + bounds.max.x = max; + boundsUnit.horizontalAxis = inputs[i] + .getAttribute('axis') + .toLowerCase(); + break; + case 'y': + case 'latitude': + case 'row': + case 'northing': + boundsUnit.name = M.axisToCS( + inputs[i].getAttribute('axis').toLowerCase() + ); + bounds.min.y = min; + bounds.max.y = max; + boundsUnit.verticalAxis = inputs[i] + .getAttribute('axis') + .toLowerCase(); + break; + default: + break; + } } } - if (numberOfAxes >= 2) { + if ( + boundsUnit.horizontalAxis && + boundsUnit.verticalAxis && + ((boundsUnit.horizontalAxis === 'x' && boundsUnit.verticalAxis === 'y') || + (boundsUnit.horizontalAxis === 'longitude' && + boundsUnit.verticalAxis === 'latitude') || + (boundsUnit.horizontalAxis === 'column' && + boundsUnit.verticalAxis === 'row') || + (boundsUnit.horizontalAxis === 'easting' && + boundsUnit.verticalAxis === 'northing')) + ) { locInputs = true; } - if (!locInputs) { + if (locInputs) { + let zoomValue = this._templateVars.zoom?.hasAttribute('value') + ? +this._templateVars.zoom.getAttribute('value') + : 0; + bounds = M.boundsToPCRSBounds( + bounds, + zoomValue, + projection, + boundsUnit.name + ); + } else if (!locInputs) { bounds = this.getFallbackBounds(projection); - } else if (locInputs) { - bounds = M.boundsToPCRSBounds(bounds, value, projection, boundsUnit); - } else { - bounds = M[projection].options.crs.pcrs.bounds; } return bounds; } - getBase() { - let layer = this.getRootNode().host; - // - let relativeURL = - this.getRootNode().querySelector('map-base') && - this.getRootNode() instanceof ShadowRoot - ? this.getRootNode().querySelector('map-base').getAttribute('href') - : /* local content? */ !(this.getRootNode() instanceof ShadowRoot) - ? /* use the baseURI algorithm which takes into account any */ - this.getRootNode().querySelector('map-base')?.getAttribute('href') || - this.baseURI - : /* else use the resolved value */ new URL( - layer.src, - layer.baseURI - ).href; - - // when remote content, use layer.src as base else use baseURI of map-link - let baseURL = - this.getRootNode() instanceof ShadowRoot - ? new URL(layer.src, layer.baseURI).href - : this.baseURI; - return new URL(relativeURL, baseURL).href; - } getFallbackBounds(projection) { let bounds; @@ -629,11 +622,34 @@ export class MapLink extends HTMLElement { cs ); } else { - let crs = M[this.parentElement.units]; + let crs = M[projection]; bounds = crs.options.crs.pcrs.bounds; } return bounds; } + getBase() { + let layer = this.getRootNode().host; + // + let relativeURL = + this.getRootNode().querySelector('map-base') && + this.getRootNode() instanceof ShadowRoot + ? this.getRootNode().querySelector('map-base').getAttribute('href') + : /* local content? */ !(this.getRootNode() instanceof ShadowRoot) + ? /* use the baseURI algorithm which takes into account any */ + this.getRootNode().querySelector('map-base')?.getAttribute('href') || + this.baseURI + : /* else use the resolved value */ new URL( + layer.src, + layer.baseURI + ).href; + + // when remote content, use layer.src as base else use baseURI of map-link + let baseURL = + this.getRootNode() instanceof ShadowRoot + ? new URL(layer.src, layer.baseURI).href + : this.baseURI; + return new URL(relativeURL, baseURL).href; + } /** * Return BOTH min/max(Display)Zoom AND min/maxNativeZoom which * are options that can be passed to L.GridLayer... diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 75ebc8683..e4e787705 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -267,6 +267,8 @@ export class MapViewer extends HTMLElement { if (!this._map) { this._map = L.map(this._container, { center: new L.LatLng(this.lat, this.lon), + minZoom: 0, + maxZoom: M[this.projection].options.resolutions.length - 1, projection: this.projection, query: true, contextMenu: true, diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index da78070d8..a0cfb2a8e 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -9,7 +9,6 @@ export var TemplatedTileLayer = L.TileLayer.extend({ // _setUpTileTemplateVars needs options.crs, not available unless we set // options first... options.tms = template.tms; - L.setOptions(this, options); // it's critical to have this.options.minZoom, minNativeZoom, maxZoom, maxNativeZoom // because they are used by Leaflet Map and GridLayer, but we // don't need two copies of that info on our options object, so set the @@ -20,7 +19,8 @@ export var TemplatedTileLayer = L.TileLayer.extend({ delete options.zoomBounds; // unpack object to this.options.minZ... etc where minZ... are the props // of the this.zoomBounds object: - L.extend(this.options, this.zoomBounds); + L.extend(options, this.zoomBounds); + L.setOptions(this, options); // _setup call here relies on this.options.minZ.. etc this._setUpTileTemplateVars(template); this._linkEl = options.linkEl; From 9a78afc6e67145ecede48cbaa521415eedfcbf69 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Mon, 11 Dec 2023 10:19:36 -0500 Subject: [PATCH 35/46] WIP on map-link - add media attribute to map-link, work towards media query support for prefers-map-content - re-organize promise then/catch for fetches - adopt consistent style of detecting projection, prefers-map-content etc to throw during loading, triggering src changes --- src/layer.js | 49 +++++++++++++----- src/map-link.js | 133 ++++++++++++++++++++++++++---------------------- 2 files changed, 109 insertions(+), 73 deletions(-) diff --git a/src/layer.js b/src/layer.js index 221ac7cc1..1d059b651 100644 --- a/src/layer.js +++ b/src/layer.js @@ -125,6 +125,7 @@ export class MapLayer extends HTMLElement { // this._opacity is used to record the current opacity value (with or without updates), // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 this._opacity = this.opacity || 1.0; + this._renderingMapContent = M.options.contentPreference; this.attachShadow({ mode: 'open' }); } disconnectedCallback() { @@ -183,7 +184,11 @@ export class MapLayer extends HTMLElement { 'changestyle', function (e) { e.stopPropagation(); - this.src = e.detail.src; + // if user changes the style in layer control + if (e.detail) { + this._renderingMapContent = e.detail._renderingMapContent; + this.src = e.detail.src; + } }, { once: true } ); @@ -212,8 +217,14 @@ export class MapLayer extends HTMLElement { content.querySelector('parsererror') || !content.querySelector('mapml-') ) { + // cut short whenReady with the _fetchError property + this._fetchError = true; + console.log('Error fetching layer content'); throw new Error('Parser error'); } + return content; + }) + .then((content) => { this.copyRemoteContentToShadowRoot(content.querySelector('mapml-')); let elements = this.shadowRoot.querySelectorAll('*'); let elementsReady = []; @@ -226,8 +237,6 @@ export class MapLayer extends HTMLElement { .then(() => { // may throw: this.selectAlternateOrChangeProjection(); - }).then(() => { - // may throw this.checkForPreferredContent(); }) .then(() => { @@ -246,12 +255,6 @@ export class MapLayer extends HTMLElement { resolve(); }) .catch((error) => { - if (error.message === 'changeprojection') { - console.log('Changing projection'); - } else { - this._fetchError = true; - console.log('Error fetching layer content: ' + error); - } reject(error); }); } else { @@ -265,6 +268,7 @@ export class MapLayer extends HTMLElement { .then(() => { // may throw: this.selectAlternateOrChangeProjection(); + this.checkForPreferredContent(); }) .then(() => { this._layer = M.mapMLLayer(null, this, null, { @@ -277,9 +281,6 @@ export class MapLayer extends HTMLElement { resolve(); }) .catch((error) => { - if (error.message === 'changeprojection') { - console.log('Changing projection'); - } reject(error); }); } @@ -294,6 +295,13 @@ export class MapLayer extends HTMLElement { ); this.parentElement.projection = e.cause.mapprojection; } + } else if (e.message === 'findmatchingpreferredcontent') { + if (e.cause.href) { + console.log( + 'Changing layer to matching preferred content at: ' + e.cause.href + ); + this.src = e.cause.href; + } } else { console.log(e); this.dispatchEvent( @@ -333,6 +341,23 @@ export class MapLayer extends HTMLElement { } } + checkForPreferredContent() { + let mapml = this.src ? this.shadowRoot : this; + let availablePreferMapContents = mapml.querySelector( + `map-link[rel="style"][media="prefers-map-content=${this._renderingMapContent}"][href]` + ); + if (availablePreferMapContents) { + // resolve href + let url = new URL( + availablePreferMapContents.getAttribute('href'), + availablePreferMapContents.getBase() + ).href; + throw new Error('findmatchingpreferredcontent', { + cause: { href: url } + }); + } + } + copyRemoteContentToShadowRoot(mapml) { let shadowRoot = this.shadowRoot; // get the map-meta[name=projection/cs/extent/zoom] from map-head of remote mapml, attach them to the shadowroot diff --git a/src/map-link.js b/src/map-link.js index 26de9422e..6d3db5429 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -14,6 +14,9 @@ export class MapLink extends HTMLElement { 'projection' ]; } + /* jshint ignore:start */ + #hasConnected; + /* jshint ignore:end */ get type() { return this.getAttribute('type') || 'image/*'; } @@ -97,7 +100,10 @@ export class MapLink extends HTMLElement { } } get media() { - return this.getAttribute('media'); + // return the content of media attribute as an object + // maybe memoizing the object to avoid repeated formatting + // the Util function may need to be renamed? + return M._metaContentToObject(this.getAttribute('media')); } set media(val) { this.setAttribute('media', val); @@ -134,66 +140,68 @@ export class MapLink extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { //['type','rel','href','hreflang','tref','tms','projection']; // fold to lowercase - switch (name) { - case 'type': - // rel = tile, features, etc. TBD when it is used - // ttype = !t.hasAttribute('type') - // ? 'image/*' - // : t.getAttribute('type').toLowerCase(), + if (this.#hasConnected /* jshint ignore:line */) { + switch (name) { + case 'type': + // rel = tile, features, etc. TBD when it is used + // ttype = !t.hasAttribute('type') + // ? 'image/*' + // : t.getAttribute('type').toLowerCase(), - if (oldValue !== newValue) { - // default value image/* - // handle side effects - } - break; - case 'rel': - // mandatory attribute, no default value - if (oldValue !== newValue) { - // handle side effects - if (newValue === 'query') { + if (oldValue !== newValue) { + // default value image/* + // handle side effects } - } - break; - // case 'title': - // if (oldValue !== newValue) { - // // handle side effects - // } - // break; - case 'href': - // rel = license, legend, stylesheet, self, style, self style, style self, zoomin, zoomout - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'hreflang': - // rel = *all* - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#hreflang - // idea is that we can have multiple map-links with different hreflang, and map-extent chooses a map-link that matches with user's lang. Not a priority. - create an use-case issue? - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'tref': - // rel = tile, image, features, query - if (oldValue !== newValue) { - // create or reset the _templateVars property - this._initTemplateVars(); - } - break; - case 'media': - break; - case 'tms': - // rel = tile - if (oldValue !== newValue) { - // handle side effects - } - break; - case 'projection': - // rel = alternate - if (oldValue !== newValue) { - // handle side effects - } - break; + break; + case 'rel': + // mandatory attribute, no default value + if (oldValue !== newValue) { + // handle side effects + if (newValue === 'query') { + } + } + break; + // case 'title': + // if (oldValue !== newValue) { + // // handle side effects + // } + // break; + case 'href': + // rel = license, legend, stylesheet, self, style, self style, style self, zoomin, zoomout + if (oldValue !== newValue) { + // handle side effects + } + break; + case 'hreflang': + // rel = *all* + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#hreflang + // idea is that we can have multiple map-links with different hreflang, and map-extent chooses a map-link that matches with user's lang. Not a priority. - create an use-case issue? + if (oldValue !== newValue) { + // handle side effects + } + break; + case 'tref': + // rel = tile, image, features, query + if (oldValue !== newValue) { + // create or reset the _templateVars property + this._initTemplateVars(); + } + break; + case 'media': + break; + case 'tms': + // rel = tile + if (oldValue !== newValue) { + // handle side effects + } + break; + case 'projection': + // rel = alternate + if (oldValue !== newValue) { + // handle side effects + } + break; + } } } constructor() { @@ -201,12 +209,14 @@ export class MapLink extends HTMLElement { super(); } connectedCallback() { + this.#hasConnected = true; /* jshint ignore:line */ switch (this.rel.toLowerCase()) { // for some cases, require a dependency check case 'tile': case 'image': case 'features': case 'query': + this._initTemplateVars(); this._createTemplatedLink(); break; case 'style': @@ -708,7 +718,8 @@ export class MapLink extends HTMLElement { layerEl.dispatchEvent( new CustomEvent('changestyle', { detail: { - src: e.target.getAttribute('data-href') + src: e.target.getAttribute('data-href'), + preference: this.media['prefers-map-content'] } }) ); @@ -739,7 +750,7 @@ export class MapLink extends HTMLElement { styleOptionInput.checked = true; } this._styleOption = styleOption; - styleOptionInput.addEventListener('click', changeStyle); + styleOptionInput.addEventListener('click', changeStyle.bind(this)); } getLayerControlOption() { return this._styleOption; From 9a4daf1a2b92a2d476b2980e46ccd7e88d73066f Mon Sep 17 00:00:00 2001 From: Aliyan Haq <55751566+AliyanH@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:44:44 -0500 Subject: [PATCH 36/46] Convert MapMLLayer to a Layer group + map-feature independence * layer- mutation observer to initialize map-features * move validateLayerZoom from MapMLLayer -> layer-, and move to validateDisabled, removed from zoomAnim handler * remove zoom-in/zoom-out workaround for map projection change #2553 * remove map-feature's dependency on MapMLLayer, to create it's own layer independently * map-feature - rerender feature on min, max, zoom attributes * queryHandler no longer appends directly to the map, appends itself to mapmlvectors * for templatedFeatures - we move everything from the mapml file's head and body to the shadowroot of map-link * layer- always has a shadowRoot, but it is not always used - used based on src attribute * map-extent handleChange is not dependent on the parentLayer.checked --- src/layer.js | 124 +++++- src/map-extent.js | 13 +- src/map-feature.js | 492 ++++++++++----------- src/mapml-viewer.js | 4 - src/mapml/handlers/QueryHandler.js | 7 +- src/mapml/layers/FeatureLayer.js | 174 ++++---- src/mapml/layers/ImageLayer.js | 2 +- src/mapml/layers/MapMLLayer.js | 141 ++---- src/mapml/layers/TemplatedFeaturesLayer.js | 49 +- src/mapml/layers/TemplatedLayer.js | 1 - src/web-map.js | 4 - 11 files changed, 500 insertions(+), 511 deletions(-) diff --git a/src/layer.js b/src/layer.js index 1d059b651..51a2844f2 100644 --- a/src/layer.js +++ b/src/layer.js @@ -136,6 +136,9 @@ export class MapLayer extends HTMLElement { } _onRemove() { + if (this._observer) { + this._observer.disconnect(); + } let l = this._layer, lc = this._layerControl, lchtml = this._layerControlHTML; @@ -188,7 +191,7 @@ export class MapLayer extends HTMLElement { if (e.detail) { this._renderingMapContent = e.detail._renderingMapContent; this.src = e.detail.src; - } + } }, { once: true } ); @@ -240,18 +243,24 @@ export class MapLayer extends HTMLElement { this.checkForPreferredContent(); }) .then(() => { - this._layer = M.mapMLLayer( - new URL(this.src, base).href, - this, - this.shadowRoot, - { - projection: this.getProjection(), - opacity: this.opacity - } - ); + this._layer = M.mapMLLayer(new URL(this.src, base).href, this, { + projection: this.getProjection(), + opacity: this.opacity + }); this._createLayerControlHTML(); this._attachedToMap(); + // initializing map-features + this._runMutationObserver(this.shadowRoot.children); + this._bindMutationObserver(); this._validateDisabled(); + // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied + // to MapML extent as metadata + // Should always be fired at the end of initialization process + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event + // https://maps4html.org/web-map-doc/docs/api/layer-api#events + this.dispatchEvent( + new CustomEvent('loadedmetadata', { detail: { target: this } }) + ); resolve(); }) .catch((error) => { @@ -271,13 +280,23 @@ export class MapLayer extends HTMLElement { this.checkForPreferredContent(); }) .then(() => { - this._layer = M.mapMLLayer(null, this, null, { + this._layer = M.mapMLLayer(null, this, { projection: this.getProjection(), opacity: this.opacity }); this._createLayerControlHTML(); this._attachedToMap(); + this._runMutationObserver(this.children); + this._bindMutationObserver(); this._validateDisabled(); + // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied + // to MapML extent as metadata + // Should always be fired at the end of initialization process + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event + // https://maps4html.org/web-map-doc/docs/api/layer-api#events + this.dispatchEvent( + new CustomEvent('loadedmetadata', { detail: { target: this } }) + ); resolve(); }) .catch((error) => { @@ -404,6 +423,52 @@ export class MapLayer extends HTMLElement { } return projection; } + /* + * Runs the effects of the mutation observer, which is to add map-features' and + * map-extents' leaflet layer implementations to the appropriate container in + * the layer-._layer: either as a sub-layer directly in the L.LayerGroup + * (MapMLLayer._layer) or as a sub-layer in the MapMLLayer._mapmlvectors + * L.FeatureGroup + */ + _runMutationObserver(elementsGroup) { + const _addFeatureToMapMLVectors = (feature) => { + this.whenReady().then(() => { + feature.addFeature(this._layer._mapmlvectors); + }); + }; + for (let i = 0; i < elementsGroup.length; ++i) { + let element = elementsGroup[i]; + switch (element.nodeName) { + case 'MAP-FEATURE': + _addFeatureToMapMLVectors(element); + break; + case 'MAP-EXTENT': + // if extent is checked, ... + // TODO: Static Tiles + default: + break; + } + } + } + /* + * Set up a function to watch additions of child elements of layer- or + * layer-.shadowRoot and to invoke desired side effects of those additions + * via _runMutationObserver + */ + _bindMutationObserver() { + // mutation observer + this._observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + // the attributes changes should be handled by attributeChangedCallback() + if (mutation.type === 'childList') { + this._runMutationObserver(mutation.addedNodes); + } + } + }); + this._observer.observe(this.src ? this.shadowRoot : this, { + childList: true + }); + } _attachedToMap() { // set i to the position of this layer element in the set of layers var i = 0, @@ -459,12 +524,6 @@ export class MapLayer extends HTMLElement { } ]; } - // re-use 'loadedmetadata' event from HTMLMediaElement inteface, applied - // to MapML extent as metadata - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event - this.dispatchEvent( - new CustomEvent('loadedmetadata', { detail: { target: this } }) - ); } _validateDisabled() { @@ -474,6 +533,7 @@ export class MapLayer extends HTMLElement { let layer = this._layer, map = layer?._map; if (map) { + this._validateLayerZoom({ zoom: map.getZoom() }); // prerequisite: no inline and remote mapml elements exists at the same time const mapExtents = this.src ? this.shadowRoot.querySelectorAll('map-extent') @@ -526,7 +586,35 @@ export class MapLayer extends HTMLElement { } }, 0); } - + _validateLayerZoom(e) { + // get the min and max zooms from all extents + let toZoom = e.zoom; + let min = this.extent.zoom.minZoom; + let max = this.extent.zoom.maxZoom; + let inLink = this.src + ? this.shadowRoot.querySelector('map-link[rel=zoomin]') + : this.querySelector('map-link[rel=zoomin]'), + outLink = this.src + ? this.shadowRoot.querySelector('map-link[rel=zoomout]') + : this.querySelector('map-link[rel=zoomout]'); + let targetURL; + if (!(min <= toZoom && toZoom <= max)) { + if (inLink && toZoom > max) { + targetURL = inLink.href; + } else if (outLink && toZoom < min) { + targetURL = outLink.href; + } + if (targetURL) { + this.dispatchEvent( + new CustomEvent('zoomchangesrc', { + detail: { + href: targetURL + } + }) + ); + } + } + } // disable/italicize layer control elements based on the layer-.disabled property toggleLayerControlDisabled() { let input = this._layerControlCheckbox, diff --git a/src/map-extent.js b/src/map-extent.js index 65ca2ff9a..7f50ebe97 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -267,16 +267,17 @@ export class MapExtent extends HTMLElement { } _handleChange() { - // if the parent layer- is checked, add _extentLayer to map if map-extent is checked, otherwise remove it - if (this.checked && this.parentLayer.checked && !this.disabled) { - this._extentLayer.addTo(this._map); + // add _extentLayer to map if map-extent is checked, otherwise remove it + if (this.checked && !this.disabled) { + // can be added to mapmllayer layerGroup no matter layer- is checked or not + this._extentLayer.addTo(this.parentLayer._layer); this._extentLayer.setZIndex( Array.from(this.parentLayer.querySelectorAll('map-extent')).indexOf( this ) ); } else { - this._map.removeLayer(this._extentLayer); + this.parentLayer._layer.removeLayer(this._extentLayer); } // change the checkbox in the layer control to match map-extent.checked // doesn't trigger the event handler because it's not user-caused AFAICT @@ -309,7 +310,9 @@ export class MapExtent extends HTMLElement { // remove layer control for map-extent from layer control DOM // TODO: for the case of projection change, the layer control for map-extent has been created while _extentLayer has not yet been ready this._layerControlHTML.remove(); - this._map.removeLayer(this._extentLayer); + if (this.parentLayer._layer) { + this.parentLayer._layer.removeLayer(this._extentLayer); + } this.parentLayer.removeEventListener('map-change', this._changeHandler); this.mapEl.removeEventListener('map-projectionchange', this._changeHandler); delete this._extentLayer; diff --git a/src/map-feature.js b/src/map-feature.js index 1f0684e2c..19da81055 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -3,6 +3,9 @@ export class MapFeature extends HTMLElement { return ['zoom', 'min', 'max']; } + /* jshint ignore:start */ + #hasConnected; + /* jshint ignore:end */ get zoom() { return +(this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0); } @@ -18,19 +21,20 @@ export class MapFeature extends HTMLElement { // fallback: the minimum zoom bound of layer- element return +(this.hasAttribute('min') ? this.getAttribute('min') - : this._layer._layerEl.extent.zoom.minZoom); + : this.getLayerEl().extent.zoom.minZoom); } set min(val) { var parsedVal = parseInt(val, 10); + var layerZoomBounds = this.getLayerEl().extent.zoom; if (!isNaN(parsedVal)) { if ( - parsedVal >= this._layer._layerEl.extent.zoom.minZoom && - parsedVal <= this._layer._layerEl.extent.zoom.maxZoom + parsedVal >= layerZoomBounds.minZoom && + parsedVal <= layerZoomBounds.maxZoom ) { this.setAttribute('min', parsedVal); } else { - this.setAttribute('min', this._layer._layerEl.extent.zoom.minZoom); + this.setAttribute('min', layerZoomBounds.minZoom); } } } @@ -39,19 +43,20 @@ export class MapFeature extends HTMLElement { // fallback: the maximum zoom bound of layer- element return +(this.hasAttribute('max') ? this.getAttribute('max') - : this._layer._layerEl.extent.zoom.maxZoom); + : this.getLayerEl().extent.zoom.maxZoom); } set max(val) { var parsedVal = parseInt(val, 10); + var layerZoomBounds = this.getLayerEl().extent.zoom; if (!isNaN(parsedVal)) { if ( - parsedVal >= this._layer._layerEl.extent.zoom.minZoom && - parsedVal <= this._layer._layerEl.extent.zoom.maxZoom + parsedVal >= layerZoomBounds.minZoom && + parsedVal <= layerZoomBounds.maxZoom ) { this.setAttribute('max', parsedVal); } else { - this.setAttribute('max', this._layer._layerEl.extent.zoom.maxZoom); + this.setAttribute('max', layerZoomBounds.maxZoom); } } } @@ -66,30 +71,51 @@ export class MapFeature extends HTMLElement { return this._getFeatureExtent(); } } + getLayerEl() { + let layerEl; + if (this.getRootNode() instanceof ShadowRoot) { + if (this.getRootNode().host.getRootNode() instanceof ShadowRoot) { + // layer- src + // > sd + // map-extent + // map-link + // > sd + // map-feature (1) + layerEl = this.getRootNode().host.getRootNode().host; + } else if (this.getRootNode().host.nodeName === 'MAP-LINK') { + // layer- + // map-extent + // map-link + // > sd + // map-feature (4) + layerEl = this.getRootNode().host.closest('layer-'); + } else { + // layer- src + // > sd + // map-feature (2) + layerEl = this.getRootNode().host; + } + } else { + // layer- + // map-feature (3) + layerEl = this.closest('layer-'); + } + return layerEl; + } + getMapEl() { + return this.getLayerEl().closest('mapml-viewer,map[is=web-map]'); + } attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case 'zoom': { - if (oldValue !== newValue && this._layer) { - let layer = this._layer, - zoom = newValue, - mapmlvectors = layer._mapmlvectors; - // if the vector layer only has static features, should update zoom bounds when zoom attribute is changed - if (mapmlvectors._staticFeature) { - this._removeInFeatureList(oldValue); - if (zoom in mapmlvectors._features) { - mapmlvectors._features[zoom].push(this._featureGroup); - } else { - mapmlvectors._features[zoom] = [this._featureGroup]; - } - let native = this._getNativeZoomAndCS(layer._content); - mapmlvectors.zoomBounds = M.getZoomBounds( - layer._content, - native.zoom - ); + if (this.#hasConnected /* jshint ignore:line */) { + switch (name) { + case 'min': + case 'max': + case 'zoom': + if (oldValue !== newValue) { + this.reRender(this._featureLayer); } - } - break; + break; } } } @@ -100,156 +126,132 @@ export class MapFeature extends HTMLElement { } connectedCallback() { - // if the features are connected to the remote mapml - if (this.closest('mapml-')) return; + /* jshint ignore:start */ + this.#hasConnected = true; + /* jshint ignore:end */ this._parentEl = this.parentNode.nodeName.toUpperCase() === 'LAYER-' || this.parentNode.nodeName.toUpperCase() === 'MAP-LINK' ? this.parentNode : this.parentNode.host; - this._parentEl.whenReady().then(() => { - this._layer = this._parentEl._layer || this._parentEl.parentExtent._layer; - delete this._parentEl.bounds; - if ( - this._layer._layerEl.hasAttribute('data-moving') || - this._parentEl.hasAttribute('data-moving') - ) - return; - // if mapFeature element is not connected to layer- or layer-'s shadowroot, - // or the parent layer- element has a "data-moving" attribute - if ( - (this.parentNode.nodeType !== document.DOCUMENT_FRAGMENT_NODE && - this.parentNode.nodeName.toLowerCase() !== 'layer-') || - (this.parentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE && - this.parentNode.host.hasAttribute('data-moving')) || - (this.parentNode.nodeName.toLowerCase() === 'layer-' && - this.parentNode.hasAttribute('data-moving')) - ) { - return; - } - // set up the map-feature object properties - this._addFeature(); - // use observer to monitor the changes in mapFeature's subtree - // (i.e. map-properties, map-featurecaption, map-coordinates) - this._observer = new MutationObserver((mutationList) => { - for (let mutation of mutationList) { - // the attributes changes of element should be handled by attributeChangedCallback() - if (mutation.type === 'attributes' && mutation.target === this) { - return; - } - // re-render feature if there is any observed change - this._reRender(); - delete this._parentEl.bounds; + if ( + this.getLayerEl().hasAttribute('data-moving') || + this._parentEl.parentElement.hasAttribute('data-moving') + ) + return; + // use observer to monitor the changes in mapFeature's subtree + // (i.e. map-properties, map-featurecaption, map-coordinates) + this._observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + // the attributes changes of element should be handled by attributeChangedCallback() + if (mutation.type === 'attributes' && mutation.target === this) { + return; } - }); - this._observer.observe(this, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - characterData: true - }); + // re-render feature if there is any observed change + this.reRender(this._featureLayer); + // TODO: move this code to layer- mutation observer + // delete this._parentEl.bounds; + } + }); + this._observer.observe(this, { + childList: true, + subtree: true, + attributes: true, + attributeOldValue: true, + characterData: true }); } disconnectedCallback() { - if (!this._layer) return; - if (this._layer._layerEl.hasAttribute('data-moving')) return; - this._removeFeature(); + if (!this.getLayerEl()?.hasAttribute('data-moving')) return; + this.removeFeature(this._featureLayer); this._observer.disconnect(); - delete this._parentEl.bounds; } - _reRender() { + reRender(layerToRenderOn) { + // this is for re-generating the rendering in case of changes + // based on mutation observers. Kinda brute force. if (this._groupEl.isConnected) { - let native = this._getNativeZoomAndCS(this._layer._content); + let fallbackZoom = this._getFallbackZoom(); + let fallbackCS = this._getFallbackCS(); let placeholder = document.createElement('span'); this._groupEl.insertAdjacentElement('beforebegin', placeholder); - - this._featureGroup._map.removeLayer(this._featureGroup); + if (layerToRenderOn._staticFeature) { + layerToRenderOn._removeFromFeaturesList(this._geometry); + } + layerToRenderOn.removeLayer(this._geometry); // Garbage collection needed - this._featureGroup = this._layer._mapmlvectors - .addData(this, native.cs, native.zoom) - .addTo(this._map); - placeholder.replaceWith(this._featureGroup.options.group); + this._geometry = layerToRenderOn + .addData(this, fallbackCS, fallbackZoom) // side effect: this._groupEl set + .addTo(layerToRenderOn); + placeholder.replaceWith(this._geometry.options.group); + layerToRenderOn._resetFeatures(); // TODO: getBounds() should dynamically update the layerBounds and zoomBounds delete this._getFeatureExtent; this._setUpEvents(); } } - _removeFeature() { - // if the el is disconnected - // the el has already got removed at this point - if (this._groupEl?.isConnected) { - this._groupEl.remove(); - } - // if the el has already been disconnected, - // then _map.removeLayer(layerEl._layer) has already been invoked (inside layerEl.disconnectedCallback()) - // this._featureGroup has already got removed at this point - if (this._featureGroup?._map) { - this._featureGroup._map.removeLayer(this._featureGroup); - let mapmlvectors = this._layer._mapmlvectors; - if (mapmlvectors) { - if (mapmlvectors._staticFeature) { - if (mapmlvectors._features[this.zoom]) { - this._removeInFeatureList(this.zoom); - } - // update zoom bounds of vector layer - mapmlvectors.zoomBounds = M.getZoomBounds( - this._layer._content, - this._getNativeZoomAndCS(this._layer._content).zoom - ); - } - mapmlvectors.options.properties = null; - delete mapmlvectors._layers[this._featureGroup._leaflet_id]; - } + removeFeature(layerToRemoveFrom) { + // layerToRemoveFrom is the L.LayerGroup or L.FeatureGroup to remove this + // feature from... + layerToRemoveFrom.removeLayer(this._geometry); + // TODO: MOVE THIS LOGIC TO layerToRemoveFrom.removeLayer(M.Geometry) + // if (layerToRemoveFrom._staticFeature) { + // if (layerToRemoveFrom._features[this.zoom]) { + // this._removeInFeatureList(this.zoom); + // } + // + // TODO: update layerToRemoveFrom.removeLayer(...) to update its own + // zoomBounds + // // update zoom bounds of vector layer + // layerToRemoveFrom.zoomBounds = M.getZoomBounds( + // this._layer._content, + // this._getNativeZoomAndCS(this._layer._content).zoom + // ); + // } + if (layerToRemoveFrom._staticFeature) { + layerToRemoveFrom._removeFromFeaturesList(this._geometry); } - delete this._featureGroup; - delete this._groupEl; + layerToRemoveFrom.options.properties = null; + layerToRemoveFrom.removeLayer(this._geometry); + delete this._geometry; // ensure that feature extent can be re-calculated everytime that map-feature element is updated / re-added if (this._getFeatureExtent) delete this._getFeatureExtent; } - _addFeature() { - this._parentEl.whenReady().then(() => { - let parentLayer = - this._parentEl.nodeName.toUpperCase() === 'LAYER-' - ? this._parentEl - : this._parentEl.parentElement || this._parentEl.parentNode.host; - this._map = this._layer._map; - let mapmlvectors = this._layer._mapmlvectors; - // "synchronize" the event handlers between map-feature and - if (!this.querySelector('map-geometry')) return; - if (!this._linkEl) { - let native = this._getNativeZoomAndCS(this._layer._content); - this._featureGroup = mapmlvectors.addData(this, native.cs, native.zoom); - if (parentLayer.checked) { - this._featureGroup.addTo(this._map); - } - mapmlvectors._layers[this._featureGroup._leaflet_id] = - this._featureGroup; - if (mapmlvectors._staticFeature && !this._linkEl) { - // update zoom bounds of vector layer - mapmlvectors.zoomBounds = M.getZoomBounds( - this._layer._content, - this._getNativeZoomAndCS(this._layer._content).zoom - ); - // todo: dynamically update layer bounds of vector layer - mapmlvectors.layerBounds = M.getBounds(this._layer._content); - // update map's zoom limit - // the mapmlvectors.options should be updated with the new zoomBounds, - // to ensure the _addZoomLimit function call can read updated zoom info - // and update map zoom limit properly - L.extend(mapmlvectors.options, mapmlvectors.zoomBounds); - this._map._addZoomLimit(mapmlvectors); - // TODO: can be set as a handler of featureLayer - mapmlvectors._resetFeatures(); - } - } + addFeature(layerToAddTo) { + this._featureLayer = layerToAddTo; + let parentLayer = this.getLayerEl(); + // "synchronize" the event handlers between map-feature and + if (!this.querySelector('map-geometry')) return; + let fallbackZoom = this._getFallbackZoom(); + let fallbackCS = this._getFallbackCS(); + let content = parentLayer.src ? parentLayer.shadowRoot : parentLayer; + this._geometry = layerToAddTo + .addData(this, fallbackCS, fallbackZoom) // side effect: extends `this` with this._groupEl, points to svg g element that renders to map SD + .addTo(layerToAddTo); + layerToAddTo._layers[this._geometry._leaflet_id] = this._geometry; + if (layerToAddTo._staticFeature && this._parentEl.nodeName !== 'MAP-LINK') { + // update zoom bounds of vector layer + layerToAddTo.zoomBounds = M.getZoomBounds( + content, + // this._getNativeZoomAndCS().zoom + fallbackZoom + ); + // todo: dynamically update layer bounds of vector layer + layerToAddTo.layerBounds = M.getBounds(content); + // update map's zoom limit + // the mapmlvectors.options should be updated with the new zoomBounds, + // to ensure the _addZoomLimit function call can read updated zoom info + // and update map zoom limit properly + L.extend(layerToAddTo.options, layerToAddTo.zoomBounds); + // this._map._addZoomLimit(mapmlvectors); + // TODO: can be set as a handler of featureLayer + layerToAddTo._resetFeatures(); + } - this._setUpEvents(); - }); + this._setUpEvents(); } _setUpEvents() { @@ -276,56 +278,49 @@ export class MapFeature extends HTMLElement { }); } - _getNativeZoomAndCS(content) { - // content: referred to if the has inline , or - // referred to remote mapml if the has a src attribute, and the fetched mapml contains - // referred to [map-meta, ...] if it is query - // referred to null otherwise (i.e. has fetched in shadow, the attaches to 's shadow) - let nativeZoom, nativeCS; - if (this._linkEl) { - // feature attaches to extent's shadow - let metaZoom, metaCS; - if (this._linkEl.rel === 'query') { - // TODO migrate remote content into shadow root of linkEl... - // for query, fallback zoom is the current map zoom level that the query is returned - if (content) { - metaZoom = M._metaContentToObject( - Array.prototype.filter - .call(content, function (elem) { - return elem.matches('map-meta[name=zoom]'); - })[0] - ?.getAttribute('content') - ).content; - metaCS = M._metaContentToObject( - Array.prototype.filter - .call(content, function (elem) { - return elem.matches('map-meta[name=cs]'); - })[0] - ?.getAttribute('content') - ).content; - } - nativeZoom = metaZoom || this._map.getZoom(); - nativeCS = metaCS || 'gcrs'; - } else if (this._linkEl.rel === 'features') { - // for templated feature, read fallback from the fetched mapml's map-meta[name=zoom / cs] - nativeZoom = this._map.getZoom(); - nativeCS = 'gcrs'; - if (content) { - metaZoom = M._metaContentToObject( - content - .querySelector('map-meta[name=zoom]') - ?.getAttribute('content') - ); - metaCS = M._metaContentToObject( - content.querySelector('map-meta[name=cs]')?.getAttribute('content') - ).content; - } - nativeZoom = metaZoom || this._map.getZoom(); - nativeCS = metaCS || 'gcrs'; - } - return { zoom: nativeZoom, cs: nativeCS }; + // native zoom: used by FeatureLayer.addData(...), + // the fallback zoom for map-feature if its zoom attribute is not specified + // for query and templated features, fallback zoom is the current map zoom level + // for static features, fallback zoom is map-meta[name="zoom"] value in layer- || the current map zoom level + _getFallbackZoom() { + if (this._parentEl.nodeName === 'MAP-LINK') { + // feature attaches to link's shadow + return this.getMapEl().zoom; + } else { + // feature attaches to the layer- / layer-'s shadow + let layerEl = this.getLayerEl(); + let zoomMeta = layerEl.src + ? layerEl.shadowRoot.querySelector('map-meta[name=zoom]') + : layerEl.querySelector('map-meta[name=zoom]'); + return ( + M._metaContentToObject(zoomMeta?.getAttribute('content'))?.value || + this.getMapEl().zoom + ); + } + } + + // native cs: used by FeatureLayer.geometryToLayer(...), + // the fallback cs for map-geometry if its cs attribute is not specified + _getFallbackCS() { + let csMeta; + if (this._parentEl.nodeName === 'MAP-LINK') { + // feature attaches to link's shadow + csMeta = + this._parentEl.shadowRoot.querySelector('map-meta[name=cs]') || + this._parentEl.parentElement.getMeta('cs'); + } else { + let layerEl = this.getLayerEl(); + csMeta = layerEl.src + ? layerEl.shadowRoot.querySelector('map-meta[name=cs]') + : layerEl.querySelector('map-meta[name=cs]'); + } + if (csMeta) { + // M._metaContentObject("gcrs") -> {content: "gcrs"} + return ( + M._metaContentToObject(csMeta.getAttribute('content')).content || 'gcrs' + ); } else { - return M.getNativeVariables(content); + return 'gcrs'; } } @@ -342,16 +337,11 @@ export class MapFeature extends HTMLElement { return extentCache; } else { // calculate feature extent - let map = this._map, + let map = this.getMapEl()._map, geometry = this.querySelector('map-geometry'), - native = this._getNativeZoomAndCS( - this._layer.queryMetas?.length - ? this._layer.queryMetas - : this._layer._content - ), - cs = geometry.getAttribute('cs') || native.cs, + cs = geometry.getAttribute('cs') || this._getFallbackCS(), // zoom level that the feature rendered at - zoom = this.zoom || native.zoom, + zoom = this.zoom || this._getFallbackZoom(), shapes = geometry.querySelectorAll( 'map-point, map-linestring, map-polygon, map-multipoint, map-multilinestring' ), @@ -433,23 +423,6 @@ export class MapFeature extends HTMLElement { } } - // find and remove the feature from mapmlvectors._features if vector layer only contains static features, helper function - // prevent it from being rendered again when zooming in / out (mapmlvectors.resetFeature() is invoked) - // TODO: Can be moved to FeatureLayer.js, pass in leaflet id for layer to remove - _removeInFeatureList(zoom) { - if (zoom === null) { - return; - } - let mapmlvectors = this._layer._mapmlvectors; - for (let i = 0; i < mapmlvectors._features[zoom].length; ++i) { - let feature = mapmlvectors._features[zoom][i]; - if (feature._leaflet_id === this._featureGroup._leaflet_id) { - mapmlvectors._features[zoom].splice(i, 1); - break; - } - } - } - getMaxZoom() { let tL = this.extent.topLeft.pcrs, bR = this.extent.bottomRight.pcrs, @@ -457,8 +430,8 @@ export class MapFeature extends HTMLElement { L.point(tL.horizontal, tL.vertical), L.point(bR.horizontal, bR.vertical) ); - let projection = this._map.options.projection, - layerZoomBounds = this._layer._layerEl.extent.zoom, + let projection = this.getMapEl()._map.options.projection, + layerZoomBounds = this.getLayerEl().extent.zoom, minZoom = layerZoomBounds.minZoom ? layerZoomBounds.minZoom : 0, maxZoom = layerZoomBounds.maxZoom ? layerZoomBounds.maxZoom @@ -469,7 +442,7 @@ export class MapFeature extends HTMLElement { newZoom = this.zoom; } else { // if not, calculate the maximum zoom level that can show the feature completely - newZoom = M.getMaxZoom(bound, this._map, minZoom, maxZoom); + newZoom = M.getMaxZoom(bound, this.getMapEl()._map, minZoom, maxZoom); if (this.max < newZoom) { // if the calculated zoom is greater than the value of max zoom attribute, go with max zoom attribute newZoom = this.max; @@ -525,13 +498,14 @@ export class MapFeature extends HTMLElement { // transform to gcrs if options.transform = true (default) let source = null, - dest = null; + dest = null, + map = this.getMapEl()._map; if (options.transform) { - source = new proj4.Proj(this._map.options.crs.code); + source = new proj4.Proj(map.options.crs.code); dest = new proj4.Proj('EPSG:4326'); if ( - this._map.options.crs.code === 'EPSG:3857' || - this._map.options.crs.code === 'EPSG:4326' + map.options.crs.code === 'EPSG:3857' || + map.options.crs.code === 'EPSG:4326' ) { options.transform = false; } @@ -575,8 +549,8 @@ export class MapFeature extends HTMLElement { let properties = this.querySelector('map-properties'); if (g.getAttribute('role') === 'link') { for (let path of g.children) { - path.mousedown.call(this._featureGroup, event); - path.mouseup.call(this._featureGroup, event); + path.mousedown.call(this._geometry, event); + path.mouseup.call(this._geometry, event); } } // dispatch click event for map-feature to allow events entered by 'addEventListener' @@ -586,19 +560,19 @@ export class MapFeature extends HTMLElement { // for custom projection, layer- element may disconnect and re-attach to the map after the click // so check whether map-feature element is still connected before any further operations if (properties && this.isConnected) { - let featureGroup = this._featureGroup, - shapes = featureGroup._layers; + let geometry = this._geometry, + shapes = geometry._layers; // close popup if the popup is currently open for (let id in shapes) { if (shapes[id].isPopupOpen()) { shapes[id].closePopup(); } } - if (featureGroup.isPopupOpen()) { - featureGroup.closePopup(); + if (geometry.isPopupOpen()) { + geometry.closePopup(); } else if (!clickEv.originalEvent.cancelBubble) { // If stopPropagation is not set on originalEvent by user - featureGroup.openPopup(); + geometry.openPopup(); } } } @@ -619,14 +593,14 @@ export class MapFeature extends HTMLElement { ) { this._groupEl.blur(); // set focus to the map container - this._map._container.focus(); + this.getMapEl()._map.getContainer().focus(); } } // a method that can the viewport to be centred on the feature's extent zoomTo() { let extent = this.extent, - map = this._map; + map = this.getMapEl()._map; let tL = extent.topLeft.pcrs, bR = extent.bottomRight.pcrs, bound = L.bounds( @@ -637,29 +611,27 @@ export class MapFeature extends HTMLElement { map.setView(center, this.getMaxZoom(), { animate: false }); } whenReady() { - return this._parentEl.whenReady().then(() => { - 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() { + return new Promise((resolve, reject) => { + let interval, failureTimer; + if (this.isConnected /* jshint ignore:line */) { + resolve(); + } else { + let featureElement = this; + interval = setInterval(testForFeature, 200, featureElement); + failureTimer = setTimeout(featureNotDefined, 5000); + } + function testForFeature(featureElement) { + if (featureElement.isConnected /* jshint ignore:line */) { clearInterval(interval); clearTimeout(failureTimer); - reject('Timeout reached waiting for feature to be ready'); + 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 e4e787705..e05b094a0 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -395,10 +395,6 @@ export class MapViewer extends HTMLElement { const connect = reconnectLayers.bind(this); connect().then(() => { if (this._map && this._map.options.projection !== oldValue) { - // this awful hack is brought to you by a leaflet bug/ feature request - // https://github.com/Leaflet/Leaflet/issues/2553 - this.zoomTo(this.lat, this.lon, this.zoom + 1); - this.zoomTo(this.lat, this.lon, this.zoom - 1); // this doesn't completely work either this._resetHistory(); } diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index d9f2b45e6..3b09ba5d0 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -251,7 +251,7 @@ export var QueryHandler = L.Handler.extend({ for (let f of results) { if (f.status === 'fulfilled') { - // create connection between queried and its parent + // create connection between queried and its parent for (let feature of f.value.features) { feature._linkEl = f.value.template.linkEl; } @@ -281,7 +281,8 @@ export var QueryHandler = L.Handler.extend({ static: true, mapEl: layer.options.mapEl }); - f.addTo(map); + + f.addTo(layer); let div = L.DomUtil.create('div', 'mapml-popup-content'), c = L.DomUtil.create('iframe'); @@ -296,7 +297,7 @@ export var QueryHandler = L.Handler.extend({ layer._totalFeatureCount = features.length; layer.bindPopup(div, popupOptions).openPopup(loc); layer.on('popupclose', function () { - map.removeLayer(f); + layer.removeLayer(f); }); f.showPaginationFeature({ i: 0, diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 523274431..c6927e49b 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -17,27 +17,53 @@ export var FeatureLayer = L.FeatureGroup.extend({ // which will then remain static for the lifetime of the layer L.FeatureGroup.prototype.initialize.call(this, null, options); + // this.options.static is false ONLY for tiled vector features + // this._staticFeature is ONLY true when not used by TemplatedFeaturesLayer + // this.options.query true when created by QueryHandler.js + if (this.options.static) { - this._container = L.DomUtil.create( - 'div', - 'leaflet-layer', - this.options.pane - ); - // must have leaflet-pane class because of new/changed rule in leaflet.css - // info: https://github.com/Leaflet/Leaflet/pull/4597 - L.DomUtil.addClass( - this._container, - 'leaflet-pane mapml-vector-container' - ); + // not a tiled vector layer + this._container = null; + if (this.options.query) { + this._container = L.DomUtil.create( + 'div', + 'leaflet-layer', + this.options.pane + ); + // must have leaflet-pane class because of new/changed rule in leaflet.css + // info: https://github.com/Leaflet/Leaflet/pull/4597 + L.DomUtil.addClass( + this._container, + 'leaflet-pane mapml-vector-container' + ); + } else if (this.options._leafletLayer) { + this._container = L.DomUtil.create( + 'div', + 'leaflet-layer', + this.options.pane + ); + L.DomUtil.addClass( + this._container, + 'leaflet-pane mapml-vector-container' + ); + // static mapmlvector should always at the top + // this._container.style.zIndex = 1000; + } else { + // if the current featureLayer is a sublayer of templatedFeatureLayer, + // append directly to the templated feature container (passed in as options.pane) + this._container = this.options.pane; + L.DomUtil.addClass( + this._container, + 'leaflet-pane mapml-vector-container' + ); + } L.setOptions(this.options.renderer, { pane: this._container }); } if (this.options.query) { this._mapmlFeatures = mapml.features ? mapml.features : mapml; - let native = M.getNativeVariables(mapml); - this.options.nativeZoom = native.zoom; - this.options.nativeCS = native.cs; } else { if (mapml) { + // tileFeatures let native = M.getNativeVariables(mapml); this.addData(mapml, native.cs, native.zoom); } else if (!mapml) { @@ -104,11 +130,20 @@ export var FeatureLayer = L.FeatureGroup.extend({ this._map.featureIndex.cleanIndex(); }, - removeLayer: function (featureGroupLayer) { - L.FeatureGroup.prototype.removeLayer.call(this, featureGroupLayer); - delete this._layers[featureGroupLayer._leaflet_id]; + removeLayer: function (featureToRemove) { + L.FeatureGroup.prototype.removeLayer.call(this, featureToRemove); + delete this._layers[featureToRemove._leaflet_id]; + }, + _removeFromFeaturesList: function (layer) { + for (let zoom in this._features) + for (let i = 0; i < this._features[zoom].length; ++i) { + let feature = this._features[zoom][i]; + if (feature._leaflet_id === layer._leaflet_id) { + this._features[zoom].splice(i, 1); + break; + } + } }, - getEvents: function () { if (this._staticFeature) { return { @@ -132,11 +167,6 @@ export var FeatureLayer = L.FeatureGroup.extend({ feature._linkEl.shadowRoot.replaceChildren(); } this.clearLayers(); - feature._featureGroup = this.addData( - feature, - this.options.nativeCS, - this.options.nativeZoom - ); // append all map-meta from mapml document if (e.meta) { for (let i = 0; i < e.meta.length; i++) { @@ -144,6 +174,9 @@ export var FeatureLayer = L.FeatureGroup.extend({ } } feature._linkEl.shadowRoot.appendChild(feature); + let fallbackZoom = feature._getFallbackZoom(); + let fallbackCS = feature._getFallbackCS(); + feature._geometry = this.addData(feature, fallbackCS, fallbackZoom); e.popup._navigationBar.querySelector('p').innerText = e.i + 1 + '/' + this.options._leafletLayer._totalFeatureCount; e.popup._content @@ -230,102 +263,61 @@ export var FeatureLayer = L.FeatureGroup.extend({ } }, - addData: function (mapml, nativeCS, nativeZoom) { - var features = - mapml.nodeType === Node.DOCUMENT_NODE || mapml.nodeName === 'LAYER-' - ? mapml.getElementsByTagName('map-feature') - : null, - i, - len, - feature; - - var linkedStylesheets = - mapml.nodeType === Node.DOCUMENT_NODE - ? mapml.querySelector('map-link[rel=stylesheet],map-style') - : null; - if (linkedStylesheets) { - var base = - mapml.querySelector('map-base') && - mapml.querySelector('map-base').hasAttribute('href') - ? new URL(mapml.querySelector('map-base').getAttribute('href')).href - : mapml.URL; - M._parseStylesheetAsHTML(mapml, base, this._container); - } - if (features) { - for (i = 0, len = features.length; i < len; i++) { - // Only add this if geometry is set and not null - feature = features[i]; - var geometriesExist = - feature.getElementsByTagName('map-geometry').length && - feature.getElementsByTagName('map-coordinates').length; - if (geometriesExist) { - if (mapml.nodeType === Node.DOCUMENT_NODE) { - // if the element has migrated from mapml file, - // the featureGroup object should bind with the **CLONED** map-feature element in DOM instead of the feature in mapml - if (!feature._DOMnode) feature._DOMnode = feature.cloneNode(true); - feature._DOMnode._featureGroup = this.addData( - feature._DOMnode, - nativeCS, - nativeZoom - ); - } else { - feature._featureGroup = this.addData(feature, nativeCS, nativeZoom); - } - } - } - return this; //if templated this runs - } - + addData: function (feature, fallbackCS, fallbackZoom) { //if its a mapml with no more links this runs var options = this.options; - if (options.filter && !options.filter(mapml)) { + if (options.filter && !options.filter(feature)) { return; } - if (mapml.classList.length) { - options.className = mapml.classList.value; + if (feature.classList.length) { + options.className = feature.classList.value; } - let zoom = mapml.getAttribute('zoom') || nativeZoom, - title = mapml.querySelector('map-featurecaption'); + let zoom = feature.getAttribute('zoom') || fallbackZoom, + title = feature.querySelector('map-featurecaption'); title = title ? title.innerHTML : 'Feature'; - if (mapml.querySelector('map-properties')) { + if (feature.querySelector('map-properties')) { options.properties = document.createElement('div'); options.properties.classList.add('mapml-popup-content'); options.properties.insertAdjacentHTML( 'afterbegin', - mapml.querySelector('map-properties').innerHTML + feature.querySelector('map-properties').innerHTML ); } - let layer = this.geometryToLayer(mapml, options, nativeCS, +zoom, title); - if (layer) { + let geometry = this.geometryToLayer( + feature, + options, + fallbackCS, + +zoom, + title + ); + if (geometry) { // if the layer is being used as a query handler output, it will have // a color option set. Otherwise, copy classes from the feature - if (!layer.options.color && mapml.hasAttribute('class')) { - layer.options.className = mapml.getAttribute('class'); + if (!geometry.options.color && feature.hasAttribute('class')) { + geometry.options.className = feature.getAttribute('class'); } - layer.defaultOptions = layer.options; - this.resetStyle(layer); + geometry.defaultOptions = geometry.options; + this.resetStyle(geometry); if (options.onEachFeature) { - layer.bindTooltip(title, { interactive: true, sticky: true }); + geometry.bindTooltip(title, { interactive: true, sticky: true }); } if (this._staticFeature) { - let featureZoom = mapml.getAttribute('zoom') || nativeZoom; + let featureZoom = feature.getAttribute('zoom') || fallbackZoom; if (featureZoom in this._features) { - this._features[featureZoom].push(layer); + this._features[featureZoom].push(geometry); } else { - this._features[featureZoom] = [layer]; + this._features[featureZoom] = [geometry]; } - } else { - this.addLayer(layer); } - if (mapml.tagName.toUpperCase() === 'MAP-FEATURE') { - mapml._groupEl = layer.options.group; + if (feature.tagName.toUpperCase() === 'MAP-FEATURE') { + feature._groupEl = geometry.options.group; } - return layer; + return geometry; } }, diff --git a/src/mapml/layers/ImageLayer.js b/src/mapml/layers/ImageLayer.js index 28787200f..31c5aed28 100644 --- a/src/mapml/layers/ImageLayer.js +++ b/src/mapml/layers/ImageLayer.js @@ -25,7 +25,7 @@ export var ImageLayer = L.ImageOverlay.extend({ return events; }, - onAdd: function () { + onAdd: function (map) { this.on({ load: this._onImageLoad }); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 005eb9abb..76c9f59a6 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -1,4 +1,4 @@ -export var MapMLLayer = L.Layer.extend({ +export var MapMLLayer = L.LayerGroup.extend({ // zIndex has to be set, for the case where the layer is added to the // map before the layercontrol is used to control it (where autoZindex is used) // e.g. in the raw MapML-Leaflet-Client index.html page. @@ -9,18 +9,15 @@ export var MapMLLayer = L.Layer.extend({ opacity: '1.0' }, // initialize is executed before the layer is added to a map - initialize: function (href, layerEl, mapml, options) { + initialize: function (href, layerEl, options) { // in the custom element, the attribute is actually 'src' // the _href version is the URL received from layer-@src + L.LayerGroup.prototype.initialize.call(this, null, options); if (href) { this._href = href; } - let local; this._layerEl = layerEl; - local = layerEl.querySelector('map-feature,map-tile,map-extent') - ? true - : false; - this._content = local ? layerEl : mapml; + this._content = layerEl.src ? layerEl.shadowRoot : layerEl; L.setOptions(this, options); this._container = L.DomUtil.create('div', 'leaflet-layer'); this.changeOpacity(this.options.opacity); @@ -36,7 +33,7 @@ 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(local ? layerEl : mapml); + this._initialize(this._content); }, setZIndex: function (zIndex) { this.options.zIndex = zIndex; @@ -81,18 +78,15 @@ export var MapMLLayer = L.Layer.extend({ }, onAdd: function (map) { - this._map = map; - if (this._mapmlvectors) map.addLayer(this._mapmlvectors); + L.LayerGroup.prototype.onAdd.call(this, map); + this.getPane().appendChild(this._container); //only add the layer if there are tiles to be rendered if (this._staticTileLayer) { - map.addLayer(this._staticTileLayer); + this.addLayer(this._staticTileLayer); } - this.setZIndex(this.options.zIndex); - this.getPane().appendChild(this._container); map.on('popupopen', this._attachSkipButtons, this); - this._validateLayerZoom({ zoom: map.getZoom() }); }, _calculateBounds: function () { @@ -175,43 +169,9 @@ export var MapMLLayer = L.Layer.extend({ } }, - getEvents: function () { - return { zoomanim: this._validateLayerZoom }; - }, - _validateLayerZoom: function (e) { - // get the min and max zooms from all extents - const layerEl = this._layerEl; - let toZoom = e.zoom; - let min = layerEl.extent.zoom.minZoom; - let max = layerEl.extent.zoom.maxZoom; - let inLink = layerEl.src - ? layerEl.shadowRoot.querySelector('map-link[rel=zoomin]') - : layerEl.querySelector('map-link[rel=zoomin]'), - outLink = layerEl.src - ? layerEl.shadowRoot.querySelector('map-link[rel=zoomout]') - : layerEl.querySelector('map-link[rel=zoomout]'); - let targetURL; - if (!(min <= toZoom && toZoom <= max)) { - if (inLink && toZoom > max) { - targetURL = inLink.href; - } else if (outLink && toZoom < min) { - targetURL = outLink.href; - } - if (targetURL) { - this._layerEl.dispatchEvent( - new CustomEvent('zoomchangesrc', { - detail: { - href: targetURL - } - }) - ); - } - } - }, onRemove: function (map) { + L.LayerGroup.prototype.onRemove.call(this, map); L.DomUtil.remove(this._container); - if (this._staticTileLayer) map.removeLayer(this._staticTileLayer); - if (this._mapmlvectors) map.removeLayer(this._mapmlvectors); map.off('popupopen', this._attachSkipButtons); }, getAttribution: function () { @@ -227,16 +187,13 @@ export var MapMLLayer = L.Layer.extend({ this._href ).href; }, - _initialize: function (content) { - if (!this._href && !content) { - return; - } + _initialize: function () { var layer = this; // the this._href (comes from layer@src) should take precedence over // 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. - _processContent.call(this, content, this._href ? false : true); + _processContent.call(this, this._content, this._layerEl.src ? false : true); function _processContent(mapml, local) { var base = layer.getBase(); parseLicenseAndLegend(); @@ -282,7 +239,7 @@ export var MapMLLayer = L.Layer.extend({ geometry.bindPopup(c, { autoClose: false, minWidth: 165 }); } } - }); + }).addTo(layer); } function processTiles() { if (mapml.querySelector('map-tile')) { @@ -585,7 +542,7 @@ export var MapMLLayer = L.Layer.extend({ map.closePopup(popup); setTimeout(() => { L.DomEvent.stop(focusEvent); - map._container.focus(); + map.getContainer.focus(); }, 0); } } @@ -598,42 +555,38 @@ export var MapMLLayer = L.Layer.extend({ content.querySelector('a.mapml-zoom-link').remove(); } if (!featureEl.querySelector('map-geometry')) return; - 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(); - featureEl._map.getContainer().focus(); - }; - // we found that the popupopen event is fired as many times as there - // are layers on the map ( elements / MapMLLayers that is). - // In each case the target layer is always this layer, so we can't - // detect and conditionally add the zoomLink if the target is not this. - // so, like Ahmad, we are taking a 'delete everyting each time' - // approach (see _attachSkipButtons for this approach taken with - // feature navigation buttons); obviously he dealt with this leaflet bug - // this way some time ago, and we can't figure out how to get around it - // apart from this slightly non-optimal method. Revisit sometime! - let link = content.querySelector('.mapml-zoom-link'); - if (link) link.remove(); - content.insertBefore( - zoomLink, - content.querySelector('hr.mapml-popup-divider') - ); - }); + 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(); + map.closePopup(); + map.getContainer().focus(); + }; + // we found that the popupopen event is fired as many times as there + // are layers on the map ( elements / MapMLLayers that is). + // In each case the target layer is always this layer, so we can't + // detect and conditionally add the zoomLink if the target is not this. + // so, like Ahmad, we are taking a 'delete everyting each time' + // approach (see _attachSkipButtons for this approach taken with + // feature navigation buttons); obviously he dealt with this leaflet bug + // this way some time ago, and we can't figure out how to get around it + // apart from this slightly non-optimal method. Revisit sometime! + let link = content.querySelector('.mapml-zoom-link'); + if (link) link.remove(); + content.insertBefore( + zoomLink, + content.querySelector('hr.mapml-popup-divider') + ); } // if popup closes then the focusFeature handler can be removed @@ -649,7 +602,7 @@ export var MapMLLayer = L.Layer.extend({ } } }); -export var mapMLLayer = function (url, node, mapml, options) { +export var mapMLLayer = function (url, node, options) { if (!url && !node) return null; - return new MapMLLayer(url, node, mapml, options); + return new MapMLLayer(url, node, options); }; diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index cb0e72780..504f0df4f 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -66,15 +66,12 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ geometry.bindPopup(c, { autoClose: false, minWidth: 108 }); } }); - } else { - // if this._features exists add the layer back - this._map.addLayer(this._features); } map.fire('moveend'); // TODO: replace with moveend handler for layer and not entire map }, onRemove: function () { - this._map.removeLayer(this._features); + L.DomUtil.remove(this._container); }, redraw: function () { this._onMoveEnd(); @@ -140,11 +137,12 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ Accept: 'text/mapml;q=0.9,application/geo+json;q=0.8' }), parser = new DOMParser(), - features = this._features, + featureLayer = this._features, linkEl = this._linkEl, map = this._map, context = this, MAX_PAGES = 10, + // TODO: Fetching logic should migrate to map-link _pullFeatureFeed = function (url, limit) { return fetch(url, { redirect: 'follow', headers: headers }) .then(function (response) { @@ -162,31 +160,22 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ ? mapml.querySelector('map-link[rel=next]').getAttribute('href') : null; url = url ? new URL(url, base).href : null; - // TODO if the xml parser barfed but the response is application/geo+json, use the parent addData method - let nativeZoom = (linkEl._nativeZoom = - (mapml.querySelector('map-meta[name=zoom]') && - +M._metaContentToObject( - mapml - .querySelector('map-meta[name=zoom]') - .getAttribute('content') - ).value) || - 0); - let nativeCS = (linkEl._nativeCS = - (mapml.querySelector('map-meta[name=cs]') && - M._metaContentToObject( - mapml - .querySelector('map-meta[name=cs]') - .getAttribute('content') - ).content) || - 'GCRS'); - features.addData(mapml, nativeCS, nativeZoom); - // "migrate" to extent's shadow - // make a clone, prevent the elements from being removed from mapml file - // same as _attachToLayer() in MapMLLayer.js - for (let el of mapml.querySelector('map-body').children) { - linkEl.shadowRoot.append(el._DOMnode); - el._DOMnode._linkEl = linkEl; + let frag = document.createDocumentFragment(); + let elements = mapml.querySelectorAll('map-head > *, map-body > *'); + for (let i = 0; i < elements.length; i++) { + frag.appendChild(elements[i]); } + linkEl.shadowRoot.appendChild(frag); + let features = linkEl.shadowRoot.querySelectorAll('map-feature'); + let featuresReady = []; + for (let i = 0; i < features.length; i++) { + featuresReady.push(features[i].whenReady()); + } + Promise.allSettled(featuresReady).then(() => { + for (let i = 0; i < features.length; i++) { + features[i].addFeature(featureLayer); + } + }); if (url && --limit) { return _pullFeatureFeed(url, limit); } @@ -195,7 +184,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._url = url; _pullFeatureFeed(url, MAX_PAGES) .then(function () { - map.addLayer(features); + map.addLayer(featureLayer); //Fires event for feature index overlay map.fire('templatedfeatureslayeradd'); M.TemplatedFeaturesLayer.prototype._updateTabIndex(context); diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index 13d0b0384..628d5aa81 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -23,7 +23,6 @@ export var TemplatedLayer = L.LayerGroup.extend({ }, onAdd: function (map) { L.LayerGroup.prototype.onAdd.call(this, map); - // add to this.options.pane let pane = this.options.extentEl.parentLayer._layer._container; pane.appendChild(this._container); }, diff --git a/src/web-map.js b/src/web-map.js index 3c2bb4553..20eeb2ff2 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -439,10 +439,6 @@ export class WebMap extends HTMLMapElement { resolve(); }).then(() => { if (this._map && this._map.options.projection !== oldValue) { - // this awful hack is brought to you by a leaflet bug/ feature request - // https://github.com/Leaflet/Leaflet/issues/2553 - this.zoomTo(this.lat, this.lon, this.zoom + 1); - this.zoomTo(this.lat, this.lon, this.zoom - 1); // this doesn't completely work either this._resetHistory(); } From 4b5f2286940089624d787fb9427b79e485ba4684 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 19 Dec 2023 17:00:22 -0500 Subject: [PATCH 37/46] WIP on map-link: - adapt FeatureLayer so that it doesn't expect feature data during initialization. Change options.static to be options.tiles and let it be false by default: FeatureLayer is used only for its addData function by TemplatedTileLayer, in which case and only in that case options.tiles is passed as true; - adapt TemplatedTileLayer only uses M.featureLayer for access to the addData function, which it now calls once for each feature found in the fetched MapML vector tile response; map-feature elements are not added to the DOM for each feature, only their geometry is rendered as SVG and elements. - adapt TemplatedFeaturesLayer - remove use of options.static which now has a default of false - remove use of M.featureLayer.options.static by MapMLLayer._mapmlvectors, because it now has a default of false (and is more correctly called options.tiles). - remove QueryHandler.featureLayer.options.static, because that option is false by default and has been renamed to options.tiles - update map-feature to extend the bounds of the M.featureLayer to which it is added, since the bounds of the Leaflet layers are harvested to calculate the extent / bounds of the parent element. --- src/map-feature.js | 4 +++- src/mapml/handlers/QueryHandler.js | 1 - src/mapml/layers/FeatureLayer.js | 20 +++++++------------- src/mapml/layers/MapMLLayer.js | 1 - src/mapml/layers/TemplatedFeaturesLayer.js | 1 - src/mapml/layers/TemplatedTileLayer.js | 18 ++++++++++++------ 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/map-feature.js b/src/map-feature.js index 19da81055..098f3f5d4 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -240,7 +240,9 @@ export class MapFeature extends HTMLElement { fallbackZoom ); // todo: dynamically update layer bounds of vector layer - layerToAddTo.layerBounds = M.getBounds(content); + layerToAddTo.layerBounds = layerToAddTo.layerBounds + ? layerToAddTo.layerBounds.extend(M.extentToBounds(this.extent, 'pcrs')) + : M.extentToBounds(this.extent, 'pcrs'); // update map's zoom limit // the mapmlvectors.options should be updated with the new zoomBounds, // to ensure the _addZoomLimit function call can read updated zoom info diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index 3b09ba5d0..b7451b0a7 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -278,7 +278,6 @@ export var QueryHandler = L.Handler.extend({ projection: map.options.projection, _leafletLayer: layer, query: true, - static: true, mapEl: layer.options.mapEl }); diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index c6927e49b..523d44aab 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -21,7 +21,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ // this._staticFeature is ONLY true when not used by TemplatedFeaturesLayer // this.options.query true when created by QueryHandler.js - if (this.options.static) { + if (!this.options.tiles) { // not a tiled vector layer this._container = null; if (this.options.query) { @@ -61,18 +61,12 @@ export var FeatureLayer = L.FeatureGroup.extend({ } if (this.options.query) { this._mapmlFeatures = mapml.features ? mapml.features : mapml; - } else { - if (mapml) { - // tileFeatures - let native = M.getNativeVariables(mapml); - this.addData(mapml, native.cs, native.zoom); - } else if (!mapml) { - // use this.options._leafletLayer to distinguish the featureLayer constructed for initialization and for templated features / tiles - if (this.options._leafletLayer) { - // this._staticFeature should be set to true to make sure the _getEvents works properly - this._features = {}; - this._staticFeature = true; - } + } else if (!mapml) { + // use this.options._leafletLayer to distinguish the featureLayer constructed for initialization and for templated features / tiles + if (this.options._leafletLayer) { + // this._staticFeature should be set to true to make sure the _getEvents works properly + this._features = {}; + this._staticFeature = true; } } }, diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 76c9f59a6..f5a106fa2 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -228,7 +228,6 @@ export var MapMLLayer = L.LayerGroup.extend({ native: native, // each owned child layer gets a reference to the root layer _leafletLayer: layer, - static: true, mapEl: layer._layerEl.parentElement, onEachFeature: function (properties, geometry) { // need to parse as HTML to preserve semantics and styles diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index 504f0df4f..29ad4c2fa 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -56,7 +56,6 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ zoomBounds: this.zoomBounds, opacity: opacity, projection: map.options.projection, - static: true, mapEl: this._linkEl.getMapEl(), onEachFeature: function (properties, geometry) { // need to parse as HTML to preserve semantics and styles diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index a0cfb2a8e..8dcf5b92a 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -184,18 +184,24 @@ export var TemplatedTileLayer = L.TileLayer.extend({ xOffset = coords.x * tileSize, yOffset = coords.y * tileSize; - let tileFeatures = M.featureLayer(markup, { + let tileFeatures = M.featureLayer(null, { projection: this._map.options.projection, - static: false, + tiles: true, layerBounds: this.extentBounds, zoomBounds: this.zoomBounds, interactive: false, mapEl: this._linkEl.getMapEl() }); - - for (let groupID in tileFeatures._layers) { - for (let featureID in tileFeatures._layers[groupID]._layers) { - let layer = tileFeatures._layers[groupID]._layers[featureID]; + let fallback = M.getNativeVariables(markup); + let features = markup.querySelectorAll('map-feature'); + for (let i = 0; i < features.length; i++) { + let feature = tileFeatures.addData( + features[i], + fallback.cs, + fallback.zoom + ); + for (let featureID in feature._layers) { + let layer = feature._layers[featureID]; M.FeatureRenderer.prototype._initPath(layer, false); layer._project(this._map, L.point([xOffset, yOffset]), coords.z); M.FeatureRenderer.prototype._addPath(layer, g, false); From 16a097450be01a3adf1e892fc261d6067c6c6ef3 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 22 Dec 2023 13:33:49 -0500 Subject: [PATCH 38/46] WIP on map-link: - add console log of document that causes parser error, useful for debugging in geoserver query layers, which serves errors as xml documents - change layer-.zoomTo() use of whenElemsReady() to use this.whenReady() because the layer is only ready after the elements are ready, so whenElemsReady() doesn't wait long enough in some cases. - add M.geometry.layerBounds and M.geometry.zoomBounds via passed options of the same name. Necessary so that the M.featureLayer._layers hash table has a value of these properties for every layer it holds, which is necessary when setting the bounds of the overall - make map-feature getters for zoom,min, max return fallback values if the attribute isn't set - map-feature disconnectedCallback can happen when ancestor elements are disconnected, which may have cleaned up resources used by map-feature, so the disconnectedCallback should test if they've been dealt with already and not throw errors if so. - rename-refactor M.FeatureLayer.addData to M.FeatureLayer.renderToLeaflet - fix bug in map-feature._updateExtent (used non-uppercased comparison for tagName value, which returns uppercase always) - add map-feature.extent.zoom property to match that of layer-.extent - add map-feature.getMeta API which returns the appropriate map-meta element in the ancestors of the map-feature, if any - update M.FeatureLayer: - add addLayer(layerToAdd) which conforms to L.LayerGroup API - update removeLayer(layerToRemove) to maintain internal structures of M.FeatureLayer, including .layerBounds, .zoomBounds and ._layers - make removeLayer return this, conforming to L.LayerGroup API - refactor geometryToLayer to tag it as 'private': _geometryToLayer - when feature is queried or static, we need to extend the M.featureLayer layerBounds and zoomBounds - for QueryHandler: - synthesize a for all features returned by query if they do not posses a . Uses :has() because this was recently deemed fit for use across browsers, with FF 121 - change approach to parsing: text/mapml documents are handled the same way, but mapml documents returned under a different media type should get handled ok. text/html documents will get parsed and appended to a synthesized map-feature>map-properties element, which should display them as parsed HTML. Other text documents or unrecognized stuff will display as text in the popup, hopefully. --- src/layer.js | 6 +- src/map-feature.js | 158 ++++++++++++++++--------- src/mapml/features/geometry.js | 3 + src/mapml/handlers/QueryHandler.js | 78 +++++++++--- src/mapml/layers/FeatureLayer.js | 121 ++++++++++++++----- src/mapml/layers/TemplatedTileLayer.js | 2 +- 6 files changed, 267 insertions(+), 101 deletions(-) diff --git a/src/layer.js b/src/layer.js index 51a2844f2..aecfacdac 100644 --- a/src/layer.js +++ b/src/layer.js @@ -222,7 +222,7 @@ export class MapLayer extends HTMLElement { ) { // cut short whenReady with the _fetchError property this._fetchError = true; - console.log('Error fetching layer content'); + console.log('Error fetching layer content:\n\n' + mapml + '\n'); throw new Error('Parser error'); } return content; @@ -445,6 +445,8 @@ export class MapLayer extends HTMLElement { case 'MAP-EXTENT': // if extent is checked, ... // TODO: Static Tiles + case 'MAP-META': + // name=projection, name=zoom, name=extent default: break; } @@ -715,7 +717,7 @@ export class MapLayer extends HTMLElement { } zoomTo() { - this.whenElemsReady().then(() => { + this.whenReady().then(() => { let map = this.parentElement._map, extent = this.extent, tL = extent.topLeft.pcrs, diff --git a/src/map-feature.js b/src/map-feature.js index 098f3f5d4..b40285e58 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -7,7 +7,26 @@ export class MapFeature extends HTMLElement { #hasConnected; /* jshint ignore:end */ get zoom() { - return +(this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0); + // for templated or queried features ** native zoom is only used for zoomTo() ** + let meta = M._metaContentToObject(this.getMeta('zoom')); + let projectionMaxZoom = + this.getMapEl()._map.options.crs.options.resolutions.length - 1; + if (this._parentEl.nodeName === 'MAP-LINK') { + // nativeZoom = zoom attribute || sd.map-meta zoom 'value' || map-link maxNativeZoom + return +(this.hasAttribute('zoom') + ? this.getAttribute('zoom') + : meta.value + ? meta.value + : this._parentEl.getZoomBounds().maxNativeZoom); + } else { + // for "static" features + // nativeZoom zoom attribute || map-meta zoom || projection maxZoom + return +(this.hasAttribute('zoom') + ? this.getAttribute('zoom') + : meta.value + ? meta.value + : projectionMaxZoom); + } } set zoom(val) { @@ -18,10 +37,25 @@ export class MapFeature extends HTMLElement { } get min() { - // fallback: the minimum zoom bound of layer- element - return +(this.hasAttribute('min') - ? this.getAttribute('min') - : this.getLayerEl().extent.zoom.minZoom); + // for templated or queried features ** native zoom is only used for zoomTo() ** + let meta = M._metaContentToObject(this.getMeta('zoom')); + let projectionMinZoom = 0; + if (this._parentEl.nodeName === 'MAP-LINK') { + // minZoom = min attribute || sd.map-meta min zoom || map-link minZoom + return +(this.hasAttribute('min') + ? this.getAttribute('min') + : meta.min + ? meta.min + : this._parentEl.getZoomBounds().minZoom); + } else { + // for "static" features + // minZoom = min attribute || map-meta zoom || projection minZoom + return +(this.hasAttribute('min') + ? this.getAttribute('min') + : meta.min + ? meta.min + : projectionMinZoom); + } } set min(val) { @@ -40,10 +74,26 @@ export class MapFeature extends HTMLElement { } get max() { - // fallback: the maximum zoom bound of layer- element - return +(this.hasAttribute('max') - ? this.getAttribute('max') - : this.getLayerEl().extent.zoom.maxZoom); + // for templated or queried features ** native zoom is only used for zoomTo() ** + let meta = M._metaContentToObject(this.getMeta('zoom')); + let projectionMaxZoom = + this.getMapEl()._map.options.crs.options.resolutions.length - 1; + if (this._parentEl.nodeName === 'MAP-LINK') { + // maxZoom = max attribute || sd.map-meta max zoom || map-link maxZoom + return +(this.hasAttribute('max') + ? this.getAttribute('max') + : meta.max + ? meta.max + : this._parentEl.getZoomBounds().maxZoom); + } else { + // for "static" features + // maxZoom = max attribute || map-meta zoom max || projection maxZoom + return +(this.hasAttribute('max') + ? this.getAttribute('max') + : meta.max + ? meta.max + : projectionMaxZoom); + } } set max(val) { @@ -163,9 +213,15 @@ export class MapFeature extends HTMLElement { } disconnectedCallback() { - if (!this.getLayerEl()?.hasAttribute('data-moving')) return; - this.removeFeature(this._featureLayer); + if ( + this._parentEl.hasAttribute('data-moving') || + this._parentEl.parentElement?.hasAttribute('data-moving') + ) + return; this._observer.disconnect(); + if (this._featureLayer) { + this.removeFeature(this._featureLayer); + } } reRender(layerToRenderOn) { @@ -182,7 +238,7 @@ export class MapFeature extends HTMLElement { layerToRenderOn.removeLayer(this._geometry); // Garbage collection needed this._geometry = layerToRenderOn - .addData(this, fallbackCS, fallbackZoom) // side effect: this._groupEl set + .renderToLeaflet(this, fallbackCS, fallbackZoom) // side effect: this._groupEl set .addTo(layerToRenderOn); placeholder.replaceWith(this._geometry.options.group); layerToRenderOn._resetFeatures(); @@ -201,20 +257,10 @@ export class MapFeature extends HTMLElement { // if (layerToRemoveFrom._features[this.zoom]) { // this._removeInFeatureList(this.zoom); // } - // - // TODO: update layerToRemoveFrom.removeLayer(...) to update its own - // zoomBounds - // // update zoom bounds of vector layer - // layerToRemoveFrom.zoomBounds = M.getZoomBounds( - // this._layer._content, - // this._getNativeZoomAndCS(this._layer._content).zoom - // ); - // } if (layerToRemoveFrom._staticFeature) { layerToRemoveFrom._removeFromFeaturesList(this._geometry); } layerToRemoveFrom.options.properties = null; - layerToRemoveFrom.removeLayer(this._geometry); delete this._geometry; // ensure that feature extent can be re-calculated everytime that map-feature element is updated / re-added if (this._getFeatureExtent) delete this._getFeatureExtent; @@ -228,31 +274,12 @@ export class MapFeature extends HTMLElement { let fallbackZoom = this._getFallbackZoom(); let fallbackCS = this._getFallbackCS(); let content = parentLayer.src ? parentLayer.shadowRoot : parentLayer; - this._geometry = layerToAddTo - .addData(this, fallbackCS, fallbackZoom) // side effect: extends `this` with this._groupEl, points to svg g element that renders to map SD - .addTo(layerToAddTo); - layerToAddTo._layers[this._geometry._leaflet_id] = this._geometry; - if (layerToAddTo._staticFeature && this._parentEl.nodeName !== 'MAP-LINK') { - // update zoom bounds of vector layer - layerToAddTo.zoomBounds = M.getZoomBounds( - content, - // this._getNativeZoomAndCS().zoom - fallbackZoom - ); - // todo: dynamically update layer bounds of vector layer - layerToAddTo.layerBounds = layerToAddTo.layerBounds - ? layerToAddTo.layerBounds.extend(M.extentToBounds(this.extent, 'pcrs')) - : M.extentToBounds(this.extent, 'pcrs'); - // update map's zoom limit - // the mapmlvectors.options should be updated with the new zoomBounds, - // to ensure the _addZoomLimit function call can read updated zoom info - // and update map zoom limit properly - L.extend(layerToAddTo.options, layerToAddTo.zoomBounds); - // this._map._addZoomLimit(mapmlvectors); - // TODO: can be set as a handler of featureLayer - layerToAddTo._resetFeatures(); - } - + this._geometry = layerToAddTo.renderToLeaflet( + this, + fallbackCS, + fallbackZoom + ); // side effect: extends `this` with this._groupEl, points to svg g element that renders to map SD + layerToAddTo.addLayer(this._geometry); this._setUpEvents(); } @@ -280,7 +307,7 @@ export class MapFeature extends HTMLElement { }); } - // native zoom: used by FeatureLayer.addData(...), + // native zoom: used by FeatureLayer.renderToLeaflet(...), // the fallback zoom for map-feature if its zoom attribute is not specified // for query and templated features, fallback zoom is the current map zoom level // for static features, fallback zoom is map-meta[name="zoom"] value in layer- || the current map zoom level @@ -301,7 +328,7 @@ export class MapFeature extends HTMLElement { } } - // native cs: used by FeatureLayer.geometryToLayer(...), + // native cs: used by FeatureLayer._geometryToLayer(...), // the fallback cs for map-geometry if its cs attribute is not specified _getFallbackCS() { let csMeta; @@ -391,7 +418,8 @@ export class MapFeature extends HTMLElement { pcrsBound, map.options.crs, map.options.projection - ) + ), + { zoom: this._getZoomBounds() } ); // memoize calculated result extentCache = result; @@ -406,7 +434,7 @@ export class MapFeature extends HTMLElement { .replace(/<[^>]+>/g, '') .replace(/\s+/g, ' ') .split(/[<>\ ]/g); - switch (shape.tagName) { + switch (shape.tagName.toUpperCase()) { case 'MAP-POINT': bboxExtent = M._updateExtent(bboxExtent, +data[0], +data[1]); break; @@ -424,7 +452,15 @@ export class MapFeature extends HTMLElement { return bboxExtent; } } - + _getZoomBounds() { + // ** native zoom is only used for zoomTo() ** + return { + minZoom: this.min, + maxZoom: this.max, + minNativeZoom: this.zoom, + maxNativeZoom: this.zoom + }; + } getMaxZoom() { let tL = this.extent.topLeft.pcrs, bR = this.extent.bottomRight.pcrs, @@ -463,7 +499,23 @@ export class MapFeature extends HTMLElement { // should check whether the extent after zooming falls into the templated extent bound return newZoom; } - + getMeta(metaName) { + let name = metaName.toLowerCase(); + if (name !== 'cs' && name !== 'zoom' && name !== 'projection') return; + let sdMeta = this._parentEl.shadowRoot.querySelector( + `map-meta[name=${name}][content]` + ); + if (this._parentEl.nodeName === 'MAP-LINK') { + // sd.map-meta || map-extent meta || layer meta + return sdMeta || this._parentEl.parentElement.getMeta(metaName); + } else { + return this._parentEl.src + ? this._parentEl.shadowRoot.querySelector( + `map-meta[name=${name}][content]` + ) + : this._parentEl.querySelector(`map-meta[name=${name}][content]`); + } + } // internal support for returning a GeoJSON representation of geometry // The options object can contain the following: // propertyFunction - function(), A function that maps the features' element to a GeoJSON "properties" member. diff --git a/src/mapml/features/geometry.js b/src/mapml/features/geometry.js index b4933232b..d29053caa 100644 --- a/src/mapml/features/geometry.js +++ b/src/mapml/features/geometry.js @@ -14,6 +14,9 @@ export var Geometry = L.FeatureGroup.extend({ L.LayerGroup.prototype.initialize.call(this, layers, options); this._featureEl = this.options.mapmlFeature; + this.layerBounds = options.layerBounds; + this.zoomBounds = options.zoomBounds; + let firstLayer = layers[Object.keys(layers)[0]]; if (layers.length === 1 && firstLayer.options.link) this.options.link = firstLayer.options.link; diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index b7451b0a7..ba88f5ecd 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -102,6 +102,12 @@ export var QueryHandler = L.Handler.extend({ }) .then((response) => { let features = []; + let geom = + "" + + e.latlng.lng + + ' ' + + e.latlng.lat + + ''; if (response.contenttype.startsWith('text/mapml')) { // the mapmldoc could have elements that are important, perhaps // also, the mapmldoc can have many features @@ -109,6 +115,16 @@ export var QueryHandler = L.Handler.extend({ response.text, 'application/xml' ); + let geometrylessFeatures = mapmldoc.querySelectorAll( + 'map-feature:not(:has(map-geometry))' + ); + if (geometrylessFeatures.length) { + let g = parser.parseFromString(geom, 'application/xml'); + for (let i = 0; i < geometrylessFeatures.length; i++) { + let f = geometrylessFeatures[i]; + f.appendChild(g.firstElementChild.cloneNode(true)); + } + } features = Array.prototype.slice.call( mapmldoc.querySelectorAll('map-feature') ); @@ -119,24 +135,50 @@ export var QueryHandler = L.Handler.extend({ ) ); } else { - // synthesize a single feature from text or html content - let geom = - "" + - e.latlng.lng + - ' ' + - e.latlng.lat + - '', - feature = parser - .parseFromString( - '' + - response.text + - '' + - geom + - '', - 'text/html' - ) - .querySelector('map-feature'); - features.push(feature); + try { + let featureDocument = parser.parseFromString( + response.text, + 'application/xml' + ); + let featureCollection = + featureDocument.querySelectorAll('map-feature'); + if ( + featureDocument.querySelector('parsererror') || + featureCollection.length === 0 + ) { + throw new Error('parsererror'); + } + let g = parser.parseFromString(geom, 'application/xml'); + for (let feature of featureCollection) { + if (!feature.querySelector('map-geometry')) { + feature.appendChild(g.firstElementChild.cloneNode(true)); + } + features.push(feature); + } + } catch (err) { + // try the html parser; script elements are marked as non-functional + // by that api, which hopefully works! + let html = parser.parseFromString(response.text, 'text/html'); + + // synthesize a single feature from text or html content + let featureDoc = parser.parseFromString( + '' + + '' + + geom + + '', + 'text/html' + ); + if (html.body.querySelector('*')) { + featureDoc + .querySelector('map-properties') + .appendChild(html.querySelector('html')); + } else { + featureDoc + .querySelector('map-properties') + .append(response.text); + } + features.push(featureDoc.querySelector('map-feature')); + } } return { features: features, template: template }; }) diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 523d44aab..3abc81be1 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -113,7 +113,30 @@ export var FeatureLayer = L.FeatureGroup.extend({ if (this._mapmlFeatures) map.on('featurepagination', this.showPaginationFeature, this); }, + addLayer: function (layerToAdd) { + L.FeatureGroup.prototype.addLayer.call(this, layerToAdd); + this.layerBounds = this.layerBounds + ? this.layerBounds.extend(layerToAdd.layerBounds) + : layerToAdd.layerBounds; + + if (this.zoomBounds) { + if (layerToAdd.zoomBounds.minZoom < this.zoomBounds.minZoom) + this.zoomBounds.minZoom = layerToAdd.zoomBounds.minZoom; + if (layerToAdd.zoomBounds.maxZoom > this.zoomBounds.maxZoom) + this.zoomBounds.maxZoom = layerToAdd.zoomBounds.maxZoom; + } else { + this.zoomBounds = layerToAdd.zoomBounds; + } + // the mapmlvectors.options should be updated with the new zoomBounds, + // to ensure the _addZoomLimit function call can read updated zoom info + // and update map zoom limit properly + L.extend(this.options, this.zoomBounds); + if (this._staticFeature) { + this._resetFeatures(); + } + return this; + }, onRemove: function (map) { if (this._mapmlFeatures) { map.off('featurepagination', this.showPaginationFeature, this); @@ -126,7 +149,34 @@ export var FeatureLayer = L.FeatureGroup.extend({ removeLayer: function (featureToRemove) { L.FeatureGroup.prototype.removeLayer.call(this, featureToRemove); + delete this.layerBounds; + delete this.zoomBounds; delete this._layers[featureToRemove._leaflet_id]; + // iterate through all remaining layers + let layerBounds, + zoomBounds = {}; + let layerIds = Object.keys(this._layers); + for (let id of layerIds) { + let layer = this._layers[id]; + if (layerBounds) { + layerBounds.extend(layer.layerBounds); + } else { + layerBounds = layer.layerBounds; + } + if (zoomBounds) { + if (layer.zoomBounds.minZoom < zoomBounds.minZoom) + zoomBounds.minZoom = layer.zoomBounds.minZoom; + if (layer.zoomBounds.maxZoom > zoomBounds.maxZoom) + zoomBounds.maxZoom = layer.zoomBounds.maxZoom; + } else { + zoomBounds.minZoom = layer.zoomBounds.minZoom; + zoomBounds.maxZoom = layer.zoomBounds.maxZoom; + } + } + this.layerBounds = layerBounds; + this.zoomBounds = zoomBounds; + L.extend(this.options, this.zoomBounds); + return this; }, _removeFromFeaturesList: function (layer) { for (let zoom in this._features) @@ -168,9 +218,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ } } feature._linkEl.shadowRoot.appendChild(feature); - let fallbackZoom = feature._getFallbackZoom(); - let fallbackCS = feature._getFallbackCS(); - feature._geometry = this.addData(feature, fallbackCS, fallbackZoom); + feature.addFeature(this); e.popup._navigationBar.querySelector('p').innerText = e.i + 1 + '/' + this.options._leafletLayer._totalFeatureCount; e.popup._content @@ -200,9 +248,14 @@ export var FeatureLayer = L.FeatureGroup.extend({ this._resetFeatures(); } }, - - // remove or add features based on the min max attribute of the features, - // and add placeholders to maintain position + /* + * _resetFeatures prunes the features currently in the _features hashmap (created + * by us). _features categorizes features by zoom, and is used to remove or add + * features from the map based on the map-feature min/max attribute. It also + * maintains the _map.featureIndex property, which is used to control the tab + * order for interactive (static) features currently rendered on the map. + * @private + * */ _resetFeatures: function () { // since features are removed and re-added by zoom level, need to clean the feature index before re-adding if (this._map) this._map.featureIndex.cleanIndex(); @@ -257,9 +310,23 @@ export var FeatureLayer = L.FeatureGroup.extend({ } }, - addData: function (feature, fallbackCS, fallbackZoom) { - //if its a mapml with no more links this runs - var options = this.options; + /** + * Render a as a Leaflet layer that can be added to a map or + * LayerGroup as required. Kind of a "factory" method. + * + * Uses this.options, so if you need to, you can construct an M.featureLayer + * with options set as required + * + * @param feature + * @param {String} fallbackCS - "gcrs" | "pcrs" + * @param {Integer} fallbackZoom - the zoom level to use for "zoomTo()" if the + * feature geometry doesn't have a zoom attribute + * + * @returns M.Geometry, which is an L.FeatureGroup + * @public + */ + renderToLeaflet: function (feature, fallbackCS, fallbackZoom) { + let options = this.options; if (options.filter && !options.filter(feature)) { return; @@ -280,14 +347,16 @@ export var FeatureLayer = L.FeatureGroup.extend({ feature.querySelector('map-properties').innerHTML ); } - - let geometry = this.geometryToLayer( - feature, - options, - fallbackCS, - +zoom, - title - ); + let cs = + feature.getElementsByTagName('map-geometry')[0]?.getAttribute('cs') || + fallbackCS; + // options.layerBounds and options.zoomBounds are set by TemplatedTileLayer._createFeatures + // each geometry needs bounds so that it can be a good community member of this._layers + if (this._staticFeature || this.options.query) { + options.layerBounds = M.extentToBounds(feature.extent, 'PCRS'); + options.zoomBounds = feature.extent.zoom; + } + let geometry = this._geometryToLayer(feature, options, cs, +zoom, title); if (geometry) { // if the layer is being used as a query handler output, it will have // a color option set. Otherwise, copy classes from the feature @@ -346,12 +415,8 @@ export var FeatureLayer = L.FeatureGroup.extend({ this._container.removeChild(toDelete[i]); } }, - geometryToLayer: function (mapml, vectorOptions, nativeCS, zoom, title) { - let geometry = - mapml.tagName.toUpperCase() === 'MAP-FEATURE' - ? mapml.getElementsByTagName('map-geometry')[0] - : mapml, - cs = geometry?.getAttribute('cs') || nativeCS, + _geometryToLayer: function (feature, vectorOptions, cs, zoom, title) { + let geometry = feature.getElementsByTagName('map-geometry')[0], group = [], groupOptions = {}, svgGroup = L.SVG.create('g'), @@ -367,7 +432,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ nativeCS: cs, nativeZoom: zoom, projection: this.options.projection, - featureID: mapml.id, + featureID: feature.id, group: svgGroup, wrappers: this._getGeometryParents(geo.parentElement), featureLayer: this, @@ -378,12 +443,14 @@ export var FeatureLayer = L.FeatureGroup.extend({ } let groupOptions = { group: svgGroup, - mapmlFeature: mapml, - featureID: mapml.id, + mapmlFeature: feature, + featureID: feature.id, accessibleTitle: title, onEachFeature: vectorOptions.onEachFeature, properties: vectorOptions.properties, - _leafletLayer: this.options._leafletLayer + _leafletLayer: this.options._leafletLayer, + layerBounds: vectorOptions.layerBounds, + zoomBounds: vectorOptions.zoomBounds }, collections = geometry.querySelector('map-multipolygon') || diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 8dcf5b92a..128f0a191 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -195,7 +195,7 @@ export var TemplatedTileLayer = L.TileLayer.extend({ let fallback = M.getNativeVariables(markup); let features = markup.querySelectorAll('map-feature'); for (let i = 0; i < features.length; i++) { - let feature = tileFeatures.addData( + let feature = tileFeatures.renderToLeaflet( features[i], fallback.cs, fallback.zoom From bc09ed7942c2882373662e088adcf73bff786a4b Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 22 Dec 2023 16:37:18 -0500 Subject: [PATCH 39/46] WIP on map-link: - Updated DebugOverlay: make access to options._leafletLayer conditional on its existence, because it is actually iterating leaflet layers on the map and some of them are debug overlay artifacts and don't have a _leafletLayer (if _leafletLayer is taken to mean an "M.MapMLLayer" instance) tbd --- src/mapml/layers/DebugOverlay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mapml/layers/DebugOverlay.js b/src/mapml/layers/DebugOverlay.js index 1f45a321c..caeea2cd1 100644 --- a/src/mapml/layers/DebugOverlay.js +++ b/src/mapml/layers/DebugOverlay.js @@ -255,7 +255,7 @@ export var DebugVectors = L.LayerGroup.extend({ .getLayerEl() .getAttribute('data-testid') : layers[i].layerBounds && - layers[i].options._leafletLayer._layerEl.hasAttribute( + layers[i].options?._leafletLayer?._layerEl.hasAttribute( 'data-testid' ) ? layers[i].options._leafletLayer._layerEl.getAttribute( From 57858ac1643ffce159c23fed92755503e137fefa Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 5 Jan 2024 10:41:55 -0500 Subject: [PATCH 40/46] WIP on map-link - get api tests working --- src/mapml-viewer.js | 2 +- src/web-map.js | 2 +- test/e2e/api/domApi-mapml-viewer.test.js | 1 + test/e2e/api/domApi-web-map.test.js | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index e05b094a0..12eb0982f 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -1359,7 +1359,7 @@ export class MapViewer extends HTMLElement { let layersReady = []; // check if all the children elements (map-extent, map-feature) of all layer- are ready for (let layer of [...this.layers]) { - layersReady.push(layer.whenElemsReady()); + layersReady.push(layer.whenReady()); } return Promise.allSettled(layersReady); } diff --git a/src/web-map.js b/src/web-map.js index 20eeb2ff2..3bbd48c86 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -1409,7 +1409,7 @@ export class WebMap extends HTMLMapElement { let layersReady = []; // check if all the children elements (map-extent, map-feature) of all layer- are ready for (let layer of [...this.layers]) { - layersReady.push(layer.whenElemsReady()); + layersReady.push(layer.whenReady()); } return Promise.allSettled(layersReady); } diff --git a/test/e2e/api/domApi-mapml-viewer.test.js b/test/e2e/api/domApi-mapml-viewer.test.js index b1197ba1a..aabe29dd8 100644 --- a/test/e2e/api/domApi-mapml-viewer.test.js +++ b/test/e2e/api/domApi-mapml-viewer.test.js @@ -392,6 +392,7 @@ test.describe('mapml-viewer DOM API Tests', () => { '.leaflet-top.leaflet-left > .leaflet-control-fullscreen', (div) => div.hidden ); + await page.waitForTimeout(300); let layerControlHidden = await page.$eval( '.leaflet-top.leaflet-right > .leaflet-control-layers', (div) => div.hidden diff --git a/test/e2e/api/domApi-web-map.test.js b/test/e2e/api/domApi-web-map.test.js index 693e44b5e..8a797522f 100644 --- a/test/e2e/api/domApi-web-map.test.js +++ b/test/e2e/api/domApi-web-map.test.js @@ -373,6 +373,7 @@ test.describe('web-map DOM API Tests', () => { '.leaflet-top.leaflet-left > .leaflet-control-fullscreen', (div) => div.hidden ); + await page.waitForTimeout(300); let layerControlHidden = await page.$eval( '.leaflet-top.leaflet-right > .leaflet-control-layers', (div) => div.hidden From 376a0d4c6816d127674eb8902a8131148f40ec3c Mon Sep 17 00:00:00 2001 From: prushfor Date: Fri, 5 Jan 2024 15:39:10 -0500 Subject: [PATCH 41/46] WIP on map-link - truncate tcrs and tilematrix to whole numbers when converting pcrs bounds for extent calculation --- src/mapml/utils/Util.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mapml/utils/Util.js b/src/mapml/utils/Util.js index f77cfc9c5..2cefc13a0 100644 --- a/src/mapml/utils/Util.js +++ b/src/mapml/utils/Util.js @@ -16,22 +16,22 @@ export var Util = { maxConverted = crs.transformation.transform(pcrsBounds.max, scale); tcrsTopLeft.push({ - horizontal: minConverted.x, - vertical: maxConverted.y + horizontal: Math.trunc(minConverted.x), + vertical: Math.trunc(maxConverted.y) }); tcrsBottomRight.push({ - horizontal: maxConverted.x, - vertical: minConverted.y + horizontal: Math.trunc(maxConverted.x), + vertical: Math.trunc(minConverted.y) }); //converts the tcrs values from earlier to tilematrix tileMatrixTopLeft.push({ - horizontal: tcrsTopLeft[i].horizontal / tileSize, - vertical: tcrsTopLeft[i].vertical / tileSize + horizontal: Math.trunc(tcrsTopLeft[i].horizontal / tileSize), + vertical: Math.trunc(tcrsTopLeft[i].vertical / tileSize) }); tileMatrixBottomRight.push({ - horizontal: tcrsBottomRight[i].horizontal / tileSize, - vertical: tcrsBottomRight[i].vertical / tileSize + horizontal: Math.trunc(tcrsBottomRight[i].horizontal / tileSize), + vertical: Math.trunc(tcrsBottomRight[i].vertical / tileSize) }); } From 4d17d8e383096ca5a05161e8d164fa3190bf08e3 Mon Sep 17 00:00:00 2001 From: prushfor Date: Wed, 10 Jan 2024 14:18:47 -0500 Subject: [PATCH 42/46] WIP on map-link axisInferring.test.js - update map-feature.js - when calculating extent, don't use the zoom fallback value unless no zoom attribute exists. map-feature.zoom looks for map-meta in the parentage of the element, but falls back to the projection, which is not suitable for calculating extents when the coordinates require a zoom value for calculation - rename M.getBounds to M.getBoundsFromMeta - add map-meta[name=extent]-based bounds as default layer bounds for MapMLLayer._calculateBounds, so that map-meta, _mapmlvectors, static tiles and map-extent -generated bounds are additive - update Util.pointToPCRSPoint and Util.boundsToPCRSBounds logic so that zoom parameter is optional for points that don't require a zoom to transform i.e. gcrs to pcrs, pcrs to pcrs don't need a zoom value - update Util.getBoundsFromMeta to throw where it is invoked without a zoom where a zoom value is required to convert coordinate system i.e. tile,tcrs,tilematrix - remove fallback cs from Util.getBoundsFromMeta since it could be wrong - remove fallback to zoom = 0 where zoom parameter is not provided, throw instead where it could cause a problem - update axisInferring.html and .test.js. Add (commented) features that represent the bounding box of the map-meta[name=extent], for debugging purposes - update the expectations to expect that the extents of the layers involved should include the map-meta[name=extent] bounds AND the contents of the layers. --- src/map-feature.js | 4 +- src/mapml/index.js | 2 +- src/mapml/layers/MapMLLayer.js | 16 +- src/mapml/utils/Util.js | 30 +++- test/e2e/core/axisInferring.html | 241 ++++++++++++++++------------ test/e2e/core/axisInferring.test.js | 55 +++++-- 6 files changed, 214 insertions(+), 134 deletions(-) diff --git a/src/map-feature.js b/src/map-feature.js index b40285e58..7dcef71a1 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -370,7 +370,9 @@ export class MapFeature extends HTMLElement { geometry = this.querySelector('map-geometry'), cs = geometry.getAttribute('cs') || this._getFallbackCS(), // zoom level that the feature rendered at - zoom = this.zoom || this._getFallbackZoom(), + zoom = +(this.hasAttribute('zoom') + ? this.getAttribute('zoom') + : this._getFallbackZoom()), shapes = geometry.querySelectorAll( 'map-point, map-linestring, map-polygon, map-multipoint, map-multilinestring' ), diff --git a/src/mapml/index.js b/src/mapml/index.js index bcea81802..30c940ff3 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -837,7 +837,7 @@ import { DOMTokenList } from './utils/DOMTokenList'; M._pcrsToGcrs = Util._pcrsToGcrs; M.mapml2geojson = Util.mapml2geojson; M.getMaxZoom = Util.getMaxZoom; - M.getBounds = Util.getBounds; + M.getBoundsFromMeta = Util.getBoundsFromMeta; M.getZoomBounds = Util.getZoomBounds; M.getNativeVariables = Util.getNativeVariables; diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index f5a106fa2..f420d3901 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -103,6 +103,13 @@ export var MapMLLayer = L.LayerGroup.extend({ : this._layerEl.shadowRoot ? this._layerEl.shadowRoot.querySelectorAll('map-extent') : []; + bounds = + this._layerEl.src && + this._layerEl.shadowRoot.querySelector('map-meta[name=extent][content]') + ? M.getBoundsFromMeta(this._layerEl.shadowRoot) + : this._layerEl.querySelector('map-meta[name=extent][content]') + ? M.getBoundsFromMeta(this._layerEl) + : undefined; layerTypes.forEach((type) => { if (type === '_extentLayer' && mapExtents.length) { let zoomMax = zoomBounds.maxZoom, @@ -116,8 +123,7 @@ export var MapMLLayer = L.LayerGroup.extend({ bounds = templatedLayer.bounds; zoomBounds = templatedLayer.zoomBounds; } else { - bounds.extend(templatedLayer.bounds.min); - bounds.extend(templatedLayer.bounds.max); + bounds.extend(templatedLayer.bounds); zoomMax = Math.max(zoomMax, templatedLayer.zoomBounds.maxZoom); zoomMin = Math.min(zoomMin, templatedLayer.zoomBounds.minZoom); maxNativeZoom = Math.max( @@ -141,8 +147,7 @@ export var MapMLLayer = L.LayerGroup.extend({ bounds = this[type].layerBounds; zoomBounds = this[type].zoomBounds; } else { - bounds.extend(this[type].layerBounds.min); - bounds.extend(this[type].layerBounds.max); + bounds.extend(this[type].layerBounds); } } } else if ( @@ -156,8 +161,7 @@ export var MapMLLayer = L.LayerGroup.extend({ bounds = this[type].layerBounds; zoomBounds = this[type].zoomBounds; } else { - bounds.extend(this[type].layerBounds.min); - bounds.extend(this[type].layerBounds.max); + bounds.extend(this[type].layerBounds); } } } diff --git a/src/mapml/utils/Util.js b/src/mapml/utils/Util.js index 2cefc13a0..36e94cba9 100644 --- a/src/mapml/utils/Util.js +++ b/src/mapml/utils/Util.js @@ -235,8 +235,9 @@ export var Util = { pointToPCRSPoint: function (point, zoom, projection, cs) { if ( !point || - (!zoom && zoom !== 0) || - !Number.isFinite(+zoom) || + (zoom !== undefined && !Number.isFinite(+zoom)) || + (zoom === undefined && + (cs === 'TILEMATRIX' || cs === 'TCRS' || cs === 'TILE')) || !cs || !projection ) @@ -282,8 +283,9 @@ export var Util = { !bounds || !bounds.max || !bounds.min || - (!zoom && zoom !== 0) || - !Number.isFinite(+zoom) || + (zoom !== undefined && !Number.isFinite(+zoom)) || + (zoom === undefined && + (cs === 'TILEMATRIX' || cs === 'TCRS' || cs === 'TILE')) || !projection || !cs ) @@ -494,10 +496,9 @@ export var Util = { map.options.mapEl.zoom = +zoomTo.z; } }, - // TODO: make this dynamic based on the individual features/extents - getBounds: function (mapml) { + getBoundsFromMeta: function (mapml) { if (!mapml) return null; - let cs = M.FALLBACK_CS, + let cs, projection = (mapml.querySelector('map-meta[name=projection]') && M._metaContentToObject( @@ -513,7 +514,7 @@ export var Util = { mapml.querySelector('map-meta[name=extent]').getAttribute('content') ); - let zoom = meta.zoom || 0; + let zoom = meta.zoom; let metaKeys = Object.keys(meta); for (let i = 0; i < metaKeys.length; i++) { @@ -522,6 +523,19 @@ export var Util = { break; } } + // this could happen if the content didn't match the grammar for map-meta[name=extent] + if (cs === undefined) throw new Error('cs undefined when getting bounds'); + + // when cs is tilematrix, tcrs or tile, zoom is required. + // should throw / return null instead of trying to construct a bounds + + if ( + zoom === undefined && + (cs === 'TILEMATRIX' || cs === 'TCRS' || cs === 'TILE') + ) + throw new Error( + 'map-meta[name=extent] zoom= parameter not provided for tcrs,tile or tilematrix bounds' + ); let axes = M.csToAxes(cs); return M.boundsToPCRSBounds( L.bounds( diff --git a/test/e2e/core/axisInferring.html b/test/e2e/core/axisInferring.html index 558194595..ef19b925b 100644 --- a/test/e2e/core/axisInferring.html +++ b/test/e2e/core/axisInferring.html @@ -1,121 +1,154 @@ - - Inferring Axes - - - - - + } + + - - + + - - - - + + + + - -

Arizona

-
- - - -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 - -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 - 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 - -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 - 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 - -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 - 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 - -109.042503 37.000263 - - -
-
+ +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+
- - - - - - -

Arizona

-
- - - -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 - -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 - 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 - -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 - 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 - -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 - 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 - -109.042503 37.000263 - - -
-
+ + + + + + + +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+
- - - - - - -

Arizona

-
- - - -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 - -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 - 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 - -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 - 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 - -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 - 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 - -109.042503 37.000263 - - -
-
+ + + + + - - - - - - -

Arizona

-
- - - -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 - -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 - 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 - -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 - 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 - -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 - 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 - -109.042503 37.000263 - - -
-
- - + + +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+
+ + + + + + + + +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+
+ + \ No newline at end of file diff --git a/test/e2e/core/axisInferring.test.js b/test/e2e/core/axisInferring.test.js index d65418ae4..4e5c7fbd5 100644 --- a/test/e2e/core/axisInferring.test.js +++ b/test/e2e/core/axisInferring.test.js @@ -15,6 +15,17 @@ test.describe('UI Drag&Drop Test', () => { await context.close(); }); +/** + * + * The point of "axisInferring.test.js" is to validate that the extent's cs + * is correctly inferred from the axis names in the content attribute. + * + * Semantics: the extent of the layers should include the extent of the map-meta + * -specified extent PLUS the contents of the layer (a map-feature in all cases). + * + * The test expectations were actually measured from the loaded html. + */ test('TileMatrix inferring', async () => { const layerExtent = await page.$eval( 'body > map > layer-:nth-child(1)', @@ -42,8 +53,8 @@ test.describe('UI Drag&Drop Test', () => { vertical: 256 }); expect(layerExtent.bottomRight.tcrs[0]).toEqual({ - horizontal: 256, - vertical: 512 + horizontal: 868, + vertical: 1069 }); }); @@ -52,13 +63,15 @@ test.describe('UI Drag&Drop Test', () => { 'body > map > layer-:nth-child(3)', (layer) => layer.extent ); + // the top left corner is that of the map-meta[name=extent] expect(layerExtent.topLeft.pcrs).toEqual({ - horizontal: 100, - vertical: 600 + horizontal: -6601973, + vertical: 1569758 }); + // the bottom right corner is that of the map-feature expect(layerExtent.bottomRight.pcrs).toEqual({ - horizontal: 500, - vertical: 150 + horizontal: -1319475.9373123178, + vertical: -1731574.5341126453 }); }); @@ -67,13 +80,27 @@ test.describe('UI Drag&Drop Test', () => { 'body > map > layer-:nth-child(4)', (layer) => layer.extent ); - expect(layerExtent.topLeft.gcrs).toEqual({ - horizontal: -92, - vertical: 52.999999999993484 - }); - expect(layerExtent.bottomRight.gcrs).toEqual({ - horizontal: -62, - vertical: 33.99999999999964 - }); + let expectedTopLeftLongitude = -114.815198; + let expectedTopLeftLatitude = 53; + let expectedBottomRightLongitude = -62; + let expectedBottomRightLatitude = 31.331629; + + expect(layerExtent.topLeft.gcrs.horizontal).toBeCloseTo( + expectedTopLeftLongitude, + 6 + ); + expect(layerExtent.topLeft.gcrs.vertical).toBeCloseTo( + expectedTopLeftLatitude, + 6 + ); + + expect(layerExtent.bottomRight.gcrs.horizontal).toBeCloseTo( + expectedBottomRightLongitude, + 6 + ); + expect(layerExtent.bottomRight.gcrs.vertical).toBeCloseTo( + expectedBottomRightLatitude, + 6 + ); }); }); From 53fbdf264121b76a3f037b6574a16c86b4f214ff Mon Sep 17 00:00:00 2001 From: Aliyan Haq <55751566+AliyanH@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:28:08 -0500 Subject: [PATCH 43/46] Fix Layer zoomBounds - Fix zoomBounds initialization in FeatureLayer.js removeLayer, as an empty object is truthy - MapMLLayer.js - CalculateBounds() extend zoombounds for staticTileLayer and mapmlvectors --- src/mapml/layers/FeatureLayer.js | 4 ++-- src/mapml/layers/MapMLLayer.js | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 3abc81be1..693c66b0f 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -153,8 +153,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ delete this.zoomBounds; delete this._layers[featureToRemove._leaflet_id]; // iterate through all remaining layers - let layerBounds, - zoomBounds = {}; + let layerBounds, zoomBounds; let layerIds = Object.keys(this._layers); for (let id of layerIds) { let layer = this._layers[id]; @@ -169,6 +168,7 @@ export var FeatureLayer = L.FeatureGroup.extend({ if (layer.zoomBounds.maxZoom > zoomBounds.maxZoom) zoomBounds.maxZoom = layer.zoomBounds.maxZoom; } else { + zoomBounds = {}; zoomBounds.minZoom = layer.zoomBounds.minZoom; zoomBounds.maxZoom = layer.zoomBounds.maxZoom; } diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index f420d3901..da5219050 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -124,6 +124,7 @@ export var MapMLLayer = L.LayerGroup.extend({ zoomBounds = templatedLayer.zoomBounds; } else { bounds.extend(templatedLayer.bounds); + // Extend layer zoombounds zoomMax = Math.max(zoomMax, templatedLayer.zoomBounds.maxZoom); zoomMin = Math.min(zoomMin, templatedLayer.zoomBounds.minZoom); maxNativeZoom = Math.max( @@ -142,12 +143,31 @@ export var MapMLLayer = L.LayerGroup.extend({ } } } else if (type === '_staticTileLayer' && this._staticTileLayer) { + let zoomMax = zoomBounds.maxZoom, + zoomMin = zoomBounds.minZoom, + maxNativeZoom = zoomBounds.maxNativeZoom, + minNativeZoom = zoomBounds.minNativeZoom; if (this[type].layerBounds) { if (!bounds) { bounds = this[type].layerBounds; zoomBounds = this[type].zoomBounds; } else { bounds.extend(this[type].layerBounds); + // Extend layer zoombounds + zoomMax = Math.max(zoomMax, this[type].zoomBounds.maxZoom); + zoomMin = Math.min(zoomMin, this[type].zoomBounds.minZoom); + maxNativeZoom = Math.max( + maxNativeZoom, + this[type].zoomBounds.maxNativeZoom + ); + minNativeZoom = Math.min( + minNativeZoom, + this[type].zoomBounds.minNativeZoom + ); + zoomBounds.minZoom = zoomMin; + zoomBounds.maxZoom = zoomMax; + zoomBounds.minNativeZoom = minNativeZoom; + zoomBounds.maxNativeZoom = maxNativeZoom; } } } else if ( @@ -156,12 +176,31 @@ export var MapMLLayer = L.LayerGroup.extend({ this._mapmlvectors && Object.keys(this[type]._layers).length !== 0 ) { + let zoomMax = zoomBounds.maxZoom, + zoomMin = zoomBounds.minZoom, + maxNativeZoom = zoomBounds.maxNativeZoom, + minNativeZoom = zoomBounds.minNativeZoom; if (this[type].layerBounds) { if (!bounds) { bounds = this[type].layerBounds; zoomBounds = this[type].zoomBounds; } else { bounds.extend(this[type].layerBounds); + // Extend layer zoombounds + zoomMax = Math.max(zoomMax, this[type].zoomBounds.maxZoom); + zoomMin = Math.min(zoomMin, this[type].zoomBounds.minZoom); + maxNativeZoom = Math.max( + maxNativeZoom, + this[type].zoomBounds.maxNativeZoom + ); + minNativeZoom = Math.min( + minNativeZoom, + this[type].zoomBounds.minNativeZoom + ); + zoomBounds.minZoom = zoomMin; + zoomBounds.maxZoom = zoomMax; + zoomBounds.minNativeZoom = minNativeZoom; + zoomBounds.maxNativeZoom = maxNativeZoom; } } } From 0657abc3202b21fd3462683f520b98a52057dc36 Mon Sep 17 00:00:00 2001 From: prushfor Date: Wed, 10 Jan 2024 16:01:33 -0500 Subject: [PATCH 44/46] WIP on map-link - update tests to instead of using the old _templatedLayer, to use _extentLayer (renamed). - update multipleExtentsOpacity.test to slow it down a little, also simplify selector for assertion that may have been taking too long to evaluate --- test/e2e/elements/map-extent/map-extent.test.js | 6 +++--- test/e2e/layers/multipleExtentsOpacity.test.js | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/test/e2e/elements/map-extent/map-extent.test.js b/test/e2e/elements/map-extent/map-extent.test.js index d187b85f5..43f6ff5cd 100644 --- a/test/e2e/elements/map-extent/map-extent.test.js +++ b/test/e2e/elements/map-extent/map-extent.test.js @@ -113,7 +113,7 @@ test.describe('map-extent tests', () => { return extent._layerControlCheckbox.checked; }); let visibleOnMap = await extent.evaluate((extent) => { - return extent._templatedLayer._container.isConnected; + return extent._extentLayer._container.isConnected; }); let checkedProperty = await extent.evaluate((extent) => { return extent.checked; @@ -132,7 +132,7 @@ test.describe('map-extent tests', () => { return extent._layerControlCheckbox.checked; }); visibleOnMap = await extent.evaluate((extent) => { - return extent._templatedLayer._container.isConnected; + return extent._extentLayer._container.isConnected; }); checkedProperty = await extent.evaluate((extent) => { return extent.checked; @@ -147,7 +147,7 @@ test.describe('map-extent tests', () => { return extent._layerControlCheckbox.checked; }); visibleOnMap = await extent2.evaluate((extent) => { - return extent._templatedLayer._container.isConnected; + return extent._extentLayer._container.isConnected; }); checkedProperty = await extent2.evaluate((extent) => { return extent.checked; diff --git a/test/e2e/layers/multipleExtentsOpacity.test.js b/test/e2e/layers/multipleExtentsOpacity.test.js index 9dc9c8798..e98747be4 100644 --- a/test/e2e/layers/multipleExtentsOpacity.test.js +++ b/test/e2e/layers/multipleExtentsOpacity.test.js @@ -17,24 +17,25 @@ test.describe('Adding Opacity Attribute to the Element', () => { test('Setting Opacity Attibute Value to map-extent Element', async () => { let extent_opacity1 = await page.$eval( 'body > mapml-viewer > layer- > map-extent:nth-child(1)', - (extent) => extent._templatedLayer._container.style.opacity + (extent) => extent._extentLayer._container.style.opacity ); expect(extent_opacity1).toEqual('0.9'); let extent_opacity2 = await page.$eval( 'body > mapml-viewer > layer- > map-extent:nth-child(2)', - (extent) => extent._templatedLayer._container.style.opacity + (extent) => extent._extentLayer._container.style.opacity ); expect(extent_opacity2).toEqual('0.3'); let extent_opacity3 = await page.$eval( 'body > mapml-viewer > layer- > map-extent:nth-child(3)', - (extent) => extent._templatedLayer._container.style.opacity + (extent) => extent._extentLayer._container.style.opacity ); expect(extent_opacity3).toEqual('0.2'); }); test('All map-extent layerControl are hidden because of the map-extent hidden attribute', async () => { + await page.waitForTimeout(300); let layerControlExtentsFieldsetCount = await page.$eval( - 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset > div.mapml-layer-item-settings > fieldset.mapml-layer-grouped-extents', + 'fieldset.mapml-layer-grouped-extents', (group) => group.childElementCount ); expect(layerControlExtentsFieldsetCount).toEqual(0); @@ -47,6 +48,7 @@ test.describe('Adding Opacity Attribute to the Element', () => { extent.hidden = false; }); }); + await page.waitForTimeout(300); // check the opacity slider value for each map-extent layerControl let opacity_slider_value1 = await page.$eval( 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset > div.mapml-layer-item-settings > fieldset > fieldset:nth-child(1) > div.mapml-layer-item-settings > details > input[type=range]', @@ -54,7 +56,7 @@ test.describe('Adding Opacity Attribute to the Element', () => { ); let extent_opacity1 = await page.$eval( 'body > mapml-viewer > layer- > map-extent:nth-child(1)', - (extent) => extent._templatedLayer._container.style.opacity + (extent) => extent._extentLayer._container.style.opacity ); let opacity_slider_value2 = await page.$eval( 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset > div.mapml-layer-item-settings > fieldset > fieldset:nth-child(2) > div.mapml-layer-item-settings > details > input[type=range]', @@ -62,7 +64,7 @@ test.describe('Adding Opacity Attribute to the Element', () => { ); let extent_opacity2 = await page.$eval( 'body > mapml-viewer > layer- > map-extent:nth-child(2)', - (extent) => extent._templatedLayer._container.style.opacity + (extent) => extent._extentLayer._container.style.opacity ); let opacity_slider_value3 = await page.$eval( 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset > div.mapml-layer-item-settings > fieldset > fieldset:nth-child(3) > div.mapml-layer-item-settings > details > input[type=range]', @@ -70,7 +72,7 @@ test.describe('Adding Opacity Attribute to the Element', () => { ); let extent_opacity3 = await page.$eval( 'body > mapml-viewer > layer- > map-extent:nth-child(3)', - (extent) => extent._templatedLayer._container.style.opacity + (extent) => extent._extentLayer._container.style.opacity ); expect(extent_opacity1).toEqual(opacity_slider_value1); expect(extent_opacity2).toEqual(opacity_slider_value2); From 3ecc83352d0769cbfffb7c586a06c85fb13bd35e Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 10 Jan 2024 16:46:44 -0500 Subject: [PATCH 45/46] Add grunt 'geoserver' task which copies the viewer to the geoserver MapML extension 'widget' directory. Could be improved, as it clobbers the license headers in each of the geoserver copies of the file. Prettier format change to a axisInferring.test.js (?) --- Gruntfile.js | 15 +++++++++++++++ test/e2e/core/axisInferring.test.js | 22 +++++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index efc38e0bc..4552536cf 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -156,6 +156,16 @@ module.exports = function(grunt) { } ] }, + geoserver: { + files: [ + { + expand: true, + cwd: 'dist', + src: ['*.js','*.map','*.css'], + dest: '../geoserver/src/extension/mapml/src/main/resources/viewer/widget' + } + ] + }, docs: { files: [ { @@ -177,6 +187,10 @@ module.exports = function(grunt) { options: {force: true}, src: ['../mapml-extension/src/dist'] }, + geoserver: { + options: {force: true}, + src: ['../geoserver/src/extension/mapml/src/main/resources/viewer/widget/*.js','../geoserver/src/extension/mapml/src/main/resources/viewer/widget/*.map','../geoserver/src/extension/mapml/src/main/resources/viewer/widget/*.css'] + }, docs: { options: {force: true}, src: ['../web-map-doc/dist'] @@ -219,6 +233,7 @@ module.exports = function(grunt) { 'uglify', 'cssmin','clean:tidyup']); grunt.registerTask('experiments',['clean:experiments','default','copy:experiments']); grunt.registerTask('extension',['clean:extension','default','copy:extension']); + grunt.registerTask('geoserver',['clean:geoserver','default','copy:geoserver']); grunt.registerTask('docs', ['clean:docs','default','copy:docs']); grunt.registerTask('sync', ['default','experiments','extension','docs']); diff --git a/test/e2e/core/axisInferring.test.js b/test/e2e/core/axisInferring.test.js index 4e5c7fbd5..bd22c1f2a 100644 --- a/test/e2e/core/axisInferring.test.js +++ b/test/e2e/core/axisInferring.test.js @@ -15,17 +15,17 @@ test.describe('UI Drag&Drop Test', () => { await context.close(); }); -/** - * - * The point of "axisInferring.test.js" is to validate that the extent's cs - * is correctly inferred from the axis names in the content attribute. - * - * Semantics: the extent of the layers should include the extent of the map-meta - * -specified extent PLUS the contents of the layer (a map-feature in all cases). - * - * The test expectations were actually measured from the loaded html. - */ + /** + * + * The point of "axisInferring.test.js" is to validate that the extent's cs + * is correctly inferred from the axis names in the content attribute. + * + * Semantics: the extent of the layers should include the extent of the map-meta + * -specified extent PLUS the contents of the layer (a map-feature in all cases). + * + * The test expectations were actually measured from the loaded html. + */ test('TileMatrix inferring', async () => { const layerExtent = await page.$eval( 'body > map > layer-:nth-child(1)', From 6863091e12cefe2456337d696825598f47b7a2d1 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 10 Jan 2024 19:57:07 -0500 Subject: [PATCH 46/46] map-link - fix kbdAttribution.test.js, add timeout - temporarily add test-results.txt for reference of what's failing --- test-results.txt | 3368 ++++++++++++++++++++++++++ test/e2e/core/kbdAttribution.test.js | 1 + 2 files changed, 3369 insertions(+) create mode 100644 test-results.txt diff --git a/test-results.txt b/test-results.txt new file mode 100644 index 000000000..f331c68ac --- /dev/null +++ b/test-results.txt @@ -0,0 +1,3368 @@ + 1) core\drag.test.js:19:7 › UI Drag&Drop Test › Drag and drop of invalid HTML page ───────── + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + + 30 | await page.hover('.leaflet-top.leaflet-right'); + 31 | let vars = await page.$$('.leaflet-control-layers-overlays > fieldset'); + > 32 | expect(vars.length).toBe(3); + | ^ + 33 | }); + 34 | + 35 | test('Drag and drop of layers', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\drag.test.js:32:25 + + 2) core\drag.test.js:35:7 › UI Drag&Drop Test › Drag and drop of layers ──────────────────── + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + + 48 | await page.hover('.leaflet-top.leaflet-right'); + 49 | let vars = await page.$$('.leaflet-control-layers-overlays > fieldset'); + > 50 | expect(vars.length).toBe(3); + | ^ + 51 | }); + 52 | + 53 | test('Moving layer down one in control overlay', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\drag.test.js:50:25 + + 3) core\drag.test.js:53:7 › UI Drag&Drop Test › Moving layer down one in control overlay ─── + + Error: expect(received).toContain(expected) // indexOf + + Expected substring: "us population density" + Received string: "canada base map - transportation (cbmt)" + + 82 | ); + 83 | + > 84 | expect(controlText.toLowerCase()).toContain(domLayer.toLowerCase()); + | ^ + 85 | expect(layerIndex).toEqual('2'); + 86 | expect(controlText).toBe('Canada Base Map - Transportation (CBMT)'); + 87 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\drag.test.js:84:39 + + 4) core\drag.test.js:89:7 › UI Drag&Drop Test › Moving layer up one in control overlay ───── + + Error: page.$eval: Error: failed to find element matching selector ".leaflet-overlay-pane .mapml-static-tile-layer" + + 109 | (span) => span.innerText + 110 | ); + > 111 | const layerIndex = await page.$eval( + | ^ + 112 | '.leaflet-overlay-pane .mapml-static-tile-layer', + 113 | (div) => div.parentElement.style.zIndex + 114 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\drag.test.js:111:35 + + 5) core\dragDrop.test.js:19:7 › Drag and Drop Layers (layer-, GeoJSON, Link) to mapml-viewer › Drag and drop of valid mapml URL + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + + 29 | await page.hover('.leaflet-top.leaflet-right'); + 30 | let vars = await page.$$('.leaflet-control-layers-overlays > fieldset'); + > 31 | expect(vars.length).toBe(2); + | ^ + 32 | }); + 33 | + 34 | test('Drag and drop of valid geoJSON', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\dragDrop.test.js:31:25 + + 6) core\dragDrop.test.js:34:7 › Drag and Drop Layers (layer-, GeoJSON, Link) to mapml-viewer › Drag and drop of valid geoJSON + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + + 44 | await page.hover('.leaflet-top.leaflet-right'); + 45 | let vars = await page.$$('.leaflet-control-layers-overlays > fieldset'); + > 46 | expect(vars.length).toBe(3); + | ^ + 47 | }); + 48 | + 49 | test('Drag and drop of valid layer-', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\dragDrop.test.js:46:25 + + 7) core\dragDrop.test.js:49:7 › Drag and Drop Layers (layer-, GeoJSON, Link) to mapml-viewer › Drag and drop of valid layer- + + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 4 + Received: 2 + + 59 | await page.hover('.leaflet-top.leaflet-right'); + 60 | let vars = await page.$$('.leaflet-control-layers-overlays > fieldset'); + > 61 | expect(vars.length).toBe(4); + | ^ + 62 | }); + 63 | + 64 | test('Drag and drop of Invalid text', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\dragDrop.test.js:61:25 + + 8) core\dragDrop.test.js:64:7 › Drag and Drop Layers (layer-, GeoJSON, Link) to mapml-viewer › Drag and drop of Invalid text + + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 4 + Received: 1 + + 71 | await page.hover('.leaflet-top.leaflet-right'); + 72 | let vars = await page.$$('.leaflet-control-layers-overlays > fieldset'); + > 73 | expect(vars.length).toBe(4); + | ^ + 74 | }); + 75 | }); + 76 | + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\dragDrop.test.js:73:25 + + 9) core\featureIndexOverlayFocus.test.js:144:7 › Feature Index Overlay Focus tests › Feature index overlay and reticle show after following a link + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + + 149 | '#map3 .mapml-feature-index-box' + 150 | ); + > 151 | expect(await afterFollowingLinkReticle.isHidden()).toBe(false); + | ^ + 152 | + 153 | const afterFollowingLinkOutput = page.locator( + 154 | '#map3 output.mapml-feature-index' + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\featureIndexOverlayFocus.test.js:151:56 + + 11) core\layerContextMenu.test.js:73:7 › Playwright Layer Context Menu Tests › Map zooms in to layer 2 + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 11 + Received: 1 + + 89 | ); + 90 | + > 91 | expect(mapZoom).toEqual(11); + | ^ + 92 | expect(mapLocation).toEqual({ + 93 | max: { x: 43380, y: 43130 }, + 94 | min: { x: 42380, y: 42630 } + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\layerContextMenu.test.js:91:21 + + 12) core\layerContextMenu.test.js:98:7 › Playwright Layer Context Menu Tests › Map zooms out to layer 3 + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(3) > div:nth-child(1) > label > span') + ============================================================ + + 106 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div' + 107 | ); + > 108 | await page.click( + | ^ + 109 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(3) > div:nth-child(1) > label > span', + 110 | { button: 'right', force: true } + 111 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\layerContextMenu.test.js:108:16 + + 13) core\layerContextMenu.test.js:129:7 › Playwright Layer Context Menu Tests › Map zooms out to layer 4 + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(4) > div:nth-child(1) > label > span') + ============================================================ + + 137 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div' + 138 | ); + > 139 | await page.click( + | ^ + 140 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(4) > div:nth-child(1) > label > span', + 141 | { button: 'right', force: true } + 142 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\layerContextMenu.test.js:139:16 + + 14) core\layerContextMenu.test.js:160:7 › Playwright Layer Context Menu Tests › Copy layer with relative src attribute + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(5) > div:nth-child(1) > label > span') + ============================================================ + + 163 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div' + 164 | ); + > 165 | await page.click( + | ^ + 166 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(5) > div:nth-child(1) > label > span', + 167 | { button: 'right' } + 168 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\layerContextMenu.test.js:165:16 + + 15) core\layerContextMenuKeyboard.test.js:18:7 › Playwright Layer Context Menu Tests › Enter activates layer context menu item + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 78.35553458202789 + Received: 47 + + 60 | + 61 | // expect something to do with center and zoom level of map + > 62 | await expect(lat).toBe(78.35553458202789); + | ^ + 63 | await expect(lon).toBe(-151.71482148855017); + 64 | await expect(zoom).toBe(0); + 65 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\layerContextMenuKeyboard.test.js:62:23 + + 16) core\layerContextMenuKeyboard.test.js:66:7 › Playwright Layer Context Menu Tests › Space bar activates layer context menu item + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: "CBMT - INLINE" + Received: "" + + 99 | }); + 100 | + > 101 | expect(result).toBe('CBMT - INLINE'); + | ^ + 102 | }); + 103 | test('Tab can be used to move between layer context menu items', async () => { + 104 | await page.locator('mapml-viewer').press('Tab'); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\layerContextMenuKeyboard.test.js:101:20 + + 17) core\mapContextMenu.test.js:134:7 › Playwright Map Context Menu Tests › Context menu, back item + + Error: expect(received).toEqual(expected) // deep equality + + - Expected - 1 + + Received + 1 + + Object { + - "maxZoom": 25, + + "maxZoom": 3, + "minZoom": 0, + } + + 141 | + 142 | expect(extent.projection).toEqual('CBMTILE'); + > 143 | expect(extent.zoom).toEqual({ minZoom: 0, maxZoom: 25 }); + | ^ + 144 | expect(extent.topLeft.pcrs).toEqual(expectedPCRS[0]); + 145 | expect(extent.topLeft.gcrs).toEqual(expectedGCRS[0]); + 146 | expect(extent.topLeft.tilematrix[0]).toEqual(expectedFirstTileMatrix[0]); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\mapContextMenu.test.js:143:25 + + 18) core\mapContextMenu.test.js:149:7 › Playwright Map Context Menu Tests › Context menu, back and reload item at initial location disabled + + Error: expect(received).toEqual(expected) // deep equality + + Expected: false + Received: true + + 163 | + 164 | expect(backBtn).toEqual(true); + > 165 | expect(fwdBtn).toEqual(false); + | ^ + 166 | expect(reloadBtn).toEqual(true); + 167 | }); + 168 | test('Context menu, forward item', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\mapContextMenu.test.js:165:20 + + 19) core\mapContextMenu.test.js:168:7 › Playwright Map Context Menu Tests › Context menu, forward item + + Test timeout of 30000ms exceeded. + + Error: page.click: Target closed + =========================== logs =========================== + waiting for locator('div > div.mapml-contextmenu > button:nth-child(2)') + locator resolved to 6 elements. Proceeding with the first one: + attempting click action + waiting for element to be visible, enabled and stable + element is not enabled - waiting... + ============================================================ + + 63 | + 64 | test('Reload button takes you back to initial state', async () => { + > 65 | await page.click( + | ^ + 66 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.mapml-reload-button.leaflet-bar.leaflet-control > button' + 67 | ); + 68 | await page.waitForTimeout(1000); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\mapElement.test.js:65:16 + + 26) core\mapFeature.test.js:95:7 › Playwright MapFeature Custom Element Tests › Get extent of with zoom attribute = 2 + + Error: expect(received).toEqual(expected) // deep equality + + - Expected - 187 + + Received + 193 + + @@ -8,218 +8,218 @@ + "horizontal": 3351671.8482770324, + "vertical": -4954880.196427062, + }, + "tcrs": Array [ + Object { + - "horizontal": 990.6896551724137, + - "vertical": 1153.7931034482758, + + "horizontal": 990, + + "vertical": 1153, + }, + Object { + - "horizontal": 1689.9999999999998, + - "vertical": 1968.2352941176468, + + "horizontal": 1689, + + "vertical": 1968, + }, + Object { + "horizontal": 2873, + "vertical": 3346, + }, + Object { + - "horizontal": 4788.333333333333, + - "vertical": 5576.666666666667, + + "horizontal": 4788, + + "vertical": 5576, + }, + Object { + - "horizontal": 8208.571428571428, + + "horizontal": 8208, + "vertical": 9560, + }, + Object { + "horizontal": 14365, + "vertical": 16730, + }, + Object { + - "horizontal": 23941.666666666668, + - "vertical": 27883.333333333332, + + "horizontal": 23941, + + "vertical": 27883, + }, + Object { + - "horizontal": 41042.857142857145, + + "horizontal": 41042, + "vertical": 47800, + }, + Object { + - "horizontal": 71824.99999999999, + - "vertical": 83649.99999999999, + + "horizontal": 71824, + + "vertical": 83649, + }, + Object { + - "horizontal": 119708.33333333333, + - "vertical": 139416.66666666666, + + "horizontal": 119708, + + "vertical": 139416, + }, + Object { + - "horizontal": 205214.2857142857, + + "horizontal": 205214, + "vertical": 239000, + }, + Object { + - "horizontal": 342023.8095238095, + - "vertical": 398333.3333333333, + + "horizontal": 342023, + + "vertical": 398333, + }, + Object { + - "horizontal": 574599.9999999999, + - "vertical": 669199.9999999999, + + "horizontal": 574599, + + "vertical": 669199, + }, + Object { + - "horizontal": 990689.6551724137, + - "vertical": 1153793.1034482757, + + "horizontal": 990689, + + "vertical": 1153793, + }, + Object { + - "horizontal": 1689999.9999999998, + - "vertical": 1968235.294117647, + + "horizontal": 1689999, + + "vertical": 1968235, + }, + Object { + - "horizontal": 2872999.9999999995, + - "vertical": 3345999.9999999995, + + "horizontal": 2872999, + + "vertical": 3345999, + }, + Object { + - "horizontal": 4788333.333333333, + - "vertical": 5576666.666666666, + + "horizontal": 4788333, + + "vertical": 5576666, + }, + Object { + - "horizontal": 8208571.428571428, + + "horizontal": 8208571, + "vertical": 9560000, + }, + Object { + "horizontal": 14365000, + - "vertical": 16729999.999999998, + + "vertical": 16729999, + }, + Object { + - "horizontal": 23941666.666666664, + - "vertical": 27883333.333333332, + + "horizontal": 23941666, + + "vertical": 27883333, + }, + Object { + - "horizontal": 41042857.14285714, + + "horizontal": 41042857, + "vertical": 47800000, + }, + Object { + "horizontal": 71825000, + "vertical": 83650000, + }, + Object { + - "horizontal": 119708333.33333334, + - "vertical": 139416666.6666667, + + "horizontal": 119708333, + + "vertical": 139416666, + }, + Object { + - "horizontal": 205214285.71428573, + + "horizontal": 205214285, + "vertical": 239000000, + }, + Object { + - "horizontal": 342023809.5238095, + - "vertical": 398333333.3333333, + + "horizontal": 342023809, + + "vertical": 398333333, + }, + Object { + "horizontal": 574600000, + "vertical": 669200000, + }, + ], + "tilematrix": Array [ + Object { + - "horizontal": 3.869881465517241, + - "vertical": 4.507004310344827, + + "horizontal": 3, + + "vertical": 4, + }, + Object { + - "horizontal": 6.601562499999999, + - "vertical": 7.688419117647058, + + "horizontal": 6, + + "vertical": 7, + }, + Object { + - "horizontal": 11.22265625, + - "vertical": 13.0703125, + + "horizontal": 11, + + "vertical": 13, + }, + Object { + - "horizontal": 18.704427083333332, + - "vertical": 21.783854166666668, + + "horizontal": 18, + + "vertical": 21, + }, + Object { + - "horizontal": 32.06473214285714, + - "vertical": 37.34375, + + "horizontal": 32, + + "vertical": 37, + }, + Object { + - "horizontal": 56.11328125, + - "vertical": 65.3515625, + + "horizontal": 56, + + "vertical": 65, + }, + Object { + - "horizontal": 93.52213541666667, + - "vertical": 108.91927083333333, + + "horizontal": 93, + + "vertical": 108, + }, + Object { + - "horizontal": 160.32366071428572, + - "vertical": 186.71875, + + "horizontal": 160, + + "vertical": 186, + }, + Object { + - "horizontal": 280.56640624999994, + - "vertical": 326.75781249999994, + + "horizontal": 280, + + "vertical": 326, + }, + Object { + - "horizontal": 467.6106770833333, + - "vertical": 544.5963541666666, + + "horizontal": 467, + + "vertical": 544, + }, + Object { + - "horizontal": 801.6183035714286, + - "vertical": 933.59375, + + "horizontal": 801, + + "vertical": 933, + }, + Object { + - "horizontal": 1336.030505952381, + - "vertical": 1555.9895833333333, + + "horizontal": 1336, + + "vertical": 1555, + }, + Object { + - "horizontal": 2244.5312499999995, + - "vertical": 2614.0624999999995, + + "horizontal": 2244, + + "vertical": 2614, + }, + Object { + - "horizontal": 3869.881465517241, + - "vertical": 4507.004310344827, + + "horizontal": 3869, + + "vertical": 4507, + }, + Object { + - "horizontal": 6601.562499999999, + - "vertical": 7688.419117647059, + + "horizontal": 6601, + + "vertical": 7688, + }, + Object { + - "horizontal": 11222.656249999998, + - "vertical": 13070.312499999998, + + "horizontal": 11222, + + "vertical": 13070, + }, + Object { + - "horizontal": 18704.427083333332, + - "vertical": 21783.854166666664, + + "horizontal": 18704, + + "vertical": 21783, + }, + Object { + - "horizontal": 32064.73214285714, + - "vertical": 37343.75, + + "horizontal": 32064, + + "vertical": 37343, + }, + Object { + - "horizontal": 56113.28125, + - "vertical": 65351.56249999999, + + "horizontal": 56113, + + "vertical": 65351, + }, + Object { + - "horizontal": 93522.13541666666, + - "vertical": 108919.27083333333, + + "horizontal": 93522, + + "vertical": 108919, + }, + Object { + - "horizontal": 160323.6607142857, + - "vertical": 186718.75, + + "horizontal": 160323, + + "vertical": 186718, + }, + Object { + - "horizontal": 280566.40625, + - "vertical": 326757.8125, + + "horizontal": 280566, + + "vertical": 326757, + }, + Object { + - "horizontal": 467610.6770833334, + - "vertical": 544596.3541666667, + + "horizontal": 467610, + + "vertical": 544596, + }, + Object { + - "horizontal": 801618.3035714286, + - "vertical": 933593.75, + + "horizontal": 801618, + + "vertical": 933593, + }, + Object { + - "horizontal": 1336030.5059523808, + - "vertical": 1555989.5833333333, + + "horizontal": 1336030, + + "vertical": 1555989, + }, + Object { + - "horizontal": 2244531.25, + - "vertical": 2614062.5, + + "horizontal": 2244531, + + "vertical": 2614062, + }, + ], + }, + "projection": "CBMTILE", + "topLeft": Object { + @@ -231,217 +231,223 @@ + "horizontal": -35001.59173651785, + "vertical": -1568206.756413512, + }, + "tcrs": Array [ + Object { + - "horizontal": 902.4137931034481, + - "vertical": 1065.5172413793102, + + "horizontal": 902, + + "vertical": 1065, + }, + Object { + - "horizontal": 1539.411764705882, + - "vertical": 1817.647058823529, + + "horizontal": 1539, + + "vertical": 1817, + }, + Object { + "horizontal": 2617, + "vertical": 3090, + }, + Object { + - "horizontal": 4361.666666666666, + + "horizontal": 4361, + "vertical": 5150, + }, + Object { + - "horizontal": 7477.142857142856, + - "vertical": 8828.571428571428, + + "horizontal": 7477, + + "vertical": 8828, + }, + Object { + - "horizontal": 13084.999999999998, + - "vertical": 15449.999999999998, + + "horizontal": 13084, + + "vertical": 15449, + }, + Object { + - "horizontal": 21808.333333333332, + - "vertical": 25749.999999999996, + + "horizontal": 21808, + + "vertical": 25749, + }, + Object { + - "horizontal": 37385.71428571428, + - "vertical": 44142.857142857145, + + "horizontal": 37385, + + "vertical": 44142, + }, + Object { + - "horizontal": 65424.999999999985, + - "vertical": 77249.99999999999, + + "horizontal": 65424, + + "vertical": 77249, + }, + Object { + - "horizontal": 109041.66666666666, + - "vertical": 128749.99999999999, + + "horizontal": 109041, + + "vertical": 128749, + }, + Object { + - "horizontal": 186928.57142857142, + - "vertical": 220714.28571428568, + + "horizontal": 186928, + + "vertical": 220714, + }, + Object { + - "horizontal": 311547.619047619, + - "vertical": 367857.1428571428, + + "horizontal": 311547, + + "vertical": 367857, + }, + Object { + - "horizontal": 523399.9999999999, + - "vertical": 617999.9999999999, + + "horizontal": 523399, + + "vertical": 617999, + }, + Object { + - "horizontal": 902413.7931034481, + - "vertical": 1065517.2413793101, + + "horizontal": 902413, + + "vertical": 1065517, + }, + Object { + - "horizontal": 1539411.7647058822, + - "vertical": 1817647.0588235292, + + "horizontal": 1539411, + + "vertical": 1817647, + }, + Object { + - "horizontal": 2616999.9999999995, + - "vertical": 3089999.9999999995, + + "horizontal": 2616999, + + "vertical": 3089999, + }, + Object { + - "horizontal": 4361666.666666666, + - "vertical": 5149999.999999999, + + "horizontal": 4361666, + + "vertical": 5149999, + }, + Object { + - "horizontal": 7477142.857142856, + - "vertical": 8828571.428571427, + + "horizontal": 7477142, + + "vertical": 8828571, + }, + Object { + - "horizontal": 13084999.999999998, + - "vertical": 15449999.999999998, + + "horizontal": 13084999, + + "vertical": 15449999, + }, + Object { + - "horizontal": 21808333.33333333, + - "vertical": 25749999.999999996, + + "horizontal": 21808333, + + "vertical": 25749999, + }, + Object { + - "horizontal": 37385714.28571428, + - "vertical": 44142857.142857134, + + "horizontal": 37385714, + + "vertical": 44142857, + }, + Object { + - "horizontal": 65424999.99999999, + + "horizontal": 65424999, + "vertical": 77250000, + }, + Object { + - "horizontal": 109041666.66666666, + + "horizontal": 109041666, + "vertical": 128750000, + }, + Object { + - "horizontal": 186928571.4285714, + - "vertical": 220714285.7142857, + + "horizontal": 186928571, + + "vertical": 220714285, + }, + Object { + - "horizontal": 311547619.04761904, + - "vertical": 367857142.8571428, + + "horizontal": 311547619, + + "vertical": 367857142, + }, + Object { + - "horizontal": 523399999.99999994, + + "horizontal": 523399999, + "vertical": 618000000, + }, + ], + "tilematrix": Array [ + Object { + - "horizontal": 3.5250538793103443, + - "vertical": 4.162176724137931, + + "horizontal": 3, + + "vertical": 4, + }, + Object { + - "horizontal": 6.0133272058823515, + - "vertical": 7.10018382352941, + + "horizontal": 6, + + "vertical": 7, + }, + Object { + - "horizontal": 10.22265625, + - "vertical": 12.0703125, + + "horizontal": 10, + + "vertical": 12, + }, + Object { + - "horizontal": 17.037760416666664, + - "vertical": 20.1171875, + + "horizontal": 17, + + "vertical": 20, + }, + Object { + - "horizontal": 29.20758928571428, + - "vertical": 34.48660714285714, + + "horizontal": 29, + + "vertical": 34, + }, + Object { + - "horizontal": 51.11328124999999, + - "vertical": 60.35156249999999, + + "horizontal": 51, + + "vertical": 60, + }, + Object { + - "horizontal": 85.18880208333333, + - "vertical": 100.58593749999999, + + "horizontal": 85, + + "vertical": 100, + }, + Object { + - "horizontal": 146.03794642857142, + - "vertical": 172.43303571428572, + + "horizontal": 146, + + "vertical": 172, + }, + Object { + - "horizontal": 255.56640624999994, + - "vertical": 301.75781249999994, + + "horizontal": 255, + + "vertical": 301, + }, + Object { + - "horizontal": 425.94401041666663, + - "vertical": 502.92968749999994, + + "horizontal": 425, + + "vertical": 502, + }, + Object { + - "horizontal": 730.1897321428571, + - "vertical": 862.1651785714284, + + "horizontal": 730, + + "vertical": 862, + }, + Object { + - "horizontal": 1216.9828869047617, + - "vertical": 1436.941964285714, + + "horizontal": 1216, + + "vertical": 1436, + }, + Object { + - "horizontal": 2044.5312499999995, + - "vertical": 2414.0624999999995, + + "horizontal": 2044, + + "vertical": 2414, + }, + Object { + - "horizontal": 3525.053879310344, + - "vertical": 4162.17672413793, + + "horizontal": 3525, + + "vertical": 4162, + }, + Object { + - "horizontal": 6013.327205882352, + - "vertical": 7100.183823529411, + + "horizontal": 6013, + + "vertical": 7100, + }, + Object { + - "horizontal": 10222.656249999998, + - "vertical": 12070.312499999998, + + "horizontal": 10222, + + "vertical": 12070, + }, + Object { + - "horizontal": 17037.760416666664, + - "vertical": 20117.187499999996, + + "horizontal": 17037, + + "vertical": 20117, + }, + Object { + - "horizontal": 29207.589285714283, + - "vertical": 34486.60714285714, + + "horizontal": 29207, + + "vertical": 34486, + }, + Object { + - "horizontal": 51113.28124999999, + - "vertical": 60351.56249999999, + + "horizontal": 51113, + + "vertical": 60351, + }, + Object { + - "horizontal": 85188.80208333331, + - "vertical": 100585.93749999999, + + "horizontal": 85188, + + "vertical": 100585, + }, + Object { + - "horizontal": 146037.94642857142, + - "vertical": 172433.03571428568, + + "horizontal": 146037, + + "vertical": 172433, + }, + Object { + - "horizontal": 255566.40624999997, + - "vertical": 301757.8125, + + "horizontal": 255566, + + "vertical": 301757, + }, + Object { + - "horizontal": 425944.0104166666, + - "vertical": 502929.6875, + + "horizontal": 425944, + + "vertical": 502929, + }, + Object { + - "horizontal": 730189.732142857, + - "vertical": 862165.1785714285, + + "horizontal": 730189, + + "vertical": 862165, + }, + Object { + - "horizontal": 1216982.886904762, + - "vertical": 1436941.964285714, + + "horizontal": 1216982, + + "vertical": 1436941, + }, + Object { + - "horizontal": 2044531.2499999998, + - "vertical": 2414062.5, + + "horizontal": 2044531, + + "vertical": 2414062, + }, + ], + + }, + + "zoom": Object { + + "maxNativeZoom": 2, + + "maxZoom": 25, + + "minNativeZoom": 2, + + "minZoom": 0, + }, + } + + 100 | (map) => map.querySelector('.point_1').extent + 101 | ); + > 102 | expect(extent).toEqual(data.pointExtentwithZoom); + | ^ + 103 | }); + 104 | + 105 | test('Zoom to with zoom attribute = 2', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\mapFeature.test.js:102:20 + + 27) core\mapFeature.test.js:113:7 › Playwright MapFeature Custom Element Tests › Get extent of with no zoom attribute + + Error: expect(received).toEqual(expected) // deep equality + + - Expected - 208 + + Received + 214 + + @@ -8,218 +8,218 @@ + "horizontal": 1658343.59495385, + "vertical": -3261551.9431038797, + }, + "tcrs": Array [ + Object { + - "horizontal": 946.551944827586, + - "vertical": 1109.655393103448, + + "horizontal": 946, + + "vertical": 1109, + }, + Object { + - "horizontal": 1614.7062588235287, + - "vertical": 1892.9415529411758, + + "horizontal": 1614, + + "vertical": 1892, + }, + Object { + - "horizontal": 2745.0006399999993, + - "vertical": 3218.0006399999997, + + "horizontal": 2745, + + "vertical": 3218, + }, + Object { + - "horizontal": 4575.001066666666, + - "vertical": 5363.334399999999, + + "horizontal": 4575, + + "vertical": 5363, + }, + Object { + - "horizontal": 7842.858971428569, + - "vertical": 9194.28754285714, + + "horizontal": 7842, + + "vertical": 9194, + }, + Object { + - "horizontal": 13725.003199999997, + - "vertical": 16090.003199999997, + + "horizontal": 13725, + + "vertical": 16090, + }, + Object { + - "horizontal": 22875.005333333327, + - "vertical": 26816.671999999995, + + "horizontal": 22875, + + "vertical": 26816, + }, + Object { + - "horizontal": 39214.29485714285, + - "vertical": 45971.437714285705, + + "horizontal": 39214, + + "vertical": 45971, + }, + Object { + - "horizontal": 68625.01599999997, + - "vertical": 80450.01599999997, + + "horizontal": 68625, + + "vertical": 80450, + }, + Object { + - "horizontal": 114375.02666666664, + - "vertical": 134083.35999999996, + + "horizontal": 114375, + + "vertical": 134083, + }, + Object { + - "horizontal": 196071.47428571424, + - "vertical": 229857.1885714285, + + "horizontal": 196071, + + "vertical": 229857, + }, + Object { + - "horizontal": 326785.7904761904, + - "vertical": 383095.3142857142, + + "horizontal": 326785, + + "vertical": 383095, + }, + Object { + - "horizontal": 549000.1279999998, + - "vertical": 643600.1279999998, + + "horizontal": 549000, + + "vertical": 643600, + }, + Object { + - "horizontal": 946551.9448275858, + - "vertical": 1109655.393103448, + + "horizontal": 946551, + + "vertical": 1109655, + }, + Object { + - "horizontal": 1614706.258823529, + - "vertical": 1892941.5529411759, + + "horizontal": 1614706, + + "vertical": 1892941, + }, + Object { + - "horizontal": 2745000.639999999, + - "vertical": 3218000.639999999, + + "horizontal": 2745000, + + "vertical": 3218000, + }, + Object { + - "horizontal": 4575001.0666666655, + - "vertical": 5363334.3999999985, + + "horizontal": 4575001, + + "vertical": 5363334, + }, + Object { + - "horizontal": 7842858.971428569, + - "vertical": 9194287.54285714, + + "horizontal": 7842858, + + "vertical": 9194287, + }, + Object { + - "horizontal": 13725003.199999996, + - "vertical": 16090003.199999996, + + "horizontal": 13725003, + + "vertical": 16090003, + }, + Object { + - "horizontal": 22875005.333333325, + - "vertical": 26816671.999999993, + + "horizontal": 22875005, + + "vertical": 26816671, + }, + Object { + - "horizontal": 39214294.85714284, + - "vertical": 45971437.7142857, + + "horizontal": 39214294, + + "vertical": 45971437, + }, + Object { + - "horizontal": 68625015.99999999, + - "vertical": 80450015.99999999, + + "horizontal": 68625015, + + "vertical": 80450015, + }, + Object { + - "horizontal": 114375026.66666664, + - "vertical": 134083359.99999997, + + "horizontal": 114375026, + + "vertical": 134083359, + }, + Object { + - "horizontal": 196071474.28571424, + - "vertical": 229857188.57142854, + + "horizontal": 196071474, + + "vertical": 229857188, + }, + Object { + - "horizontal": 326785790.4761904, + - "vertical": 383095314.2857142, + + "horizontal": 326785790, + + "vertical": 383095314, + }, + Object { + - "horizontal": 549000127.9999999, + - "vertical": 643600127.9999999, + + "horizontal": 549000127, + + "vertical": 643600127, + }, + ], + "tilematrix": Array [ + Object { + - "horizontal": 3.697468534482758, + - "vertical": 4.334591379310344, + + "horizontal": 3, + + "vertical": 4, + }, + Object { + - "horizontal": 6.307446323529409, + - "vertical": 7.394302941176468, + + "horizontal": 6, + + "vertical": 7, + }, + Object { + - "horizontal": 10.722658749999997, + - "vertical": 12.570314999999999, + + "horizontal": 10, + + "vertical": 12, + }, + Object { + - "horizontal": 17.871097916666663, + - "vertical": 20.950524999999995, + + "horizontal": 17, + + "vertical": 20, + }, + Object { + - "horizontal": 30.636167857142848, + - "vertical": 35.915185714285705, + + "horizontal": 30, + + "vertical": 35, + }, + Object { + - "horizontal": 53.61329374999999, + - "vertical": 62.85157499999999, + + "horizontal": 53, + + "vertical": 62, + }, + Object { + - "horizontal": 89.35548958333331, + - "vertical": 104.75262499999998, + + "horizontal": 89, + + "vertical": 104, + }, + Object { + - "horizontal": 153.18083928571426, + - "vertical": 179.57592857142853, + + "horizontal": 153, + + "vertical": 179, + }, + Object { + - "horizontal": 268.0664687499999, + - "vertical": 314.2578749999999, + + "horizontal": 268, + + "vertical": 314, + }, + Object { + - "horizontal": 446.7774479166666, + - "vertical": 523.7631249999998, + + "horizontal": 446, + + "vertical": 523, + }, + Object { + - "horizontal": 765.9041964285713, + - "vertical": 897.8796428571426, + + "horizontal": 765, + + "vertical": 897, + }, + Object { + - "horizontal": 1276.5069940476187, + - "vertical": 1496.466071428571, + + "horizontal": 1276, + + "vertical": 1496, + }, + Object { + - "horizontal": 2144.531749999999, + - "vertical": 2514.062999999999, + + "horizontal": 2144, + + "vertical": 2514, + }, + Object { + - "horizontal": 3697.468534482757, + - "vertical": 4334.591379310344, + + "horizontal": 3697, + + "vertical": 4334, + }, + Object { + - "horizontal": 6307.44632352941, + - "vertical": 7394.302941176468, + + "horizontal": 6307, + + "vertical": 7394, + }, + Object { + - "horizontal": 10722.658749999997, + - "vertical": 12570.314999999997, + + "horizontal": 10722, + + "vertical": 12570, + }, + Object { + - "horizontal": 17871.097916666662, + - "vertical": 20950.524999999994, + + "horizontal": 17871, + + "vertical": 20950, + }, + Object { + - "horizontal": 30636.16785714285, + - "vertical": 35915.185714285704, + + "horizontal": 30636, + + "vertical": 35915, + }, + Object { + - "horizontal": 53613.29374999998, + - "vertical": 62851.57499999998, + + "horizontal": 53613, + + "vertical": 62851, + }, + Object { + - "horizontal": 89355.4895833333, + - "vertical": 104752.62499999997, + + "horizontal": 89355, + + "vertical": 104752, + }, + Object { + - "horizontal": 153180.83928571423, + - "vertical": 179575.92857142852, + + "horizontal": 153180, + + "vertical": 179575, + }, + Object { + - "horizontal": 268066.46874999994, + - "vertical": 314257.87499999994, + + "horizontal": 268066, + + "vertical": 314257, + }, + Object { + - "horizontal": 446777.44791666657, + - "vertical": 523763.1249999999, + + "horizontal": 446777, + + "vertical": 523763, + }, + Object { + - "horizontal": 765904.1964285712, + - "vertical": 897879.6428571427, + + "horizontal": 765904, + + "vertical": 897879, + }, + Object { + - "horizontal": 1276506.9940476187, + - "vertical": 1496466.0714285711, + + "horizontal": 1276506, + + "vertical": 1496466, + }, + Object { + - "horizontal": 2144531.7499999995, + - "vertical": 2514062.9999999995, + + "horizontal": 2144531, + + "vertical": 2514062, + }, + ], + }, + "projection": "CBMTILE", + "topLeft": Object { + @@ -231,217 +231,223 @@ + "horizontal": 1658326.6615866497, + "vertical": -3261535.0097366795, + }, + "tcrs": Array [ + Object { + - "horizontal": 946.5515034482756, + - "vertical": 1109.6549517241376, + + "horizontal": 946, + + "vertical": 1109, + }, + Object { + - "horizontal": 1614.7055058823523, + - "vertical": 1892.9407999999994, + + "horizontal": 1614, + + "vertical": 1892, + }, + Object { + - "horizontal": 2744.9993599999993, + - "vertical": 3217.9993599999993, + + "horizontal": 2744, + + "vertical": 3217, + }, + Object { + - "horizontal": 4574.998933333332, + - "vertical": 5363.332266666665, + + "horizontal": 4574, + + "vertical": 5363, + }, + Object { + - "horizontal": 7842.855314285712, + - "vertical": 9194.283885714283, + + "horizontal": 7842, + + "vertical": 9194, + }, + Object { + - "horizontal": 13724.996799999997, + - "vertical": 16089.996799999997, + + "horizontal": 13724, + + "vertical": 16089, + }, + Object { + - "horizontal": 22874.994666666662, + - "vertical": 26816.661333333326, + + "horizontal": 22874, + + "vertical": 26816, + }, + Object { + - "horizontal": 39214.27657142856, + - "vertical": 45971.41942857142, + + "horizontal": 39214, + + "vertical": 45971, + }, + Object { + - "horizontal": 68624.98399999997, + - "vertical": 80449.98399999997, + + "horizontal": 68624, + + "vertical": 80449, + }, + Object { + - "horizontal": 114374.9733333333, + - "vertical": 134083.30666666664, + + "horizontal": 114374, + + "vertical": 134083, + }, + Object { + - "horizontal": 196071.3828571428, + - "vertical": 229857.0971428571, + + "horizontal": 196071, + + "vertical": 229857, + }, + Object { + - "horizontal": 326785.638095238, + - "vertical": 383095.1619047618, + + "horizontal": 326785, + + "vertical": 383095, + }, + Object { + - "horizontal": 548999.8719999997, + - "vertical": 643599.8719999997, + + "horizontal": 548999, + + "vertical": 643599, + }, + Object { + - "horizontal": 946551.5034482755, + - "vertical": 1109654.9517241376, + + "horizontal": 946551, + + "vertical": 1109654, + }, + Object { + - "horizontal": 1614705.5058823524, + - "vertical": 1892940.7999999993, + + "horizontal": 1614705, + + "vertical": 1892940, + }, + Object { + - "horizontal": 2744999.359999999, + - "vertical": 3217999.359999999, + + "horizontal": 2744999, + + "vertical": 3217999, + }, + Object { + - "horizontal": 4574998.933333332, + - "vertical": 5363332.266666665, + + "horizontal": 4574998, + + "vertical": 5363332, + }, + Object { + - "horizontal": 7842855.314285712, + - "vertical": 9194283.885714283, + + "horizontal": 7842855, + + "vertical": 9194283, + }, + Object { + - "horizontal": 13724996.799999995, + - "vertical": 16089996.799999995, + + "horizontal": 13724996, + + "vertical": 16089996, + }, + Object { + - "horizontal": 22874994.66666666, + - "vertical": 26816661.333333325, + + "horizontal": 22874994, + + "vertical": 26816661, + }, + Object { + - "horizontal": 39214276.57142856, + - "vertical": 45971419.42857142, + + "horizontal": 39214276, + + "vertical": 45971419, + }, + Object { + - "horizontal": 68624983.99999999, + - "vertical": 80449983.99999999, + + "horizontal": 68624983, + + "vertical": 80449983, + }, + Object { + - "horizontal": 114374973.33333331, + - "vertical": 134083306.66666664, + + "horizontal": 114374973, + + "vertical": 134083306, + }, + Object { + - "horizontal": 196071382.8571428, + - "vertical": 229857097.1428571, + + "horizontal": 196071382, + + "vertical": 229857097, + }, + Object { + - "horizontal": 326785638.09523803, + - "vertical": 383095161.9047618, + + "horizontal": 326785638, + + "vertical": 383095161, + }, + Object { + - "horizontal": 548999871.9999999, + - "vertical": 643599871.9999999, + + "horizontal": 548999871, + + "vertical": 643599871, + }, + ], + "tilematrix": Array [ + Object { + - "horizontal": 3.6974668103448267, + - "vertical": 4.334589655172413, + + "horizontal": 3, + + "vertical": 4, + }, + Object { + - "horizontal": 6.307443382352939, + - "vertical": 7.394299999999998, + + "horizontal": 6, + + "vertical": 7, + }, + Object { + - "horizontal": 10.722653749999997, + - "vertical": 12.570309999999997, + + "horizontal": 10, + + "vertical": 12, + }, + Object { + - "horizontal": 17.87108958333333, + - "vertical": 20.950516666666662, + + "horizontal": 17, + + "vertical": 20, + }, + Object { + - "horizontal": 30.63615357142856, + - "vertical": 35.91517142857142, + + "horizontal": 30, + + "vertical": 35, + }, + Object { + - "horizontal": 53.61326874999999, + - "vertical": 62.85154999999999, + + "horizontal": 53, + + "vertical": 62, + }, + Object { + - "horizontal": 89.35544791666665, + - "vertical": 104.7525833333333, + + "horizontal": 89, + + "vertical": 104, + }, + Object { + - "horizontal": 153.18076785714283, + - "vertical": 179.5758571428571, + + "horizontal": 153, + + "vertical": 179, + }, + Object { + - "horizontal": 268.0663437499999, + - "vertical": 314.2577499999999, + + "horizontal": 268, + + "vertical": 314, + }, + Object { + - "horizontal": 446.7772395833332, + - "vertical": 523.7629166666666, + + "horizontal": 446, + + "vertical": 523, + }, + Object { + - "horizontal": 765.9038392857141, + - "vertical": 897.8792857142855, + + "horizontal": 765, + + "vertical": 897, + }, + Object { + - "horizontal": 1276.5063988095235, + - "vertical": 1496.4654761904758, + + "horizontal": 1276, + + "vertical": 1496, + }, + Object { + - "horizontal": 2144.530749999999, + - "vertical": 2514.061999999999, + + "horizontal": 2144, + + "vertical": 2514, + }, + Object { + - "horizontal": 3697.466810344826, + - "vertical": 4334.589655172413, + + "horizontal": 3697, + + "vertical": 4334, + }, + Object { + - "horizontal": 6307.443382352939, + - "vertical": 7394.299999999997, + + "horizontal": 6307, + + "vertical": 7394, + }, + Object { + - "horizontal": 10722.653749999996, + - "vertical": 12570.309999999996, + + "horizontal": 10722, + + "vertical": 12570, + }, + Object { + - "horizontal": 17871.089583333327, + - "vertical": 20950.51666666666, + + "horizontal": 17871, + + "vertical": 20950, + }, + Object { + - "horizontal": 30636.153571428564, + - "vertical": 35915.17142857142, + + "horizontal": 30636, + + "vertical": 35915, + }, + Object { + - "horizontal": 53613.26874999998, + - "vertical": 62851.54999999998, + + "horizontal": 53613, + + "vertical": 62851, + }, + Object { + - "horizontal": 89355.44791666664, + - "vertical": 104752.5833333333, + + "horizontal": 89355, + + "vertical": 104752, + }, + Object { + - "horizontal": 153180.7678571428, + - "vertical": 179575.8571428571, + + "horizontal": 153180, + + "vertical": 179575, + }, + Object { + - "horizontal": 268066.34374999994, + - "vertical": 314257.74999999994, + + "horizontal": 268066, + + "vertical": 314257, + }, + Object { + - "horizontal": 446777.23958333326, + - "vertical": 523762.91666666657, + + "horizontal": 446777, + + "vertical": 523762, + }, + Object { + - "horizontal": 765903.8392857141, + - "vertical": 897879.2857142856, + + "horizontal": 765903, + + "vertical": 897879, + }, + Object { + - "horizontal": 1276506.3988095236, + - "vertical": 1496465.4761904757, + + "horizontal": 1276506, + + "vertical": 1496465, + }, + Object { + - "horizontal": 2144530.7499999995, + - "vertical": 2514061.9999999995, + + "horizontal": 2144530, + + "vertical": 2514061, + }, + ], + + }, + + "zoom": Object { + + "maxNativeZoom": 25, + + "maxZoom": 25, + + "minNativeZoom": 25, + + "minZoom": 0, + }, + } + + 116 | (map) => map.querySelector('.point_2').extent + 117 | ); + > 118 | expect(extent).toEqual(data.pointExtentNoZoom); + | ^ + 119 | }); + 120 | + 121 | test('Zoom to with no zoom attribute', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\mapFeature.test.js:118:20 + + 28) core\mapFeature.test.js:121:7 › Playwright MapFeature Custom Element Tests › Zoom to with no zoom attribute + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 5 + Received: 25 + + 124 | return +map.zoom; + 125 | }); + > 126 | expect(mapZoom).toEqual(5); + | ^ + 127 | }); + 128 | + 129 | test('Get geojson representation of with single geometry', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\mapFeature.test.js:126:21 + + 29) core\mapFeature.test.js:175:7 › Playwright MapFeature Custom Element Tests › Default click method test + + Error: page.$eval: TypeError: Cannot read properties of undefined (reading 'isPopupOpen') + + at eval (eval at evaluate (:208:30), :4:42) + at UtilityScript.evaluate (:210:17) + at UtilityScript. (:1:44) + at eval (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\eval at evaluate (:208:30), :4:42) + at UtilityScript.evaluate (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\:210:17) + at UtilityScript. (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\:1:44) + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\mapFeature.test.js:178:30 + + 30) core\metaDefault.test.js:60:7 › Playwright Missing Min Max Attribute, Meta Default Tests › Inline layer extent test + + Error: expect(received).toEqual(expected) // deep equality + + - Expected - 2 + + Received + 2 + + Object { + - "horizontal": -3263369.215138428, + - "vertical": 4046262.80585894, + + "horizontal": -11940075.314370813, + + "vertical": 10941501.97685327, + } + + 67 | expect(extent.hasOwnProperty('bottomRight')).toBeTruthy(); + 68 | expect(extent.hasOwnProperty('projection')).toBeTruthy(); + > 69 | expect(extent.topLeft.pcrs).toEqual(expectedPCRSFirstLayer.topLeft); + | ^ + 70 | expect(extent.bottomRight.pcrs).toEqual(expectedPCRSFirstLayer.bottomRight); + 71 | expect(extent.topLeft.gcrs).toEqual(expectedGCRSFirstLayer.topLeft); + 72 | expect(extent.bottomRight.gcrs).toEqual(expectedGCRSFirstLayer.bottomRight); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\metaDefault.test.js:69:33 + + 31) core\missingMetaParameters.test.js:18:7 › Missing Parameters Test › Static features with missing & + + Error: expect(received).toEqual(expected) // deep equality + + - Expected - 2 + + Received + 2 + + Object { + - "horizontal": -34655800, + - "vertical": 39310000, + + "horizontal": -423497381.5764965, + + "vertical": 2171501.481669627, + } + + 22 | ); + 23 | + > 24 | expect(layerController.topLeft.pcrs).toEqual({ + | ^ + 25 | horizontal: -34655800, + 26 | vertical: 39310000 + 27 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\missingMetaParameters.test.js:24:42 + + 32) core\missingMetaParameters.test.js:40:7 › Missing Parameters Test › Static tiles with missing + + TypeError: Cannot read properties of null (reading 'topLeft') + + 44 | ); + 45 | + > 46 | expect(layerController.topLeft.pcrs).toEqual({ + | ^ + 47 | horizontal: -4175739.0398780815, + 48 | vertical: 5443265.599864535 + 49 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\missingMetaParameters.test.js:46:28 + + 33) core\missingMetaParameters.test.js:63:7 › Missing Parameters Test › Templated features with missing + + TypeError: Cannot read properties of null (reading 'topLeft') + + 67 | ); + 68 | + > 69 | expect(layerController.topLeft.pcrs).toEqual({ + | ^ + 70 | horizontal: 1501645.2210838948, + 71 | vertical: -66110.70639331453 + 72 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\missingMetaParameters.test.js:69:28 + + 34) core\missingMetaParameters.test.js:85:7 › Missing Parameters Test › Templated tiles with missing & extent + + TypeError: Cannot read properties of null (reading 'topLeft') + + 89 | ); + 90 | + > 91 | expect(layerController.topLeft.pcrs).toEqual({ + | ^ + 92 | horizontal: -180, + 93 | vertical: 90 + 94 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\missingMetaParameters.test.js:91:28 + + 35) core\missingMetaParameters.test.js:107:7 › Missing Parameters Test › Templated image with missing + + TypeError: Cannot read properties of null (reading 'topLeft') + + 111 | ); + 112 | + > 113 | expect(layerController.topLeft.pcrs).toEqual({ + | ^ + 114 | horizontal: 28448056, + 115 | vertical: 42672085 + 116 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\missingMetaParameters.test.js:113:28 + + 36) core\styleParsing.test.js:36:7 › Style Parsed and Implemented Test › CSS from a retrieved MapML file added inorder inside templated-layer container + + Error: page.$eval: Error: failed to find element matching selector "css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-templatedlayer-container > div > div > link" + + 35 | + 36 | test('CSS from a retrieved MapML file added inorder inside templated-layer container', async () => { + > 37 | const firstStyle = await page.$eval( + | ^ + 38 | 'css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-templatedlayer-container > div > div > link', + 39 | (styleE) => styleE.outerHTML + 40 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\styleParsing.test.js:37:35 + + 37) core\styleParsing.test.js:58:7 › Style Parsed and Implemented Test › CSS from a retrieved MapML File added to templated-layer container + + Error: expect(received).toBeTruthy() + + Received: null + + 63 | 'css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(2) > div.leaflet-layer.mapml-templatedlayer-container > div > div > link:nth-child(2)' + 64 | ); + > 65 | await expect(foundStyleLinkOne).toBeTruthy(); + | ^ + 66 | await expect(foundStyleLinkTwo).toBeTruthy(); + 67 | }); + 68 | + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\styleParsing.test.js:65:37 + + 38) core\touchDevice.test.js:21:7 › Playwright touch device tests › Tap/Long press to show layer control + + Error: expect(received).toEqual(expected) // deep equality + + Expected: true + Received: false + + 49 | el._isExpanded + 50 | ); + > 51 | expect(className).toEqual(true); + | ^ + 52 | + 53 | // expect the layer context menu not show after the long press + 54 | const aHandle = await page.evaluateHandle(() => + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\touchDevice.test.js:51:23 + + 39) core\zoomChangeProjection.test.js:35:7 › Playwright zoomin zoomout Projection Change Tests › zoomout link changes projections + + Test timeout of 30000ms exceeded. + + Error: page.click: Target closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.leaflet-control-zoom.leaflet-bar.leaflet-control > a.leaflet-control-zoom-out') + locator resolved to + attempting click action + waiting for element to be visible, enabled and stable + element is not enabled - waiting... + ============================================================ + + 34 | + 35 | test('zoomout link changes projections', async () => { + > 36 | await page.click( + | ^ + 37 | 'div > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.leaflet-control-zoom.leaflet-bar.leaflet-control > a.leaflet-control-zoom-out' + 38 | ); + 39 | await page.waitForTimeout(1000); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\core\zoomChangeProjection.test.js:36:16 + + 40) geojson\geojson2mapml.test.js:112:7 › GeoJSON API - geojson2mapml › Feature ──────────── + + Error: expect(received).toEqual(expected) // deep equality + + Expected: "Milk River-112.0866667 49.15
Property nameProperty value
id7d615dd1d05511d892e2080020a0f4c9
label_frMilk River
label_enMilk River
label_abMilk River
type_frTOWN-Ville
type_enTOWN-Town
" + Received: "Milk River-112.0866667 49.15
Property nameProperty value
id7d615dd1d05511d892e2080020a0f4c9
label_frMilk River
label_enMilk River
label_abMilk River
type_frTOWN-Ville
type_enTOWN-Town
" + + 116 | (node) => node.outerHTML + 117 | ); + > 118 | expect(out).toEqual(exp); + | ^ + 119 | }); + 120 | }); + 121 | + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\geojson\geojson2mapml.test.js:118:17 + + 41) layers\clientTemplatedTileLayer.test.js:18:7 › Playwright Client Tile Tests › Custom Tiles Loaded In, Accurate Coordinates + + Error: page.$eval: Error: failed to find element matching selector "xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.leaflet-layer.mapml-templatedlayer-container > div > div > div:nth-child(1) > p" + + 17 | + 18 | test('Custom Tiles Loaded In, Accurate Coordinates', async () => { + > 19 | const one = await page.$eval( + | ^ + 20 | 'xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.leaflet-layer.mapml-templatedlayer-container > div > div > div:nth-child(1) > p', + 21 | (tile) => tile.textContent + 22 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\clientTemplatedTileLayer.test.js:19:28 + + 42) layers\CustomProjectionLayers.test.js:20:7 › Custom Projection Feature & Extent Tests › map-extent Image access ._layer + + Error: expect(received).toEqual(expected) // deep equality + + Expected: "object" + Received: "undefined" + + 24 | (map) => typeof map.getElementsByTagName('map-extent')[0]._layer + 25 | ); + > 26 | expect(layer).toEqual('object'); + | ^ + 27 | }); + 28 | + 29 | test.describe('Feature', () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\CustomProjectionLayers.test.js:26:19 + + 43) layers\CustomProjectionLayers.test.js:30:9 › Custom Projection Feature & Extent Tests › Feature › access ._layer + + Error: expect(received).toEqual(expected) // deep equality + + Expected: "object" + Received: "undefined" + + 34 | (map) => typeof map.getElementsByTagName('map-feature')[0]._layer + 35 | ); + > 36 | expect(layer).toEqual('object'); + | ^ + 37 | }); + 38 | + 39 | test('feature method - ZoomTo()', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\CustomProjectionLayers.test.js:36:21 + + 44) layers\general\isVisible.js:13:9 › Playwright featureLayer (Static Features) Layer Tests › isVisible Property Tests for featureLayer › isVisible property false when zoomed out of bounds (zooming in) + + Error: expect(received).toEqual(expected) // deep equality + + Expected: true + Received: false + + 22 | ); + 23 | + > 24 | expect(layerLabelItalic).toEqual(true); + | ^ + 25 | }); + 26 | test('isVisible property false when zoomed out of bounds (zooming out)', async () => { + 27 | for (let i = 0; i < zoomOut + zoomIn - 1; i++) { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\isVisible.js:24:32 + + 45) layers\general\isVisible.js:26:9 › Playwright featureLayer (Static Features) Layer Tests › isVisible Property Tests for featureLayer › isVisible property false when zoomed out of bounds (zooming out) + + Test timeout of 30000ms exceeded. + + 46) layers\general\zoomLimit.js:17:9 › Playwright featureLayer (Static Features) Layer Tests › Map Zoom Limit Tests for featureLayer › Limit map zooming (zooming in) + + Error: expect(received).toMatch(expected) + + Expected substring: "disabled" + Received string: "leaflet-control-zoom-in" + + 25 | ); + 26 | + > 27 | expect(zoomButton).toMatch('disabled'); + | ^ + 28 | }); + 29 | + 30 | test('Allow zooming before reaching limit (zooming in)', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\zoomLimit.js:27:26 + + 47) layers\general\zoomLimit.js:41:9 › Playwright featureLayer (Static Features) Layer Tests › Map Zoom Limit Tests for featureLayer › Limit map zooming (zooming out) + + Test timeout of 30000ms exceeded. + + 48) layers\general\extentProperty.js:31:9 › Playwright featureLayer (Static Features) Layer Tests › .extent Property Tests for featureLayer › 2nd .extent test + + TypeError: Cannot read properties of null (reading 'hasOwnProperty') + + 34 | (layer) => layer.extent + 35 | ); + > 36 | expect(extent.hasOwnProperty('zoom')).toBeTruthy(); + | ^ + 37 | expect(extent.hasOwnProperty('topLeft')).toBeTruthy(); + 38 | expect(extent.hasOwnProperty('bottomRight')).toBeTruthy(); + 39 | expect(extent.hasOwnProperty('projection')).toBeTruthy(); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\extentProperty.js:36:21 + + 49) layers\featureLayer.test.js:81:9 › Playwright featureLayer (Static Features) Layer Tests › Retrieved Static Features Tests › valid .extent + + Error: expect(received).toEqual(expected) // deep equality + + - Expected - 3 + + Received + 3 + + Object { + - "maxNativeZoom": 0, + - "maxZoom": 2, + + "maxNativeZoom": 25, + + "maxZoom": 25, + "minNativeZoom": 0, + - "minZoom": 2, + + "minZoom": 0, + } + + 100 | vertical: -60.79110984572508 + 101 | }); + > 102 | expect(layerExtent.zoom).toEqual({ + | ^ + 103 | maxNativeZoom: 0, + 104 | minNativeZoom: 0, + 105 | maxZoom: 2, + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\featureLayer.test.js:102:32 + + 50) layers\multipleExtents.test.js:14:7 › Adding and Removing Multiple Extents › Layer's multiple extents display on map and in layer control + + Error: page.$eval: Error: failed to find element matching selector "div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div:nth-child(4) > div > div" + + 13 | + 14 | test("Layer's multiple extents display on map and in layer control", async () => { + > 15 | const cbmtExtent = await page.$eval( + | ^ + 16 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div:nth-child(4) > div > div', + 17 | (div) => div.childElementCount + 18 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleExtents.test.js:15:35 + + 51) layers\multipleExtents.test.js:34:7 › Adding and Removing Multiple Extents › Changing extent opacity, removing and adding extent effects expected changes to map container layer content + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 2 + Received: 1 + + 63 | (div) => div.className + 64 | ); + > 65 | expect(startExtentCount).toEqual(2); + | ^ + 66 | expect(alabama).toEqual('leaflet-layer mapml-templatedlayer-container'); + 67 | + 68 | // restore the cbmt extent + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleExtents.test.js:65:30 + + 52) layers\multipleExtents.test.js:116:7 › Adding and Removing Multiple Extents › Changing extent opacity, removing and adding extent effects expected changes to only that specific content + + Test timeout of 30000ms exceeded. + + 53) layers\multipleExtents.test.js:180:7 › Adding and Removing Multiple Extents › Extents retain their state when turning layer off and on + + Test timeout of 30000ms exceeded. + + 54) layers\multipleExtents.test.js:244:7 › Multiple Extents Bounds Tests › Only Extent Bounds show in debug mode + + Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected) + + Locator: locator('.mapml-debug-vectors') + Expected: 4 + Received: 8 + Call log: + - expect.toHaveCount with timeout 5000ms + - waiting for locator('.mapml-debug-vectors') + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + - locator resolved to 8 elements + - unexpected value "8" + + + 252 | // the bounds expected to show include the "projection center", and 3 bounds + 253 | // one for each map-link + > 254 | await expect(page.locator('.mapml-debug-vectors')).toHaveCount(4); + | ^ + 255 | await expect( + 256 | page.locator('.mapml-debug-vectors.projection-centre ') + 257 | ).toHaveCount(1); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleExtents.test.js:254:56 + + 55) layers\multipleExtents.test.js:266:7 › Multiple Extents Bounds Tests › When unchecked, extent bounds removed from debug layer + + Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected) + + Locator: locator('.mapml-debug-vectors') + Expected: 3 + Received: 0 + Call log: + - expect.toHaveCount with timeout 5000ms + - waiting for locator('.mapml-debug-vectors') + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + - locator resolved to 0 elements + - unexpected value "0" + + + 280 | await page.$eval('body > mapml-viewer', (map) => map.toggleDebug()); + 281 | + > 282 | await expect(page.locator('.mapml-debug-vectors')).toHaveCount(3); + | ^ + 283 | await expect( + 284 | page.locator('.mapml-debug-vectors.projection-centre ') + 285 | ).toHaveCount(1); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleExtents.test.js:282:56 + + 56) layers\multipleExtents.test.js:294:7 › Multiple Extents Bounds Tests › Checking an extent adds its bounds, unchecking an extent removes its bounds + + Test timeout of 30000ms exceeded. + + 57) layers\multipleExtents.test.js:347:7 › Multiple Extents Bounds Tests › Extent is individually disabled in layer control when out of bounds + + Error: Timed out 5000ms waiting for expect(locator).toHaveCSS(expected) + + Locator: getByText('alabama_feature') + Expected string: "normal" + Received string: "italic" + Call log: + - expect.toHaveCSS with timeout 5000ms + - waiting for getByText('alabama_feature') + - locator resolved to 354 | await expect(alabamaExtentItem).toHaveCSS('font-style', 'normal'); + | ^ + 355 | + 356 | const alabamaMapExtent = page.locator('map-extent[label=alabama_feature]'); + 357 | await expect(alabamaMapExtent).toHaveCount(1); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleExtents.test.js:354:37 + + 58) layers\multipleExtents.test.js:484:7 › Multiple Extents Reordering and ZIndices Tests › Ensure Same Order When Extent and Layer Checked Off/On + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 1 + Received: 2 + + 518 | (divs) => divs.length + 519 | ); + > 520 | expect(cbmt).toEqual(1); + | ^ + 521 | + 522 | await page.click("text='Multiple Extents'"); + 523 | layersCount = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleExtents.test.js:520:18 + + 59) layers\multipleExtents.test.js:559:7 › Multiple Extents Reordering and ZIndices Tests › Move Extent Back Up in the Layer Control + + TypeError: Cannot read properties of null (reading 'x') + + 564 | let controlBBox = await control.boundingBox(); + 565 | await page.mouse.move( + > 566 | controlBBox.x + controlBBox.width / 2, + | ^ + 567 | controlBBox.y + controlBBox.height / 2 + 568 | ); + 569 | await page.mouse.down(); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleExtents.test.js:566:19 + + 60) layers\multipleHeterogeneousQueryExtents.test.js:18:7 › Multiple Extent Queries with heterogeneous response content types › Query multiple overlapping extents which return heterogeneous document types (text/mapml, text/html) + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: "This is an HTML document response for a MapML query." + Received: " Multiple Heterogeneous Query Extents Test Data file This is an HTML document response for a MapML query. " + + 31 | (iframe) => iframe.contentWindow.document.querySelector('body').innerText + 32 | ); + > 33 | expect(content).toBe( + | ^ + 34 | 'This is an HTML document response for a MapML query.' + 35 | ); + 36 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleHeterogeneousQueryExtents.test.js:33:21 + + 61) layers\multipleHeterogeneousQueryExtents.test.js:37:7 › Multiple Extent Queries with heterogeneous response content types › Re-order queryable extents, verify response order changes accordingly + + Test timeout of 30000ms exceeded. + + Error: page.waitForSelector: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > p') to be visible + ============================================================ + + 95 | + 96 | await page.click('div'); + > 97 | await page.waitForSelector( + | ^ + 98 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > p' + 99 | ); + 100 | let numFeatures = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleHeterogeneousQueryExtents.test.js:97:16 + + 62) layers\multipleHeterogeneousQueryExtents.test.js:127:7 › Multiple Extent Queries with heterogeneous response content types › Enusre extents that are unchecked or removed are not included in query results + + Error: expect(received).toEqual(expected) // deep equality + + Expected: "mapml query response" + Received: "html query response" + + 136 | (span) => span.innerText.toLowerCase() + 137 | ); + > 138 | expect(secondExtentInLayerControl).toEqual('mapml query response'); + | ^ + 139 | let thirdExtentInLayerControl = await page.$eval( + 140 | 'fieldset.mapml-layer-grouped-extents > fieldset:nth-child(3) span', + 141 | (span) => span.innerText.toLowerCase() + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleHeterogeneousQueryExtents.test.js:138:40 + + 63) layers\multipleQueryExtents.test.js:82:7 › Multiple Extent Query Tests › Navigate back from second query result set to end of first query result set by clicking '< / Previous' + + Error: expect(received).toBeFalsy() + + Received: "ref: " + + 86 | (g) => (g.firstElementChild ? g.firstElementChild : false) + 87 | ); + > 88 | expect(feature).toBeFalsy(); + | ^ + 89 | + 90 | const popup = await page.$eval( + 91 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > iframe', + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\multipleQueryExtents.test.js:88:21 + + 64) layers\queryLink.test.js:103:9 › Playwright Query Link Tests › Queried Feature Tests › First feature added + popup content updated + + Error: page.$eval: Error: failed to find element matching selector "div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div:nth-child(5) > svg > g > g > path" + + 109 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div' + 110 | ); + > 111 | const feature = await page.$eval( + | ^ + 112 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div:nth-child(5) > svg > g > g > path', + 113 | (tile) => tile.getAttribute('d') + 114 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:111:34 + + 65) layers\queryLink.test.js:141:9 › Playwright Query Link Tests › Queried Feature Tests › Next feature added + popup content updated + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)') + ============================================================ + + 140 | + 141 | test('Next feature added + popup content updated ', async () => { + > 142 | await page.click( + | ^ + 143 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)' + 144 | ); + 145 | const feature = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:142:18 + + 66) layers\queryLink.test.js:175:9 › Playwright Query Link Tests › Queried Feature Tests › Previous feature added + popup content updated + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(2)') + ============================================================ + + 174 | + 175 | test('Previous feature added + popup content updated ', async () => { + > 176 | await page.click( + | ^ + 177 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(2)' + 178 | ); + 179 | const feature = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:176:18 + + 67) layers\queryLink.test.js:209:9 › Playwright Query Link Tests › Queried Feature Tests › PCRS feature added + popup content updated + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)') + ============================================================ + + 209 | test('PCRS feature added + popup content updated ', async () => { + 210 | for (let i = 0; i < 2; i++) + > 211 | await page.click( + | ^ + 212 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)' + 213 | ); + 214 | const feature = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:211:20 + + 68) layers\queryLink.test.js:227:9 › Playwright Query Link Tests › Queried Feature Tests › TCRS feature added + popup content updated + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)') + ============================================================ + + 226 | + 227 | test('TCRS feature added + popup content updated ', async () => { + > 228 | await page.click( + | ^ + 229 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)' + 230 | ); + 231 | const feature = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:228:18 + + 69) layers\queryLink.test.js:244:9 › Playwright Query Link Tests › Queried Feature Tests › Tilematrix feature added + popup content updated + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)') + ============================================================ + + 243 | + 244 | test('Tilematrix feature added + popup content updated ', async () => { + > 245 | await page.click( + | ^ + 246 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)' + 247 | ); + 248 | const feature = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:245:18 + + 70) layers\queryLink.test.js:260:9 › Playwright Query Link Tests › Queried Feature Tests › Synthesized point, valid location + + + Test timeout of 30000ms exceeded. + + Error: page.click: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)') + ============================================================ + + 259 | }); + 260 | test('Synthesized point, valid location ', async () => { + > 261 | await page.click( + | ^ + 262 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > div > nav > button:nth-child(4)' + 263 | ); + 264 | const feature = await page.$eval( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:261:18 + + 71) layers\queryLink.test.js:282:9 › Playwright Query Link Tests › Queried Feature Tests › 'Zoom to here' link test + + Test timeout of 30000ms exceeded. + + Error: page.waitForSelector: Page closed + =========================== logs =========================== + waiting for locator('div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div') to be visible + ============================================================ + + 300 | + 301 | await page.click('div'); + > 302 | await page.waitForSelector( + | ^ + 303 | 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane > div' + 304 | ); + 305 | await page.keyboard.press('Tab'); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:302:18 + + 72) layers\queryLink.test.js:331:9 › Playwright Query Link Tests › Queried Feature Tests › hidden layer is not queryable, hidden map-extent doesn't matter + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + + 336 | let popups = await popupPane.evaluate((pane) => pane.childElementCount); + 337 | // expect results + > 338 | expect(popups).toBe(1); + | ^ + 339 | const layer = await page.locator('layer-'); + 340 | // dismiss the popup + 341 | await page.keyboard.press('Escape'); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\queryLink.test.js:338:22 + + 73) layers\general\isVisible.js:13:9 › Playwright StaticTile Layer Tests › isVisible Property Tests for staticTileLayer › isVisible property false when zoomed out of bounds (zooming in) + + Test timeout of 30000ms exceeded. + + 74) layers\general\isVisible.js:26:9 › Playwright StaticTile Layer Tests › isVisible Property Tests for staticTileLayer › isVisible property false when zoomed out of bounds (zooming out) + + Test timeout of 30000ms exceeded. + + 75) layers\general\zoomLimit.js:17:9 › Playwright StaticTile Layer Tests › Map Zoom Limit Tests for staticTileLayer › Limit map zooming (zooming in) + + Error: expect(received).toMatch(expected) + + Expected substring: "disabled" + Received string: "leaflet-control-zoom-in" + + 25 | ); + 26 | + > 27 | expect(zoomButton).toMatch('disabled'); + | ^ + 28 | }); + 29 | + 30 | test('Allow zooming before reaching limit (zooming in)', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\zoomLimit.js:27:26 + + 76) layers\general\zoomLimit.js:41:9 › Playwright StaticTile Layer Tests › Map Zoom Limit Tests for staticTileLayer › Limit map zooming (zooming out) + + Test timeout of 30000ms exceeded. + + 77) layers\general\extentProperty.js:17:9 › Playwright StaticTile Layer Tests › .extent Property Tests for staticTileLayer › .extent test + + TypeError: Cannot read properties of null (reading 'hasOwnProperty') + + 20 | (layer) => layer.extent + 21 | ); + > 22 | expect(extent.hasOwnProperty('zoom')).toBeTruthy(); + | ^ + 23 | expect(extent.hasOwnProperty('topLeft')).toBeTruthy(); + 24 | expect(extent.hasOwnProperty('bottomRight')).toBeTruthy(); + 25 | expect(extent.hasOwnProperty('projection')).toBeTruthy(); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\extentProperty.js:22:21 + + 78) layers\general\extentProperty.js:31:9 › Playwright StaticTile Layer Tests › .extent Property Tests for staticTileLayer › 2nd .extent test + + TypeError: Cannot read properties of null (reading 'hasOwnProperty') + + 34 | (layer) => layer.extent + 35 | ); + > 36 | expect(extent.hasOwnProperty('zoom')).toBeTruthy(); + | ^ + 37 | expect(extent.hasOwnProperty('topLeft')).toBeTruthy(); + 38 | expect(extent.hasOwnProperty('bottomRight')).toBeTruthy(); + 39 | expect(extent.hasOwnProperty('projection')).toBeTruthy(); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\extentProperty.js:36:21 + + 79) layers\staticTileLayer.test.js:47:9 › Playwright StaticTile Layer Tests › General Tests › Tiles load in on default map zoom level + + Error: page.$eval: Error: failed to find element matching selector ".mapml-static-tile-layer > div" + + 46 | + 47 | test('Tiles load in on default map zoom level', async () => { + > 48 | const tiles = await page.$eval( + | ^ + 49 | '.mapml-static-tile-layer > div', + 50 | (tileGroup) => tileGroup.getElementsByTagName('map-tile').length + 51 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\staticTileLayer.test.js:48:32 + + 80) layers\step\request.js:48:9 › Templated features layer with step › Request Tests › On add requests zoom level 0 + + Error: expect(received).toContain(expected) // indexOf + + Expected substring: "-8030725.916518498-9758400.22013378111151604.1148082329423929.8111929520" + Received string: "" + + 56 | await page.reload(); + 57 | await page.waitForTimeout(500); + > 58 | expect(url).toContain(u1); + | ^ + 59 | }); + 60 | + 61 | test('At zoom level 1, zooming in to 2', async () => { + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\step\request.js:58:19 + + 81) layers\step\request.js:61:9 › Templated features layer with step › Request Tests › At zoom level 1, zooming in to 2 + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 0 + Received: 1 + + 16 | await page.keyboard.press('Equal'); + 17 | await page.waitForTimeout(1000); + > 18 | expect(requests).toEqual(requestCount); + | ^ + 19 | if (url !== '') expect(u).toContain(url); + 20 | } + 21 | + + at zoomIn (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\step\request.js:18:20) + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\step\request.js:64:7 + + 82) layers\step\request.js:67:9 › Templated features layer with step › Request Tests › At zoom level 2, zooming in to 3 + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 1 + Received: 0 + + 16 | await page.keyboard.press('Equal'); + 17 | await page.waitForTimeout(1000); + > 18 | expect(requests).toEqual(requestCount); + | ^ + 19 | if (url !== '') expect(u).toContain(url); + 20 | } + 21 | + + at zoomIn (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\step\request.js:18:20) + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\step\request.js:68:7 + + 83) layers\step\request.js:88:9 › Templated features layer with step › Request Tests › Panning makes new request as needed + + Error: expect(received).toContain(expected) // indexOf + + Expected substring: "-437169.06273812056-1766644.65328931063531588.8747777492202113.28422656663" + Received string: "" + + 100 | } + 101 | + > 102 | expect(u).toContain(panU); + | ^ + 103 | }); + 104 | }); + 105 | }; + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\step\request.js:102:17 + + 84) layers\general\isVisible.js:13:9 › Playwright templatedFeatures Layer Tests › isVisible Property Tests for templatedFeatures › isVisible property false when zoomed out of bounds (zooming in) + + Test timeout of 30000ms exceeded. + + 85) layers\general\isVisible.js:26:9 › Playwright templatedFeatures Layer Tests › isVisible Property Tests for templatedFeatures › isVisible property false when zoomed out of bounds (zooming out) + + Test timeout of 30000ms exceeded. + + 86) layers\general\extentProperty.js:17:9 › Playwright templatedFeatures Layer Tests › .extent Property Tests for templatedFeatures › .extent test + + TypeError: Cannot read properties of null (reading 'hasOwnProperty') + + 20 | (layer) => layer.extent + 21 | ); + > 22 | expect(extent.hasOwnProperty('zoom')).toBeTruthy(); + | ^ + 23 | expect(extent.hasOwnProperty('topLeft')).toBeTruthy(); + 24 | expect(extent.hasOwnProperty('bottomRight')).toBeTruthy(); + 25 | expect(extent.hasOwnProperty('projection')).toBeTruthy(); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\extentProperty.js:22:21 + + 87) layers\general\extentProperty.js:31:9 › Playwright templatedFeatures Layer Tests › .extent Property Tests for templatedFeatures › 2nd .extent test + + TypeError: Cannot read properties of null (reading 'hasOwnProperty') + + 34 | (layer) => layer.extent + 35 | ); + > 36 | expect(extent.hasOwnProperty('zoom')).toBeTruthy(); + | ^ + 37 | expect(extent.hasOwnProperty('topLeft')).toBeTruthy(); + 38 | expect(extent.hasOwnProperty('bottomRight')).toBeTruthy(); + 39 | expect(extent.hasOwnProperty('projection')).toBeTruthy(); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\general\extentProperty.js:36:21 + + 88) layers\templatedFeatures.test.js:48:9 › Playwright templatedFeatures Layer Tests › Templated Features in shadowRoot › Templated features attaches to MapExtent's shadow root + + Error: page.evaluate: TypeError: Cannot read properties of null (reading 'querySelectorAll') + + at eval (eval at evaluate (:208:30), :1:65) + at eval () + at UtilityScript.evaluate (:208:30) + at UtilityScript. (:1:44) + at eval (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\eval at evaluate (:208:30), :1:65) + at UtilityScript.evaluate (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\:208:30) + at UtilityScript. (C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\:1:44) + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeatures.test.js:52:35 + + 89) layers\templatedFeatures.test.js:61:9 › Playwright templatedFeatures Layer Tests › Templated Features Zoom To Extent Tests › Zoom to layer applies meta extent + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 1508601.8288036585 + Received: 1509616.5079163536 + + 77 | `document.querySelector('#map2').extent.bottomRight.pcrs` + 78 | ); + > 79 | expect(endTopLeft.horizontal).toBe(1508601.8288036585); + | ^ + 80 | expect(endTopLeft.vertical).toBe(-169068.77063754946); + 81 | expect(endBottomRight.horizontal).toBe(1512570.5867411792); + 82 | expect(endBottomRight.vertical).toBe(-173037.52857506275); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeatures.test.js:79:37 + + 90) layers\templatedFeatures.test.js:85:9 › Playwright templatedFeatures Layer Tests › Templated Features Zoom To Extent Tests › Templated features zoomTo method test + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 1508601.8288036585 + Received: 1509616.5079163536 + + 93 | `document.querySelector('#map2').zoom` + 94 | ); + > 95 | expect(startTopLeft.horizontal).toBe(1508601.8288036585); + | ^ + 96 | expect(startTopLeft.vertical).toBe(-169068.77063754946); + 97 | expect(startBottomRight.horizontal).toBe(1512570.5867411792); + 98 | expect(startBottomRight.vertical).toBe(-173037.52857506275); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeatures.test.js:95:39 + + 91) layers\templatedFeatures.test.js:121:9 › Playwright templatedFeatures Layer Tests › Retreived Features Loading Tests › Loading in tilematrix feature + + Error: page.$eval: Error: failed to find element matching selector "xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div.leaflet-layer.mapml-templatedlayer-container > div > div > svg > g > g:nth-child(3) > path.leaflet-interactive" + + 121 | test('Loading in tilematrix feature', async () => { + 122 | await page.waitForTimeout(200); + > 123 | const feature = await page.$eval( + | ^ + 124 | 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div.leaflet-layer.mapml-templatedlayer-container > div > div > svg > g > g:nth-child(3) > path.leaflet-interactive', + 125 | (tile) => tile.getAttribute('d') + 126 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeatures.test.js:123:34 + + 92) layers\templatedFeatures.test.js:130:9 › Playwright templatedFeatures Layer Tests › Retreived Features Loading Tests › Loading in pcrs feature + + Error: page.$eval: Error: failed to find element matching selector "xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div.leaflet-layer.mapml-templatedlayer-container > div > div > svg > g > g:nth-child(1) > path.leaflet-interactive" + + 129 | + 130 | test('Loading in pcrs feature', async () => { + > 131 | const feature = await page.$eval( + | ^ + 132 | 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div.leaflet-layer.mapml-templatedlayer-container > div > div > svg > g > g:nth-child(1) > path.leaflet-interactive', + 133 | (tile) => tile.getAttribute('d') + 134 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeatures.test.js:131:34 + + 93) layers\templatedFeatures.test.js:138:9 › Playwright templatedFeatures Layer Tests › Retreived Features Loading Tests › Loading in tcrs feature + + Error: page.$eval: Error: failed to find element matching selector "xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div.leaflet-layer.mapml-templatedlayer-container > div > div > svg > g > g:nth-child(2) > path.leaflet-interactive" + + 137 | + 138 | test('Loading in tcrs feature', async () => { + > 139 | const feature = await page.$eval( + | ^ + 140 | 'xpath=//html/body/map/div >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div:nth-child(1) > div.leaflet-layer.mapml-templatedlayer-container > div > div > svg > g > g:nth-child(2) > path.leaflet-interactive', + 141 | (tile) => tile.getAttribute('d') + 142 | ); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeatures.test.js:139:34 + + 94) layers\templatedFeatures.test.js:146:9 › Playwright templatedFeatures Layer Tests › Retreived Features Loading Tests › templated features disabled when panned out of bounds + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + + 161 | }); + 162 | + > 163 | expect(layerAndLayerCheckboxDisabled).toBe(true); + | ^ + 164 | }); + 165 | }); + 166 | }); + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeatures.test.js:163:45 + + 95) layers\templatedFeaturesFilter.test.js:15:7 › Simple query by select values without map extent filter tests › All features loaded at start + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 8 + Received: 0 + + 17 | '.mapml-templatedlayer-container > div > div > svg > g > g' + 18 | ); + > 19 | expect(features.length).toEqual(8); + | ^ + 20 | }); + 21 | test('User can select/filter features by category', async () => { + 22 | const restaurants = await page.$$( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeaturesFilter.test.js:19:29 + + 96) layers\templatedFeaturesFilter.test.js:21:7 › Simple query by select values without map extent filter tests › User can select/filter features by category + + Error: expect(received).toEqual(expected) // deep equality + + Expected: 8 + Received: 0 + + 23 | '.mapml-templatedlayer-container > div > div > svg > g > g' + 24 | ); + > 25 | expect(restaurants.length).toEqual(8); + | ^ + 26 | + 27 | await page.hover('.leaflet-top.leaflet-right > div'); + 28 | await page.click( + + at C:\Users\prush\Documents\GitHub\Web-Map-Custom-Element\test\e2e\layers\templatedFeaturesFilter.test.js:25:32 + + 97) layers\templatedFeaturesFilter.test.js:44:7 › Simple query by select values without map extent filter tests › attributes are copied to layer control