mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
progress
This commit is contained in:
1 parent
606a0e974a
commit
67a4c090bd
107 files changed
+3474
-3948
No files matched your search
@@ -18,7 +18,7 @@
|
||||
Construction,
|
||||
} from '@lucide/svelte';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { Coordinates } from 'gpx';
|
||||
import type { Coordinates } from '$lib/geo';
|
||||
import type { SelectionStatistics } from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
|
||||
@@ -21,7 +21,7 @@ import Chart, {
|
||||
type TooltipItem,
|
||||
} from 'chart.js/auto';
|
||||
import { get, type Readable, type Writable } from 'svelte/store';
|
||||
import type { Coordinates } from 'gpx';
|
||||
import type { Coordinates } from '$lib/geo';
|
||||
import {
|
||||
engine,
|
||||
NO_TIME,
|
||||
@@ -456,7 +456,7 @@ export class ElevationProfile {
|
||||
highway: highways[index],
|
||||
sacScale: sacScales[index],
|
||||
mtbScale: mtbScales[index],
|
||||
coordinates: { lat: data.lat[index], lon: data.lng[index] },
|
||||
coordinates: { lat: data.lat[index], lng: data.lng[index] },
|
||||
index,
|
||||
});
|
||||
if (data.speed && (global.totalTime ?? 0) > 0) {
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
import { untrack } from 'svelte';
|
||||
import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
|
||||
import { engine } from '$lib/engine';
|
||||
import { hoveredPoint } from '$lib/logic/statistics';
|
||||
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { hoveredPoint, slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
|
||||
let {
|
||||
useHash = true,
|
||||
@@ -39,7 +38,6 @@
|
||||
distanceUnits,
|
||||
velocityUnits,
|
||||
temperatureUnits,
|
||||
fileOrder,
|
||||
distanceMarkers,
|
||||
directionMarkers,
|
||||
} = settings;
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
let manualCamera = $state(false);
|
||||
let zoom = $state('0');
|
||||
let lat = $state('0');
|
||||
let lon = $state('0');
|
||||
let lng = $state('0');
|
||||
let bearing = $state('0');
|
||||
let pitch = $state('0');
|
||||
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '');
|
||||
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lng}/${bearing}/${pitch}` : '');
|
||||
|
||||
$effect(() => {
|
||||
if (options.elevation.show || options.elevation.height) {
|
||||
@@ -78,7 +78,7 @@
|
||||
if ($map) {
|
||||
let center = $map.getCenter();
|
||||
lat = center.lat.toFixed(4);
|
||||
lon = center.lng.toFixed(4);
|
||||
lng = center.lng.toFixed(4);
|
||||
zoom = $map.getZoom().toFixed(2);
|
||||
bearing = $map.getBearing().toFixed(1);
|
||||
pitch = $map.getPitch().toFixed(0);
|
||||
@@ -293,7 +293,7 @@
|
||||
</Label>
|
||||
<Label class="flex flex-col gap-1">
|
||||
<span>{i18n._('embedding.longitude')}</span>
|
||||
<span>{lon}</span>
|
||||
<span>{lng}</span>
|
||||
</Label>
|
||||
<Label class="flex flex-col gap-1">
|
||||
<span>{i18n._('embedding.zoom')}</span>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||
import FileListNode from './FileListNode.svelte';
|
||||
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||
import { ListLevel } from './file-list';
|
||||
import { allowedMoves, dragging, SortableFileList } from './sortable-file-list';
|
||||
import { allowedMoves, dragging, SortableFileList, type ListLevel } from './sortable-file-list';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import type { FileTreeNode } from '$lib/selection-helpers';
|
||||
|
||||
@@ -26,14 +25,14 @@
|
||||
let container: HTMLElement;
|
||||
let sortableLevel: ListLevel =
|
||||
node === null
|
||||
? ListLevel.FILE
|
||||
? 'file'
|
||||
: node.type === 'file'
|
||||
? waypointRoot
|
||||
? ListLevel.WAYPOINTS
|
||||
: ListLevel.TRACK
|
||||
? 'waypoints'
|
||||
: 'track'
|
||||
: node.type === 'waypoints'
|
||||
? ListLevel.WAYPOINT
|
||||
: ListLevel.SEGMENT;
|
||||
? 'waypoint'
|
||||
: 'segment';
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
|
||||
let canDrop = $derived($dragging !== null && allowedMoves[$dragging].includes(sortableLevel));
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
Scissors,
|
||||
FileStack,
|
||||
} from '@lucide/svelte';
|
||||
import { ListLevel } from './file-list';
|
||||
import { getContext } from 'svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
|
||||
@@ -56,14 +55,7 @@
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
let embedding = getContext<boolean>('embedding');
|
||||
|
||||
const levels = {
|
||||
file: ListLevel.FILE,
|
||||
track: ListLevel.TRACK,
|
||||
segment: ListLevel.SEGMENT,
|
||||
waypoints: ListLevel.WAYPOINTS,
|
||||
waypoint: ListLevel.WAYPOINT,
|
||||
};
|
||||
let level = $derived(levels[node.type]);
|
||||
let level = $derived(node.type);
|
||||
|
||||
let singleSelection = $derived(selectionSize($selection) === 1);
|
||||
|
||||
@@ -128,7 +120,7 @@
|
||||
<MetadataDialog bind:open={openEditMetadata} {fileState} {node} />
|
||||
<StyleDialog bind:open={openEditStyle} {fileState} {node} />
|
||||
{/if}
|
||||
{#if level === ListLevel.FILE || level === ListLevel.TRACK}
|
||||
{#if level === 'file' || level === 'track'}
|
||||
<div
|
||||
class="absolute {orientation === 'vertical'
|
||||
? 'top-0 bottom-0 right-0 w-1'
|
||||
@@ -178,9 +170,9 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if level === ListLevel.SEGMENT}
|
||||
{#if level === 'segment'}
|
||||
<Waypoints size="16" class="mx-1 shrink-0" />
|
||||
{:else if level === ListLevel.WAYPOINT}
|
||||
{:else if level === 'waypoint'}
|
||||
{#if symbolKey && symbols[symbolKey].icon}
|
||||
{@const SymbolIcon = symbols[symbolKey].icon}
|
||||
<SymbolIcon size="16" class="mx-1 shrink-0" />
|
||||
@@ -250,7 +242,7 @@
|
||||
<ContextMenu.Separator />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if level !== ListLevel.WAYPOINTS}
|
||||
{#if level !== 'waypoints'}
|
||||
<ContextMenu.Item onclick={() => engine.selectAll()}>
|
||||
<FileStack size="16" />
|
||||
{i18n._('menu.select_all')}
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
export enum ListLevel {
|
||||
ROOT,
|
||||
FILE,
|
||||
TRACK,
|
||||
SEGMENT,
|
||||
WAYPOINTS,
|
||||
WAYPOINT,
|
||||
}
|
||||
|
||||
export abstract class ListItem {
|
||||
[x: string]: any;
|
||||
level: ListLevel;
|
||||
|
||||
constructor(level: ListLevel) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
abstract getId(): string | number;
|
||||
abstract getFullId(): string;
|
||||
abstract getIdAtLevel(level: ListLevel): string | number | undefined;
|
||||
abstract getFileId(): string;
|
||||
abstract getParent(): ListItem;
|
||||
abstract extend(id: string | number): ListItem;
|
||||
}
|
||||
|
||||
export class ListRootItem extends ListItem {
|
||||
constructor() {
|
||||
super(ListLevel.ROOT);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'root';
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return 'root';
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(id: string): ListFileItem {
|
||||
return new ListFileItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListFileItem extends ListItem {
|
||||
fileId: string;
|
||||
|
||||
constructor(fileId: string) {
|
||||
super(ListLevel.FILE);
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListRootItem();
|
||||
}
|
||||
|
||||
extend(id: number | 'waypoints'): ListTrackItem | ListWaypointsItem {
|
||||
if (id === 'waypoints') {
|
||||
return new ListWaypointsItem(this.fileId);
|
||||
} else {
|
||||
return new ListTrackItem(this.fileId, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ListTrackItem extends ListItem {
|
||||
fileId: string;
|
||||
trackIndex: number;
|
||||
|
||||
constructor(fileId: string, trackIndex: number) {
|
||||
super(ListLevel.TRACK);
|
||||
this.fileId = fileId;
|
||||
this.trackIndex = trackIndex;
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return this.trackIndex;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-track-${this.trackIndex}`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return this.trackIndex;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getTrackIndex(): number {
|
||||
return this.trackIndex;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListFileItem(this.fileId);
|
||||
}
|
||||
|
||||
extend(id: number): ListTrackSegmentItem {
|
||||
return new ListTrackSegmentItem(this.fileId, this.trackIndex, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListTrackSegmentItem extends ListItem {
|
||||
fileId: string;
|
||||
trackIndex: number;
|
||||
segmentIndex: number;
|
||||
|
||||
constructor(fileId: string, trackIndex: number, segmentIndex: number) {
|
||||
super(ListLevel.SEGMENT);
|
||||
this.fileId = fileId;
|
||||
this.trackIndex = trackIndex;
|
||||
this.segmentIndex = segmentIndex;
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return this.segmentIndex;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-track-${this.trackIndex}--${this.segmentIndex}`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return this.trackIndex;
|
||||
case ListLevel.TRACK:
|
||||
return this.segmentIndex;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getTrackIndex(): number {
|
||||
return this.trackIndex;
|
||||
}
|
||||
|
||||
getSegmentIndex(): number {
|
||||
return this.segmentIndex;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListTrackItem(this.fileId, this.trackIndex);
|
||||
}
|
||||
|
||||
extend(): ListTrackSegmentItem {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ListWaypointsItem extends ListItem {
|
||||
fileId: string;
|
||||
|
||||
constructor(fileId: string) {
|
||||
super(ListLevel.WAYPOINTS);
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'waypoints';
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-waypoints`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return 'waypoints';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListFileItem(this.fileId);
|
||||
}
|
||||
|
||||
extend(id: number): ListWaypointItem {
|
||||
return new ListWaypointItem(this.fileId, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListWaypointItem extends ListItem {
|
||||
fileId: string;
|
||||
waypointIndex: number;
|
||||
|
||||
constructor(fileId: string, waypointIndex: number) {
|
||||
super(ListLevel.WAYPOINT);
|
||||
this.fileId = fileId;
|
||||
this.waypointIndex = waypointIndex;
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return this.waypointIndex;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-waypoint-${this.waypointIndex}`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return 'waypoints';
|
||||
case ListLevel.WAYPOINTS:
|
||||
return this.waypointIndex;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getWaypointIndex(): number {
|
||||
return this.waypointIndex;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListWaypointsItem(this.fileId);
|
||||
}
|
||||
|
||||
extend(): ListWaypointItem {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function sortItems(items: ListItem[], reverse: boolean = false) {
|
||||
items.sort((a, b) => {
|
||||
if (a instanceof ListTrackItem && b instanceof ListTrackItem) {
|
||||
return a.getTrackIndex() - b.getTrackIndex();
|
||||
} else if (a instanceof ListTrackSegmentItem && b instanceof ListTrackSegmentItem) {
|
||||
return a.getSegmentIndex() - b.getSegmentIndex();
|
||||
} else if (a instanceof ListWaypointItem && b instanceof ListWaypointItem) {
|
||||
return a.getWaypointIndex() - b.getWaypointIndex();
|
||||
}
|
||||
return a.level - b.level;
|
||||
});
|
||||
if (reverse) {
|
||||
items.reverse();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { isMac } from '$lib/utils';
|
||||
import Sortable, { type Direction } from 'sortablejs/Sortable';
|
||||
import { ListLevel } from './file-list';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { tick } from 'svelte';
|
||||
import { engine, type MoveTarget, type Selection } from '$lib/engine';
|
||||
import { isSelected, type FileTreeNode } from '$lib/selection-helpers';
|
||||
|
||||
/** What a list holds: the files, the tracks of a file, the segments of a track, or the waypoints. */
|
||||
export type ListLevel = 'file' | 'track' | 'segment' | 'waypoints' | 'waypoint';
|
||||
|
||||
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
[ListLevel.ROOT]: [],
|
||||
[ListLevel.FILE]: [ListLevel.FILE],
|
||||
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
||||
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
||||
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||
file: ['file'],
|
||||
track: ['file', 'track'],
|
||||
segment: ['file', 'track', 'segment'],
|
||||
waypoints: ['waypoints'],
|
||||
waypoint: ['waypoints', 'waypoint'],
|
||||
};
|
||||
|
||||
export const dragging = writable<ListLevel | null>(null);
|
||||
@@ -84,11 +85,11 @@ export class SortableFileList {
|
||||
childNode(id: string): FileTreeNode | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
return { type: 'file', fileId: id };
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackId: id } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
return parent?.type === 'track'
|
||||
? {
|
||||
type: 'segment',
|
||||
@@ -97,9 +98,9 @@ export class SortableFileList {
|
||||
segmentId: id,
|
||||
}
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
return parent
|
||||
? { type: 'waypoint', fileId: parent.fileId, waypointId: id }
|
||||
: undefined;
|
||||
@@ -110,11 +111,11 @@ export class SortableFileList {
|
||||
private selectionOf(ids: string[]): Selection | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
return { type: 'file', fileIds: ids };
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackIds: ids } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
return parent?.type === 'track'
|
||||
? {
|
||||
type: 'segment',
|
||||
@@ -123,9 +124,9 @@ export class SortableFileList {
|
||||
segmentIds: ids,
|
||||
}
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
return parent
|
||||
? { type: 'waypoint', fileId: parent.fileId, waypointIds: ids }
|
||||
: undefined;
|
||||
@@ -136,20 +137,20 @@ export class SortableFileList {
|
||||
private targetAt(index: number): MoveTarget | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
return { type: 'files', index };
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
return parent ? { type: 'tracks', fileId: parent.fileId, index } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
return parent?.type === 'track'
|
||||
? { type: 'segments', fileId: parent.fileId, trackId: parent.trackId, index }
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
// the list holds the node standing for the waypoints: they go after the others
|
||||
return parent
|
||||
? { type: 'waypoints', fileId: parent.fileId, index: Infinity }
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId, index } : undefined;
|
||||
}
|
||||
}
|
||||
@@ -161,7 +162,7 @@ export class SortableFileList {
|
||||
return;
|
||||
}
|
||||
|
||||
if (from === to && this._sortableLevel === ListLevel.FILE) {
|
||||
if (from === to && this._sortableLevel === 'file') {
|
||||
// the order of the files is the one of the list
|
||||
this.updateToFileOrder();
|
||||
return;
|
||||
@@ -169,7 +170,7 @@ export class SortableFileList {
|
||||
|
||||
// The event is triggered on the source and on the destination list: handle it once.
|
||||
// Nothing to move in the list of the single node of the waypoints.
|
||||
if (to !== this || (from === to && this._sortableLevel === ListLevel.WAYPOINTS)) {
|
||||
if (to !== this || (from === to && this._sortableLevel === 'waypoints')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,19 +246,19 @@ export class SortableFileList {
|
||||
engine.select([]);
|
||||
} else {
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
engine.select(selected);
|
||||
break;
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
engine.selectTracks(parent!.fileId, selected);
|
||||
break;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
engine.selectSegments(parent!.fileId, (parent as any).trackId, selected);
|
||||
break;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
engine.selectWaypointGroup(parent!.fileId);
|
||||
break;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
engine.selectWaypoints(parent!.fileId, selected);
|
||||
break;
|
||||
}
|
||||
@@ -266,7 +267,7 @@ export class SortableFileList {
|
||||
}
|
||||
|
||||
updateFromFileOrder() {
|
||||
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
|
||||
if (!this._sortable || this._sortableLevel !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -282,7 +283,7 @@ export class SortableFileList {
|
||||
}
|
||||
|
||||
updateToFileOrder() {
|
||||
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
|
||||
if (!this._sortable || this._sortableLevel !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,7 +305,7 @@ export class SortableFileList {
|
||||
if (element instanceof HTMLElement) {
|
||||
let attr = element.getAttribute('data-id');
|
||||
if (attr) {
|
||||
if (this._sortableLevel === ListLevel.FILE && !files.has(attr)) {
|
||||
if (this._sortableLevel === 'file' && !files.has(attr)) {
|
||||
element.remove();
|
||||
} else {
|
||||
this._elements[attr] = element;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { get, type Readable } from 'svelte/store';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import maplibregl, {
|
||||
type GeoJSONSource,
|
||||
type FilterSpecification,
|
||||
@@ -13,7 +12,6 @@ import { getElevation, loadSVGIcon } from '$lib/utils';
|
||||
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
||||
import { MapPin, Square } from 'lucide-static';
|
||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { engine, type FileState, type Selection } from '$lib/engine';
|
||||
import { isCovered, hasSelectionWithin, type FileTreeNode } from '$lib/selection-helpers';
|
||||
import { isSegmentHidden, isWaypointHidden } from '$lib/file-visibility';
|
||||
@@ -615,7 +613,7 @@ export class GPXLayer {
|
||||
getElevation([
|
||||
{
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng,
|
||||
lng: e.lngLat.lng,
|
||||
},
|
||||
]).then(async (ele) => {
|
||||
if (this.draggedWaypointIndex === null) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { hoveredPoint } from '$lib/logic/statistics';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { get } from 'svelte/store';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { allHidden } from '$lib/all-hidden';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import { loadSVGIcon } from '$lib/utils';
|
||||
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { hoveredPoint, slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { engine } from '$lib/engine';
|
||||
|
||||
const startMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -93,7 +92,7 @@ export class StartEndMarkers {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [hovered.lon, hovered.lat],
|
||||
coordinates: [hovered.lng, hovered.lat],
|
||||
},
|
||||
properties: {
|
||||
icon: 'hover-marker',
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
const desc = Object.entries(tags)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n');
|
||||
const [ele] = await getElevation([{ lat: poi.item.lat, lon: poi.item.lng }]);
|
||||
const [ele] = await getElevation([{ lat: poi.item.lat, lng: poi.item.lng }]);
|
||||
await engine.newWaypoint({
|
||||
lng: poi.item.lng,
|
||||
lat: poi.item.lat,
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
import { SphericalMercator } from '@mapbox/sphericalmercator';
|
||||
import { getLayers } from './utils';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { overpassQueryData } from '$lib/assets/layers';
|
||||
import { MapPopup } from '$lib/components/map/map-popup';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { db } from '$lib/db';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||
import { loadSVGIcon } from '$lib/utils';
|
||||
import { cachedFetch } from '$lib/cached-fetch';
|
||||
|
||||
const { currentOverpassQueries } = settings;
|
||||
|
||||
/** The responses of Overpass are kept in the browser for a week. */
|
||||
const CACHE = { name: 'overpass', maxAge: 7 * 24 * 3600 * 1000 };
|
||||
|
||||
const mercator = new SphericalMercator({
|
||||
size: 256,
|
||||
});
|
||||
|
||||
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
||||
|
||||
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
||||
data.set({ type: 'FeatureCollection', features: pois.map((poi) => poi.poi) });
|
||||
});
|
||||
|
||||
export class OverpassLayer {
|
||||
overpassUrl = 'https://overpass.gpx.studio/api/interpreter';
|
||||
minZoom = 12;
|
||||
queryZoom = 12;
|
||||
expirationTime = 7 * 24 * 3600 * 1000;
|
||||
map: maplibregl.Map;
|
||||
layerEventManager: MapLayerEventManager;
|
||||
popup: MapPopup;
|
||||
|
||||
data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
||||
|
||||
// What was queried this session: the queries that were made for each tile. The responses are
|
||||
// kept longer, see `cachedFetch`.
|
||||
queriedTiles = new Map<string, Set<string>>();
|
||||
currentQueries: Set<string> = new Set();
|
||||
nextQueries: Map<string, { x: number; y: number; queries: string[] }> = new Map();
|
||||
|
||||
unsubscribes: (() => void)[] = [];
|
||||
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
||||
@@ -54,7 +53,7 @@ export class OverpassLayer {
|
||||
add() {
|
||||
this.map.on('moveend', this.queryIfNeededBinded);
|
||||
this.map.on('style.load', this.updateBinded);
|
||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(this.data.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(
|
||||
currentOverpassQueries.subscribe(() => {
|
||||
this.updateBinded();
|
||||
@@ -77,7 +76,7 @@ export class OverpassLayer {
|
||||
update() {
|
||||
this.loadIcons();
|
||||
|
||||
const fullData = get(data);
|
||||
const fullData = get(this.data);
|
||||
const queries = getCurrentQueries();
|
||||
const d: GeoJSON.FeatureCollection = {
|
||||
type: 'FeatureCollection',
|
||||
@@ -158,7 +157,6 @@ export class OverpassLayer {
|
||||
}
|
||||
|
||||
let tileLimits = mercator.xyz(bbox, this.queryZoom);
|
||||
let time = Date.now();
|
||||
|
||||
for (let x = tileLimits.minX; x <= tileLimits.maxX; x++) {
|
||||
for (let y = tileLimits.minY; y <= tileLimits.maxY; y++) {
|
||||
@@ -166,66 +164,35 @@ export class OverpassLayer {
|
||||
continue;
|
||||
}
|
||||
|
||||
db.overpasstiles
|
||||
.where('[x+y]')
|
||||
.equals([x, y])
|
||||
.toArray()
|
||||
.then((querytiles) => {
|
||||
let missingQueries = queries.filter(
|
||||
(query) =>
|
||||
!querytiles.some(
|
||||
(querytile) =>
|
||||
querytile.query === query &&
|
||||
time - querytile.time < this.expirationTime
|
||||
)
|
||||
);
|
||||
if (missingQueries.length > 0) {
|
||||
this.queryTile(x, y, missingQueries);
|
||||
}
|
||||
});
|
||||
const queried = this.queriedTiles.get(`${x},${y}`);
|
||||
const missingQueries = queries.filter((query) => !queried?.has(query));
|
||||
if (missingQueries.length > 0) {
|
||||
this.queryTile(x, y, missingQueries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryTile(x: number, y: number, queries: string[]) {
|
||||
if (this.currentQueries.size > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentQueries.add(`${x},${y}`);
|
||||
|
||||
const bounds = mercator.bbox(x, y, this.queryZoom);
|
||||
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
this.currentQueries.delete(`${x},${y}`);
|
||||
return Promise.reject();
|
||||
},
|
||||
() => this.currentQueries.delete(`${x},${y}`)
|
||||
)
|
||||
cachedFetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`, CACHE)
|
||||
.then((response) => (response.ok ? response.json() : Promise.reject()))
|
||||
.then((data) => this.storeOverpassData(x, y, queries, data))
|
||||
.catch(() => this.currentQueries.delete(`${x},${y}`));
|
||||
}
|
||||
|
||||
storeOverpassData(x: number, y: number, queries: string[], data: any) {
|
||||
let time = Date.now();
|
||||
let queryTiles = queries.map((query) => ({ x, y, query, time }));
|
||||
let pois: { query: string; id: number; poi: GeoJSON.Feature }[] = [];
|
||||
|
||||
if (data.elements === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let element of data.elements) {
|
||||
for (let query of queries) {
|
||||
if (belongsToQuery(element, query)) {
|
||||
pois.push({
|
||||
query,
|
||||
id: element.id,
|
||||
poi: {
|
||||
this.data.update((updatedData) => {
|
||||
for (let element of data.elements) {
|
||||
for (let query of queries) {
|
||||
if (belongsToQuery(element, query)) {
|
||||
updatedData.features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
@@ -242,17 +209,17 @@ export class OverpassLayer {
|
||||
tags: element.tags,
|
||||
type: element.type,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.transaction('rw', db.overpasstiles, db.overpassdata, async () => {
|
||||
await db.overpasstiles.bulkPut(queryTiles);
|
||||
await db.overpassdata.bulkPut(pois);
|
||||
return updatedData;
|
||||
});
|
||||
|
||||
const queried = this.queriedTiles.get(`${x},${y}`) ?? new Set<string>();
|
||||
queries.forEach((query) => queried.add(query));
|
||||
this.queriedTiles.set(`${x},${y}`, queried);
|
||||
|
||||
this.currentQueries.delete(`${x},${y}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { engine } from '$lib/engine';
|
||||
import { get } from 'svelte/store';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
|
||||
type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
|
||||
@@ -246,17 +247,23 @@ export class MapLayerEventManager {
|
||||
layerIds: string[],
|
||||
bounds: maplibregl.LngLatBounds
|
||||
): string[] {
|
||||
const files = get(engine.files);
|
||||
let result = layerIds.filter((layerId) => {
|
||||
if (!this._map.getLayer(layerId)) return false;
|
||||
const fileId = layerId.replace('-waypoints', '');
|
||||
if (fileId === layerId) {
|
||||
return fileStateCollection.getStatistics(fileId)?.intersectsBBox(bounds) ?? true;
|
||||
} else {
|
||||
return (
|
||||
fileStateCollection.getStatistics(fileId)?.intersectsWaypointBBox(bounds) ??
|
||||
true
|
||||
);
|
||||
// the tracks of a file are all within its bounds
|
||||
const file = files.get(fileId);
|
||||
const box = file && get(file).statistics.bounds;
|
||||
return box
|
||||
? bounds.getWest() <= box.east &&
|
||||
bounds.getEast() >= box.west &&
|
||||
bounds.getSouth() <= box.north &&
|
||||
bounds.getNorth() >= box.south
|
||||
: true;
|
||||
}
|
||||
// the waypoints have no bounds of their own
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
async function addElevation() {
|
||||
const stats = get(statistics);
|
||||
const points = Array.from(stats.lng, (lon, i) => ({ lon, lat: stats.lat[i] }));
|
||||
const points = Array.from(stats.lng, (lng, i) => ({ lng, lat: stats.lat[i] }));
|
||||
const ele = await getElevation(points);
|
||||
|
||||
// the selection or the files may have changed while the elevation was loading
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { distance, type Coordinates } from 'gpx';
|
||||
import { distance, type Coordinates } from '$lib/geo';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import maplibregl, {
|
||||
type MapMouseEvent,
|
||||
@@ -50,17 +50,11 @@ type Anchor = {
|
||||
type AnchorProperties = { anchorIndex: number; minZoom: number };
|
||||
type AnchorFeature = GeoJSON.Feature<GeoJSON.Point, AnchorProperties>;
|
||||
|
||||
type Position = { lng: number; lat: number };
|
||||
|
||||
/** Whether the tool has to show controls for a selection: it covers segments, or can get some. */
|
||||
function isRoutable(selection: Selection) {
|
||||
return selection.type === 'file' || selection.type === 'track' || selection.type === 'segment';
|
||||
}
|
||||
|
||||
function toCoordinates({ lng, lat }: Position): Coordinates {
|
||||
return { lat, lon: lng };
|
||||
}
|
||||
|
||||
/**
|
||||
* The anchors of the selection of the engine, shown on the map where the user can drag them to
|
||||
* reroute the segments, or click on the segments to add some. Every interaction is turned into a
|
||||
@@ -103,7 +97,7 @@ export class RoutingControls {
|
||||
* Where the pointer hovers a segment of the selection: an anchor that can be dragged or
|
||||
* clicked to be added.
|
||||
*/
|
||||
temporaryAnchor: (Position & { segment: number }) | null = null;
|
||||
temporaryAnchor: (Coordinates & { segment: number }) | null = null;
|
||||
showTemporaryAnchorBinded: (e: MapLayerMouseEvent) => void =
|
||||
this.showTemporaryAnchor.bind(this);
|
||||
updateTemporaryAnchorBinded: (e: MapMouseEvent) => void = this.updateTemporaryAnchor.bind(this);
|
||||
@@ -328,11 +322,11 @@ export class RoutingControls {
|
||||
this.popup.remove();
|
||||
}
|
||||
|
||||
position(index: number): Position {
|
||||
position(index: number): Coordinates {
|
||||
return { lng: this.statistics!.lng[index], lat: this.statistics!.lat[index] };
|
||||
}
|
||||
|
||||
async moveAnchor(anchor: Anchor, coordinates: Position) {
|
||||
async moveAnchor(anchor: Anchor, coordinates: Coordinates) {
|
||||
// Move the anchor and update the route from and to the neighbouring anchors
|
||||
const initialAnchor = anchor;
|
||||
const initialCoordinates = { lng: anchor.lng, lat: anchor.lat };
|
||||
@@ -349,7 +343,7 @@ export class RoutingControls {
|
||||
const [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
|
||||
|
||||
const chain: Anchor[] = [];
|
||||
const targets: Position[] = [];
|
||||
const targets: Coordinates[] = [];
|
||||
if (previousAnchor) {
|
||||
chain.push(previousAnchor);
|
||||
targets.push(this.position(previousAnchor.index));
|
||||
@@ -457,7 +451,7 @@ export class RoutingControls {
|
||||
const first = this.position(start);
|
||||
const last = this.position(end - 1);
|
||||
// the end of the segment has to be close to its start, up to a kilometer
|
||||
return distance(toCoordinates(first), toCoordinates(last)) <= 1000;
|
||||
return distance(first, last) <= 1000;
|
||||
}
|
||||
|
||||
async appendAnchor(e: maplibregl.MapMouseEvent) {
|
||||
@@ -480,7 +474,7 @@ export class RoutingControls {
|
||||
this.appendAnchorWithCoordinates({ lng: e.lngLat.lng, lat: e.lngLat.lat });
|
||||
}
|
||||
|
||||
async appendAnchorWithCoordinates(coordinates: Position) {
|
||||
async appendAnchorWithCoordinates(coordinates: Coordinates) {
|
||||
// Add a new anchor to the end of the last segment
|
||||
const statistics = this.statistics;
|
||||
const revision = this.anchorData?.revision;
|
||||
@@ -546,7 +540,7 @@ export class RoutingControls {
|
||||
* single segment) or where they were moved to, and replaces what is between the first and
|
||||
* the last anchor by the route.
|
||||
*/
|
||||
async routeBetweenAnchors(anchors: Anchor[], targets: Position[]): Promise<boolean> {
|
||||
async routeBetweenAnchors(anchors: Anchor[], targets: Coordinates[]): Promise<boolean> {
|
||||
const revision = this.anchorData?.revision;
|
||||
if (revision === undefined) {
|
||||
return false;
|
||||
@@ -565,7 +559,7 @@ export class RoutingControls {
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await route(targets.map(toCoordinates));
|
||||
response = await route(targets);
|
||||
} catch (e: any) {
|
||||
toast.error(i18n._(e.message, e.message));
|
||||
return false;
|
||||
@@ -581,9 +575,9 @@ export class RoutingControls {
|
||||
const keepFirst =
|
||||
first.index !== segmentStart &&
|
||||
(first.index !== lastIndex ||
|
||||
distance(toCoordinates(targets[0]), {
|
||||
distance(targets[0], {
|
||||
lat: response.lat[0],
|
||||
lon: response.lng[0],
|
||||
lng: response.lng[0],
|
||||
}) > 1);
|
||||
const keepLast = last.index !== lastIndex;
|
||||
|
||||
@@ -843,7 +837,7 @@ export class RoutingControls {
|
||||
return false;
|
||||
}
|
||||
|
||||
moveAnchorFeature(anchor: Anchor, coordinates: Position) {
|
||||
moveAnchorFeature(anchor: Anchor, coordinates: Coordinates) {
|
||||
const source = get(map)?.getSource(
|
||||
this.layers.get(anchor.id === this.anchors.length ? 0 : anchor.zoom)?.id ?? ''
|
||||
) as GeoJSONSource | undefined;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { distance, type Coordinates } from 'gpx';
|
||||
import { distance, type Coordinates } from '$lib/geo';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { getElevation } from '$lib/utils';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -143,7 +143,7 @@ async function getGraphHopperRoute(
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
points: points.map((point) => [point.lon, point.lat]),
|
||||
points: points.map((point) => [point.lng, point.lat]),
|
||||
profile: graphHopperProfile,
|
||||
elevation: true,
|
||||
points_encoded: false,
|
||||
@@ -209,7 +209,7 @@ async function getBRouterRoute(
|
||||
points: Coordinates[],
|
||||
brouterProfile: string
|
||||
): Promise<RoutedPoints> {
|
||||
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
|
||||
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lng.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
|
||||
|
||||
let response = await fetch(url);
|
||||
|
||||
@@ -283,15 +283,15 @@ function getIntermediatePoints(points: Coordinates[]): Promise<RoutedPoints> {
|
||||
const dist = distance(points[i], points[i + 1]) / 1000;
|
||||
for (let d = 0; d < dist; d += step) {
|
||||
const lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat);
|
||||
const lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon);
|
||||
addPoint(route, lon, lat, 0);
|
||||
const lng = points[i].lng + (d / dist) * (points[i + 1].lng - points[i].lng);
|
||||
addPoint(route, lng, lat, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const last = points[points.length - 1];
|
||||
addPoint(route, last.lon, last.lat, 0);
|
||||
addPoint(route, last.lng, last.lat, 0);
|
||||
|
||||
return getElevation(route.lng.map((lng, i) => ({ lon: lng, lat: route.lat[i] }))).then(
|
||||
return getElevation(route.lng.map((lng, i) => ({ lng, lat: route.lat[i] }))).then(
|
||||
(elevations) => {
|
||||
route.ele = elevations;
|
||||
return route;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
|
||||
|
||||
const earthRadius = 6371008.8;
|
||||
|
||||
export const MIN_ANCHOR_ZOOM = 0;
|
||||
export const MAX_ANCHOR_ZOOM = 22;
|
||||
|
||||
export function getZoomLevelForDistance(latitude: number, distance?: number): number {
|
||||
if (distance === undefined) {
|
||||
return MIN_ANCHOR_ZOOM;
|
||||
}
|
||||
|
||||
const rad = Math.PI / 180;
|
||||
const lat = latitude * rad;
|
||||
|
||||
return Math.min(
|
||||
MAX_ANCHOR_ZOOM,
|
||||
Math.max(MIN_ANCHOR_ZOOM, Math.round(Math.log2((earthRadius * Math.cos(lat)) / distance)))
|
||||
);
|
||||
}
|
||||
|
||||
export function updateAnchorPoints(file: GPXFile) {
|
||||
let segments = file.getSegments();
|
||||
|
||||
for (let segment of segments) {
|
||||
if (!segment._data.anchors) {
|
||||
// New segment, compute anchor points for it
|
||||
computeAnchorPoints(segment);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (segment.trkpt.length > 0) {
|
||||
// Ensure first and last points are anchors and always visible
|
||||
segment.trkpt[0]._data.anchor = true;
|
||||
segment.trkpt[0]._data.zoom = 0;
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.anchor = true;
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.zoom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function computeAnchorPoints(segment: TrackSegment) {
|
||||
let points = segment.trkpt;
|
||||
let anchors = ramerDouglasPeucker(points, 1);
|
||||
anchors.forEach((anchor) => {
|
||||
let point = anchor.point;
|
||||
point._data.anchor = true;
|
||||
point._data.zoom = getZoomLevelForDistance(point.getLatitude(), anchor.distance);
|
||||
});
|
||||
segment._data.anchors = true;
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
longitude = parseFloat(longitude.toFixed(6));
|
||||
|
||||
const target = $selectedWaypoint;
|
||||
const [ele] = await getElevation([{ lat: latitude, lon: longitude }]);
|
||||
const [ele] = await getElevation([{ lat: latitude, lng: longitude }]);
|
||||
const waypoint = {
|
||||
lng: longitude,
|
||||
lat: latitude,
|
||||
|
||||
Reference in new issue
Block a user