routing progress

This commit is contained in:
vcoppe
2024-04-25 19:02:34 +02:00
parent 7ef19adf53
commit fec275574c
5 changed files with 182 additions and 47 deletions

View File

@@ -8,7 +8,7 @@ function cloneJSON<T>(obj: T): T {
}
// An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy
abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
_data: { [key: string]: any } = {};
abstract isLeaf(): boolean;
@@ -27,6 +27,8 @@ abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[];
}
export type AnyGPXTreeElement = GPXTreeElement<GPXTreeElement<any>>;
// An abstract class that can be extended to facilitate functions working similarly with Tracks and TrackSegments
abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement<T> {
isLeaf(): boolean {
@@ -286,6 +288,7 @@ export class TrackSegment extends GPXTreeLeaf {
const points = this.trkpt;
for (let i = 0; i < points.length; i++) {
points[i]._data['index'] = i;
points[i]._data['segment'] = this;
// distance
let dist = 0;
@@ -370,6 +373,11 @@ export class TrackSegment extends GPXTreeLeaf {
this._computeStatistics();
}
replace(start: number, end: number, points: TrackPoint[]): void {
this.trkpt.splice(start, end - start + 1, ...points);
this._computeStatistics();
}
reverse(originalNextTimestamp: Date | undefined, newPreviousTimestamp: Date | undefined): void {
if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
let originalEndTimestamp = this.getEndTimestamp();
@@ -460,6 +468,10 @@ export class TrackPoint {
return this.attributes;
}
setCoordinates(coordinates: Coordinates): void {
this.attributes = coordinates;
}
getLatitude(): number {
return this.attributes.lat;
}
@@ -599,7 +611,7 @@ export type TrackPointStatistics = {
}
const earthRadius = 6371008.8;
function distance(coord1: Coordinates, coord2: Coordinates): number {
export function distance(coord1: Coordinates, coord2: Coordinates): number {
const rad = Math.PI / 180;
const lat1 = coord1.lat * rad;
const lat2 = coord2.lat * rad;

View File

@@ -35,7 +35,7 @@ export function route(points: Coordinates[]): Promise<TrackPoint[]> {
}
async function getRoute(points: Coordinates[], brouterProfile: string, privateRoads: boolean): Promise<TrackPoint[]> {
let url = `https://routing.gpx.studio?lonlats=${points.map(point => `${point.lon},${point.lat}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
let url = `https://routing.gpx.studio?lonlats=${points.map(point => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
let response = await fetch(url);
let geojson = await response.json();

View File

@@ -1,9 +1,9 @@
import type { Coordinates, GPXFile } from "gpx";
import { distance, type Coordinates, type GPXFile, type TrackSegment } from "gpx";
import { get, type Writable } from "svelte/store";
import { computeAnchorPoints } from "./Simplify";
import { computeAnchorPoints, type SimplifiedTrackPoint } from "./Simplify";
import mapboxgl from "mapbox-gl";
import { route } from "./Routing";
import { applyToFileStore } from "$lib/stores";
import { applyToFileElement, applyToFileStore } from "$lib/stores";
export class RoutingControls {
map: mapboxgl.Map;
@@ -17,14 +17,10 @@ export class RoutingControls {
constructor(map: mapboxgl.Map, file: Writable<GPXFile>) {
this.map = map;
this.file = file;
computeAnchorPoints(get(file));
this.createMarkers();
this.add();
}
add() {
this.toggleMarkersForZoomLevelAndBounds();
this.map.on('zoom', this.toggleMarkersForZoomLevelAndBoundsBinded);
this.map.on('move', this.toggleMarkersForZoomLevelAndBoundsBinded);
this.map.on('click', this.extendFileBinded);
@@ -34,7 +30,28 @@ export class RoutingControls {
updateControls() {
// Update controls
console.log('updateControls');
for (let segment of get(this.file).getSegments()) {
if (!segment._data.anchors) { // New segment
computeAnchorPoints(segment);
this.createMarkers(segment);
continue;
}
let anchors = segment._data.anchors;
for (let i = 0; i < anchors.length;) {
let anchor = anchors[i];
if (anchor.point._data.index >= segment.trkpt.length || anchor.point !== segment.trkpt[anchor.point._data.index]) { // Point removed
anchors.splice(i, 1);
let markerIndex = this.markers.findIndex(marker => marker._simplified === anchor);
this.markers[markerIndex].remove();
this.markers.splice(markerIndex, 1);
continue;
}
i++;
}
}
this.toggleMarkersForZoomLevelAndBounds();
}
remove() {
@@ -48,35 +65,138 @@ export class RoutingControls {
this.unsubscribe();
}
createMarkers() {
for (let segment of get(this.file).getSegments()) {
for (let anchor of segment._data.anchors) {
let marker = getMarker(anchor.point.getCoordinates(), true);
Object.defineProperty(marker, '_simplified', {
value: anchor
});
this.markers.push(marker);
}
createMarkers(segment: TrackSegment) {
for (let anchor of segment._data.anchors) {
this.createMarker(anchor);
}
}
createMarker(anchor: SimplifiedTrackPoint) {
let element = document.createElement('div');
element.className = `h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`;
let marker = new mapboxgl.Marker({
draggable: true,
element
}).setLngLat(anchor.point.getCoordinates());
Object.defineProperty(marker, '_simplified', {
value: anchor
});
anchor.marker = marker;
marker.on('dragend', this.updateAnchor.bind(this));
this.markers.push(marker);
}
toggleMarkersForZoomLevelAndBounds() {
let zoom = this.map.getZoom();
this.markers.forEach((marker) => {
if (marker._simplified.zoom <= zoom && this.map.getBounds().contains(marker.getLngLat())) {
marker.addTo(this.map);
Object.defineProperty(marker, '_inZoom', {
value: true,
writable: true
});
} else {
marker.remove();
Object.defineProperty(marker, '_inZoom', {
value: false,
writable: true
});
}
});
}
updateAnchor(e: any) {
let marker = e.target;
let anchor = marker._simplified;
let latlng = marker.getLngLat();
let coordinates = {
lat: latlng.lat,
lon: latlng.lng
};
let segment = anchor.point._data.segment;
let anchors = segment._data.anchors;
let previousAnchor: SimplifiedTrackPoint | null = null;
let nextAnchor: SimplifiedTrackPoint | null = null;
for (let i = 0; i < anchors.length; i++) {
if (anchors[i].point._data.index < anchor.point._data.index && anchors[i].marker._inZoom) {
if (!previousAnchor || anchors[i].point._data.index > previousAnchor.point._data.index) {
previousAnchor = anchors[i];
}
} else if (anchors[i].point._data.index > anchor.point._data.index && anchors[i].marker._inZoom) {
if (!nextAnchor || anchors[i].point._data.index < nextAnchor.point._data.index) {
nextAnchor = anchors[i];
}
}
}
let routeCoordinates = [];
if (previousAnchor) {
routeCoordinates.push(previousAnchor.point.getCoordinates());
}
routeCoordinates.push(coordinates);
if (nextAnchor) {
routeCoordinates.push(nextAnchor.point.getCoordinates());
}
let start = previousAnchor ? previousAnchor.point._data.index + 1 : anchor.point._data.index;
let end = nextAnchor ? nextAnchor.point._data.index - 1 : anchor.point._data.index;
if (routeCoordinates.length === 1) {
return;
} else {
route(routeCoordinates).then((response) => {
if (previousAnchor) {
previousAnchor.zoom = 0;
} else {
anchor.zoom = 0;
anchor.point = response[0];
}
if (nextAnchor) {
nextAnchor.zoom = 0;
} else {
anchor.zoom = 0;
anchor.point = response[response.length - 1];
}
// find closest point to the dragged marker
// and transfer the marker to that point
if (previousAnchor && nextAnchor) {
let minDistance = Number.MAX_VALUE;
for (let i = 1; i < response.length - 1; i++) {
let dist = distance(response[i].getCoordinates(), anchor.point.getCoordinates());
if (dist < minDistance) {
minDistance = dist;
anchor.zoom = 0;
anchor.point = response[i];
}
}
}
marker.setLngLat(anchor.point.getCoordinates());
applyToFileElement(this.file, segment, (segment) => {
segment.replace(start, end, response);
}, true);
});
}
}
async extendFile(e: mapboxgl.MapMouseEvent) {
let segments = get(this.file).getSegments();
if (segments.length === 0) {
return;
}
let anchors = segments[segments.length - 1]._data.anchors;
let segment = segments[segments.length - 1];
let anchors = segment._data.anchors;
let lastAnchor = anchors[anchors.length - 1];
let newPoint = {
@@ -86,15 +206,13 @@ export class RoutingControls {
let response = await route([lastAnchor.point.getCoordinates(), newPoint]);
let anchor = {
point: response[response.length - 1],
zoom: 0
};
segment._data.anchors.push(anchor);
this.createMarker(anchor);
applyToFileStore(this.file, (f) => f.append(response), true);
}
}
export function getMarker(coordinates: Coordinates, draggable: boolean = false): mapboxgl.Marker {
let element = document.createElement('div');
element.className = `h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`;
return new mapboxgl.Marker({
draggable,
element
}).setLngLat(coordinates);
}

View File

@@ -1,6 +1,6 @@
import type { Coordinates, GPXFile, TrackPoint } from "gpx";
import type { Coordinates, TrackPoint, TrackSegment } from "gpx";
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number, zoom?: number };
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number, zoom?: number, marker?: mapboxgl.Marker };
const earthRadius = 6371008.8;
@@ -15,27 +15,22 @@ export function getZoomLevelForDistance(latitude: number, distance?: number): nu
return Math.min(20, Math.max(0, Math.floor(Math.log2((earthRadius * Math.cos(lat)) / distance))));
}
export function computeAnchorPoints(file: GPXFile) {
for (let segment of file.getSegments()) {
let points = segment.trkpt;
let anchors = ramerDouglasPeucker(points);
anchors.forEach((point) => {
point.zoom = getZoomLevelForDistance(point.point.getLatitude(), point.distance);
});
segment._data['anchors'] = anchors;
}
export function computeAnchorPoints(segment: TrackSegment) {
let points = segment.trkpt;
let anchors = ramerDouglasPeucker(points);
anchors.forEach((point) => {
point.zoom = getZoomLevelForDistance(point.point.getLatitude(), point.distance);
});
segment._data['anchors'] = anchors;
}
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] {
let simplified = [{
point: points[start],
index: start,
point: points[start]
}];
ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified);
simplified.push({
point: points[end],
index: end
point: points[end]
});
return simplified;
}

View File

@@ -1,7 +1,7 @@
import { writable, get, type Writable } from 'svelte/store';
import mapboxgl from 'mapbox-gl';
import { GPXFile, buildGPX, parseGPX } from 'gpx';
import { GPXFile, buildGPX, parseGPX, type AnyGPXTreeElement } from 'gpx';
export const map = writable<mapboxgl.Map | null>(null);
export const files = writable<Writable<GPXFile>[]>([]);
@@ -26,6 +26,16 @@ export function getFileIndex(file: GPXFile): number {
return get(files).findIndex(store => get(store) === file);
}
export function applyToFileElement<T extends AnyGPXTreeElement>(store: Writable<GPXFile>, element: T, callback: (element: T) => void, updateSelected: boolean) {
store.update($file => {
callback(element);
return $file;
});
if (updateSelected) {
selectedFiles.update($selected => $selected);
}
}
export function applyToFile(file: GPXFile, callback: (file: GPXFile) => void, updateSelected: boolean) {
let store = getFileStore(file);
applyToFileStore(store, callback, updateSelected);