Skip to content

Instantly share code, notes, and snippets.

@andygup
Last active July 1, 2020 23:58
Show Gist options
  • Save andygup/a470f8fb813e336c6ca1c51a86563001 to your computer and use it in GitHub Desktop.
Save andygup/a470f8fb813e336c6ca1c51a86563001 to your computer and use it in GitHub Desktop.
Automated MapView test using a basemap and a modal map
<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