Greasy Fork is available in English.

WME GPX/KML/WKT/GML/GeoJSON Overlay

Overlay GPX, KML, WKT, GML or GeoJSON files onto Waze Map Editor

// ==UserScript==
// @name        WME GPX/KML/WKT/GML/GeoJSON Overlay
// @namespace   https://www.waze.com/
// @version     1.4
// @description Overlay GPX, KML, WKT, GML or GeoJSON files onto Waze Map Editor
// @author      Dosojintaizo
// @license     MIT/BSD/X11
// @include     https://www.waze.com/editor*
// @include     https://www.waze.com/*/editor*
// @include     https://beta.waze.com/editor*
// @include     https://beta.waze.com/*/editor*
// @require     https://update.greasyfork.org/scripts/520574/1502033/togeojson.js
// @grant       none
// ==/UserScript==

(function () {
  "use strict";

  if (W?.userscripts?.state.isReady) {
    initializeScript();
  } else {
    document.addEventListener("wme-ready", initializeScript, { once: true });
  }

  const overlays = [];

  async function initializeScript() {
    console.log("WME GPX/KML/WKT/GML/GeoJSON Overlay script initialized.");

    const EPSG_4326 = new OpenLayers.Projection("EPSG:4326"); // lat,lon
    const EPSG_4269 = new OpenLayers.Projection("EPSG:4269"); // NAD 83
    const EPSG_3857 = new OpenLayers.Projection("EPSG:3857"); // WGS 84

    const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(
      "wme-strava-kml-overlay"
    );
    tabLabel.innerText = "Geo Overlay";
    tabLabel.title =
      "Import and manage GPX/KML/WKT/GML/GeoJSON overlays on the map";

    tabPane.innerHTML = `<div>
            <h5>Geo Overlay</h5>
            <p>Import GPX, KML, WKT, GML or GeoJSON files to overlay them on the map.</p>
            <label for="fileInput" style="
                display: inline-block;
                padding: 10px 20px;
                background-color: #0078d7;
                color: white;
                border: 1px solid #005bb5;
                border-radius: 8px;
                cursor: pointer;
                font-size: 14px;
                text-align: center;
                transition: background-color 0.3s ease;">
                Select File
            </label>
            <input type="file" id="fileInput" accept=".gpx,.kml,.wkt,.gml,.geojson" style="display: none;" />
            <div id="overlayList" style="margin-top: 20px;"></div>
            <div id="status" style="margin-top: 10px; color: green;"></div>
        </div>`;

    await W.userscripts.waitForElementConnected(tabPane);

    const fileInput = tabPane.querySelector("#fileInput");
    const overlayList = tabPane.querySelector("#overlayList");
    const status = tabPane.querySelector("#status");

    fileInput.addEventListener("change", async (event) => {
      const file = event.target.files[0];

      if (!file) {
        status.textContent = "No file selected.";
        return;
      }

      try {
        const text = await file.text();
        let geoJSON;

        if (file.name.endsWith(".gpx")) {
          geoJSON = parseGPXToGeoJSON(text);
        } else if (file.name.endsWith(".kml")) {
          geoJSON = parseKMLToGeoJSON(text);
        } else if (file.name.endsWith(".geojson")) {
          geoJSON = JSON.parse(text);
        } else if (file.name.endsWith(".wkt")) {
          geoJSON = parseWKTToGeoJSON(text);
        } else if (file.name.endsWith(".gml")) {
          geoJSON = parseGMLToGeoJSON(text);
        } else {
          throw new Error(
            "Unsupported file format. Please upload a GPX, KML, GeoJSON, WKT, or GML file."
          );
        }

        addOverlay(file.name, geoJSON);
      } catch (error) {
        console.error("Error processing file:", error);
        status.textContent = `Error: ${error.message}`;
      }
    });
  }

  function parseGPXToGeoJSON(gpxText) {
    const parser = new DOMParser();
    const gpxDoc = parser.parseFromString(gpxText, "application/xml");
    return toGeoJSON.gpx(gpxDoc);
  }

  function parseKMLToGeoJSON(kmlText) {
    const parser = new DOMParser();
    const kmlDoc = parser.parseFromString(kmlText, "application/xml");
    return toGeoJSON.kml(kmlDoc);
  }

  function parseWKTToGeoJSON(wktText) {
    if (typeof Wkt !== "undefined") {
      const wkt = new Wkt.Wkt();
      wkt.read(wktText);
      return wkt.toJson();
    }
    throw new Error("WKT parsing requires Wicket.js library.");
  }

  function parseGMLToGeoJSON(gmlText) {
    const parser = new DOMParser();
    const gmlDoc = parser.parseFromString(gmlText, "application/xml");
    const format = new OpenLayers.Format.GML();
    const features = format.read(gmlDoc);
    const geoJSON = new OpenLayers.Format.GeoJSON();
    return geoJSON.write(features);
  }

  function addOverlay(fileName, geoJSON) {
    // Check if an overlay with the same name already exists
    if (overlays.some((overlay) => overlay.name === fileName)) {
      const status = document.getElementById("status");
      status.textContent = `Error: The file "${fileName}" has already been added.`;
      status.style.color = "red";
      return;
    }

    const layerName = fileName;
    const vectorLayer = new OpenLayers.Layer.Vector(layerName, {
      styleMap: new OpenLayers.StyleMap({
        default: new OpenLayers.Style({
          strokeColor: "#FFFF00",
          strokeWidth: 3,
          fillOpacity: 0.4,
        }),
      }),
    });

    geoJSON.features.forEach((feature) => {
      if (feature.geometry && feature.geometry.coordinates) {
        feature.geometry.coordinates = removeZCoordinates(
          feature.geometry.coordinates
        );
      }

      const olGeometry = W.userscripts.toOLGeometry(feature.geometry);
      const vectorFeature = new OpenLayers.Feature.Vector(olGeometry);
      vectorLayer.addFeatures([vectorFeature]);
    });

    W.map.addLayer(vectorLayer);

    const overlay = {
      name: fileName,
      layer: vectorLayer,
      color: "#FFFF00",
      width: 3,
    };

    overlays.push(overlay);
    renderOverlayList();
  }

  function removeZCoordinates(coords) {
    if (Array.isArray(coords[0])) {
      return coords.map(removeZCoordinates);
    } else if (coords.length >= 2) {
      return coords.slice(0, 2);
    }
    return coords;
  }

  function renderOverlayList() {
    const overlayList = document.getElementById("overlayList");
    overlayList.innerHTML = "";

    overlays.forEach((overlay, index) => {
      const item = document.createElement("div");
      item.style.marginBottom = "10px";
      item.style.position = "relative";
      item.style.border = "1px solid #ccc";
      item.style.borderRadius = "8px";
      item.style.padding = "10px";
      item.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
      item.style.display = "flex";
      item.style.flexDirection = "column";

      // Top row container
      const topRow = document.createElement("div");
      topRow.style.display = "flex";
      topRow.style.alignItems = "center";
      topRow.style.justifyContent = "space-between";

      const leftContainer = document.createElement("div");
      leftContainer.style.display = "flex";
      leftContainer.style.alignItems = "center";

      const title = document.createElement("span");
      title.textContent = overlay.name;
      title.style.fontWeight = "bold";
      title.style.marginLeft = "10px";
      title.style.marginRight = "10px";

      const checkboxContainer = document.createElement("label");
      checkboxContainer.style.position = "relative";
      checkboxContainer.style.display = "inline-block";
      checkboxContainer.style.width = "24px";
      checkboxContainer.style.height = "24px";
      checkboxContainer.style.border = "2px solid #ccc";
      checkboxContainer.style.borderRadius = "4px";
      checkboxContainer.style.cursor = "pointer";
      checkboxContainer.style.transition = "all 0.3s ease";

      const toggle = document.createElement("input");
      toggle.type = "checkbox";
      toggle.style.display = "none";
      toggle.checked = true;

      const customCheckbox = document.createElement("span");
      customCheckbox.style.position = "absolute";
      customCheckbox.style.top = "50%";
      customCheckbox.style.left = "50%";
      customCheckbox.style.transform = "translate(-50%, -50%)";
      customCheckbox.style.width = "16px";
      customCheckbox.style.height = "16px";
      customCheckbox.style.backgroundColor = "#FFFF00";
      customCheckbox.style.borderRadius = "2px";
      customCheckbox.style.transition = "background-color 0.3s ease";

      toggle.addEventListener("change", () => {
        overlay.layer.setVisibility(toggle.checked);
        customCheckbox.style.backgroundColor = toggle.checked
          ? overlay.color // Use overlay color when checked
          : "transparent"; // Transparent when unchecked
      });

      checkboxContainer.appendChild(toggle);
      checkboxContainer.appendChild(customCheckbox);

      const iconsContainer = document.createElement("div");
      iconsContainer.style.display = "flex";
      iconsContainer.style.alignItems = "center";

      const gearIcon = document.createElement("span");
      gearIcon.textContent = "🎨";
      gearIcon.style.cursor = "pointer";
      gearIcon.style.fontSize = "20px";
      gearIcon.style.marginLeft = "10px";

      const trashIcon = document.createElement("span");
      trashIcon.textContent = "❌";
      trashIcon.style.cursor = "pointer";
      trashIcon.style.marginLeft = "10px";
      trashIcon.style.fontSize = "10px";
      trashIcon.addEventListener("click", () => {
        W.map.removeLayer(overlay.layer);
        overlays.splice(index, 1);
        renderOverlayList();
      });

      leftContainer.appendChild(checkboxContainer);
      leftContainer.appendChild(title);
      iconsContainer.appendChild(gearIcon);
      iconsContainer.appendChild(trashIcon);

      topRow.appendChild(leftContainer);
      topRow.appendChild(iconsContainer);

      // Settings container
      const settings = document.createElement("div");
      settings.style.display = "none";
      settings.style.marginTop = "10px";
      settings.style.padding = "10px";
      settings.style.border = "1px solid #ccc";
      settings.style.borderRadius = "8px";
      settings.style.backgroundColor = "#f9f9f9";

      const colorRow = document.createElement("div");
      colorRow.style.display = "flex";
      colorRow.style.alignItems = "center";
      colorRow.style.marginBottom = "10px";

      const colorLabel = document.createElement("label");
      colorLabel.textContent = "Line Color:";
      colorLabel.style.marginRight = "10px";

      const colorInput = document.createElement("input");
      colorInput.type = "color";
      colorInput.value = overlay.color;

      colorInput.addEventListener("input", (event) => {
        overlay.color = event.target.value;
        overlay.layer.styleMap.styles.default.defaultStyle.strokeColor =
          overlay.color;
        overlay.layer.redraw();
        customCheckbox.style.backgroundColor = overlay.color;
      });

      colorRow.appendChild(colorLabel);
      colorRow.appendChild(colorInput);

      const widthRow = document.createElement("div");
      widthRow.style.display = "flex";
      widthRow.style.alignItems = "center";

      const widthLabel = document.createElement("label");
      widthLabel.textContent = "Line Width:";
      widthLabel.style.marginRight = "10px";

      const widthInput = document.createElement("input");
      widthInput.type = "number";
      widthInput.value = overlay.width;
      widthInput.min = 1;
      widthInput.max = 10;
      widthInput.style.width = "50px";

      widthInput.addEventListener("input", (event) => {
        overlay.width = parseInt(event.target.value, 10) || 1;
        overlay.layer.styleMap.styles.default.defaultStyle.strokeWidth =
          overlay.width;
        overlay.layer.redraw();
      });

      widthRow.appendChild(widthLabel);
      widthRow.appendChild(widthInput);

      settings.appendChild(colorRow);
      settings.appendChild(widthRow);

      // Toggle settings visibility
      gearIcon.addEventListener("click", () => {
        settings.style.display =
          settings.style.display === "none" ? "block" : "none";
      });

      // Assemble item
      item.appendChild(topRow);
      item.appendChild(settings);
      overlayList.appendChild(item);
    });
  }
})();