mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 16:52:31 +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
|
// 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 } = {};
|
_data: { [key: string]: any } = {};
|
||||||
|
|
||||||
abstract isLeaf(): boolean;
|
abstract isLeaf(): boolean;
|
||||||
@@ -27,6 +27,8 @@ abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
|||||||
abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[];
|
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
|
// 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> {
|
abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement<T> {
|
||||||
isLeaf(): boolean {
|
isLeaf(): boolean {
|
||||||
@@ -286,6 +288,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
const points = this.trkpt;
|
const points = this.trkpt;
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
points[i]._data['index'] = i;
|
points[i]._data['index'] = i;
|
||||||
|
points[i]._data['segment'] = this;
|
||||||
|
|
||||||
// distance
|
// distance
|
||||||
let dist = 0;
|
let dist = 0;
|
||||||
@@ -370,6 +373,11 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
this._computeStatistics();
|
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 {
|
reverse(originalNextTimestamp: Date | undefined, newPreviousTimestamp: Date | undefined): void {
|
||||||
if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
|
if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
|
||||||
let originalEndTimestamp = this.getEndTimestamp();
|
let originalEndTimestamp = this.getEndTimestamp();
|
||||||
@@ -460,6 +468,10 @@ export class TrackPoint {
|
|||||||
return this.attributes;
|
return this.attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCoordinates(coordinates: Coordinates): void {
|
||||||
|
this.attributes = coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
getLatitude(): number {
|
getLatitude(): number {
|
||||||
return this.attributes.lat;
|
return this.attributes.lat;
|
||||||
}
|
}
|
||||||
@@ -599,7 +611,7 @@ export type TrackPointStatistics = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
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 rad = Math.PI / 180;
|
||||||
const lat1 = coord1.lat * rad;
|
const lat1 = coord1.lat * rad;
|
||||||
const lat2 = coord2.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[]> {
|
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 response = await fetch(url);
|
||||||
let geojson = await response.json();
|
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 { get, type Writable } from "svelte/store";
|
||||||
import { computeAnchorPoints } from "./Simplify";
|
import { computeAnchorPoints, type SimplifiedTrackPoint } from "./Simplify";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import { route } from "./Routing";
|
import { route } from "./Routing";
|
||||||
import { applyToFileStore } from "$lib/stores";
|
import { applyToFileElement, applyToFileStore } from "$lib/stores";
|
||||||
|
|
||||||
export class RoutingControls {
|
export class RoutingControls {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -17,14 +17,10 @@ export class RoutingControls {
|
|||||||
constructor(map: mapboxgl.Map, file: Writable<GPXFile>) {
|
constructor(map: mapboxgl.Map, file: Writable<GPXFile>) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
|
||||||
computeAnchorPoints(get(file));
|
|
||||||
this.createMarkers();
|
|
||||||
this.add();
|
this.add();
|
||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.toggleMarkersForZoomLevelAndBounds();
|
|
||||||
this.map.on('zoom', this.toggleMarkersForZoomLevelAndBoundsBinded);
|
this.map.on('zoom', this.toggleMarkersForZoomLevelAndBoundsBinded);
|
||||||
this.map.on('move', this.toggleMarkersForZoomLevelAndBoundsBinded);
|
this.map.on('move', this.toggleMarkersForZoomLevelAndBoundsBinded);
|
||||||
this.map.on('click', this.extendFileBinded);
|
this.map.on('click', this.extendFileBinded);
|
||||||
@@ -34,7 +30,28 @@ export class RoutingControls {
|
|||||||
|
|
||||||
updateControls() {
|
updateControls() {
|
||||||
// Update controls
|
// 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() {
|
remove() {
|
||||||
@@ -48,35 +65,138 @@ export class RoutingControls {
|
|||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
createMarkers() {
|
createMarkers(segment: TrackSegment) {
|
||||||
for (let segment of get(this.file).getSegments()) {
|
for (let anchor of segment._data.anchors) {
|
||||||
for (let anchor of segment._data.anchors) {
|
this.createMarker(anchor);
|
||||||
let marker = getMarker(anchor.point.getCoordinates(), true);
|
|
||||||
Object.defineProperty(marker, '_simplified', {
|
|
||||||
value: anchor
|
|
||||||
});
|
|
||||||
this.markers.push(marker);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
toggleMarkersForZoomLevelAndBounds() {
|
||||||
let zoom = this.map.getZoom();
|
let zoom = this.map.getZoom();
|
||||||
this.markers.forEach((marker) => {
|
this.markers.forEach((marker) => {
|
||||||
if (marker._simplified.zoom <= zoom && this.map.getBounds().contains(marker.getLngLat())) {
|
if (marker._simplified.zoom <= zoom && this.map.getBounds().contains(marker.getLngLat())) {
|
||||||
marker.addTo(this.map);
|
marker.addTo(this.map);
|
||||||
|
Object.defineProperty(marker, '_inZoom', {
|
||||||
|
value: true,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
marker.remove();
|
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) {
|
async extendFile(e: mapboxgl.MapMouseEvent) {
|
||||||
let segments = get(this.file).getSegments();
|
let segments = get(this.file).getSegments();
|
||||||
if (segments.length === 0) {
|
if (segments.length === 0) {
|
||||||
return;
|
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 lastAnchor = anchors[anchors.length - 1];
|
||||||
|
|
||||||
let newPoint = {
|
let newPoint = {
|
||||||
@@ -86,15 +206,13 @@ export class RoutingControls {
|
|||||||
|
|
||||||
let response = await route([lastAnchor.point.getCoordinates(), newPoint]);
|
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);
|
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;
|
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))));
|
return Math.min(20, Math.max(0, Math.floor(Math.log2((earthRadius * Math.cos(lat)) / distance))));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeAnchorPoints(file: GPXFile) {
|
export function computeAnchorPoints(segment: TrackSegment) {
|
||||||
for (let segment of file.getSegments()) {
|
let points = segment.trkpt;
|
||||||
let points = segment.trkpt;
|
let anchors = ramerDouglasPeucker(points);
|
||||||
let anchors = ramerDouglasPeucker(points);
|
anchors.forEach((point) => {
|
||||||
anchors.forEach((point) => {
|
point.zoom = getZoomLevelForDistance(point.point.getLatitude(), point.distance);
|
||||||
point.zoom = getZoomLevelForDistance(point.point.getLatitude(), point.distance);
|
});
|
||||||
});
|
segment._data['anchors'] = anchors;
|
||||||
segment._data['anchors'] = anchors;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] {
|
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] {
|
||||||
let simplified = [{
|
let simplified = [{
|
||||||
point: points[start],
|
point: points[start]
|
||||||
index: start,
|
|
||||||
|
|
||||||
}];
|
}];
|
||||||
ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified);
|
ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified);
|
||||||
simplified.push({
|
simplified.push({
|
||||||
point: points[end],
|
point: points[end]
|
||||||
index: end
|
|
||||||
});
|
});
|
||||||
return simplified;
|
return simplified;
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { writable, get, type Writable } from 'svelte/store';
|
import { writable, get, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
import mapboxgl from 'mapbox-gl';
|
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 map = writable<mapboxgl.Map | null>(null);
|
||||||
export const files = writable<Writable<GPXFile>[]>([]);
|
export const files = writable<Writable<GPXFile>[]>([]);
|
||||||
@@ -26,6 +26,16 @@ export function getFileIndex(file: GPXFile): number {
|
|||||||
return get(files).findIndex(store => get(store) === file);
|
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) {
|
export function applyToFile(file: GPXFile, callback: (file: GPXFile) => void, updateSelected: boolean) {
|
||||||
let store = getFileStore(file);
|
let store = getFileStore(file);
|
||||||
applyToFileStore(store, callback, updateSelected);
|
applyToFileStore(store, callback, updateSelected);
|
||||||
|
Reference in New Issue
Block a user