From 6e2355ce72df12f97248c9def6a18329af01f8f0 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 2 May 2025 09:14:46 -0400 Subject: [PATCH] Add map-tile element layer.js - no longer tests to see if the bounds have been previously calculated, updates the _layer.bounds field whenever extent getter executes - MapMLLayer usage refactored to use MapLayer per the element name association - _validateDisabled method updated to account for replacement of StaticTileLayer by MapTileLayer - replaced reliance of _validateDisabled on presence of properties such as MapLayer['_staticTileLayer'] by using the array ['_staticTileLayer','_mapmlvectors','_extentLayer'] only as a driver of the loop used to validate that each child layer of the LayerGroup's extent is enabled or not. the _mapmlvectors step is not completely worked out, will get done in subsequent work tbc... map-extent.js - applied name refactoring required by renaming ExtentLayer.js to MapExtentLayer.js map-feature.js - changed addFeature a bit. changes look ok tbd, but the whole dependence of map-feature.js on MapFeatureLayer.createGeometry as a factory method for creating the geometry as a Leaflet layer should be revisited and hopefully simplified. Why not just construct a geometry via new Geometry? - for the use case of a loaded via a , created a group of new methods, patterned after/ copied from : - _createOrGetFeatureLayer - runs conditional on the feature being loaded as part of processing. Called during connectedCallback - isFirst - determines if this element is the first such element in a sequence of elements named the same i.e. map-feature. If it is, used by _createOrGetFeatureLayer to construct a FeatureLayeer constructed as a holder for features that are constructed and removed with each map movement. - getPrevious - returns the previous sibling element of the with no checks as to what kind of element it is. The notion is that it's only called when isFirst returns false, so the returned sibling element is guaranteed to be a map-feature (well, isFirst tests the name such that it must be equal to the name of this node, which in this case is map-feature). map-link.js - imports and constructs the newly refactored TemplatedFeaturesOrTilesLayer instead of only TemplatedFeaturesLayer map-select.js - changed a comment that mentioned MapMLLayer map-tile.js - new. mapml-viewer.js - imports and defines the map-tile element and HTMLTileElement interface LayerControl.js - udpated a comment that referred to MapMLLayer AnnounceMovement.js updated a comment that referred to MapMLLayer index.js - imports and defines the map-tile element and HTMLTileElement interface from mapml-viewer.js ExtentLayer.js - renamed to MapExtentLayer.js FeatureLayer.js - updated a comment that referred to MapMLLayer MapFeatureLayer.js - new. not used yet MapLayer.js - refactoring of MapMLLayer.js: - adds getContainer public method - in onAdd, removes addition of static tile layer to LayerGroup - removes the use of MapMLLayer['_staticTileLayer'], replaces with using the string '_staticTileLayer' as a key to determine action to be taken in the bounds calculation loop - removes the processTiles construction method MapTileLayer.js - new. Replaces StaticTileLayer. TemplatedFeaturesOrTilesLayer.js - new. replaces TemplatedFeaturesLayer, since we now support in template processing response documents. TemplatedTileLayer.js - updated to support vector tiles that contain elements. map-bounding-box.test.js - add waitForTimeout to overcome flakiness. drag.test.js - replace detection of class that was renamed / removed when StaticTileLayer was eliminated missingMetaParameters.html - add comment to clarify intent to future me map-link-media.html - remove use of external resource, replace with content from test data featureLayer.test.js - udpate comment mentioning MapMLLayer to MapLayer multipleExtents.test.js, multipleExtents.html - update to support new classes used by features or tiles support in templated link. Modernize the test a bit to make it more readable. Still an ugly large test. staticTileLayer.test.js - update templatedPMTilesCBMTILETest.html - added map-link for stylesheet module. Not sure why this test was working previously, maybe I misunderstood, but I think it was a cut and paste error. templatedPMTilesMVTLayer.test.js - changed expectation that layer will not be disabled to expect that layer WILL be disabled, because the particular layer didn't have a stylesheet and so could never be enabled, despite multiple projections available customTCRS.test.js - one test was using static tiles, updated to continue working playwright.config.js - added global directive to ignore https errors. These were causing intermittent problems with tests on work network. See if we can get ci testing to pass... --- .gitignore | 2 +- package-lock.json | 4 +- playwright.config.js | 5 +- src/layer.js | 33 +- src/map-extent.js | 8 +- src/map-feature.js | 91 ++- src/map-link.js | 12 +- src/map-select.js | 2 +- src/map-tile.js | 281 +++++++++ src/mapml-viewer.js | 3 + src/mapml/control/LayerControl.js | 2 +- src/mapml/handlers/AnnounceMovement.js | 2 +- src/mapml/index.js | 2 + src/mapml/layers/FeatureLayer.js | 2 +- .../{ExtentLayer.js => MapExtentLayer.js} | 36 +- src/mapml/layers/MapFeatureLayer.js | 555 ++++++++++++++++++ .../layers/{MapMLLayer.js => MapLayer.js} | 93 ++- src/mapml/layers/MapTileLayer.js | 357 +++++++++++ src/mapml/layers/StaticTileLayer.js | 176 ------ src/mapml/layers/TemplatedFeaturesLayer.js | 376 ------------ .../layers/TemplatedFeaturesOrTilesLayer.js | 323 ++++++++++ src/mapml/layers/TemplatedTileLayer.js | 73 ++- .../api/matchMedia/map-bounding-box.test.js | 1 + test/e2e/core/drag.test.js | 3 +- test/e2e/core/missingMetaParameters.html | 5 +- .../e2e/elements/map-link/map-link-media.html | 2 +- test/e2e/layers/featureLayer.test.js | 2 +- test/e2e/layers/multipleExtents.html | 9 +- test/e2e/layers/multipleExtents.test.js | 71 +-- test/e2e/layers/staticTileLayer.test.js | 28 +- .../layers/templatedPMTilesCBMTILETest.html | 2 +- .../layers/templatedPMTilesMVTLayer.test.js | 4 +- test/e2e/mapml-viewer/customTCRS.test.js | 2 +- test/layers/StaticTileLayer.spec.js | 354 ----------- test/layers/mapMLLayer.spec.js | 137 ----- 35 files changed, 1864 insertions(+), 1194 deletions(-) create mode 100644 src/map-tile.js rename src/mapml/layers/{ExtentLayer.js => MapExtentLayer.js} (55%) create mode 100644 src/mapml/layers/MapFeatureLayer.js rename src/mapml/layers/{MapMLLayer.js => MapLayer.js} (91%) create mode 100644 src/mapml/layers/MapTileLayer.js delete mode 100644 src/mapml/layers/StaticTileLayer.js delete mode 100644 src/mapml/layers/TemplatedFeaturesLayer.js create mode 100644 src/mapml/layers/TemplatedFeaturesOrTilesLayer.js delete mode 100644 test/layers/StaticTileLayer.spec.js delete mode 100644 test/layers/mapMLLayer.spec.js diff --git a/.gitignore b/.gitignore index 393215552..2a30c7d44 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ /test-results/ .idea/ *.iml -test.html \ No newline at end of file +test.html diff --git a/package-lock.json b/package-lock.json index 00b33245e..3a3810702 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@maps4html/mapml", - "version": "0.15.0", + "version": "0.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@maps4html/mapml", - "version": "0.15.0", + "version": "0.16.0", "hasInstallScript": true, "license": "W3C", "devDependencies": { diff --git a/playwright.config.js b/playwright.config.js index 7b9d3f897..0d739b668 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -11,6 +11,7 @@ export default defineConfig({ use: { headless: true, browserName: 'chromium', - baseURL: 'http://localhost:30001/' + baseURL: 'http://localhost:30001/', + ignoreHTTPSErrors: true } -}); \ No newline at end of file +}); diff --git a/src/layer.js b/src/layer.js index dd16c5e2b..6c1e3ed11 100644 --- a/src/layer.js +++ b/src/layer.js @@ -1,7 +1,8 @@ import { setOptions, DomUtil, bounds, point } from 'leaflet'; import { Util } from './mapml/utils/Util.js'; -import { MapMLLayer, mapMLLayer } from './mapml/layers/MapMLLayer.js'; +import { MapLayer, mapLayer } from './mapml/layers/MapLayer.js'; +import { MapTileLayer } from './mapml/layers/MapTileLayer.js'; import { createLayerControlHTML } from './mapml/elementSupport/layers/createLayerControlForLayer.js'; export class BaseLayerElement extends HTMLElement { @@ -72,7 +73,7 @@ export class BaseLayerElement extends HTMLElement { get extent() { // calculate the bounds of all content, return it. - if (this._layer && !this._layer.bounds) { + if (this._layer) { this._layer._calculateBounds(); } return this._layer @@ -296,7 +297,7 @@ export class BaseLayerElement extends HTMLElement { this.selectAlternateOrChangeProjection(); }) .then(() => { - this._layer = mapMLLayer(new URL(this.src, base).href, this, { + this._layer = mapLayer(new URL(this.src, base).href, this, { projection: this.getProjection(), opacity: this.opacity }); @@ -333,7 +334,7 @@ export class BaseLayerElement extends HTMLElement { this.selectAlternateOrChangeProjection(); }) .then(() => { - this._layer = mapMLLayer(null, this, { + this._layer = mapLayer(null, this, { projection: this.getProjection(), opacity: this.opacity }); @@ -467,7 +468,7 @@ export class BaseLayerElement extends HTMLElement { * 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 map-layer._layer: either as a sub-layer directly in the LayerGroup - * (MapMLLayer._layer) or as a sub-layer in the MapMLLayer._mapmlvectors + * (MapLayer._layer) or as a sub-layer in the MapLayer._mapmlvectors * FeatureGroup */ _runMutationObserver(elementsGroup) { @@ -633,6 +634,19 @@ export class BaseLayerElement extends HTMLElement { } _validateDisabled() { + const countTileLayers = () => { + let totalCount = 0; + let disabledCount = 0; + + this._layer.eachLayer((layer) => { + if (layer instanceof MapTileLayer) { + totalCount++; + if (!layer.isVisible()) disabledCount++; + } + }); + + return { totalCount, disabledCount }; + }; // setTimeout is necessary to make the validateDisabled happen later than the moveend operations etc., // to ensure that the validated result is correct setTimeout(() => { @@ -671,10 +685,15 @@ export class BaseLayerElement extends HTMLElement { if (mapExtents[i]._validateDisabled()) disabledExtentCount++; } - } else if (layer[type]) { - // not a templated layer + } else if (type === '_mapmlvectors') { + // inline / static features totalExtentCount++; if (!layer[type].isVisible()) disabledExtentCount++; + } else { + // inline tiles + const tileLayerCounts = countTileLayers(); + totalExtentCount += tileLayerCounts.totalCount; + disabledExtentCount += tileLayerCounts.disabledCount; } } } diff --git a/src/map-extent.js b/src/map-extent.js index cdc1f0395..1941c4ad5 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -1,7 +1,7 @@ import { bounds as Lbounds, point as Lpoint } from 'leaflet'; import { Util } from './mapml/utils/Util.js'; -import { extentLayer } from './mapml/layers/ExtentLayer.js'; +import { mapExtentLayer } from './mapml/layers/MapExtentLayer.js'; import { createLayerControlExtentHTML } from './mapml/elementSupport/extents/createLayerControlForExtent.js'; /* global M */ @@ -251,7 +251,7 @@ export class HTMLExtentElement extends HTMLElement { // when projection is changed, the parent map-layer._layer is created (so whenReady is fulfilled) but then removed, // then the map-extent disconnectedCallback will be triggered by map-layer._onRemove() (clear the shadowRoot) // even before connectedCallback is finished - // in this case, the microtasks triggered by the fulfillment of the removed MapMLLayer should be stopped as well + // in this case, the microtasks triggered by the fulfillment of the removed MapLayer should be stopped as well // !this.isConnected <=> the disconnectedCallback has run before if (!this.isConnected) return; /* jshint ignore:start */ @@ -263,7 +263,7 @@ export class HTMLExtentElement 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 = extentLayer({ + this._extentLayer = mapExtentLayer({ opacity: this.opacity, crs: M[this.units], extentZIndex: Array.from( @@ -432,7 +432,7 @@ export class HTMLExtentElement extends HTMLElement { _handleChange() { // add _extentLayer to map if map-extent is checked, otherwise remove it if (this.checked && !this.disabled && this.parentLayer._layer) { - // can be added to mapmllayer layerGroup no matter map-layer is checked or not + // can be added to MapLayer LayerGroup no matter map-layer is checked or not this._extentLayer.addTo(this.parentLayer._layer); this._extentLayer.setZIndex( Array.from( diff --git a/src/map-feature.js b/src/map-feature.js index 5ae172fd4..338c05769 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -1,5 +1,7 @@ -import { bounds, point } from 'leaflet'; +import { bounds, point, extend } from 'leaflet'; +import { featureLayer } from './mapml/layers/FeatureLayer.js'; +import { featureRenderer } from './mapml/features/featureRenderer.js'; import { Util } from './mapml/utils/Util.js'; import proj4 from 'proj4'; @@ -180,6 +182,9 @@ export class HTMLFeatureElement extends HTMLElement { this._parentEl.parentElement?.hasAttribute('data-moving') ) return; + if (this._parentEl.nodeName === 'MAP-LINK') { + this._createOrGetFeatureLayer(); + } // use observer to monitor the changes in mapFeature's subtree // (i.e. map-properties, map-featurecaption, map-coordinates) this._observer = new MutationObserver((mutationList) => { @@ -256,17 +261,97 @@ export class HTMLFeatureElement extends HTMLElement { addFeature(layerToAddTo) { this._featureLayer = layerToAddTo; - let parentLayer = this.getLayerEl(); // "synchronize" the event handlers between map-feature and if (!this.querySelector('map-geometry')) return; let fallbackCS = this._getFallbackCS(); - let content = parentLayer.src ? parentLayer.shadowRoot : parentLayer; this._geometry = layerToAddTo.createGeometry(this, fallbackCS); // side effect: extends `this` with this._groupEl if successful, points to svg g element that renders to map SD if (!this._geometry) return; + this._geometry._layerEl = this.getLayerEl(); layerToAddTo.addLayer(this._geometry); this._setUpEvents(); } + isFirst() { + // Get the previous element sibling + const prevSibling = this.previousElementSibling; + + // If there's no previous sibling, return true + if (!prevSibling) { + return true; + } + + // Compare the node names (tag names) - return true if they're different + return this.nodeName !== prevSibling.nodeName; + } + getPrevious() { + // Check if this is the first element of a sequence + if (this.isFirst()) { + return null; // No previous element available + } + + // Since we know it's not the first, we can safely return the previous element sibling + return this.previousElementSibling; + } + _createOrGetFeatureLayer() { + if (this.isFirst() && this._parentEl._templatedLayer) { + const parentElement = this._parentEl; + let map = parentElement.getMapEl()._map; + + // Create a new FeatureLayer + this._featureLayer = featureLayer(null, { + // pass the vector layer a renderer of its own, otherwise leaflet + // puts everything into the overlayPane + renderer: featureRenderer(), + // pass the vector layer the container for the parent into which + // it will append its own container for rendering into + pane: parentElement._templatedLayer.getContainer(), + // the bounds will be static, fixed, constant for the lifetime of the layer + layerBounds: parentElement.getBounds(), + zoomBounds: this._getZoomBounds(), + projection: map.options.projection, + mapEl: parentElement.getMapEl(), + onEachFeature: function (properties, geometry) { + if (properties) { + const popupOptions = { + autoClose: false, + autoPan: true, + maxHeight: map.getSize().y * 0.5 - 50, + maxWidth: map.getSize().x * 0.7, + minWidth: 165 + }; + var c = document.createElement('div'); + c.classList.add('mapml-popup-content'); + c.insertAdjacentHTML('afterbegin', properties.innerHTML); + geometry.bindPopup(c, popupOptions); + } + } + }); + // this is used by DebugOverlay testing "multipleExtents.test.js + // but do we really need or want each feature to have the bounds of the + // map link? tbd + extend(this._featureLayer.options, { + _leafletLayer: Object.assign(this._featureLayer, { + _layerEl: this.getLayerEl() + }) + }); + + this.addFeature(this._featureLayer); + + // add featureLayer to TemplatedFeaturesOrTilesLayer of the parentElement + if ( + parentElement._templatedLayer && + parentElement._templatedLayer.addLayer + ) { + parentElement._templatedLayer.addLayer(this._featureLayer); + } + } else { + // get the previous feature's layer + this._featureLayer = this.getPrevious()?._featureLayer; + if (this._featureLayer) { + this.addFeature(this._featureLayer); + } + } + } _setUpEvents() { ['click', 'focus', 'blur', 'keyup', 'keydown'].forEach((name) => { // when is clicked / focused / blurred diff --git a/src/map-link.js b/src/map-link.js index 600b2f423..83ed24ba7 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -10,7 +10,7 @@ import { import { Util } from './mapml/utils/Util.js'; import { templatedImageLayer } from './mapml/layers/TemplatedImageLayer.js'; import { templatedTileLayer } from './mapml/layers/TemplatedTileLayer.js'; -import { templatedFeaturesLayer } from './mapml/layers/TemplatedFeaturesLayer.js'; +import { templatedFeaturesOrTilesLayer } from './mapml/layers/TemplatedFeaturesOrTilesLayer.js'; import { templatedPMTilesLayer } from './mapml/layers/TemplatedPMTilesLayer.js'; /* global M */ @@ -436,7 +436,8 @@ export class HTMLLinkElement extends HTMLElement { // be loaded as part of a templated layer processing i.e. on moveend // and the generated that implements this should be located // in the parent ._templatedLayer.container root node if - // the _templatedLayer is an instance of TemplatedTileLayer or TemplatedFeaturesLayer + // the _templatedLayer is an instance of TemplatedTileLayer or + // TemplatedFeaturesOrTilesLayer // // if the parent node (or the host of the shadow root parent node) is map-layer, the link should be created in the _layer // container @@ -551,12 +552,15 @@ export class HTMLLinkElement extends HTMLElement { if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); } - this._templatedLayer = templatedFeaturesLayer(this._templateVars, { + // Use the FeaturesTilesLayerGroup to handle both map-feature and map-tile elements + this._templatedLayer = templatedFeaturesOrTilesLayer(this._templateVars, { zoomBounds: this.getZoomBounds(), extentBounds: this.getBounds(), zIndex: this.zIndex, pane: this.parentExtent._extentLayer.getContainer(), - linkEl: this + linkEl: this, + projection: this.mapEl._map.options.projection, + renderer: this.mapEl._map.options.renderer }).addTo(this.parentExtent._extentLayer); } else if (this.rel === 'query') { if (!this.shadowRoot) { diff --git a/src/map-select.js b/src/map-select.js index 56451839d..c4eae4091 100644 --- a/src/map-select.js +++ b/src/map-select.js @@ -39,7 +39,7 @@ export class HTMLSelectElement extends HTMLElement { 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... + // originally part of MapLayer... // // use a throwaway div to parse the input from MapML into HTML this._createLayerControlForSelect(); diff --git a/src/map-tile.js b/src/map-tile.js new file mode 100644 index 000000000..5dca12045 --- /dev/null +++ b/src/map-tile.js @@ -0,0 +1,281 @@ +import { bounds as Lbounds, point as Lpoint } from 'leaflet'; + +import { Util } from './mapml/utils/Util.js'; +import { mapTileLayer } from './mapml/layers/MapTileLayer.js'; + +/* global M */ + +export class HTMLTileElement extends HTMLElement { + static get observedAttributes() { + return ['row', 'col', 'zoom', 'src']; + } + /* jshint ignore:start */ + #hasConnected; + /* jshint ignore:end */ + get row() { + return +(this.hasAttribute('row') ? this.getAttribute('row') : 0); + } + set row(val) { + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal)) { + this.setAttribute('row', parsedVal); + } + } + get col() { + return +(this.hasAttribute('col') ? this.getAttribute('col') : 0); + } + set col(val) { + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal)) { + this.setAttribute('col', parsedVal); + } + } + get zoom() { + // for templated or queried features ** native zoom is only used for zoomTo() ** + let meta = {}, + metaEl = this.getMeta('zoom'); + if (metaEl) + meta = Util._metaContentToObject(metaEl.getAttribute('content')); + if (this._parentElement.nodeName === 'MAP-LINK') { + // nativeZoom = zoom attribute || (sd.map-meta zoom 'value' || 'max') || this._initialZoom + return +(this.hasAttribute('zoom') + ? this.getAttribute('zoom') + : meta.value + ? meta.value + : meta.max + ? meta.max + : this._initialZoom); + } else { + // for "static" features + // nativeZoom zoom attribute || this._initialZoom + // NOTE we don't use map-meta here, because the map-meta is the minimum + // zoom bounds for the layer, and is extended by additional features + // if added / removed during layer lifetime + return +(this.hasAttribute('zoom') + ? this.getAttribute('zoom') + : this._initialZoom); + } + } + set zoom(val) { + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal) && parsedVal >= 0 && parsedVal <= 25) { + this.setAttribute('zoom', parsedVal); + } + } + get src() { + return this.hasAttribute('src') ? this.getAttribute('src') : ''; + } + set src(val) { + if (val) { + this.setAttribute('src', val); + } + } + get extent() { + if (!this._extent) { + this._calculateExtent(); + } + return this._extent; + } + constructor() { + // Always call super first in constructor + super(); + } + async connectedCallback() { + // initialization is done in connectedCallback, attribute initialization + // calls (which happen first) are effectively ignored, so we should be able + // to rely on them being all correctly set by this time e.g. zoom, row, col + // all now have a value that together identify this tiled bit of space + /* jshint ignore:start */ + this.#hasConnected = true; + /* jshint ignore:end */ + // set the initial zoom of the map when features connected + // used for fallback zoom getter for static features + // ~copied from map-feature.js + this._initialZoom = this.getMapEl().zoom; + // Get parent element to determine how to handle the tile + // Need to handle shadow DOM correctly like map-feature does + this._parentElement = + this.parentNode.nodeName.toUpperCase() === 'MAP-LAYER' || + this.parentNode.nodeName.toUpperCase() === 'LAYER-' || + this.parentNode.nodeName.toUpperCase() === 'MAP-LINK' + ? this.parentNode + : this.parentNode.host; + + // in the case of that is rendered but never connected, this won't + // matter, but it speeds up rendering for tiles that go through here... + const imgObj = new Image(); + imgObj.src = this.getAttribute('src'); + + await this._createOrGetTileLayer(); + } + + disconnectedCallback() { + // If this is a map-tile connected to a tile layer, remove it from the layer + if (this._tileLayer) { + this._tileLayer.removeMapTile(this); + + // If this was the last tile in the layer, clean up the layer + if (this._tileLayer._mapTiles && this._tileLayer._mapTiles.length === 0) { + this._tileLayer = null; + delete this._tileLayer; + } + } + } + isFirst() { + // Get the previous element sibling + const prevSibling = this.previousElementSibling; + + // If there's no previous sibling, return true + if (!prevSibling) { + return true; + } + + // Compare the node names (tag names) - return true if they're different + return this.nodeName !== prevSibling.nodeName; + } + getPrevious() { + // Check if this is the first element of a sequence + if (this.isFirst()) { + return null; // No previous element available + } + + // Since we know it's not the first, we can safely return the previous element sibling + return this.previousElementSibling; + } + zoomTo() { + let extent = this.extent; + let map = this.getMapEl()._map, + xmin = extent.topLeft.pcrs.horizontal, + xmax = extent.bottomRight.pcrs.horizontal, + ymin = extent.bottomRight.pcrs.vertical, + ymax = extent.topLeft.pcrs.vertical, + bounds = Lbounds(Lpoint(xmin, ymin), Lpoint(xmax, ymax)), + center = map.options.crs.unproject(bounds.getCenter(true)), + maxZoom = extent.zoom.maxZoom, + minZoom = extent.zoom.minZoom; + map.setView(center, Util.getMaxZoom(bounds, map, minZoom, maxZoom), { + animate: false + }); + } + getMapEl() { + return Util.getClosest(this, 'mapml-viewer,map[is=web-map]'); + } + getLayerEl() { + return Util.getClosest(this, 'map-layer,layer-'); + } + attributeChangedCallback(name, oldValue, newValue) { + if (this.#hasConnected /* jshint ignore:line */) { + switch (name) { + case 'src': + case 'row': + case 'col': + case 'zoom': + if (oldValue !== newValue) { + // If we've already calculated an extent, recalculate it + if (this._extent) { + this._calculateExtent(); + } + + // If this tile is connected to a tile layer, update it + if (this._tileLayer) { + // Remove and re-add to update the tile's position + this._tileLayer.removeMapTile(this); + this._tileLayer.addMapTile(this); + } + } + break; + } + } + } + // ~copied/reimplemented from map-feature.js + getMeta(metaName) { + let name = metaName.toLowerCase(); + if (name !== 'cs' && name !== 'zoom' && name !== 'projection') return; + let sdMeta = this._parentElement.shadowRoot.querySelector( + `map-meta[name=${name}][content]` + ); + if (this._parentElement.nodeName === 'MAP-LINK') { + // sd.map-meta || map-extent meta || layer meta + return sdMeta || this._parentElement.parentElement.getMeta(metaName); + } else { + return this._parentElement.src + ? this._parentElement.shadowRoot.querySelector( + `map-meta[name=${name}][content]` + ) + : this._parentElement.querySelector(`map-meta[name=${name}][content]`); + } + } + async _createOrGetTileLayer() { + await this._parentElement.whenReady(); + if (this.isFirst()) { + const parentElement = this._parentElement; + + // Create a new MapTileLayer + this._tileLayer = mapTileLayer({ + projection: this.getMapEl().projection, + opacity: 1, + // used by map-link and map-layer, both have containers + pane: + parentElement._templatedLayer?.getContainer() || + parentElement._layer.getContainer() + }); + this._tileLayer.addMapTile(this); + + // add MapTileLayer to TemplatedFeaturesOrTilesLayer of the MapLink + if (parentElement._templatedLayer?.addLayer) { + parentElement._templatedLayer.addLayer(this._tileLayer); + } else { + // OR to the MapLayer's layer + parentElement._layer.addLayer(this._tileLayer); + } + } else { + // get the previous tile's layer + this._tileLayer = this.getPrevious()?._tileLayer; + if (this._tileLayer) { + this._tileLayer.addMapTile(this); + } + } + } + _calculateExtent() { + const mapEl = this.getMapEl(); + + if (!mapEl || !mapEl._map) { + // Can't calculate extent without a map + return; + } + + const map = mapEl._map; + const projection = map.options.projection; + const tileSize = M[projection].options.crs.tile.bounds.max.x; + + // Convert tile coordinates to pixel bounds + const pixelX = this.col * tileSize; + const pixelY = this.row * tileSize; + const pixelBounds = Lbounds( + Lpoint(pixelX, pixelY), + Lpoint(pixelX + tileSize, pixelY + tileSize) + ); + + // Convert pixel bounds to PCRS bounds + const pcrsBounds = Util.pixelToPCRSBounds( + pixelBounds, + this.zoom, + projection + ); + + // Format the extent similar to feature extents + this._extent = Util._convertAndFormatPCRS( + pcrsBounds, + map.options.crs, + projection + ); + + // Add zoom information + this._extent.zoom = { + minZoom: this.zoom, + maxZoom: this.zoom, + minNativeZoom: this.zoom, + maxNativeZoom: this.zoom + }; + } +} diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index b22cbfe51..826c8fa60 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -14,6 +14,7 @@ import { HTMLLayerElement } from './map-layer.js'; import { LayerDashElement } from './layer-.js'; import { HTMLMapCaptionElement } from './map-caption.js'; import { HTMLFeatureElement } from './map-feature.js'; +import { HTMLTileElement } from './map-tile.js'; import { HTMLExtentElement } from './map-extent.js'; import { HTMLInputElement } from './map-input.js'; import { HTMLSelectElement } from './map-select.js'; @@ -1491,6 +1492,7 @@ window.customElements.define('map-layer', HTMLLayerElement); window.customElements.define('layer-', LayerDashElement); window.customElements.define('map-caption', HTMLMapCaptionElement); window.customElements.define('map-feature', HTMLFeatureElement); +window.customElements.define('map-tile', HTMLTileElement); window.customElements.define('map-extent', HTMLExtentElement); window.customElements.define('map-input', HTMLInputElement); window.customElements.define('map-select', HTMLSelectElement); @@ -1501,6 +1503,7 @@ export { HTMLLayerElement, HTMLMapCaptionElement, HTMLFeatureElement, + HTMLTileElement, HTMLExtentElement, HTMLInputElement, HTMLSelectElement, diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index 3e1c3a6b0..d4304e68e 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -119,7 +119,7 @@ export var LayerControl = Control.Layers.extend({ // <----------- MODIFICATION from the default _update method // sort the layercontrol layers object based on the zIndex - // provided by MapMLLayer + // provided by MapLayer if (this.options.sortLayers) { this._layers.sort((a, b) => this.options.sortFunction(a.layer, b.layer, a.name, b.name) diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js index 5d0c06bf5..0c8d7f0b8 100644 --- a/src/mapml/handlers/AnnounceMovement.js +++ b/src/mapml/handlers/AnnounceMovement.js @@ -126,7 +126,7 @@ export var AnnounceMovement = Handler.extend({ }, totalBounds: function (e) { - // don't bother with non-MapMLLayer layers... + // don't bother with non-MapLayer layers... if (!e.layer._layerEl) return; let map = this.options.mapEl; map.whenLayersReady().then(() => { diff --git a/src/mapml/index.js b/src/mapml/index.js index 92d55728a..2548596c4 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -44,6 +44,7 @@ import { HTMLMapmlViewerElement } from '../mapml-viewer.js'; import { HTMLLayerElement } from '../mapml-viewer.js'; import { HTMLMapCaptionElement } from '../mapml-viewer.js'; import { HTMLFeatureElement } from '../mapml-viewer.js'; +import { HTMLTileElement } from '../mapml-viewer.js'; import { HTMLExtentElement } from '../mapml-viewer.js'; import { HTMLInputElement } from '../mapml-viewer.js'; import { HTMLSelectElement } from '../mapml-viewer.js'; @@ -57,6 +58,7 @@ window.MapML = { HTMLLayerElement, HTMLMapCaptionElement, HTMLFeatureElement, + HTMLTileElement, HTMLExtentElement, HTMLInputElement, HTMLSelectElement, diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index d3a516439..5d72cac8f 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -14,7 +14,7 @@ export var FeatureLayer = FeatureGroup.extend({ /* * M.MapML turns any MapML feature data into a Leaflet layer. Based on L.GeoJSON. * - * Used by MapMLLayer to create _mapmlvectors property, used to render features + * Used by MapLayer to create _mapmlvectors property, used to render features */ initialize: function (mapml, options) { /* diff --git a/src/mapml/layers/ExtentLayer.js b/src/mapml/layers/MapExtentLayer.js similarity index 55% rename from src/mapml/layers/ExtentLayer.js rename to src/mapml/layers/MapExtentLayer.js index 6fd95f14d..64881063b 100644 --- a/src/mapml/layers/ExtentLayer.js +++ b/src/mapml/layers/MapExtentLayer.js @@ -1,7 +1,35 @@ import { LayerGroup, DomUtil } from 'leaflet'; import { renderStyles } from '../elementSupport/layers/renderStyles.js'; - -export var ExtentLayer = LayerGroup.extend({ +/** + * Leaflet layer implementing map-extent elements + * Extends LayerGroup to create a single layer containing "templated" layers + * from child map-link[@tref] elements + * + * Similar in intent to MapFeatureLayer and MapTileLayer, which are LayerGroup or + * GridLayer for map-feature and map-tile elements' leaflet layer object, respectively. + * + * This layer will be inserted into the LayerGroup hosted by the + * immediately after creation, so that its index within the _layers array of + * that LayerGroup will be equal to its z-index within the LayerGroup's container + * + * LayerGroup._layers[0] <- each *set* of adjacent tiles + * LayerGroup._layers[0] <- is a *single* MapTileLayer + * diff --git a/test/e2e/layers/featureLayer.test.js b/test/e2e/layers/featureLayer.test.js index b51851ef7..9932b22de 100644 --- a/test/e2e/layers/featureLayer.test.js +++ b/test/e2e/layers/featureLayer.test.js @@ -100,7 +100,7 @@ test.describe('Playwright featureLayer (Static Features) Layer Tests', () => { horizontal: 79.6961805581841, vertical: -60.79110984572508 }); - // corrected logic for MapMLLayer._calculateBounds min/maxNativeZoom + // corrected logic for MapLayer._calculateBounds min/maxNativeZoom // there are a bunch of features loaded at map zoom=2. Two have default // (no) zoom attribute, all the others have zoom=0. So, the minNativeZoom // should be 0, while the maxNativeZoom should be 2. diff --git a/test/e2e/layers/multipleExtents.html b/test/e2e/layers/multipleExtents.html index 6701d2f63..f0d774f56 100644 --- a/test/e2e/layers/multipleExtents.html +++ b/test/e2e/layers/multipleExtents.html @@ -25,19 +25,20 @@ - + - + - + +