add elevation tool

This commit is contained in:
vcoppe
2024-09-04 19:11:56 +02:00
parent 9ba07ce1ed
commit 8985623639
12 changed files with 1390 additions and 88 deletions

View File

@@ -372,14 +372,14 @@ export class GPXFile extends GPXTreeNode<Track>{
});
}
addElevation(callback: (Coordinates) => number, trackIndices?: number[], segmentIndices?: number[], waypointIndices?: number[]) {
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
addElevation(elevations: number[], trackIndices?: number[], segmentIndices?: number[], waypointIndices?: number[]) {
let index = 0;
this.trk.forEach((track, trackIndex) => {
if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
track.trkseg.forEach((segment, segmentIndex) => {
if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
segment.trkpt.forEach((point, pointIndex) => {
point.ele = callback(og.trk[trackIndex].trkseg[segmentIndex].trkpt[pointIndex].attributes);
segment.trkpt.forEach((point) => {
point.ele = elevations[index++];
});
}
});
@@ -387,7 +387,7 @@ export class GPXFile extends GPXTreeNode<Track>{
});
this.wpt.forEach((waypoint, waypointIndex) => {
if (waypointIndices === undefined || waypointIndices.includes(waypointIndex)) {
waypoint.ele = callback(og.wpt[waypointIndex].attributes);
waypoint.ele = elevations[index++];
}
});
}

1260
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,8 +21,10 @@
"@types/eslint": "^8.56.10",
"@types/events": "^3.0.3",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
"@types/mapbox__tilebelt": "^1.0.4",
"@types/mapbox-gl": "^3.1.0",
"@types/node": "^20.14.6",
"@types/png.js": "^0.2.3",
"@types/sanitize-html": "^2.11.0",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.13.1",
@@ -43,13 +45,15 @@
"tslib": "^2.6.3",
"tsx": "^4.15.7",
"typescript": "^5.4.5",
"vite": "^5.3.1"
"vite": "^5.3.1",
"vite-plugin-node-polyfills": "^0.22.0"
},
"type": "module",
"dependencies": {
"@internationalized/date": "^3.5.4",
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"@mapbox/sphericalmercator": "^1.2.0",
"@mapbox/tilebelt": "^1.0.2",
"@types/mapbox__sphericalmercator": "^1.2.3",
"bits-ui": "^0.21.12",
"chart.js": "^4.4.3",
@@ -63,6 +67,7 @@
"mapbox-gl": "^3.4.0",
"mapillary-js": "^4.1.2",
"mode-watcher": "^0.3.1",
"png.js": "^0.2.1",
"sanitize-html": "^2.13.0",
"sortablejs": "^1.15.2",
"svelte-i18n": "^4.0.0",

View File

@@ -1,11 +1,11 @@
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer } from "lucide-svelte";
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte";
import type { ComponentType } from "svelte";
export const guides: Record<string, string[]> = {
'getting-started': [],
menu: ['file', 'edit', 'view', 'settings'],
'files-and-stats': [],
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'minify', 'clean'],
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'elevation', 'minify', 'clean'],
'map-controls': [],
'gpx': [],
'integration': [],
@@ -27,6 +27,7 @@ export const guideIcons: Record<string, string | ComponentType<Icon>> = {
"time": CalendarClock,
"merge": Group,
"extract": Ungroup,
"elevation": MountainSnow,
"minify": Filter,
"clean": SquareDashedMousePointer,
"map-controls": "🗺",

View File

@@ -261,14 +261,16 @@ export class GPXLayer {
marker.on('dragend', (e) => {
resetCursor();
marker.getElement().style.cursor = '';
dbUtils.applyToFile(this.fileId, (file) => {
let latLng = marker.getLngLat();
let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({
lat: latLng.lat,
lon: latLng.lng
getElevation([marker._waypoint]).then((ele) => {
dbUtils.applyToFile(this.fileId, (file) => {
let latLng = marker.getLngLat();
let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({
lat: latLng.lat,
lon: latLng.lng
});
wpt.ele = ele[0];
});
wpt.ele = getElevation(this.map, wpt.getCoordinates());
});
dragEndTimestamp = Date.now()
});

View File

@@ -125,13 +125,11 @@ function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
}
}));
let m = get(map);
route.forEach((point) => {
point.setSurface("unknown");
if (m) {
point.ele = getElevation(m, point.getCoordinates());
}
return getElevation(route).then((elevations) => {
route.forEach((point, i) => {
point.setSurface("unknown");
point.ele = elevations[i];
});
return route;
});
return new Promise((resolve) => resolve(route));
}

View File

@@ -10,7 +10,7 @@ import { dbUtils, type GPXFileWithStatistics } from "$lib/db";
import { getOrderedSelection, selection } from "$lib/components/file-list/Selection";
import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList";
import { currentTool, streetViewEnabled, Tool } from "$lib/stores";
import { getClosestLinePoint, getElevation, resetCursor, setGrabbingCursor } from "$lib/utils";
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from "$lib/utils";
export const canChangeStart = writable(false);

View File

@@ -8,7 +8,7 @@ import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selec
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList';
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
import { getClosestLinePoint, getElevation, getPreciseElevations } from '$lib/utils';
import { getClosestLinePoint, getElevation } from '$lib/utils';
import { browser } from '$app/environment';
import type mapboxgl from 'mapbox-gl';
@@ -911,29 +911,30 @@ export const dbUtils = {
if (m === null) {
return;
}
let ele = getElevation(m, waypoint.attributes);
if (item) {
dbUtils.applyToFile(item.getFileId(), (file) => {
let wpt = file.wpt[item.getWaypointIndex()];
wpt.name = waypoint.name;
wpt.desc = waypoint.desc;
wpt.cmt = waypoint.cmt;
wpt.sym = waypoint.sym;
wpt.link = waypoint.link;
wpt.setCoordinates(waypoint.attributes);
wpt.ele = ele;
});
} else {
let fileIds = new Set<string>();
get(selection).getSelected().forEach((item) => {
fileIds.add(item.getFileId());
});
let wpt = new Waypoint(waypoint);
wpt.ele = ele;
dbUtils.applyToFiles(Array.from(fileIds), (file) =>
file.replaceWaypoints(file.wpt.length, file.wpt.length, [wpt])
);
}
getElevation([waypoint.attributes]).then((elevation) => {
if (item) {
dbUtils.applyToFile(item.getFileId(), (file) => {
let wpt = file.wpt[item.getWaypointIndex()];
wpt.name = waypoint.name;
wpt.desc = waypoint.desc;
wpt.cmt = waypoint.cmt;
wpt.sym = waypoint.sym;
wpt.link = waypoint.link;
wpt.setCoordinates(waypoint.attributes);
wpt.ele = elevation[0];
});
} else {
let fileIds = new Set<string>();
get(selection).getSelected().forEach((item) => {
fileIds.add(item.getFileId());
});
let wpt = new Waypoint(waypoint);
wpt.ele = elevation[0];
dbUtils.applyToFiles(Array.from(fileIds), (file) =>
file.replaceWaypoints(file.wpt.length, file.wpt.length, [wpt])
);
}
});
},
setStyleToSelection: (style: LineStyleExtension) => {
if (get(selection).size === 0) {
@@ -1052,27 +1053,25 @@ export const dbUtils = {
}
});
getPreciseElevations(map, points).then((elevations) => {
let callback = (coordinates: Coordinates) => elevations.get(`${coordinates.lat},${coordinates.lon}`) ?? 0;
getElevation(points).then((elevations) => {
applyGlobal((draft) => {
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = draft.get(fileId);
if (file) {
if (level === ListLevel.FILE) {
file.addElevation(callback);
file.addElevation(elevations);
} else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
file.addElevation(callback, trackIndices, undefined, []);
file.addElevation(elevations, trackIndices, undefined, []);
} else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
file.addElevation(callback, trackIndices, segmentIndices, []);
file.addElevation(elevations, trackIndices, segmentIndices, []);
} else if (level === ListLevel.WAYPOINTS) {
file.addElevation(callback, [], [], undefined);
file.addElevation(elevations, [], [], undefined);
} else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
file.addElevation(callback, [], [], waypointIndices);
file.addElevation(elevations, [], [], waypointIndices);
}
}
});

View File

@@ -31,9 +31,4 @@ Someone more experienced with OpenStreetMap will then review your note and make
More information on how to contribute to OpenStreetMap can be found <a href="https://wiki.openstreetmap.org/wiki/How_to_contribute" target="_blank">here</a>.
</DocsNote>
### Why is the elevation profile for my GPX file empty?
If the elevation profile for your GPX file is empty, it means that the GPX file does not contain elevation data.
You can add elevation data to your GPX file by using <a href="https://www.gpsvisualizer.com/elevation" target="_blank">GPS Visualizer</a>.
</DocsNote>

View File

@@ -0,0 +1,20 @@
---
title: Elevation
---
<script>
import { MountainSnow } from 'lucide-svelte';
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# <MountainSnow size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
This tool allows you to add elevation data to traces and [points of interest](../gpx), or to replace the existing data.
<div class="flex flex-row justify-center">
<Elevation class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Elevation data is provided by <a href="https://mapbox.com" target="_blank">Mapbox</a>.
You can learn more about its origin and accuracy in the <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.

View File

@@ -7,8 +7,11 @@ import { map } from "./stores";
import { base } from "$app/paths";
import { languages } from "$lib/languages";
import { locale } from "svelte-i18n";
import type mapboxgl from "mapbox-gl";
import { type TrackPoint, type Waypoint, type Coordinates, crossarcDistance, distance } from "gpx";
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from "gpx";
import mapboxgl from "mapbox-gl";
import tilebelt from "@mapbox/tilebelt";
import { PUBLIC_MAPBOX_TOKEN } from "$env/static/public";
import PNGReader from "png.js";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -90,32 +93,52 @@ export function getClosestLinePoint(points: TrackPoint[], point: TrackPoint | Co
return closest;
}
export function getElevation(map: mapboxgl.Map, coordinates: Coordinates): number {
let elevation = map.queryTerrainElevation(coordinates, { exaggerated: false });
return elevation === null ? 0 : elevation;
}
export function getElevation(points: (TrackPoint | Waypoint | Coordinates)[], ELEVATION_ZOOM: number = 13, tileSize = 512): Promise<number[]> {
let coordinates = points.map((point) => (point instanceof TrackPoint || point instanceof Waypoint) ? point.getCoordinates() : point);
let bbox = new mapboxgl.LngLatBounds();
coordinates.forEach((coord) => bbox.extend(coord));
export async function getPreciseElevation(map: mapboxgl.Map, coordinates: Coordinates | mapboxgl.LngLat): Promise<number> {
if (!map.getBounds().contains(coordinates) || map.getZoom() < 14) {
map.flyTo({ center: coordinates, zoom: 14 });
await map.once('idle');
}
let elevation = map.queryTerrainElevation(coordinates, { exaggerated: false });
return elevation === null ? 0 : elevation;
}
let tiles = coordinates.map((coord) => tilebelt.pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM));
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) => tile.split(',').map((x) => parseInt(x)));
let pngs = new Map<string, PNGReader>();
export async function getPreciseElevations(map: mapboxgl.Map, points: (TrackPoint | Waypoint)[]): Promise<Map<string, number>> {
let elevations = new Map<string, number>();
let promises = uniqueTiles.map((tile) => fetch(`https://api.mapbox.com/v4/mapbox.mapbox-terrain-dem-v1/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}@2x.pngraw?access_token=${PUBLIC_MAPBOX_TOKEN}`, { cache: 'force-cache' }).then((response) => response.arrayBuffer()).then((buffer) => new Promise((resolve, reject) => {
let png = new PNGReader(new Uint8Array(buffer));
png.parse((err, png) => {
if (err) {
reject(err);
} else {
resolve(png);
}
});
})).then((png) => {
pngs.set(tile.join(','), png);
}));
for (let point of points) {
let key = `${point.getLatitude()},${point.getLongitude()}`;
if (elevations.has(key)) {
continue;
}
elevations.set(key, await getPreciseElevation(map, point.getCoordinates()));
}
return Promise.all(promises).then(() => coordinates.map((coord, index) => {
let tile = tiles[index];
let tf = tilebelt.pointToTileFraction(coord.lon, coord.lat, ELEVATION_ZOOM);
let x = tileSize * (tf[0] - tile[0]);
let y = tileSize * (tf[1] - tile[1]);
let _x = Math.floor(x);
let _y = Math.floor(y);
let dx = x - _x;
let dy = y - _y;
return elevations;
// Get the four pixels surrounding the point
let png = pngs.get(tile.join(','))!;
const p00 = png.getPixel(_x, _y);
const p01 = png.getPixel(_x, _y + (_y + 1 == tileSize ? 0 : 1));
const p10 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y);
const p11 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y + (_y + 1 == tileSize ? 0 : 1));
let ele00 = -10000 + ((p00[0] * 256 * 256 + p00[1] * 256 + p00[2]) * 0.1);
let ele01 = -10000 + ((p01[0] * 256 * 256 + p01[1] * 256 + p01[2]) * 0.1);
let ele10 = -10000 + ((p10[0] * 256 * 256 + p10[1] * 256 + p10[2]) * 0.1);
let ele11 = -10000 + ((p11[0] * 256 * 256 + p11[1] * 256 + p11[2]) * 0.1);
return ele00 * (1 - dx) * (1 - dy) + ele01 * (1 - dx) * dy + ele10 * dx * (1 - dy) + ele11 * dx * dy;
}));
}
let previousCursors: string[] = [];

View File

@@ -1,7 +1,8 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { enhancedImages } from '@sveltejs/enhanced-img';
import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills'
export default defineConfig({
plugins: [enhancedImages(), sveltekit()]
plugins: [nodePolyfills(), enhancedImages(), sveltekit()]
});