diff --git a/src/layer.js b/src/layer.js index 59c492d0a..cfb9629dc 100644 --- a/src/layer.js +++ b/src/layer.js @@ -108,7 +108,7 @@ export class MapLayer extends HTMLElement { this._createLayerControlHTML = M._createLayerControlHTML.bind(this); // this._opacity is used to record the current opacity value (with or without updates), // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 - this._opacity = +(this.getAttribute('opacity') || 1.0); + this._opacity = this.opacity || 1.0; const doConnected = this._onAdd.bind(this); this.parentElement .whenReady() @@ -345,25 +345,20 @@ export class MapLayer extends HTMLElement { '_mapmlvectors', '_templatedLayer' ]; - if (layer.validProjection) { - for (let j = 0; j < layerTypes.length; j++) { - let type = layerTypes[j]; - if (this.checked) { - if (type === '_templatedLayer' && mapExtents.length > 0) { - for (let i = 0; i < mapExtents.length; i++) { - totalExtentCount++; - if (mapExtents[i]._validateDisabled()) disabledExtentCount++; - } - } else if (layer[type]) { - // not a templated layer + for (let j = 0; j < layerTypes.length; j++) { + let type = layerTypes[j]; + if (this.checked) { + if (type === '_templatedLayer' && mapExtents.length > 0) { + for (let i = 0; i < mapExtents.length; i++) { totalExtentCount++; - if (!layer[type].isVisible) disabledExtentCount++; + if (mapExtents[i]._validateDisabled()) disabledExtentCount++; } + } else if (layer[type]) { + // not a templated layer + totalExtentCount++; + if (!layer[type].isVisible) disabledExtentCount++; } } - } else { - disabledExtentCount = 1; - totalExtentCount = 1; } // if all extents are not visible / disabled, set layer to disabled if ( diff --git a/src/map-extent.js b/src/map-extent.js index 1bde6a396..0aa6bdbf1 100644 --- a/src/map-extent.js +++ b/src/map-extent.js @@ -140,6 +140,10 @@ export class MapExtent extends HTMLElement { this.attachShadow({ mode: 'open' }); } await this.parentLayer.whenReady(); + this.mapEl = this.parentLayer.closest('mapml-viewer,map[is=web-map]'); + await this.mapEl.whenProjectionDefined(this.units).catch(() => { + throw new Error('Undefined projection:' + this.units); + }); // when projection is changed, the parent layer-._layer is created (so whenReady is fulfilled) but then removed, // then the map-extent disconnectedCallback will be triggered by layer-._onRemove() (clear the shadowRoot) // even before connectedCallback is finished @@ -168,9 +172,10 @@ export class MapExtent extends HTMLElement { ); this._changeHandler = this._handleChange.bind(this); this.parentLayer.addEventListener('map-change', this._changeHandler); + this.mapEl.addEventListener('map-projectionchange', this._changeHandler); // this._opacity is used to record the current opacity value (with or without updates), // the initial value of this._opacity should be set as opacity attribute value, if exists, or the default value 1.0 - this._opacity = +(this.getAttribute('opacity') || 1.0); + this._opacity = this.opacity || 1.0; this._templatedLayer = M.templatedLayer(this._templateVars, { pane: this._layer._container, opacity: this.opacity, @@ -522,6 +527,7 @@ export class MapExtent extends HTMLElement { this._layerControlHTML.remove(); this._map.removeLayer(this._templatedLayer); this.parentLayer.removeEventListener('map-change', this._changeHandler); + this.mapEl.removeEventListener('map-projectionchange', this._changeHandler); delete this._templatedLayer; delete this.parentLayer.bounds; } diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index cdb0e0100..6eb7a2d63 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -377,9 +377,13 @@ export class MapViewer extends HTMLElement { this.zoomTo(lat, lon, zoom); if (M.options.announceMovement) this._map.announceMovement.enable(); - this.querySelectorAll('layer-').forEach((layer) => { - layer.dispatchEvent(new CustomEvent('map-change')); - }); + // required to delay until map-extent.disabled is correctly set + // which happens as a result of layer-._validateDisabled() + // which happens so much we have to delay until they calls are + // completed + setTimeout(() => { + this.dispatchEvent(new CustomEvent('map-projectionchange')); + }, 0); }); } }; diff --git a/src/mapml/handlers/ContextMenu.js b/src/mapml/handlers/ContextMenu.js index a316d9e4c..62e76bba1 100644 --- a/src/mapml/handlers/ContextMenu.js +++ b/src/mapml/handlers/ContextMenu.js @@ -798,7 +798,6 @@ export var ContextMenu = L.Handler.extend({ .closest('fieldset') .parentNode.parentNode.parentNode.querySelector('span') : elem.querySelector('span'); - if (!elem.layer.validProjection) return; this._layerClicked = elem; this._layerMenu.removeAttribute('hidden'); this._showAtPoint(e.containerPoint, e, this._layerMenu); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 809b76f62..670f5ca54 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -43,17 +43,6 @@ export var MapMLLayer = L.Layer.extend({ // OR use the extent of the content provided this._initialize(local ? layerEl : mapml); - - // a default extent can't be correctly set without the map to provide - // its bounds , projection, zoom range etc, so if that stuff's not - // established by metadata in the content, we should use map properties - // to set the extent, but the map won't be available until the - // element is attached to the element, wait for that to happen. - // weirdness. options is actually undefined here, despite the hardcoded - // options above. If you use this.options, you see the options defined - // above. Not going to change this, but failing to understand ATM. - // may revisit some time. - this.validProjection = true; }, setZIndex: function (zIndex) { this.options.zIndex = zIndex; @@ -98,11 +87,6 @@ export var MapMLLayer = L.Layer.extend({ }, onAdd: function (map) { - // probably don't need it except for layer context menu usage - if (this._properties && !this._validProjection(map)) { - this.validProjection = false; - return; - } this._map = map; if (this._mapmlvectors) map.addLayer(this._mapmlvectors); @@ -218,26 +202,6 @@ export var MapMLLayer = L.Layer.extend({ } }, - _validProjection: function (map) { - const mapExtents = this._layerEl.querySelectorAll('map-extent'); - let noLayer = false; - if (this._properties && mapExtents.length > 0) { - for (let i = 0; i < mapExtents.length; i++) { - if (mapExtents[i]._templateVars) { - for (let template of mapExtents[i]._templateVars) - if ( - !template.projectionMatch && - template.projection !== map.options.projection - ) { - noLayer = true; // if there's a single template where projections don't match, set noLayer to true - break; - } - } - } - } - return !(noLayer || this.getProjection() !== map.options.projection); - }, - addTo: function (map) { map.addLayer(this); return this; diff --git a/src/web-map.js b/src/web-map.js index 8ad2c1187..3c2bb4553 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -422,9 +422,13 @@ export class WebMap extends HTMLMapElement { this.zoomTo(lat, lon, zoom); if (M.options.announceMovement) this._map.announceMovement.enable(); - this.querySelectorAll('layer-').forEach((layer) => { - layer.dispatchEvent(new CustomEvent('map-change')); - }); + // required to delay until map-extent.disabled is correctly set + // which happens as a result of layer-._validateDisabled() + // which happens so much we have to delay until they calls are + // completed + setTimeout(() => { + this.dispatchEvent(new CustomEvent('map-projectionchange')); + }, 0); }); } }; diff --git a/test/e2e/api/events/map-projectionchange.html b/test/e2e/api/events/map-projectionchange.html new file mode 100644 index 000000000..3aa7365a9 --- /dev/null +++ b/test/e2e/api/events/map-projectionchange.html @@ -0,0 +1,38 @@ + + + + + map-projectionchange event + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/e2e/api/events/map-projectionchange.test.js b/test/e2e/api/events/map-projectionchange.test.js new file mode 100644 index 000000000..cfb7e473f --- /dev/null +++ b/test/e2e/api/events/map-projectionchange.test.js @@ -0,0 +1,84 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('map-projectionchange test ', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + }); + + test.beforeEach(async function () { + await page.goto('events/map-projectionchange.html'); + await page.waitForTimeout(1000); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('Multiple map-extents in different projections adapt to map-projectionchange', async () => { + const viewer = await page.locator('mapml-viewer'); + expect(await viewer.evaluate((v) => v.projection)).toEqual('OSMTILE'); + expect( + await viewer.evaluate((v) => { + return v.querySelector('map-extent[units=OSMTILE]').disabled; + }) + ).toBe(false); + expect( + await viewer.evaluate((v) => { + return v.querySelector('map-extent[units=CBMTILE]').disabled; + }) + ).toBe(true); + await viewer.evaluate(() => changeProjection()); + await page.waitForTimeout(500); + expect(await viewer.evaluate((v) => v.projection)).toEqual('CBMTILE'); + expect( + await viewer.evaluate((v) => { + return v.querySelector('map-extent[units=OSMTILE]').disabled; + }) + ).toBe(true); + expect( + await viewer.evaluate((v) => { + return v.querySelector('map-extent[units=CBMTILE]').disabled; + }) + ).toBe(false); + }); + test.skip('History is empty after map-projectionchange', async () => { + // history api needs a complete review; test can't pass without many + // odd hacks, so skip for now. + const viewer = await page.locator('mapml-viewer'); + expect(await viewer.evaluate((v) => v.projection)).toEqual('OSMTILE'); + await viewer.evaluate(() => changeProjection()); + await page.waitForTimeout(500); + expect(await viewer.evaluate((v) => v.projection)).toEqual('CBMTILE'); + const reload = await page.getByLabel('Reload'); + expect(await reload.evaluate((button) => button.ariaDisabled)).toBe('true'); + }); + test('Opacity is maintained on layer- and map-extent after map-projectionchange', async () => { + const viewer = await page.locator('mapml-viewer'); + const layer = await page.locator('layer-'); + await page.pause(); + await layer.evaluate((layer) => (layer.opacity = 0.5)); + expect( + await layer.evaluate((layer) => { + return layer.opacity; + }) + ).toBe(0.5); + const osmtileExtent = await page.locator('map-extent[units=OSMTILE]'); + await osmtileExtent.evaluate((e) => (e.opacity = 0.4)); + const cbmtileExtent = await page.locator('map-extent[units=CBMTILE]'); + await cbmtileExtent.evaluate((e) => (e.opacity = 0.3)); + await viewer.evaluate(() => changeProjection()); + await page.waitForTimeout(1000); + expect(await osmtileExtent.evaluate((e) => e.opacity)).toBe(0.4); + expect(await cbmtileExtent.evaluate((e) => e.opacity)).toBe(0.3); + expect( + await layer.evaluate((layer) => { + return layer.opacity; + }) + ).toBe(0.5); + }); +}); diff --git a/test/e2e/elements/map-extent/map-extent.html b/test/e2e/elements/map-extent/map-extent.html index 42597ff3e..3fab1d6bb 100644 --- a/test/e2e/elements/map-extent/map-extent.html +++ b/test/e2e/elements/map-extent/map-extent.html @@ -33,6 +33,12 @@ + + + + + + diff --git a/test/e2e/elements/map-extent/map-extent.test.js b/test/e2e/elements/map-extent/map-extent.test.js index ecfab22d3..d187b85f5 100644 --- a/test/e2e/elements/map-extent/map-extent.test.js +++ b/test/e2e/elements/map-extent/map-extent.test.js @@ -3,106 +3,108 @@ import { test, expect, chromium } from '@playwright/test'; test.describe('map-extent tests', () => { let page; let context; - test.describe('attribute tests', () => { - test.beforeEach(async function () { - context = await chromium.launchPersistentContext('', { slowMo: 500 }); - page = - context.pages().find((page) => page.url() === 'about:blank') || - (await context.newPage()); - await page.goto('map-extent.html'); - }); - test('Basic hidden functionality and API', async () => { - const extent = await page.getByTestId('ext1'); - let hiddenInLayerControl = await extent.evaluate((extent) => { - return !extent._layerControlHTML.isConnected; - }); - expect(hiddenInLayerControl).toBe(true); + test.beforeAll(async function () { + context = await chromium.launchPersistentContext('', { slowMo: 500 }); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('map-extent.html'); + }); + test('Basic hidden functionality and API', async () => { + const extent = await page.getByTestId('ext1'); + let hiddenInLayerControl = await extent.evaluate((extent) => { + return !extent._layerControlHTML.isConnected; + }); + expect(hiddenInLayerControl).toBe(true); - await extent.evaluate((extent) => { - extent.hidden = false; - }); - hiddenInLayerControl = await extent.evaluate((extent) => { - return !extent._layerControlHTML.isConnected; - }); - expect(hiddenInLayerControl).toBe(false); + await extent.evaluate((extent) => { + extent.hidden = false; + }); + hiddenInLayerControl = await extent.evaluate((extent) => { + return !extent._layerControlHTML.isConnected; + }); + expect(hiddenInLayerControl).toBe(false); - let labelProperty = await extent.evaluate((extent) => { - return extent.label; - }); - expect(labelProperty === 'User-generated label').toBe(true); + let labelProperty = await extent.evaluate((extent) => { + return extent.label; + }); + expect(labelProperty === 'User-generated label').toBe(true); - let labelInLayerControl = await extent.evaluate((extent) => { - return extent._layerControlLabel.innerText; - }); - expect(labelInLayerControl === labelProperty).toBe(true); + let labelInLayerControl = await extent.evaluate((extent) => { + return extent._layerControlLabel.innerText; + }); + expect(labelInLayerControl === labelProperty).toBe(true); - await extent.evaluate((extent) => { - extent.removeAttribute('label'); - }); + await extent.evaluate((extent) => { + extent.removeAttribute('label'); + }); - await page.waitForTimeout(500); - const labelChangesToDefaultAndLayerNotHidden = await extent.evaluate( - (extent) => { - return ( - extent.label === 'Sub-layer' && - !extent.hidden && - extent._layerControlLabel.innerText === extent.label - ); - } - ); - expect(labelChangesToDefaultAndLayerNotHidden).toBe(true); + await page.waitForTimeout(500); + const labelChangesToDefaultAndLayerNotHidden = await extent.evaluate( + (extent) => { + return ( + extent.label === 'Sub-layer' && + !extent.hidden && + extent._layerControlLabel.innerText === extent.label + ); + } + ); + expect(labelChangesToDefaultAndLayerNotHidden).toBe(true); + await extent.evaluate((extent) => { + // restore original state + extent.hidden = true; }); + }); - test('hidden DOM order maintained when unhiding', async () => { - const t = await page.getByTestId('template'); - await t.evaluate((t) => { - let extents = t.content.cloneNode(true); - let l = document.querySelector('#cbmt1'); - l.appendChild(extents); - }); - await page.waitForTimeout(500); - const layer = await page.getByTestId('cbmt1'); - let unhiddenMapExtentCount = await layer.evaluate((layer) => { - return layer._propertiesGroupAnatomy.childElementCount; - }); - // all hidden extents - expect(unhiddenMapExtentCount).toEqual(0); - await layer.evaluate((layer) => { - return layer.whenElemsReady(); - }); - await layer.evaluate((layer) => { - layer.querySelector('[data-testid="ext3"]').hidden = false; - }); - await layer.evaluate((layer) => { - layer.querySelector('[data-testid="ext1"]').hidden = false; - }); - await layer.evaluate((layer) => { - layer.querySelector('[data-testid="ext2"]').hidden = false; - }); - unhiddenMapExtentCount = await layer.evaluate((layer) => { - return layer._propertiesGroupAnatomy.childElementCount; - }); - // no hidden extents - expect(unhiddenMapExtentCount).toBe(3); + test('hidden DOM order maintained when unhiding', async () => { + const t = await page.getByTestId('template'); + await t.evaluate((t) => { + let extents = t.content.cloneNode(true); + let l = document.querySelector('#cbmt1'); + l.appendChild(extents); + }); + await page.waitForTimeout(500); + const layer = await page.getByTestId('cbmt1'); + let unhiddenMapExtentCount = await layer.evaluate((layer) => { + return layer._propertiesGroupAnatomy.childElementCount; + }); + // all hidden extents + expect(unhiddenMapExtentCount).toEqual(0); + await layer.evaluate((layer) => { + return layer.whenElemsReady(); + }); + await layer.evaluate((layer) => { + layer.querySelector('[data-testid="ext3"]').hidden = false; + }); + await layer.evaluate((layer) => { + layer.querySelector('[data-testid="ext1"]').hidden = false; + }); + await layer.evaluate((layer) => { + layer.querySelector('[data-testid="ext2"]').hidden = false; + }); + unhiddenMapExtentCount = await layer.evaluate((layer) => { + return layer._propertiesGroupAnatomy.childElementCount; + }); + // no hidden extents + expect(unhiddenMapExtentCount).toBe(3); - const orderOfDOMExtentsEqualsLayerControlOrder = await layer.evaluate( - (layer) => { - let extents = layer.querySelectorAll('map-extent'); - let match = true; - for (let i = 0; i < extents.length; i++) { - if ( - extents[i]._layerControlHTML !== - layer._propertiesGroupAnatomy.children[i] - ) { - match = false; - break; - } + const orderOfDOMExtentsEqualsLayerControlOrder = await layer.evaluate( + (layer) => { + let extents = layer.querySelectorAll('map-extent'); + let match = true; + for (let i = 0; i < extents.length; i++) { + if ( + extents[i]._layerControlHTML !== + layer._propertiesGroupAnatomy.children[i] + ) { + match = false; + break; } - return match; } - ); - expect(orderOfDOMExtentsEqualsLayerControlOrder).toBe(true); - }); + return match; + } + ); + expect(orderOfDOMExtentsEqualsLayerControlOrder).toBe(true); }); test('Basic checked functionality and API', async () => { // extent ext2-1 starts life checked and hidden @@ -154,4 +156,27 @@ test.describe('map-extent tests', () => { expect(visibleOnMap).toBe(false); expect(checkedProperty).toBe(false); }); + test('Ensure that undefined projection throws exception', async () => { + let errorLogs = []; + page.on('pageerror', (err) => { + errorLogs.push(err.message); + }); + const viewer = page.getByTestId('firstmap'); + await viewer.evaluate((viewer) => { + const l = document.createElement('layer-'); + l.label = 'Layer'; + const e = document + .querySelector('template') + .content.querySelector('[data-testid=ext4]') + .cloneNode(true); + l.checked = true; + l.appendChild(e); + viewer.appendChild(l); + }); + // map-extent.connectedCallback does an await map.whenProjectionDefined('foo') + // which has a timeout of 5 seconds + await page.waitForTimeout(5500); + expect(errorLogs.length).toBe(1); + expect(errorLogs[0]).toBe('Undefined projection:foo'); + }); }); diff --git a/test/e2e/layers/multipleExtents.test.js b/test/e2e/layers/multipleExtents.test.js index 0a3574896..c57fa3f84 100644 --- a/test/e2e/layers/multipleExtents.test.js +++ b/test/e2e/layers/multipleExtents.test.js @@ -352,7 +352,7 @@ test.describe('Multiple Extents Bounds Tests', () => { const alabamaExtentItem = page.getByText('alabama_feature'); await expect(alabamaExtentItem).toHaveCount(1); await expect(alabamaExtentItem).toHaveCSS('font-style', 'normal'); - + const alabamaMapExtent = page.locator('map-extent[label=alabama_feature]'); await expect(alabamaMapExtent).toHaveCount(1); await expect(alabamaMapExtent).not.toHaveAttribute('disabled'); diff --git a/test/e2e/mapml-viewer/customTCRS.test.js b/test/e2e/mapml-viewer/customTCRS.test.js index 32ba8a9d6..d3b4f7a07 100644 --- a/test/e2e/mapml-viewer/customTCRS.test.js +++ b/test/e2e/mapml-viewer/customTCRS.test.js @@ -17,7 +17,7 @@ test.describe('Playwright Custom TCRS Tests', () => { }); test('Simple Custom TCRS, tiles load, mismatched layer disabled', async () => { - await page.waitForTimeout(100); + await page.waitForTimeout(500); const misMatchedLayerDisabled = await page.$eval( 'body > mapml-viewer:nth-child(1)', (map) => map.querySelectorAll('layer-')[0].hasAttribute('disabled')