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 () => {