Skip to content

Data Models

Alex McCafferty edited this page Jul 2, 2024 · 10 revisions

The most important data models used by Trailtuner for input and output are in GeoJSON format. There is one other non-typed model within the source code that borrows the same Feature objects found in a GeoJSON, along with some additional attributes. However, if you understand the basics of how a GeoJSON is structured, it should be fairly easy to understand all of the objects in this application.

Note: in hindsight, this project should have been written in TypeScript, but uses type-less JavaScript instead. Oh well.

Overview

At the highest level, this application takes in a trail and outputs a route.

A trail is a data structure representing an entire trail. Effectively, its a GPS file made up a series of coordinates. Each coordinate is like a node. Connecting nodes makes line, which is how a LineString GeoJSON feature can be visualized. A Point feature is simply a single coordinate pair. A GeoJSON object with a LineString feature with its associated Point features (trailheads and campsites) compose a trail.

A route is a data structure representing an itinerary along a trail. It the resulting output made by running a route generation algorithm on the input trail. Variables include input by the user, such as the number of days the route should take, or the number of miles each day should be traveled between two trailheads. A route can can also be represented as a GeoJSON object, the key difference when compared to the trail GeoJSON is that it only includes a start and end trailhead, a campsite for each planned night, and a series of lines/coordinates for each day of traveling.

Trail

A trail is represented in GeoJSON format as a FeatureCollection including:

  1. A Trail Folder feature that contains one LineString feature composed of all the coordinates of the trail
  2. A Trailheads Folder feature that contains two or more Point features to serve as the start and end of the route
  3. A Campsites Folder feature that contains one or more Point feature(s) for each overnight stay along the route

Notes:

  • the coordinates of each Point must be on trail, or the app will not recognize it as valid
  • creating all of the above features is easiest using CalTopo and exporting to GeoJSON format

Example Trail w/o elevation

Here is an "Example Trail" exported to GeoJSON directly from CalTopo:

{"features": [{
  "geometry": null,
  "id": "179b5ea4-95d9-44f4-ae94-c95d3be6285b",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Campsites",
    "class": "Folder",
    "updated": 1719687343133,
    "labelVisible": true
  }
},
{
  "geometry": null,
  "id": "54336a47-d75b-4268-b626-9315f3e59261",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Trail",
    "class": "Folder",
    "updated": 1719687343131,
    "labelVisible": true
  }
},
{
  "geometry": null,
  "id": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Trailheads",
    "class": "Folder",
    "updated": 1719687343134,
    "labelVisible": true
  }
},
{
  "geometry": {
    "coordinates": [
      -80.0472,
      40.45939,
      0,
      0
    ],
    "type": "Point"
  },
  "id": "1c89f93f-4868-42ca-8dcb-6705ada55723",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Trailhead 1",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687375869,
    "folderId": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
    "marker-rotation": null
  }
},
{
  "geometry": {
    "coordinates": [
      -80.02829,
      40.44469,
      0,
      0
    ],
    "type": "Point"
  },
  "id": "e3cac00f-512c-4405-8ec9-7d41d9b972f9",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Trailhead 2",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687402030,
    "folderId": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
    "marker-rotation": null
  }
},
{
  "geometry": {
    "coordinates": [
      -80.03877,
      40.45239,
      0,
      0
    ],
    "type": "Point"
  },
  "id": "6cce7628-1b4b-4b5d-8885-c89802038f27",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Campsite 1",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687426331,
    "folderId": "179b5ea4-95d9-44f4-ae94-c95d3be6285b",
    "marker-rotation": null
  }
},
{
  "geometry": {
    "coordinates": [
      [
        -80.04720,
        40.45938
      ],
      [
        -80.04714,
        40.45941
      ],
      [
        -80.04715,
        40.45942
      ],
      [
        -80.04715,
        40.45942
      ],
      [
        -80.04728,
        40.45951
      ]
    ],
    "type": "LineString"
  },
  "id": "6bf55fa5-b0db-4122-9b26-13556a6114db",
  "type": "Feature",
  "properties": {
    "stroke-opacity": 1,
    "creator": "T71VU8",
    "pattern": "solid",
    "description": "",
    "stroke-width": 2,
    "title": "Example Trail",
    "fill": "#FF0000",
    "stroke": "#FF0000",
    "class": "Shape",
    "updated": 1719687382578,
    "folderId": "54336a47-d75b-4268-b626-9315f3e59261"
  }
}], "type": "FeatureCollection"}

Coordinates (latitude and longitude) are decimal numbers in index [0] and [1] of coordinates[] respectively. Every feature, except for a Folder, will include coordinates.

However, notice that CalTopo only exports the coordinates and there is no associated elevation data. This is because elevation data is stored in a standardized database that can be accessed via APIs, such as Google Elevation API. Most, if not all, mapping tools do not bother exporting elevation data because they did not generate it in the first place! Consider that mapping on a 2D map does not factor in elevation, only latitude and longitude coordinates of each position sample.

Example Trail w/ elevation

For Trailtuner, elevation is considered an input and must be pre-appended for the elevation chart and elevation details to load. Elevation is passed in as a decimal number in the [2]nd index of coordinates[].

coordinates: [lat, long, elev]

Retrieving elevation data for every coordinate pair and appending it to the GeoJSON is a bit of a PITA, but only needs to be done once. I wrote an external script to call the Google Maps Elevation API to retrieve elevation data and append the elevation to the original GeoJSON input:

https://github.com/amccafferty42/geojson-elevation

Here is the same "Example Trail" after running the elevation script:

{"features": [{
  "geometry": null,
  "id": "179b5ea4-95d9-44f4-ae94-c95d3be6285b",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Campsites",
    "class": "Folder",
    "updated": 1719687343133,
    "labelVisible": true
  }
},
{
  "geometry": null,
  "id": "54336a47-d75b-4268-b626-9315f3e59261",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Trail",
    "class": "Folder",
    "updated": 1719687343131,
    "labelVisible": true
  }
},
{
  "geometry": null,
  "id": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Trailheads",
    "class": "Folder",
    "updated": 1719687343134,
    "labelVisible": true
  }
},
{
  "geometry": {
    "coordinates": [
      -80.0472,
      40.45939,
      2100.594970703125,
      0
    ],
    "type": "Point"
  },
  "id": "1c89f93f-4868-42ca-8dcb-6705ada55723",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Trailhead 1",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687375869,
    "folderId": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
    "marker-rotation": null
  }
},
{
  "geometry": {
    "coordinates": [
      -80.02829,
      40.44469,
      1898.144409179688,
      0
    ],
    "type": "Point"
  },
  "id": "e3cac00f-512c-4405-8ec9-7d41d9b972f9",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Trailhead 2",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687402030,
    "folderId": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
    "marker-rotation": null
  }
},
{
  "geometry": {
    "coordinates": [
      -80.03877,
      40.45239,
      2056.326416015625,
      0
    ],
    "type": "Point"
  },
  "id": "6cce7628-1b4b-4b5d-8885-c89802038f27",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Campsite 1",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687426331,
    "folderId": "179b5ea4-95d9-44f4-ae94-c95d3be6285b",
    "marker-rotation": null
  }
},
{
  "geometry": {
    "coordinates": [
      [
        -80.04720,
        40.45938,
        231.57737
      ],
      [
        -80.04714,
        40.45941,
        228.57859
      ],
      [
        -80.04715,
        40.45942,
        228.56114
      ],
      [
        -80.04715,
        40.45942,
        228.56114
      ],
      [
        -80.04728,
        40.45951,
        231.79740
      ]
    ],
    "type": "LineString"
  },
  "id": "6bf55fa5-b0db-4122-9b26-13556a6114db",
  "type": "Feature",
  "properties": {
    "stroke-opacity": 1,
    "creator": "T71VU8",
    "pattern": "solid",
    "description": "",
    "stroke-width": 2,
    "title": "Example Trail",
    "fill": "#FF0000",
    "stroke": "#FF0000",
    "class": "Shape",
    "updated": 1719687382578,
    "folderId": "54336a47-d75b-4268-b626-9315f3e59261"
  }
}], "type": "FeatureCollection"}

This trail GeoJSON is now ready to be input into the application via the change trail button on the UI. It is important to understand that a trail does not quite exist outside of the GeoJSON format that is initially fed into the application. Rather, the application simply extracts all of the Point features (linked to their respective Folder features) into variables: trailFeature, trailheadFeatures[], and campsiteFeatures[]. From this point, the data is still in the JSON structure of an individual Feature, but the GeoJSON as a whole is sort of forgotten about.

Route

A route is represented in GeoJSON format as a FeatureCollection including:

  1. A Trail Folder feature that contains one LineString feature composed of every coordinate of the full trail, as well as an individual LineString for each day
  2. A Trailheads Folder feature that contains two Point features to serve as the start and end of the route
  3. A Campsites Folder feature that contains zero or more Point features for each overnight stay along the route

Example Route

Here is an "Example Trail" exported directly from Trailtuner:

{"features": [{
  "geometry": null,
  "id": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Trailheads",
    "class": "Folder",
    "updated": 1719687343134,
    "labelVisible": true
  }
},
{
  "geometry": null,
  "id": "179b5ea4-95d9-44f4-ae94-c95d3be6285b",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Campsites",
    "class": "Folder",
    "updated": 1719687343133,
    "labelVisible": true
  }
},
{
  "geometry": null,
  "id": "54336a47-d75b-4268-b626-9315f3e59261",
  "type": "Feature",
  "properties": {
    "creator": "T71VU8",
    "visible": true,
    "title": "Trail",
    "class": "Folder",
    "updated": 1719687343131,
    "labelVisible": true
  }
},
{
  "geometry": {
    "coordinates": [
      -80.0472,
      40.45939,
      2100.594970703125,
      0,
      0
    ],
    "type": "Point"
  },
  "id": "1c89f93f-4868-42ca-8dcb-6705ada55723",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Trailhead 1",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687375869,
    "folderId": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
    "marker-rotation": null,
    "distance": 0,
    "elevation": 231.5773773193359,
    "elevationGain": 0,
    "elevationLoss": 0
  }
},
{
  "geometry": {
    "coordinates": [
      -80.0472,
      40.45939,
      2100.594970703125,
      0,
      0
    ],
    "type": "Point"
  },
  "id": "1c89f93f-4868-42ca-8dcb-6705ada55723",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Trailhead 1",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687375869,
    "folderId": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
    "marker-rotation": null,
    "distance": 0,
    "elevation": 231.5773773193359,
    "elevationGain": 0,
    "elevationLoss": 0
  }
},
{
  "geometry": {
    "coordinates": [
      -80.02829,
      40.44469,
      1898.144409179688,
      2.346022393003848,
      2.346022393003848
    ],
    "type": "Point"
  },
  "id": "e3cac00f-512c-4405-8ec9-7d41d9b972f9",
  "type": "Feature",
  "properties": {
    "marker-symbol": "point",
    "creator": "T71VU8",
    "marker-color": "FF0000",
    "description": "",
    "title": "Example Trailhead 2",
    "marker-size": "1",
    "class": "Marker",
    "updated": 1719687402030,
    "folderId": "d2b4bbd1-7f79-4c7c-94c8-1c7afe609b1c",
    "marker-rotation": null,
    "distance": 2.346022393003848,
    "elevation": 222.6386566162109,
    "elevationGain": 3707.1033477783203,
    "elevationLoss": 3716.0420684814453
  }
},
{
  "geometry": {
    "type": "LineString",
    "coordinates": [
      [
        -80.0472,
        40.45939,
        2100.594970,
        0,
        0
      ],
      [
        -80.0472,
        40.45967,
        2101.59497,
        0,
        0
      ]
    ]
  },
  "properties": {
    "stroke-opacity": 1,
    "creator": "T71VU8",
    "pattern": "solid",
    "description": "",
    "stroke-width": 2,
    "title": "Day 1",
    "fill": "#FF0000",
    "stroke": "#FF0000",
    "class": "Shape",
    "updated": 1719687382578,
    "folderId": "54336a47-d75b-4268-b626-9315f3e59261",
    "date": "Mon, 7/1/24",
    "elevationGain": 0,
    "elevationLoss": 0
  },
  "type": "Feature"
},
{
  "geometry": {
    "type": "LineString",
    "coordinates": [
      [
        -80.0472,
        40.45939,
        2100.594970703125,
        0,
        0
      ],
      [
        -80.047146481023,
        40.45941577723,
        228.5785980224609,
        0.006328421833432172,
        0.006328421833432172
      ],
      [
        -80.04715,
        40.45942,
        228.5611419677734,
        0.006884677409546043,
        0.006884677409546043
      ],
      [
        -80.04715,
        40.45942,
        228.5611419677734,
        0.006884677409546043,
        0.006884677409546043
      ],
      [
        -80.04728,
        40.45951,
        231.7974090576172,
        0.02210285093748887,
        0.02210285093748887
      ]
    ]
  },
  "properties": {
    "stroke-opacity": 1,
    "creator": "T71VU8",
    "pattern": "solid",
    "description": "",
    "stroke-width": 2,
    "title": "Day 2",
    "fill": "#FF0000",
    "stroke": "#FF0000",
    "class": "Shape",
    "updated": 1719687382578,
    "folderId": "54336a47-d75b-4268-b626-9315f3e59261",
    "date": "Tue, 7/2/24",
    "elevationGain": 0,
    "elevationLoss": 0
  },
  "type": "Feature"
},
{
  "geometry": {
    "type": "LineString",
    "coordinates": [
      [
        -80.0472,
        40.45939,
        2100.594970703125,
        0,
        0
      ],
      [
        -80.047146481023,
        40.45941577723,
        228.5785980224609,
        0.006328421833432172
      ],
      [
        -80.04715,
        40.45942,
        228.5611419677734,
        0.006884677409546043
      ],
      [
        -80.04715,
        40.45942,
        228.5611419677734,
        0.006884677409546043
      ],
      [
        -80.04728,
        40.45951,
        231.7974090576172,
        0.02210285093748887
      ]
    ]
  },
  "properties": {
    "stroke-opacity": 1,
    "creator": "T71VU8",
    "pattern": "solid",
    "description": "",
    "stroke-width": 2,
    "title": "Full Route",
    "fill": "#FF0000",
    "stroke": "#FF0000",
    "class": "Shape",
    "updated": 1719687382578,
    "folderId": "54336a47-d75b-4268-b626-9315f3e59261"
  },
  "type": "Feature"
}], "type": "FeatureCollection"}

The Trail folder now contains several LineString features, each with a sequential "title": "Day #". Combining each days' LineString features would make up the same coordinates as the LineString with "title": "Full Route".

You will also notice some additional numbers generated in the output of index [3] and [4] of coordinates[]. These are for distance in kilometers and adjusted distance in kilometers. These are not particularly important at this point, but they are essentially relative distances used in the route generation process.

Day

As previously covered, a route is the exported output of the Trailtuner application in GeoJSON format. However, when the app is processing the inputs before a finalized GeoJSON export is requested by the user, the data structure of a route is quite different. To clarify, a route is not in GeoJSON format when initially generated in the source code. This other representation of a route can be thought of as a collection of individually contained day objects.

A route as it exists in the source code is represented by a global array, route[], where each index is a day of the route. The object structure of a day is as follows:

  1. date refers to the JavaScript Date
  2. start is a Point feature where the day starts (trailhead or campsite)
  3. end is a Point feature where the day ends (trailhead or campsite)
  4. prev_site is a Point feature that points to the previous campsite relative to the end
  5. next_site is a Point feature that points to the next campsite relative to the end
  6. length refers to the distance in kilometers between the start and end
  7. elevationGain refers to the elevation gain in meters between the start and end
  8. elevationGain refers to the elevation loss in meters between the start and end

Example Day

Here is an "Example Day" of a route[]:

[{
  "date": "2024-07-01T04:00:00.000Z",
  "start": {
    "geometry": {
      "coordinates": [
        -79.49006371200086,
        39.871190202850165,
        373,
        0,
        0
      ],
      "type": "Point"
    },
    "id": "90c3c475-176f-4975-bb93-08c32546466c",
    "type": "Feature",
    "properties": {
      "marker-symbol": "circle-p",
      "creator": "T71VU8",
      "marker-color": "000000",
      "description": "",
      "title": "Example Trailhead",
      "marker-size": "1",
      "class": "Marker",
      "updated": 1692019490179,
      "folderId": "5f0f1da4-99a7-455a-831a-2eb30b38f9aa",
      "marker-rotation": null,
      "distance": 0,
      "elevation": 373.028,
      "elevationGain": 0,
      "elevationLoss": 0
    }
  },
  "end": {
    "geometry": {
      "coordinates": [
        -79.27279472351076,
        40.059089072199264,
        871.4,
        45.59384781275352,
        45.59384781275352
      ],
      "type": "Point"
    },
    "id": "304b8a9a-5bf0-49ed-90e2-945bb811a068",
    "type": "Feature",
    "properties": {
      "marker-symbol": "circle-p",
      "creator": "T71VU8",
      "marker-color": "000000",
      "description": "",
      "title": "Example Campsite",
      "marker-size": "1",
      "class": "Marker",
      "updated": 1692019490183,
      "folderId": "5f0f1da4-99a7-455a-831a-2eb30b38f9aa",
      "marker-rotation": null,
      "distance": 45.59384781275352,
      "elevation": 869.197,
      "elevationGain": 1906.3759999999986,
      "elevationLoss": 1410.2070000000026
    }
  },
  "length": 45.59384781275352,
  "elevationGain": 1906.3759999999986,
  "elevationLoss": 1410.2070000000026
},
{ . . . }]

This representation of a route includes more metadata than is necessary to export into the final GeoJSON route. Notice how there is no reference to any LineString features. This is because the specific coordinates of each days' path are not necessary for calculation at all! The original LineString for the full trail eventually needs to be split up into day-by-day sections and appended to the GeoJSON route, but this is irrelevant for generating a route candidate (it is only used to display the route segment on the map and chart).

length, elevationGain, and elevationLoss are related to the daily distance/elevation covered for each day. This information is only used to help the user plan the route and is not stored in the exported GeoJSON. This also includes the date of each day.

prev_site and next_site are references to the sites that fall just before and just after the end site, respectively. The pointers are there to allow the user to easily switch the preferred campsite of each day.

distance, and some other metadata like elevation, is stored in the properties{} of each Point feature. Trailtuner uses the relative distances in kilometers of all the Point features to generate a route, not the coordinates. Relative distance simply means that one trailhead is selected as a starting point (i.e. distance: 0), and all other trailheads/campsites are some distance in kilometers away from it (i.e. distance: 10). Notice that this data is not included in the input trail. All distances and changes in elevation are calculated when the application is loaded.

start and end Point features of each day are pieced together with the segmented LineString features derived from the trail to form the route when it is exported.

To understand how the start and end of each day of the route are determined in the first place, refer to the Route Generation page.

Clone this wiki locally