diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index f7fbbb155..d5e8fc46d 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -100,7 +100,6 @@ export class MapViewer extends HTMLElement { constructor() { // Always call super first in constructor super(); - this._source = this.outerHTML; let tmpl = document.createElement('template'); tmpl.innerHTML = @@ -110,7 +109,10 @@ export class MapViewer extends HTMLElement { let shadowRoot = this.attachShadow({mode: 'open'}); this._container = document.createElement('div'); - + + let output = ""; + this._container.insertAdjacentHTML("beforeend", output); + // Set default styles for the map element. let mapDefaultCSS = document.createElement('style'); mapDefaultCSS.innerHTML = @@ -198,6 +200,8 @@ export class MapViewer extends HTMLElement { projection: this.projection, query: true, contextMenu: true, + //Will replace with M.options.announceMoves + announceMovement: true, mapEl: this, crs: M[this.projection], zoom: this.zoom, diff --git a/src/mapml.css b/src/mapml.css index 30efc2271..2ebd8dace 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -73,7 +73,7 @@ .mapml-layer-item-name a { color: revert; } - + .leaflet-top .leaflet-control { margin-top: 5px; } diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js new file mode 100644 index 000000000..c4a4ca4e4 --- /dev/null +++ b/src/mapml/handlers/AnnounceMovement.js @@ -0,0 +1,124 @@ +export var AnnounceMovement = L.Handler.extend({ + addHooks: function () { + this._map.on({ + layeradd: this.totalBounds, + layerremove: this.totalBounds, + }); + + this._map.options.mapEl.addEventListener('moveend', this.announceBounds); + this._map.options.mapEl.addEventListener('focus', this.focusAnnouncement); + this._map.dragging._draggable.addEventListener('dragstart', this.dragged); + }, + removeHooks: function () { + this._map.off({ + layeradd: this.totalBounds, + layerremove: this.totalBounds, + }); + + this._map.options.mapEl.removeEventListener('moveend', this.announceBounds); + this._map.options.mapEl.removeEventListener('focus', this.focusAnnouncement); + this._map.dragging._draggable.removeEventListener('dragstart', this.dragged); + }, + + focusAnnouncement: function () { + let el = this.querySelector(".mapml-web-map") ? this.querySelector(".mapml-web-map").shadowRoot.querySelector(".leaflet-container") : + this.shadowRoot.querySelector(".leaflet-container"); + + let mapZoom = this._map.getZoom(); + let location = M.gcrsToTileMatrix(this); + let standard = " zoom level " + mapZoom + " column " + location[0] + " row " + location[1]; + + if(mapZoom === this._map._layersMaxZoom){ + standard = "At maximum zoom level, zoom in disabled " + standard; + } + else if(mapZoom === this._map._layersMinZoom){ + standard = "At minimum zoom level, zoom out disabled " + standard; + } + + el.setAttribute("aria-roledescription", "region " + standard); + }, + + announceBounds: function () { + if(this._traversalCall > 0){ + return; + } + let mapZoom = this._map.getZoom(); + let mapBounds = M.pixelToPCRSBounds(this._map.getPixelBounds(),mapZoom,this._map.options.projection); + + let visible = true; + if(this._map.totalLayerBounds){ + visible = mapZoom <= this._map._layersMaxZoom && mapZoom >= this._map._layersMinZoom && + this._map.totalLayerBounds.overlaps(mapBounds); + } + + let output = this.querySelector(".mapml-web-map") ? this.querySelector(".mapml-web-map").shadowRoot.querySelector(".mapml-screen-reader-output") : + this.shadowRoot.querySelector(".mapml-screen-reader-output"); + + //GCRS to TileMatrix + let location = M.gcrsToTileMatrix(this); + let standard = "zoom level " + mapZoom + " column " + location[0] + " row " + location[1]; + + if(!visible){ + let outOfBoundsPos = this._history[this._historyIndex]; + let inBoundsPos = this._history[this._historyIndex - 1]; + this.back(); + this._history.pop(); + + if(outOfBoundsPos.zoom !== inBoundsPos.zoom){ + output.innerText = "Zoomed out of bounds, returning to"; + } + else if(this._map.dragging._draggable.wasDragged){ + output.innerText = "Dragged out of bounds, returning to "; + } + else if(outOfBoundsPos.x > inBoundsPos.x){ + output.innerText = "Reached east bound, panning east disabled"; + } + else if(outOfBoundsPos.x < inBoundsPos.x){ + output.innerText = "Reached west bound, panning west disabled"; + } + else if(outOfBoundsPos.y < inBoundsPos.y){ + output.innerText = "Reached north bound, panning north disabled"; + } + else if(outOfBoundsPos.y > inBoundsPos.y){ + output.innerText = "Reached south bound, panning south disabled"; + } + + } + else{ + let prevZoom = this._history[this._historyIndex - 1].zoom; + if(mapZoom === this._map._layersMaxZoom && mapZoom !== prevZoom){ + output.innerText = "At maximum zoom level, zoom in disabled " + standard; + } + else if(mapZoom === this._map._layersMinZoom && mapZoom !== prevZoom){ + output.innerText = "At minimum zoom level, zoom out disabled " + standard; + } + else { + output.innerText = standard; + } + } + this._map.dragging._draggable.wasDragged = false; + }, + + totalBounds: function () { + let layers = Object.keys(this._layers); + let bounds = L.bounds(); + + layers.forEach(i => { + if(this._layers[i].layerBounds){ + if(!bounds){ + let point = this._layers[i].layerBounds.getCenter(); + bounds = L.bounds(point, point); + } + bounds.extend(this._layers[i].layerBounds.min); + bounds.extend(this._layers[i].layerBounds.max); + } + }); + + this.totalLayerBounds = bounds; + }, + + dragged: function () { + this.wasDragged = true; + } + +}); \ No newline at end of file diff --git a/src/mapml/index.js b/src/mapml/index.js index b7eb63ea5..53c268d60 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -56,6 +56,7 @@ import { Crosshair, crosshair } from "./layers/Crosshair"; import { Feature, feature } from "./features/feature"; import { FeatureRenderer, featureRenderer } from './features/featureRenderer'; import { FeatureGroup, featureGroup} from './features/featureGroup'; +import {AnnounceMovement} from "./handlers/AnnounceMovement"; import { Options } from "./options"; /* global L, Node */ @@ -599,13 +600,16 @@ M.coordsToArray = Util.coordsToArray; M.parseStylesheetAsHTML = Util.parseStylesheetAsHTML; M.pointToPCRSPoint = Util.pointToPCRSPoint; M.pixelToPCRSPoint = Util.pixelToPCRSPoint; +M.gcrsToTileMatrix = Util.gcrsToTileMatrix; M.QueryHandler = QueryHandler; M.ContextMenu = ContextMenu; +M.AnnounceMovement = AnnounceMovement; // see https://leafletjs.com/examples/extending/extending-3-controls.html#handlers L.Map.addInitHook('addHandler', 'query', M.QueryHandler); L.Map.addInitHook('addHandler', 'contextMenu', M.ContextMenu); +L.Map.addInitHook('addHandler', 'announceMovement', M.AnnounceMovement); M.MapMLLayer = MapMLLayer; M.mapMLLayer = mapMLLayer; diff --git a/src/mapml/layers/DebugLayer.js b/src/mapml/layers/DebugLayer.js index 66d02e205..0b2a0bf07 100644 --- a/src/mapml/layers/DebugLayer.js +++ b/src/mapml/layers/DebugLayer.js @@ -178,6 +178,7 @@ export var DebugVectors = L.LayerGroup.extend({ j = 0; this.addLayer(this._centerVector); + for (let i of id) { if (layers[i].layerBounds) { let boundsArray = [ @@ -199,6 +200,20 @@ export var DebugVectors = L.LayerGroup.extend({ j++; } } + + if(map.totalLayerBounds){ + let totalBoundsArray = [ + map.totalLayerBounds.min, + L.point(map.totalLayerBounds.max.x, map.totalLayerBounds.min.y), + map.totalLayerBounds.max, + L.point(map.totalLayerBounds.min.x, map.totalLayerBounds.max.y) + ]; + + let totalBounds = projectedExtent( + totalBoundsArray, + {color: "#808080", weight: 5, opacity: 0.5, fill: false}); + this.addLayer(totalBounds); + } }, _mapLayerUpdate: function (e) { diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 6431ca192..f30518ebd 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -57,7 +57,7 @@ export var TemplatedTileLayer = L.TileLayer.extend({ let mapBounds = M.pixelToPCRSBounds(this._map.getPixelBounds(),mapZoom,this._map.options.projection); this.isVisible = mapZoom <= this.options.maxZoom && mapZoom >= this.options.minZoom && this.layerBounds.overlaps(mapBounds); - if(!(this.isVisible))return; + if(!(this.isVisible))return; this._parentOnMoveEnd(); }, createTile: function (coords) { diff --git a/src/mapml/utils/Util.js b/src/mapml/utils/Util.js index 3b7a74851..5e969a27a 100644 --- a/src/mapml/utils/Util.js +++ b/src/mapml/utils/Util.js @@ -385,4 +385,12 @@ export var Util = { map.getContainer().focus(); } }, + + gcrsToTileMatrix: function (mapEl) { + let point = mapEl._map.project(mapEl._map.getCenter()); + let tileSize = mapEl._map.options.crs.options.crs.tile.bounds.max.y; + let column = Math.trunc(point.x / tileSize); + let row = Math.trunc(point.y / tileSize); + return [column, row]; + } }; \ No newline at end of file diff --git a/src/web-map.js b/src/web-map.js index 0fb1624f7..9331a9ce8 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -117,7 +117,10 @@ export class WebMap extends HTMLMapElement { let shadowRoot = rootDiv.attachShadow({mode: 'open'}); this._container = document.createElement('div'); - + + let output = ""; + this._container.insertAdjacentHTML("beforeend", output); + // Set default styles for the map element. let mapDefaultCSS = document.createElement('style'); mapDefaultCSS.innerHTML = @@ -211,6 +214,8 @@ export class WebMap extends HTMLMapElement { projection: this.projection, query: true, contextMenu: true, + //Will replace with M.options.announceMoves + announceMovement: true, mapEl: this, crs: M[this.projection], zoom: this.zoom, diff --git a/test/e2e/core/announceMovement.test.js b/test/e2e/core/announceMovement.test.js new file mode 100644 index 000000000..77dd61985 --- /dev/null +++ b/test/e2e/core/announceMovement.test.js @@ -0,0 +1,119 @@ +const playwright = require("playwright"); +jest.setTimeout(50000); +(async () => { + for (const browserType of BROWSER) { + describe( + "Announce movement test " + browserType, + ()=> { + beforeAll(async () => { + browser = await playwright[browserType].launch({ + headless: ISHEADLESS, + slowMo: 100, + }); + context = await browser.newContext(); + page = await context.newPage(); + if (browserType === "firefox") { + await page.waitForNavigation(); + } + await page.goto(PATH + "mapml-viewer.html"); + }); + afterAll(async function () { + await browser.close(); + }); + + test("[" + browserType + "]" + " Output values are correct during regular movement", async ()=>{ + const announceMovement = await page.$eval( + "body > mapml-viewer", + (map) => map._map.announceMovement._enabled + ); + if(!announceMovement){ + return; + } + await page.keyboard.press("Tab"); + await page.keyboard.press("ArrowUp"); + await page.waitForTimeout(100); + + const movedUp = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(movedUp).toEqual("zoom level 0 column 3 row 3"); + + for(let i = 0; i < 2; i++){ + await page.keyboard.press("ArrowLeft"); + await page.waitForTimeout(100); + } + + const movedLeft = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(movedLeft).toEqual("zoom level 0 column 2 row 3"); + + await page.keyboard.press("Equal"); + await page.waitForTimeout(100); + + const zoomedIn = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(zoomedIn).toEqual("zoom level 1 column 4 row 6"); + }); + + test("[" + browserType + "]" + " Output values are correct at bounds and bounces back", async ()=>{ + const announceMovement = await page.$eval( + "body > mapml-viewer", + (map) => map._map.announceMovement._enabled + ); + if(!announceMovement){ + return; + } + //Zoom out to min layer bound + await page.keyboard.press("Minus"); + await page.waitForTimeout(100); + + const minZoom = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(minZoom).toEqual("At minimum zoom level, zoom out disabled zoom level 0 column 2 row 3"); + + //Pan out of west bounds, expect the map to bounce back + for(let i = 0; i < 4; i++){ + await page.waitForTimeout(100); + await page.keyboard.press("ArrowLeft"); + } + + const westBound = await page.waitForFunction(() => + document.querySelector("body > mapml-viewer").shadowRoot.querySelector("div > output").innerHTML === "Reached west bound, panning west disabled", + {}, {timeout: 1000} + ); + expect(await westBound.jsonValue()).toEqual(true); + + const bouncedBack = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(bouncedBack).toEqual("zoom level 0 column 1 row 3"); + + //Zoom in out of bounds, expect the map to zoom back + await page.keyboard.press("Equal"); + + const zoomedOutOfBounds = await page.waitForFunction(() => + document.querySelector("body > mapml-viewer").shadowRoot.querySelector("div > output").innerHTML === "Zoomed out of bounds, returning to", + {}, {timeout: 1000} + ); + expect(await zoomedOutOfBounds.jsonValue()).toEqual(true); + + const zoomedBack = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(zoomedBack).toEqual("zoom level 0 column 1 row 3"); + + }); + + } + ); + } +})(); \ No newline at end of file diff --git a/test/e2e/core/debugMode.test.js b/test/e2e/core/debugMode.test.js index 952d71abb..a38caa117 100644 --- a/test/e2e/core/debugMode.test.js +++ b/test/e2e/core/debugMode.test.js @@ -169,7 +169,7 @@ jest.setTimeout(50000); "xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g", (tile) => tile.childElementCount ); - expect(feature).toEqual(3); + expect(feature).toEqual(4); }); test("[" + browserType + "]" + " Layer deselected then reselected", async () => {