GeoGPXer

GeoGPXer is a JavaScript library designed to convert GPX data into GeoJSON format efficiently. It supports the conversion of waypoints, tracks, and routes, with additional handling for GPX extensions.

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/523870/1614123/GeoGPXer.js

// ==UserScript==
// @name                GeoGPXer
// @namespace           https://github.com/JS55CT
// @description         GeoGPXer is a JavaScript library designed to convert GPX data into GeoJSON format efficiently. It supports the conversion of waypoints, tracks, and routes, with additional handling for GPX extensions.
// @version             2.1.1
// @author              JS55CT
// @license             GNU General Public License v3.0
// @match              *://this-library-is-not-supposed-to-run.com/*
// ==/UserScript==

/***********************************************************
 * ## Project Home < https://github.com/JS55CT/WME-GeoFile/tree/main/GeoGPXer >
 *  Derived from logic of https://github.com/M-Reimer/gpx2geojson/tree/master (LGPL-3.0 license)
 **************************************************************/

/**
 * @desc The GeoGPXer namespace.
 * @namespace
 * @global
 */
var GeoGPXer = (function () {
  // Define the GeoGPXer constructor
  function GeoGPXer(obj) {
    if (obj instanceof GeoGPXer) return obj;
    if (!(this instanceof GeoGPXer)) return new GeoGPXer(obj);
    this._wrapped = obj;
  }

  /**
   * @desc Compares two coordinate arrays to determine if they are identical.
   *        Assumes coordinates are arrays of numbers representing geographic points.
   * @param {Array} coord1 - First coordinate array.
   * @param {Array} coord2 - Second coordinate array.
   * @return {Boolean} Returns true if both coordinates are identical, false otherwise.
   */
  function areCoordsSame(coord1, coord2) {
    if (coord1.length !== coord2.length) return false;
    for (let i = 0; i < coord1.length; i++) {
      if (coord1[i] !== coord2[i]) return false;
    }
    return true;
  }

  /**
   * @desc Parses GPX text and returns an XML Document.
   * @param {String} gpxText - The GPX data as a string.
   * @return {Document} Parsed XML Document.
   */
  GeoGPXer.prototype.read = function (gpxText) {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(gpxText, '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 new Error('Failed to parse GPX. See console for details.');
    }

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

  /**
   * @desc Converts an XML Document to GeoJSON FeatureCollection.
   * @param {Document} document - Parsed XML document of GPX data.
   * @param {Boolean} includeElevation - Whether to include elevation data in coordinates.
   * @return {Object} GeoJSON FeatureCollection.
   */
  GeoGPXer.prototype.toGeoJSON = function (document, includeElevation = false) {
    const features = [];
    for (const n of document.firstChild.childNodes) {
      switch (n.tagName) {
        case 'wpt':
          features.push(this.wptToPoint(n, includeElevation));
          break;
        case 'trk':
          features.push(...this.trkToMultiLineStringOrPolygon(n, includeElevation));
          break;
        case 'rte':
          const routeFeature = this.rteToLineStringOrPolygon(n, includeElevation);
          if (routeFeature) {
            features.push(routeFeature);
          }
          break;
      }
    }
    return {
      type: 'FeatureCollection',
      features: features,
    };
  };

  /**
   * @desc Extracts coordinates from a node.
   * @param {Node} node - GPX node containing coordinates.
   * @param {Boolean} includeElevation - Whether to include elevation data.
   * @return {Array} Array of coordinates [longitude, latitude, elevation].
   */
  GeoGPXer.prototype.coordFromNode = function (node, includeElevation = false) {
    const coords = [parseFloat(node.getAttribute('lon')), parseFloat(node.getAttribute('lat'))];
    if (includeElevation) {
      const eleNode = node.getElementsByTagName('ele')[0];
      const elevation = eleNode ? parseFloat(eleNode.textContent) : 0;
      coords.push(elevation);
    }
    return coords;
  };

  /**
   * @desc Creates a GeoJSON feature.
   * @param {String} type - Type of geometry (Point, LineString, etc.).
   * @param {Array} coords - Coordinates for the geometry.
   * @param {Object} props - Properties of the feature.
   * @return {Object} GeoJSON feature.
   */
  GeoGPXer.prototype.makeFeature = function (type, coords, props) {
    return {
      type: 'Feature',
      geometry: {
        type: type,
        coordinates: coords,
      },
      properties: props,
    };
  };

  /**
   * @desc Converts a waypoint node to a GeoJSON Point feature.
   * @param {Node} node - GPX waypoint node.
   * @param {Boolean} includeElevation - Whether to include elevation data.
   * @return {Object} GeoJSON Point feature.
   */
  GeoGPXer.prototype.wptToPoint = function (node, includeElevation = false) {
    const coord = this.coordFromNode(node, includeElevation);
    const props = this.extractProperties(node);
    return this.makeFeature('Point', coord, props);
  };

  /**
   * @desc Converts a track node to a GeoJSON Polygon or MultiLineString features.
   *        Determines if each track segment should be converted to a Polygon by
   *        checking if it has four or more coordinate pairs with the first and last
   *        coordinates being the same. If not, it remains a MultiLineString.
   * @param {Node} node - GPX track node.
   * @param {Boolean} includeElevation - Whether to include elevation data in coordinates.
   * @return {Array} Array of GeoJSON features which could either be Polygons or MultiLineStrings.
   */
  GeoGPXer.prototype.trkToMultiLineStringOrPolygon = function (node, includeElevation = false) {
    const features = [];
    const props = this.extractProperties(node);
    for (const n of node.childNodes) {
      if (n.tagName === 'trkseg') {
        const coords = [];
        for (const trkpt of n.getElementsByTagName('trkpt')) {
          coords.push(this.coordFromNode(trkpt, includeElevation));
        }

        if (coords.length >= 4 && areCoordsSame(coords[0], coords[coords.length - 1])) {
          // Convert to Polygon if conditions are met
          features.push(this.makeFeature('Polygon', [coords], props));
        } else {
          // Otherwise treat as MultiLineString
          features.push(this.makeFeature('MultiLineString', [coords], props));
        }
      }
    }
    return features;
  };

  /**
   * @desc Converts a route node to a GeoJSON feature as either a Polygon or LineString.
   *        Determines whether the route can be converted into a Polygon by checking
   *        if it has four or more coordinate pairs with the first and last coordinates
   *        being the same. If not, it treats the route as a LineString.
   * @param {Node} node - GPX route node.
   * @param {Boolean} includeElevation - Whether to include elevation data in coordinates.
   * @return {Object} GeoJSON feature, which could be a Polygon or LineString.
   */
  GeoGPXer.prototype.rteToLineStringOrPolygon = function (node, includeElevation = false) {
    const coords = [];
    const props = this.extractProperties(node);
    for (const n of node.childNodes) {
      if (n.tagName === 'rtept') {
        coords.push(this.coordFromNode(n, includeElevation));
      }
    }

    if (coords.length >= 4 && areCoordsSame(coords[0], coords[coords.length - 1])) {
      // Convert to Polygon if conditions are met
      return this.makeFeature('Polygon', [coords], props);
    } else {
      // Otherwise treat as LineString
      return this.makeFeature('LineString', coords, props);
    }
  };

  /**
   * @desc Extracts properties from a GPX node.
   * @param {Node} node - GPX node.
   * @return {Object} Properties extracted from the node.
   */
  GeoGPXer.prototype.extractProperties = function (node) {
    const props = {};
    for (const n of node.childNodes) {
      if (n.nodeType === Node.ELEMENT_NODE && n.tagName !== 'extensions') {
        props[n.tagName] = n.textContent;
      }
    }
    const extensions = node.getElementsByTagName('extensions');
    if (extensions.length > 0) {
      for (const ext of extensions[0].childNodes) {
        if (ext.nodeType === Node.ELEMENT_NODE) {
          props[`ex_${ext.tagName}`] = ext.textContent;
        }
      }
    }
    return props;
  };

  return GeoGPXer; // Return the GeoGPXer constructor
})();