From c445deabe64bc3c2038c86fe05b3e299aae5ef73 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Fri, 2 May 2025 09:14:46 -0400 Subject: [PATCH 01/16] Add map-tile element Set up index.html with local geoserver spearfish layer to test map-tile Change getElementsByTagName to querySelectorAll - probably will cause test failures and needs to be thought out and refactored once map-tile is complete WIP on map-tile, TemplatedFeaturesOrTilesLayerGroup, map-link WIP on map-tile, TemplatedFeaturesOrTilesLayerGroup, map-link remove redundant method from TemplatedFeaturesOrTilesLayerGroup WIP on map-tile, TemplatedFeaturesOrTilesLayerGroup, map-link remove redundant method from TemplatedFeaturesOrTilesLayerGroup Comment out CI Hopefully disable CI by temporarily removing ci-testing.yml Add templates parameter to TemplatedFeaturesOrTilesLayerGroup (TFOTLG) Add provisional / draft implementation of moveend handler for TFOTLG, based on simplified version of that from TemplatedFeaturesLayer source Exclude handling of for the moment to get things working incrementally, maybe. Add a hard-coded isVisible() implementation that returns true for the moment Add some code to onAdd and onRemove Layer method overrides Add provisional _setUpTemplateVars function Get rendering of map-tile kinda working. Save some work from today hopefully without breaking stuff Delete MapFeatureLayerGroup.js - only need FeatureLayer.js I hope Keep similar code between map-feature.js and map-tile.js in sync Fix missed isFirst / getPrevious method reconciliation between map-tile and map-feature --- .github/workflows/ci-testing.yml | 25 - .gitignore | 3 +- CLAUDE.md | 24 + example-tiles-and-features.mapml.xml | 128 ++++ index.html | 194 +----- newXMLDocument.xml | 24 + src/map-feature.js | 62 ++ src/map-link.js | 25 +- src/map-tile.js | 231 ++++++++ src/mapml-viewer.js | 3 + src/mapml/index.js | 2 + src/mapml/layers/MapFeatureLayerGroup.js | 556 ++++++++++++++++++ src/mapml/layers/MapMLLayer.js | 2 +- src/mapml/layers/MapTileLayer.js | 354 +++++++++++ src/mapml/layers/TemplatedFeaturesLayer.js | 376 ------------ .../TemplatedFeaturesOrTilesLayerGroup.js | 288 +++++++++ 16 files changed, 1723 insertions(+), 574 deletions(-) delete mode 100644 .github/workflows/ci-testing.yml create mode 100644 CLAUDE.md create mode 100644 example-tiles-and-features.mapml.xml create mode 100644 newXMLDocument.xml create mode 100644 src/map-tile.js create mode 100644 src/mapml/layers/MapFeatureLayerGroup.js create mode 100644 src/mapml/layers/MapTileLayer.js delete mode 100644 src/mapml/layers/TemplatedFeaturesLayer.js create mode 100644 src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js diff --git a/.github/workflows/ci-testing.yml b/.github/workflows/ci-testing.yml deleted file mode 100644 index ee8a5b8ff..000000000 --- a/.github/workflows/ci-testing.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Continuous Testing - -on: [pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: latest - - run: sudo apt-get install xvfb - - run: npm install - - run: npx playwright install --with-deps - - run: npm install -g grunt-cli - - run: grunt default - - run: xvfb-run --auto-servernum -- npx playwright test --grep-invert="popupTabNavigation\.test\.js|layerContextMenuKeyboard\.test\.js" --workers=1 --retries=3 -# - run: xvfb-run --auto-servernum -- npx playwright test --grep="popupTabNavigation\.test\.js|layerContextMenuKeyboard\.test\.js" --workers=1 --retries=3 -# - run: xvfb-run --auto-servernum -- npm run jest - env: - CI: true diff --git a/.gitignore b/.gitignore index 393215552..a8cd4bdd2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ /test-results/ .idea/ *.iml -test.html \ No newline at end of file +test.html +**/.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..8d1c0399e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,24 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and Test Commands +- Build project: `grunt default` +- Format and lint: `grunt format` +- Run tests: `npx playwright test` +- Run single test: `npx playwright test test/e2e/path/to/test.test.js` +- Start test server: `node test/server.js` +- Test with specific browser: `npx playwright test --project=chromium` + +## Code Style Guidelines +- JavaScript: ES6+, esversion 11 +- Formatting: Prettier with singleQuote: true, trailingComma: "none" +- Testing: Playwright for E2E tests, Jest for unit tests +- Style the code like existing files, following established patterns +- Use jshint for linting +- Components use custom HTML elements pattern +- Prefer absolute paths over relative paths +- MapML is a custom extension of HTML for maps +- Prefer async/await in test files +- Error handling should follow existing patterns in similar code +- Include meaningful test descriptions \ No newline at end of file diff --git a/example-tiles-and-features.mapml.xml b/example-tiles-and-features.mapml.xml new file mode 100644 index 000000000..9906fde11 --- /dev/null +++ b/example-tiles-and-features.mapml.xml @@ -0,0 +1,128 @@ + + + sfdem, streams, roads, restricted, archsites, bugsites + + + + + + .bbox {display:none} .capitals-r1-s1{r:48.0; stroke-opacity:1.0; + stroke-dashoffset:0; well-known-name:circle; stroke-width:2.0; opacity:1.0; + fill:#FFFFFF; fill-opacity:1.0; stroke:#000000; stroke-linecap:butt} + + + + + + + + + + + + + + + + + + -11538847.8 5531019.6 -11538860.6 5531045.3 -11538871.2 + 5531077.4 -11538896 5531123.7 -11538899.6 5531138 -11538917.3 5531170.1 + -11539009.6 5531252.1 -11539119.7 5531316.3 -11539254.6 5531433.9 + -11539300.8 5531483.8 -11539332.7 5531548 -11539329.1 5531637.1 -11539311.4 + 5531683.4 -11539279.4 5531701.2 -11539130.2 5531722.6 -11539073.4 5531765.3 + -11539041.4 5531843.7 -11538995.2 5531847.2 -11538959.7 5531829.4 + -11538874.4 5531832.9 -11538847.8 5531846.3 + + + + + + + -11543278.6 5529563.7 -11543275 5529602.9 -11543257.3 5529642 + -11543236 5529656.3 -11543157.8 5529770.3 -11543125.9 5529852.3 -11543111.7 + 5529941.4 -11543108.2 5530012.6 -11543093.9 5530119.5 -11543062 5530240.7 + -11543026.5 5530294.1 -11543005.2 5530301.3 -11542969.7 5530297.7 + -11542937.7 5530272.8 -11542905.7 5530226.4 -11542891.5 5530176.6 + -11542838.3 5530098.2 -11542763.7 5530101.7 -11542593.2 5530190.8 + -11542458.3 5530201.5 -11542397.9 5530233.6 -11542316.2 5530247.8 + -11542270.1 5530369 -11542263 5530415.3 -11542220.3 5530483 -11542170.6 + 5530500.8 -11542081.8 5530486.6 -11542039.2 5530461.6 -11541978.8 5530401 + -11541964.7 5530369 -11541946.9 5530351.2 -11541922 5530301.3 -11541875.9 + 5530247.8 -11541858.1 5530240.7 -11541801.3 5530247.8 -11541765.8 5530287 + -11541662.8 5530426 -11541606 5530486.5 -11541531.4 5530497.2 -11541439 + 5530458 -11541353.8 5530404.5 -11541197.6 5530265.5 -11541140.8 5530197.8 + -11541101.7 5530180 -11541041.4 5530183.6 -11540956.1 5530229.9 -11540856.7 + 5530222.7 -11540743.1 5530190.6 -11540672 5530204.9 -11540604.5 5530304.6 + -11540579.7 5530318.9 -11540448.3 5530329.6 -11540224.5 5530415 -11540157 + 5530429.2 -11540061.1 5530429.2 -11539951.1 5530350.8 -11539890.7 5530322.3 + -11539787.8 5530233.2 -11539752.3 5530204.6 -11539667 5530204.6 -11539599.5 + 5530222.4 -11539535.6 5530265.1 -11539496.5 5530318.6 -11539471.7 5530375.6 + -11539425.4 5530518.1 -11539393.4 5530635.7 -11539372.1 5530678.5 + -11539350.8 5530710.5 -11539283.3 5530739 -11539166.1 5530739 -11539080.9 + 5530706.9 -11539041.8 5530660.5 -11539020.6 5530599.9 -11538985.1 5530557.1 + -11538931.8 5530550 -11538896.3 5530560.7 -11538847.8 5530609.3 + + + + + + + -11543097.4 5528690.3 -11543097.4 5528740.6 -11543118.7 + 5528765.5 -11543186.2 5528797.6 -11543232.4 5528897.4 -11543235.9 5528918.7 + -11543250.1 5528936.6 -11543260.8 5528965.1 -11543267.9 5529072 -11543243 + 5529153.9 -11543161.4 5529275 -11543129.4 5529307.1 -11543129.4 5529381.9 + -11543211.1 5529471 -11543260.8 5529513.8 -11543278.6 5529549.4 -11543278.6 + 5529563.7 + + + + + + + -1.154197545486741E7 5528690.2558668 -1.15419842E7 5528702.6 + -1.15420779E7 5528948.8 -1.15421423E7 5529066.4 -1.15423138E7 5529238.3 + -1.15423223E7 5529233.2 -1.15424015E7 5529256.6 -1.15424412E7 5529352.7 + -1.15424537E7 5529429.8 + + + + + + + -1.15432167E7 5529271.1 -1.1543219E7 5529347.4 -1.15432537E7 + 5529412.6 -1.15433427E7 5529536.3 -1.15433579E7 5529585.2 -1.1543356E7 + 5529639.1 -1.1543219E7 5529868.4 -1.15432153E7 5529937.8 -1.15432021E7 + 5530059.4 -1.15431755E7 5530144.2 -1.15431196E7 5530299.9 -1.15431364E7 + 5530354 -1.15431942E7 5530403 -1.15433533E7 5530481.5 -1.15434197E7 + 5530475.7 -1.15435152E7 5530461.4 -1.15435587E7 5530450.4 -1.15435759E7 + 5530410.2 -1.1543599E7 5530374.3 -1.15436169E7 5530375.2 -1.15436371E7 + 5530437.8 -1.15436496E7 5530524.4 -1.15436859E7 5530638.5 -1.15436064E7 + 5530707.6 -1.15436037E7 5530757.3 -1.1543651E7 5530900.6 -1.154373E7 + 5530962.5 -1.15437836E7 5530985 -1.15438474E7 5531032.3 + + + + + + + -1.15438474E7 5531032.3 -1.15438747E7 5531008.4 + -1.154394996991142E7 5530977.68753614 + + + + + \ No newline at end of file diff --git a/index.html b/index.html index 18f6bf4a3..0cfc4abee 100644 --- a/index.html +++ b/index.html @@ -1,173 +1,43 @@ - - - + index-map.html - - + + - - - - - - - - - - - - - - - - - All cuisines - African - Asian - Cajun - Indian - Italian - Mexican - - - - - - - - - - - - - - - - - - - - All cuisines - African - Asian - Cajun - Indian - Italian - Mexican - - - - - - - + + + + + + + + diff --git a/newXMLDocument.xml b/newXMLDocument.xml new file mode 100644 index 000000000..e4a0571e4 --- /dev/null +++ b/newXMLDocument.xml @@ -0,0 +1,24 @@ + + + Spearfish + + + + + + + + + + + + \ No newline at end of file diff --git a/src/map-feature.js b/src/map-feature.js index 5ae172fd4..52e3895a5 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -1,5 +1,7 @@ import { bounds, point } 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) => { @@ -266,7 +271,64 @@ export class HTMLFeatureElement extends HTMLElement { 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() + }); + this.addFeature(this._featureLayer); + + // add featureLayer to TemplatedFeaturesOrTilesLayerGroup 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..8edddaf3f 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -10,8 +10,8 @@ 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 { templatedPMTilesLayer } from './mapml/layers/TemplatedPMTilesLayer.js'; +import { templatedFeaturesOrTilesLayerGroup } from './mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js'; /* global M */ export class HTMLLinkElement extends HTMLElement { @@ -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 + // TemplatedFeaturesOrTilesLayerGroup // // 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,13 +552,19 @@ export class HTMLLinkElement extends HTMLElement { if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); } - this._templatedLayer = templatedFeaturesLayer(this._templateVars, { - zoomBounds: this.getZoomBounds(), - extentBounds: this.getBounds(), - zIndex: this.zIndex, - pane: this.parentExtent._extentLayer.getContainer(), - linkEl: this - }).addTo(this.parentExtent._extentLayer); + // Use the FeaturesTilesLayerGroup to handle both map-feature and map-tile elements + this._templatedLayer = templatedFeaturesOrTilesLayerGroup( + this._templateVars, + { + zoomBounds: this.getZoomBounds(), + extentBounds: this.getBounds(), + zIndex: this.zIndex, + pane: this.parentExtent._extentLayer.getContainer(), + 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) { this.attachShadow({ mode: 'open' }); diff --git a/src/map-tile.js b/src/map-tile.js new file mode 100644 index 000000000..5286100bd --- /dev/null +++ b/src/map-tile.js @@ -0,0 +1,231 @@ +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() { + return +(this.hasAttribute('zoom') ? this.getAttribute('zoom') : 0); + } + set zoom(val) { + var parsedVal = parseInt(val, 10); + if (!isNaN(parsedVal) && parsedVal >= 0 && parsedVal <= 25) { + this.setAttribute('zoom', parsedVal); + } + } + 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(); + } + 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 */ + + // 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; + + this._createOrGetTileLayer(); + + // Calculate the extent + //this._calculateExtent(); + } + + 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) { + // Clean up happens in the map-link that created the layer + // The map-link handles this through FeaturesTilesLayerGroup + } + } + } + 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; + } + } + } + _createOrGetTileLayer() { + if (this.isFirst()) { + const parentElement = this._parentElement; + + // Create a new MapTileLayer + this._tileLayer = mapTileLayer({ + projection: this.getMapEl()._map.options.projection, + opacity: 1, + pane: parentElement._templatedLayer.getContainer() + }); + this._tileLayer.addMapTile(this); + + // add MapTileLayer to TemplatedFeaturesOrTilesLayerGroup of the parentElement + if ( + parentElement._templatedLayer && + parentElement._templatedLayer.addLayer + ) { + parentElement._templatedLayer.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/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/MapFeatureLayerGroup.js b/src/mapml/layers/MapFeatureLayerGroup.js new file mode 100644 index 000000000..9628daddf --- /dev/null +++ b/src/mapml/layers/MapFeatureLayerGroup.js @@ -0,0 +1,556 @@ +import { + FeatureGroup, + DomUtil, + bounds, + SVG, + Util as LeafletUtil, + Browser +} from 'leaflet'; +import { Util } from '../utils/Util.js'; +import { path } from '../features/path.js'; +import { geometry } from '../features/geometry.js'; + +export var MapFeatureLayerGroup = FeatureGroup.extend({ + /* + * This is the feature equivalent of MapTileLayer. The intended use is to + * represent an adjacent sequence of elements found in a templated + * response, but MAYBE we'll be able to use it to represent such a sequence + * in a static document response, too, tbd. + * + * 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] + * LayerGroup._layers[0] + * LayerGroup._layers[1] <- each set of adjacent features + * LayerGroup._layers[1] <- is a single MapFeatureLayerGroup + * LayerGroup._layers[2] + * LayerGroup._layers[2] + * LayerGroup._layers[3] + * LayerGroup._layers[3] + * and so on + * + */ + initialize: function (mapml, options) { + /* + mapml: + 1. for query: an array of map-feature elements that it fetches + 2. for static templated feature: null + 3. for non-templated feature: map-layer (with no src) or mapml file (with src) + */ + 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.tiles) { + // not a tiled vector layer + this._container = null; + if (this.options.query) { + this._container = DomUtil.create( + 'div', + 'leaflet-layer', + this.options.pane + ); + DomUtil.addClass( + this._container, + 'leaflet-pane mapml-vector-container' + ); + } else if (this.options._leafletLayer) { + this._container = DomUtil.create( + 'div', + 'leaflet-layer', + this.options.pane + ); + DomUtil.addClass( + this._container, + 'leaflet-pane mapml-vector-container' + ); + } 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; + DomUtil.addClass( + this._container, + 'leaflet-pane mapml-vector-container' + ); + } + this.options.renderer.options.pane = this._container; + } + if (this.options.query) { + this._queryFeatures = mapml.features ? mapml.features : mapml; + } 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; + } + } + }, + + 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, + // if it is empty, isVisible = false + // this._staticFeature: flag to determine if the featureLayer is used by static features only + // this._features: check if the current static featureLayer is empty + // (Object.keys(this._features).length === 0 => this._features is an empty object) + else if (this._staticFeature && Object.keys(this._features).length === 0) { + return false; + } else { + let mapZoom = map.getZoom(), + zoomBounds = this.zoomBounds || this.options.zoomBounds, + layerBounds = this.layerBounds || this.options.layerBounds, + withinZoom = zoomBounds + ? mapZoom <= zoomBounds.maxZoom && mapZoom >= zoomBounds.minZoom + : false; + return ( + withinZoom && + this._layers && + layerBounds && + layerBounds.overlaps( + Util.pixelToPCRSBounds( + map.getPixelBounds(), + mapZoom, + map.options.projection + ) + ) + ); + } + }, + + onAdd: function (map) { + this._map = map; + FeatureGroup.prototype.onAdd.call(this, map); + if (this._staticFeature) { + this._validateRendering(); + } + if (this._queryFeatures) { + map.on('featurepagination', this.showPaginationFeature, this); + } + }, + addLayer: function (layerToAdd) { + FeatureGroup.prototype.addLayer.call(this, layerToAdd); + if (!this.options.layerBounds) { + this.layerBounds = this.layerBounds + ? this.layerBounds.extend(layerToAdd.layerBounds) + : bounds(layerToAdd.layerBounds.min, layerToAdd.layerBounds.max); + + 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; + if (layerToAdd.zoomBounds.minNativeZoom < this.zoomBounds.minNativeZoom) + this.zoomBounds.minNativeZoom = layerToAdd.zoomBounds.minNativeZoom; + if (layerToAdd.zoomBounds.maxNativeZoom > this.zoomBounds.maxNativeZoom) + this.zoomBounds.maxNativeZoom = layerToAdd.zoomBounds.maxNativeZoom; + } else { + this.zoomBounds = layerToAdd.zoomBounds; + } + } + if (this._staticFeature) { + // TODO: validate the use the feature.zoom which is new (was in createGeometry) + let featureZoom = layerToAdd.options.mapmlFeature.zoom; + if (featureZoom in this._features) { + this._features[featureZoom].push(layerToAdd); + } else { + this._features[featureZoom] = [layerToAdd]; + } + // hide/display features based on the their zoom limits + this._validateRendering(); + } + return this; + }, + addRendering: function (featureToAdd) { + FeatureGroup.prototype.addLayer.call(this, featureToAdd); + }, + onRemove: function (map) { + if (this._queryFeatures) { + map.off('featurepagination', this.showPaginationFeature, this); + delete this._queryFeatures; + DomUtil.remove(this._container); + } + FeatureGroup.prototype.onRemove.call(this, map); + this._map.featureIndex.cleanIndex(); + }, + + removeLayer: function (featureToRemove) { + FeatureGroup.prototype.removeLayer.call(this, featureToRemove); + if (!this.options.layerBounds) { + delete this.layerBounds; + // this ensures that the .extent gets recalculated if needed + delete this.options._leafletLayer.bounds; + delete this.zoomBounds; + // this ensures that the .extent gets recalculated if needed + delete this.options._leafletLayer.zoomBounds; + delete this._layers[featureToRemove._leaflet_id]; + this._removeFromFeaturesList(featureToRemove); + // iterate through all remaining layers + let layerBounds, zoomBounds; + let layerIds = Object.keys(this._layers); + // re-calculate the layerBounds and zoomBounds for the whole layer when + // a feature is permanently removed from the overall layer + // bug alert: it's necessary to create a new bounds object to initialize + // this.layerBounds, to avoid changing the layerBounds of the first geometry + // added to this layer + for (let id of layerIds) { + let layer = this._layers[id]; + if (layerBounds) { + layerBounds.extend(layer.layerBounds); + } else { + layerBounds = bounds(layer.layerBounds.min, layer.layerBounds.max); + } + if (zoomBounds) { + if (layer.zoomBounds.minZoom < zoomBounds.minZoom) + zoomBounds.minZoom = layer.zoomBounds.minZoom; + if (layer.zoomBounds.maxZoom > zoomBounds.maxZoom) + zoomBounds.maxZoom = layer.zoomBounds.maxZoom; + if (layer.zoomBounds.minNativeZoom < zoomBounds.minNativeZoom) + zoomBounds.minNativeZoom = layer.zoomBounds.minNativeZoom; + if (layer.zoomBounds.maxNativeZoom > zoomBounds.maxNativeZoom) + zoomBounds.maxNativeZoom = layer.zoomBounds.maxNativeZoom; + } else { + zoomBounds = {}; + zoomBounds.minZoom = layer.zoomBounds.minZoom; + zoomBounds.maxZoom = layer.zoomBounds.maxZoom; + zoomBounds.minNativeZoom = layer.zoomBounds.minNativeZoom; + zoomBounds.maxNativeZoom = layer.zoomBounds.maxNativeZoom; + } + } + // If the last feature is removed, we should remove the .layerBounds and + // .zoomBounds properties, so that the FeatureLayer may be ignored + if (layerBounds) { + this.layerBounds = layerBounds; + } else { + delete this.layerBounds; + } + if (zoomBounds) { + this.zoomBounds = zoomBounds; + } else { + delete this.zoomBounds; + delete this.options.zoomBounds; + } + } + return this; + }, + /** + * Remove the geomtry rendering (an svg g/ M.Geomtry) from the L.FeatureGroup + * _layers array, so that it's not visible on the map, but still contributes + * to the bounds and zoom limits of the FeatureLayer. + * + * @param {type} featureToRemove + * @returns {undefined} + */ + removeRendering: function (featureToRemove) { + FeatureGroup.prototype.removeLayer.call(this, featureToRemove); + }, + _removeFromFeaturesList: function (feature) { + 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 === feature._leaflet_id) { + this._features[zoom].splice(i, 1); + break; + } + } + }, + getEvents: function () { + if (this._staticFeature) { + return { + moveend: this._handleMoveEnd, + zoomend: this._handleZoomEnd + }; + } + return {}; + }, + + // for query + showPaginationFeature: function (e) { + if (this.options.query && this._queryFeatures[e.i]) { + let feature = this._queryFeatures[e.i]; + feature._linkEl.shadowRoot.replaceChildren(); + this.clearLayers(); + // append all map-meta from mapml document + if (feature.meta) { + for (let i = 0; i < feature.meta.length; i++) { + feature._linkEl.shadowRoot.appendChild(feature.meta[i]); + } + } + feature._linkEl.shadowRoot.appendChild(feature); + feature.addFeature(this); + e.popup._navigationBar.querySelector('p').innerText = + e.i + 1 + '/' + this.options._leafletLayer._totalFeatureCount; + e.popup._content + .querySelector('iframe') + .setAttribute('sandbox', 'allow-same-origin allow-forms'); + e.popup._content.querySelector('iframe').srcdoc = + feature.querySelector('map-properties').innerHTML; + // "zoom to here" link need to be re-set for every pagination + this._map.fire('attachZoomLink', { i: e.i, currFeature: feature }); + this._map.once( + 'popupclose', + function (e) { + this.shadowRoot.innerHTML = ''; + }, + feature._linkEl + ); + } + }, + + _handleMoveEnd: function () { + this._removeCSS(); + }, + + _handleZoomEnd: function (e) { + // handle zoom end gets called twice for every zoom, this condition makes it go through once only. + if (this.zoomBounds) { + this._validateRendering(); + } + }, + /* + * _validateRendering 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 getters. 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 + * */ + _validateRendering: 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(); + let map = this._map || this.options._leafletLayer._map; + // it's important that we not try to validate rendering if the FeatureLayer + // isn't actually being rendered (i.e. on the map. the _map property can't + // be used because once it's assigned (by onAdd, above) it's never unassigned. + if (!map.hasLayer(this)) return; + if (this._features) { + for (let zoom in this._features) { + for (let k = 0; k < this._features[zoom].length; k++) { + let geometry = this._features[zoom][k], + renderable = geometry._checkRender( + map.getZoom(), + this.zoomBounds.minZoom, + this.zoomBounds.maxZoom + ); + if (!renderable) { + // insert a placeholder in the dom rendering for the geometry + // so that it retains its layering order when it is next rendered + let placeholder = document.createElement('span'); + placeholder.id = geometry._leaflet_id; + // geometry.defaultOptions.group is the rendered svg g element in sd + geometry.defaultOptions.group.insertAdjacentElement( + 'beforebegin', + placeholder + ); + // removing the rendering without removing the feature from the feature list + this.removeRendering(geometry); + } else if ( + // checking for _map so we do not enter this code block during the connectedCallBack of the map-feature + !map.hasLayer(geometry) && + !geometry._map + ) { + this.addRendering(geometry); + // update the layerbounds + let placeholder = + geometry.defaultOptions.group.parentNode.querySelector( + `span[id="${geometry._leaflet_id}"]` + ); + placeholder.replaceWith(geometry.defaultOptions.group); + } + } + } + } + }, + + _setZoomTransform: function (center, clampZoom) { + var scale = this._map.getZoomScale(this._map.getZoom(), clampZoom), + translate = center + .multiplyBy(scale) + .subtract(this._map._getNewPixelOrigin(center, this._map.getZoom())) + .round(); + + if (Browser.any3d) { + DomUtil.setTransform(this._layers[clampZoom], translate, scale); + } else { + DomUtil.setPosition(this._layers[clampZoom], translate); + } + }, + + /** + * 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 a FeatureLayer + * with options set as required + * + * @param feature - a element + * @param {String} fallbackCS - "gcrs" | "pcrs" + * @param {String} tileZoom - the zoom of the map at which the coordinates will exist + * + * @returns Geometry, which is an L.FeatureGroup + * @public + */ + createGeometry: function (feature, fallbackCS, tileZoom) { + // was let options = this.options, but that was causing unwanted side-effects + // because we were adding .layerBounds and .zoomBounds to it before passing + // to _createGeometry, which meant that FeatureLayer was sprouting + // options.layerBounds and .zoomBounds when it should not have those props + let options = Object.assign({}, this.options); + + if (options.filter && !options.filter(feature)) { + return; + } + + if (feature.classList.length) { + options.className = feature.classList.value; + } + // tileZoom is only used when the map-feature is discarded i.e. for rendering + // vector tiles' feature geometries in bulk (in this case only the geomtry + // is rendered on a tile-shaped FeatureLayer + let zoom = feature.zoom ?? tileZoom, + title = feature.querySelector('map-featurecaption'); + title = title + ? title.innerHTML + : this.options.mapEl.locale.dfFeatureCaption; + + if (feature.querySelector('map-properties')) { + options.properties = document.createElement('div'); + options.properties.classList.add('mapml-popup-content'); + options.properties.insertAdjacentHTML( + 'afterbegin', + feature.querySelector('map-properties').innerHTML + ); + } + 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 = Util.extentToBounds(feature.extent, 'PCRS'); + options.zoomBounds = feature.extent.zoom; + } + let geom = this._geometryToLayer(feature, options, cs, +zoom, title); + if (geom && Object.keys(geom._layers).length !== 0) { + // 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 (!geom.options.color && feature.hasAttribute('class')) { + geom.options.className = feature.getAttribute('class'); + } + geom.defaultOptions = geom.options; + this.resetStyle(geom); + + if (options.onEachFeature) { + geom.bindTooltip(title, { interactive: true, sticky: true }); + } + if (feature.tagName.toUpperCase() === 'MAP-FEATURE') { + feature._groupEl = geom.options.group; + } + return geom; + } + }, + + resetStyle: function (layer) { + var style = this.options.style; + if (style) { + // reset any custom styles + LeafletUtil.extend(layer.options, layer.defaultOptions); + this._setLayerStyle(layer, style); + } + }, + + setStyle: function (style) { + this.eachLayer(function (layer) { + this._setLayerStyle(layer, style); + }, this); + }, + + _setLayerStyle: function (layer, style) { + if (typeof style === 'function') { + style = style(layer.feature); + } + if (layer.setStyle) { + layer.setStyle(style); + } + }, + _removeCSS: function () { + let toDelete = this._container.querySelectorAll( + 'link[rel=stylesheet],style' + ); + for (let i = 0; i < toDelete.length; i++) { + this._container.removeChild(toDelete[i]); + } + }, + _geometryToLayer: function (feature, vectorOptions, cs, zoom, title) { + let geom = feature.getElementsByTagName('map-geometry')[0], + group = [], + groupOptions = {}, + svgGroup = SVG.create('g'), + copyOptions = Object.assign({}, vectorOptions); + svgGroup._featureEl = feature; // rendered has a reference to map-feature + if (geom) { + for (let geo of geom.querySelectorAll( + 'map-polygon, map-linestring, map-multilinestring, map-point, map-multipoint' + )) { + group.push( + path( + geo, + Object.assign(copyOptions, { + nativeCS: cs, + nativeZoom: zoom, + projection: this.options.projection, + featureID: feature.id, + group: svgGroup, + wrappers: this._getGeometryParents(geo.parentElement), + featureLayer: this, + _leafletLayer: this.options._leafletLayer + }) + ) + ); + } + let groupOptions = { + group: svgGroup, + mapmlFeature: feature, + featureID: feature.id, + accessibleTitle: title, + onEachFeature: vectorOptions.onEachFeature, + properties: vectorOptions.properties, + _leafletLayer: this.options._leafletLayer, + layerBounds: vectorOptions.layerBounds, + zoomBounds: vectorOptions.zoomBounds + }, + collections = + geom.querySelector('map-multipolygon') || + geom.querySelector('map-geometrycollection'); + if (collections) + groupOptions.wrappers = this._getGeometryParents( + collections.parentElement + ); + return geometry(group, groupOptions); + } + }, + + _getGeometryParents: function (subType, elems = []) { + if (subType && subType.tagName.toUpperCase() !== 'MAP-GEOMETRY') { + if ( + subType.tagName.toUpperCase() === 'MAP-MULTIPOLYGON' || + subType.tagName.toUpperCase() === 'MAP-GEOMETRYCOLLECTION' + ) + return this._getGeometryParents(subType.parentElement, elems); + return this._getGeometryParents( + subType.parentElement, + elems.concat([subType]) + ); + } else { + return elems; + } + } +}); +export var mapFeatureLayerGroup = function (mapml, options) { + return new MapFeatureLayerGroup(mapml, options); +}; diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 06ea80c52..ee9be2e71 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -317,7 +317,7 @@ export var MapMLLayer = LayerGroup.extend({ (zoom && zoom.getAttribute('value')) || '0' ); - var newTiles = mapml.getElementsByTagName('map-tile'); + var newTiles = mapml.querySelectorAll('map-tile'); for (var nt = 0; nt < newTiles.length; nt++) { tiles.appendChild(document.importNode(newTiles[nt], true)); } diff --git a/src/mapml/layers/MapTileLayer.js b/src/mapml/layers/MapTileLayer.js new file mode 100644 index 000000000..0b7b986d6 --- /dev/null +++ b/src/mapml/layers/MapTileLayer.js @@ -0,0 +1,354 @@ +import { GridLayer, DomUtil, point, bounds } from 'leaflet'; +import { Util } from '../utils/Util.js'; + +/** + * Leaflet layer for handling map-tile elements + * Extends GridLayer to create tiles based on map-tile elements + * + * Similar in intent to MapFeatureLayerGroup, which is a receiver for + * map-feature elements' leaflet layer object + * + * 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 + * LayerGroup._layers[1] + * LayerGroup._layers[1] + * LayerGroup._layers[2] + * LayerGroup._layers[2] + * LayerGroup._layers[3] + * LayerGroup._layers[3] + * and so on + */ +export var MapTileLayer = GridLayer.extend({ + initialize: function (options) { + GridLayer.prototype.initialize.call(this, options); + this._mapTiles = options.mapTiles || []; + this._tileMap = {}; + this._pendingTiles = {}; + this._buildTileMap(); + this._container = DomUtil.create('div', 'leaflet-layer'); + DomUtil.addClass(this._container, 'mapml-static-tile-container'); + // Store bounds for visibility checks + // this.layerBounds = this._computeLayerBounds(); + // this.zoomBounds = this._computeZoomBounds(); + }, + + onAdd: function (map) { + this.options.pane.appendChild(this._container); + // Call the parent method + GridLayer.prototype.onAdd.call(this, map); + }, + onRemove: function (map) { + // Clean up pending tiles + this._pendingTiles = {}; + DomUtil.remove(this._container); + }, + + /** + * Adds a map-tile element to the layer + * @param {HTMLTileElement} mapTile - The map-tile element to add + */ + addMapTile: function (mapTile) { + if (!this._mapTiles.includes(mapTile)) { + this._mapTiles.push(mapTile); + this._addToTileMap(mapTile); + // this._updateBounds(); + // this.redraw(); + } + }, + + /** + * Removes a map-tile element from the layer + * @param {HTMLTileElement} mapTile - The map-tile element to remove + */ + removeMapTile: function (mapTile) { + const index = this._mapTiles.indexOf(mapTile); + if (index !== -1) { + this._mapTiles.splice(index, 1); + this._removeFromTileMap(mapTile); + // this._updateBounds(); + // this.redraw(); + } + }, + + /** + * Checks if the layer is currently visible on the map + * @returns {boolean} - True if the layer is visible, false otherwise + */ + isVisible: function () { + if (!this._map) return false; + + const mapZoom = this._map.getZoom(); + // Clamp zoom to layer's zoom bounds + const zoomLevel = Math.max( + this.zoomBounds.minNativeZoom, + Math.min(mapZoom, this.zoomBounds.maxNativeZoom) + ); + + return ( + mapZoom >= this.zoomBounds.minZoom && + mapZoom <= this.zoomBounds.maxZoom && + this.layerBounds.overlaps( + Util.pixelToPCRSBounds( + this._map.getPixelBounds(), + mapZoom, + this._map.options.projection + ) + ) + ); + }, + + /** + * Overrides GridLayer createTile to use map-tile elements + * @param {Object} coords - Tile coordinates + * @param {Function} done - Callback to be called when the tile is ready, with error and tile element params + * @returns {HTMLElement} - The created tile element + */ + createTile: function (coords, done) { + const tileKey = this._tileCoordsToKey(coords); + const tileSize = this.getTileSize(); + + // Create container element + const tileElement = document.createElement('div'); + tileElement.setAttribute('col', coords.x); + tileElement.setAttribute('row', coords.y); + tileElement.setAttribute('zoom', coords.z); + DomUtil.addClass(tileElement, 'leaflet-tile'); + + // Set size + tileElement.style.width = tileSize.x + 'px'; + tileElement.style.height = tileSize.y + 'px'; + + // Find matching tile in our map + const matchingTile = this._tileMap[tileKey]; + + if (matchingTile) { + // Create an image element with the src from the matching map-tile + const img = document.createElement('img'); + img.src = matchingTile.src; + img.width = tileSize.x; + img.height = tileSize.y; + img.alt = ''; + img.setAttribute('role', 'presentation'); + // bidirectional link map-tile element and rendered div + tileElement._mapTile = matchingTile; + matchingTile._tileDiv = tileElement; + + tileElement.appendChild(img); + + // Add the loaded class manually to ensure tile is visible + DomUtil.addClass(tileElement, 'leaflet-tile-loaded'); + + // Call the done callback to signal that the tile is ready + done(null, tileElement); + } else { + // The tile might be added later, register a pending tile + if (!this._pendingTiles) { + this._pendingTiles = {}; + } + + // Store the tile element and done callback for later update + this._pendingTiles[tileKey] = { + element: tileElement, + done: done + }; + + // Don't call done yet - we'll call it when the map-tile is added + } + + return tileElement; + }, + + /** + * Builds the tile map from the current map-tile elements + * @private + */ + _buildTileMap: function () { + this._tileMap = {}; + for (const mapTile of this._mapTiles) { + this._addToTileMap(mapTile); + } + }, + + /** + * Adds a map-tile element to the tile map + * @param {HTMLTileElement} mapTile - The map-tile element to add + * @private + */ + _addToTileMap: function (mapTile) { + const tileKey = `${mapTile.col}:${mapTile.row}:${mapTile.zoom}`; + this._tileMap[tileKey] = mapTile; + + // Check if this tile was requested but not available at that time + if (this._pendingTiles && this._pendingTiles[tileKey]) { + const pendingTile = this._pendingTiles[tileKey]; + const tileElement = pendingTile.element; + const doneCallback = pendingTile.done; + + // Create and append the image to the tile + const tileSize = this.getTileSize(); + const img = document.createElement('img'); + img.src = mapTile.src; + img.width = tileSize.x; + img.height = tileSize.y; + img.alt = ''; + img.setAttribute('role', 'presentation'); + + // bidirectional link map-tile element and rendered div + tileElement._mapTile = mapTile; + mapTile._tileDiv = tileElement; + + tileElement.appendChild(img); + + // Add the loaded class manually to ensure tile is visible + DomUtil.addClass(tileElement, 'leaflet-tile-loaded'); + + // Call the done callback to signal that the tile is now ready + if (doneCallback) { + doneCallback(null, tileElement); + } + + // Remove from pending tiles + delete this._pendingTiles[tileKey]; + } + }, + /** + * Removes a map-tile element from the tile map + * @param {HTMLTileElement} mapTile - The map-tile element to remove + * @private + */ + _removeFromTileMap: function (mapTile) { + const tileKey = `${mapTile.col}:${mapTile.row}:${mapTile.zoom}`; + delete this._tileMap[tileKey]; + + // Also remove from pending tiles if it exists there + if (this._pendingTiles && this._pendingTiles[tileKey]) { + delete this._pendingTiles[tileKey]; + } + }, + + /** + * Updates layer bounds when tiles are added or removed + * @private + */ + _updateBounds: function () { + this.layerBounds = this._computeLayerBounds(); + this.zoomBounds = this._computeZoomBounds(); + }, + + /** + * Computes the layer bounds from all map-tile elements + * @returns {L.Bounds} - The computed layer bounds + * @private + */ + _computeLayerBounds: function () { + if (this._mapTiles.length === 0) { + return bounds([0, 0], [0, 0]); + } + + const tilesByZoom = {}; + const projection = this.options.projection; + const tileSize = M[projection].options.crs.tile.bounds.max.x; + + // Group tiles by zoom + for (const mapTile of this._mapTiles) { + const zoom = mapTile.zoom; + + if (!tilesByZoom[zoom]) { + tilesByZoom[zoom] = []; + } + + tilesByZoom[zoom].push({ + x: mapTile.col, + y: mapTile.row, + z: zoom + }); + } + + // Calculate bounds for each zoom level + const layerBoundsByZoom = {}; + for (const zoom in tilesByZoom) { + const tiles = tilesByZoom[zoom]; + let pixelBounds = null; + + for (const tile of tiles) { + const pixelX = tile.x * tileSize; + const pixelY = tile.y * tileSize; + + if (!pixelBounds) { + pixelBounds = bounds( + point(pixelX, pixelY), + point(pixelX + tileSize, pixelY + tileSize) + ); + } else { + pixelBounds.extend(point(pixelX, pixelY)); + pixelBounds.extend(point(pixelX + tileSize, pixelY + tileSize)); + } + } + + if (pixelBounds) { + layerBoundsByZoom[zoom] = Util.pixelToPCRSBounds( + pixelBounds, + parseInt(zoom, 10), + projection + ); + } + } + + // Combine all zoom level bounds + let combinedBounds = null; + for (const zoom in layerBoundsByZoom) { + if (!combinedBounds) { + combinedBounds = layerBoundsByZoom[zoom].clone(); + } else { + combinedBounds.extend(layerBoundsByZoom[zoom].min); + combinedBounds.extend(layerBoundsByZoom[zoom].max); + } + } + + return combinedBounds || bounds([0, 0], [0, 0]); + }, + + /** + * Computes zoom bounds from all map-tile elements + * @returns {Object} - The computed zoom bounds + * @private + */ + _computeZoomBounds: function () { + const result = { + minZoom: Infinity, + maxZoom: -Infinity, + minNativeZoom: Infinity, + maxNativeZoom: -Infinity + }; + + if (this._mapTiles.length === 0) { + return { + minZoom: 0, + maxZoom: 22, + minNativeZoom: 0, + maxNativeZoom: 22 + }; + } + + // Find min/max zoom from map-tile elements + for (const mapTile of this._mapTiles) { + const zoom = mapTile.zoom; + result.minNativeZoom = Math.min(result.minNativeZoom, zoom); + result.maxNativeZoom = Math.max(result.maxNativeZoom, zoom); + } + + // Set min/max zoom based on native zoom + result.minZoom = result.minNativeZoom; + result.maxZoom = result.maxNativeZoom; + + return result; + } +}); + +export var mapTileLayer = function (options) { + return new MapTileLayer(options); +}; diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js deleted file mode 100644 index fe224f369..000000000 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ /dev/null @@ -1,376 +0,0 @@ -import { - Layer, - DomUtil, - extend, - setOptions, - Util as LeafletUtil -} from 'leaflet'; - -import { Util } from '../utils/Util.js'; -import { featureLayer } from '../layers/FeatureLayer.js'; -import { featureRenderer } from '../features/featureRenderer.js'; -import { renderStyles } from '../elementSupport/layers/renderStyles.js'; - -export var TemplatedFeaturesLayer = Layer.extend({ - // this and M.ImageLayer could be merged or inherit from a common parent - initialize: function (template, options) { - this._template = template; - this._container = DomUtil.create('div', 'leaflet-layer'); - DomUtil.addClass(this._container, 'mapml-features-container'); - this.zoomBounds = options.zoomBounds; - this.extentBounds = options.extentBounds; - // get rid of duplicate info, it can be confusing - delete options.zoomBounds; - delete options.extentBounds; - this._linkEl = options.linkEl; - setOptions( - this, - extend(options, this._setUpFeaturesTemplateVars(template)) - ); - }, - - isVisible: function () { - let map = this._linkEl.getMapEl()._map; - let mapZoom = map.getZoom(); - let mapBounds = Util.pixelToPCRSBounds( - map.getPixelBounds(), - mapZoom, - map.options.projection - ); - return ( - mapZoom <= this.zoomBounds.maxZoom && - mapZoom >= this.zoomBounds.minZoom && - this.extentBounds.overlaps(mapBounds) - ); - }, - - getEvents: function () { - var events = { - moveend: this._onMoveEnd - }; - return events; - }, - onAdd: function (map) { - this._map = map; - // this causes the layer (this._features) to actually render... - this.options.pane.appendChild(this._container); - var opacity = this.options.opacity || 1, - container = this._container; - if (!this._features) { - this._features = 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: container, - // the bounds will be static, fixed, constant for the lifetime of the layer - layerBounds: this.extentBounds, - zoomBounds: this.zoomBounds, - opacity: opacity, - projection: map.options.projection, - mapEl: this._linkEl.getMapEl(), - onEachFeature: function (properties, geometry) { - const popupOptions = { - autoClose: false, - autoPan: true, - maxHeight: map.getSize().y * 0.5 - 50, - maxWidth: map.getSize().x * 0.7, - minWidth: 108 - }; - // need to parse as HTML to preserve semantics and styles - var c = document.createElement('div'); - c.classList.add('mapml-popup-content'); - c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, popupOptions); - } - }); - extend(this._features.options, { _leafletLayer: this._features }); - this._features._layerEl = this._linkEl.getLayerEl(); - } else { - this._features.eachLayer((layer) => layer.addTo(map)); - } - this._onMoveEnd(); - }, - onRemove: function () { - if (this._features) this._features.eachLayer((layer) => layer.remove()); - DomUtil.remove(this._container); - }, - renderStyles, - - redraw: function () { - this._onMoveEnd(); - }, - - _removeCSS: function () { - let toDelete = this._container.querySelectorAll( - 'link[rel=stylesheet],style' - ); - for (let i = 0; i < toDelete.length; i++) { - let parent = toDelete[i].parentNode; - parent.removeChild(toDelete[i]); - } - }, - - _onMoveEnd: function () { - let history = this._map.options.mapEl._history; - 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 ( - (step !== '1' && - (mapZoom + 1) % step === 0 && - current.zoom === previous.zoom - 1) || - current.zoom === previous.zoom || - Math.floor(mapZoom / step) * step !== - Math.floor(previous.zoom / step) * step - ) { - steppedZoom = Math.floor(mapZoom / step) * step; - } - //No request needed if in a step interval (unless panning) - else if (mapZoom % this._template.step !== 0) return; - - let scaleBounds = this._map.getPixelBounds( - this._map.getCenter(), - steppedZoom - ); - - // should set this.isVisible properly BEFORE return, otherwise will cause map-layer.validateDisabled not work properly - let url = this._getfeaturesUrl(steppedZoom, scaleBounds); - // No request needed if the current template url is the same as the url to request - if (url === this._url) return; - - // do cleaning up for new request - this._features.clearLayers(); - // shadow may has not yet attached to for the first-time rendering - if (this._linkEl.shadowRoot) { - this._linkEl.shadowRoot.innerHTML = ''; - } - this._removeCSS(); - //Leave the layers cleared if the layer is not visible - if (!this.isVisible() && steppedZoom === mapZoom) { - this._url = ''; - return; - } - - // TODO add preference with a bit less weight than that for text/mapml; 0.8 for application/geo+json; 0.6 - var mapml, - headers = new Headers({ - Accept: 'text/mapml;q=0.9,application/geo+json;q=0.8' - }), - parser = new DOMParser(), - 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) { - return response.text(); - }) - .then(function (text) { - //TODO wrap this puppy in a try/catch/finally to parse application/geo+json if necessary - mapml = parser.parseFromString(text, 'application/xml'); - var base = new URL( - mapml.querySelector('map-base') - ? mapml.querySelector('map-base').getAttribute('href') - : url - ).href; - url = mapml.querySelector('map-link[rel=next]') - ? mapml.querySelector('map-link[rel=next]').getAttribute('href') - : null; - url = url ? new URL(url, base).href : null; - 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); - } - }); - }; - this._url = url; - _pullFeatureFeed(url, MAX_PAGES) - .then(function () { - map.addLayer(featureLayer); - //Fires event for feature index overlay - map.fire('templatedfeatureslayeradd'); - TemplatedFeaturesLayer.prototype._updateTabIndex(context); - }) - .catch(function (error) { - console.log(error); - }); - }, - setZIndex: function (zIndex) { - this.options.zIndex = zIndex; - this._updateZIndex(); - return this; - }, - _updateTabIndex: function (context) { - let c = context || this; - for (let layerNum in c._features._layers) { - let layer = c._features._layers[layerNum]; - if (layer._path) { - if (layer._path.getAttribute('d') !== 'M0 0') { - layer._path.setAttribute('tabindex', 0); - } else { - layer._path.removeAttribute('tabindex'); - } - if (layer._path.childElementCount === 0) { - let title = document.createElement('title'); - title.innerText = mapEl.locale.dfFeatureCaption; - layer._path.appendChild(title); - } - } - } - }, - _updateZIndex: function () { - if ( - this._container && - this.options.zIndex !== undefined && - this.options.zIndex !== null - ) { - this._container.style.zIndex = this.options.zIndex; - } - }, - _getfeaturesUrl: function (zoom, bounds) { - if (zoom === undefined) zoom = this._map.getZoom(); - if (bounds === undefined) bounds = this._map.getPixelBounds(); - var obj = {}; - if (this.options.feature.zoom) { - obj[this.options.feature.zoom] = zoom; - } - if (this.options.feature.width) { - obj[this.options.feature.width] = this._map.getSize().x; - } - if (this.options.feature.height) { - obj[this.options.feature.height] = this._map.getSize().y; - } - if (this.options.feature.bottom) { - obj[this.options.feature.bottom] = this._TCRSToPCRS(bounds.max, zoom).y; - } - if (this.options.feature.left) { - obj[this.options.feature.left] = this._TCRSToPCRS(bounds.min, zoom).x; - } - if (this.options.feature.top) { - obj[this.options.feature.top] = this._TCRSToPCRS(bounds.min, zoom).y; - } - if (this.options.feature.right) { - obj[this.options.feature.right] = this._TCRSToPCRS(bounds.max, zoom).x; - } - // hidden and other variables that may be associated - for (var v in this.options.feature) { - if ( - ['width', 'height', 'left', 'right', 'top', 'bottom', 'zoom'].indexOf( - v - ) < 0 - ) { - obj[v] = this.options.feature[v]; - } - } - return LeafletUtil.template(this._template.template, obj); - }, - _TCRSToPCRS: function (coords, zoom) { - // TCRS pixel point to Projected CRS point (in meters, presumably) - var map = this._map, - crs = map.options.crs, - loc = crs.transformation.untransform(coords, crs.scale(zoom)); - return loc; - }, - _setUpFeaturesTemplateVars: function (template) { - // process the inputs and create an object named "extent" - // with member properties as follows: - // {width: {name: 'widthvarname'}, // value supplied by map if necessary - // height: {name: 'heightvarname'}, // value supplied by map if necessary - // left: {name: 'leftvarname', axis: 'leftaxisname'}, // axis name drives (coordinate system of) the value supplied by the map - // right: {name: 'rightvarname', axis: 'rightaxisname'}, // axis name (coordinate system of) drives the value supplied by the map - // top: {name: 'topvarname', axis: 'topaxisname'}, // axis name drives (coordinate system of) the value supplied by the map - // bottom: {name: 'bottomvarname', axis: 'bottomaxisname'} // axis name drives (coordinate system of) the value supplied by the map - // zoom: {name: 'zoomvarname'} - // hidden: [{name: name, value: value}]} - - var featuresVarNames = { feature: {} }, - inputs = template.values; - featuresVarNames.feature.hidden = []; - for (var i = 0; i < inputs.length; i++) { - // this can be removed when the spec removes the deprecated inputs... - 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'), - value = inputs[i].getAttribute('value'), - select = inputs[i].tagName.toLowerCase() === 'map-select'; - if (type === 'width') { - featuresVarNames.feature.width = name; - } else if (type === 'height') { - featuresVarNames.feature.height = name; - } else if (type === 'zoom') { - featuresVarNames.feature.zoom = name; - } else if ( - type === 'location' && - (units === 'pcrs' || units === 'gcrs') - ) { - // diff --git a/test/e2e/layers/multipleExtents.test.js b/test/e2e/layers/multipleExtents.test.js index 68001d74b..6d01914de 100644 --- a/test/e2e/layers/multipleExtents.test.js +++ b/test/e2e/layers/multipleExtents.test.js @@ -77,7 +77,7 @@ test.describe('Adding and Removing Multiple Extents', () => { (div) => div.className ); expect(alabama).toEqual( - 'leaflet-layer mapml-features-container leaflet-pane mapml-vector-container' + 'leaflet-layer mapml-features-tiles-container leaflet-pane mapml-vector-container' ); const cbmt = await page.$eval( 'div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div:nth-child(4) > div', @@ -171,7 +171,7 @@ test.describe('Adding and Removing Multiple Extents', () => { // alabama is a templated feature extent // the opacity of the alabama features is tested by the selector expect(alabama).toEqual( - 'leaflet-layer mapml-features-container leaflet-pane mapml-vector-container' + 'leaflet-layer mapml-features-tiles-container leaflet-pane mapml-vector-container' ); // cbmt is a templated tile extent // the opacity of the cbmt tiles is tested by the selector @@ -226,7 +226,7 @@ test.describe('Adding and Removing Multiple Extents', () => { ); // alabama opacity is tested by the selector expect(alabamaClass).toEqual( - 'leaflet-layer mapml-features-container leaflet-pane mapml-vector-container' + 'leaflet-layer mapml-features-tiles-container leaflet-pane mapml-vector-container' ); // cbmt opacity is tested by the selector expect(cbmtClass).toEqual('leaflet-layer mapml-templated-tile-container'); @@ -435,7 +435,7 @@ test.describe('Multiple Extents Reordering and ZIndices Tests', () => { expect(secondExtentInLayerControl).toEqual('alabama_feature'); // alabama (a templated features layer) should have a higher zIndex than cbmt let alabamaIndex = await page.$eval( - 'div.mapml-features-container', + 'div.mapml-features-tiles-container', (div) => +div.closest('.mapml-extentlayer-container').style.zIndex ); let cbmtIndex = await page.$eval( @@ -481,7 +481,7 @@ test.describe('Multiple Extents Reordering and ZIndices Tests', () => { expect(secondExtentInLayerControl).toEqual('cbmt'); // alabama (a templated features layer) should have a lower zIndex than cbmt alabamaIndex = await page.$eval( - 'div.mapml-features-container', + 'div.mapml-features-tiles-container', (div) => +div.closest('.mapml-extentlayer-container').style.zIndex ); cbmtIndex = await page.$eval( @@ -519,7 +519,7 @@ test.describe('Multiple Extents Reordering and ZIndices Tests', () => { expect(secondExtentInLayerControl).toEqual('cbmt'); let alabama = await page.$$eval( - 'div.mapml-features-container', + 'div.mapml-features-tiles-container', (divs) => divs.length ); expect(alabama).toEqual(1); @@ -555,7 +555,7 @@ test.describe('Multiple Extents Reordering and ZIndices Tests', () => { expect(secondExtentInLayerControl).toEqual('cbmt'); alabama = await page.$$eval( - 'div.mapml-features-container', + 'div.mapml-features-tiles-container', (divs) => divs.length ); expect(alabama).toEqual(1); @@ -598,7 +598,7 @@ test.describe('Multiple Extents Reordering and ZIndices Tests', () => { // alabama (a templated features layer) should now have a higher zIndex than cbmt let alabamaIndex = await page.$eval( - 'div.mapml-features-container', + 'div.mapml-features-tiles-container', (div) => +div.closest('.mapml-extentlayer-container').style.zIndex ); let cbmtIndex = await page.$eval( From 478455ba93acc6bb87fbbf006f7e533dae123350 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 14 May 2025 13:34:55 -0400 Subject: [PATCH 04/16] Unbreak multipleExtents.test.js. Keep this commit separate perhaps, as it ensures that the FeatureLayer created by templated features has the options._leafletLayer and options._leafletLayer._layerEl props, which are used by DebugOverlay, possibly in not a great way. The question is should the FeatureLayer's features _renderings_ (geometry Leaflet layer) EACH have the layerBounds of the parent map-link OR should they not have bounds that can be used in DebugOverlay. --- src/map-feature.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/map-feature.js b/src/map-feature.js index a1cd6bb89..1e0cd7eb0 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -261,13 +261,12 @@ 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(); } @@ -327,7 +326,14 @@ export class HTMLFeatureElement extends HTMLElement { } } }); - extend(this._featureLayer.options, { _leafletLayer: this._featureLayer }); + // 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); From 592fda415c9b338fd0929e02adbef5a53b079810 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 14 May 2025 15:28:39 -0400 Subject: [PATCH 05/16] Add temporary debug logging to TemplatedTileLayer.js --- src/mapml/layers/TemplatedTileLayer.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index f5f7d418a..79248901c 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -273,6 +273,22 @@ export var TemplatedTileLayer = TileLayer.extend({ mapEl: this._linkEl.getMapEl() }); let fallback = Util.getNativeVariables(markup); + let tiles = markup.querySelectorAll('map-tile'); + for (let i = 0; i < tiles.length; i++) { + let row = tiles[i].getAttribute('row'), + col = tiles[i].getAttribute('col'), + z = tiles[i].getAttribute('zoom'); + console.log( + 'Total tiles for row: ' + + row + + ', col: ' + + col + + ', z: ' + + z + + ': ' + + tiles.length + ); + } let features = markup.querySelectorAll('map-feature:has(> map-geometry)'); for (let i = 0; i < features.length; i++) { let feature = tileFeatures.createGeometry( From 8c044464daf14d4a019ed77c62004a040137a0b2 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 14 May 2025 15:46:16 -0400 Subject: [PATCH 06/16] Restore / add redraw method to TemplatedFeaturesOrTilesLayerGroup --- src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js b/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js index c93e795f1..4caf6c8a9 100644 --- a/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js +++ b/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js @@ -98,6 +98,9 @@ export var TemplatedFeaturesOrTilesLayerGroup = LayerGroup.extend({ this.extentBounds.overlaps(mapBounds) ); }, + redraw: function () { + this._onMoveEnd(); + }, _onMoveEnd: function () { let history = this._map.options.mapEl._history; let current = history[history.length - 1]; From bee4b65880df67c19a1a3124879e0d530006baae Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 14 May 2025 16:35:42 -0400 Subject: [PATCH 07/16] Tentative stab at rendering in TemplatedTileLayer.js --- src/mapml/layers/TemplatedTileLayer.js | 45 ++++++++++++++++++-------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 79248901c..d095720c4 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -273,6 +273,7 @@ export var TemplatedTileLayer = TileLayer.extend({ mapEl: this._linkEl.getMapEl() }); let fallback = Util.getNativeVariables(markup); + // temporary, log the tiles in case there's more than one... let tiles = markup.querySelectorAll('map-tile'); for (let i = 0; i < tiles.length; i++) { let row = tiles[i].getAttribute('row'), @@ -289,19 +290,37 @@ export var TemplatedTileLayer = TileLayer.extend({ tiles.length ); } - let features = markup.querySelectorAll('map-feature:has(> map-geometry)'); - for (let i = 0; i < features.length; i++) { - let feature = tileFeatures.createGeometry( - features[i], - fallback.cs, - coords.z - ); - for (let featureID in feature._layers) { - let layer = feature._layers[featureID]; - FeatureRenderer.prototype._initPath(layer, false); - layer._project(this._map, point([xOffset, yOffset]), coords.z); - FeatureRenderer.prototype._addPath(layer, g, false); - FeatureRenderer.prototype._updateFeature(layer); + let currentTileSelector = + '[row=' + coords.y + '][col=' + coords.x + '][zoom=' + coords.z + ']'; + + // this should select and process the features and tiles in DOM order + let featuresOrTiles = markup.querySelectorAll( + 'map-feature:has(> map-geometry),map-tile' + currentTileSelector + ); + for (let i = 0; i < featuresOrTiles.length; i++) { + if (featuresOrTiles.NODE_NAME === 'MAP-FEATURE') { + let feature = tileFeatures.createGeometry( + featuresOrTiles[i], + fallback.cs, + coords.z + ); + for (let featureID in feature._layers) { + // layer is an M.Path instance + let layer = feature._layers[featureID]; + FeatureRenderer.prototype._initPath(layer, false); + // does something to layer + layer._project(this._map, point([xOffset, yOffset]), coords.z); + // appends the guts of layer to g + FeatureRenderer.prototype._addPath(layer, g, false); + // updates the guts of layer that have already been appended to g + FeatureRenderer.prototype._updateFeature(layer); + } + } else { + // render the tile as an svg image element + let tile = featuresOrTiles[i]; + let img = SVG.create('image'); + img.href = tile.src; + g.appendChild(img); } } svg.setAttribute('width', tileSize.toString()); From f659f50e548e195539ce4b708b5cda94b3a4e881 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 14 May 2025 17:02:25 -0400 Subject: [PATCH 08/16] Tweak TemplatedTileLayer._createFeatures method to render tiles --- index.html | 2 +- src/mapml/layers/TemplatedTileLayer.js | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index 0cfc4abee..1ebacbc95 100644 --- a/index.html +++ b/index.html @@ -35,7 +35,7 @@ - + diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index d095720c4..31c9d481b 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -291,14 +291,20 @@ export var TemplatedTileLayer = TileLayer.extend({ ); } let currentTileSelector = - '[row=' + coords.y + '][col=' + coords.x + '][zoom=' + coords.z + ']'; + '[row="' + + coords.y + + '"][col="' + + coords.x + + '"][zoom="' + + coords.z + + '"]'; // this should select and process the features and tiles in DOM order let featuresOrTiles = markup.querySelectorAll( 'map-feature:has(> map-geometry),map-tile' + currentTileSelector ); for (let i = 0; i < featuresOrTiles.length; i++) { - if (featuresOrTiles.NODE_NAME === 'MAP-FEATURE') { + if (featuresOrTiles[i].nodeName === 'map-feature') { let feature = tileFeatures.createGeometry( featuresOrTiles[i], fallback.cs, @@ -316,10 +322,10 @@ export var TemplatedTileLayer = TileLayer.extend({ FeatureRenderer.prototype._updateFeature(layer); } } else { - // render the tile as an svg image element + // render tile as an svg image element let tile = featuresOrTiles[i]; let img = SVG.create('image'); - img.href = tile.src; + img.setAttribute('href', tile.getAttribute('src')); g.appendChild(img); } } From 8f1f8297552dc92d2457b109ffe0bbad4b8551d2 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 14 May 2025 17:18:24 -0400 Subject: [PATCH 09/16] Optimize tile image loading for --- src/mapml/layers/TemplatedTileLayer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 31c9d481b..4667fdd48 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -324,8 +324,12 @@ export var TemplatedTileLayer = TileLayer.extend({ } else { // render tile as an svg image element let tile = featuresOrTiles[i]; + // No need to append to DOM, the browser will cache it + // observed to be a bit faster than waiting until img is appended to DOM + const imgObj = new Image(); + imgObj.src = tile.getAttribute('src'); let img = SVG.create('image'); - img.setAttribute('href', tile.getAttribute('src')); + img.setAttribute('href', imgObj.src); g.appendChild(img); } } From 39430f6550faf1bdd9457f82a40708f80a13104c Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 14 May 2025 17:28:19 -0400 Subject: [PATCH 10/16] Optimize tile image loading for s that connect to DOM. Improves the rendering time of tiles loaded via --- src/map-tile.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/map-tile.js b/src/map-tile.js index 5286100bd..26901b301 100644 --- a/src/map-tile.js +++ b/src/map-tile.js @@ -75,6 +75,11 @@ export class HTMLTileElement extends HTMLElement { ? 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'); + this._createOrGetTileLayer(); // Calculate the extent From 53453f8188a853e6a9797ee55e8bb2d21b5985df Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Sat, 5 Jul 2025 20:48:05 -0400 Subject: [PATCH 11/16] Rename MapFeatureLayerGroup -> MapFeatureLayer Add to do list --- ToDo | 18 +++++++ src/map-link.js | 2 +- ...eatureLayerGroup.js => MapFeatureLayer.js} | 48 +++++++++---------- src/mapml/layers/MapTileLayer.js | 6 +-- .../TemplatedFeaturesOrTilesLayerGroup.js | 10 ++-- 5 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 ToDo rename src/mapml/layers/{MapFeatureLayerGroup.js => MapFeatureLayer.js} (93%) diff --git a/ToDo b/ToDo new file mode 100644 index 000000000..db0050133 --- /dev/null +++ b/ToDo @@ -0,0 +1,18 @@ +Can we get rid of StaticTileLayer ? I believe it's only used in MapMLLayer.js... + +Could "TemplatedFeaturesOrTilesLayerGroup" be renamed to reduce cognitive burden? +- to consider: semantic overlap with TemplatedTileLayer + +MapFeatureLayer and MapTileLayer are intended to work together as individual layers +in a LayerGroup. On the one hand, we have TemplatedFeaturesOrTilesLayerGroup as +a parent layer, and on the other we have MapMLLayer which acts as a parent container +but in an odd way - see StaticTileLayer and how the FeatureLayer is constructed and +used. + +There is a privileged relation between and FeatureLayer, as well, +which is determined by how the FeatureLayer is constructed, in contrast to how it +is constructed by MapMLLayer, as well as how it's constructed during a query. + +An additional complication is that FeatureLayer uses a custom renderer, and the custom +renderer has some behaviour that is a bit odd for a renderer, at least from an +outside perspective. diff --git a/src/map-link.js b/src/map-link.js index 8edddaf3f..0bd6cdee2 100644 --- a/src/map-link.js +++ b/src/map-link.js @@ -10,8 +10,8 @@ import { import { Util } from './mapml/utils/Util.js'; import { templatedImageLayer } from './mapml/layers/TemplatedImageLayer.js'; import { templatedTileLayer } from './mapml/layers/TemplatedTileLayer.js'; -import { templatedPMTilesLayer } from './mapml/layers/TemplatedPMTilesLayer.js'; import { templatedFeaturesOrTilesLayerGroup } from './mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js'; +import { templatedPMTilesLayer } from './mapml/layers/TemplatedPMTilesLayer.js'; /* global M */ export class HTMLLinkElement extends HTMLElement { diff --git a/src/mapml/layers/MapFeatureLayerGroup.js b/src/mapml/layers/MapFeatureLayer.js similarity index 93% rename from src/mapml/layers/MapFeatureLayerGroup.js rename to src/mapml/layers/MapFeatureLayer.js index 9628daddf..57abe8a2b 100644 --- a/src/mapml/layers/MapFeatureLayerGroup.js +++ b/src/mapml/layers/MapFeatureLayer.js @@ -10,28 +10,28 @@ import { Util } from '../utils/Util.js'; import { path } from '../features/path.js'; import { geometry } from '../features/geometry.js'; -export var MapFeatureLayerGroup = FeatureGroup.extend({ - /* - * This is the feature equivalent of MapTileLayer. The intended use is to - * represent an adjacent sequence of elements found in a templated - * response, but MAYBE we'll be able to use it to represent such a sequence - * in a static document response, too, tbd. - * - * 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] - * LayerGroup._layers[0] - * LayerGroup._layers[1] <- each set of adjacent features - * LayerGroup._layers[1] <- is a single MapFeatureLayerGroup - * LayerGroup._layers[2] - * LayerGroup._layers[2] - * LayerGroup._layers[3] - * LayerGroup._layers[3] - * and so on - * - */ +/** + * This is the feature equivalent of MapTileLayer. The intended use is to + * represent an adjacent sequence of elements found in a templated + * response, but MAYBE we'll be able to use it to represent such a sequence + * in a static document response, too, tbd. + * + * 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 + * LayerGroup._layers[1] <- each set of adjacent features + * LayerGroup._layers[1] <- is a single MapFeatureLayer + * LayerGroup._layers[2] + * LayerGroup._layers[2] + * LayerGroup._layers[3] + * LayerGroup._layers[3] + * and so on + * + */ +export var MapFeatureLayer = FeatureGroup.extend({ initialize: function (mapml, options) { /* mapml: @@ -551,6 +551,6 @@ export var MapFeatureLayerGroup = FeatureGroup.extend({ } } }); -export var mapFeatureLayerGroup = function (mapml, options) { - return new MapFeatureLayerGroup(mapml, options); +export var mapFeatureLayer = function (mapml, options) { + return new MapFeatureLayer(mapml, options); }; diff --git a/src/mapml/layers/MapTileLayer.js b/src/mapml/layers/MapTileLayer.js index 0b7b986d6..2f3b375cc 100644 --- a/src/mapml/layers/MapTileLayer.js +++ b/src/mapml/layers/MapTileLayer.js @@ -5,7 +5,7 @@ import { Util } from '../utils/Util.js'; * Leaflet layer for handling map-tile elements * Extends GridLayer to create tiles based on map-tile elements * - * Similar in intent to MapFeatureLayerGroup, which is a receiver for + * Similar in intent to MapFeatureLayer, which is a receiver for * map-feature elements' leaflet layer object * * This layer will be inserted into the LayerGroup hosted by the @@ -14,8 +14,8 @@ import { Util } from '../utils/Util.js'; * * LayerGroup._layers[0] <- each set of adjacent tiles * LayerGroup._layers[0] <- is a single MapTileLayer - * LayerGroup._layers[1] - * LayerGroup._layers[1] + * LayerGroup._layers[1] <- each set of adjacent features + * LayerGroup._layers[1] <- is a single MapFeatureLayer * LayerGroup._layers[2] * LayerGroup._layers[2] * LayerGroup._layers[3] diff --git a/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js b/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js index 4caf6c8a9..7ebf4bc3a 100644 --- a/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js +++ b/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js @@ -11,16 +11,16 @@ import { featureLayer } from './FeatureLayer.js'; import { renderStyles } from '../elementSupport/layers/renderStyles.js'; /** - * Layer group for managing map-tile and map-feature elements retrieved via + * LayerGroup for managing map-tile and map-feature elements retrieved via * * * Layers in this layer group will correspond to the following MapML elements * retrieved by the template processing: * - * LayerGroup._layers[0] - * LayerGroup._layers[0] - * LayerGroup._layers[1] - * LayerGroup._layers[1] + * LayerGroup._layers[0] <- each set of adjacent tiles + * LayerGroup._layers[0] <- is a single MapTileLayer + * LayerGroup._layers[1] <- each set of adjacent features + * LayerGroup._layers[1] <- is a single MapFeatureLayer * LayerGroup._layers[2] * LayerGroup._layers[2] * LayerGroup._layers[3] From df4c1ad2f2dd1e1f3f0886a75cbd98df19fb7fe7 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Sat, 5 Jul 2025 21:40:48 -0400 Subject: [PATCH 12/16] Rename TemplatedFeaturesOrTilesLayerGroup to TemplatedFeaturesOrTilesLayer Update ideas in To Do list --- ToDo | 28 ++++++++++++++++++- src/map-feature.js | 2 +- src/map-link.js | 25 ++++++++--------- src/map-tile.js | 2 +- ...up.js => TemplatedFeaturesOrTilesLayer.js} | 6 ++-- 5 files changed, 43 insertions(+), 20 deletions(-) rename src/mapml/layers/{TemplatedFeaturesOrTilesLayerGroup.js => TemplatedFeaturesOrTilesLayer.js} (98%) diff --git a/ToDo b/ToDo index db0050133..28c8fc947 100644 --- a/ToDo +++ b/ToDo @@ -1,7 +1,7 @@ Can we get rid of StaticTileLayer ? I believe it's only used in MapMLLayer.js... Could "TemplatedFeaturesOrTilesLayerGroup" be renamed to reduce cognitive burden? -- to consider: semantic overlap with TemplatedTileLayer +- to consider: semantic overlap with TemplatedTileLayer DONE MapFeatureLayer and MapTileLayer are intended to work together as individual layers in a LayerGroup. On the one hand, we have TemplatedFeaturesOrTilesLayerGroup as @@ -16,3 +16,29 @@ is constructed by MapMLLayer, as well as how it's constructed during a query. An additional complication is that FeatureLayer uses a custom renderer, and the custom renderer has some behaviour that is a bit odd for a renderer, at least from an outside perspective. + +task - eliminate StaticTileLayer by using MapMLLayer (a LayerGroup) in the same role as +TemplatedFeaturesOrTilesLayerGroup, EXCEPT that a MapMLLayer can have +, and/or in any order, whereas a +TemplatedFeaturesOrTilesLayerGroup can ONLY have and/or + +- to accomplish above, would have to behave like , in that +it would have to add its own LayerGroup to the parent MapMLLayer's LayerGroup +(I think it must do this already - CHECK IT). + +Idea: could we rename Templated* layers to MapLink* + +Does that mean we need to rename MapMLLayer to MapLayer for consistency with the +custom element name? + +Could we rename mapml- to map-document, and provide backwards-compatibility to the +old name? + +Rename ExtentLayer to MapExtentLayer + +FeatureLayer is fundamental currently. How to get rid of? + +There is ImageLayer, but no MapImage element. Is this a shortcoming? + + + diff --git a/src/map-feature.js b/src/map-feature.js index 1e0cd7eb0..338c05769 100644 --- a/src/map-feature.js +++ b/src/map-feature.js @@ -337,7 +337,7 @@ export class HTMLFeatureElement extends HTMLElement { this.addFeature(this._featureLayer); - // add featureLayer to TemplatedFeaturesOrTilesLayerGroup of the parentElement + // add featureLayer to TemplatedFeaturesOrTilesLayer of the parentElement if ( parentElement._templatedLayer && parentElement._templatedLayer.addLayer diff --git a/src/map-link.js b/src/map-link.js index 0bd6cdee2..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 { templatedFeaturesOrTilesLayerGroup } from './mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js'; +import { templatedFeaturesOrTilesLayer } from './mapml/layers/TemplatedFeaturesOrTilesLayer.js'; import { templatedPMTilesLayer } from './mapml/layers/TemplatedPMTilesLayer.js'; /* global M */ @@ -437,7 +437,7 @@ export class HTMLLinkElement extends HTMLElement { // 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 - // TemplatedFeaturesOrTilesLayerGroup + // 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 @@ -553,18 +553,15 @@ export class HTMLLinkElement extends HTMLElement { this.attachShadow({ mode: 'open' }); } // Use the FeaturesTilesLayerGroup to handle both map-feature and map-tile elements - this._templatedLayer = templatedFeaturesOrTilesLayerGroup( - this._templateVars, - { - zoomBounds: this.getZoomBounds(), - extentBounds: this.getBounds(), - zIndex: this.zIndex, - pane: this.parentExtent._extentLayer.getContainer(), - linkEl: this, - projection: this.mapEl._map.options.projection, - renderer: this.mapEl._map.options.renderer - } - ).addTo(this.parentExtent._extentLayer); + this._templatedLayer = templatedFeaturesOrTilesLayer(this._templateVars, { + zoomBounds: this.getZoomBounds(), + extentBounds: this.getBounds(), + zIndex: this.zIndex, + pane: this.parentExtent._extentLayer.getContainer(), + 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) { this.attachShadow({ mode: 'open' }); diff --git a/src/map-tile.js b/src/map-tile.js index 26901b301..5e0abd1ee 100644 --- a/src/map-tile.js +++ b/src/map-tile.js @@ -176,7 +176,7 @@ export class HTMLTileElement extends HTMLElement { }); this._tileLayer.addMapTile(this); - // add MapTileLayer to TemplatedFeaturesOrTilesLayerGroup of the parentElement + // add MapTileLayer to TemplatedFeaturesOrTilesLayer of the parentElement if ( parentElement._templatedLayer && parentElement._templatedLayer.addLayer diff --git a/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js b/src/mapml/layers/TemplatedFeaturesOrTilesLayer.js similarity index 98% rename from src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js rename to src/mapml/layers/TemplatedFeaturesOrTilesLayer.js index 7ebf4bc3a..a71a5aa53 100644 --- a/src/mapml/layers/TemplatedFeaturesOrTilesLayerGroup.js +++ b/src/mapml/layers/TemplatedFeaturesOrTilesLayer.js @@ -31,7 +31,7 @@ import { renderStyles } from '../elementSupport/layers/renderStyles.js'; * * Extends LayerGroup */ -export var TemplatedFeaturesOrTilesLayerGroup = LayerGroup.extend({ +export var TemplatedFeaturesOrTilesLayer = LayerGroup.extend({ initialize: function (template, options) { LayerGroup.prototype.initialize.call(this, []); this._template = template; @@ -318,6 +318,6 @@ export var TemplatedFeaturesOrTilesLayerGroup = LayerGroup.extend({ renderStyles }); -export var templatedFeaturesOrTilesLayerGroup = function (template, options) { - return new TemplatedFeaturesOrTilesLayerGroup(template, options); +export var templatedFeaturesOrTilesLayer = function (template, options) { + return new TemplatedFeaturesOrTilesLayer(template, options); }; From 0f1dc1d0152b55a0603469c9e17644bd221dbe9c Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Mon, 7 Jul 2025 11:05:22 -0400 Subject: [PATCH 13/16] Rename MapMLLayer.js -> MapLayer.js --- package-lock.json | 4 +- src/layer.js | 8 ++-- src/map-extent.js | 4 +- src/map-select.js | 2 +- src/mapml/control/LayerControl.js | 2 +- src/mapml/handlers/AnnounceMovement.js | 2 +- src/mapml/layers/FeatureLayer.js | 2 +- .../layers/{MapMLLayer.js => MapLayer.js} | 8 ++-- src/mapml/layers/TemplatedTileLayer.js | 2 +- test/e2e/layers/featureLayer.test.js | 2 +- .../layers/templatedPMTilesCBMTILETest.html | 2 +- .../layers/templatedPMTilesMVTLayer.test.js | 4 +- test/layers/mapMLLayer.spec.js | 40 +++++++++---------- 13 files changed, 42 insertions(+), 40 deletions(-) rename src/mapml/layers/{MapMLLayer.js => MapLayer.js} (99%) 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/src/layer.js b/src/layer.js index dd16c5e2b..d74b934bf 100644 --- a/src/layer.js +++ b/src/layer.js @@ -1,7 +1,7 @@ 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 { createLayerControlHTML } from './mapml/elementSupport/layers/createLayerControlForLayer.js'; export class BaseLayerElement extends HTMLElement { @@ -296,7 +296,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 +333,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 +467,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) { diff --git a/src/map-extent.js b/src/map-extent.js index cdc1f0395..d6c6afb76 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -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 */ @@ -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-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/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/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/MapMLLayer.js b/src/mapml/layers/MapLayer.js similarity index 99% rename from src/mapml/layers/MapMLLayer.js rename to src/mapml/layers/MapLayer.js index ee9be2e71..1a0444a49 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapLayer.js @@ -13,7 +13,7 @@ import { staticTileLayer } from './StaticTileLayer.js'; import { featureRenderer } from '../features/featureRenderer.js'; import { renderStyles } from '../elementSupport/layers/renderStyles.js'; -export var MapMLLayer = LayerGroup.extend({ +export var MapLayer = LayerGroup.extend({ options: { zIndex: 0, opacity: '1.0' @@ -651,7 +651,7 @@ export var MapMLLayer = LayerGroup.extend({ }; // we found that the popupopen event is fired as many times as there - // are layers on the map ( elements / MapMLLayers that is). + // are layers on the map ( elements / MapLayers 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' @@ -682,7 +682,7 @@ export var MapMLLayer = LayerGroup.extend({ } } }); -export var mapMLLayer = function (url, node, options) { +export var mapLayer = function (url, node, options) { if (!url && !node) return null; - return new MapMLLayer(url, node, options); + return new MapLayer(url, node, options); }; diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 4667fdd48..3e4e9d5ca 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -147,7 +147,7 @@ export var TemplatedTileLayer = TileLayer.extend({ DomUtil.addClass(tile, 'leaflet-tile-loaded'); }, // instead of being child of a pane, the TemplatedTileLayers are 'owned' by the group, - // and so are DOM children of the group, not the pane element (the MapMLLayer is + // and so are DOM children of the group, not the pane element (the MapLayer is // a child of the overlay pane and always has a set of sub-layers) getPane: function () { return this.options.pane; 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/templatedPMTilesCBMTILETest.html b/test/e2e/layers/templatedPMTilesCBMTILETest.html index 033cf2c94..1245dcd45 100644 --- a/test/e2e/layers/templatedPMTilesCBMTILETest.html +++ b/test/e2e/layers/templatedPMTilesCBMTILETest.html @@ -33,7 +33,7 @@ {z}/{x}/{y}.mvt test - + diff --git a/test/e2e/layers/templatedPMTilesMVTLayer.test.js b/test/e2e/layers/templatedPMTilesMVTLayer.test.js index 16efd6faf..2f39cb372 100644 --- a/test/e2e/layers/templatedPMTilesMVTLayer.test.js +++ b/test/e2e/layers/templatedPMTilesMVTLayer.test.js @@ -95,7 +95,9 @@ test.describe('Playwright templatedPMTilesLayer Tests', () => { await expect(fixedProjectionLayer).toHaveAttribute('disabled'); await viewer.evaluate((v) => (v.projection = 'OSMTILE')); await page.waitForTimeout(500); - await expect(flexProjectionLayer).not.toHaveAttribute('disabled'); + // the flex projection layer will still be disabled because the pmtiles + // layer lacks a stylesheet... + await expect(flexProjectionLayer).toHaveAttribute('disabled'); await expect(fixedProjectionLayer).not.toHaveAttribute('disabled'); }); test('A protomaps map-link, parent map-extent and ancestor map-layer are disabled when out of bounds', async ({ diff --git a/test/layers/mapMLLayer.spec.js b/test/layers/mapMLLayer.spec.js index 08d95245e..557ec394f 100644 --- a/test/layers/mapMLLayer.spec.js +++ b/test/layers/mapMLLayer.spec.js @@ -1,8 +1,8 @@ -describe('MapMLLayer Constructor Tests', () => { - describe('M.mapMLLayer(url) factory function ', () => { - test('(null content, null options) should return a MapMLLayer object', async () => { +describe('MapLayer Constructor Tests', () => { + describe('M.mapLayer(url) factory function ', () => { + test('(null content, null options) should return a MapLayer object', async () => { var url = 'https://geogratis.gc.ca/mapml/en/cbmtile/cbmt/'; - var ml = M.mapMLLayer(url); + var ml = M.mapLayer(url); await expect(ml._content).toBeFalsy(); await expect(ml._container).toBeTruthy(); await expect( @@ -17,7 +17,7 @@ describe('MapMLLayer Constructor Tests', () => { }); }); - describe('M.mapMLLayer(null, , options) factory function ', () => { + describe('M.mapLayer(null, , options) factory function ', () => { var content; beforeEach(async () => { content = document.createElement('map-layer'); @@ -30,8 +30,8 @@ describe('MapMLLayer Constructor Tests', () => { await expect(content instanceof HTMLElement).toBeTruthy(); }); - test('null url should return a MapMLLayer object', async () => { - var ml = M.mapMLLayer(null, content); + test('null url should return a MapLayer object', async () => { + var ml = M.mapLayer(null, content); await expect(ml._content).toBeTruthy(); await expect( ml._container.classList.contains('leaflet-layer') @@ -43,9 +43,9 @@ describe('MapMLLayer Constructor Tests', () => { await expect(ml._href).toBeFalsy(); await expect(ml.options.zIndex).toBe(0); }); - test('url should return a MapMLLayer object', async () => { + test('url should return a MapLayer object', async () => { var url = 'https://geogratis.gc.ca/mapml/en/cbmtile/cbmt/'; - var ml = M.mapMLLayer(url, content); + var ml = M.mapLayer(url, content); await expect(ml._content).toBeFalsy(); await expect(ml._layerEl).toBeTruthy(); await expect(ml._layerEl).toBe(content); @@ -62,7 +62,7 @@ describe('MapMLLayer Constructor Tests', () => { }); }); - describe('M.mapMLLayer(url | null, mapml content, options) factory function ', () => { + describe('M.mapLayer(url | null, mapml content, options) factory function ', () => { var content; beforeEach(async () => { content = document.createElement('foo'); @@ -75,9 +75,9 @@ describe('MapMLLayer Constructor Tests', () => { await expect(content instanceof HTMLElement).toBeTruthy(); }); - test('url value should return a url-based MapMLLayer object', async () => { + test('url value should return a url-based MapLayer object', async () => { var url = 'https://geogratis.gc.ca/mapml/en/cbmtile/cbmt/'; - var ml = M.mapMLLayer(url, content); + var ml = M.mapLayer(url, content); await expect(ml._content).toBeFalsy(); await expect(ml._layerEl).toBeTruthy(); await expect(ml._layerEl).toBe(content); @@ -92,8 +92,8 @@ describe('MapMLLayer Constructor Tests', () => { await expect(ml._href).toBe(url); await expect(ml.options.zIndex).toBe(0); }); - test('null url should return a inline-content based MapMLLayer object', async () => { - var ml = M.mapMLLayer(null, content); + test('null url should return a inline-content based MapLayer object', async () => { + var ml = M.mapLayer(null, content); await expect(ml._content).toBeTruthy(); await expect(ml._layerEl).toBeTruthy(); await expect(ml._layerEl).toBe(content); @@ -108,11 +108,11 @@ describe('MapMLLayer Constructor Tests', () => { await expect(ml.options.zIndex).toBe(0); }); }); - describe('M.mapMLLayer(url, (empty), options) factory function ', () => { - test('url value should return a url-based MapMLLayer object', async () => { + describe('M.mapLayer(url, (empty), options) factory function ', () => { + test('url value should return a url-based MapLayer object', async () => { var url = 'https://geogratis.gc.ca/mapml/en/cbmtile/cbmt/'; var empty = document.createElement('bar'); - var ml = M.mapMLLayer(url, empty); + var ml = M.mapLayer(url, empty); await expect(ml._content).toBeFalsy(); await expect(ml._layerEl).toBeTruthy(); await expect(ml._layerEl).toBe(empty); @@ -128,9 +128,9 @@ describe('MapMLLayer Constructor Tests', () => { await expect(ml.options.zIndex).toBe(0); }); }); - describe('M.mapMLLayer(null, null, options) factory function ', () => { - test('null url, null content, any object params should NOT return a MapMLLayer object', async () => { - var ml = M.mapMLLayer(null, null, {}); + describe('M.mapLayer(null, null, options) factory function ', () => { + test('null url, null content, any object params should NOT return a MapLayer object', async () => { + var ml = M.mapLayer(null, null, {}); await expect(ml).toBeFalsy(); }); }); From f726e63115b2a4e0c05c613a633064dd8ba8139e Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Mon, 7 Jul 2025 19:20:09 -0400 Subject: [PATCH 14/16] Update To Do list --- ToDo | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ToDo b/ToDo index 28c8fc947..af92ef93b 100644 --- a/ToDo +++ b/ToDo @@ -1,4 +1,26 @@ -Can we get rid of StaticTileLayer ? I believe it's only used in MapMLLayer.js... +Can we get rid of StaticTileLayer ? I believe it's only used in MapMLLayer.js. +- MapLayer has a _mapTileContainer. Should we keep it, or should all content +get rendered in the _container? + +map-tile.js has a function _createOrGetTileLayer which either constructs a new +MapTileLayer or obtains a reference to it from a previous . + +- The map-tile.js has a property called _parentElement which refers to either +the map-link that is responsible for loading the tile, or the map-layer, taking into account that the tile may be connected in a shadow root. That code is probably +duplicative of similar code in and should be de-duped if possible. + +ANYWAY, if map-tile invokes the constructor of MapTileLayer during _createOrGetTileLayer, +it sets the options.pane to parentElement._templatedLayer.getContainer(). If we +want that to work for MapLayer equally, we would want to use MapLayer.getContainer(), +I think (which needs to be implemented). THEN it invokes the (LayerGroup) addLayer(layer) +where layer is set to the constructed MapTileLayer. If the constructor is not +invoked (i.e. the map-tile is not the 1st in a sequence of map-tiles), the code +obtains the reference to the MapTileLayer and invokes addMapTile(this). + +map-feature.js has a similar method called _createOrGetFeatureLayer that seems to be +a new feature of map-feature probably introduced when TemplatedFeaturesOrTilesLayer +was introduced, because again it depends on the templated layer and needs to be +generalized to the other use cases, specifically for use with MapLayer. Could "TemplatedFeaturesOrTilesLayerGroup" be renamed to reduce cognitive burden? - to consider: semantic overlap with TemplatedTileLayer DONE @@ -29,7 +51,7 @@ it would have to add its own LayerGroup to the parent MapMLLayer's LayerGroup Idea: could we rename Templated* layers to MapLink* Does that mean we need to rename MapMLLayer to MapLayer for consistency with the -custom element name? +custom element name? DONE Could we rename mapml- to map-document, and provide backwards-compatibility to the old name? From ecf9a2745c2fc8c909d05ad3e105b6e63ae00b64 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 8 Jul 2025 11:20:05 -0400 Subject: [PATCH 15/16] Rename ExtentLayer.js to MapExtentLayer.js De-flake some tests --- src/map-extent.js | 4 ++-- src/mapml/layers/{ExtentLayer.js => MapExtentLayer.js} | 6 +++--- test/e2e/api/matchMedia/map-bounding-box.test.js | 6 +++++- test/e2e/core/styleParsing.test.js | 4 +++- test/e2e/layers/step/request.js | 5 ++++- 5 files changed, 17 insertions(+), 8 deletions(-) rename src/mapml/layers/{ExtentLayer.js => MapExtentLayer.js} (94%) diff --git a/src/map-extent.js b/src/map-extent.js index d6c6afb76..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 */ @@ -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( diff --git a/src/mapml/layers/ExtentLayer.js b/src/mapml/layers/MapExtentLayer.js similarity index 94% rename from src/mapml/layers/ExtentLayer.js rename to src/mapml/layers/MapExtentLayer.js index 6fd95f14d..9de69d32a 100644 --- a/src/mapml/layers/ExtentLayer.js +++ b/src/mapml/layers/MapExtentLayer.js @@ -1,7 +1,7 @@ import { LayerGroup, DomUtil } from 'leaflet'; import { renderStyles } from '../elementSupport/layers/renderStyles.js'; -export var ExtentLayer = LayerGroup.extend({ +export var MapExtentLayer = LayerGroup.extend({ initialize: function (options) { // Call LayerGroup's initialize to trigger Leaflet's setup LayerGroup.prototype.initialize.call(this, null, options); @@ -85,6 +85,6 @@ export var ExtentLayer = LayerGroup.extend({ }, renderStyles }); -export var extentLayer = function (options) { - return new ExtentLayer(options); +export var mapExtentLayer = function (options) { + return new MapExtentLayer(options); }; diff --git a/test/e2e/api/matchMedia/map-bounding-box.test.js b/test/e2e/api/matchMedia/map-bounding-box.test.js index 81876bbc6..a166f0b0e 100644 --- a/test/e2e/api/matchMedia/map-bounding-box.test.js +++ b/test/e2e/api/matchMedia/map-bounding-box.test.js @@ -4,7 +4,10 @@ test.describe('matchMedia map-bounding-box tests', () => { let page; let context; test.beforeAll(async () => { - context = await chromium.launchPersistentContext('', { headless: false }); + context = await chromium.launchPersistentContext('', { + headless: false, + ignoreHTTPSErrors: true + }); page = context.pages().find((page) => page.url() === 'about:blank') || (await context.newPage()); @@ -17,6 +20,7 @@ test.describe('matchMedia map-bounding-box tests', () => { }); test('matchMedia API detects changes in map-extents', async () => { + await page.waitForTimeout(500); const map = page.locator('mapml-viewer'); const zoomIn = page.locator('.leaflet-control-zoom-in'); const zoomOut = page.locator('.leaflet-control-zoom-out'); diff --git a/test/e2e/core/styleParsing.test.js b/test/e2e/core/styleParsing.test.js index ecbde373f..1fb2710ab 100644 --- a/test/e2e/core/styleParsing.test.js +++ b/test/e2e/core/styleParsing.test.js @@ -4,7 +4,9 @@ test.describe('map-style and map-link[rel=stylesheet] tests', () => { let page; let context; test.beforeAll(async () => { - context = await chromium.launchPersistentContext(''); + context = await chromium.launchPersistentContext('', { + ignoreHTTPSErrors: true + }); page = context.pages().find((page) => page.url() === 'about:blank') || (await context.newPage()); diff --git a/test/e2e/layers/step/request.js b/test/e2e/layers/step/request.js index d90bb09a7..0fb175b6f 100644 --- a/test/e2e/layers/step/request.js +++ b/test/e2e/layers/step/request.js @@ -37,7 +37,10 @@ exports.test = ( ) => { test.describe('Request Tests', () => { test.beforeAll(async () => { - context = await chromium.launchPersistentContext(''); + context = await chromium.launchPersistentContext('', { + // was causing strange test failures + ignoreHTTPSErrors: true + }); page = await context.newPage(); await page.goto(path); }); From 2f1d76e7e46e09c20815cf7f0f3817b37be5eb04 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Tue, 8 Jul 2025 17:21:50 -0400 Subject: [PATCH 16/16] Remove link to StaticTileLayer.js prior to deleting it. A few tests are broken as a result. Refactoring of bounds calculation for MapLayer is required. --- ToDo | 20 ++++-- src/map-tile.js | 24 +++++--- src/mapml/layers/MapLayer.js | 82 ++++++++++--------------- src/mapml/layers/MapTileLayer.js | 9 +-- test/e2e/layers/staticTileLayer.test.js | 25 +++++--- 5 files changed, 87 insertions(+), 73 deletions(-) diff --git a/ToDo b/ToDo index af92ef93b..76e884390 100644 --- a/ToDo +++ b/ToDo @@ -48,7 +48,12 @@ TemplatedFeaturesOrTilesLayerGroup can ONLY have and/or element? Currently, there's no "MapLinkLayer", but +these could all potentially be subclasses of MapLinkLayer, tbd, sharing some +common code, perhaps?? map-link.js itself might be that common code, but we should +investigate if there's any behaviour implemented across those layers that could +be consolidated. Does that mean we need to rename MapMLLayer to MapLayer for consistency with the custom element name? DONE @@ -56,11 +61,18 @@ custom element name? DONE Could we rename mapml- to map-document, and provide backwards-compatibility to the old name? -Rename ExtentLayer to MapExtentLayer +DONE Rename ExtentLayer to MapExtentLayer -FeatureLayer is fundamental currently. How to get rid of? +FeatureLayer is fundamental currently. How to get rid of? We don't: we replace +it with MapFeatureLayer. MapFeatureLayer gets inserted into the MapLayer.layers +(LayerGroup) array (via LayerGroup.addLayer(l)) when the attaches +to the DOM. +- the MapLayer currently has a ._mapmlvectors property, that should be removed, +managed by the inherent LayerGroup-ness of MapLayer. +- also, the MapLayer currently has ._mapmlTileContainer and ._staticTileLayer which +should be removed and managed via the LayerGroup per features -There is ImageLayer, but no MapImage element. Is this a shortcoming? +There is ImageLayer, but no MapImage element. Is this a shortcoming? TBD diff --git a/src/map-tile.js b/src/map-tile.js index 5e0abd1ee..9d11f74bc 100644 --- a/src/map-tile.js +++ b/src/map-tile.js @@ -57,7 +57,7 @@ export class HTMLTileElement extends HTMLElement { // Always call super first in constructor super(); } - connectedCallback() { + 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 @@ -80,7 +80,7 @@ export class HTMLTileElement extends HTMLElement { const imgObj = new Image(); imgObj.src = this.getAttribute('src'); - this._createOrGetTileLayer(); + await this._createOrGetTileLayer(); // Calculate the extent //this._calculateExtent(); @@ -164,24 +164,28 @@ export class HTMLTileElement extends HTMLElement { } } } - _createOrGetTileLayer() { + async _createOrGetTileLayer() { + await this._parentElement.whenReady(); if (this.isFirst()) { const parentElement = this._parentElement; // Create a new MapTileLayer this._tileLayer = mapTileLayer({ - projection: this.getMapEl()._map.options.projection, + projection: this.getMapEl().projection, opacity: 1, - pane: parentElement._templatedLayer.getContainer() + // 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 parentElement - if ( - parentElement._templatedLayer && - parentElement._templatedLayer.addLayer - ) { + // 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 diff --git a/src/mapml/layers/MapLayer.js b/src/mapml/layers/MapLayer.js index 1a0444a49..7ef264a85 100644 --- a/src/mapml/layers/MapLayer.js +++ b/src/mapml/layers/MapLayer.js @@ -9,7 +9,7 @@ import { } from 'leaflet'; import { Util } from '../utils/Util.js'; import { featureLayer } from './FeatureLayer.js'; -import { staticTileLayer } from './StaticTileLayer.js'; +import { MapTileLayer } from './MapTileLayer.js'; import { featureRenderer } from '../features/featureRenderer.js'; import { renderStyles } from '../elementSupport/layers/renderStyles.js'; @@ -33,18 +33,14 @@ export var MapLayer = LayerGroup.extend({ this.changeOpacity(this.options.opacity); DomUtil.addClass(this._container, 'mapml-layer'); - // this layer 'owns' a mapmlTileLayer, which is a subclass of L.GridLayer - // it 'passes' what tiles to load via the content of this._mapmlTileContainer - this._mapmlTileContainer = DomUtil.create( - 'div', - 'mapml-tile-container', - this._container - ); // hit the service to determine what its extent might be // OR use the extent of the content provided this._initialize(this._content); }, + getContainer: function () { + return this._container; + }, setZIndex: function (zIndex) { this.options.zIndex = zIndex; this._updateZIndex(); @@ -88,13 +84,9 @@ export var MapLayer = LayerGroup.extend({ }, onAdd: function (map) { - LayerGroup.prototype.onAdd.call(this, map); this.getPane().appendChild(this._container); + LayerGroup.prototype.onAdd.call(this, map); - //only add the layer if there are tiles to be rendered - if (this._staticTileLayer) { - this.addLayer(this._staticTileLayer); - } this.setZIndex(this.options.zIndex); map.on('popupopen', this._attachSkipButtons, this); }, @@ -172,10 +164,7 @@ export var MapLayer = LayerGroup.extend({ } } } - } else if ( - type === '_mapmlvectors' || - (type === '_staticTileLayer' && this._staticTileLayer) - ) { + } else if (type === '_mapmlvectors') { if (this[type].layerBounds) { if (!bnds) { bnds = this[type].layerBounds; @@ -204,6 +193,34 @@ export var MapLayer = LayerGroup.extend({ zoomBounds.maxNativeZoom = maxNativeZoom; } } + } else { + // inline tiles + this.eachLayer((layer) => { + if (layer instanceof MapTileLayer) { + if (layer.layerBounds) { + // extend bnds with layer layerBounds + bnds.extend(layer.layerBounds); + } + + if (layer.zoomBounds) { + // Extend zoomBounds with layer zoomBounds + zoomMax = Math.max(zoomMax, layer.zoomBounds.maxZoom); + zoomMin = Math.min(zoomMin, layer.zoomBounds.minZoom); + maxNativeZoom = Math.max( + maxNativeZoom, + layer.zoomBounds.maxNativeZoom + ); + minNativeZoom = Math.min( + minNativeZoom, + layer.zoomBounds.minNativeZoom + ); + zoomBounds.minZoom = zoomMin; + zoomBounds.maxZoom = zoomMax; + zoomBounds.minNativeZoom = minNativeZoom; + zoomBounds.maxNativeZoom = maxNativeZoom; + } + } + }); } }); if (bnds) { @@ -256,8 +273,6 @@ export var MapLayer = LayerGroup.extend({ mapml = this._content; parseLicenseAndLegend(); setLayerTitle(); - // crs is only set if the layer has the same projection as the map - if (M[layer.options.projection]) processTiles(); processFeatures(); // update controls if needed based on mapml-viewer controls/controlslist attribute if (layer._layerEl.parentElement) { @@ -305,35 +320,6 @@ export var MapLayer = LayerGroup.extend({ } }).addTo(layer); } - function processTiles() { - if (mapml.querySelector('map-tile')) { - var tiles = document.createElement('map-tiles'), - zoom = - mapml.querySelector('map-meta[name=zoom][content]') || - mapml.querySelector('map-input[type=zoom][value]'); - tiles.setAttribute( - 'zoom', - (zoom && zoom.getAttribute('content')) || - (zoom && zoom.getAttribute('value')) || - '0' - ); - var newTiles = mapml.querySelectorAll('map-tile'); - for (var nt = 0; nt < newTiles.length; nt++) { - tiles.appendChild(document.importNode(newTiles[nt], true)); - } - layer._mapmlTileContainer.appendChild(tiles); - layer._staticTileLayer = staticTileLayer({ - pane: layer._container, - _leafletLayer: layer, - projection: layer.options.projection, - className: 'mapml-static-tile-layer', - tileContainer: layer._mapmlTileContainer, - maxZoomBound: - M[layer.options.projection].options.resolutions.length - 1, - tileSize: M[layer.options.projection].options.crs.tile.bounds.max.x - }); - } - } function setLayerTitle() { if (mapml.querySelector('map-title')) { layer._title = mapml.querySelector('map-title').textContent.trim(); diff --git a/src/mapml/layers/MapTileLayer.js b/src/mapml/layers/MapTileLayer.js index 2f3b375cc..b2ff9221a 100644 --- a/src/mapml/layers/MapTileLayer.js +++ b/src/mapml/layers/MapTileLayer.js @@ -55,7 +55,7 @@ export var MapTileLayer = GridLayer.extend({ if (!this._mapTiles.includes(mapTile)) { this._mapTiles.push(mapTile); this._addToTileMap(mapTile); - // this._updateBounds(); + this._updateBounds(); // this.redraw(); } }, @@ -69,7 +69,7 @@ export var MapTileLayer = GridLayer.extend({ if (index !== -1) { this._mapTiles.splice(index, 1); this._removeFromTileMap(mapTile); - // this._updateBounds(); + this._updateBounds(); // this.redraw(); } }, @@ -292,7 +292,7 @@ export var MapTileLayer = GridLayer.extend({ if (pixelBounds) { layerBoundsByZoom[zoom] = Util.pixelToPCRSBounds( pixelBounds, - parseInt(zoom, 10), + parseInt(zoom), projection ); } @@ -302,7 +302,8 @@ export var MapTileLayer = GridLayer.extend({ let combinedBounds = null; for (const zoom in layerBoundsByZoom) { if (!combinedBounds) { - combinedBounds = layerBoundsByZoom[zoom].clone(); + // makes a clone of the bounds, including methods + combinedBounds = layerBoundsByZoom[zoom].pad(0); } else { combinedBounds.extend(layerBoundsByZoom[zoom].min); combinedBounds.extend(layerBoundsByZoom[zoom].max); diff --git a/test/e2e/layers/staticTileLayer.test.js b/test/e2e/layers/staticTileLayer.test.js index c31d1e74d..428105761 100644 --- a/test/e2e/layers/staticTileLayer.test.js +++ b/test/e2e/layers/staticTileLayer.test.js @@ -44,13 +44,24 @@ test.describe('Playwright StaticTile Layer Tests', () => { await context.close(); }); - test('Tiles load in on default map zoom level', async () => { - await page.waitForTimeout(1000); - const tiles = await page.$eval( - '.mapml-static-tile-layer > div', - (tileGroup) => tileGroup.getElementsByTagName('map-tile').length - ); - expect(tiles).toEqual(3); + test('Correct tiles render on initial map zoom level', async () => { + const renderedTiles = await page.locator('map-tile[zoom="2"]'); + await expect(renderedTiles).toHaveCount(3); + + for (const tile of await page.locator('map-tile[zoom="2"]').all()) { + const isRendered = await tile.evaluate((t) => t._tileDiv !== undefined); + // expect that tiles at z=2 are rendered + expect(isRendered).toBe(true); + } + + const nonRenderedTiles = await page.locator('map-tile[zoom="3"]'); + await expect(nonRenderedTiles).toHaveCount(1); + + for (const tile of await page.locator('map-tile[zoom="3"]').all()) { + const isRendered = await tile.evaluate((t) => t._tileDiv !== undefined); + // expect that tiles not at z=2 are not rendered + expect(isRendered).toBe(false); + } }); }); });