WME GPX/KML/WKT/GML/GeoJSON Overlay

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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