Migration to new Rust-based engine (#401)

* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
vcoppeandvcoppe authored and GitHub committed 2026-10-31 18:08:22 +01:00
1 parent 6a0d434371
commit 8fe7d8d64d
484 files changed
+53949 -41152

No files matched your search

+39 -115
View File
@@ -1,12 +1,10 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { base } from '$app/paths';
import { languages } from '$lib/languages';
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
import { Map as MapLibreMap, LngLatBounds } from 'maplibre-gl';
import { resolve } from '$app/paths';
import { languages } from '$lib/i18n/languages';
import type { Coordinates } from '$lib/geo/coordinates';
import { LngLatBounds, type Map as MapLibreMap } from 'maplibre-gl';
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -19,108 +17,27 @@ export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'childre
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
export function getClosestLinePoint(
points: TrackPoint[],
point: TrackPoint | Coordinates,
details: any = {}
): TrackPoint {
let closest = points[0];
let closestDist = Number.MAX_VALUE;
for (let i = 0; i < points.length - 1; i++) {
let dist = crossarcDistance(points[i], points[i + 1], point);
if (dist < closestDist) {
closestDist = dist;
if (distance(points[i], point) <= distance(points[i + 1], point)) {
closest = points[i];
details['before'] = true;
details['index'] = i;
} else {
closest = points[i + 1];
details['before'] = false;
details['index'] = i + 1;
}
}
}
details['distance'] = closestDist;
return closest;
}
export function getClosestTrackSegments(
file: GPXFile,
statistics: GPXStatisticsTree,
point: Coordinates
): [number, number][] {
let segmentBoundsDistances: [number, number, number][] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentStatistics = statistics.getStatisticsFor(
new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex)
);
let segmentBounds = segmentStatistics.global.bounds;
let northEast = segmentBounds.northEast;
let southWest = segmentBounds.southWest;
let bounds = new LngLatBounds(southWest, northEast);
if (bounds.contains(point)) {
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
} else {
let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon };
let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon };
let distanceToBounds = Math.min(
crossarcDistance(northWest, northEast, point),
crossarcDistance(northEast, southEast, point),
crossarcDistance(southEast, southWest, point),
crossarcDistance(southWest, northWest, point)
);
segmentBoundsDistances.push([distanceToBounds, trackIndex, segmentIndex]);
}
});
segmentBoundsDistances.sort((a, b) => a[0] - b[0]);
let closest: { distance: number; indices: [number, number][] } = {
distance: Number.MAX_VALUE,
indices: [],
};
for (let s = 0; s < segmentBoundsDistances.length; s++) {
if (segmentBoundsDistances[s][0] > closest.distance) {
break;
}
const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]);
segment.trkpt.forEach((pt) => {
let dist = distance(pt.getCoordinates(), point);
if (dist < closest.distance) {
closest.distance = dist;
closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]];
} else if (dist === closest.distance) {
closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]);
}
});
}
return closest.indices;
}
export function getElevation(
points: (TrackPoint | Waypoint | Coordinates)[],
points: Coordinates[],
ELEVATION_ZOOM: number = 12,
tileSize = 512
): Promise<number[]> {
let coordinates = points.map((point) =>
point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point
);
let bbox = new LngLatBounds();
const coordinates = points;
const bbox = new LngLatBounds();
coordinates.forEach((coord) => bbox.extend(coord));
let tiles = coordinates.map((coord) => pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM));
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
const tiles = coordinates.map((coord) => pointToTile(coord.lng, coord.lat, ELEVATION_ZOOM));
const uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
tile.split(',').map((x) => parseInt(x))
);
let images = new Map<string, ImageData>();
const images = new Map<string, ImageData>();
const getPixelFromImageData = (imageData: ImageData, x: number, y: number): number[] => {
const index = (y * imageData.width + x) * 4;
return [imageData.data[index], imageData.data[index + 1], imageData.data[index + 2]];
};
let promises = uniqueTiles.map((tile) =>
const promises = uniqueTiles.map((tile) =>
fetch(`https://tiles.gpx.studio/mapterhorn/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}.webp`, {
cache: 'force-cache',
})
@@ -154,20 +71,20 @@ export function getElevation(
return Promise.all(promises).then(() =>
coordinates.map((coord, index) => {
let tile = tiles[index];
let imageData = images.get(tile.join(','));
const tile = tiles[index];
const imageData = images.get(tile.join(','));
if (!imageData) {
return 0;
}
let tf = 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;
const tf = pointToTileFraction(coord.lng, coord.lat, ELEVATION_ZOOM);
const x = tileSize * (tf[0] - tile[0]);
const y = tileSize * (tf[1] - tile[1]);
const _x = Math.floor(x);
const _y = Math.floor(y);
const dx = x - _x;
const dy = y - _y;
const p00 = getPixelFromImageData(imageData, _x, _y);
const p01 = getPixelFromImageData(imageData, _x, _y + (_y + 1 == tileSize ? 0 : 1));
@@ -178,10 +95,10 @@ export function getElevation(
_y + (_y + 1 == tileSize ? 0 : 1)
);
let ele00 = -32768 + p00[0] * 256 + p00[1] + p00[2] / 256;
let ele01 = -32768 + p01[0] * 256 + p01[1] + p01[2] / 256;
let ele10 = -32768 + p10[0] * 256 + p10[1] + p10[2] / 256;
let ele11 = -32768 + p11[0] * 256 + p11[1] + p11[2] / 256;
const ele00 = -32768 + p00[0] * 256 + p00[1] + p00[2] / 256;
const ele01 = -32768 + p01[0] * 256 + p01[1] + p01[2] / 256;
const ele10 = -32768 + p10[0] * 256 + p10[1] + p10[2] / 256;
const ele11 = -32768 + p11[0] * 256 + p11[1] + p11[2] / 256;
return (
ele00 * (1 - dx) * (1 - dy) +
@@ -195,7 +112,7 @@ export function getElevation(
export function loadSVGIcon(map: MapLibreMap, id: string, svg: string, size: number = 100) {
if (!map.hasImage(id)) {
let icon = new Image(size, size);
const icon = new Image(size, size);
icon.onload = () => {
if (!map.hasImage(id)) {
map.addImage(id, icon);
@@ -213,11 +130,19 @@ export function isSafari() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
/**
* The path of a page in another language, without the base path (pass it to `resolve`). `path` is
* the path of the page, with or without the base path.
*/
export function getURLForLanguage(lang: string, path: string): string {
let newPath = path.replace(base, '');
const base = resolve('/').slice(0, -1);
let newPath =
base && (path === base || path.startsWith(`${base}/`))
? path.slice(base.length) || '/'
: path;
let languageInPath = newPath.split('/')[1];
if (!languages.hasOwnProperty(languageInPath)) {
if (!Object.hasOwn(languages, languageInPath)) {
languageInPath = 'en';
}
@@ -227,17 +152,16 @@ export function getURLForLanguage(lang: string, path: string): string {
if (languageInPath === 'en') {
if (lang === 'en') {
return `${base}${newPath}`;
return newPath;
} else {
return `${base}/${lang}${newPath}`;
return `/${lang}${newPath}`;
}
} else {
if (lang === 'en') {
newPath = newPath.replace(`/${languageInPath}`, '');
return newPath === '' ? `${base}/` : `${base}${newPath}`;
return newPath === '' ? '/' : newPath;
} else {
newPath = newPath.replace(`/${languageInPath}`, `/${lang}`);
return `${base}${newPath}`;
return newPath.replace(`/${languageInPath}`, `/${lang}`);
}
}
}