GeoKMLer

geoKMLer is a JavaScript library designed to convert KML data into GeoJSON format efficiently. It supports conversion of Placemarks containing Point, LineString, Polygon, and MultiGeometry elements.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://updategreasyfork.deno.dev/scripts/524747/1542062/GeoKMLer.js

// ==UserScript==
// @name                GeoKMLer
// @namespace           https://github.com/JS55CT
// @description         geoKMLer is a JavaScript library designed to convert KML data into GeoJSON format efficiently. It supports conversion of Placemarks containing Point, LineString, Polygon, and MultiGeometry elements.
// @version             2.2.0
// @author              JS55CT
// @license             MIT
// @match              *://this-library-is-not-supposed-to-run.com/*
// ==/UserScript==

/***********************************************************
 * ## Project Home < https://github.com/JS55CT/GeoKMLer >
 *  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.
 **************************************************************/
var GeoKMLer = (function () {
  /**
   * GeoKMLer constructor function.
   * @param {Object} obj - Optional object to wrap.
   * @returns {GeoKMLer} - An instance of GeoKMLer.
   */
  function GeoKMLer(obj) {
    if (obj instanceof GeoKMLer) return obj;
    if (!(this instanceof GeoKMLer)) return new GeoKMLer(obj);
    this._wrapped = obj;
  }

  /**
   * Parses a KML string into an XML DOM.
   * @param {string} kmlText - The KML text to parse.
   * @returns {Document} - The parsed XML document.
   */
  GeoKMLer.prototype.read = function (kmlText) {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(kmlText, "application/xml");

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

      console.error(errorMessages);

      // Throw an error to indicate parsing failure
      throw new Error("Failed to parse KML. See console for details.");
    }

    // If parsing is successful, return the parsed XML document
    return xmlDoc;
  };

  /**
   * Converts a KML document to a GeoJSON FeatureCollection.
   * @param {Document} document - The KML document to convert.
   * @param {boolean} includeCrs - Optional boolean to determine if CRS should be included.
   * @returns {Object} - The resulting GeoJSON FeatureCollection.
   *
   * NOTE:
   * KML files inherently assume the use of the EPSG:4326 (WGS 84) coordinate reference system
   * for all geographic coordinates. As such, when converting from KML to GeoJSON, the coordinates
   * are retained in the standard WGS 84 format.
   *
   * The GeoJSON output will conform to this CRS standard, and no additional CRS transformation are needed.
   * Users can rely on the spatial information being accurate with respect to the WGS 84 datum.
   *
   * Additionally, this function includes an option to add CRS information explicitly to the GeoJSON output (none standard).
   * By setting the `includeCrs` parameter to `true`, the resulting GeoJSON will include a 'crs' property
   * that specifies the use of EPSG:4326:  the geoJSON standard.
   *
   * crs: {
   *   type: "name",
   *   properties: {
   *     name: "EPSG:4326",
   *   },
   * }
   */
  GeoKMLer.prototype.toGeoJSON = function (document, includeCrs = false) {
    const features = [];
    for (const placemark of document.getElementsByTagName("Placemark")) {
      features.push(...this.handlePlacemark(placemark));
    }

    const geoJson = {
      type: "FeatureCollection",
      features: features,
    };

    if (includeCrs) {
      geoJson.crs = {
        type: "name",
        properties: {
          name: "EPSG:4326",
        },
      };
    }

    return geoJson;
  };

  /**
   * Processes a KML Placemark and converts its geometries to GeoJSON features.
   * @param {Element} placemark - The Placemark element to process.
   * @returns {Array} - An array of GeoJSON features.
   */
  GeoKMLer.prototype.handlePlacemark = function (placemark) {
    const features = [];
    const properties = this.extractProperties(placemark);
    // Merge extended data directly into the properties without an additional 'ExtendedData' entry
    Object.assign(properties, this.extractExtendedData(placemark));

    for (let i = 0; i < placemark.children.length; i++) {
      const element = placemark.children[i];
      switch (element.tagName) {
        case "Point":
          features.push(this.pointToPoint(element, placemark, properties));
          break;
        case "LineString":
          features.push(this.lineStringToLineString(element, placemark, properties));
          break;
        case "Polygon":
          features.push(this.polygonToPolygon(element, placemark, properties));
          break;
        case "MultiGeometry":
          features.push(...this.handleMultiGeometry(element, placemark, properties));
          break;
      }
    }
    return features;
  };

  /**
   * Converts coordinate strings into arrays of [longitude, latitude].
   * @param {string} coordString - The coordinate string from KML.
   * @returns {Array} - An array of [longitude, latitude] pairs.
   */
  GeoKMLer.prototype.coordFromString = function(coordString) {
    return coordString.trim().split(/\s+/).map(coord => {
      const [lon, lat, ele] = coord.split(',').map(parseFloat);
      return [lon, lat, ele]; // Include ele for elevation
    });
  };

  /**
   * Parses a single coordinate string into a numeric array.
   * @param {string} v - The coordinate string.
   * @returns {Array} - An array of parsed coordinate values.
   */
  GeoKMLer.prototype.coord1 = function (v) {
    const removeSpace = /\s*/g;
    return v.replace(removeSpace, "").split(",").map(parseFloat);
  };

  /**
   * Parses multiple coordinate strings into an array of coordinate arrays.
   * @param {string} v - The coordinate string with multiple coordinates.
   * @returns {Array} - A nested array of parsed coordinate values.
   */
  GeoKMLer.prototype.coord = function (v) {
    const trimSpace = /^\s*|\s*$/g;
    const splitSpace = /\s+/;
    const coords = v.replace(trimSpace, "").split(splitSpace);
    return coords.map((coord) => this.coord1(coord));
  };

  /**
   * Extracts extended data from a KML placemark.
   * @param {Element} placemark - The Placemark element to extract from.
   * @returns {Object} - An object containing extended data properties.
   */
  GeoKMLer.prototype.extractExtendedData = function (placemark) {
    const extendedData = {};
    const extendedDataTag = this.getChildNode(placemark, "ExtendedData");
    if (!extendedDataTag) return extendedData;

    const simpleDatas = this.getChildNodes(extendedDataTag, "SimpleData");
    simpleDatas.forEach((data) => {
      const name = data.getAttribute("name");
      const value = this.nodeVal(data);
      if (name && value !== null) {
        extendedData[`ex_${name}`] = value.trim();
      }
    });

    return extendedData;
  };

  /**
   * Fetches the value of a text node.
   * @param {Node} x - The node to extract the value from.
   * @returns {string} - The text content of the node.
   */
  GeoKMLer.prototype.nodeVal = function (x) {
    return x ? x.textContent || "" : "";
  };

  /**
   * Retrieves a single child node of a specified tag name.
   * @param {Element} x - The parent element.
   * @param {string} y - The tag name of the child node.
   * @returns {Element|null} - The first matching child node or null if none are found.
   */
  GeoKMLer.prototype.getChildNode = function (x, y) {
    const nodeList = x.getElementsByTagName(y);
    return nodeList.length ? nodeList[0] : null;
  };

  /**
   * Retrieves all child nodes of a specified tag name.
   * @param {Element} x - The parent element.
   * @param {string} y - The tag name of the child nodes.
   * @returns {Array} - An array of matching child nodes.
   */
  GeoKMLer.prototype.getChildNodes = function (x, y) {
    return Array.from(x.getElementsByTagName(y));
  };

  /**
   * Retrieves an attribute value from an element.
   * @param {Element} x - The element to extract the attribute from.
   * @param {string} y - The name of the attribute.
   * @returns {string|null} - The attribute value or null if not present.
   */
  GeoKMLer.prototype.attr = function (x, y) {
    return x.getAttribute(y);
  };

  /**
   * Retrieves a floating-point attribute value from an element.
   * @param {Element} x - The element to extract the attribute from.
   * @param {string} y - The name of the attribute.
   * @returns {number} - The parsed floating-point attribute value.
   */
  GeoKMLer.prototype.attrf = function (x, y) {
    return parseFloat(this.attr(x, y));
  };

  /**
   * Normalizes an XML node to combine adjacent text nodes.
   * @param {Node} el - The XML node to normalize.
   * @returns {Node} - The normalized node.
   */
  GeoKMLer.prototype.norm = function (el) {
    if (el.normalize) el.normalize();
    return el;
  };

  /**
   * Creates a GeoJSON feature for a given geometry type and coordinates.
   * @param {string} type - The geometry type (Point, LineString, Polygon).
   * @param {Array} coords - The coordinates for the geometry.
   * @param {Object} props - The properties of the feature.
   * @returns {Object} - The created GeoJSON feature.
   */
  GeoKMLer.prototype.makeFeature = function (type, coords, props) {
    return {
      type: "Feature",
      geometry: {
        type: type,
        coordinates: coords,
      },
      properties: props,
    };
  };

  /**
   * Converts a KML Point to a GeoJSON Point feature.
   * @param {Element} node - The Point element.
   * @param {Element} placemark - The parent Placemark element.
   * @param {Object} props - The properties of the feature.
   * @returns {Object} - A GeoJSON Point feature.
   */
  GeoKMLer.prototype.pointToPoint = function (node, placemark, props) {
    const coord = this.coordFromString(node.getElementsByTagName("coordinates")[0].textContent)[0];
    return this.makeFeature("Point", coord, props);
  };

  /**
   * Converts a KML LineString to a GeoJSON LineString feature.
   * @param {Element} node - The LineString element.
   * @param {Element} placemark - The parent Placemark element.
   * @param {Object} props - The properties of the feature.
   * @returns {Object} - A GeoJSON LineString feature.
   */
  GeoKMLer.prototype.lineStringToLineString = function (node, placemark, props) {
    const coords = this.coordFromString(node.getElementsByTagName("coordinates")[0].textContent);
    return this.makeFeature("LineString", coords, props);
  };

  /**
   * Converts a KML Polygon to a GeoJSON Polygon feature.
   * @param {Element} node - The Polygon element.
   * @param {Element} placemark - The parent Placemark element.
   * @param {Object} props - The properties of the feature.
   * @returns {Object} - A GeoJSON Polygon feature.
   */
  GeoKMLer.prototype.polygonToPolygon = function (node, placemark, props) {
    const coords = [];
    for (const boundary of node.getElementsByTagName("LinearRing")) {
      coords.push(this.coordFromString(boundary.getElementsByTagName("coordinates")[0].textContent));
    }
    return this.makeFeature("Polygon", coords, props);
  };

  /**
   * Processes a MultiGeometry and converts its geometries to GeoJSON features.
   * @param {Element} node - The MultiGeometry element.
   * @param {Element} placemark - The parent Placemark element.
   * @param {Object} props - The properties of the features.
   * @returns {Array} - An array of GeoJSON features.
   */
  GeoKMLer.prototype.handleMultiGeometry = function (node, placemark, props) {
    const features = [];
    for (const element of node.children) {
      switch (element.tagName) {
        case "Point":
          features.push(this.pointToPoint(element, placemark, props));
          break;
        case "LineString":
          features.push(this.lineStringToLineString(element, placemark, props));
          break;
        case "Polygon":
          features.push(this.polygonToPolygon(element, placemark, props));
          break;
        case "MultiGeometry":
          features.push(...this.handleMultiGeometry(element, placemark, props));
          break;
      }
    }
    return features;
  };

  /**
   * Extracts properties from a Placemark, excluding geometry elements.
   * @param {Element} placemark - The Placemark element to extract properties from.
   * @returns {Object} - An object containing placemark properties.
   */
  GeoKMLer.prototype.extractProperties = function (placemark) {
    const props = {};
    for (const n of placemark.children) {
      if (!["Point", "LineString", "Polygon", "MultiGeometry", "LinearRing", "style", "styleMap", "styleUrl", "TimeSpan", "TimeStamp"].includes(n.tagName)) {
        // Ensure "ExtendedData" is not added directly.
        if (n.tagName !== "ExtendedData") {
          props[n.tagName] = n.textContent.trim();
        }
      }
    }
    return props;
  };

  return GeoKMLer;
})();