7 Commits

Author SHA1 Message Date
vcoppe
8fe6565527 use browser navigation for /app 2025-11-28 17:48:21 +01:00
vcoppe
69b018022d fix waypoint default sym value 2025-11-27 08:01:05 +01:00
vcoppe
467cb2e589 update extension api 2025-11-25 19:18:54 +01:00
vcoppe
f13d8c1e22 New translations en.json (Chinese Simplified) (#280) 2025-11-25 18:23:13 +01:00
vcoppe
e230d55b82 fix cloning 2025-11-25 18:22:51 +01:00
vcoppe
46fcdb4bb2 elevation gain computation hybrid between ramer-douglas-peucker and smoothing 2025-11-21 20:20:42 +01:00
vcoppe
429212ef23 use standard sprite path 2025-11-20 08:53:24 +01:00
11 changed files with 133 additions and 63 deletions

View File

@@ -17,6 +17,9 @@ import {
import { immerable, isDraft, original, freeze } from 'immer';
function cloneJSON<T>(obj: T): T {
if (obj === undefined) {
return undefined;
}
if (obj === null || typeof obj !== 'object') {
return null;
}
@@ -973,7 +976,7 @@ export class TrackSegment extends GPXTreeLeaf {
_elevationComputation(statistics: GPXStatistics) {
let simplified = ramerDouglasPeucker(
this.trkpt,
5,
20,
getElevationDistanceFunction(statistics)
);
@@ -981,59 +984,67 @@ export class TrackSegment extends GPXTreeLeaf {
let start = simplified[i].point._data.index;
let end = simplified[i + 1].point._data.index;
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
const dist =
statistics.local.distance.total[end] - statistics.local.distance.total[start];
let cumulEle = 0;
let currentStart = start;
let currentEnd = start;
let smoothedEle = distanceWindowSmoothing(start, end + 1, statistics, 0.1, (s, e) => {
for (let i = currentStart; i < s; i++) {
cumulEle -= this.trkpt[i].ele ?? 0;
}
for (let i = currentEnd; i <= e; i++) {
cumulEle += this.trkpt[i].ele ?? 0;
}
currentStart = s;
currentEnd = e + 1;
return cumulEle / (e - s + 1);
});
smoothedEle[0] = this.trkpt[start].ele ?? 0;
smoothedEle[smoothedEle.length - 1] = this.trkpt[end].ele ?? 0;
for (let j = start; j < end + (i + 1 == simplified.length - 1 ? 1 : 0); j++) {
const localDist =
statistics.local.distance.total[j] - statistics.local.distance.total[start];
const localEle = dist > 0 ? (localDist / dist) * ele : 0;
statistics.local.elevation.gain.push(
statistics.global.elevation.gain + (localEle > 0 ? localEle : 0)
);
statistics.local.elevation.loss.push(
statistics.global.elevation.loss + (localEle < 0 ? -localEle : 0)
);
}
for (let j = start; j < end; j++) {
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
if (ele > 0) {
statistics.global.elevation.gain += ele;
} else if (ele < 0) {
statistics.global.elevation.loss -= ele;
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
if (ele > 0) {
statistics.global.elevation.gain += ele;
} else if (ele < 0) {
statistics.global.elevation.loss -= ele;
}
}
}
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
let slope = [];
let length = [];
for (let a = 0; a < simplified.length - 1; ) {
let b = a + 1;
while (b < simplified.length - 1 && simplified[b].distance < 20) {
b++;
}
let start = simplified[a].point._data.index;
let end = simplified[b].point._data.index;
for (let i = 0; i < simplified.length - 1; i++) {
let start = simplified[i].point._data.index;
let end = simplified[i + 1].point._data.index;
let dist =
statistics.local.distance.total[end] - statistics.local.distance.total[start];
let ele = (simplified[b].point.ele ?? 0) - (simplified[a].point.ele ?? 0);
let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0);
for (let j = start; j < end + (b === simplified.length - 1 ? 1 : 0); j++) {
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
slope.push((0.1 * ele) / dist);
length.push(dist);
}
a = b;
}
statistics.local.slope.segment = slope;
statistics.local.slope.length = length;
statistics.local.slope.at = distanceWindowSmoothing(statistics, 0.05, (start, end) => {
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
const dist =
statistics.local.distance.total[end] - statistics.local.distance.total[start];
return dist > 0 ? (0.1 * ele) / dist : 0;
});
statistics.local.slope.at = distanceWindowSmoothing(
0,
this.trkpt.length,
statistics,
0.05,
(start, end) => {
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
const dist =
statistics.local.distance.total[end] - statistics.local.distance.total[start];
return dist > 0 ? (0.1 * ele) / dist : 0;
}
);
}
getNumberOfTrackPoints(): number {
@@ -1484,12 +1495,18 @@ export class Waypoint {
this.attributes = waypoint.attributes;
this.ele = waypoint.ele;
this.time = waypoint.time;
this.name = waypoint.name;
this.cmt = waypoint.cmt;
this.desc = waypoint.desc;
this.link = waypoint.link;
this.sym = waypoint.sym;
this.type = waypoint.type;
this.name = waypoint.name === '' ? undefined : waypoint.name;
this.cmt = waypoint.cmt === '' ? undefined : waypoint.cmt;
this.desc = waypoint.desc === '' ? undefined : waypoint.desc;
this.link =
!waypoint.link ||
!waypoint.link.attributes ||
!waypoint.link.attributes.href ||
waypoint.link.attributes.href === ''
? undefined
: waypoint.link;
this.sym = waypoint.sym === '' ? undefined : waypoint.sym;
this.type = waypoint.type === '' ? undefined : waypoint.type;
if (waypoint.hasOwnProperty('_data')) {
this._data = waypoint._data;
}
@@ -1938,36 +1955,39 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
}
function windowSmoothing(
length: number,
left: number,
right: number,
distance: (index1: number, index2: number) => number,
window: number,
compute: (start: number, end: number) => number
): number[] {
let result = [];
let start = 0,
end = 0;
for (var i = 0; i < length; i++) {
let start = left;
for (var i = left; i < right; i++) {
while (start + 1 < i && distance(start, i) > window) {
start++;
}
end = Math.min(i + 2, length);
while (end < length && distance(i, end) <= window) {
let end = Math.min(i + 2, right);
while (end < right && distance(i, end) <= window) {
end++;
}
result[i] = compute(start, end - 1);
result.push(compute(start, end - 1));
}
return result;
}
function distanceWindowSmoothing(
left: number,
right: number,
statistics: GPXStatistics,
window: number,
compute: (start: number, end: number) => number
): number[] {
return windowSmoothing(
statistics.local.points.length,
left,
right,
(index1, index2) =>
statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
window,
@@ -1981,6 +2001,7 @@ function timeWindowSmoothing(
compute: (start: number, end: number) => number
): number[] {
return windowSmoothing(
0,
points.length,
(index1, index2) =>
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,

View File

@@ -34,6 +34,7 @@
{i18n._('homepage.home')}
</Button>
<Button
data-sveltekit-reload
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/app')}

View File

@@ -23,6 +23,7 @@
{i18n._('homepage.home')}
</Button>
<Button
data-sveltekit-reload
variant="link"
class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/app')}

View File

@@ -85,7 +85,7 @@
{:else if anySelectedLayer(node[id])}
<CollapsibleTreeNode {id}>
{#snippet trigger()}
<span>{i18n._(`layers.label.${id}`)}</span>
<span>{i18n._(`layers.label.${id}`, id)}</span>
{/snippet}
{#snippet content()}
<div class="ml-2">

View File

@@ -8,6 +8,7 @@ import { map } from '$lib/components/map/map';
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
export type CustomOverlay = {
extensionName: string;
id: string;
name: string;
tileUrls: string[];
@@ -46,8 +47,16 @@ export class ExtensionAPI {
}
addOrUpdateOverlay(overlay: CustomOverlay) {
if (!overlay.id || !overlay.name || !overlay.tileUrls || overlay.tileUrls.length === 0) {
throw new Error('Overlay must have an id, name, and at least one tile URL.');
if (
!overlay.extensionName ||
!overlay.id ||
!overlay.name ||
!overlay.tileUrls ||
overlay.tileUrls.length === 0
) {
throw new Error(
'Overlay must have an extensionName, id, name, and at least one tile URL.'
);
}
overlay.id = this.getOverlayId(overlay.id);
@@ -75,10 +84,17 @@ export class ExtensionAPI {
],
};
overlayTree.overlays.world[overlay.id] = true;
if (!overlayTree.overlays.hasOwnProperty(overlay.extensionName)) {
overlayTree.overlays[overlay.extensionName] = {};
}
overlayTree.overlays[overlay.extensionName][overlay.id] = true;
selectedOverlayTree.update((selected) => {
selected.overlays.world[overlay.id] = true;
if (!selected.overlays.hasOwnProperty(overlay.extensionName)) {
selected.overlays[overlay.extensionName] = {};
}
selected.overlays[overlay.extensionName][overlay.id] = true;
return selected;
});
@@ -94,7 +110,10 @@ export class ExtensionAPI {
}
currentOverlays.update((current) => {
current.overlays.world[overlay.id] = show;
if (!current.overlays.hasOwnProperty(overlay.extensionName)) {
current.overlays[overlay.extensionName] = {};
}
current.overlays[overlay.extensionName][overlay.id] = show;
return current;
});
}
@@ -133,6 +152,29 @@ export class ExtensionAPI {
});
}
updateOverlaysOrder(ids: string[]) {
ids = ids.map((id) => this.getOverlayId(id));
selectedOverlayTree.update((selected) => {
let isSelected: Record<string, boolean> = {};
ids.forEach((id) => {
const overlay = get(this._overlays).get(id);
if (
overlay &&
selected.overlays.hasOwnProperty(overlay.extensionName) &&
selected.overlays[overlay.extensionName].hasOwnProperty(id)
) {
isSelected[id] = selected.overlays[overlay.extensionName][id];
delete selected.overlays[overlay.extensionName][id];
}
});
Object.entries(isSelected).forEach(([id, value]) => {
const overlay = get(this._overlays).get(id)!;
selected.overlays[overlay.extensionName][id] = value;
});
return selected;
});
}
isLayerFromExtension = derived(this._overlays, ($overlays) => {
return (id: string) => $overlays.has(id);
});

View File

@@ -43,7 +43,7 @@ export class MapboxGLMap {
sources: {},
layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${accessToken}`,
sprite: 'mapbox://sprites/mapbox/outdoors-v12',
},
},
{

View File

@@ -95,7 +95,7 @@
desc: description.length > 0 ? description : undefined,
cmt: description.length > 0 ? description : undefined,
link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: sym,
sym: sym.length > 0 ? sym : undefined,
},
selectedWaypoint.wpt && selectedWaypoint.fileId
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)

View File

@@ -14,7 +14,7 @@ class Locale {
private _isLoadingInitial = $state(true);
private _isLoading = $state(true);
private dictionary: Dictionary = $state({});
private _t = $derived((key: string) => {
private _t = $derived((key: string, fallback?: string) => {
const keys = key.split('.');
let value: string | Dictionary = this.dictionary;
@@ -22,7 +22,7 @@ class Locale {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return key;
return fallback || key;
}
}

View File

@@ -28,7 +28,7 @@
"undo": "撤销",
"redo": "恢复",
"delete": "删除",
"delete_all": "Delete all",
"delete_all": "",
"select_all": "全选",
"view": "显示",
"elevation_profile": "海拔剖面图",

View File

@@ -19,6 +19,7 @@
{i18n._('homepage.home')}
</Button>
<Button
data-sveltekit-reload
href={getURLForLanguage(i18n.lang, '/app')}
class="text-base w-1/4 min-w-fit rounded-full"
>

View File

@@ -64,7 +64,11 @@
{i18n._('metadata.description')}
</div>
<div class="w-full flex flex-row justify-center gap-3">
<Button href={getURLForLanguage(i18n.lang, '/app')} class="w-1/3 min-w-fit">
<Button
data-sveltekit-reload
href={getURLForLanguage(i18n.lang, '/app')}
class="w-1/3 min-w-fit"
>
<Map size="18" />
{i18n._('homepage.app')}
</Button>