single simplify call to create whole anchor point hierarchy

This commit is contained in:
vcoppe
2024-04-23 19:33:11 +02:00
parent dff39366fa
commit 9bc743bc18
3 changed files with 65 additions and 80 deletions

View File

@@ -20,6 +20,7 @@ abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
abstract getStartTimestamp(): Date; abstract getStartTimestamp(): Date;
abstract getEndTimestamp(): Date; abstract getEndTimestamp(): Date;
abstract getTrackPoints(): TrackPoint[];
abstract getTrackPointsAndStatistics(): { points: TrackPoint[], statistics: TrackPointStatistics }; abstract getTrackPointsAndStatistics(): { points: TrackPoint[], statistics: TrackPointStatistics };
abstract toGeoJSON(): any; abstract toGeoJSON(): any;
@@ -71,6 +72,10 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
return this.getChildren()[this.getChildren().length - 1].getEndTimestamp(); return this.getChildren()[this.getChildren().length - 1].getEndTimestamp();
} }
getTrackPoints(): TrackPoint[] {
return this.getChildren().flatMap((child) => child.getTrackPoints());
}
getTrackPointsAndStatistics(): { points: TrackPoint[]; statistics: TrackPointStatistics; } { getTrackPointsAndStatistics(): { points: TrackPoint[]; statistics: TrackPointStatistics; } {
let points: TrackPoint[] = []; let points: TrackPoint[] = [];
let statistics: TrackPointStatistics = { let statistics: TrackPointStatistics = {
@@ -379,6 +384,10 @@ export class TrackSegment extends GPXTreeLeaf {
return this.trkpt[this.trkpt.length - 1].time; return this.trkpt[this.trkpt.length - 1].time;
} }
getTrackPoints(): TrackPoint[] {
return this.trkpt;
}
getTrackPointsAndStatistics(): { points: TrackPoint[], statistics: TrackPointStatistics } { getTrackPointsAndStatistics(): { points: TrackPoint[], statistics: TrackPointStatistics } {
return { return {
points: this.trkpt, points: this.trkpt,

View File

@@ -35,14 +35,14 @@
let file: GPXFile | null = null; let file: GPXFile | null = null;
let kdbush: KDBush | null = null; let kdbush: KDBush | null = null;
function addMarkersForZoomLevel() { function toggleMarkersForZoomLevelAndBounds() {
if ($map) { if ($map) {
let zoom = $map.getZoom(); let zoom = $map.getZoom();
markers.forEach((marker) => { markers.forEach((marker) => {
if (marker._hierarchy.lowestLevel <= zoom) { if (marker._simplified.zoom <= zoom && $map.getBounds().contains(marker.getLngLat())) {
marker.removeClassName('hidden'); marker.addTo($map);
} else { } else {
marker.addClassName('hidden'); marker.remove();
} }
}); });
} }
@@ -86,7 +86,8 @@
}); });
markers = []; markers = [];
if ($map) { if ($map) {
$map.off('zoom', addMarkersForZoomLevel); $map.off('zoom', toggleMarkersForZoomLevelAndBounds);
$map.off('move', toggleMarkersForZoomLevelAndBounds);
$map.off('click', extendFile); $map.off('click', extendFile);
if (file) { if (file) {
$map.off('mouseover', file.layerId, showInsertableMarker); $map.off('mouseover', file.layerId, showInsertableMarker);
@@ -109,14 +110,15 @@
let end = performance.now(); let end = performance.now();
console.log('Time to create anchor points: ' + (end - start) + 'ms'); console.log('Time to create anchor points: ' + (end - start) + 'ms');
markers = anchorPoints.getMarkers($map); markers = anchorPoints.getMarkers();
addMarkersForZoomLevel(); toggleMarkersForZoomLevelAndBounds();
$map.on('zoom', addMarkersForZoomLevel); $map.on('zoom', toggleMarkersForZoomLevelAndBounds);
$map.on('move', toggleMarkersForZoomLevelAndBounds);
$map.on('click', extendFile); $map.on('click', extendFile);
$map.on('mouseover', file.layerId, showInsertableMarker); $map.on('mouseover', file.layerId, showInsertableMarker);
let points = file.getTrackPointsAndStatistics().points; let points = file.getTrackPoints();
start = performance.now(); start = performance.now();
kdbush = new KDBush(points.length); kdbush = new KDBush(points.length);

View File

@@ -1,102 +1,77 @@
import type { Coordinates, GPXFile, TrackPoint } from "gpx"; import type { Coordinates, GPXFile, TrackPoint } from "gpx";
import mapboxgl from "mapbox-gl"; import mapboxgl from "mapbox-gl";
export function getMarker(coordinates: Coordinates, draggable: boolean = false, hidden: boolean = false): mapboxgl.Marker { export function getMarker(coordinates: Coordinates, draggable: boolean = false): mapboxgl.Marker {
let element = document.createElement('div'); let element = document.createElement('div');
element.className = `${hidden ? 'hidden' : ''} h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`; element.className = `h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`;
return new mapboxgl.Marker({ return new mapboxgl.Marker({
draggable, draggable,
element element
}).setLngLat(coordinates); }).setLngLat(coordinates);
} }
export type TrackPointWithIndex = { point: TrackPoint, index: number }; export type SimplifiedTrackPoint = { point: TrackPoint, index: number, distance?: number, segment?: number, zoom?: number };
export class AnchorPointHierarchy { export class AnchorPointHierarchy {
level: number; points: SimplifiedTrackPoint[][];
lowestLevel: number;
point: TrackPointWithIndex | null;
left: AnchorPointHierarchy[] | null = null;
right: AnchorPointHierarchy[] | null = null;
leftParent: AnchorPointHierarchy | null = null;
rightParent: AnchorPointHierarchy | null = null;
constructor(level: number, point: TrackPointWithIndex | null) { constructor() {
this.level = level; this.points = [];
this.lowestLevel = level; for (let i = 0; i <= 20; i++) {
this.point = point; this.points.push([]);
}
} }
getMarkers(map: mapboxgl.Map, last: boolean = true, markers: mapboxgl.Marker[] = []): mapboxgl.Marker[] { getMarkers(): mapboxgl.Marker[] {
if (this.left == null && this.right == null && this.point) { let markers = [];
let marker = getMarker(this.point.point.getCoordinates()); for (let points of this.points) {
marker.addTo(map); for (let point of points) {
Object.defineProperty(marker, '_hierarchy', { value: this }); let marker = getMarker(point.point.getCoordinates(), true);
markers.push(marker); Object.defineProperty(marker, '_simplified', { value: point });
markers.push(marker);
}
} }
if (this.right) {
this.right.forEach((point, index) => {
if ((index < this.right.length - 1) || last) {
// (index >= this.right.length - 2) because the last point must be drawn by the second to last AnchorPointHierarchy
// because only the right children are drawn
point.getMarkers(map, (index >= this.right.length - 2) && last, markers);
}
});
}
return markers; return markers;
} }
static create(file: GPXFile, initialEpsilon: number = 50000, minEpsilon: number = 50): AnchorPointHierarchy { static create(file: GPXFile, epsilon: number = 50): AnchorPointHierarchy {
let hierarchies = []; let hierarchy = new AnchorPointHierarchy();
let s = 0;
for (let track of file.getChildren()) { for (let track of file.getChildren()) {
for (let segment of track.getChildren()) { for (let segment of track.getChildren()) {
let points = segment.trkpt; let points = segment.trkpt;
let hierarchy = new AnchorPointHierarchy(0, null); let simplified = ramerDouglasPeucker(points, epsilon);
hierarchy.right = AnchorPointHierarchy.createRecursive(1, 1, 1, points, initialEpsilon, minEpsilon); // Assign segment number to each point
hierarchies.push(hierarchy); simplified.forEach((point) => {
point.segment = s;
point.zoom = getZoomLevelForDistance(point.point.getLatitude(), point.distance);
hierarchy.points[point.zoom].push(point);
});
s++;
} }
} }
let hierarchy = new AnchorPointHierarchy(0, null);
hierarchy.right = hierarchies;
return hierarchy;
}
static createRecursive(level: number, levelLeft: number, levelRight: number, points: TrackPoint[], epsilon: number, minEpsilon: number, start: number = 0, end: number = points.length - 1): AnchorPointHierarchy[] {
if (start == end) {
return [new AnchorPointHierarchy(Math.min(levelLeft, levelRight), { point: points[start], index: start })];
} else if (epsilon < minEpsilon || end - start == 1) {
return [new AnchorPointHierarchy(levelLeft, { point: points[start], index: start }), new AnchorPointHierarchy(levelRight, { point: points[end], index: end })];
}
let simplified = ramerDouglasPeucker(points, epsilon, start, end);
let hierarchy = [];
for (let i = 0; i < simplified.length; i++) {
hierarchy.push(new AnchorPointHierarchy(
i == 0 ? levelLeft : i == simplified.length - 1 ? levelRight : level,
simplified[i]
));
}
let childHierarchies = [];
for (let i = 0; i < simplified.length - 1; i++) {
childHierarchies.push(AnchorPointHierarchy.createRecursive(level + 1, i == 0 ? levelLeft : level, i == simplified.length - 2 ? levelRight : level, points, epsilon / 1.54, minEpsilon, simplified[i].index, simplified[i + 1].index));
hierarchy[i].right = childHierarchies[i];
hierarchy[i + 1].left = childHierarchies[i];
}
return hierarchy; return hierarchy;
} }
} }
function ramerDouglasPeucker(points: TrackPoint[], epsilon: number, start: number = 0, end: number = points.length - 1): TrackPointWithIndex[] { function getZoomLevelForDistance(latitude: number, distance?: number): number {
if (distance === undefined) {
return 0;
}
const rad = Math.PI / 180;
const lat = latitude * rad;
return Math.min(20, Math.max(0, Math.floor(Math.log2((earthRadius * Math.cos(lat)) / distance))));
}
function ramerDouglasPeucker(points: TrackPoint[], epsilon: number, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] {
let simplified = [{ let simplified = [{
point: points[start], point: points[start],
index: start index: start,
}]; }];
ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified); ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified);
simplified.push({ simplified.push({
@@ -106,7 +81,7 @@ function ramerDouglasPeucker(points: TrackPoint[], epsilon: number, start: numbe
return simplified; return simplified;
} }
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, start: number, end: number, simplified: TrackPointWithIndex[]) { function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
let largest = { let largest = {
index: 0, index: 0,
distance: 0 distance: 0
@@ -122,12 +97,11 @@ function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, sta
if (largest.distance > epsilon) { if (largest.distance > epsilon) {
ramerDouglasPeuckerRecursive(points, epsilon, start, largest.index, simplified); ramerDouglasPeuckerRecursive(points, epsilon, start, largest.index, simplified);
simplified.push({ point: points[largest.index], index: largest.index }); simplified.push({ point: points[largest.index], index: largest.index, distance: largest.distance });
ramerDouglasPeuckerRecursive(points, epsilon, largest.index, end, simplified); ramerDouglasPeuckerRecursive(points, epsilon, largest.index, end, simplified);
} }
} }
const earthRadius = 6371008.8; const earthRadius = 6371008.8;
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number { function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {