Greasy Fork is available in English.

Waze GeoPortal DE

Geoportal Overlay für Deutschland

// ==UserScript==
// @name         Waze GeoPortal DE
// @namespace    https://greasyfork.org/de/users/863740-horst-wittlich
// @version      2024.06.13
// @description  Geoportal Overlay für Deutschland
// @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==

// Versions Format
// yyyy.mm.dd

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

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

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

  // Define WMTS Layers
  const sources = [
    {
      name: "Basemap DE",
      unique: "__DrawBasemapDE",
      id: "layer-switcher-basemap-de",
      source:
        "https://sgx.geodatenzentrum.de/wmts_basemapde/1.0.0/WMTSCapabilities.xml",
      layerName: "de_basemapde_web_raster_farbe",
      matrixSet: "GLOBAL_WEBMERCATOR",
      requestEncoding: "REST",
    },
    {
      name: "GeoDatenZentrum DE",
      unique: "__DrawGeoPortalDE",
      id: "layer-switcher-geoportal-de",
      source:
        "https://sgx.geodatenzentrum.de/wmts_topplus_open/1.0.0/WMTSCapabilities.xml",
      layerName: "web",
      matrixSet: "WEBMERCATOR",
      requestEncoding: "REST",
    },
    {
      name: "GeoPortal BW",
      unique: "__DrawGeoPortalBW",
      id: "layer-switcher-geoportal-bw",
      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",
      unique: "__DrawGeoPortalNRW",
      id: "layer-switcher-geoportal-nrw",
      source:
        "https://www.wmts.nrw.de/geobasis/wmts_nw_dtk/1.0.0/WMTSCapabilities.xml",
      layerName: "nw_dtk_col",
      matrixSet: "EPSG_3857_16",
      requestEncoding: "REST",
    },
    {
      name: "GeoPortal NRW Overlay",
      unique: "__DrawGeoPortalNRWOverlay",
      id: "layer-switcher-geoportal-nrw-overlay",
      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,
      requestEncoding: "REST",
    },
    {
      name: "GeoPortal BY",
      unique: "__DrawGeoPortalBY",
      id: "layer-switcher-geoportal-by",
      source:
        "https://geoservices.bayern.de/od/wmts/geobasis/v1/1.0.0/WMTSCapabilities.xml",
      layerName: "by_webkarte",
      matrixSet: "smerc",
      requestEncoding: "REST",
    },
  ];

  // Define WMTS Layers. Hide this function for better readability
  function loadWMTSLayer() {
    if (uOpenLayers.Layer.WMTS) {
      return;
    }
    /**
     * Class: OpenLayers.Format.XML.VersionedOGC
     * Base class for versioned formats, i.e. a format which supports multiple
     * versions.
     *
     * To enable checking if parsing succeeded, you will need to define a property
     * called errorProperty on the parser you want to check. The parser will then
     * check the returned object to see if that property is present. If it is, it
     * assumes the parsing was successful. If it is not present (or is null), it will
     * pass the document through an OGCExceptionReport parser.
     *
     * If errorProperty is undefined for the parser, this error checking mechanism
     * will be disabled.
     *
     *
     *
     * Inherits from:
     *  - <OpenLayers.Format.XML>
     */
    uOpenLayers.Format.XML.VersionedOGC = uOpenLayers.Class(
      uOpenLayers.Format.XML,
      {
        /**
         * APIProperty: defaultVersion
         * {String} Version number to assume if none found.
         */
        defaultVersion: null,

        /**
         * APIProperty: version
         * {String} Specify a version string if one is known.
         */
        version: null,

        /**
         * APIProperty: profile
         * {String} If provided, use a custom profile.
         */
        profile: null,

        /**
         * APIProperty: allowFallback
         * {Boolean} If a profiled parser cannot be found for the returned version,
         * use a non-profiled parser as the fallback. Application code using this
         * should take into account that the return object structure might be
         * missing the specifics of the profile. Defaults to false.
         */
        allowFallback: false,

        /**
         * Property: name
         * {String} The name of this parser, this is the part of the CLASS_NAME
         * except for "OpenLayers.Format."
         */
        name: null,

        /**
         * APIProperty: stringifyOutput
         * {Boolean} If true, write will return a string otherwise a DOMElement.
         * Default is false.
         */
        stringifyOutput: false,

        /**
         * Property: parser
         * {Object} Instance of the versioned parser.  Cached for multiple read and
         *     write calls of the same version.
         */
        parser: null,

        /**
         * Constructor: OpenLayers.Format.XML.VersionedOGC.
         * Constructor.
         *
         * Parameters:
         * options - {Object} Optional object whose properties will be set on
         *     the object.
         */
        initialize: function (options) {
          uOpenLayers.Format.XML.prototype.initialize.apply(this, [options]);
          var className = this.CLASS_NAME;
          this.name = className.substring(className.lastIndexOf(".") + 1);
        },

        /**
         * Method: getVersion
         * Returns the version to use. Subclasses can override this function
         * if a different version detection is needed.
         *
         * Parameters:
         * root - {DOMElement}
         * options - {Object} Optional configuration object.
         *
         * Returns:
         * {String} The version to use.
         */
        getVersion: function (root, options) {
          var version;
          // read
          if (root) {
            version = this.version;
            if (!version) {
              version = root.getAttribute("version");
              if (!version) {
                version = this.defaultVersion;
              }
            }
          } else {
            // write
            version =
              (options && options.version) ||
              this.version ||
              this.defaultVersion;
          }
          return version;
        },

        /**
         * Method: getParser
         * Get an instance of the cached parser if available, otherwise create one.
         *
         * Parameters:
         * version - {String}
         *
         * Returns:
         * {<OpenLayers.Format>}
         */
        getParser: function (version) {
          version = version || this.defaultVersion;
          var profile = this.profile ? "_" + this.profile : "";
          if (!this.parser || this.parser.VERSION != version) {
            var format =
              uOpenLayers.Format[this.name][
                "v" + version.replace(/\./g, "_") + profile
              ];
            if (!format) {
              if (profile !== "" && this.allowFallback) {
                // fallback to the non-profiled version of the parser
                profile = "";
                format =
                  uOpenLayers.Format[this.name][
                    "v" + version.replace(/\./g, "_")
                  ];
              }
              if (!format) {
                throw (
                  "Can't find a " +
                  this.name +
                  " parser for version " +
                  version +
                  profile
                );
              }
            }
            this.parser = new format(this.options);
          }
          return this.parser;
        },

        /**
         * APIMethod: write
         * Write a document.
         *
         * Parameters:
         * obj - {Object} An object representing the document.
         * options - {Object} Optional configuration object.
         *
         * Returns:
         * {String} The document as a string
         */
        write: function (obj, options) {
          var version = this.getVersion(null, options);
          this.parser = this.getParser(version);
          var root = this.parser.write(obj, options);
          if (this.stringifyOutput === false) {
            return root;
          } else {
            return uOpenLayers.Format.XML.prototype.write.apply(this, [root]);
          }
        },

        /**
         * APIMethod: read
         * Read a doc and return an object representing the document.
         *
         * Parameters:
         * data - {String | DOMElement} Data to read.
         * options - {Object} Options for the reader.
         *
         * Returns:
         * {Object} An object representing the document.
         */
        read: function (data, options) {
          if (typeof data == "string") {
            data = uOpenLayers.Format.XML.prototype.read.apply(this, [data]);
          }
          var root = data.documentElement;
          var version = this.getVersion(root);
          this.parser = this.getParser(version); // Select the parser
          var obj = this.parser.read(data, options); // Parse the data

          var errorProperty = this.parser.errorProperty || null;
          if (errorProperty !== null && obj[errorProperty] === undefined) {
            // an error must have happened, so parse it and report back
            var format = new uOpenLayers.Format.OGCExceptionReport();
            obj.error = format.read(data);
          }
          obj.version = version;
          return obj;
        },

        CLASS_NAME: "OpenLayers.Format.XML.VersionedOGC",
      }
    );

    /**
     * Class: OpenLayers.Format.WMTSCapabilities
     * Read WMTS Capabilities.
     *
     * Inherits from:
     *  - <OpenLayers.Format.XML.VersionedOGC>
     */
    uOpenLayers.Format.WMTSCapabilities = uOpenLayers.Class(
      uOpenLayers.Format.XML.VersionedOGC,
      {
        /**
         * APIProperty: defaultVersion
         * {String} Version number to assume if none found.  Default is "1.0.0".
         */
        defaultVersion: "1.0.0",

        /**
         * APIProperty: yx
         * {Object} Members in the yx object are used to determine if a CRS URN
         *     corresponds to a CRS with y,x axis order.  Member names are CRS URNs
         *     and values are boolean.  By default, the following CRS URN are
         *     assumed to correspond to a CRS with y,x axis order:
         *
         * * urn:ogc:def:crs:EPSG::4326
         */
        yx: {
          "urn:ogc:def:crs:EPSG::4326": true,
        },

        /**
         * Constructor: OpenLayers.Format.WMTSCapabilities
         * Create a new parser for WMTS capabilities.
         *
         * Parameters:
         * options - {Object} An optional object whose properties will be set on
         *     this instance.
         */

        /**
         * APIMethod: read
         * Read capabilities data from a string, and return information about
         * the service (offering and observedProperty mostly).
         *
         * Parameters:
         * data - {String} or {DOMElement} data to read/parse.
         *
         * Returns:
         * {Object} Info about the WMTS Capabilities
         */

        /**
         * APIMethod: createLayer
         * Create a WMTS layer given a capabilities object.
         *
         * Parameters:
         * capabilities - {Object} The object returned from a <read> call to this
         *     format.
         * config - {Object} Configuration properties for the layer.  Defaults for
         *     the layer will apply if not provided.
         *
         * Required config properties:
         * layer - {String} The layer identifier.
         *
         * Optional config properties:
         * matrixSet - {String} The matrix set identifier, required if there is
         *      more than one matrix set in the layer capabilities.
         * style - {String} The name of the style
         * format - {String} Image format for the layer. Default is the first
         *     format returned in the GetCapabilities response.
         * param - {Object} The dimensions values eg: {"Year": "2012"}
         *
         * Returns:
         * {<OpenLayers.Layer.WMTS>} A properly configured WMTS layer.  Throws an
         *     error if an incomplete config is provided.  Returns undefined if no
         *     layer could be created with the provided config.
         */
        createLayer: function (capabilities, config) {
          var layer;

          // confirm required properties are supplied in config
          if (!("layer" in config)) {
            throw new Error("Missing property 'layer' in configuration.");
          }

          var contents = capabilities.contents;

          // find the layer definition with the given identifier
          var layers = contents.layers;
          var layerDef;
          for (var i = 0, ii = contents.layers.length; i < ii; ++i) {
            if (contents.layers[i].identifier === config.layer) {
              layerDef = contents.layers[i];
              break;
            }
          }
          if (!layerDef) {
            console.error(
              "No layer with identifier ",
              config.layer,
              "in",
              contents.layers
            );
            console.log(
              "available layers: ",
              contents.layers.map((l) => l.identifier)
            );
            throw new Error("Layer not found");
          }

          var format = config.format;
          if (!format && layerDef.formats && layerDef.formats.length) {
            format = layerDef.formats[0];
          }

          // find the matrixSet definition
          var matrixSet;
          if (config.matrixSet) {
            matrixSet = contents.tileMatrixSets[config.matrixSet];
          } else if (layerDef.tileMatrixSetLinks.length >= 1) {
            matrixSet =
              contents.tileMatrixSets[
                layerDef.tileMatrixSetLinks[0].tileMatrixSet
              ];
          }
          if (!matrixSet) {
            console.error(
              "No matrix set with identifier ",
              config.matrixSet,
              "in",
              contents.tileMatrixSets
            );
            console.log(
              "available matrix sets: ",
              Object.keys(contents.tileMatrixSets)
            );
            throw new Error("matrixSet not found");
          }

          // get the default style for the layer
          var style;
          for (var i = 0, ii = layerDef.styles.length; i < ii; ++i) {
            style = layerDef.styles[i];
            if (style.isDefault) {
              break;
            }
          }

          var requestEncoding = config.requestEncoding;
          if (!requestEncoding) {
            requestEncoding = "KVP";
            if (capabilities.operationsMetadata.GetTile.dcp.http) {
              var http = capabilities.operationsMetadata.GetTile.dcp.http;
              // Get first get method
              if (http.get[0].constraints) {
                var constraints = http.get[0].constraints;
                var allowedValues = constraints.GetEncoding.allowedValues;

                // The OGC documentation is not clear if we should use
                // REST or RESTful, ArcGis use RESTful,
                // and OpenLayers use REST.
                if (
                  !allowedValues.KVP &&
                  (allowedValues.REST || allowedValues.RESTful)
                ) {
                  requestEncoding = "REST";
                }
              }
            }
          }

          var dimensions = [];
          var params = config.params || {};
          // to don't overwrite the changes in the applyDefaults
          delete config.params;
          for (var id = 0, ld = layerDef.dimensions.length; id < ld; id++) {
            var dimension = layerDef.dimensions[id];
            dimensions.push(dimension.identifier);
            if (!params.hasOwnProperty(dimension.identifier)) {
              params[dimension.identifier] = dimension["default"];
            }
          }

          var projection =
            config.projection ||
            matrixSet.supportedCRS.replace(
              /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/,
              "$1:$3"
            );
          var units =
            config.units || (projection === "EPSG:4326" ? "degrees" : "m");

          var resolutions = [];
          for (var mid in matrixSet.matrixIds) {
            if (matrixSet.matrixIds.hasOwnProperty(mid)) {
              resolutions.push(
                (matrixSet.matrixIds[mid].scaleDenominator * 0.28e-3) /
                  uOpenLayers.METERS_PER_INCH /
                  uOpenLayers.INCHES_PER_UNIT[units]
              );
            }
          }

          var url;
          if (requestEncoding === "REST" && layerDef.resourceUrls) {
            url = [];
            var resourceUrls = layerDef.resourceUrls,
              resourceUrl;
            for (var t = 0, tt = layerDef.resourceUrls.length; t < tt; ++t) {
              resourceUrl = layerDef.resourceUrls[t];
              if (
                resourceUrl.format === format &&
                resourceUrl.resourceType === "tile"
              ) {
                url.push(resourceUrl.template);
              }
            }
          } else {
            var httpGet = capabilities.operationsMetadata.GetTile.dcp.http.get;
            url = [];
            var constraint;
            for (var i = 0, ii = httpGet.length; i < ii; i++) {
              constraint = httpGet[i].constraints;
              if (
                !constraint ||
                (constraint &&
                  constraint.GetEncoding.allowedValues[requestEncoding])
              ) {
                url.push(httpGet[i].url);
              }
            }
          }

          return new uOpenLayers.Layer.WMTS(
            uOpenLayers.Util.applyDefaults(config, {
              url: url,
              requestEncoding: requestEncoding,
              name: layerDef.title,
              style: style.identifier,
              format: format,
              matrixIds: matrixSet.matrixIds,
              matrixSet: matrixSet.identifier,
              projection: projection,
              units: units,
              resolutions:
                config.isBaseLayer === false ? undefined : resolutions,
              serverResolutions: resolutions,
              tileFullExtent: matrixSet.bounds,
              dimensions: dimensions,
              params: params,
            })
          );
        },

        CLASS_NAME: "OpenLayers.Format.WMTSCapabilities",
      }
    );

    /**
     * Class: OpenLayers.Format.OWSCommon
     * Read OWSCommon. Create a new instance with the <OpenLayers.Format.OWSCommon>
     *     constructor.
     *
     * Inherits from:
     *  - <OpenLayers.Format.XML.VersionedOGC>
     */
    uOpenLayers.Format.OWSCommon = uOpenLayers.Class(
      uOpenLayers.Format.XML.VersionedOGC,
      {
        /**
         * APIProperty: defaultVersion
         * {String} Version number to assume if none found.  Default is "1.0.0".
         */
        defaultVersion: "1.0.0",

        /**
         * Constructor: OpenLayers.Format.OWSCommon
         * Create a new parser for OWSCommon.
         *
         * Parameters:
         * options - {Object} An optional object whose properties will be set on
         *     this instance.
         */

        /**
         * Method: getVersion
         * Returns the version to use. Subclasses can override this function
         * if a different version detection is needed.
         *
         * Parameters:
         * root - {DOMElement}
         * options - {Object} Optional configuration object.
         *
         * Returns:
         * {String} The version to use.
         */
        getVersion: function (root, options) {
          var version = this.version;
          if (!version) {
            // remember version does not correspond to the OWS version
            // it corresponds to the WMS/WFS/WCS etc. request version
            var uri = root.getAttribute("xmlns:ows");
            // the above will fail if the namespace prefix is different than
            // ows and if the namespace is declared on a different element
            if (uri && uri.substring(uri.lastIndexOf("/") + 1) === "1.1") {
              version = "1.1.0";
            }
            if (!version) {
              version = this.defaultVersion;
            }
          }
          return version;
        },

        /**
         * APIMethod: read
         * Read an OWSCommon document and return an object.
         *
         * Parameters:
         * data - {String | DOMElement} Data to read.
         * options - {Object} Options for the reader.
         *
         * Returns:
         * {Object} An object representing the structure of the document.
         */

        CLASS_NAME: "OpenLayers.Format.OWSCommon",
      }
    );

    /**
     * Class: OpenLayers.Format.OWSCommon.v1
     * Common readers and writers for OWSCommon v1.X formats
     *
     * Inherits from:
     *  - <OpenLayers.Format.XML>
     */
    uOpenLayers.Format.OWSCommon.v1 = uOpenLayers.Class(
      uOpenLayers.Format.XML,
      {
        /**
         * Property: regExes
         * Compiled regular expressions for manipulating strings.
         */
        regExes: {
          trimSpace: /^\s*|\s*$/g,
          removeSpace: /\s*/g,
          splitSpace: /\s+/,
          trimComma: /\s*,\s*/g,
        },

        /**
         * Method: read
         *
         * Parameters:
         * data - {DOMElement} An OWSCommon document element.
         * options - {Object} Options for the reader.
         *
         * Returns:
         * {Object} An object representing the OWSCommon document.
         */
        read: function (data, options) {
          options = uOpenLayers.Util.applyDefaults(options, this.options);
          var ows = {};
          this.readChildNodes(data, ows);
          return ows;
        },

        /**
         * Property: readers
         * Contains public functions, grouped by namespace prefix, that will
         *     be applied when a namespaced node is found matching the function
         *     name.  The function will be applied in the scope of this parser
         *     with two arguments: the node being read and a context object passed
         *     from the parent.
         */
        readers: {
          ows: {
            Exception: function (node, exceptionReport) {
              var exception = {
                code: node.getAttribute("exceptionCode"),
                locator: node.getAttribute("locator"),
                texts: [],
              };
              exceptionReport.exceptions.push(exception);
              this.readChildNodes(node, exception);
            },
            ExceptionText: function (node, exception) {
              var text = this.getChildValue(node);
              exception.texts.push(text);
            },
            ServiceIdentification: function (node, obj) {
              obj.serviceIdentification = {};
              this.readChildNodes(node, obj.serviceIdentification);
            },
            Title: function (node, obj) {
              obj.title = this.getChildValue(node);
            },
            Abstract: function (node, serviceIdentification) {
              serviceIdentification["abstract"] = this.getChildValue(node);
            },
            Keywords: function (node, serviceIdentification) {
              serviceIdentification.keywords = {};
              this.readChildNodes(node, serviceIdentification.keywords);
            },
            Keyword: function (node, keywords) {
              keywords[this.getChildValue(node)] = true;
            },
            ServiceType: function (node, serviceIdentification) {
              serviceIdentification.serviceType = {
                codeSpace: node.getAttribute("codeSpace"),
                value: this.getChildValue(node),
              };
            },
            ServiceTypeVersion: function (node, serviceIdentification) {
              serviceIdentification.serviceTypeVersion =
                this.getChildValue(node);
            },
            Fees: function (node, serviceIdentification) {
              serviceIdentification.fees = this.getChildValue(node);
            },
            AccessConstraints: function (node, serviceIdentification) {
              serviceIdentification.accessConstraints =
                this.getChildValue(node);
            },
            ServiceProvider: function (node, obj) {
              obj.serviceProvider = {};
              this.readChildNodes(node, obj.serviceProvider);
            },
            ProviderName: function (node, serviceProvider) {
              serviceProvider.providerName = this.getChildValue(node);
            },
            ProviderSite: function (node, serviceProvider) {
              serviceProvider.providerSite = this.getAttributeNS(
                node,
                this.namespaces.xlink,
                "href"
              );
            },
            ServiceContact: function (node, serviceProvider) {
              serviceProvider.serviceContact = {};
              this.readChildNodes(node, serviceProvider.serviceContact);
            },
            IndividualName: function (node, serviceContact) {
              serviceContact.individualName = this.getChildValue(node);
            },
            PositionName: function (node, serviceContact) {
              serviceContact.positionName = this.getChildValue(node);
            },
            ContactInfo: function (node, serviceContact) {
              serviceContact.contactInfo = {};
              this.readChildNodes(node, serviceContact.contactInfo);
            },
            Phone: function (node, contactInfo) {
              contactInfo.phone = {};
              this.readChildNodes(node, contactInfo.phone);
            },
            Voice: function (node, phone) {
              phone.voice = this.getChildValue(node);
            },
            Address: function (node, contactInfo) {
              contactInfo.address = {};
              this.readChildNodes(node, contactInfo.address);
            },
            DeliveryPoint: function (node, address) {
              address.deliveryPoint = this.getChildValue(node);
            },
            City: function (node, address) {
              address.city = this.getChildValue(node);
            },
            AdministrativeArea: function (node, address) {
              address.administrativeArea = this.getChildValue(node);
            },
            PostalCode: function (node, address) {
              address.postalCode = this.getChildValue(node);
            },
            Country: function (node, address) {
              address.country = this.getChildValue(node);
            },
            ElectronicMailAddress: function (node, address) {
              address.electronicMailAddress = this.getChildValue(node);
            },
            Role: function (node, serviceContact) {
              serviceContact.role = this.getChildValue(node);
            },
            OperationsMetadata: function (node, obj) {
              obj.operationsMetadata = {};
              this.readChildNodes(node, obj.operationsMetadata);
            },
            Operation: function (node, operationsMetadata) {
              var name = node.getAttribute("name");
              operationsMetadata[name] = {};
              this.readChildNodes(node, operationsMetadata[name]);
            },
            DCP: function (node, operation) {
              operation.dcp = {};
              this.readChildNodes(node, operation.dcp);
            },
            HTTP: function (node, dcp) {
              dcp.http = {};
              this.readChildNodes(node, dcp.http);
            },
            Get: function (node, http) {
              if (!http.get) {
                http.get = [];
              }
              var obj = {
                url: this.getAttributeNS(node, this.namespaces.xlink, "href"),
              };
              this.readChildNodes(node, obj);
              http.get.push(obj);
            },
            Post: function (node, http) {
              if (!http.post) {
                http.post = [];
              }
              var obj = {
                url: this.getAttributeNS(node, this.namespaces.xlink, "href"),
              };
              this.readChildNodes(node, obj);
              http.post.push(obj);
            },
            Parameter: function (node, operation) {
              if (!operation.parameters) {
                operation.parameters = {};
              }
              var name = node.getAttribute("name");
              operation.parameters[name] = {};
              this.readChildNodes(node, operation.parameters[name]);
            },
            Constraint: function (node, obj) {
              if (!obj.constraints) {
                obj.constraints = {};
              }
              var name = node.getAttribute("name");
              obj.constraints[name] = {};
              this.readChildNodes(node, obj.constraints[name]);
            },
            Value: function (node, allowedValues) {
              allowedValues[this.getChildValue(node)] = true;
            },
            OutputFormat: function (node, obj) {
              obj.formats.push({ value: this.getChildValue(node) });
              this.readChildNodes(node, obj);
            },
            WGS84BoundingBox: function (node, obj) {
              var boundingBox = {};
              boundingBox.crs = node.getAttribute("crs");
              if (obj.BoundingBox) {
                obj.BoundingBox.push(boundingBox);
              } else {
                obj.projection = boundingBox.crs;
                boundingBox = obj;
              }
              this.readChildNodes(node, boundingBox);
            },
            BoundingBox: function (node, obj) {
              // FIXME: We consider that BoundingBox is the same as WGS84BoundingBox
              // LowerCorner = "min_x min_y"
              // UpperCorner = "max_x max_y"
              // It should normally depend on the projection
              this.readers["ows"]["WGS84BoundingBox"].apply(this, [node, obj]);
            },
            LowerCorner: function (node, obj) {
              var str = this.getChildValue(node).replace(
                this.regExes.trimSpace,
                ""
              );
              str = str.replace(this.regExes.trimComma, ",");
              var pointList = str.split(this.regExes.splitSpace);
              obj.left = pointList[0];
              obj.bottom = pointList[1];
            },
            UpperCorner: function (node, obj) {
              var str = this.getChildValue(node).replace(
                this.regExes.trimSpace,
                ""
              );
              str = str.replace(this.regExes.trimComma, ",");
              var pointList = str.split(this.regExes.splitSpace);
              obj.right = pointList[0];
              obj.top = pointList[1];
              obj.bounds = new uOpenLayers.Bounds(
                obj.left,
                obj.bottom,
                obj.right,
                obj.top
              );
              delete obj.left;
              delete obj.bottom;
              delete obj.right;
              delete obj.top;
            },
            Language: function (node, obj) {
              obj.language = this.getChildValue(node);
            },
          },
        },

        /**
         * Property: writers
         * As a compliment to the readers property, this structure contains public
         *     writing functions grouped by namespace alias and named like the
         *     node names they produce.
         */
        writers: {
          ows: {
            BoundingBox: function (options, nodeName) {
              var node = this.createElementNSPlus(
                nodeName || "ows:BoundingBox",
                {
                  attributes: {
                    crs: options.projection,
                  },
                }
              );
              this.writeNode("ows:LowerCorner", options, node);
              this.writeNode("ows:UpperCorner", options, node);
              return node;
            },
            LowerCorner: function (options) {
              var node = this.createElementNSPlus("ows:LowerCorner", {
                value: options.bounds.left + " " + options.bounds.bottom,
              });
              return node;
            },
            UpperCorner: function (options) {
              var node = this.createElementNSPlus("ows:UpperCorner", {
                value: options.bounds.right + " " + options.bounds.top,
              });
              return node;
            },
            Identifier: function (identifier) {
              var node = this.createElementNSPlus("ows:Identifier", {
                value: identifier,
              });
              return node;
            },
            Title: function (title) {
              var node = this.createElementNSPlus("ows:Title", {
                value: title,
              });
              return node;
            },
            Abstract: function (abstractValue) {
              var node = this.createElementNSPlus("ows:Abstract", {
                value: abstractValue,
              });
              return node;
            },
            OutputFormat: function (format) {
              var node = this.createElementNSPlus("ows:OutputFormat", {
                value: format,
              });
              return node;
            },
          },
        },

        CLASS_NAME: "OpenLayers.Format.OWSCommon.v1",
      }
    );

    /**
     * Class: OpenLayers.Format.OWSCommon.v1_1_0
     * Parser for OWS Common version 1.1.0.
     *
     * Inherits from:
     *  - <OpenLayers.Format.OWSCommon.v1>
     */
    uOpenLayers.Format.OWSCommon.v1_1_0 = uOpenLayers.Class(
      uOpenLayers.Format.OWSCommon.v1,
      {
        /**
         * Property: namespaces
         * {Object} Mapping of namespace aliases to namespace URIs.
         */
        namespaces: {
          ows: "http://www.opengis.net/ows/1.1",
          xlink: "http://www.w3.org/1999/xlink",
        },

        /**
         * Property: readers
         * Contains public functions, grouped by namespace prefix, that will
         *     be applied when a namespaced node is found matching the function
         *     name.  The function will be applied in the scope of this parser
         *     with two arguments: the node being read and a context object passed
         *     from the parent.
         */
        readers: {
          ows: uOpenLayers.Util.applyDefaults(
            {
              ExceptionReport: function (node, obj) {
                obj.exceptionReport = {
                  version: node.getAttribute("version"),
                  language: node.getAttribute("xml:lang"),
                  exceptions: [],
                };
                this.readChildNodes(node, obj.exceptionReport);
              },
              AllowedValues: function (node, parameter) {
                parameter.allowedValues = {};
                this.readChildNodes(node, parameter.allowedValues);
              },
              AnyValue: function (node, parameter) {
                parameter.anyValue = true;
              },
              DataType: function (node, parameter) {
                parameter.dataType = this.getChildValue(node);
              },
              Range: function (node, allowedValues) {
                allowedValues.range = {};
                this.readChildNodes(node, allowedValues.range);
              },
              MinimumValue: function (node, range) {
                range.minValue = this.getChildValue(node);
              },
              MaximumValue: function (node, range) {
                range.maxValue = this.getChildValue(node);
              },
              Identifier: function (node, obj) {
                obj.identifier = this.getChildValue(node);
              },
              SupportedCRS: function (node, obj) {
                obj.supportedCRS = this.getChildValue(node);
              },
            },
            uOpenLayers.Format.OWSCommon.v1.prototype.readers["ows"]
          ),
        },

        /**
         * Property: writers
         * As a compliment to the readers property, this structure contains public
         *     writing functions grouped by namespace alias and named like the
         *     node names they produce.
         */
        writers: {
          ows: uOpenLayers.Util.applyDefaults(
            {
              Range: function (range) {
                var node = this.createElementNSPlus("ows:Range", {
                  attributes: {
                    "ows:rangeClosure": range.closure,
                  },
                });
                this.writeNode("ows:MinimumValue", range.minValue, node);
                this.writeNode("ows:MaximumValue", range.maxValue, node);
                return node;
              },
              MinimumValue: function (minValue) {
                var node = this.createElementNSPlus("ows:MinimumValue", {
                  value: minValue,
                });
                return node;
              },
              MaximumValue: function (maxValue) {
                var node = this.createElementNSPlus("ows:MaximumValue", {
                  value: maxValue,
                });
                return node;
              },
              Value: function (value) {
                var node = this.createElementNSPlus("ows:Value", {
                  value: value,
                });
                return node;
              },
            },
            OpenLayers.Format.OWSCommon.v1.prototype.writers["ows"]
          ),
        },

        CLASS_NAME: "OpenLayers.Format.OWSCommon.v1_1_0",
      }
    );

    /**
     * Class: OpenLayers.Format.WMTSCapabilities.v1_0_0
     * Read WMTS Capabilities version 1.0.0.
     *
     * Inherits from:
     *  - <OpenLayers.Format.WMTSCapabilities>
     */
    uOpenLayers.Format.WMTSCapabilities.v1_0_0 = uOpenLayers.Class(
      uOpenLayers.Format.OWSCommon.v1_1_0,
      {
        /**
         * Property: version
         * {String} The parser version ("1.0.0").
         */
        version: "1.0.0",

        /**
         * Property: namespaces
         * {Object} Mapping of namespace aliases to namespace URIs.
         */
        namespaces: {
          ows: "http://www.opengis.net/ows/1.1",
          wmts: "http://www.opengis.net/wmts/1.0",
          xlink: "http://www.w3.org/1999/xlink",
        },

        /**
         * Property: yx
         * {Object} Members in the yx object are used to determine if a CRS URN
         *     corresponds to a CRS with y,x axis order.  Member names are CRS URNs
         *     and values are boolean.  Defaults come from the
         *     <OpenLayers.Format.WMTSCapabilities> prototype.
         */
        yx: null,

        /**
         * Property: defaultPrefix
         * {String} The default namespace alias for creating element nodes.
         */
        defaultPrefix: "wmts",

        /**
         * Constructor: OpenLayers.Format.WMTSCapabilities.v1_0_0
         * Create a new parser for WMTS capabilities version 1.0.0.
         *
         * Parameters:
         * options - {Object} An optional object whose properties will be set on
         *     this instance.
         */
        initialize: function (options) {
          uOpenLayers.Format.XML.prototype.initialize.apply(this, [options]);
          this.options = options;
          var yx = uOpenLayers.Util.extend(
            {},
            uOpenLayers.Format.WMTSCapabilities.prototype.yx
          );
          this.yx = uOpenLayers.Util.extend(yx, this.yx);
        },

        /**
         * APIMethod: read
         * Read capabilities data from a string, and return info about the WMTS.
         *
         * Parameters:
         * data - {String} or {DOMElement} data to read/parse.
         *
         * Returns:
         * {Object} Information about the SOS service.
         */
        read: function (data) {
          if (typeof data == "string") {
            data = uOpenLayers.Format.XML.prototype.read.apply(this, [data]);
          }
          if (data && data.nodeType == 9) {
            data = data.documentElement;
          }
          var capabilities = {};
          this.readNode(data, capabilities);
          capabilities.version = this.version;
          return capabilities;
        },

        /**
         * Property: readers
         * Contains public functions, grouped by namespace prefix, that will
         *     be applied when a namespaced node is found matching the function
         *     name.  The function will be applied in the scope of this parser
         *     with two arguments: the node being read and a context object passed
         *     from the parent.
         */
        readers: {
          wmts: {
            Capabilities: function (node, obj) {
              this.readChildNodes(node, obj);
            },
            Contents: function (node, obj) {
              obj.contents = {};
              obj.contents.layers = [];
              obj.contents.tileMatrixSets = {};
              this.readChildNodes(node, obj.contents);
            },
            Layer: function (node, obj) {
              var layer = {
                styles: [],
                formats: [],
                dimensions: [],
                tileMatrixSetLinks: [],
              };
              layer.layers = [];
              this.readChildNodes(node, layer);
              obj.layers.push(layer);
            },
            Style: function (node, obj) {
              var style = {};
              style.isDefault = node.getAttribute("isDefault") === "true";
              this.readChildNodes(node, style);
              obj.styles.push(style);
            },
            Format: function (node, obj) {
              obj.formats.push(this.getChildValue(node));
            },
            TileMatrixSetLink: function (node, obj) {
              var tileMatrixSetLink = {};
              this.readChildNodes(node, tileMatrixSetLink);
              obj.tileMatrixSetLinks.push(tileMatrixSetLink);
            },
            TileMatrixSet: function (node, obj) {
              // node could be child of wmts:Contents or wmts:TileMatrixSetLink
              // duck type wmts:Contents by looking for layers
              if (obj.layers) {
                // TileMatrixSet as object type in schema
                var tileMatrixSet = {
                  matrixIds: [],
                };
                this.readChildNodes(node, tileMatrixSet);
                obj.tileMatrixSets[tileMatrixSet.identifier] = tileMatrixSet;
              } else {
                // TileMatrixSet as string type in schema
                obj.tileMatrixSet = this.getChildValue(node);
              }
            },
            TileMatrix: function (node, obj) {
              var tileMatrix = {
                supportedCRS: obj.supportedCRS,
              };
              this.readChildNodes(node, tileMatrix);
              obj.matrixIds.push(tileMatrix);
            },
            ScaleDenominator: function (node, obj) {
              obj.scaleDenominator = parseFloat(this.getChildValue(node));
            },
            TopLeftCorner: function (node, obj) {
              var topLeftCorner = this.getChildValue(node);
              var coords = topLeftCorner.split(" ");
              // decide on axis order for the given CRS
              var yx;
              if (obj.supportedCRS) {
                // extract out version from URN
                var crs = obj.supportedCRS.replace(
                  /urn:ogc:def:crs:(\w+):.+:(\w+)$/,
                  "urn:ogc:def:crs:$1::$2"
                );
                yx = !!this.yx[crs];
              }
              if (yx) {
                obj.topLeftCorner = new OpenLayers.LonLat(coords[1], coords[0]);
              } else {
                obj.topLeftCorner = new OpenLayers.LonLat(coords[0], coords[1]);
              }
            },
            TileWidth: function (node, obj) {
              obj.tileWidth = parseInt(this.getChildValue(node));
            },
            TileHeight: function (node, obj) {
              obj.tileHeight = parseInt(this.getChildValue(node));
            },
            MatrixWidth: function (node, obj) {
              obj.matrixWidth = parseInt(this.getChildValue(node));
            },
            MatrixHeight: function (node, obj) {
              obj.matrixHeight = parseInt(this.getChildValue(node));
            },
            ResourceURL: function (node, obj) {
              obj.resourceUrl = obj.resourceUrl || {};
              var resourceType = node.getAttribute("resourceType");
              if (!obj.resourceUrls) {
                obj.resourceUrls = [];
              }
              var resourceUrl = (obj.resourceUrl[resourceType] = {
                format: node.getAttribute("format"),
                template: node.getAttribute("template"),
                resourceType: resourceType,
              });
              obj.resourceUrls.push(resourceUrl);
            },
            // not used for now, can be added in the future though
            /*"Themes": function(node, obj) {
                obj.themes = [];
                this.readChildNodes(node, obj.themes);
            },
            "Theme": function(node, obj) {
                var theme = {};
                this.readChildNodes(node, theme);
                obj.push(theme);
            },*/
            WSDL: function (node, obj) {
              obj.wsdl = {};
              obj.wsdl.href = node.getAttribute("xlink:href");
              // TODO: other attributes of <WSDL> element
            },
            ServiceMetadataURL: function (node, obj) {
              obj.serviceMetadataUrl = {};
              obj.serviceMetadataUrl.href = node.getAttribute("xlink:href");
              // TODO: other attributes of <ServiceMetadataURL> element
            },
            LegendURL: function (node, obj) {
              obj.legend = {};
              obj.legend.href = node.getAttribute("xlink:href");
              obj.legend.format = node.getAttribute("format");
            },
            Dimension: function (node, obj) {
              var dimension = { values: [] };
              this.readChildNodes(node, dimension);
              obj.dimensions.push(dimension);
            },
            Default: function (node, obj) {
              obj["default"] = this.getChildValue(node);
            },
            Value: function (node, obj) {
              obj.values.push(this.getChildValue(node));
            },
          },
          ows: uOpenLayers.Format.OWSCommon.v1_1_0.prototype.readers["ows"],
        },

        CLASS_NAME: "OpenLayers.Format.WMTSCapabilities.v1_0_0",
      }
    );

    /**
     * Class: OpenLayers.Layer.WMTS
     * Instances of the WMTS class allow viewing of tiles from a service that
     *     implements the OGC WMTS specification version 1.0.0.
     *
     * Inherits from:
     *  - <OpenLayers.Layer.Grid>
     */
    uOpenLayers.Layer.WMTS = uOpenLayers.Class(uOpenLayers.Layer.Grid, {
      /**
       * APIProperty: isBaseLayer
       * {Boolean} The layer will be considered a base layer.  Default is true.
       */
      isBaseLayer: true,

      /**
       * Property: version
       * {String} WMTS version.  Default is "1.0.0".
       */
      version: "1.0.0",

      /**
       * APIProperty: requestEncoding
       * {String} Request encoding.  Can be "REST" or "KVP".  Default is "KVP".
       */
      requestEncoding: "KVP",

      /**
       * APIProperty: url
       * {String|Array(String)} The base URL or request URL template for the WMTS
       * service. Must be provided. Array is only supported for base URLs, not
       * for request URL templates. URL templates are only supported for
       * REST <requestEncoding>.
       */
      url: null,

      /**
       * APIProperty: layer
       * {String} The layer identifier advertised by the WMTS service.  Must be
       *     provided.
       */
      layer: null,

      /**
       * APIProperty: matrixSet
       * {String} One of the advertised matrix set identifiers.  Must be provided.
       */
      matrixSet: null,

      /**
       * APIProperty: style
       * {String} One of the advertised layer styles.  Must be provided.
       */
      style: null,

      /**
       * APIProperty: format
       * {String} The image MIME type.  Default is "image/jpeg".
       */
      format: "image/jpeg",

      /**
       * APIProperty: tileOrigin
       * {<OpenLayers.LonLat>} The top-left corner of the tile matrix in map
       *     units.  If the tile origin for each matrix in a set is different,
       *     the <matrixIds> should include a topLeftCorner property.  If
       *     not provided, the tile origin will default to the top left corner
       *     of the layer <maxExtent>.
       */
      tileOrigin: null,

      /**
       * APIProperty: tileFullExtent
       * {<OpenLayers.Bounds>}  The full extent of the tile set.  If not supplied,
       *     the layer's <maxExtent> property will be used.
       */
      tileFullExtent: null,

      /**
       * APIProperty: formatSuffix
       * {String} For REST request encoding, an image format suffix must be
       *     included in the request.  If not provided, the suffix will be derived
       *     from the <format> property.
       */
      formatSuffix: null,

      /**
       * APIProperty: matrixIds
       * {Array} A list of tile matrix identifiers.  If not provided, the matrix
       *     identifiers will be assumed to be integers corresponding to the
       *     map zoom level.  If a list of strings is provided, each item should
       *     be the matrix identifier that corresponds to the map zoom level.
       *     Additionally, a list of objects can be provided.  Each object should
       *     describe the matrix as presented in the WMTS capabilities.  These
       *     objects should have the propertes shown below.
       *
       * Matrix properties:
       * identifier - {String} The matrix identifier (required).
       * scaleDenominator - {Number} The matrix scale denominator.
       * topLeftCorner - {<OpenLayers.LonLat>} The top left corner of the
       *     matrix.  Must be provided if different than the layer <tileOrigin>.
       * tileWidth - {Number} The tile width for the matrix.  Must be provided
       *     if different than the width given in the layer <tileSize>.
       * tileHeight - {Number} The tile height for the matrix.  Must be provided
       *     if different than the height given in the layer <tileSize>.
       */
      matrixIds: null,

      /**
       * APIProperty: dimensions
       * {Array} For RESTful request encoding, extra dimensions may be specified.
       *     Items in this list should be property names in the <params> object.
       *     Values of extra dimensions will be determined from the corresponding
       *     values in the <params> object.
       */
      dimensions: null,

      /**
       * APIProperty: params
       * {Object} Extra parameters to include in tile requests.  For KVP
       *     <requestEncoding>, these properties will be encoded in the request
       *     query string.  For REST <requestEncoding>, these properties will
       *     become part of the request path, with order determined by the
       *     <dimensions> list.
       */
      params: null,

      /**
       * APIProperty: zoomOffset
       * {Number} If your cache has more levels than you want to provide
       *     access to with this layer, supply a zoomOffset.  This zoom offset
       *     is added to the current map zoom level to determine the level
       *     for a requested tile.  For example, if you supply a zoomOffset
       *     of 3, when the map is at the zoom 0, tiles will be requested from
       *     level 3 of your cache.  Default is 0 (assumes cache level and map
       *     zoom are equivalent).  Additionally, if this layer is to be used
       *     as an overlay and the cache has fewer zoom levels than the base
       *     layer, you can supply a negative zoomOffset.  For example, if a
       *     map zoom level of 1 corresponds to your cache level zero, you would
       *     supply a -1 zoomOffset (and set the maxResolution of the layer
       *     appropriately).  The zoomOffset value has no effect if complete
       *     matrix definitions (including scaleDenominator) are supplied in
       *     the <matrixIds> property.  Defaults to 0 (no zoom offset).
       */
      zoomOffset: 0,

      /**
       * APIProperty: serverResolutions
       * {Array} A list of all resolutions available on the server.  Only set this
       *     property if the map resolutions differ from the server. This
       *     property serves two purposes. (a) <serverResolutions> can include
       *     resolutions that the server supports and that you don't want to
       *     provide with this layer; you can also look at <zoomOffset>, which is
       *     an alternative to <serverResolutions> for that specific purpose.
       *     (b) The map can work with resolutions that aren't supported by
       *     the server, i.e. that aren't in <serverResolutions>. When the
       *     map is displayed in such a resolution data for the closest
       *     server-supported resolution is loaded and the layer div is
       *     stretched as necessary.
       */
      serverResolutions: null,

      /**
       * Property: formatSuffixMap
       * {Object} a map between WMTS 'format' request parameter and tile image file suffix
       */
      formatSuffixMap: {
        "image/png": "png",
        "image/png8": "png",
        "image/png24": "png",
        "image/png32": "png",
        png: "png",
        "image/jpeg": "jpg",
        "image/jpg": "jpg",
        jpeg: "jpg",
        jpg: "jpg",
      },

      /**
       * Property: matrix
       * {Object} Matrix definition for the current map resolution.  Updated by
       *     the <updateMatrixProperties> method.
       */
      matrix: null,

      /**
       * Constructor: OpenLayers.Layer.WMTS
       * Create a new WMTS layer.
       *
       * Example:
       * (code)
       * var wmts = new OpenLayers.Layer.WMTS({
       *     name: "My WMTS Layer",
       *     url: "http://example.com/wmts",
       *     layer: "layer_id",
       *     style: "default",
       *     matrixSet: "matrix_id"
       * });
       * (end)
       *
       * Parameters:
       * config - {Object} Configuration properties for the layer.
       *
       * Required configuration properties:
       * url - {String} The base url for the service.  See the <url> property.
       * layer - {String} The layer identifier.  See the <layer> property.
       * style - {String} The layer style identifier.  See the <style> property.
       * matrixSet - {String} The tile matrix set identifier.  See the <matrixSet>
       *     property.
       *
       * Any other documented layer properties can be provided in the config object.
       */
      initialize: function (config) {
        // confirm required properties are supplied
        var required = {
          url: true,
          layer: true,
          style: true,
          matrixSet: true,
        };
        for (var prop in required) {
          if (!(prop in config)) {
            throw new Error(
              "Missing property '" + prop + "' in layer configuration."
            );
          }
        }

        config.params = uOpenLayers.Util.upperCaseObject(config.params);
        var args = [config.name, config.url, config.params, config];
        uOpenLayers.Layer.Grid.prototype.initialize.apply(this, args);

        // determine format suffix (for REST)
        if (!this.formatSuffix) {
          this.formatSuffix =
            this.formatSuffixMap[this.format] || this.format.split("/").pop();
        }

        // expand matrixIds (may be array of string or array of object)
        if (this.matrixIds) {
          var len = this.matrixIds.length;
          if (len && typeof this.matrixIds[0] === "string") {
            var ids = this.matrixIds;
            this.matrixIds = new Array(len);
            for (var i = 0; i < len; ++i) {
              this.matrixIds[i] = { identifier: ids[i] };
            }
          }
        }
      },

      /**
       * Method: setMap
       */
      setMap: function () {
        uOpenLayers.Layer.Grid.prototype.setMap.apply(this, arguments);
      },

      /**
       * Method: updateMatrixProperties
       * Called when map resolution changes to update matrix related properties.
       */
      updateMatrixProperties: function () {
        this.matrix = this.getMatrix();
        if (this.matrix) {
          if (this.matrix.topLeftCorner) {
            this.tileOrigin = this.matrix.topLeftCorner;
          }
          if (this.matrix.tileWidth && this.matrix.tileHeight) {
            this.tileSize = new uOpenLayers.Size(
              this.matrix.tileWidth,
              this.matrix.tileHeight
            );
          }
          if (!this.tileOrigin) {
            this.tileOrigin = new uOpenLayers.LonLat(
              this.maxExtent.left,
              this.maxExtent.top
            );
          }
          if (!this.tileFullExtent) {
            this.tileFullExtent = this.maxExtent;
          }
        }
      },

      /**
       * Method: moveTo
       *
       * Parameters:
       * bounds - {<OpenLayers.Bounds>}
       * zoomChanged - {Boolean} Tells when zoom has changed, as layers have to
       *     do some init work in that case.
       * dragging - {Boolean}
       */
      moveTo: function (bounds, zoomChanged, dragging) {
        if (zoomChanged || !this.matrix) {
          this.updateMatrixProperties();
        }
        return uOpenLayers.Layer.Grid.prototype.moveTo.apply(this, arguments);
      },

      /**
       * APIMethod: clone
       *
       * Parameters:
       * obj - {Object}
       *
       * Returns:
       * {<OpenLayers.Layer.WMTS>} An exact clone of this <OpenLayers.Layer.WMTS>
       */
      clone: function (obj) {
        if (obj == null) {
          obj = new uOpenLayers.Layer.WMTS(this.options);
        }
        //get all additions from superclasses
        obj = uOpenLayers.Layer.Grid.prototype.clone.apply(this, [obj]);
        // copy/set any non-init, non-simple values here
        return obj;
      },

      /**
       * Method: getIdentifier
       * Get the current index in the matrixIds array.
       */
      getIdentifier: function () {
        return this.getServerZoom();
      },

      /**
       * Method: getMatrix
       * Get the appropriate matrix definition for the current map resolution.
       */
      getMatrix: function () {
        var matrix;
        if (!this.matrixIds || this.matrixIds.length === 0) {
          matrix = { identifier: this.getIdentifier() };
        } else {
          // get appropriate matrix given the map scale if possible
          if ("scaleDenominator" in this.matrixIds[0]) {
            // scale denominator calculation based on WMTS spec
            var denom =
              (uOpenLayers.METERS_PER_INCH *
                uOpenLayers.INCHES_PER_UNIT[this.units] *
                this.getServerResolution()) /
              0.28e-3;
            var diff = Number.POSITIVE_INFINITY;
            var delta;
            for (var i = 0, ii = this.matrixIds.length; i < ii; ++i) {
              delta = Math.abs(1 - this.matrixIds[i].scaleDenominator / denom);
              if (delta < diff) {
                diff = delta;
                matrix = this.matrixIds[i];
              }
            }
          } else {
            // fall back on zoom as index
            matrix = this.matrixIds[this.getIdentifier()];
          }
        }
        return matrix;
      },

      /**
       * Method: getTileInfo
       * Get tile information for a given location at the current map resolution.
       *
       * Parameters:
       * loc - {<OpenLayers.LonLat} A location in map coordinates.
       *
       * Returns:
       * {Object} An object with "col", "row", "i", and "j" properties.  The col
       *     and row values are zero based tile indexes from the top left.  The
       *     i and j values are the number of pixels to the left and top
       *     (respectively) of the given location within the target tile.
       */
      getTileInfo: function (loc) {
        var res = this.getServerResolution();

        var fx = (loc.lon - this.tileOrigin.lon) / (res * this.tileSize.w);
        var fy = (this.tileOrigin.lat - loc.lat) / (res * this.tileSize.h);

        var col = Math.floor(fx);
        var row = Math.floor(fy);

        return {
          col: col,
          row: row,
          i: Math.floor((fx - col) * this.tileSize.w),
          j: Math.floor((fy - row) * this.tileSize.h),
        };
      },

      /**
       * Method: getURL
       *
       * Parameters:
       * bounds - {<OpenLayers.Bounds>}
       *
       * Returns:
       * {String} A URL for the tile corresponding to the given bounds.
       */
      getURL: function (bounds) {
        bounds = this.adjustBounds(bounds);
        var url = "";
        if (
          !this.tileFullExtent ||
          this.tileFullExtent.intersectsBounds(bounds)
        ) {
          var center = bounds.getCenterLonLat();
          var info = this.getTileInfo(center);
          var matrixId = this.matrix.identifier;
          var dimensions = this.dimensions,
            params;

          if (uOpenLayers.Util.isArray(this.url)) {
            url = this.selectUrl(
              [
                this.version,
                this.style,
                this.matrixSet,
                this.matrix.identifier,
                info.row,
                info.col,
              ].join(","),
              this.url
            );
          } else {
            url = this.url;
          }

          if (this.requestEncoding.toUpperCase() === "REST") {
            params = this.params;
            if (url.indexOf("{") !== -1) {
              var template = url.replace(/\{/g, "${");
              var context = {
                // spec does not make clear if capital S or not
                style: this.style,
                Style: this.style,
                TileMatrixSet: this.matrixSet,
                TileMatrix: this.matrix.identifier,
                TileRow: info.row,
                TileCol: info.col,
              };
              if (dimensions) {
                var dimension, i;
                for (i = dimensions.length - 1; i >= 0; --i) {
                  dimension = dimensions[i];
                  context[dimension] = params[dimension.toUpperCase()];
                }
              }
              url = uOpenLayers.String.format(template, context);
            } else {
              // include 'version', 'layer' and 'style' in tile resource url
              var path =
                this.version + "/" + this.layer + "/" + this.style + "/";

              // append optional dimension path elements
              if (dimensions) {
                for (var i = 0; i < dimensions.length; i++) {
                  if (params[dimensions[i]]) {
                    path = path + params[dimensions[i]] + "/";
                  }
                }
              }

              // append other required path elements
              path =
                path +
                this.matrixSet +
                "/" +
                this.matrix.identifier +
                "/" +
                info.row +
                "/" +
                info.col +
                "." +
                this.formatSuffix;

              if (!url.match(/\/$/)) {
                url = url + "/";
              }
              url = url + path;
            }
          } else if (this.requestEncoding.toUpperCase() === "KVP") {
            // assemble all required parameters
            params = {
              SERVICE: "WMTS",
              REQUEST: "GetTile",
              VERSION: this.version,
              LAYER: this.layer,
              STYLE: this.style,
              TILEMATRIXSET: this.matrixSet,
              TILEMATRIX: this.matrix.identifier,
              TILEROW: info.row,
              TILECOL: info.col,
              FORMAT: this.format,
            };
            url = uOpenLayers.Layer.Grid.prototype.getFullRequestString.apply(
              this,
              [params]
            );
          }
        }
        return url;
      },

      /**
       * APIMethod: mergeNewParams
       * Extend the existing layer <params> with new properties.  Tiles will be
       *     reloaded with updated params in the request.
       *
       * Parameters:
       * newParams - {Object} Properties to extend to existing <params>.
       */
      mergeNewParams: function (newParams) {
        if (this.requestEncoding.toUpperCase() === "KVP") {
          return uOpenLayers.Layer.Grid.prototype.mergeNewParams.apply(this, [
            uOpenLayers.Util.upperCaseObject(newParams),
          ]);
        }
      },

      CLASS_NAME: "OpenLayers.Layer.WMTS",
    });
  }

  function geoportal_init() {
    loadWMTSLayer();

    let displayGroupSelector = $("#layer-switcher-group_display");
    if (displayGroupSelector.length) {
      let displayGroup = $("ul.collapsible-GROUP_DISPLAY");

      $.each(sources, 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 Geoportal Layer ${index + 1}/${
                  sources.length
                }: ${source.name}`
              );
              return;
            }

            let format = new uOpenLayers.Format.WMTSCapabilities({});
            var doc = responseXML;
            var capabilities = format.read(doc);
            source.layer = format.createLayer(capabilities, {
              layer: source.layerName,
              matrixSet: source.matrixSet,
              format: "image/png",
              opacity: source.opacity ?? opacity,
              isBaseLayer: false,
              requestEncoding: source.requestEncoding ?? "KVP",
            });

            uWaze.map.addLayer(source.layer);
            uWaze.map.setLayerIndex(source.layer, 3);

            // Check if layer was active previously
            if (localStorage[source.unique]) {
              source.layer.setVisibility(localStorage[source.unique] == "true");
            }

            // Make checkbox and add to "display" section
            let toggleEntry = $("<li></li>");
            let checkbox = $("<wz-checkbox></wz-checkbox>", {
              id: source.id,
              class: "hydrated",
              disabled: !displayGroupSelector.prop("checked"),
              checked: source.layer.getVisibility(),
              text: source.name,
            });

            toggleEntry.append(checkbox);
            displayGroup.append(toggleEntry);

            checkbox.on("click", function (e) {
              source.layer.setVisibility(e.target.checked);
              localStorage[source.unique] = source.layer.getVisibility();
            });

            displayGroupSelector.on("click", function (e) {
              source.layer.setVisibility(
                e.target.checked && checkbox.prop("checked")
              );
              checkbox.prop("disabled", !e.target.checked);
              localStorage[source.unique] = source.layer.getVisibility();
            });

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

  function ui_init() {
    //Add a Opacity control to the map controls
    const controlContainer = $(".overlay-buttons-container.bottom").first();
    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>
    </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>
    </wz-basic-tooltip>
    </div>
    `);
    controlContainer.append(opacityControl);

    // Add event listeners to the opacity control
    $(".opacity-plus").on("click", function () {
      opacity = Math.min(opacity + 0.1, 1);
      localStorage.setItem("geoportal_opacity", opacity);
      sources.forEach((source) => {
        source.layer.setOpacity(opacity);
      });
    });

    $(".opacity-minus").on("click", function () {
      opacity = Math.max(opacity - 0.1, 0);
      localStorage.setItem("geoportal_opacity", opacity);
      sources.forEach((source) => {
        source.layer.setOpacity(opacity);
      });
    });
  }

  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...");
      geoportal_init();
      ui_init();
    }
  }

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

`);