mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 15:43:25 +00:00
routing progress
This commit is contained in:
@@ -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;
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user