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