WME Geometries

Import geometry files into Waze Map Editor. Supports GeoJSON, GML, WKT, KML and GPX.

当前为 2025-03-06 提交的版本,查看 最新版本

// ==UserScript==
// @name                WME Geometries
// @version             2025.03.05.002
// @description         Import geometry files into Waze Map Editor. Supports GeoJSON, GML, WKT, KML and GPX.
// @match               https://www.waze.com/*/editor*
// @match               https://www.waze.com/editor*
// @match               https://beta.waze.com/*
// @exclude             https://www.waze.com/*user/*editor/*
// @require             https://cdn.jsdelivr.net/npm/@tmcw/[email protected]/dist/togeojson.umd.min.js
// @require             https://unpkg.com/@terraformer/wkt
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/gml2geojson.min.js
// @require             https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js
// @require             https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @grant               none
// @author              Timbones
// @contributor         wlodek76
// @contributor         Twister-UK
// @contributor         Karlsosha
// @namespace           https://greasyfork.org/users/3339
// @run-at              document-idle
// ==/UserScript==
/* global WazeWrap */
"use strict";
// import { WmeSDK } from "wme-sdk-typings";
// import * as toGeoJSON from "@tmcw/togeojson";
// import * as Terraformer from "@terraformer/wkt";
// import * as turf from "@turf/turf";
// import { GeoJsonProperties } from 'geojson';
window.SDK_INITIALIZED.then(geometries);
function geometries() {
    const GF_LINK = "https://greasyfork.org/en/scripts/8129-wme-geometries";
    const FORUM_LINK = "https://www.waze.com/discuss/t/script-wme-geometries-v1-7-june-2021/291428/8";
    const GEOMETRIES_UPDATE_NOTES = `<b>NEW:</b><br>
    - Converted to WME SDK<br>
    - Added ability to remove individual layers<br>
    - Added ability to select field to display as label for the added shape.
<b>KNOWN ISSUES:</b><br>
    - Label Property is a radio Button vs ability to select multiple properties.<br>
    - Draw State Boundary is no longer available<br>
    - Some 3rd Party Data Files may cause issues for display<br>
    - 3D Points are not Supported. (LAT, LON, ALT)<br>
`;
    // show labels using first attribute that starts or ends with 'name' (case insensitive regexp)
    var defaultLabelName = /^name|name$/;
    // each loaded file will be rendered with one of these colours in ascending order
    var colorList = new Set([
        "deepskyblue",
        "magenta",
        "limegreen",
        "orange",
        "teal",
        "navy",
        "maroon",
    ]);
    let usedColors = new Set();
    // Id of div element for Checkboxes:
    const checkboxListID = "geometries-cb-list-id";
    let geometryLayers = {};
    let parser;
    let Formats;
    (function (Formats) {
        Formats[Formats["GEOJSON"] = 0] = "GEOJSON";
        Formats[Formats["KML"] = 1] = "KML";
        Formats[Formats["WKT"] = 2] = "WKT";
        Formats[Formats["GML"] = 3] = "GML";
        Formats[Formats["GMX"] = 4] = "GMX";
    })(Formats || (Formats = {}));
    let formathelp = "GeoJSON, KML, WKT, GPX, GML";
    var layerindex = 0;
    let selectedAttrib = "";
    if (!window.getWmeSdk) {
        throw new Error("SDK is not installed");
    }
    const sdk = window.getWmeSdk({
        scriptId: "wme-geometries",
        scriptName: "WME Geometries",
    });
    console.log(`SDK v ${sdk.getSDKVersion()} on ${sdk.getWMEVersion()} initialized`);
    // delayed initialisation
    sdk.Events.once({ eventName: "wme-map-data-loaded" }).then(() => {
        init();
    });
    // function processMapUpdateEvent() {
    //     if (Object.keys(geometryLayers).length === 0) return;
    //     for (const l in geometryLayers) {
    //         sdk.Map.removeLayer({ layerName: l });
    //         sdk.LayerSwitcher.removeLayerCheckbox({ name: l });
    //     }
    //     geometryLayers = {};
    //     loadLayers();
    // }
    // sdk.Events.on({ eventName: "wme-map-move-end", eventHandler: processMapUpdateEvent });
    // sdk.Events.on({ eventName: "wme-map-zoom-changed", eventHandler: processMapUpdateEvent });
    sdk.Events.on({
        eventName: "wme-layer-checkbox-toggled",
        eventHandler(payload) {
            sdk.Map.setLayerVisibility({ layerName: payload.name, visibility: payload.checked });
        },
    });
    class LayerStoreObj {
        fileContent;
        color;
        fileExt;
        fileName;
        formatType;
        constructor(fileContent, color, fileext, filename) {
            this.fileContent = fileContent;
            this.color = color;
            this.fileExt = fileext;
            this.fileName = filename;
            this.formatType = fileext.toUpperCase();
        }
    }
    function loadLayers() {
        // Parse any locally stored layer objects
        let files = JSON.parse(localStorage.getItem("WMEGeoLayers") || "[]");
        for (const f in files)
            processGeometryFile(files[f]);
    }
    // add interface to Settings tab
    function init() {
        if (!WazeWrap.Ready) {
            setTimeout(() => {
                init();
            }, 100);
            return;
        }
        var geobox = document.createElement("div");
        geobox.style.paddingTop = "6px";
        console.group();
        let sidepanelAreas = $("#sidepanel-areas");
        sidepanelAreas.append(geobox);
        var geotitle = document.createElement("h4");
        geotitle.innerHTML = "Import Geometry File";
        geobox.appendChild(geotitle);
        geolist = document.createElement("ul");
        geobox.appendChild(geolist);
        var geoform = document.createElement("form");
        geobox.appendChild(geoform);
        var inputfile = document.createElement("input");
        inputfile.type = "file";
        inputfile.id = "GeometryFile";
        inputfile.title = ".geojson, .gml or .wkt";
        inputfile.addEventListener("change", addGeometryLayer, false);
        geoform.appendChild(inputfile);
        var notes = document.createElement("p");
        notes.style.marginTop = "12px";
        notes.innerHTML =
            `<b>Formats:</b> <span id="formathelp">${formathelp}</span><br> ` +
                "<b>Coords:</b> EPSG:4326, EPSG:4269, EPSG:3857";
        geoform.appendChild(notes);
        // var inputstate = document.createElement("input");
        // inputstate.type = "button";
        // inputstate.value = "Draw State Boundary";
        // inputstate.title = "Draw the boundary for the topmost state";
        // inputstate.onclick = drawStateBoundary;
        // geoform.appendChild(inputstate);
        var inputclear = document.createElement("input");
        inputclear.type = "button";
        inputclear.value = "Clear All";
        inputclear.style.marginLeft = "8px";
        inputclear.onclick = removeGeometryLayers;
        geoform.appendChild(inputclear);
        loadLayers();
        WazeWrap.Interface.ShowScriptUpdate(GM_info.script.name, GM_info.script.version, GEOMETRIES_UPDATE_NOTES, GF_LINK, FORUM_LINK);
        console.log("WME Geometries is now available....");
        console.groupEnd();
    }
    function addFormat(format) {
        $("#formathelp")[0].innerText += ", " + format;
    }
    // function drawStateBoundary() {
    //     let topState: State | null = sdk.DataModel.States.getTopState();
    //     if (!topState) {
    //         console.info("WME Geometries: no state or geometry available, sorry");
    //         return;
    //     }
    //     var layerName = `(${topState.name})`;
    //     var layers = W.map.getLayersBy("layerGroup", "wme_geometry");
    //     for (var i = 0; i < layers.length; i++) {
    //         if (layers[i].name === "Geometry: " + layerName) {
    //             console.info("WME Geometries: current state already loaded");
    //             return;
    //         }
    //     }
    //     var geo = formats.GEOJSON.parseGeometry(topState.name);
    //     var json = formats.GEOJSON.write(geo);
    //     var obj = new layerStoreObj(json, "grey", "GEOJSON", layerName);
    //     parseFile(obj);
    // }
    // import selected file as a vector layer
    function addGeometryLayer() {
        // get the selected file from user
        var fileList = document.getElementById("GeometryFile");
        if (!fileList)
            return;
        var file = fileList.files[0];
        fileList.value = "";
        processGeometryFile(file);
    }
    function processGeometryFile(file) {
        if (colorList.size === 0) {
            console.error("Cannot add Any more Layers at this point");
        }
        var fileext = file?.name?.split(".").pop();
        var filename = file?.name?.replace("." + fileext, "");
        if (!file || !file?.name || !fileext || !filename)
            return;
        fileext = fileext ? fileext.toUpperCase() : "";
        // add list item
        var color = colorList.values().next().value;
        if (!color) {
            console.error("Cannot add Any more Layers at this point");
        }
        colorList.delete(color);
        usedColors.add(color);
        var fileitem = document.createElement("li");
        fileitem.id = file.name.toLowerCase();
        fileitem.style.color = color;
        fileitem.innerHTML = "Loading...";
        geolist.appendChild(fileitem);
        // check if format is supported
        let parser = {
            read: null,
            internalProjection: null,
            externalProjection: null,
        };
        if (!parser) {
            fileitem.innerHTML = fileext.toUpperCase() + " format not supported :(";
            fileitem.style.color = "red";
            return;
        }
        // read the file into the new layer, and update the localStorage layer cache
        var reader = new FileReader();
        reader.onload = (function (theFile) {
            return function (e) {
                var tObj = new LayerStoreObj(e.target.result, color, fileext, filename);
                parseFile(tObj);
                let filenames = JSON.parse(localStorage.getItem("WMEGeoLayers") || "[]");
                filenames[color] = theFile;
                localStorage.setItem("WMEGeoLayers", JSON.stringify(filenames));
            };
        })(file);
        reader.readAsText(file);
    }
    const layerConfig = {
        defaultRule: {
            styleContext: {
                strokeColor: (context) => {
                    let style = context?.feature?.properties?.style;
                    if (!style)
                        return style;
                    return style?.strokeColor;
                },
                fillColor: (context) => {
                    let style = context?.feature?.properties?.style;
                    if (!style)
                        return style;
                    return style?.fillColor;
                },
                labelOutlineColor: (context) => {
                    let style = context?.feature?.properties?.style;
                    if (!style)
                        return style;
                    return style?.labelOutlineColor;
                },
                label: (context) => {
                    let style = context?.feature?.properties?.style;
                    if (!style)
                        return style;
                    return style?.label;
                },
            },
            styleRules: [
                {
                    predicate: () => {
                        return true;
                    },
                    style: {
                        strokeColor: "${strokeColor}",
                        strokeOpacity: 0.75,
                        strokeWidth: 3,
                        fillColor: "${fillColor}",
                        fillOpacity: 0.1,
                        pointRadius: 6,
                        fontColor: "white",
                        labelOutlineColor: "${labelOutlineColor}",
                        labelOutlineWidth: 4,
                        labelAlign: "center",
                        label: "${label}",
                    },
                },
            ],
        },
    };
    // Renders a layer object
    function parseFile(layerObj) {
        // add a new layer for the geometry
        var layerid = "wme_geometry_" + ++layerindex;
        sdk.Map.addLayer({
            layerName: layerid,
            styleRules: layerConfig.defaultRule.styleRules,
            styleContext: layerConfig.defaultRule.styleContext,
        });
        sdk.Map.setLayerVisibility({ layerName: layerid, visibility: true });
        sdk.LayerSwitcher.addLayerCheckbox({ name: layerid });
        let features = [];
        switch (layerObj.formatType) {
            case "GEOJSON":
                let jsonObject = JSON.parse(layerObj.fileContent);
                {
                    jsonObject = turf.flatten(jsonObject);
                    features = jsonObject.features;
                }
                geometryLayers[layerid] = features;
                break;
            case "KML":
                let kmlData = new DOMParser().parseFromString(layerObj.fileContent, "application/xml");
                let geoJson = toGeoJSON.kml(kmlData);
                {
                    geoJson = turf.flatten(geoJson);
                    features = geoJson.features;
                }
                geometryLayers[layerid] = features;
                break;
            case "GPX":
                let gpxData = new DOMParser().parseFromString(layerObj.fileContent, "application/xml");
                let gpxGeoGson = toGeoJSON.gpx(gpxData);
                {
                    gpxGeoGson = turf.flatten(gpxGeoGson);
                    features = gpxGeoGson.features;
                }
                geometryLayers[layerid] = features;
                break;
            case "WKT":
                const wktGeoJson = Terraformer.wktToGeoJSON(layerObj.fileContent);
                switch (wktGeoJson.type) {
                    case "Polygon":
                        features = [
                            {
                                type: "Feature",
                                properties: { name: layerObj.fileName },
                                geometry: wktGeoJson,
                            },
                        ];
                        break;
                    case "GeometryCollection":
                        features = [];
                        for (let g in wktGeoJson.geometries) {
                            features.push({
                                type: "Feature",
                                properties: { name: layerObj.fileName },
                                geometry: wktGeoJson.geometries[g],
                            });
                        }
                        let featureCollection = turf.featureCollection(features);
                        featureCollection = turf.flatten(featureCollection);
                        features = featureCollection.features;
                        break;
                    default:
                        let errorMessage = "Unknown Type has been Encountered";
                        console.error(errorMessage);
                        throw Error(errorMessage);
                        break;
                }
                break;
            case "GML":
                // let gmlData = new DOMParser().parseFromString(layerObj.fileContent, "application/xml");
                let gmlGeoJSON = gml2geojson.parseGML(layerObj.fileContent);
                {
                    gmlGeoJSON = turf.flatten(gmlGeoJSON);
                    features = gmlGeoJSON.features;
                }
                geometryLayers[layerid] = features;
                break;
            default:
                throw new Error(`Format Type: ${layerObj.formatType} is not implemented`);
        }
        // hack in translation:
        // I18n.translations[sdk.Settings.getLocale()].layers.name[layerid] = "WME Geometries: " + layerObj.filename;
        // if (/"EPSG:3857"|:EPSG::3857"/.test(layerObj.fileContent)) {
        //     parser.externalProjection = EPSG_3857;
        // }
        // else if (/"EPSG:4269"|:EPSG::4269"/.test(layerObj.fileContent)) {
        //     parser.externalProjection = EPSG_4269;
        // }
        // else default to EPSG:4326
        // load geometry files
        // var features = parser.read(layerObj.fileContent);
        // Append Div for Future Use for picking the Layer with Name
        let layersList = document.createElement("ul");
        layersList.className = "geometries-cb-list";
        layersList.id = checkboxListID;
        let trigger = null;
        // check we have features to render
        if (features.length > 0) {
            // check which attribute can be used for labels
            var labelWith = "(no labels)";
            for (const attrib in features[0].properties) {
                let attribLC = attrib.toLowerCase();
                let attribClassName = `geometries-${layerindex}-` + attribLC;
                let attribIdName = `geometries-${layerindex}-` + attribLC;
                let listElement = document.createElement("li");
                let inputElement = document.createElement("input");
                inputElement.className = attribClassName;
                inputElement.id = attribIdName;
                inputElement.setAttribute("type", "radio");
                inputElement.setAttribute("name", `geometries-name-label-${layerindex}`);
                inputElement.textContent = attrib;
                listElement.appendChild(inputElement);
                let labelElement = document.createElement("label");
                labelElement.textContent = attrib;
                labelElement.className = "geometries-cb-label";
                labelElement.setAttribute("for", attribIdName);
                labelElement.style.color = "black";
                listElement.appendChild(labelElement);
                // let selectorString = "<li><input type=radio class='" + attribClassName + "' id='" +
                //     attribIdName + "' name='geometries-name-label'/>" +
                //     "<label class='geometries-cb-label'>" + attrib + "</label></li>"
                layersList.appendChild(listElement);
                $(inputElement).on("change", function (event) {
                    addFeatures(features, event);
                });
                if (selectedAttrib && selectedAttrib === attrib) {
                    trigger = $(inputElement);
                }
                else if (!selectedAttrib && defaultLabelName.test(attribLC) === true) {
                    trigger = $(inputElement);
                }
            }
        }
        if (trigger) {
            trigger[0].checked = true;
            trigger.trigger("change");
        }
        function createClearButton(layerObj, layerid) {
            let clearButtonObject = document.createElement("button");
            clearButtonObject.textContent = "Clear Layer";
            clearButtonObject.name = "clear-" + (layerObj.fileName + "." + layerObj.fileExt).toLowerCase();
            clearButtonObject.id = "clear-" + layerid;
            clearButtonObject.className = "clear-layer-button";
            clearButtonObject.style.backgroundColor = layerObj.color;
            return clearButtonObject;
        }
        // When called as part of loading a new file, the list object will already have been created,
        // whereas if called as part of reloding cached data we need to create it here...
        var liObj = document.getElementById((layerObj.fileName + "." + layerObj.fileExt).toLowerCase());
        if (liObj === null) {
            liObj = document.createElement("li");
            liObj.id = (layerObj.fileName + "." + layerObj.fileExt).toLowerCase();
            liObj.style.color = layerObj.color;
            geolist.appendChild(liObj);
        }
        if (features.length === 0) {
            liObj.innerHTML = "No features loaded :(";
            liObj.style.color = "red";
        }
        else {
            liObj.innerHTML = layerObj.fileName;
            liObj.title =
                layerObj.fileExt.toUpperCase() +
                    // " " +
                    // parser.externalProjection.projCode +
                    ": " +
                    features.length +
                    " features loaded\n" +
                    labelWith;
            liObj.appendChild(layersList);
            let clearButtonObject = createClearButton(layerObj, layerid);
            liObj.appendChild(clearButtonObject);
            console.info("WME Geometries: Loaded " + liObj.title);
            $(".clear-layer-button").on("click", function () {
                let clearLayerId = this.id;
                clearLayerId = clearLayerId.replace("clear-", "");
                let clearListId = "";
                if (this.hasAttribute("name")) {
                    clearListId = this.getAttribute("name");
                    clearListId = clearListId?.replace("clear-", "");
                    if (clearListId) {
                        let elem = document.getElementById(clearListId);
                        elem?.remove();
                    }
                }
                sdk.Map.removeLayer({ layerName: clearLayerId });
                delete geometryLayers[clearLayerId];
                sdk.LayerSwitcher.removeLayerCheckbox({ name: clearLayerId });
                let listId = this.textContent?.replace("Clear ", "");
                if (!listId)
                    return;
                let elementToRemove = document.getElementById(listId);
                elementToRemove?.remove();
                let files = JSON.parse(localStorage.getItem("WMEGeoLayers") || "[]");
                delete files[this.style.backgroundColor];
                localStorage.setItem("WMEGeoLayers", JSON.stringify(files));
                usedColors.delete(this.style.backgroundColor);
                colorList.add(this.style.backgroundColor);
                this.remove();
            });
        }
        function addFeatures(features, event) {
            sdk.Map.removeAllFeaturesFromLayer({ layerName: layerid });
            selectedAttrib = event && event.target ? event.target.textContent : "";
            for (const f of features) {
                if (f.properties) {
                    labelWith = "Labels: " + selectedAttrib;
                    let layerStyle = {
                        strokeColor: layerObj.color,
                        fillColor: layerObj.color,
                        labelOutlineColor: layerObj.color,
                        label: typeof f.properties[selectedAttrib] === "string"
                            ? `${f.properties[selectedAttrib]}`
                            : "undefined",
                    };
                    if (!f.properties?.style)
                        f.properties.style = {};
                    Object.assign(f.properties.style, layerStyle);
                }
                if (!f.id) {
                    f.id = layerid + "_" + layerindex.toString();
                }
                sdk.Map.addFeatureToLayer({ feature: f, layerName: layerid });
            }
        }
    }
    // clear all
    function removeGeometryLayers() {
        for (const l in geometryLayers) {
            sdk.Map.removeLayer({ layerName: l });
            sdk.LayerSwitcher.removeLayerCheckbox({ name: l });
        }
        geometryLayers = {};
        geolist.innerHTML = "";
        layerindex = 0;
        // Clear the cached layers
        localStorage.removeItem("WMEGeoLayers");
        for (const c in usedColors) {
            colorList.add(c);
        }
        usedColors.clear();
        return false;
    }
}