Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPX metadataType #111

Open
Raruto opened this issue Jun 4, 2023 · 2 comments
Open

GPX metadataType #111

Raruto opened this issue Jun 4, 2023 · 2 comments

Comments

@Raruto
Copy link

Raruto commented Jun 4, 2023

Hi Tom,

over years I come back to ask myself this question: Is it valid to have a properties element in an geoJSON featureCollection?

So first of all here's just a friendly reminder:

RFC 7946 - Extending GeoJSON

6.1. Foreign Members

Members not described in this specification ("foreign members") MAY be used in a GeoJSON document. Note that support for foreign members can vary across implementations, and no normative processing model for foreign members is defined. Accordingly, implementations that rely too heavily on the use of foreign members might experience reduced interoperability with other implementations.

For example, in the (abridged) Feature object shown below

{
  "type": "Feature",
  "id": "f1",
  "geometry": {...},
  "properties": {...},
  "title": "Example Feature"
}

the name/value pair of "title": "Example Feature" is a foreign member. When the value of a foreign member is an object, all the descendant members of that object are themselves foreign members.

GPX 1.1 Schema Documentation - metadataType

<xsd:complexType name="metadataType">
  <xsd:sequence>
    <-- elements must appear in this order -->
    <xsd:element name="name" type="[xsd](https://www.topografix.com/GPX/1/1/#ns_xsd):string" minOccurs="0"/>
    <xsd:element name="desc" type="[xsd](https://www.topografix.com/GPX/1/1/#ns_xsd):string" minOccurs="0"/>
    <xsd:element name="author" type="[personType](https://www.topografix.com/GPX/1/1/#type_personType)" minOccurs="0"/>
    <xsd:element name="copyright" type="[copyrightType](https://www.topografix.com/GPX/1/1/#type_copyrightType)" minOccurs="0"/>
    <xsd:element name="link" type="[linkType](https://www.topografix.com/GPX/1/1/#type_linkType)" minOccurs="0" maxOccurs="unbounded"/>
    <xsd:element name="time" type="[xsd](https://www.topografix.com/GPX/1/1/#ns_xsd):dateTime" minOccurs="0"/>
    <xsd:element name="keywords" type="[xsd](https://www.topografix.com/GPX/1/1/#ns_xsd):string" minOccurs="0"/>
   <xsd:element name="bounds" type="[boundsType](https://www.topografix.com/GPX/1/1/#type_boundsType)" minOccurs="0"/>
   <xsd:element name="extensions" type="[extensionsType](https://www.topografix.com/GPX/1/1/#type_extensionsType)" minOccurs="0"/>
  </xsd:sequence>
</xsd:complexType>

Motivation

Mainly, make it easier to access the root gpx file <name>.

Right now, others can achieve this more or less by doing like so:

// Ref: https://github.com/Raruto/leaflet-elevation/blob/e0c68cba9a71d140e4c5a4179c51ae34588b7327/src/control.js#L965-L973

let xml  = (new DOMParser()).parseFromString(data, "text/xml");
let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
let name = xml.getElementsByTagName('name');
if (xml.getElementsByTagName('parsererror').length) {
  throw 'Invalid XML';
}
if (!(type in toGeoJSON)) {
  type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
}
let geojson  = toGeoJSON[type](xml);
geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';

Draft implementation

Essentially, augmenting the FeatureCollection interface by providing a new property: metadata

lib/gpx.ts#L181-L197

import { extractMetadata } from "./gpx/metadata";

...

export function gpx(node: Document): FeatureCollection {
  return {
    type: "FeatureCollection",
    features: Array.from(gpxGen(node)),
    metadata: extractMetadata(node),
  };
}

lib/gpx/metadata.ts

Almost the same as: lib/gpx/properties.ts#L1-L36

NB some functions parameters types invoked in here must be double-checked (ref: Element and Document interfaces)

import { $, getMulti, nodeVal } from "../shared";

export function extractMetadata(node: Document) {
  const properties = getMulti(node, [
    "name",
    "desc",
    // "author",
    // "copyright",
    "time",
    "keywords",
    // bounds
  ]);

  const extensions = Array.from(
    node.getElementsByTagNameNS(
      "http://www.garmin.com/xmlschemas/GpxExtensions/v3",
      "*"
    )
  );
  for (const child of extensions) {
    if (child.parentNode?.parentNode === node) {
      properties[child.tagName.replace(":", "_")] = nodeVal(child);
    }
  }

  const links = $(node, "link");
  if (links.length) {
    properties.links = links.map((link) =>
      Object.assign(
        { href: link.getAttribute("href") },
        getMulti(link, ["text", "type"])
      )
    );
  }

  return properties;
}

lib/index.d.ts

I'm not very knowledgeable about typescript, anyway I think it could result in something like this:

// Based on https://stackoverflow.com/questions/42262565/how-to-augment-typescript-interface-in-d-ts

import * as geojson from 'geojson';

declare module 'geojson' {
  namespace GeoJSON {
    export interface FeatureCollection<G extends geojson.Geometry | null = geojson.Geometry, P = geojson.GeoJsonProperties> extends geojson.GeoJsonObject {
      metadata?: {
        name?: string;
        desc?: string;
        author?: {
          name?: string;
          email?: string;
          link?: {
            href: string;
            text?: string;
            type?: string;
          };
        };
        copyright?: {
          author?: string;
          year?: string;
          license?: string;
        };
        time?: string;
        keywords?: string;
        bounds?: [number, number, number, number]
        extensions?: any;
      };
    }
  }
}

I'm really sorry, but at the moment I can't say how much additional re-work might be needed to integrate it in the generation of current dist/index.d.ts file.

Hoping that somehow these notes can be useful

👋 Raruto

@tmcw
Copy link
Collaborator

tmcw commented Jun 5, 2023

Personally I think I'd prefer a separate method, like toGeoJSON.gpxMetadata() -> metadata object rather than putting properties on FeatureCollection. I still think that putting properties on a FeatureCollection is technically allowed but practically dangerous: as soon as you send that GeoJSON through a transformation step like Turf.js, the extra properties will be removed. They won't be accessible via most GeoJSON editors or through map libraries.

I think it just makes more sense to have a separate process to get metadata, and if people strongly want to combine that metadata with their FeatureCollection object, they can do so with { ...featureCollection, ...metadata }.

@Raruto
Copy link
Author

Raruto commented Jun 5, 2023

Based on: https://gis.stackexchange.com/a/415412

You can insert a Polygon feature without any coordinates and set your properties as you like:

{
 "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [          
        ]
      },
      "properties": {
        "description": "This is the geometry for..."
      }
    },
    {
      // Other features
    }
  ]
}

Maybe something like this would be even more easier to store and therefore portable (also when it comes to snapshot testing):

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [ ]     // eventually populated by https://www.topografix.com/GPX/1/1/#type_boundsType
      },
      "properties": {
        "_gpxType": "metadata" // everything else related to https://www.topografix.com/GPX/1/1/#type_metadataType
        "name": "...",
        "desc": "...",
        "author": {
            "name": "...",
            "email": "",
            "link": {
               "href": "...",
               "text": "...",
               "type": "...",
            }
        },
        "copyright": {
          "author": "...",
          "year": "...",
          "license": "..."
        },
        "time": "...",
        "keywords": "...",
        "extensions": ??
    },
    {
      // Other features
    }
  ]
}

Not really sure if these property names make sense:

  • "_gpxType": "metadata" --> similarly to what was already done to distinguish <rte> and <trk> tags
  • "extensions": ?? --> probably a more complex structure (right now, I don't remember what's the current gpx extensions specfication..)

but that's just to give you a quick idea.

Related info:

  • exports[`toGeoJSON > ground_overlay.kml 1`] = `
    {
    "features": [
    {
    "geometry": {
    "coordinates": [
    [
    [
    14.600668902543442,
    37.91801270483731,
    ],
    [
    15.35770894852841,
    37.92006947469352,
    ],
    [
    15.358941332345658,
    37.466463107960706,
    ],
    [
    14.60190128636069,
    37.464406338104496,
    ],
    [
    14.600668902543442,
    37.91801270483731,
    ],
    ],
    ],
    "type": "Polygon",
    },
    "properties": {
    "@geometry-type": "groundoverlay",
    "description": "Overlay shows Mount Etna erupting
    on July 13th, 2001.",
    "icon": "https://developers.google.com/kml/documentation/images/etna.jpg",
    "name": "Large-scale overlay on terrain",
    },
    "type": "Feature",
    },
    ],
    "type": "FeatureCollection",
    }
    `;
  • togeojson/lib/gpx.ts

    Lines 181 to 191 in d395dde

    /**
    *
    * Convert a GPX document to GeoJSON. The first argument, `doc`, must be a GPX
    * document as an XML DOM - not as a string. You can get this using jQuery's default
    * `.ajax` function or using a bare XMLHttpRequest with the `.response` property
    * holding an XML DOM.
    *
    * The output is a JavaScript object of GeoJSON data, same as `.kml` outputs, with the
    * addition of a `_gpxType` property on each `LineString` feature that indicates whether
    * the feature was encoded as a route (`rte`) or track (`trk`) in the GPX document.
    */

👋 Raruto

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants