Last active
July 1, 2020 23:58
-
-
Save andygup/a470f8fb813e336c6ca1c51a86563001 to your computer and use it in GitHub Desktop.
Automated MapView test using a basemap and a modal map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" /> | |
<link rel="stylesheet" href="https://jsdev.arcgis.com/next/esri/themes/light/main.css" /> | |
<style> | |
html, | |
body, | |
#viewDiv, | |
.map-view { | |
padding: 0; | |
margin: 0; | |
height: 100%; | |
width: 100%; | |
} | |
button { | |
/* position: absolute; | |
top: 20px; | |
right: 20px; */ | |
height: 40px; | |
color: white; | |
background-color: green; | |
} | |
#modal { | |
display: none; | |
height: 80%; | |
width: 80%; | |
position: absolute; | |
top: 40px; | |
bottom: 0; | |
right: 0; | |
left: 0; | |
overflow: hidden; | |
background-color: white; | |
border: 2px solid black; | |
outline: #4CAF50 solid 10px; | |
margin: auto; | |
} | |
#modal.show { | |
display: block; | |
animation: appear 0.3s; | |
} | |
@keyframes appear { | |
from { | |
opacity: 0; | |
transform: translateY(100px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
#modal button { | |
z-index: 1; | |
} | |
</style> | |
<script src="https://js.arcgis.com/next"></script> | |
<script> | |
// The upper-part of this file contains declarations. The entry-point code begins further below with the `require` function. | |
// MapViewService is responsible for creating, storing, and re-using instances of MapViews with their associated MapView containers/DOM nodes and WebMaps. | |
// This service exists because we learned from the ArcGIS JS API dev team that MapViews should not be destroyed because there are unrecoverable WebGL contexts/resources that cannot be reclaimed by the browser and can lead to app crashes. Here we are also re-using MapView containers/DOM nodes and WebMaps as an attempt at further memory optimization. | |
// This service will create and maintain a "pool" of MapViews; if a MapView is requested and none are in the pool available for use, it will create a MapView. As MapViews are no longer needed they are released and marked as being available for re-use at a subsequent time. | |
class MapViewService { | |
constructor(EsriMapView, EsriWebMap, EsriPoint) { | |
this.EsriMapView = EsriMapView; | |
this.EsriWebMap = EsriWebMap; | |
this.EsriPoint = EsriPoint; | |
this.mapViewDataList = []; | |
} | |
// Retrieves a MapView. If a MapView is not available, one will be created (along with its associated container/DOM node and WebMap). If an existing MapView is available, it will be retrieved and re-used (along with its associated container/DOM node and WebMap). | |
async getMapView(parentElement, mapViewProperties) { | |
// Find an available, existing MapView from the pool. | |
let mapViewData = this.mapViewDataList.find( | |
mapViewDataItem => mapViewDataItem.available | |
); | |
if (!mapViewData) { | |
// Create the container DOM node. | |
const mapViewEl = document.createElement("div"); | |
/************************** | |
Andy: Create a unique identifier in order to create an association between the MapView and its container/DOM nodes that will be used later to find and re-use a container/DOM nodes for a given MapView. | |
***************************/ | |
const mapViewId = (this.mapViewDataList.length + 1).toString(); | |
mapViewEl.setAttribute("map-view-id", mapViewId); | |
mapViewEl.classList.add("map-view"); | |
// Place the container element where it should appear in the DOM (relative to the parentElement that was passed in). | |
parentElement.appendChild(mapViewEl); | |
mapViewProperties.container = mapViewEl; | |
/************************** | |
Andy: added MapView.mapViewId as a property so that releaseMapView() now works correctly. | |
Before, it was endlessly creating new MapViews with each cycle causing a memory leak. | |
***************************/ | |
mapViewProperties.mapViewId = mapViewId; | |
const mapView = new this.EsriMapView(mapViewProperties); | |
// Store the MapView object instance, identifier for the MapView's container/DOM nodes, and that the MapView is not available for us (it will be used immediately by the client code that called this method to get a MapView). | |
mapViewData = { mapView, mapViewId, available: false }; | |
this.mapViewDataList.push(mapViewData); | |
console.log("mapViewDataList ", mapViewId); | |
} else { | |
// With an available MapView for re-use, mark that it will no longer be available because it's about to be used. | |
mapViewData.available = false; | |
// In the location of the DOM where the MapView containers/DOM nodes that were previously used and then released are stored, find the container element associated with the MapView that's being re-used via the unique identifier. | |
const mapsEl = document.querySelector("body > .maps"); | |
const mapViewEl = mapsEl.querySelector( | |
`[map-view-id="${mapViewData.mapViewId}"]` | |
); | |
// Move the container/DOM node tree from the storage location in the DOM to where it should now appear in the DOM. | |
parentElement.appendChild(mapViewEl); | |
mapViewProperties.container = mapViewEl; | |
// Re-configure the MapView with new parameter values. | |
mapViewData.mapView.center = new this.EsriPoint({ | |
x: mapViewProperties.center[0], | |
y: mapViewProperties.center[1] | |
}); | |
mapViewData.mapView.zoom = mapViewProperties.zoom; | |
} | |
const m = this.setWebMap(mapViewData.mapView); | |
/************************** | |
Andy: I fixed/removed/streamlined a number of embedded promises, including removing async assignments | |
that were no longer needed. | |
***************************/ | |
await m.when(); | |
return m; | |
// return mapViewData.mapView; | |
} | |
setWebMap(mapView) { | |
const portalItem = { | |
id: "55ebf90799fa4a3fa57562700a68c405" | |
}; | |
// If the MapView already has a WebMap, re-configure it. Otherwise create a WebMap. | |
if (!mapView.map) { | |
mapView.map = new this.EsriWebMap({ | |
portalItem | |
}); | |
return mapView; | |
} | |
mapView.map.set({ | |
portalItem | |
}); | |
return mapView; | |
} | |
// When a MapView is no longer needed, do what's needed to make it available for later re-use. | |
releaseMapView(mapView) { | |
/************************** | |
Andy: check if a MapView is in the pool by comparing id's | |
This replaces the original (===) equality check that was silently failing when comparing two | |
non-primitive, complex Objects. | |
***************************/ | |
const mapViewData = this.mapViewDataList.find( | |
mapViewDataItem => mapViewDataItem.mapView.mapViewId === mapView.mapViewId | |
); | |
if (mapViewData) { | |
// Get a handle to, and create if it doesn't exist, the place in the DOM where we'll store the MapView containers/DOM nodes. | |
const bodyEl = document.querySelector("body"); | |
let mapsEl = bodyEl.querySelector(".maps"); | |
if (!mapsEl) { | |
mapsEl = document.createElement("div"); | |
mapsEl.classList.add("maps"); | |
bodyEl.appendChild(mapsEl); | |
} | |
// Move the container/DOM node tree from the location where it was used to the storage location for later re-use. | |
mapsEl.appendChild(mapViewData.mapView.container); | |
// Mark the MapView as being available for re-use. | |
mapViewData.available = true; | |
} | |
} | |
} | |
// MapService is responsible for loading and tearing down a map. It is used for both the main map and the modal map. | |
class MapService { | |
constructor(mapViewService) { | |
this.mapViewService = mapViewService; | |
} | |
loadMap(parentElement) { | |
// Get a MapView. The MapView returned could be a re-used MapView or a new one if no MapView is currently available for use. | |
this.mapView = this.mapViewService.getMapView(parentElement, { | |
center: [-91.5, 42.5], | |
zoom: 9 | |
}); | |
return this.mapView; | |
} | |
// Clean up map-related resources and release the MapView for future re-use. | |
/************************** | |
Andy: this method now allows you to specify a mapView in the constructor instead of depending on this.mapView. | |
Also MapView is no longer wrapped in a Promise. | |
***************************/ | |
teardown(mapView) { | |
// this.mapView.map.removeAll(); | |
this.mapViewService.releaseMapView(mapView); | |
} | |
} | |
require([ | |
"esri/views/MapView", | |
"esri/WebMap", | |
"esri/geometry/Point", | |
"esri/core/watchUtils" | |
], ( | |
EsriMapView, | |
EsriWebMap, | |
EsriPoint, | |
watchUtils | |
) => { | |
const mapViewService = new MapViewService( | |
EsriMapView, | |
EsriWebMap, | |
EsriPoint | |
); | |
const storageKey = "WEBGLTEST"; | |
let timeoutID; | |
let counter = 0; | |
let kill = false; | |
const startTime = performance.now(); | |
console.log("Test result", localStorage.getItem(storageKey)); | |
const timeConversion = (millisec) => { | |
const seconds = (millisec / 1000).toFixed(1); | |
const minutes = (millisec / (1000 * 60)).toFixed(1); | |
const hours = (millisec / (1000 * 60 * 60)).toFixed(1); | |
const days = (millisec / (1000 * 60 * 60 * 24)).toFixed(1); | |
if (seconds < 60) { | |
return seconds + " Sec"; | |
} else if (minutes < 60) { | |
return minutes + " Min"; | |
} else if (hours < 24) { | |
return hours + " Hrs"; | |
} else { | |
return days + " Days" | |
} | |
} | |
const countDownTimer = (delay) => { | |
if (!kill) { | |
return new Promise((resolve) => { | |
timeoutID = setTimeout(resolve, delay); | |
}); | |
} | |
else { | |
console.log("Stopping countDownTimer"); | |
} | |
} | |
async function zoomInOut(view) { | |
await localStorage.setItem(storageKey, timeConversion(performance.now() - startTime)); | |
console.log("Test elapsed time: ", localStorage.getItem(storageKey)); | |
await countDownTimer(2000); | |
await view.goTo({ | |
center: [-91.5, 42.5], | |
zoom: 12 | |
}); | |
await countDownTimer(2000); | |
await view.goTo({ | |
center: [-122.3, 47.6], | |
zoom: 15 | |
}); | |
await countDownTimer(2000); | |
await view.goTo({ | |
center: [-122.3, 47.6], | |
zoom: 5 | |
}); | |
await countDownTimer(2000); | |
return; | |
}; | |
const mapService = new MapService(mapViewService); | |
const mapViewParentElement = document.getElementById("viewDiv"); | |
const openModalButton = document.getElementById("openModal"); | |
const modalEl = document.getElementById("modal"); | |
// Clean up map-related resources and release the MapView for future re-use. | |
const closeModal = (mv) => { | |
mapService.teardown(mv); | |
modalEl.classList.remove("show"); | |
console.log("Modal window closed"); | |
} | |
const initStartButton = (mapView) => { | |
const startBtn = document.getElementById("startTest"); | |
startBtn.addEventListener("click", async () => { | |
if (startBtn.innerText == "Start Test") { | |
startBtn.innerText = "Stop Test"; | |
const runTest = async () => { | |
await zoomInOut(mapView); | |
if (!kill) { | |
modalEl.classList.add("show"); | |
console.log("Modal window open"); | |
// Load a map in a modal. Subsequent modal appearances will re-use a MapView/container/WebMap. | |
mapService.loadMap(modalEl) | |
.then((mv) => { | |
/************************** | |
Andy: added watchUtils here to insure the MapView has finished updating before proceeding | |
***************************/ | |
watchUtils.whenFalse(mv.basemapView, "updating", async () => { | |
await zoomInOut(mv); | |
console.log("done"); | |
// Clean up map-related resources and release the MapView for future re-use. | |
closeModal(mv); | |
runTest(); | |
}); | |
}) | |
} | |
else { | |
console.log("Stopping test"); | |
} | |
} | |
runTest(); | |
} | |
else if (startBtn.innerText == "Stop Test") { | |
kill = true; | |
startBtn.innerText = "Start Test"; | |
} | |
}); | |
} | |
// return the mapView | |
mapService.loadMap(mapViewParentElement) | |
.then((mapView) => { | |
/************************** | |
Andy: added watchUtils here to insure the MapView has finished updating before proceeding | |
***************************/ | |
watchUtils.whenFalse(mapView.basemapView, "updating", () => { | |
console.log("updating false! "); | |
initStartButton(mapView); | |
}); | |
}) | |
openModalButton.addEventListener("click", async () => { | |
if (openModalButton.innerText == "Open Modal") { | |
openModalButton.innerText = "Close Modal"; | |
// Load a map in a modal. Subsequent modal appearances will re-use a MapView/container/WebMap. | |
mapService.loadMap(modalEl, true); | |
modalEl.classList.add("show"); | |
console.log("Modal window open"); | |
} | |
else if (openModalButton.innerText == "Close Modal") { | |
openModalButton.innerText = "Open Modal"; | |
closeModal(); | |
} | |
}); | |
}); | |
</script> | |
</head> | |
<body> | |
<button id="openModal">Open Modal</button> | |
<button id="startTest">Start Test</button> | |
<div id="viewDiv"></div> | |
<div id="modal"></div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment