// ==UserScript==
// @name WME COL Basemap
// @namespace https://fxzfun.com/
// @version 3.1.1
// @description Adds aerials from the COL GIS as a basemap for WME
// @author FXZFun
// @match https://*.waze.com/*/editor*
// @match https://*.waze.com/editor*
// @exclude https://*.waze.com/user/editor*
// @connect query.cityoflewisville.com
// @icon https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
// @grant GM_xmlhttpRequest
// @license GNU GPLv3
// ==/UserScript==
/* global W, OpenLayers, WazeWrap, trustedTypes */
(function() {
'use strict';
const STORAGE_TICKET_KEY = "wmecolbasemap-ticket";
const STORAGE_EXPIRE_KEY = "wmecolbasemap-ticketExpiryDate";
const STORAGE_DATES_KEY = "wmecolbasemap-aerialDates";
const STORAGE_MOST_RECENT_DATE_KEY = "wmecolbasemap-mostRecentDate";
const STORAGE_SHORTCUT_KEY = "wmecolbasemap-shortcut";
const STORAGE_BUBBLE_KEY = "wmecolbasemap-settingsBubbleEnabled";
const ELEMENT_BUBBLE_ID = "wmecolbasemap-datePickerContainer";
const ELEMENT_DATE_PICKER_ID = "wmecolbasemap-datePicker";
const ELEMENT_REFRESH_ID = "wmecolbasemap-refreshBtn";
const ELEMENT_POPUP_CHECKBOX_ID = "wmecolbasemap-popupCheckbox";
const ELEMENT_SETTINGS_BUBBLE_ID = "wmecolbasemap-settingsBubble";
const ELEMENT_SETTINGS_TOGGLE_ID = "wmecolbasemap-settingsLayerToggle";
const ELEMENT_SETTINGS_DATE_PICKER_ID = "wmecolbasemap-settingsDatePicker";
const ELEMENT_SETTINGS_RELOAD_ID = "wmecolbasemap-settingsRefreshBtn";
const DEBUG = true;
const NAME = "WME COL Basemap";
const pageWindow = unsafeWindow ?? window;
const policy = trustedTypes?.createPolicy('wmecolbasemapPolicy', { createHTML: (input) => input});
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
let layer;
let layerEnabled = false;
let bubbleEnabled = JSON.parse(localStorage.getItem(STORAGE_BUBBLE_KEY)) ?? false;
let currentDate;
let errorCount = 0;
function getJSON(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: async function(response) {
resolve(JSON.parse(response.response));
},
onerror: function(error) {
reject(error);
}
});
});
}
async function getTicket() {
let ticket = localStorage.getItem(STORAGE_TICKET_KEY);
let ticketExpired = (localStorage.getItem(STORAGE_EXPIRE_KEY) ?? 0) < (new Date().getTime() + (60 * 60 * 1000)); // get new ticket if less than an hour left
if (!ticket || ticketExpired) ticket = await refreshTicket();
return ticket;
}
async function refreshTicket() {
const json = await getJSON("https://query.cityoflewisville.com/v2/?webservice=NearmapTicketAndDates");
const data = json[0][0];
const dates = JSON.parse(data.aerialdates).map(d => d.date);
localStorage.setItem(STORAGE_TICKET_KEY, data?.ticket);
localStorage.setItem(STORAGE_DATES_KEY, JSON.stringify(dates));
localStorage.setItem(STORAGE_MOST_RECENT_DATE_KEY, dates[0]);
localStorage.setItem(STORAGE_EXPIRE_KEY, new Date().getTime() + (24 * 60 * 60 * 1000)); // expires every 24 hours
return data?.ticket;
}
function getMostRecentDate() {
return localStorage.getItem(STORAGE_MOST_RECENT_DATE_KEY)?.replaceAll(".", "") ?? "20230531";
}
function createURL(date, ticket) {
return "https://us0.nearmap.com/maps/?z=${z}&x=${x}&y=${y}&nml=V&version=2&nmd=" + date + "&ticket=" + ticket;
}
/*
Add the layer to the map if it does not exist
*/
async function addLayer() {
const date = getMostRecentDate();
const ticket = await getTicket();
const url = createURL(date, ticket);
layer = new OpenLayers.Layer.XYZ(
NAME,
url,
{
isBaseLayer: false,
uniqueName: 'colgis',
tileSize: new OpenLayers.Size(256,256),
transitionEffect: 'resize',
displayInLayerSwitcher: true,
opacity: 1,
visibility: false
});
layer.events.on({
'loadend': async function (evt) {
if (DEBUG) console.log("WME COL Basemap: Loaded tiles");
const refreshBtn = document.getElementById(ELEMENT_REFRESH_ID);
if (refreshBtn) {
refreshBtn.style.animation = "3s wmecolbasemapRefresh";
await sleep(3000);
refreshBtn.style.animation = "";
}
},
'loaderror': async function(evt) {
if (errorCount++ === 0) {
console.error("WME COL Basemap: Error loading tile");
await refreshTicket();
updateLayerDate(currentDate ?? getMostRecentDate());
} else if (errorCount === 50) {
toggleBasemap();
alert("WME COL Basemap failed to reach imagery endpoint");
}
}
});
W.map.addLayer(layer);
W.map.setLayerIndex(layer, 3);
}
async function updateLayerDate(date) {
if (!layer) return;
let ticket = await getTicket();
layer.url = createURL(date, ticket);
layer.redraw();
}
/*
Add the date picker element to the top left of the map
*/
function addSettingsBubble() {
var dates = JSON.parse(localStorage.getItem(STORAGE_DATES_KEY) ?? "[]");
const div = document.createElement("div");
div.id = ELEMENT_BUBBLE_ID;
div.innerHTML = policy.createHTML(
`<style>
.wmecolbasemap { position: absolute; top: 40px; left: 60px; background-color: #fafafa; padding: 0.5em 0.75em; border-radius: 2em; }
.wmecolbasemap i { padding: 0.5em; vertical-align: middle; }
.wmecolbasemap #select-wrapper { display: none; }
@keyframes wmecolbasemapRefresh { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }
</style>
<div class="wmecolbasemap">
<wz-checkbox id="${ELEMENT_POPUP_CHECKBOX_ID}" value="on" style="display: inline-block; vertical-align: middle; margin-left: 0.5em;" checked></wz-checkbox>
<wz-select id="${ELEMENT_DATE_PICKER_ID}" value="${currentDate ?? dates[0]}" style="display: inline-block">
<div class="select-wrapper" id="select-wrapper"><div tabindex="0" class="select-box"><div class="selected-value-wrapper"><span class="selected-value">${currentDate ?? dates[0]}</span></div></div></div>
${dates.map(d => "<wz-option value=" + d + ">" + d + "</wz-option>").join("")}
</wz-select>
<i id="${ELEMENT_REFRESH_ID}" class="w-icon w-icon-refresh"></i>
</div>`);
document.querySelector(".olMap").appendChild(div);
document.getElementById(ELEMENT_POPUP_CHECKBOX_ID).addEventListener("click", toggleBasemap);
const select = document.getElementById(ELEMENT_DATE_PICKER_ID);
select.addEventListener("optionClicked", (evt) => {
currentDate = evt.detail.value ?? currentDate;
select.value = currentDate;
updateLayerDate(select.value.replaceAll(".", ""));
let settingsSelect = document.getElementById(ELEMENT_SETTINGS_DATE_PICKER_ID)
if (!!settingsSelect) settingsSelect.value = currentDate;
});
const refreshBtn = document.getElementById(ELEMENT_REFRESH_ID);
refreshBtn.addEventListener("click", async () => {
refreshBtn.style.animation = "1s wmecolbasemapRefresh";
updateLayerDate(currentDate ?? getMostRecentDate());
await sleep(1000);
div.remove();
addSettingsBubble();
});
}
function toggleBubble() {
bubbleEnabled = !bubbleEnabled;
const bubble = document.getElementById(ELEMENT_BUBBLE_ID);
if (!!bubble && !bubbleEnabled) bubble.remove();
else if (layerEnabled) addSettingsBubble();
localStorage.setItem(STORAGE_BUBBLE_KEY, bubbleEnabled);
}
/*
Callback for shortcut and layer checkbox, toggles the visibility of the layer
*/
function toggleBasemap() {
layerEnabled = !layerEnabled;
layer?.setVisibility(layerEnabled);
document.querySelector("#layer-switcher-item_wme_col_basemap").checked = layerEnabled;
document.getElementById(ELEMENT_SETTINGS_TOGGLE_ID).checked = layerEnabled;
// toggles date picker
if (layerEnabled && bubbleEnabled) addSettingsBubble();
else document.getElementById(ELEMENT_BUBBLE_ID)?.remove();
}
/*
Main entry point of the script
Adds layer checkbox, shortcut, and layer
*/
async function initialize() {
console.log("WME COL Basemap: Start");
if (DEBUG) pageWindow.wmeColBasemap = { getJSON, getTicket, refreshTicket, getMostRecentDate, createURL, addLayer, updateLayerDate, addSettingsBubble, toggleBasemap, layer };
addLayer();
console.log("WME COL Basemap: Added Layer");
const i = setInterval(() => {
if (WazeWrap?.Ready) {
clearInterval(i);
WazeWrap.Interface.AddLayerCheckbox(
"display",
NAME,
false,
toggleBasemap,
layer ?? W.map.getLayerByName(NAME));
new WazeWrap.Interface.Shortcut('COLBasemapDisplay', 'Toggle COL Basemap',
'layers', 'layersToggleCOLBasemapDisplay', localStorage.getItem(STORAGE_SHORTCUT_KEY) ?? "", toggleBasemap, null).add();
}
}, 500);
const dates = JSON.parse(localStorage.getItem(STORAGE_DATES_KEY) ?? "[]");
const { tabLabel, tabPane } = W.userscripts.registerSidebarTab("wmeColBasemap");
tabLabel.innerText = 'COL';
tabLabel.title = NAME;
tabPane.style = "display: flex; flex-direction: column; height: 100%; gap: 5px;";
tabPane.parentElement.style.height = "calc(100% - 30px)";
tabPane.innerHTML = policy.createHTML(`
<h3>WME COL Basemap <small>v3.1</small></h3>
<p>provided by <a href="https://maps.cityoflewisville.com" target="_blank">maps.cityoflewisville.com</a></p>
<b style="margin-top: 20px">Options</b>
<wz-checkbox id="${ELEMENT_SETTINGS_BUBBLE_ID}" value="${bubbleEnabled ? "on" : "off"}" name="" ${bubbleEnabled ? "checked=''" : ''}>Show Settings Bubble</wz-checkbox>
<wz-checkbox id="${ELEMENT_SETTINGS_TOGGLE_ID}" value="off" name="">Toggle Basemap Layer</wz-checkbox>
<span style="margin-top:20px">Aerial Date</span>
<wz-select id="${ELEMENT_SETTINGS_DATE_PICKER_ID}" value="${currentDate ?? dates[0]}" style="display: inline-block">
<div class="select-wrapper" id="select-wrapper" style="display: none"><div tabindex="0" class="select-box"><div class="selected-value-wrapper"><span class="selected-value">${currentDate ?? dates[0]}</span></div></div></div>
${dates.map(d => "<wz-option value=" + d + ">" + d + "</wz-option>").join("")}
</wz-select>
<wz-button id="${ELEMENT_SETTINGS_RELOAD_ID}" color="text" size="sm">Clear token and reload layer</wz-button>
<p style="margin-top: 20px">Current Shortcut: ${localStorage.getItem(STORAGE_SHORTCUT_KEY) ?? "None"}</p>
<i style="margin-top: 20px">Note that aerial dates are not the date the imagery was taken, but the date that Lewisville updated their site</i>
<p style="margin-top: auto;"><b>Note:</b> please do not use as your default basemap - only enable when needed, as we do not want to abuse the service provided by the City of Lewisville GIS.</p>
<em>Aerial Imagery © Nearmap - nearmap.com</em>
`);
await W.userscripts.waitForElementConnected(tabPane);
document.getElementById(ELEMENT_SETTINGS_BUBBLE_ID).addEventListener("click", toggleBubble);
document.getElementById(ELEMENT_SETTINGS_TOGGLE_ID).addEventListener("click", toggleBasemap);
document.getElementById(ELEMENT_SETTINGS_RELOAD_ID).addEventListener("click", () => updateLayerDate(currentDate ?? getMostRecentDate()));
const select = document.getElementById(ELEMENT_SETTINGS_DATE_PICKER_ID);
select.addEventListener("optionClicked", (evt) => {
currentDate = evt.detail.value ?? currentDate;
select.value = currentDate;
updateLayerDate(select.value.replaceAll(".", ""));
let bubbleSelect = document.getElementById(ELEMENT_DATE_PICKER_ID)
if (!!bubbleSelect) bubbleSelect.value = currentDate;
});
pageWindow.addEventListener("beforeunload", () => {
const shortcut = W.accelerators.Actions.COLBasemapDisplay?.shortcut;
if (!shortcut) return;
const modifiers = [];
if (shortcut.ctrlKey) modifiers.push("Ctrl");
if (shortcut.altKey) modifiers.push("Alt");
if (shortcut.shiftKey) modifiers.push("Shift");
const newShortcut = `${modifiers.join("+")}${String.fromCharCode(shortcut.keyCode)}`;
localStorage.setItem(STORAGE_SHORTCUT_KEY, newShortcut);
console.log("Saved WME COL Basemap settings");
});
}
W?.userscripts?.state?.isReady ? initialize() : document.addEventListener("wme-ready", initialize, { once: true });
})();