add highway info to elevation profile, closes #65

This commit is contained in:
vcoppe
2024-10-03 18:14:01 +02:00
parent 0c16ddd534
commit d7a02f714a
11 changed files with 225 additions and 86 deletions

View File

@@ -775,12 +775,16 @@ export class TrackSegment extends GPXTreeLeaf {
}
}
if (i > 0 && points[i - 1].extensions && points[i - 1].extensions["gpxtpx:TrackPointExtension"] && points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] && points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface) {
let surface = points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface;
if (statistics.global.surface[surface] === undefined) {
statistics.global.surface[surface] = 0;
if (i > 0 && points[i - 1].extensions && points[i - 1].extensions["gpxtpx:TrackPointExtension"] && points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]) {
Object.entries(points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]).forEach(([key, value]) => {
if (statistics.global.extensions[key] === undefined) {
statistics.global.extensions[key] = {};
}
statistics.global.surface[surface] += dist;
if (statistics.global.extensions[key][value] === undefined) {
statistics.global.extensions[key][value] = 0;
}
statistics.global.extensions[key][value] += dist;
});
}
}
@@ -1075,11 +1079,10 @@ export class TrackPoint {
return this.extensions && this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] ? this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] : undefined;
}
getSurface(): string {
return this.extensions && this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface ? this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface : undefined;
setExtensions(extensions: Record<string, string>) {
if (Object.keys(extensions).length === 0) {
return;
}
setSurface(surface: string): void {
if (!this.extensions) {
this.extensions = {};
}
@@ -1089,7 +1092,13 @@ export class TrackPoint {
if (!this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]) {
this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] = {};
}
this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]["surface"] = surface;
Object.entries(extensions).forEach(([key, value]) => {
this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"][key] = value;
});
}
getExtensions(): Record<string, string> {
return this.extensions && this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] ? this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] : {};
}
toTrackPointType(exclude: string[] = []): TrackPointType {
@@ -1119,8 +1128,11 @@ export class TrackPoint {
if (this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] && !exclude.includes('power')) {
trkpt.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] = this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"];
}
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface && !exclude.includes('surface')) {
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] = { surface: this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface };
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] && !exclude.includes('extensions')) {
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] = {};
Object.entries(this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"]).forEach(([key, value]) => {
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"][key] = value;
});
}
}
return trkpt;
@@ -1270,7 +1282,7 @@ export class GPXStatistics {
avg: number,
count: number,
},
surface: Record<string, number>,
extensions: Record<string, Record<string, number>>,
};
local: {
points: TrackPoint[],
@@ -1341,7 +1353,7 @@ export class GPXStatistics {
avg: 0,
count: 0,
},
surface: {},
extensions: {},
};
this.local = {
points: [],
@@ -1411,11 +1423,16 @@ export class GPXStatistics {
this.global.cad.count += other.global.cad.count;
this.global.power.avg = (this.global.power.count * this.global.power.avg + other.global.power.count * other.global.power.avg) / Math.max(1, this.global.power.count + other.global.power.count);
this.global.power.count += other.global.power.count;
Object.keys(other.global.surface).forEach((surface) => {
if (this.global.surface[surface] === undefined) {
this.global.surface[surface] = 0;
Object.keys(other.global.extensions).forEach((extension) => {
if (this.global.extensions[extension] === undefined) {
this.global.extensions[extension] = {};
}
this.global.surface[surface] += other.global.surface[surface];
Object.keys(other.global.extensions[extension]).forEach((value) => {
if (this.global.extensions[extension][value] === undefined) {
this.global.extensions[extension][value] = 0;
}
this.global.extensions[extension][value] += other.global.extensions[extension][value];
});
});
}

View File

@@ -92,9 +92,7 @@ export type TrackPointExtension = {
'gpxtpx:atemp'?: number;
'gpxtpx:hr'?: number;
'gpxtpx:cad'?: number;
'gpxtpx:Extensions'?: {
surface?: string;
};
'gpxtpx:Extensions'?: Record<string, string>;
}
export type PowerExtension = {

View File

@@ -0,0 +1,64 @@
export const surfaceColors: { [key: string]: string } = {
"missing": "#d1d1d1",
"paved": "#8c8c8c",
"unpaved": "#6b443a",
"asphalt": "#8c8c8c",
"concrete": "#8c8c8c",
"cobblestone": "#ffd991",
"paving_stones": "#8c8c8c",
"sett": "#ffd991",
"metal": "#8c8c8c",
"wood": "#6b443a",
"compacted": "#ffffa8",
"fine_gravel": "#ffffa8",
"gravel": "#ffffa8",
"pebblestone": "#ffffa8",
"rock": "#ffd991",
"dirt": "#ffffa8",
"ground": "#6b443a",
"earth": "#6b443a",
"mud": "#6b443a",
"sand": "#ffffc4",
"grass": "#61b55c",
"grass_paver": "#61b55c",
"clay": "#6b443a",
"stone": "#ffd991",
};
export const highwayColors: { [key: string]: string } = {
"missing": "#d1d1d1",
"residential": "#73b2ff",
"service": "#b3b3cc",
"track": "#946f43",
"unclassified": "#e0e0e0",
"footway": "#a3c989",
"tertiary": "#ffdd7f",
"path": "#a3c989",
"secondary": "#ffd75f",
"primary": "#ff6e5c",
"cycleway": "#ffbb6e",
"trunk": "#ff5e4d",
"living_street": "#9de2ff",
"motorway": "#ff4d33",
"motorway_link": "#ff947f",
"steps": "#8d91a2",
"road": "#e0e0e0",
"pedestrian": "#c1c8e4",
"trunk_link": "#ff947f",
"primary_link": "#ff8d7b",
"secondary_link": "#ffcb66",
"tertiary_link": "#ffdd8b",
"construction": "#e09a4a",
"bridleway": "#a3c989",
"platform": "#c4c4c4",
"proposed": "#e0e0e0",
"raceway": "#ff0000",
"rest_area": "#73b2ff",
"abandoned": "#e0e0e0",
"services": "#ffe066",
"corridor": "#c4c4c4",
"bus_stop": "#ffe6e6",
"busway": "#ffe6e6",
"elevator": "#c4c4c4",
"via_ferrata": "#8d91a2"
};

View File

@@ -1,26 +0,0 @@
export const surfaceColors: { [key: string]: string } = {
'missing': '#d1d1d1',
'paved': '#8c8c8c',
'unpaved': '#6b443a',
'asphalt': '#8c8c8c',
'concrete': '#8c8c8c',
'cobblestone': '#ffd991',
'paving_stones': '#8c8c8c',
'sett': '#ffd991',
'metal': '#8c8c8c',
'wood': '#6b443a',
'compacted': '#ffffa8',
'fine_gravel': '#ffffa8',
'gravel': '#ffffa8',
'pebblestone': '#ffffa8',
'rock': '#ffd991',
'dirt': '#ffffa8',
'ground': '#6b443a',
'earth': '#6b443a',
'mud': '#6b443a',
'sand': '#ffffc4',
'grass': '#61b55c',
'grass_paver': '#61b55c',
'clay': '#6b443a',
'stone': '#ffd991',
}

View File

@@ -16,9 +16,10 @@
Zap,
Circle,
Check,
ChartNoAxesColumn
ChartNoAxesColumn,
Construction
} from 'lucide-svelte';
import { surfaceColors } from '$lib/assets/surfaces';
import { surfaceColors, highwayColors } from '$lib/assets/colors';
import { _, locale } from 'svelte-i18n';
import {
getCadenceWithUnits,
@@ -43,7 +44,7 @@
export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let additionalDatasets: string[];
export let elevationFill: 'slope' | 'surface' | undefined;
export let elevationFill: 'slope' | 'surface' | 'highway' | undefined;
export let showControls: boolean = true;
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
@@ -151,7 +152,10 @@
segment: point.slope.segment.toFixed(1),
length: getDistanceWithUnits(point.slope.length)
};
let surface = point.surface ? point.surface : 'unknown';
let surface = point.extensions.surface ? point.extensions.surface : 'unknown';
let highway = point.extensions.highway ? point.extensions.highway : 'unknown';
let sacScale = point.extensions.sac_scale;
let mtbScale = point.extensions['mtb:scale'];
let labels = [
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
@@ -164,6 +168,17 @@
);
}
if (elevationFill === 'highway') {
labels.push(
` ${$_('quantities.highway')}: ${$_(`toolbar.routing.highway.${highway}`)}${
sacScale ? ` (${$_(`toolbar.routing.sac_scale.${sacScale}`)})` : ''
}`
);
if (mtbScale) {
labels.push(` ${$_('toolbar.routing.mtb_scale')}: ${mtbScale}`);
}
}
if (point.time) {
labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`);
}
@@ -353,7 +368,7 @@
segment: data.local.slope.segment[index],
length: data.local.slope.length[index]
},
surface: point.getSurface(),
extensions: point.getExtensions(),
coordinates: point.getCoordinates(),
index: index
};
@@ -448,10 +463,15 @@
}
function surfaceFillCallback(context) {
let surface = context.p0.raw.surface;
let surface = context.p0.raw.extensions.surface;
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
}
function highwayFillCallback(context) {
let highway = context.p0.raw.extensions.highway;
return highwayColors[highway] ? highwayColors[highway] : highwayColors.missing;
}
$: if (chart) {
if (elevationFill === 'slope') {
chart.data.datasets[0]['segment'] = {
@@ -461,6 +481,10 @@
chart.data.datasets[0]['segment'] = {
backgroundColor: surfaceFillCallback
};
} else if (elevationFill === 'highway') {
chart.data.datasets[0]['segment'] = {
backgroundColor: highwayFillCallback
};
} else {
chart.data.datasets[0]['segment'] = {};
}
@@ -551,7 +575,7 @@
<ChartNoAxesColumn size="18" />
</ButtonWithTooltip>
</Popover.Trigger>
<Popover.Content class="w-fit p-0 flex flex-col divide-y" side="left" sideOffset={-32}>
<Popover.Content class="w-fit p-0 flex flex-col divide-y" side="top" sideOffset={-32}>
<ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1"
type="single"
@@ -582,6 +606,19 @@
<BrickWall size="15" class="mr-1" />
{$_('quantities.surface')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="highway"
variant="outline"
>
<div class="w-6 flex justify-center items-center">
{#if elevationFill === 'highway'}
<Circle class="h-1.5 w-1.5 fill-current text-current" />
{/if}
</div>
<Construction size="15" class="mr-1" />
{$_('quantities.highway')}
</ToggleGroup.Item>
</ToggleGroup.Root>
<ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1"

View File

@@ -16,7 +16,7 @@
import {
Download,
Zap,
BrickWall,
Earth,
HeartPulse,
Orbit,
Thermometer,
@@ -31,19 +31,19 @@
let open = false;
let exportOptions: Record<string, boolean> = {
time: true,
surface: true,
hr: true,
cad: true,
atemp: true,
power: true
power: true,
extensions: true
};
let hide: Record<string, boolean> = {
time: false,
surface: false,
hr: false,
cad: false,
atemp: false,
power: false
power: false,
extensions: false
};
$: if ($exportState !== ExportState.NONE) {
@@ -63,11 +63,11 @@
}
hide.time = statistics.global.time.total === 0;
hide.surface = !Object.keys(statistics.global.surface).some((key) => key !== 'unknown');
hide.hr = statistics.global.hr.count === 0;
hide.cad = statistics.global.cad.count === 0;
hide.atemp = statistics.global.atemp.count === 0;
hide.power = statistics.global.power.count === 0;
hide.extensions = Object.keys(statistics.global.extensions).length === 0;
}
$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
@@ -144,11 +144,11 @@
{$_('quantities.time')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.surface ? 'hidden' : ''}">
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
<Label for="export-surface" class="flex flex-row items-center gap-1">
<BrickWall size="16" />
{$_('quantities.surface')}
<div class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}">
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
<Label for="export-extensions" class="flex flex-row items-center gap-1">
<Earth size="16" />
{$_('quantities.osm_extensions')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">

View File

@@ -10,7 +10,7 @@ export type EmbeddingOptions = {
show: boolean;
height: number;
controls: boolean;
fill: 'slope' | 'surface' | undefined;
fill: 'slope' | 'surface' | 'highway' | undefined;
speed: boolean;
hr: boolean;
cad: boolean;

View File

@@ -142,7 +142,7 @@
let value = selected?.value;
if (value === 'none') {
options.elevation.fill = undefined;
} else if (value === 'slope' || value === 'surface') {
} else if (value === 'slope' || value === 'surface' || value === 'highway') {
options.elevation.fill = value;
}
}}
@@ -153,6 +153,7 @@
<Select.Content>
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item>
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item>
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
</Select.Content>
</Select.Root>

View File

@@ -65,7 +65,7 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
const latIdx = messages[0].indexOf("Latitude");
const tagIdx = messages[0].indexOf("WayTags");
let messageIdx = 1;
let surface = messageIdx < messages.length ? getSurface(messages[messageIdx][tagIdx]) : undefined;
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
for (let i = 0; i < coordinates.length; i++) {
let coord = coordinates[i];
@@ -82,25 +82,25 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
messageIdx++;
if (messageIdx == messages.length) surface = undefined;
else surface = getSurface(messages[messageIdx][tagIdx]);
if (messageIdx == messages.length) tags = {};
else tags = getTags(messages[messageIdx][tagIdx]);
}
if (surface) {
route[route.length - 1].setSurface(surface);
}
route[route.length - 1].setExtensions(tags);
}
return route;
}
function getSurface(message: string): string | undefined {
function getTags(message: string): { [key: string]: string } {
const fields = message.split(" ");
for (let i = 0; i < fields.length; i++) if (fields[i].startsWith("surface=")) {
return fields[i].substring(8);
let tags: { [key: string]: string } = {};
for (let i = 0; i < fields.length; i++) {
let tag = fields[i].split("=");
tags[tag[0]] = tag[1];
}
return undefined;
};
return tags;
}
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
let route: TrackPoint[] = [];

View File

@@ -3,7 +3,7 @@ title: Files and statistics
---
<script>
import { TriangleRight, BrickWall, Zap, HeartPulse, Orbit, Thermometer, SquareActivity } from 'lucide-svelte';
import { ChartNoAxesColumn } from 'lucide-svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -73,9 +73,9 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
### Additional data
Using the button at the bottom-right of the elevation profile, you can optionally color the elevation profile by:
- **slope** <TriangleRight size="16" class="inline-block" style="margin-bottom: 2px" /> information computed from the elevation data, or
- **surface** <BrickWall size="16" class="inline-block" style="margin-bottom: 2px" /> data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> tags.
Using the <kbd><ChartNoAxesColumn size="16" class="inline-block" style="margin-bottom: 2px"/></kbd> button at the bottom-right of the elevation profile, you can optionally color the elevation profile by:
- **slope** information computed from the elevation data, or
- **surface** or **category** data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> and <a href="https://wiki.openstreetmap.org/wiki/Key:highway" target="_blank">highway</a> tags.
This is only available for files created with **gpx.studio**.
If your selection includes it, you can also visualize: **speed** <Zap size="16" class="inline-block" style="margin-bottom: 2px" />, **heart rate** <HeartPulse size="16" class="inline-block" style="margin-bottom: 2px" />, **cadence** <Orbit size="16" class="inline-block" style="margin-bottom: 2px" />, **temperature** <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" />, and **power** <SquareActivity size="16" class="inline-block" style="margin-bottom: 2px" /> data on the elevation profile.
If your selection includes it, you can also visualize: **speed**, **heart rate**, **cadence**, **temperature** and **power** data on the elevation profile.

View File

@@ -139,6 +139,52 @@
"clay": "Clay",
"stone": "Stone"
},
"highway": {
"unknown": "Unknown",
"residential": "Residential",
"service": "Service",
"track": "Track",
"unclassified": "Unclassified",
"footway": "Footway",
"tertiary": "Tertiary",
"path": "Path",
"secondary": "Secondary",
"primary": "Primary",
"cycleway": "Cycleway",
"trunk": "Trunk",
"living_street": "Living Street",
"motorway": "Motorway",
"motorway_link": "Motorway Link",
"steps": "Steps",
"road": "Road",
"pedestrian": "Pedestrian",
"trunk_link": "Trunk Link",
"primary_link": "Primary Link",
"secondary_link": "Secondary Link",
"tertiary_link": "Tertiary Link",
"construction": "Construction",
"bridleway": "Bridleway",
"platform": "Platform",
"proposed": "Proposed",
"raceway": "Raceway",
"rest_area": "Rest Area",
"abandoned": "Abandoned",
"services": "Services",
"corridor": "Corridor",
"bus_stop": "Bus Stop",
"busway": "Busway",
"elevator": "Elevator",
"via_ferrata": "Via Ferrata"
},
"sac_scale": {
"hiking": "Hiking",
"mountain_hiking": "Mountain hiking",
"demanding_mountain_hiking": "Demanding mountain hiking",
"alpine_hiking": "Alpine hiking",
"demanding_alpine_hiking": "Demanding alpine hiking",
"difficult_alpine_hiking": "Difficult alpine hiking"
},
"mtb_scale": "MTB scale",
"error": {
"from": "The start point is too far from the nearest road",
"via": "The via point is too far from the nearest road",
@@ -355,9 +401,11 @@
"power": "Power",
"slope": "Slope",
"surface": "Surface",
"highway": "Category",
"time": "Time",
"moving": "Moving",
"total": "Total"
"total": "Total",
"osm_extensions": "OpenStreetMap data"
},
"units": {
"meters": "m",