WME COL Basemap

Adds aerials from the COL GIS as a basemap for WME

目前为 2023-08-24 提交的版本。查看 最新版本

// ==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 });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址