-
Notifications
You must be signed in to change notification settings - Fork 0
Data Models
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.
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.
A trail
is represented in GeoJSON format as a FeatureCollection
including:
- A
Trail
Folder
feature that contains oneLineString
feature composed of all the coordinates of the trail - A
Trailheads
Folder
feature that contains two or morePoint
features to serve as the start and end of the route - A
Campsites
Folder
feature that contains one or morePoint
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
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.
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.
A route
is represented in GeoJSON format as a FeatureCollection
including:
- A
Trail
Folder
feature that contains oneLineString
feature composed of every coordinate of the full trail, as well as an individualLineString
for each day - A
Trailheads
Folder
feature that contains twoPoint
features to serve as the start and end of the route - A
Campsites
Folder
feature that contains zero or morePoint
features for each overnight stay along the 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.
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:
-
date
refers to the JavaScript Date -
start
is aPoint
feature where the day starts (trailhead or campsite) -
end
is aPoint
feature where the day ends (trailhead or campsite) -
prev_site
is aPoint
feature that points to the previous campsite relative to theend
-
next_site
is aPoint
feature that points to the next campsite relative to theend
-
length
refers to the distance in kilometers between thestart
andend
-
elevationGain
refers to the elevation gain in meters between thestart
andend
-
elevationGain
refers to the elevation loss in meters between thestart
andend
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.