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

+2 -2
View File
@@ -15,9 +15,9 @@ export async function load({ params }) {
const { language } = params;
const guideTitles: Record<string, string> = {};
for (let guide of Object.keys(guides)) {
for (const guide of Object.keys(guides)) {
guideTitles[guide] = (await getModule(language, guide)).metadata.title;
for (let subguide of guides[guide]) {
for (const subguide of guides[guide]) {
guideTitles[`${guide}/${subguide}`] = (
await getModule(language, `${guide}/${subguide}`)
).metadata.title;
+6 -6
View File
@@ -1,14 +1,14 @@
<script lang="ts">
import '../app.css';
import { ModeWatcher } from 'mode-watcher';
import { i18n } from '$lib/i18n.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import { page } from '$app/state';
import Nav from '$lib/components/Nav.svelte';
import Footer from '$lib/components/Footer.svelte';
import { onMount, type Snippet } from 'svelte';
import { convertOldEmbeddingOptions } from '$lib/components/embedding/embedding';
import { base } from '$app/paths';
import { languages } from '$lib/languages';
import { resolve } from '$app/paths';
import { languages } from '$lib/i18n/languages';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { getURLForLanguage } from '$lib/utils';
@@ -31,7 +31,7 @@
let folders = page.url.pathname.split('/');
let locale =
folders.indexOf('l') >= 0 ? (folders[folders.indexOf('l') + 1] ?? 'en') : 'en';
window.location.href = `${getURLForLanguage(locale, '/embed')}?options=${encodeURIComponent(JSON.stringify(convertOldEmbeddingOptions(page.url.searchParams)))}`;
window.location.href = `${resolve(getURLForLanguage(locale, '/embed'))}?options=${encodeURIComponent(JSON.stringify(convertOldEmbeddingOptions(page.url.searchParams)))}`;
}
});
@@ -40,10 +40,10 @@
if (page.params.language) {
let lang = page.params.language.replace('/', '');
if (i18n.lang !== lang) {
if (languages.hasOwnProperty(lang)) {
if (Object.hasOwn(languages, lang)) {
i18n.lang = lang;
} else if (browser) {
goto(`${base}/404`);
goto(resolve('/404'));
}
}
} else if (i18n.lang !== 'en') {
+5 -4
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { Button } from '$lib/components/ui/button';
import Logo from '$lib/components/Logo.svelte';
import { getURLForLanguage } from '$lib/utils';
import { House, Map, BookOpenText } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
</script>
<div class="grow px-12 my-12 flex flex-col items-center justify-center gap-6">
@@ -12,7 +13,7 @@
<Logo class="h-40 my-3 animate-spin" style="animation-duration: 20000ms" iconOnly={true} />
<div class="w-full flex flex-row flex-wrap gap-3 justify-center">
<Button
href={getURLForLanguage(i18n.lang, '/')}
href={resolve(getURLForLanguage(i18n.lang, '/'))}
class="text-base w-1/4 min-w-fit rounded-full"
>
<House size="18" />
@@ -20,14 +21,14 @@
</Button>
<Button
data-sveltekit-reload
href={getURLForLanguage(i18n.lang, '/app')}
href={resolve(getURLForLanguage(i18n.lang, '/app'))}
class="text-base w-1/4 min-w-fit rounded-full"
>
<Map size="18" />
{i18n._('homepage.app')}
</Button>
<Button
href={getURLForLanguage(i18n.lang, '/help')}
href={resolve(getURLForLanguage(i18n.lang, '/help'))}
class="text-base w-1/4 min-w-fit rounded-full"
>
<BookOpenText size="18" />
@@ -1,4 +1,4 @@
import { languages } from '$lib/languages';
import { languages } from '$lib/i18n/languages';
import type { EntryGenerator } from './$types';
export const entries: EntryGenerator = () => {
+14 -14
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { Button } from '$lib/components/ui/button';
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
@@ -16,16 +17,16 @@
Languages,
ExternalLink,
} from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import { exampleStatistics } from '$lib/data/example';
import { getURLForLanguage } from '$lib/utils';
import { exampleGPXFile } from '$lib/assets/example';
import { writable } from 'svelte/store';
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { onDestroy, onMount } from 'svelte';
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let statistics = writable(exampleStatistics);
let slicedStatistics = writable(undefined);
let hoveredPoint = writable(null);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable(undefined);
@@ -58,7 +59,7 @@
<div class="w-full flex flex-row xs:justify-center gap-3">
<Button
data-sveltekit-reload
href={getURLForLanguage(i18n.lang, '/app')}
href={resolve(getURLForLanguage(i18n.lang, '/app'))}
class="w-1/3 min-w-fit"
>
<Map size="18" />
@@ -66,7 +67,7 @@
</Button>
<Button
variant="secondary"
href={getURLForLanguage(i18n.lang, '/help')}
href={resolve(getURLForLanguage(i18n.lang, '/help'))}
class="w-1/3 min-w-fit"
>
<BookOpenText size="18" />
@@ -74,7 +75,10 @@
</Button>
</div>
</div>
<a href={getURLForLanguage(i18n.lang, '/app')} aria-label="Link to the web app.">
<a
href={resolve(getURLForLanguage(i18n.lang, '/app'))}
aria-label="Link to the web app."
>
<enhanced:img
src="/src/lib/assets/img/docs/getting-started/interface.webp"
alt="The gpx.studio interface."
@@ -138,18 +142,14 @@
>
<div class="grow">
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{statistics}
{slicedStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<GPXStatistics {statistics} {slicedStatistics} orientation="horizontal" />
</div>
</div>
<div class="grid md:grid-rows-subgrid md:row-start-3 md:row-end-5 gap-4">
@@ -1,4 +1,4 @@
import { languages } from '$lib/languages';
import { languages } from '$lib/i18n/languages';
import type { EntryGenerator } from './$types';
export const entries: EntryGenerator = () => {
+131 -38
View File
@@ -1,7 +1,9 @@
<script lang="ts">
import { resolve } from '$app/paths';
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
import FileList from '$lib/components/file-list/FileList.svelte';
import Explorer from '$lib/components/explorer/Explorer.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/map/Map.svelte';
import Menu from '$lib/components/Menu.svelte';
@@ -11,57 +13,100 @@
import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte';
import Resizer from '$lib/components/Resizer.svelte';
import { Toaster } from '$lib/components/ui/sonner';
import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings';
import { loadFiles } from '$lib/logic/file-actions';
import { onDestroy, onMount } from 'svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { i18n } from '$lib/i18n/i18n.svelte';
import { settings } from '$lib/state/settings';
import { loadFiles } from '$lib/actions/file-actions';
import { onMount } from 'svelte';
import { page } from '$app/state';
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
import { goto } from '$app/navigation';
import { get } from 'svelte/store';
import { getURLForLanguage } from '$lib/utils';
import { inDesktopApp } from '$lib/engine/backend';
import { openLinksInBrowser } from '$lib/desktop/links';
import { hoveredPoint, slicedStatistics } from '$lib/state/selection-statistics';
import { engine } from '$lib/engine';
import { boundsManager } from '$lib/state/bounds';
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
import { db } from '$lib/db';
import { fileStateCollection } from '$lib/logic/file-state';
const outdated = engine.outdated;
const workspace = engine.workspace;
const {
treeFileView,
elevationProfile,
bottomPanelSize,
rightPanelSize,
explorerPanelSize,
additionalDatasets,
elevationFill,
} = settings;
let bottomPanelWidth: number | undefined = $state();
// the file list and the explorer share the right panel, half of it each until it is resized
let rightPanelHeight = $state(0);
let explorerHeight = $derived(
$treeFileView
? Math.min(
Math.max($explorerPanelSize > 0 ? $explorerPanelSize : rightPanelHeight / 2, 60),
Math.max(rightPanelHeight - 80, 60)
)
: rightPanelHeight
);
let bottomPanelOrientation = $derived(
bottomPanelWidth && bottomPanelWidth >= 540 && $elevationProfile ? 'horizontal' : 'vertical'
);
onMount(async () => {
settings.connectToDatabase(db);
fileStateCollection.connectToDatabase(db).then(() => {
let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]');
let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]');
let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile));
// the language of the page, which is in its address (none for English)
let language = $derived(page.params.language ?? 'en');
// whether the language that was chosen last was gone back to (the desktop app)
let languageRestored = $state(false);
if (urls.length > 0) {
let downloads: Promise<File | null>[] = [];
urls.forEach((url) => {
downloads.push(
fetch(url)
.then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? ''))
);
});
Promise.all(downloads).then((files) => {
loadFiles(files.filter((file) => file !== null));
});
}
});
$effect(() => {
if (languageRestored) {
settings.language.set(language);
}
});
onDestroy(() => {
settings.disconnectFromDatabase();
fileStateCollection.disconnectFromDatabase();
onMount(async () => {
// files that the system opens with the app while it runs
engine.onFilesOpened((ids) => boundsManager.fitBoundsOnLoad(ids));
settings.connect(await engine.openStorage());
if (inDesktopApp()) {
openLinksInBrowser();
// the window of the desktop app always opens the same page, in English: it goes to the
// language that was chosen last
const saved = get(settings.language);
if (saved !== undefined && saved !== language) {
await goto(
resolve(
`${getURLForLanguage(saved, page.url.pathname)}${page.url.search}${page.url.hash}`
),
{ replaceState: true }
);
}
languageRestored = true;
}
let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]');
let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]');
let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile));
if (urls.length > 0) {
let downloads: Promise<File | null>[] = [];
urls.forEach((url) => {
downloads.push(
fetch(url)
.then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? ''))
);
});
Promise.all(downloads).then((files) => {
loadFiles(files.filter((file) => file !== null));
});
}
});
</script>
@@ -118,7 +163,7 @@
<CoordinatesPopup />
<Toaster richColors />
{#if !$treeFileView}
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
<div class="h-10 w-full pointer-events-none absolute bottom-0 left-0 z-30">
<FileList orientation="horizontal" />
</div>
{/if}
@@ -139,14 +184,14 @@
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
>
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
statistics={engine.statistics}
{slicedStatistics}
orientation={bottomPanelOrientation == 'horizontal' ? 'vertical' : 'horizontal'}
/>
{#if $elevationProfile}
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
statistics={engine.statistics}
{slicedStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
@@ -154,12 +199,60 @@
{/if}
</div>
</div>
{#if $treeFileView}
{#if $treeFileView || $workspace?.folder}
<Resizer orientation="col" bind:after={$rightPanelSize} minAfter={100} maxAfter={400} />
<FileList orientation="vertical" recursive={true} style="width: {$rightPanelSize}px" />
<div
bind:clientHeight={rightPanelHeight}
class="flex flex-col h-full min-h-0 shrink-0"
style="width: {$rightPanelSize}px"
>
{#if $treeFileView}
{#if $workspace}
<div class="px-2 py-1 text-sm font-semibold shrink-0">
{i18n._('menu.workspace')}
</div>
{/if}
<!-- a row, as the file list was: its scroll area is as high as this, whatever its content -->
<div class="grow min-h-0 flex flex-row">
<FileList
orientation="vertical"
recursive={true}
style="width: {$rightPanelSize}px"
/>
</div>
{/if}
<!-- the open folder (the desktop app) under the files that are open -->
{#if $workspace?.folder}
{#if $treeFileView}
<Resizer
orientation="row"
bind:after={() => explorerHeight, (height) => ($explorerPanelSize = height)}
minAfter={60}
maxAfter={Math.max(rightPanelHeight - 80, 60)}
/>
{/if}
<div class="flex flex-col min-h-0 shrink-0" style="height: {explorerHeight}px">
<Explorer />
</div>
{/if}
</div>
{/if}
</div>
<AlertDialog.Root open={$outdated}>
<AlertDialog.Content escapeKeydownBehavior="ignore" interactOutsideBehavior="ignore">
<AlertDialog.Header>
<AlertDialog.Title>{i18n._('outdated.title')}</AlertDialog.Title>
<AlertDialog.Description>{i18n._('outdated.description')}</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Action onclick={() => location.reload()}>
{i18n._('outdated.reload')}
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
<style lang="postcss">
@reference "tailwindcss";
@@ -1,4 +1,4 @@
import { languages } from '$lib/languages';
import { languages } from '$lib/i18n/languages';
import type { EntryGenerator } from './$types';
export const entries: EntryGenerator = () => {
@@ -1,8 +1,9 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { Button } from '$lib/components/ui/button';
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
import { getURLForLanguage } from '$lib/utils';
import { i18n } from '$lib/i18n.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import { page } from '$app/state';
import { guides } from '$lib/components/docs/docs';
import type { Snippet } from 'svelte';
@@ -24,10 +25,10 @@
<div class="mb-2">
<AlgoliaDocSearch />
</div>
{#each Object.keys(guides) as guide}
{#each Object.keys(guides) as guide (guide)}
<Button
variant="link"
href={getURLForLanguage(i18n.lang, `/help/${guide}`)}
href={resolve(getURLForLanguage(i18n.lang, `/help/${guide}`))}
class="min-h-5 h-fit p-0 w-fit border-none text-muted-foreground hover:text-foreground hover:no-underline font-normal hover:font-semibold items-start whitespace-normal {page
.params.guide === guide
? 'font-semibold text-foreground'
@@ -35,10 +36,10 @@
>
{data.guideTitles[guide]}
</Button>
{#each guides[guide] as subGuide}
{#each guides[guide] as subGuide (subGuide)}
<Button
variant="link"
href={getURLForLanguage(i18n.lang, `/help/${guide}/${subGuide}`)}
href={resolve(getURLForLanguage(i18n.lang, `/help/${guide}/${subGuide}`))}
class="min-h-5 h-fit p-0 w-fit border-none text-muted-foreground hover:text-foreground hover:no-underline font-normal hover:font-semibold items-start whitespace-normal ml-3 {page
.params.guide ===
guide + '/' + subGuide
@@ -1,4 +1,4 @@
import { languages } from '$lib/languages';
import { languages } from '$lib/i18n/languages';
import type { EntryGenerator } from './$types';
export const entries: EntryGenerator = () => {
@@ -1,7 +1,8 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { Button } from '$lib/components/ui/button';
import { getURLForLanguage } from '$lib/utils';
import { i18n } from '$lib/i18n.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import { guides, guideIcons } from '$lib/components/docs/docs';
let {
@@ -14,10 +15,10 @@
</script>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
{#each Object.keys(guides) as guide}
{#each Object.keys(guides) as guide (guide)}
<Button
variant="outline"
href={getURLForLanguage(i18n.lang, `/help/${guide}`)}
href={resolve(getURLForLanguage(i18n.lang, `/help/${guide}`))}
class="min-h-36 h-full pt-6 pb-3 px-0"
>
<div class="flex flex-col w-full">
@@ -28,10 +29,12 @@
{data.guideTitles[guide]}
</div>
<div class="flex flex-row justify-center flex-wrap gap-x-6 px-6">
{#each guides[guide] as subGuide}
{#each guides[guide] as subGuide (subGuide)}
<Button
variant="link"
href={getURLForLanguage(i18n.lang, `/help/${guide}/${subGuide}`)}
href={resolve(
getURLForLanguage(i18n.lang, `/help/${guide}/${subGuide}`)
)}
class="min-h-8 h-fit min-w-24 px-0 py-1 text-muted-foreground text-base text-center whitespace-normal"
>
{#if typeof guideIcons[subGuide] === 'string'}
@@ -1,16 +1,16 @@
import { languages } from '$lib/languages';
import { languages } from '$lib/i18n/languages';
import { guides } from '$lib/components/docs/docs';
import type { EntryGenerator } from './$types';
export const entries: EntryGenerator = () => {
let entries = [];
for (let lang of Object.keys(languages)) {
for (let guide of Object.keys(guides)) {
const entries = [];
for (const lang of Object.keys(languages)) {
for (const guide of Object.keys(guides)) {
entries.push({
language: lang == 'en' ? '' : lang,
guide,
});
for (let subguide of guides[guide]) {
for (const subguide of guides[guide]) {
entries.push({
language: lang == 'en' ? '' : lang,
guide: `${guide}/${subguide}`,
@@ -1,16 +1,21 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { getNextGuide, getPreviousGuide } from '$lib/components/docs/docs';
import DocsContainer from '$lib/components/docs/DocsContainer.svelte';
import { Button } from '$lib/components/ui/button';
import { getURLForLanguage } from '$lib/utils';
import { ChevronLeft, ChevronRight, PenLine, CornerDownRight } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import type { Component } from 'svelte';
let { data }: { data: { guideModule: any; guideTitles: Record<string, string> } } = $props();
let {
data,
}: { data: { guideModule: { default: Component }; guideTitles: Record<string, string> } } =
$props();
let previousGuide = $derived(getPreviousGuide(page.params.guide));
let nextGuide = $derived(getNextGuide(page.params.guide));
let previousGuide = $derived(getPreviousGuide(page.params.guide ?? ''));
let nextGuide = $derived(getNextGuide(page.params.guide ?? ''));
</script>
<div class="markdown flex flex-col gap-3">
@@ -22,7 +27,7 @@
<Button
variant="outline"
class="mr-auto"
href={getURLForLanguage(i18n.lang, `/help/${previousGuide}`)}
href={resolve(getURLForLanguage(i18n.lang, `/help/${previousGuide}`))}
>
<ChevronLeft size="14" class="mt-0.5" />
{data.guideTitles[previousGuide]}
@@ -32,7 +37,7 @@
<Button
variant="outline"
class="ml-auto"
href={getURLForLanguage(i18n.lang, `/help/${nextGuide}`)}
href={resolve(getURLForLanguage(i18n.lang, `/help/${nextGuide}`))}
>
{data.guideTitles[nextGuide]}
<ChevronRight size="14" class="mt-0.5" />