Greasy Fork is available in English.

GeoGMLer

GeoGMLer is a JavaScript library for converting GML data into GeoJSON. It translates FeatureMembers with Points, LineStrings, and Polygons, handling coordinates via gml:coordinates and gml:posList. Supports multi-geometries to ensure conversion to GeoJSON's FeatureCollection.

이 스크립트는 직접 설치해서 쓰는 게 아닙니다. 다른 스크립트가 메타 명령 // @require https://update.greasyfork.org/scripts/526229/1537672/GeoGMLer.js(으)로 포함하여 쓰는 라이브러리입니다.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name                GeoGMLer
// @namespace           https://github.com/JS55CT
// @description         GeoGMLer is a JavaScript library for converting GML data into GeoJSON. It translates FeatureMembers with Points, LineStrings, and Polygons, handling coordinates via gml:coordinates and gml:posList. Supports multi-geometries to ensure conversion to GeoJSON's FeatureCollection.
// @version             2.1.0
// @author              JS55CT
// @license             MIT
// @match              *://this-library-is-not-supposed-to-run.com/*
// ==/UserScript==

/***********************************************************
 * ## Project Home < https://github.com/JS55CT/GeoGMLer >
 *  MIT License
 * Copyright (c) 2025 Justin
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * - Project was inspired by the work of [gml2geojson](https://github.com/deyihu/gml2geojson) (MIT licensed)
 *  and builds upon the concepts and implementations found there
 **************************************************************/

/** TO DO:  intergrate pro4.js to convert to ESPG:4326 standerd
 * The default Coordinate Reference System (CRS) for GeoJSON is WGS 84, which is represented by the EPSG code 4326.
 * This means that coordinates in a GeoJSON file are expected to be in longitude and latitude format, following the WGS 84 datum.
 * In the GeoJSON format, the coordinates are typically ordered as [longitude, latitude].
 * It's important to adhere to this order to ensure proper geospatial data interpretation and interoperability with GIS tools
 * and applications that conform to the GeoJSON specification.
 *
 * While GeoJSON does allow specifying other coordinate reference systems through extensions,
 * the use of any CRS other than WGS 84 is not recommended as it breaks the convention and could impact interoperability
 * and usability across services and applications that expect WGS 84.
 */

var GeoGMLer = (function () {
  /**
   * GeoGMLer constructor function.
   * @returns {GeoGMLer} - An instance of GeoGMLer.
   */
  function GeoGMLer(obj) {
    if (obj instanceof GeoGMLer) return obj;
    if (!(this instanceof GeoGMLer)) return new GeoGMLer(obj);
    this._wrapped = obj;
  }

  const GEONODENAMES = ["geometryproperty", "geometryProperty"];

  /**
   * Reads a GML string and prepares it for conversion by extracting
   * both the parsed XML document and its coordinate reference system (CRS).
   * @param {string} str - The GML string to read.
   * @returns {Object} - An object containing the parsed XML document and CRS name.
   * @property {Document} xmlDoc - The parsed XML document.
   * @property {string} crsName - The name of the coordinate reference system extracted from the GML.
   */
  GeoGMLer.prototype.read = function (gmlText) {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(gmlText, "application/xml");

    // Check for parsing errors by looking for parser error tags
    const parseErrors = xmlDoc.getElementsByTagName("parsererror");
    if (parseErrors.length > 0) {
      const errorMessages = Array.from(parseErrors)
        .map((errorElement, index) => {
          return `Parsing Error ${index + 1}: ${errorElement.textContent}`;
        })
        .join("\n");

      console.error(errorMessages);
      throw new Error("Failed to parse GML. See console for details.");
    }

    // Extract the CRS directly within the read function if parsing is successful
    const crsName = this.getCRS(xmlDoc);

    // Return both the XML document and the CRS
    return {
      xmlDoc,
      crsName,
    };
  };

  /**
   * Converts a parsed GML XML document to a GeoJSON object, incorporating the specified CRS.
   * @param {Object} params - The parameters required for conversion.
   * @param {Document} params.xmlDoc - The parsed XML document to convert.
   * @param {string} params.crsName - The name of the coordinate reference system.
   * @returns {Object} - The GeoJSON object representing the features and their CRS.
   *
   * WARNING: The input GML geometries may specify a spatial reference system (SRS) through the `srsName` attribute.
   * This function extracts the `srsName` and includes it in the GeoJSON output under the 'crs' property:
   *
   * crs: {
   *   type: "name",
   *   properties: {
   *     name: crsName,
   *   },
   * },
   *
   * However, the function does not transform the coordinate values to match the EPSG:4326 (WGS 84) geoJSON standard.
   * This means the coordinate values remain in the original SRS specified by `srsName`.
   * Users should be aware that the GeoJSON output may not conform to expected standards if the original SRS
   * is not compatible with their intended use. It is essential to handle coordinate transformations as needed
   * for accurate spatial data representation.
   */
  GeoGMLer.prototype.toGeoJSON = function ({ xmlDoc, crsName }) {
    const geojson = {
      type: "FeatureCollection",
      features: [],
      crs: {
        type: "name",
        properties: {
          name: crsName,
        },
      },
    };

    // Get the main element of the feature collection
    const featureCollectionEle = xmlDoc.children[0];

    // Check if the node is a FeatureCollection, considering possible namespace prefixes
    const nodeName = this.getNodeName(featureCollectionEle); // this returns lowercase by default
    const isFeatureCollection = featureCollectionEle && featureCollectionEle.nodeName && nodeName.includes("featurecollection");

    // Validate the document structure
    if (!isFeatureCollection) {
      console.error("Invalid GML structure: The document does not contain a valid FeatureCollection element.");
      return geojson; // Return empty GeoJSON if the structure is incorrect
    }

    const features = [];

    // Iterate over each child node to extract feature members
    for (let i = 0; i < featureCollectionEle.children.length; i++) {
      const featureEle = featureCollectionEle.children.item(i);

      if (featureEle) {
        const childNodeName = this.getNodeName(featureEle);

        // Identify and collect feature member elements
        if (childNodeName.includes("featuremember") && featureEle.children[0]) {
          features.push(featureEle.children[0]);
        }
      }
    }

    // Process each feature member to extract properties and geometry
    for (let i = 0, len = features.length; i < len; i++) {
      const f = features[i];

      const properties = this.getFeatureEleProperties(f); // Extract properties
      const geometry = this.getFeatureEleGeometry(f, crsName); // Extract geometry using the provided CRS

      if (!geometry || !properties) {
        console.error(`Skipping feature ${i + 1} due to missing geometry or properties.`);
        continue; // Skip if geometry or properties are missing
      }

      const feature = {
        type: "Feature",
        geometry,
        properties,
      };
      geojson.features.push(feature); // Add the feature to the GeoJSON features array
    }

    return geojson; // Return the constructed GeoJSON object
  };

  /**
   * Retrieves the CRS (Coordinate Reference System) from GML.
   * @param {string} gmlString - The GML string.
   * @returns {string|null} - The CRS name or null.
   */
  // Enhanced getCRS function to search for srsName attribute in various geometry nodes
  GeoGMLer.prototype.getCRS = function (xmlDoc) {
    // Define a list of common GML geometry elements to check for srsName attribute
    const geometryTags = [
      "gml:Envelope",
      "gml:Point",
      "gml:LineString",
      "gml:Polygon",
      "gml:MultiPoint",
      "gml:MultiLineString",
      "gml:MultiPolygon",
      "gml:Surface",
      "gml:Solid",
      // Add other geometry types as needed
    ];

    for (const tag of geometryTags) {
      const elements = xmlDoc.getElementsByTagName(tag);
      for (let i = 0; i < elements.length; i++) {
        const srsName = elements[i].getAttribute("srsName");
        if (srsName) {
          return srsName.trim();
        }
      }
    }

    // Consider additional handling or logging if no srsName is found
    return null;
  };

  /**
   * Extracts the geometry from a GML feature element.
   * @param {Element} featureEle - The feature element.
   * @param {string} crsName - The name of the CRS.
   * @returns {Object|null} - The geometry object or null.
   */
  GeoGMLer.prototype.getFeatureEleGeometry = function (featureEle, crsName) {
    const children = featureEle.children || [];
    let type;
    let coordinates = [];

    for (let i = 0, len = children.length; i < len; i++) {
      const node = children[i];
      const nodeName = this.getNodeName(node);
      if (!this.isGeoAttribute(nodeName)) {
        continue;
      }

      if (node.children && node.children[0]) {
        type = node.children[0].nodeName.split("gml:")[1] || "";
        if (!type) {
          continue;
        }
        const geoElement = node.children[0];

        if (type === "Point") {
          coordinates = this.processPoint(geoElement, crsName);
          break;
        } else if (type === "MultiPoint") {
          coordinates = this.processMultiPoint(geoElement, crsName);
          break;
        } else if (type === "MultiSurface" || type === "MultiPolygon") {
          coordinates = this.processMultiSurface(geoElement, crsName);
          break;
        } else if (type === "MultiCurve" || type === "MultiLineString") {
          coordinates = this.processMultiCurve(geoElement, crsName);
          break;
        } else if (geoElement.children.length > 0) {
          let geoNodes = Array.from(geoElement.children);
          if (this.isMulti(this.getNodeName(geoNodes[0]))) {
            geoNodes = this.flatMultiGeoNodes(geoNodes);
          }

          if (geoNodes.length) {
            geoNodes.forEach((geoNode) => {
              let coords = this.parseGeoCoordinates(geoNode.children, crsName);
              if (!this.geoIsPolygon(type) && this.isMultiLine(type)) {
                coords = coords[0];
              }
              coordinates.push(coords);
            });
            break;
          }
        }
      }
    }

    if (!type || !coordinates.length) {
      return null;
    }

    return {
      type: this.mapGmlTypeToGeoJson(type),
      coordinates,
    };
  };

  /**
   * Processes a multi-surface element to extract polygons.
   * @param {Element} multiSurfaceElement - The multi-surface element.
   * @param {string} crsName - The name of the CRS.
   * @returns {Array} - Array of polygons.
   */
  GeoGMLer.prototype.processMultiSurface = function (multiSurfaceElement, crsName) {
    const polygons = [];
    const surfaceMembers = multiSurfaceElement.getElementsByTagName("gml:surfaceMember");

    for (let j = 0; j < surfaceMembers.length; j++) {
      const polygon = this.processPolygon(surfaceMembers[j].getElementsByTagName("gml:Polygon")[0], crsName);
      if (polygon) {
        polygons.push(polygon);
      }
    }
    return polygons;
  };

  /**
   * Processes a polygon element.
   * @param {Element} polygonElement - The polygon element.
   * @param {string} crsName - The name of the CRS.
   * @returns {Array} - Array representing the polygon.
   */
  GeoGMLer.prototype.processPolygon = function (polygonElement, crsName) {
    const polygon = [];
    const exteriorElements = polygonElement.getElementsByTagName("gml:exterior");

    if (exteriorElements.length > 0) {
      const exterior = this.parseRing(exteriorElements[0], crsName);
      if (exterior) {
        polygon.push(exterior);
      }
    }

    const interiorElements = polygonElement.getElementsByTagName("gml:interior");
    for (let k = 0; k < interiorElements.length; k++) {
      const interior = this.parseRing(interiorElements[k], crsName);
      if (interior) {
        polygon.push(interior);
      }
    }
    return polygon;
  };

  /**
   * Parses a ring element to extract coordinates.
   * @param {Element} ringElement - The ring element.
   * @param {string} crsName - The name of the CRS.
   * @returns {Array} - Array of coordinates.
   */
  GeoGMLer.prototype.parseRing = function (ringElement, crsName) {
    const coordNodes = ringElement.getElementsByTagName("gml:posList");
    if (coordNodes.length > 0) {
      return this.parseGeoCoordinates(coordNodes, crsName);
    }
    return [];
  };

  /**
   * Processes a multi-curve element to extract line strings.
   * @param {Element} multiCurveElement - The multi-curve element.
   * @param {string} crsName - The name of the CRS.
   * @returns {Array} - Array of line strings.
   */
  GeoGMLer.prototype.processMultiCurve = function (multiCurveElement, crsName) {
    const lineStrings = [];
    const curveMembers = multiCurveElement.getElementsByTagName("gml:curveMember");

    for (let j = 0; j < curveMembers.length; j++) {
      const lineStringElement = curveMembers[j].getElementsByTagName("gml:LineString")[0];
      if (lineStringElement) {
        const lineString = this.processLineString(lineStringElement, crsName);
        if (lineString) {
          lineStrings.push(lineString);
        }
      }
    }
    return lineStrings;
  };

  /**
   * Processes a line string element.
   * @param {Element} lineStringElement - The line string element.
   * @param {string} crsName - The name of the CRS.
   * @returns {Array} - Array of coordinates representing the line string.
   */
  GeoGMLer.prototype.processLineString = function (lineStringElement, crsName) {
    const coordNodes = lineStringElement.getElementsByTagName("gml:posList");
    if (coordNodes.length > 0) {
      return this.parseGeoCoordinates(coordNodes, crsName);
    }
    return [];
  };

  /**
   * Processes a GML Point geometry element to extract its coordinates.
   *
   * @param {Element} geoElement - The GML element representing the Point geometry.
   * @param {string} crsName - The coordinate reference system (CRS) name, used to determine if coordinate order needs to be reversed.
   *
   * @returns {Array} An array containing the coordinates for the Point. If no valid coordinates are found, an empty array is returned.
   *
   * The function first attempts to find the coordinates using the `<gml:pos>` element. If that is not available,
   * it looks for the `<gml:coordinates>` element instead. It utilizes the `parseGeoCoordinates` method to convert
   * the raw coordinate text into an array of numbers, considering the specified CRS.
   */
  GeoGMLer.prototype.processPoint = function (geoElement, crsName) {
    let coordNode = geoElement.getElementsByTagName("gml:pos");

    if (coordNode.length === 0) {
      coordNode = geoElement.getElementsByTagName("gml:coordinates");
    }

    if (coordNode.length > 0) {
      // Parse the coordinates
      const parsedCoords = this.parseGeoCoordinates([coordNode[0]], crsName);
      // Flatten them if necessary (should only be length 1 for a valid Point)
      return parsedCoords.length > 0 ? parsedCoords[0] : [];
    } else {
      return [];
    }
  };

  /**
   * Processes a multi-point element to extract the coordinates of each point.
   * @param {Element} multiPointElement - The element representing the MultiPoint geometry.
   * @param {string} crsName - The coordinate reference system (CRS) name.
   * @returns {Array} - An array of coordinate arrays for the multipoint.
   */
  GeoGMLer.prototype.processMultiPoint = function (multiPointElement, crsName) {
    const points = [];
    const pointMembers = multiPointElement.getElementsByTagName("gml:pointMember");

    for (let j = 0; j < pointMembers.length; j++) {
      const pointElement = pointMembers[j].getElementsByTagName("gml:Point")[0];
      if (pointElement) {
        const coordinates = this.processPoint(pointElement, crsName);
        if (coordinates.length > 0) {
          points.push(coordinates);
        }
      }
    }

    return points;
  };

  /**
   * Parses coordinate nodes into arrays of coordinates, considering the coordinate reference system (CRS)
   * and the coordinate formatting requirements.
   *
   * @param {HTMLCollection} coordNodes - The collection of coordinate nodes to be parsed.
   * @param {string} crsName - The name of the coordinate reference system (CRS).
   * @returns {Array} - An array of parsed coordinates.
   *
   * The `needsReversal` flag is determined by the CRS name. It is set to true for common geographic
   * coordinate systems like "EPSG:4326", "CRS84", or "WGS84", which typically use a "latitude, longitude"
   * format. Reversing is necessary when converting to systems expecting the "longitude, latitude" order
   * like geoJSON.
   *
   * The `isCommaSeparated` flag is used to determine the delimiter in the coordinate parsing. It checks
   * if the coordinates node is named with ":coordinates", which indicates that commas are used to
   * separate coordinate values (older versions of GML). This is essential for correctly interpreting data where commas are
   * the delimiter, distinguishing from systems using whitespace (GML3.X) with :pos and :posList.
   */
  GeoGMLer.prototype.parseGeoCoordinates = function (coordNodes, crsName) {
    const coordinates = [];
    const needsReversal = crsName.includes("4326") || crsName.includes("CRS84") || crsName.includes("WGS84");

    if (coordNodes.length === 0) {
    }

    for (let i = 0, len = coordNodes.length; i < len; i++) {
      const coordNode = this.findCoordsNode(coordNodes[i]);

      if (!coordNode) {
        continue;
      }

      const isCommaSeparated = this.getNodeName(coordNode).indexOf(":coordinates") > -1;
      const textContent = coordNode.textContent.trim();
      const coords = this.parseCoordinates(textContent, isCommaSeparated, needsReversal);

      coordinates.push(...coords);
    }
    return coordinates;
  };

  /**
   * Parses a coordinate string into an array of coordinate pairs, considering whether the input
   * uses commas as separators and whether the coordinate pair order needs to be reversed.
   *
   * @param {string} text - The text containing coordinates, which may be in different formats
   * based on the input data (e.g., comma-separated or space-separated).
   * @param {boolean} isCommaSeparated - A flag indicating if the coordinate string uses commas as
   * separators between individual coordinates. This is often observed in older data formats where
   * coordinates are presented as "x,y".
   * @param {boolean} needsReversal - A flag indicating if the latitude and longitude values need to
   * be reversed in order, which is particularly necessary for compatibility with modern formats like
   * GeoJSON. Older versions of GML (such as 1 and 2), when using the :coordinates tag and a CRS like
   * "EPSG:4326", often present coordinates in "latitude, longitude" format. In contrast, GeoJSON and
   * other modern systems require "longitude, latitude". However, in GML 3.x, it is more common to use
   * elements like :pos and :posList, which typically follow the "longitude, latitude" order, aligning
   * with modern geographic data representations regardless of the projection system used.
   * @returns {Array} - An array of coordinate pairs, where each pair is represented as an array of the
   * form [longitude, latitude] or [latitude, longitude] depending on the `needsReversal` flag.
   */
  GeoGMLer.prototype.parseCoordinates = function (text, isCommaSeparated, needsReversal) {
    if (!text) return [];

    const coords = text.trim().split(/\s+/);
    const coordinates = [];

    for (let i = 0; i < coords.length; i++) {
      let c1, c2;
      const coord = coords[i];

      if (isCommaSeparated) {
        if (coord.includes(",")) {
          const [x, y] = coord.split(",");
          c1 = this.trimAndParse(x);
          c2 = this.trimAndParse(y);
          coordinates.push(needsReversal ? [c1, c2] : [c2, c1]);
        }
      } else {
        c1 = this.trimAndParse(coord);
        c2 = this.trimAndParse(coords[i + 1]);
        i++; // Skip the next coordinate since it's already processed
        coordinates.push(needsReversal ? [c2, c1] : [c1, c2]);
      }
    }
    return coordinates;
  };

  /**
   * Trims and parses a string into a float.
   * @param {string} str - The string to parse.
   * @returns {number} - The parsed float.
   */
  GeoGMLer.prototype.trimAndParse = function (str) {
    return parseFloat(str.replace(/\s+/g, ""));
  };

  /**
   * Finds the coordinate node within a given node.
   * @param {Node} node - The node to search.
   * @returns {Node} - The coordinate node found.
   */
  GeoGMLer.prototype.findCoordsNode = function (node) {
    let nodeName = this.getNodeName(node);

    while (nodeName.indexOf(":coordinates") === -1 && nodeName.indexOf(":posList") === -1 && nodeName.indexOf(":pos") === -1) {
      node = node.children[0];
      nodeName = this.getNodeName(node);
    }
    return node;
  };

  /**
   * Retrieves the node name.
   * @param {Node} node - The node object.
   * @param {boolean} [lowerCase=true] - Whether to convert the name to lower case.
   * @returns {string} - The node name.
   */
  GeoGMLer.prototype.getNodeName = function (node, lowerCase = true) {
    if (lowerCase) {
      return (node.nodeName || "").toLocaleLowerCase();
    } else {
      return node.nodeName || "";
    }
  };

  /**
   * Checks if the geometry type is a polygon.
   * @param {string} type - The geometry type.
   * @returns {boolean} - True if the type is a polygon.
   */
  GeoGMLer.prototype.geoIsPolygon = function (type) {
    return type.indexOf("Polygon") > -1;
  };

  /**
   * Maps GML geometry types to GeoJSON types.
   * @param {string} type - The GML type.
   * @returns {string} - The corresponding GeoJSON type.
   */
  GeoGMLer.prototype.mapGmlTypeToGeoJson = function (type) {
    switch (type) {
      case "MultiCurve":
        return "MultiLineString";
      case "MultiSurface":
        return "MultiPolygon";
      default:
        return type; // Return as-is for matching types
    }
  };

  /**
   * Extracts feature element properties.
   * @param {Element} featureEle - The feature element.
   * @returns {Object} - The properties object.
   */
  GeoGMLer.prototype.getFeatureEleProperties = function (featureEle) {
    const children = featureEle.children || [];
    const properties = {};

    for (let i = 0, len = children.length; i < len; i++) {
      const node = children[i];
      const nodeName = this.getNodeName(node);

      // Skip geometry-related attributes
      if (this.isGeoAttribute(nodeName) && node.children.length) {
        continue;
      }

      // Skip boundedBy or other GML-specific elements
      if (nodeName === "gml:boundedby" || nodeName === "gml:geometryproperty") {
        continue;
      }

      // Extract feature properties
      const key = node.nodeName.includes(":") ? node.nodeName.split(":")[1] : node.nodeName;
      if (!key) {
        continue;
      }

      const value = node.textContent || "";
      properties[key] = value;
    }
    return properties;
  };

  /**
   * Flattens multi-geometry nodes.
   * @param {Array} nodes - The multi-geometry nodes.
   * @returns {Array} - Array of geometry nodes.
   */
  GeoGMLer.prototype.flatMultiGeoNodes = function (nodes) {
    const geoNodes = [];
    for (let i = 0, len = nodes.length; i < len; i++) {
      const children = nodes[i].children;
      for (let j = 0, len1 = children.length; j < len1; j++) {
        geoNodes.push(children[j].children[0]);
      }
    }
    return geoNodes;
  };

  /**
   * Checks if the node name indicates a multi-geometry.
   * @param {string} nodeName - The node name.
   * @returns {boolean} - True if the node denotes a multi-geometry.
   */
  GeoGMLer.prototype.isMulti = function (nodeName) {
    return nodeName.indexOf("member") > -1;
  };

  /**
   * Checks if the geometry type is a multi-line.
   * @param {string} type - The geometry type.
   * @returns {boolean} - True if the type is a multi-line.
   */
  GeoGMLer.prototype.isMultiLine = function (type) {
    return type === "MultiCurve" || type === "MultiLineString";
  };

  /**
   * Checks if the node name is a geometry attribute.
   * @param {string} nodeName - The node name.
   * @returns {boolean} - True if the attribute is geometry-related.
   */
  GeoGMLer.prototype.isGeoAttribute = function (nodeName) {
    return GEONODENAMES.some((geoName) => nodeName.indexOf(geoName) > -1);
  };

  return GeoGMLer;
})();