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

@@ -0,0 +1,153 @@
// The API that browser extensions use to add overlays to the map (`window.gpxstudio`).
import assert from 'node:assert/strict';
import { afterEach, beforeEach, describe, it } from 'node:test';
import { get } from 'svelte/store';
import { overlays, overlayTree, type LayerTreeType } from '$lib/data/layers';
import { engine } from '$lib/engine';
import { settings } from '$lib/state/settings';
import { ExtensionAPI, type CustomOverlay } from './extension-api';
import { isSelected } from './utils';
const original = { set: engine.setSetting, delete: engine.deleteSetting };
/** The raster source that an overlay added by an extension uses. */
function rasterSource(id: string) {
const style = overlays[id];
assert.ok(typeof style === 'object');
const source = style.sources[id];
assert.ok(source?.type === 'raster');
return source;
}
const overlayTreeBefore = JSON.stringify(overlayTree);
const overlayIdsBefore = Object.keys(overlays).sort();
const overlay = (id: string, extensionName = 'My extension'): CustomOverlay => ({
extensionName,
id,
name: `Overlay ${id}`,
tileUrls: [`https://tiles.example.com/${id}/{z}/{x}/{y}.png`],
});
let api: ExtensionAPI;
beforeEach(() => {
engine.setSetting = () => {};
engine.deleteSetting = () => {};
settings.connect({});
settings.initialize();
api = new ExtensionAPI();
});
afterEach(() => {
// the layers are shared by the whole app: leave them as they were
api.filterOverlays([]);
for (const extension of ['My extension', 'Other extension']) {
delete (overlayTree.overlays as LayerTreeType)[extension];
}
engine.setSetting = original.set;
engine.deleteSetting = original.delete;
assert.equal(JSON.stringify(overlayTree), overlayTreeBefore);
assert.deepEqual(Object.keys(overlays).sort(), overlayIdsBefore);
});
describe('ExtensionAPI', () => {
it('adds an overlay as a layer, in the tree of its extension', () => {
api.addOrUpdateOverlay(overlay('one'));
const source = rasterSource('extension-one');
assert.deepEqual(source.tiles, ['https://tiles.example.com/one/{z}/{x}/{y}.png']);
assert.equal(source.tileSize, 256);
assert.equal(source.maxzoom, 22);
assert.ok((overlayTree.overlays as LayerTreeType)['My extension']);
assert.equal(isSelected(overlayTree, 'extension-one'), true);
assert.equal(isSelected(get(settings.selectedOverlayTree), 'extension-one'), true);
// not shown until the user chooses to
assert.equal(isSelected(get(settings.currentOverlays)!, 'extension-one'), false);
assert.ok(get(api.isLayerFromExtension)('extension-one'));
assert.equal(get(api.getLayerName)('extension-one'), 'Overlay one');
assert.equal(get(api.getLayerName)('other'), '');
});
it('uses big tiles when the urls say so, and the max zoom it is given', () => {
api.addOrUpdateOverlay({
...overlay('big'),
tileUrls: ['https://tiles.example.com/512/{z}/{x}/{y}.png'],
maxZoom: 15,
});
const source = rasterSource('extension-big');
assert.equal(source.tileSize, 512);
assert.equal(source.maxzoom, 15);
});
it('refuses an overlay that is not complete', () => {
for (const bad of [
{ ...overlay('a'), extensionName: '' },
{ ...overlay('a'), id: '' },
{ ...overlay('a'), name: '' },
{ ...overlay('a'), tileUrls: [] },
{ ...overlay('a'), tileUrls: undefined as unknown as string[] },
]) {
assert.throws(() => api.addOrUpdateOverlay(bad), /must have/);
}
assert.ok(!('extension-a' in overlays));
});
it('updates an overlay that is added again, keeping it shown', () => {
api.addOrUpdateOverlay(overlay('one'));
settings.currentOverlays.update((current) => {
(current.overlays as LayerTreeType)['My extension'] = { 'extension-one': true };
return current;
});
api.addOrUpdateOverlay({
...overlay('one'),
name: 'Renamed',
tileUrls: ['https://other/{z}'],
});
assert.equal(get(api.getLayerName)('extension-one'), 'Renamed');
assert.deepEqual(rasterSource('extension-one').tiles, ['https://other/{z}']);
assert.equal(isSelected(get(settings.currentOverlays)!, 'extension-one'), true);
});
it('removes the overlays that are not in the list anymore, everywhere', () => {
api.addOrUpdateOverlay(overlay('one'));
api.addOrUpdateOverlay(overlay('two', 'Other extension'));
api.filterOverlays(['two']);
assert.ok(!('extension-one' in overlays));
assert.ok('extension-two' in overlays);
assert.equal(get(api.isLayerFromExtension)('extension-one'), false);
assert.equal(get(api.isLayerFromExtension)('extension-two'), true);
for (const tree of [
overlayTree,
get(settings.selectedOverlayTree),
get(settings.currentOverlays)!,
]) {
assert.equal(isSelected(tree, 'extension-one'), false);
}
assert.equal(isSelected(overlayTree, 'extension-two'), true);
api.filterOverlays([]);
assert.ok(!('extension-two' in overlays));
});
it('puts the overlays in the order it is given', () => {
api.addOrUpdateOverlay(overlay('one'));
api.addOrUpdateOverlay(overlay('two'));
api.addOrUpdateOverlay(overlay('three'));
api.updateOverlaysOrder(['three', 'one', 'two']);
const extension = (get(settings.selectedOverlayTree).overlays as LayerTreeType)[
'My extension'
];
assert.deepEqual(Object.keys(extension as LayerTreeType), [
'extension-three',
'extension-one',
'extension-two',
]);
// ids it does not know are ignored
api.updateOverlaysOrder(['unknown', 'two']);
assert.equal(Object.keys(extension as LayerTreeType).at(-1), 'extension-two');
});
});