WME GeoPortal Overlay DACH Beta

Waze Geoportal Overlay für Deutschland, Österreich und Schweiz

// ==UserScript==
// @name         WME GeoPortal Overlay DACH Beta
// @namespace    https://greasyfork.org/de/users/863740-horst-wittlich
// @version      2024.07.13
// @description  Waze Geoportal Overlay für Deutschland, Österreich und Schweiz
// @author       vertexcode, hiwi234, SaiCode
// @include      /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @grant        GM_xmlhttpRequest
// @grant        GM_info
// @grant        GM_addStyle
// @license      MIT

// ==/UserScript==

/*global I18n, $, W, OpenLayers*/

/* globals OpenLayers: true */
// Versions Format
// yyyy.mm.dd

DEFAULT_SOURCES = {
  de: {
    name: "GeoOverlays DE",
    flag: "🇩🇪",
    enabled: true,
    layers: [
      {
        name: "Basemap DE",
        enabled: true,
        active: false,
        unique: "__DrawBasemapDE",
        type: "WMTS",
        source:
          "https://sgx.geodatenzentrum.de/wmts_basemapde/1.0.0/WMTSCapabilities.xml",
        layerName: "de_basemapde_web_raster_farbe",
        matrixSet: "GLOBAL_WEBMERCATOR",
      },
      {
        name: "GeoDatenZentrum DE",
        enabled: true,
        active: false,
        unique: "__DrawGeoPortalDE",
        type: "WMTS",
        source:
          "https://sgx.geodatenzentrum.de/wmts_topplus_open/1.0.0/WMTSCapabilities.xml",
        layerName: "web",
        matrixSet: "WEBMERCATOR",
      },
      {
        name: "GeoPortal BW",
        enabled: true,
        active: false,
        unique: "__DrawGeoPortalBW",
        type: "WMTS",
        source:
          "https://owsproxy.lgl-bw.de/owsproxy/ows/WMTS_LGL-BW_Basiskarte?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities&user=ZentrKomp&password=viewerprod",
        layerName: "Basiskarte",
        matrixSet: "GoogleMapsCompatible",
      },
      {
        name: "GeoPortal NRW",
        enabled: true,
        active: false,
        unique: "__DrawGeoPortalNRW",
        type: "WMTS",
        source:
          "https://www.wmts.nrw.de/geobasis/wmts_nw_dtk/1.0.0/WMTSCapabilities.xml",
        layerName: "nw_dtk_col",
        matrixSet: "EPSG_3857_16",
      },
      {
        name: "GeoPortal NRW Overlay",
        enabled: true,
        active: false,
        unique: "__DrawGeoPortalNRWOverlay",
        type: "WMTS",
        source:
          "https://www.wmts.nrw.de/geobasis/wmts_nw_dop_overlay/1.0.0/WMTSCapabilities.xml",
        layerName: "nw_dop_overlay",
        matrixSet: "EPSG_3857_16",
        opacity: 1,
      },
      {
        name: "GeoPortal BY",
        enabled: true,
        active: false,
        unique: "__DrawGeoPortalBY",
        type: "WMTS",
        source:
          "https://geoservices.bayern.de/od/wmts/geobasis/v1/1.0.0/WMTSCapabilities.xml",
        layerName: "by_webkarte",
        matrixSet: "smerc",
      },
    ],
  },
  at: {
    name: "GeoOverlays AT",
    flag: "🇦🇹",
    enabled: true,
    layers: [
      {
        name: "Basemap AT",
        enabled: true,
        active: false,
        unique: "__DrawBasemapAT",
        type: "WMTS",
        source:
          "https://mapsneu.wien.gv.at/basemapneu/1.0.0/WMTSCapabilities.xml",
        layerName: "geolandbasemap",
        matrixSet: "google3857",
      },
      {
        name: "Overlay AT",
        enabled: true,
        active: false,
        unique: "__DrawOverlayAT",
        type: "WMTS",
        source: "https://www.basemap.at/wmts/1.0.0/WMTSCapabilities.xml",
        layerName: "bmapoverlay",
        matrixSet: "google3857",
      },
    ],
  },
  ch: {
    name: "GeoOverlays CH",
    flag: "🇨🇭",
    enabled: true,
    layers: [
      {
        name: "Strassenkarte",
        enabled: true,
        active: false,
        unique: "__DrawSwissTopoStrassenkarte",
        type: "WMTS",
        source:
          "https://wmts.geo.admin.ch/EPSG/3857/1.0.0/WMTSCapabilities.xml",
        layerName: "ch.swisstopo.swisstne-base",
        matrixSet: "3857_18",
      },
      {
        name: "Basisnetz",
        enabled: true,
        active: false,
        unique: "__DrawSwissBasisnetz",
        type: "WMTS",
        source:
          "https://wmts.geo.admin.ch/EPSG/3857/1.0.0/WMTSCapabilities.xml",
        layerName: "ch.swisstopo.swisstlm3d-strassen",
        matrixSet: "3857_18",
      },
      {
        name: "Luftbild",
        enabled: true,
        active: false,
        unique: "__DrawSwissTopoLuftbild",
        type: "WMTS",
        source:
          "https://wmts.geo.admin.ch/EPSG/3857/1.0.0/WMTSCapabilities.xml",
        layerName: "ch.swisstopo.swissimage-product",
        matrixSet: "3857_20",
      },
    ],
  },
};

(() => {
  let uOpenLayers;
  let uWaze;

  let shiftPressed = false;

  let opacity = localStorage.getItem("geoportal_opacity") || 0.5;

  if (isNaN(opacity)) {
    opacity = 0.5;
  }

  //check if opacity is in a valid range
  if (opacity < 0 || opacity > 1) {
    opacity = 0.5;
  }

  // Define WMTS Layers


  const layersList = [];

  let sources = loadSettings();

  /**
   * Define WMTS/WMS Layers. Hide this function for better readability
   */
  function geoportal_init() {
    const layerControl = $(".layer-switcher").find(".list-unstyled.togglers");
    if (layerControl.length) {
      console.info(`Loading Geoportal Layers...`);
      $.each(sources, function (key, country) {
        const controlEntry = `
        <li class="group layer-toggle-${country.flag}" style="${
          country.enabled ? "" : "display: none;"
        }">
          <div class="layer-switcher-toggler-tree-category">
            <wz-button class="expand-country-${key}" color="clear-icon" size="xs">
              <i class="toggle-category w-icon w-icon-caret-down"></i>
            </wz-button>
            <label class="label-text" for="layer-switcher-group_overlay">
              ${country.name}
            </label>
          </div>
          <ul class="collapsible-GROUP_OVERLAY ${key}"></ul>
        </li>
        `;
        // append the control entry as the second last entry
        layerControl.children().eq(-1).before(controlEntry);
        //make the caret clickable
        $(`.expand-country-${key}`).on("click", function () {
          $(`.collapsible-GROUP_OVERLAY.${key}`).toggle();
          //rotate the caret transform: rotate(90deg);
          if ($(this).find("i").css("transform") === "none") {
            $(this).find("i").css("transform", "rotate(-90deg)");
          } else {
            $(this).find("i").css("transform", "");
          }
        });
        country_init(key, country.layers, country.flag);
      });
    }
  }

  /**
   * Initialize the country layers
   * @param {string} country
   * @param {Array} layers
   * @param {string} flag
   */
  function country_init(country, layers, flag) {
    let overlayGroup = $(`ul.collapsible-GROUP_OVERLAY.${country}`);
    $.each(layers, function (index, source) {
      // Make and add layer
      GM.xmlHttpRequest({
        method: "GET",
        url: source.source,
        onload: (response) => {
          var responseXML = response.responseXML;
          // Inject responseXML into existing Object (only appropriate for XML content).
          if (!response.responseXML) {
            responseXML = new DOMParser().parseFromString(
              response.responseText,
              "text/xml"
            );
          }
          //if responseXML is not a XML document, cancel the loading
          if (!responseXML || responseXML instanceof XMLDocument === false) {
            console.error(
              `Failed to load ${flag} Layer ${index + 1}/${layers.length}: ${
                source.name
              }`
            );
            return;
          }

          //chec if WMST or WMTS and load the correct format
          if (source.type === "WMTS") {
            var format = new OpenLayers.Format.WMTSCapabilities();
            var doc = responseXML;
            var capabilities = format.read(doc);
            layersList[source.unique] = format.createLayer(capabilities, {
              layer: source.layerName,
              matrixSet: source.matrixSet,
              opacity: source.opacity ?? opacity,
              isBaseLayer: false,
              requestEncoding: source.requestEncoding ?? "REST",
              visibility: source.active,
            });
          } else if (source.type === "WMS") {
            var format = new OpenLayers.Format.WMSCapabilities();
            var doc = responseXML;
            var capabilities = format.read(doc);

            // Find the specific layer by its name
            var wmsLayer = capabilities.capability.layers.find(
              (layer) => layer.name === source.layerName
            );

            if (wmsLayer) {
              console.log(wmsLayer);
              layersList[source.unique] = new OpenLayers.Layer.Tile({
                source: new OpenLayers.Source.TileWMS({
                  url: wmsLayer.url,
                  params: {
                    LAYERS: wmsLayer.name,
                    FORMAT: "image/png",
                  },
                }),
                name: source.name,
                opacity: source.opacity ?? opacity,
                isBaseLayer: false,
                visibility: false,
              });
              JSON.stringify(result, null, 2);
              console.log(wmsLayer.url);
            } else {
              console.error(
                `Layer ${source.layerName} not found in WMS capabilities for ${flag} ${source.name}`
              );
              return;
            }
          }

          uWaze.map.addLayer(layersList[source.unique]);
          uWaze.map.setLayerIndex(layersList[source.unique], 3);

          //check for errors
          if (!layersList[source.unique]) {
            console.error(
              `Failed to load ${flag} Layer ${index + 1}/${layers.length}: ${
                source.name
              }`
            );
            return;
          }
          if (!layersList[source.unique].url.length) {
            console.error(
              `Failed to load ${flag} Layer ${index + 1}/${layers.length}: ${
                source.name
              }. No URL found in capabilities.`
            );
            console.log(layersList[source.unique]);
            return;
          }

          console.debug(layersList[source.unique].url);

          // Make checkbox and add to the section
          let toggleEntry = $("<li></li>");
          let checkbox = $("<wz-checkbox></wz-checkbox>", {
            id: source.unique,
            class: "hydrated",
            checked: layersList[source.unique].getVisibility(),
            text: source.name,
          });

          toggleEntry.append(checkbox).toggle(source.enabled);
          overlayGroup.append(toggleEntry);

          checkbox.on("click", function (e) {
            layersList[source.unique].setVisibility(e.target.checked);
            sources[country].layers[index].active = e.target.checked;
            saveSettings();
          });

          console.log(
            `${flag} Layer ${index + 1}/${layers.length}: ${source.name} loaded`
          );
        },
        onerror: (response) => {
          console.error(
            `Failed to load ${flag} Layer ${index + 1}/${layers.length}: ${
              source.name
            }`
          );
        },
        ontimeout: (response) => {
          console.error(
            `Request to ${flag} Layer ${index + 1}/${layers.length}: ${
              source.name
            } timed out`
          );
        },
      });
    });
  }

  /**
   * Initialize the UI
   * @returns {void}
   */
  function ui_init() {
    //Add a Opacity control to the map controls
    const controlContainer = $(".overlay-buttons-container.bottom").first();
    //check if the opacity control already exists
    if (controlContainer.find(".opacity-control-container").length) {
      return;
    }

    const opacityControl = $(`
    <div class="opacity-control-container">
    <wz-basic-tooltip class="sc-wz-basic-tooltip-h sc-wz-basic-tooltip-s">
        <wz-tooltip class="sc-wz-basic-tooltip sc-wz-basic-tooltip-s">
            <wz-tooltip-source class="sc-wz-tooltip-source-h sc-wz-tooltip-source-s">
                <wz-button color="clear-icon" class="opacity-button opacity-plus">
                  <wz-tooltip-target class="sc-wz-tooltip-target-h sc-wz-tooltip-target-s">
                  </wz-tooltip-target><i class="w-icon w-icon-eye-fill"></i>
                </wz-button>
            </wz-tooltip-source>
            <wz-tooltip-content position="left">
                <span> Increase Opacity </span>
            </wz-tooltip-content>
        </wz-tooltip>
    </wz-basic-tooltip>
    <wz-basic-tooltip class="sc-wz-basic-tooltip-h sc-wz-basic-tooltip-s">
        <wz-tooltip class="sc-wz-basic-tooltip sc-wz-basic-tooltip-s">
            <wz-tooltip-source class="sc-wz-tooltip-source-h sc-wz-tooltip-source-s">
                <wz-button color="clear-icon" disabled="false" class="opacity-button opacity-minus">
                  <wz-tooltip-target class="sc-wz-tooltip-target-h sc-wz-tooltip-target-s">
                  </wz-tooltip-target><i class="w-icon w-icon-eye2"></i>
                </wz-button>
            </wz-tooltip-source>
            <wz-tooltip-content position="left">
                <span> Decrease Opacity </span>
            </wz-tooltip-content>
        </wz-tooltip>
    </wz-basic-tooltip>
    </div>
    `);
    controlContainer.append(opacityControl);

    //check if control is pressed an update the value
    $(document).on("keydown", function (e) {
      if (e.key == "Shift") {
        shiftPressed = true;
      }
    });

    $(document).on("keyup", function (e) {
      if (e.key == "Shift") {
        shiftPressed = false;
      }
    });

    // Add event listeners to the opacity control
    $(".opacity-plus").on("click", function () {
      if (shiftPressed) {
        opacity = 1;
      } else {
        opacity = Math.min(opacity + 0.1, 1);
      }
      localStorage.setItem("geoportal_opacity", opacity);

      $.each(sources, function (index, source) {
        source.layers.forEach((source) => {
          try {
            layersList[source.unique].setOpacity(opacity);
          } catch (e) {
            console.error(`Failed to set opacity for ${source.name}`);
            return;
          }
        });
      });
    });

    $(".opacity-minus").on("click", function () {
      if (shiftPressed) {
        opacity = 0;
      } else {
        opacity = Math.max(opacity - 0.1, 0);
      }
      localStorage.setItem("geoportal_opacity", opacity);
      $.each(sources, function (index, source) {
        source.layers.forEach((source) => {
          try {
            layersList[source.unique].setOpacity(opacity);
          } catch (e) {
            console.error(`Failed to set opacity for ${source.name}`);
            return;
          }
        });
      });
    });

    //check every 10 seconds if the opacity control is still there
    setInterval(() => {
      if (
        !$(".overlay-buttons-container.bottom")
          .first()
          .find(".opacity-control-container").length
      ) {
        console.log("Opacity control not found, re-adding...");
        $(".overlay-buttons-container.bottom").first().append(opacityControl);
      }
    }, 10000);

    //add a mutation observer to check if the opacity control is removed
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (!mutation.addedNodes.length) {
          return;
        }
        mutation.addedNodes.forEach((node) => {
          if (node.id === "overlay-buttons") {
            $(".overlay-buttons-container.bottom")
              .first()
              .append(opacityControl);
          }
        });
      });
    });

    observer.observe($("#overlay-buttons-region").get(0), {
      childList: true,
    });
  }

  /**
   * Initialize the settings
   */
  async function settings_init() {
    const { tabLabel, tabPane } =
      W.userscripts.registerSidebarTab("geoportal-dach");

    tabLabel.innerText = "🌍 Geoportal";
    tabLabel.title = "Geoportal DACH";

    tabPane.innerHTML = `
  <div class="geoportal-settings">
    <h1>Settings</h1>
    <div class="geoportal-countrys">
      <div v-for="country in countries" :key="country">
        <input type="checkbox" :id="country" v-model="layers[country].enabled" @change="updateCountry" />
        <label :for="country" class="countyname">{{ layers[country].flag }} {{ layers[country].name }}</label>
        <ul class="geoportal-layers" v-if="layers[country].enabled">
          <li v-for="layer in layers[country].layers" :key="layer.unique">
            <input type="checkbox" :id="layer.unique" v-model="layer.enabled" @change="updateLayer" />
            <label :for="layer.unique">{{ layer.name }}</label>
          </li>
        </ul>
    </div>
  </div>
  `;

    await W.userscripts.waitForElementConnected(tabPane);

    //check if Vue.js is already loaded
    while (typeof Vue === "undefined") {
      await new Promise((resolve) => setTimeout(resolve, 500));
    }
    //initialize Vue.js
    const { createApp, ref } = Vue;
    createApp({
      setup() {
        const countries = ref(Object.keys(sources));
        const layers = ref(sources);

        return { countries, layers };
      },
      methods: {
        updateCountry(event) {
          //hide the layers if the country is disabled
          if (!event.target.checked) {
            sources[event.target.id].layers.forEach((layer) => {
              layersList[layer.unique].setVisibility(false);
            });
          } else {
            sources[event.target.id].layers.forEach((layer) => {
              layersList[layer.unique].setVisibility(layer.active);
            });
          }
          $(`.layer-toggle-${this.layers[event.target.id].flag}`).toggle(
            event.target.checked
          );
          saveSettings();
        },
        updateLayer(event) {
          layersList[event.target.id].setVisibility(event.target.checked);
          $(`li wz-checkbox#${event.target.id}`).toggle(
            event.target.checked
          ).prop("checked", event.target.checked);
          saveSettings();
        },
      },
    }).mount(tabPane);
  }

  /**
   * Load the settings from localStorage
   * @returns {Object} sources
   */
  function loadSettings() {
    try {
      const savedSources = JSON.parse(
        localStorage.getItem("geoportal_sources")
      );

      if (!savedSources || typeof savedSources !== "object") {
        return DEFAULT_SOURCES;
      }

      // Check if each key in savedSources matches the keys in DEFAULT_SOURCES
      for (const key in savedSources) {
        const savedSource = savedSources[key];

        // Check if the structure of savedSource matches the structure of defaultSource
        if (
          typeof savedSource.name !== "string" ||
          typeof savedSource.flag !== "string" ||
          !Array.isArray(savedSource.layers)
        ) {
          throw new Error("Invalid country format");
        }
        savedSource.enabled = savedSource.enabled ?? true;

        for (const layer of savedSource.layers) {
          if (
            typeof layer.name !== "string" ||
            typeof layer.unique !== "string" ||
            typeof layer.type !== "string" ||
            typeof layer.source !== "string" ||
            typeof layer.layerName !== "string" ||
            typeof layer.matrixSet !== "string"
          ) {
            throw new Error("Invalid layer format");
          }
          layer.enabled = layer.enabled ?? true;
          layer.active = layer.active ?? false;
        }
      }
      // If all checks passed, use savedSources
      return savedSources;
    } catch (error) {
      // If any check fails, log the error and use DEFAULT_SOURCES
      console.error(error.message);
      return DEFAULT_SOURCES;
    }
  }

  /**
   * Save the settings to localStorage
   */
  async function saveSettings() {
    localStorage.setItem("geoportal_sources", JSON.stringify(sources));
  }

  /**
   * Bootstrap the Geoportal Overlays
   */
  function geoportal_bootstrap() {
    uWaze = unsafeWindow.W;
    uOpenLayers = unsafeWindow.OpenLayers;
    if (
      !uOpenLayers ||
      !uWaze ||
      !uWaze.map ||
      !document.querySelector(".list-unstyled.togglers .group")
    ) {
      setTimeout(geoportal_bootstrap, 500);
    } else {
      console.log("Loading Geoportal Maps...");
      settings_init();
      geoportal_init();
      ui_init();
    }
  }

  /**
   * Patch OpenLayers to fix missing features
   * @returns {void}
   */
  async function patchOpenLayers() {
    console.groupCollapsed("WME Geometries: Patching missing features...");
    if (!OpenLayers.VERSION_NUMBER.match(/^Release [0-9.]*$/)) {
      console.error(
        "WME Geometries: OpenLayers version mismatch (" +
          OpenLayers.VERSION_NUMBER +
          ") - cannot apply patch"
      );
      return;
    }
    loadOLScript("lib/OpenLayers/Format/XML");
    loadOLScript("lib/OpenLayers/Format/XML/VersionedOGC");
    loadOLScript("lib/OpenLayers/Layer/WMTS");
    loadOLScript("lib/OpenLayers/Layer/Tile");
    loadOLScript("lib/OpenLayers/Format/OWSCommon");
    loadOLScript("lib/OpenLayers/Format/OWSCommon/v1");
    loadOLScript("lib/OpenLayers/Format/OWSCommon/v1_1_0");
    loadOLScript("lib/OpenLayers/Format/WMSCapabilities");
    loadOLScript("lib/OpenLayers/Format/WMSCapabilities/v1");
    loadOLScript("lib/OpenLayers/Format/WMSCapabilities/v1_3");
    loadOLScript("lib/OpenLayers/Format/WMSCapabilities/v1_3_0");
    loadOLScript("lib/OpenLayers/Format/WMTSCapabilities");
    loadOLScript("lib/OpenLayers/Format/WMTSCapabilities/v1_0_0");

    console.groupEnd();
  }

  /**
   * Load OpenLayers script from CDN
   * @param {string} filename
   */
  function loadOLScript(filename) {
    var version = OpenLayers.VERSION_NUMBER.replace(/Release /, "");
    console.info("Loading openlayers/" + version + "/" + filename + ".js");

    var openlayers = document.createElement("script");
    openlayers.src =
      "https://cdnjs.cloudflare.com/ajax/libs/openlayers/" +
      version +
      "/" +
      filename +
      ".js";
    openlayers.type = "text/javascript";
    openlayers.async = false;
    document.head.appendChild(openlayers);
  }

  function loadVueJS() {
    //check if Vue.js is already loaded
    if (typeof Vue !== "undefined") {
      return;
    }
    console.log("Loading Vue.js");
    var vuejs = document.createElement("script");
    vuejs.src = "https://unpkg.com/vue@3/dist/vue.global.js";
    document.head.appendChild(vuejs);
  }

  loadVueJS();
  patchOpenLayers();
  geoportal_bootstrap();
})();

GM_addStyle(`

  .opacity-control-container {
    align-items: center;
    background: var(--background_default);
    border-radius: 100px;
    display: flex;
    flex-direction: column;
    gap: 3px;
  }

  .overlay-buttons-container.bottom {
    bottom: 42px;
  }

  .geoportal-settings {
    padding: 10px;
    user-select: none;
  }

  .geoportal-settings label {
    margin-left: 6px;
  }

`);