mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-02 18:12:11 +00:00
Compare commits
7 Commits
4ea0ea6a7a
...
l10n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe6565527 | ||
|
|
69b018022d | ||
|
|
467cb2e589 | ||
|
|
f13d8c1e22 | ||
|
|
e230d55b82 | ||
|
|
46fcdb4bb2 | ||
|
|
429212ef23 |
123
gpx/src/gpx.ts
123
gpx/src/gpx.ts
@@ -17,6 +17,9 @@ import {
|
|||||||
import { immerable, isDraft, original, freeze } from 'immer';
|
import { immerable, isDraft, original, freeze } from 'immer';
|
||||||
|
|
||||||
function cloneJSON<T>(obj: T): T {
|
function cloneJSON<T>(obj: T): T {
|
||||||
|
if (obj === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (obj === null || typeof obj !== 'object') {
|
if (obj === null || typeof obj !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -973,7 +976,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
_elevationComputation(statistics: GPXStatistics) {
|
_elevationComputation(statistics: GPXStatistics) {
|
||||||
let simplified = ramerDouglasPeucker(
|
let simplified = ramerDouglasPeucker(
|
||||||
this.trkpt,
|
this.trkpt,
|
||||||
5,
|
20,
|
||||||
getElevationDistanceFunction(statistics)
|
getElevationDistanceFunction(statistics)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -981,59 +984,67 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
let start = simplified[i].point._data.index;
|
let start = simplified[i].point._data.index;
|
||||||
let end = simplified[i + 1].point._data.index;
|
let end = simplified[i + 1].point._data.index;
|
||||||
|
|
||||||
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
let cumulEle = 0;
|
||||||
const dist =
|
let currentStart = start;
|
||||||
statistics.local.distance.total[end] - statistics.local.distance.total[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++) {
|
for (let j = start; j < end; j++) {
|
||||||
const localDist =
|
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||||
statistics.local.distance.total[j] - statistics.local.distance.total[start];
|
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ele > 0) {
|
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
|
||||||
statistics.global.elevation.gain += ele;
|
if (ele > 0) {
|
||||||
} else if (ele < 0) {
|
statistics.global.elevation.gain += ele;
|
||||||
statistics.global.elevation.loss -= 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 slope = [];
|
||||||
let length = [];
|
let length = [];
|
||||||
for (let a = 0; a < simplified.length - 1; ) {
|
for (let i = 0; i < simplified.length - 1; i++) {
|
||||||
let b = a + 1;
|
let start = simplified[i].point._data.index;
|
||||||
while (b < simplified.length - 1 && simplified[b].distance < 20) {
|
let end = simplified[i + 1].point._data.index;
|
||||||
b++;
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = simplified[a].point._data.index;
|
|
||||||
let end = simplified[b].point._data.index;
|
|
||||||
let dist =
|
let dist =
|
||||||
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
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);
|
slope.push((0.1 * ele) / dist);
|
||||||
length.push(dist);
|
length.push(dist);
|
||||||
}
|
}
|
||||||
|
|
||||||
a = b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.slope.segment = slope;
|
statistics.local.slope.segment = slope;
|
||||||
statistics.local.slope.length = length;
|
statistics.local.slope.length = length;
|
||||||
statistics.local.slope.at = distanceWindowSmoothing(statistics, 0.05, (start, end) => {
|
statistics.local.slope.at = distanceWindowSmoothing(
|
||||||
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
0,
|
||||||
const dist =
|
this.trkpt.length,
|
||||||
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
statistics,
|
||||||
return dist > 0 ? (0.1 * ele) / dist : 0;
|
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 {
|
getNumberOfTrackPoints(): number {
|
||||||
@@ -1484,12 +1495,18 @@ export class Waypoint {
|
|||||||
this.attributes = waypoint.attributes;
|
this.attributes = waypoint.attributes;
|
||||||
this.ele = waypoint.ele;
|
this.ele = waypoint.ele;
|
||||||
this.time = waypoint.time;
|
this.time = waypoint.time;
|
||||||
this.name = waypoint.name;
|
this.name = waypoint.name === '' ? undefined : waypoint.name;
|
||||||
this.cmt = waypoint.cmt;
|
this.cmt = waypoint.cmt === '' ? undefined : waypoint.cmt;
|
||||||
this.desc = waypoint.desc;
|
this.desc = waypoint.desc === '' ? undefined : waypoint.desc;
|
||||||
this.link = waypoint.link;
|
this.link =
|
||||||
this.sym = waypoint.sym;
|
!waypoint.link ||
|
||||||
this.type = waypoint.type;
|
!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')) {
|
if (waypoint.hasOwnProperty('_data')) {
|
||||||
this._data = waypoint._data;
|
this._data = waypoint._data;
|
||||||
}
|
}
|
||||||
@@ -1938,36 +1955,39 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function windowSmoothing(
|
function windowSmoothing(
|
||||||
length: number,
|
left: number,
|
||||||
|
right: number,
|
||||||
distance: (index1: number, index2: number) => number,
|
distance: (index1: number, index2: number) => number,
|
||||||
window: number,
|
window: number,
|
||||||
compute: (start: number, end: number) => number
|
compute: (start: number, end: number) => number
|
||||||
): number[] {
|
): number[] {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
let start = 0,
|
let start = left;
|
||||||
end = 0;
|
for (var i = left; i < right; i++) {
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
while (start + 1 < i && distance(start, i) > window) {
|
while (start + 1 < i && distance(start, i) > window) {
|
||||||
start++;
|
start++;
|
||||||
}
|
}
|
||||||
end = Math.min(i + 2, length);
|
let end = Math.min(i + 2, right);
|
||||||
while (end < length && distance(i, end) <= window) {
|
while (end < right && distance(i, end) <= window) {
|
||||||
end++;
|
end++;
|
||||||
}
|
}
|
||||||
result[i] = compute(start, end - 1);
|
result.push(compute(start, end - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothing(
|
function distanceWindowSmoothing(
|
||||||
|
left: number,
|
||||||
|
right: number,
|
||||||
statistics: GPXStatistics,
|
statistics: GPXStatistics,
|
||||||
window: number,
|
window: number,
|
||||||
compute: (start: number, end: number) => number
|
compute: (start: number, end: number) => number
|
||||||
): number[] {
|
): number[] {
|
||||||
return windowSmoothing(
|
return windowSmoothing(
|
||||||
statistics.local.points.length,
|
left,
|
||||||
|
right,
|
||||||
(index1, index2) =>
|
(index1, index2) =>
|
||||||
statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
|
statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
|
||||||
window,
|
window,
|
||||||
@@ -1981,6 +2001,7 @@ function timeWindowSmoothing(
|
|||||||
compute: (start: number, end: number) => number
|
compute: (start: number, end: number) => number
|
||||||
): number[] {
|
): number[] {
|
||||||
return windowSmoothing(
|
return windowSmoothing(
|
||||||
|
0,
|
||||||
points.length,
|
points.length,
|
||||||
(index1, index2) =>
|
(index1, index2) =>
|
||||||
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
data-sveltekit-reload
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
data-sveltekit-reload
|
||||||
variant="link"
|
variant="link"
|
||||||
class="text-base px-0 has-[>svg]:px-0"
|
class="text-base px-0 has-[>svg]:px-0"
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
{:else if anySelectedLayer(node[id])}
|
{:else if anySelectedLayer(node[id])}
|
||||||
<CollapsibleTreeNode {id}>
|
<CollapsibleTreeNode {id}>
|
||||||
{#snippet trigger()}
|
{#snippet trigger()}
|
||||||
<span>{i18n._(`layers.label.${id}`)}</span>
|
<span>{i18n._(`layers.label.${id}`, id)}</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
<div class="ml-2">
|
<div class="ml-2">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { map } from '$lib/components/map/map';
|
|||||||
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
|
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
|
||||||
|
|
||||||
export type CustomOverlay = {
|
export type CustomOverlay = {
|
||||||
|
extensionName: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
tileUrls: string[];
|
tileUrls: string[];
|
||||||
@@ -46,8 +47,16 @@ export class ExtensionAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addOrUpdateOverlay(overlay: CustomOverlay) {
|
addOrUpdateOverlay(overlay: CustomOverlay) {
|
||||||
if (!overlay.id || !overlay.name || !overlay.tileUrls || overlay.tileUrls.length === 0) {
|
if (
|
||||||
throw new Error('Overlay must have an id, name, and at least one tile URL.');
|
!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);
|
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) => {
|
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;
|
return selected;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +110,10 @@ export class ExtensionAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentOverlays.update((current) => {
|
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;
|
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) => {
|
isLayerFromExtension = derived(this._overlays, ($overlays) => {
|
||||||
return (id: string) => $overlays.has(id);
|
return (id: string) => $overlays.has(id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class MapboxGLMap {
|
|||||||
sources: {},
|
sources: {},
|
||||||
layers: [],
|
layers: [],
|
||||||
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
desc: description.length > 0 ? description : undefined,
|
desc: description.length > 0 ? description : undefined,
|
||||||
cmt: description.length > 0 ? description : undefined,
|
cmt: description.length > 0 ? description : undefined,
|
||||||
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
||||||
sym: sym,
|
sym: sym.length > 0 ? sym : undefined,
|
||||||
},
|
},
|
||||||
selectedWaypoint.wpt && selectedWaypoint.fileId
|
selectedWaypoint.wpt && selectedWaypoint.fileId
|
||||||
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Locale {
|
|||||||
private _isLoadingInitial = $state(true);
|
private _isLoadingInitial = $state(true);
|
||||||
private _isLoading = $state(true);
|
private _isLoading = $state(true);
|
||||||
private dictionary: Dictionary = $state({});
|
private dictionary: Dictionary = $state({});
|
||||||
private _t = $derived((key: string) => {
|
private _t = $derived((key: string, fallback?: string) => {
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let value: string | Dictionary = this.dictionary;
|
let value: string | Dictionary = this.dictionary;
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class Locale {
|
|||||||
if (value && typeof value === 'object' && k in value) {
|
if (value && typeof value === 'object' && k in value) {
|
||||||
value = value[k];
|
value = value[k];
|
||||||
} else {
|
} else {
|
||||||
return key;
|
return fallback || key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"undo": "撤销",
|
"undo": "撤销",
|
||||||
"redo": "恢复",
|
"redo": "恢复",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"delete_all": "Delete all",
|
"delete_all": "",
|
||||||
"select_all": "全选",
|
"select_all": "全选",
|
||||||
"view": "显示",
|
"view": "显示",
|
||||||
"elevation_profile": "海拔剖面图",
|
"elevation_profile": "海拔剖面图",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
data-sveltekit-reload
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
class="text-base w-1/4 min-w-fit rounded-full"
|
class="text-base w-1/4 min-w-fit rounded-full"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -64,7 +64,11 @@
|
|||||||
{i18n._('metadata.description')}
|
{i18n._('metadata.description')}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row justify-center gap-3">
|
<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" />
|
<Map size="18" />
|
||||||
{i18n._('homepage.app')}
|
{i18n._('homepage.app')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user