mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 15:43:25 +00:00
fix urls
This commit is contained in:
@@ -11,9 +11,9 @@
|
|||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { Trash2 } from 'lucide-svelte';
|
import { Trash2 } from 'lucide-svelte';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
{$_('toolbar.clean.button')}
|
{$_('toolbar.clean.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link="./help/toolbar/clean">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/clean')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.clean.help')}
|
{$_('toolbar.clean.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -1,34 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { MountainSnow } from 'lucide-svelte';
|
import { MountainSnow } from 'lucide-svelte';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
$: validSelection = $selection.size > 0;
|
$: validSelection = $selection.size > 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="whitespace-normal h-fit"
|
class="whitespace-normal h-fit"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
dbUtils.addElevationToSelection($map);
|
dbUtils.addElevationToSelection($map);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MountainSnow size="16" class="mr-1 shrink-0" />
|
<MountainSnow size="16" class="mr-1 shrink-0" />
|
||||||
{$_('toolbar.elevation.button')}
|
{$_('toolbar.elevation.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link="./help/toolbar/elevation">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/elevation')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.elevation.help')}
|
{$_('toolbar.elevation.help')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.elevation.help_no_selection')}
|
{$_('toolbar.elevation.help_no_selection')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -11,7 +11,8 @@
|
|||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { dbUtils, getFile } from '$lib/db';
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
$: validSelection =
|
$: validSelection =
|
||||||
$selection.size > 0 &&
|
$selection.size > 0 &&
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
<Ungroup size="16" class="mr-1" />
|
<Ungroup size="16" class="mr-1" />
|
||||||
{$_('toolbar.extract.button')}
|
{$_('toolbar.extract.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link="./help/toolbar/extract">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/extract')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.extract.help')}
|
{$_('toolbar.extract.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -1,89 +1,90 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
enum MergeType {
|
enum MergeType {
|
||||||
TRACES = 'traces',
|
TRACES = 'traces',
|
||||||
CONTENTS = 'contents'
|
CONTENTS = 'contents'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
|
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { dbUtils, getFile } from '$lib/db';
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { Group } from 'lucide-svelte';
|
import { Group } from 'lucide-svelte';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
let canMergeTraces = false;
|
let canMergeTraces = false;
|
||||||
let canMergeContents = false;
|
let canMergeContents = false;
|
||||||
|
|
||||||
$: if ($selection.size > 1) {
|
$: if ($selection.size > 1) {
|
||||||
canMergeTraces = true;
|
canMergeTraces = true;
|
||||||
} else if ($selection.size === 1) {
|
} else if ($selection.size === 1) {
|
||||||
let selected = $selection.getSelected()[0];
|
let selected = $selection.getSelected()[0];
|
||||||
if (selected instanceof ListFileItem) {
|
if (selected instanceof ListFileItem) {
|
||||||
let file = getFile(selected.getFileId());
|
let file = getFile(selected.getFileId());
|
||||||
if (file) {
|
if (file) {
|
||||||
canMergeTraces = file.getSegments().length > 1;
|
canMergeTraces = file.getSegments().length > 1;
|
||||||
} else {
|
} else {
|
||||||
canMergeTraces = false;
|
canMergeTraces = false;
|
||||||
}
|
}
|
||||||
} else if (selected instanceof ListTrackItem) {
|
} else if (selected instanceof ListTrackItem) {
|
||||||
let trackIndex = selected.getTrackIndex();
|
let trackIndex = selected.getTrackIndex();
|
||||||
let file = getFile(selected.getFileId());
|
let file = getFile(selected.getFileId());
|
||||||
if (file && trackIndex < file.trk.length) {
|
if (file && trackIndex < file.trk.length) {
|
||||||
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
|
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
|
||||||
} else {
|
} else {
|
||||||
canMergeTraces = false;
|
canMergeTraces = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
canMergeContents = false;
|
canMergeContents = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: canMergeContents =
|
$: canMergeContents =
|
||||||
$selection.size > 1 &&
|
$selection.size > 1 &&
|
||||||
$selection
|
$selection
|
||||||
.getSelected()
|
.getSelected()
|
||||||
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
|
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
|
||||||
|
|
||||||
let mergeType = MergeType.TRACES;
|
let mergeType = MergeType.TRACES;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||||
<RadioGroup.Root bind:value={mergeType}>
|
<RadioGroup.Root bind:value={mergeType}>
|
||||||
<Label class="flex flex-row items-center gap-2 leading-5">
|
<Label class="flex flex-row items-center gap-2 leading-5">
|
||||||
<RadioGroup.Item value={MergeType.TRACES} />
|
<RadioGroup.Item value={MergeType.TRACES} />
|
||||||
{$_('toolbar.merge.merge_traces')}
|
{$_('toolbar.merge.merge_traces')}
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row items-center gap-2 leading-5">
|
<Label class="flex flex-row items-center gap-2 leading-5">
|
||||||
<RadioGroup.Item value={MergeType.CONTENTS} />
|
<RadioGroup.Item value={MergeType.CONTENTS} />
|
||||||
{$_('toolbar.merge.merge_contents')}
|
{$_('toolbar.merge.merge_contents')}
|
||||||
</Label>
|
</Label>
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="whitespace-normal h-fit"
|
class="whitespace-normal h-fit"
|
||||||
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
||||||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dbUtils.mergeSelection(mergeType === MergeType.TRACES);
|
dbUtils.mergeSelection(mergeType === MergeType.TRACES);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group size="16" class="mr-1 shrink-0" />
|
<Group size="16" class="mr-1 shrink-0" />
|
||||||
{$_('toolbar.merge.merge_selection')}
|
{$_('toolbar.merge.merge_selection')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link="./help/toolbar/merge">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/merge')}>
|
||||||
{#if mergeType === MergeType.TRACES && canMergeTraces}
|
{#if mergeType === MergeType.TRACES && canMergeTraces}
|
||||||
{$_('toolbar.merge.help_merge_traces')}
|
{$_('toolbar.merge.help_merge_traces')}
|
||||||
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
||||||
{$_('toolbar.merge.help_cannot_merge_traces')}
|
{$_('toolbar.merge.help_cannot_merge_traces')}
|
||||||
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
||||||
{$_('toolbar.merge.help_merge_contents')}
|
{$_('toolbar.merge.help_merge_contents')}
|
||||||
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
||||||
{$_('toolbar.merge.help_cannot_merge_contents')}
|
{$_('toolbar.merge.help_cannot_merge_contents')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,183 +1,175 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import {
|
import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList';
|
||||||
ListItem,
|
import Help from '$lib/components/Help.svelte';
|
||||||
ListRootItem,
|
import { Filter } from 'lucide-svelte';
|
||||||
ListTrackSegmentItem
|
import { _, locale } from 'svelte-i18n';
|
||||||
} from '$lib/components/file-list/FileList';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import { dbUtils, fileObservers } from '$lib/db';
|
||||||
import { Filter } from 'lucide-svelte';
|
import { map } from '$lib/stores';
|
||||||
import { _ } from 'svelte-i18n';
|
import { onDestroy } from 'svelte';
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||||
import { dbUtils, fileObservers } from '$lib/db';
|
import { derived } from 'svelte/store';
|
||||||
import { map } from '$lib/stores';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
|
||||||
import { derived } from 'svelte/store';
|
|
||||||
|
|
||||||
let sliderValue = [50];
|
let sliderValue = [50];
|
||||||
let maxPoints = 0;
|
let maxPoints = 0;
|
||||||
let currentPoints = 0;
|
let currentPoints = 0;
|
||||||
|
|
||||||
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||||
|
|
||||||
$: tolerance = 2 ** (sliderValue[0] / (100 / Math.log2(10000)));
|
$: tolerance = 2 ** (sliderValue[0] / (100 / Math.log2(10000)));
|
||||||
|
|
||||||
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
|
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
|
||||||
let unsubscribes = new Map<string, () => void>();
|
let unsubscribes = new Map<string, () => void>();
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
maxPoints = 0;
|
maxPoints = 0;
|
||||||
currentPoints = 0;
|
currentPoints = 0;
|
||||||
|
|
||||||
let data: GeoJSON.FeatureCollection = {
|
let data: GeoJSON.FeatureCollection = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: []
|
features: []
|
||||||
};
|
};
|
||||||
|
|
||||||
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
||||||
maxPoints += maxPts;
|
maxPoints += maxPts;
|
||||||
|
|
||||||
let current = points.filter(
|
let current = points.filter(
|
||||||
(point) => point.distance === undefined || point.distance >= tolerance
|
(point) => point.distance === undefined || point.distance >= tolerance
|
||||||
);
|
);
|
||||||
currentPoints += current.length;
|
currentPoints += current.length;
|
||||||
|
|
||||||
data.features.push({
|
data.features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'LineString',
|
type: 'LineString',
|
||||||
coordinates: current.map((point) => [
|
coordinates: current.map((point) => [
|
||||||
point.point.getLongitude(),
|
point.point.getLongitude(),
|
||||||
point.point.getLatitude()
|
point.point.getLatitude()
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
properties: {}
|
properties: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($map) {
|
if ($map) {
|
||||||
let source = $map.getSource('simplified');
|
let source = $map.getSource('simplified');
|
||||||
if (source) {
|
if (source) {
|
||||||
source.setData(data);
|
source.setData(data);
|
||||||
} else {
|
} else {
|
||||||
$map.addSource('simplified', {
|
$map.addSource('simplified', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!$map.getLayer('simplified')) {
|
if (!$map.getLayer('simplified')) {
|
||||||
$map.addLayer({
|
$map.addLayer({
|
||||||
id: 'simplified',
|
id: 'simplified',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'simplified',
|
source: 'simplified',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': 'white',
|
'line-color': 'white',
|
||||||
'line-width': 3
|
'line-width': 3
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$map.moveLayer('simplified');
|
$map.moveLayer('simplified');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($fileObservers) {
|
$: if ($fileObservers) {
|
||||||
unsubscribes.forEach((unsubscribe, fileId) => {
|
unsubscribes.forEach((unsubscribe, fileId) => {
|
||||||
if (!$fileObservers.has(fileId)) {
|
if (!$fileObservers.has(fileId)) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
unsubscribes.delete(fileId);
|
unsubscribes.delete(fileId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$fileObservers.forEach((fileStore, fileId) => {
|
$fileObservers.forEach((fileStore, fileId) => {
|
||||||
if (!unsubscribes.has(fileId)) {
|
if (!unsubscribes.has(fileId)) {
|
||||||
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
|
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [fs, sel]).subscribe(
|
||||||
fs,
|
([fs, sel]) => {
|
||||||
sel
|
if (fs) {
|
||||||
]).subscribe(([fs, sel]) => {
|
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (fs) {
|
let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex);
|
||||||
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
if (sel.hasAnyParent(segmentItem)) {
|
||||||
let segmentItem = new ListTrackSegmentItem(
|
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
||||||
fileId,
|
simplified.set(segmentItem.getFullId(), [
|
||||||
trackIndex,
|
segmentItem,
|
||||||
segmentIndex
|
statistics.local.points.length,
|
||||||
);
|
ramerDouglasPeucker(statistics.local.points, 1)
|
||||||
if (sel.hasAnyParent(segmentItem)) {
|
]);
|
||||||
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
update();
|
||||||
simplified.set(segmentItem.getFullId(), [
|
} else if (simplified.has(segmentItem.getFullId())) {
|
||||||
segmentItem,
|
simplified.delete(segmentItem.getFullId());
|
||||||
statistics.local.points.length,
|
update();
|
||||||
ramerDouglasPeucker(statistics.local.points, 1)
|
}
|
||||||
]);
|
});
|
||||||
update();
|
}
|
||||||
} else if (simplified.has(segmentItem.getFullId())) {
|
}
|
||||||
simplified.delete(segmentItem.getFullId());
|
);
|
||||||
update();
|
unsubscribes.set(fileId, unsubscribe);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
unsubscribes.set(fileId, unsubscribe);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (tolerance) {
|
$: if (tolerance) {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
if ($map.getLayer('simplified')) {
|
if ($map.getLayer('simplified')) {
|
||||||
$map.removeLayer('simplified');
|
$map.removeLayer('simplified');
|
||||||
}
|
}
|
||||||
if ($map.getSource('simplified')) {
|
if ($map.getSource('simplified')) {
|
||||||
$map.removeSource('simplified');
|
$map.removeSource('simplified');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
simplified.clear();
|
simplified.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
function reduce() {
|
function reduce() {
|
||||||
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
|
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
|
||||||
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
||||||
itemsAndPoints.set(
|
itemsAndPoints.set(
|
||||||
item,
|
item,
|
||||||
points
|
points
|
||||||
.filter((point) => point.distance === undefined || point.distance >= tolerance)
|
.filter((point) => point.distance === undefined || point.distance >= tolerance)
|
||||||
.map((point) => point.point)
|
.map((point) => point.point)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
dbUtils.reduce(itemsAndPoints);
|
dbUtils.reduce(itemsAndPoints);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<Slider bind:value={sliderValue} min={0} max={100} step={1} />
|
<Slider bind:value={sliderValue} min={0} max={100} step={1} />
|
||||||
</div>
|
</div>
|
||||||
<Label class="flex flex-row justify-between">
|
<Label class="flex flex-row justify-between">
|
||||||
<span>{$_('toolbar.reduce.tolerance')}</span>
|
<span>{$_('toolbar.reduce.tolerance')}</span>
|
||||||
<WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" />
|
<WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" />
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row justify-between">
|
<Label class="flex flex-row justify-between">
|
||||||
<span>{$_('toolbar.reduce.number_of_points')}</span>
|
<span>{$_('toolbar.reduce.number_of_points')}</span>
|
||||||
<span class="font-normal">{currentPoints}/{maxPoints}</span>
|
<span class="font-normal">{currentPoints}/{maxPoints}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Button variant="outline" disabled={!validSelection} on:click={reduce}>
|
<Button variant="outline" disabled={!validSelection} on:click={reduce}>
|
||||||
<Filter size="16" class="mr-1" />
|
<Filter size="16" class="mr-1" />
|
||||||
{$_('toolbar.reduce.button')}
|
{$_('toolbar.reduce.button')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Help link="./help/toolbar/minify">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/minify')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.reduce.help')}
|
{$_('toolbar.reduce.help')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.reduce.help_no_selection')}
|
{$_('toolbar.reduce.help_no_selection')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,403 +1,393 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DatePicker from '$lib/components/ui/date-picker/DatePicker.svelte';
|
import DatePicker from '$lib/components/ui/date-picker/DatePicker.svelte';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
|
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
|
||||||
import { dbUtils, settings } from '$lib/db';
|
import { dbUtils, settings } from '$lib/db';
|
||||||
import { gpxStatistics } from '$lib/stores';
|
import { gpxStatistics } from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
distancePerHourToSecondsPerDistance,
|
distancePerHourToSecondsPerDistance,
|
||||||
getConvertedVelocity,
|
getConvertedVelocity,
|
||||||
milesToKilometers,
|
milesToKilometers,
|
||||||
nauticalMilesToKilometers
|
nauticalMilesToKilometers
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
import { CalendarDate, type DateValue } from '@internationalized/date';
|
import { CalendarDate, type DateValue } from '@internationalized/date';
|
||||||
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListRootItem,
|
ListRootItem,
|
||||||
ListTrackItem,
|
ListTrackItem,
|
||||||
ListTrackSegmentItem
|
ListTrackSegmentItem
|
||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
let startDate: DateValue | undefined = undefined;
|
let startDate: DateValue | undefined = undefined;
|
||||||
let startTime: string | undefined = undefined;
|
let startTime: string | undefined = undefined;
|
||||||
let endDate: DateValue | undefined = undefined;
|
let endDate: DateValue | undefined = undefined;
|
||||||
let endTime: string | undefined = undefined;
|
let endTime: string | undefined = undefined;
|
||||||
let movingTime: number | undefined = undefined;
|
let movingTime: number | undefined = undefined;
|
||||||
let speed: number | undefined = undefined;
|
let speed: number | undefined = undefined;
|
||||||
let artificial = false;
|
let artificial = false;
|
||||||
|
|
||||||
function toCalendarDate(date: Date): CalendarDate {
|
function toCalendarDate(date: Date): CalendarDate {
|
||||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTimeString(date: Date): string {
|
function toTimeString(date: Date): string {
|
||||||
return date.toTimeString().split(' ')[0];
|
return date.toTimeString().split(' ')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { velocityUnits, distanceUnits } = settings;
|
const { velocityUnits, distanceUnits } = settings;
|
||||||
|
|
||||||
function setSpeed(value: number) {
|
function setSpeed(value: number) {
|
||||||
let speedValue = getConvertedVelocity(value);
|
let speedValue = getConvertedVelocity(value);
|
||||||
if ($velocityUnits === 'speed') {
|
if ($velocityUnits === 'speed') {
|
||||||
speedValue = parseFloat(speedValue.toFixed(2));
|
speedValue = parseFloat(speedValue.toFixed(2));
|
||||||
}
|
}
|
||||||
speed = speedValue;
|
speed = speedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGPXData() {
|
function setGPXData() {
|
||||||
if ($gpxStatistics.global.time.start) {
|
if ($gpxStatistics.global.time.start) {
|
||||||
startDate = toCalendarDate($gpxStatistics.global.time.start);
|
startDate = toCalendarDate($gpxStatistics.global.time.start);
|
||||||
startTime = toTimeString($gpxStatistics.global.time.start);
|
startTime = toTimeString($gpxStatistics.global.time.start);
|
||||||
} else {
|
} else {
|
||||||
startDate = undefined;
|
startDate = undefined;
|
||||||
startTime = undefined;
|
startTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.time.end) {
|
if ($gpxStatistics.global.time.end) {
|
||||||
endDate = toCalendarDate($gpxStatistics.global.time.end);
|
endDate = toCalendarDate($gpxStatistics.global.time.end);
|
||||||
endTime = toTimeString($gpxStatistics.global.time.end);
|
endTime = toTimeString($gpxStatistics.global.time.end);
|
||||||
} else {
|
} else {
|
||||||
endDate = undefined;
|
endDate = undefined;
|
||||||
endTime = undefined;
|
endTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.time.moving) {
|
if ($gpxStatistics.global.time.moving) {
|
||||||
movingTime = $gpxStatistics.global.time.moving;
|
movingTime = $gpxStatistics.global.time.moving;
|
||||||
} else {
|
} else {
|
||||||
movingTime = undefined;
|
movingTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.speed.moving) {
|
if ($gpxStatistics.global.speed.moving) {
|
||||||
setSpeed($gpxStatistics.global.speed.moving);
|
setSpeed($gpxStatistics.global.speed.moving);
|
||||||
} else {
|
} else {
|
||||||
speed = undefined;
|
speed = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
|
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
|
||||||
setGPXData();
|
setGPXData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDate(date: DateValue, time: string): Date {
|
function getDate(date: DateValue, time: string): Date {
|
||||||
if (date === undefined) {
|
if (date === undefined) {
|
||||||
return new Date();
|
return new Date();
|
||||||
}
|
}
|
||||||
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
|
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
|
||||||
if (seconds === undefined) {
|
if (seconds === undefined) {
|
||||||
seconds = 0;
|
seconds = 0;
|
||||||
}
|
}
|
||||||
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
|
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEnd() {
|
function updateEnd() {
|
||||||
if (startDate && movingTime !== undefined) {
|
if (startDate && movingTime !== undefined) {
|
||||||
if (startTime === undefined) {
|
if (startTime === undefined) {
|
||||||
startTime = '00:00:00';
|
startTime = '00:00:00';
|
||||||
}
|
}
|
||||||
let start = getDate(startDate, startTime);
|
let start = getDate(startDate, startTime);
|
||||||
let ratio =
|
let ratio =
|
||||||
$gpxStatistics.global.time.moving > 0
|
$gpxStatistics.global.time.moving > 0
|
||||||
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
||||||
: 1;
|
: 1;
|
||||||
let end = new Date(start.getTime() + ratio * movingTime * 1000);
|
let end = new Date(start.getTime() + ratio * movingTime * 1000);
|
||||||
endDate = toCalendarDate(end);
|
endDate = toCalendarDate(end);
|
||||||
endTime = toTimeString(end);
|
endTime = toTimeString(end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStart() {
|
function updateStart() {
|
||||||
if (endDate && movingTime !== undefined) {
|
if (endDate && movingTime !== undefined) {
|
||||||
if (endTime === undefined) {
|
if (endTime === undefined) {
|
||||||
endTime = '00:00:00';
|
endTime = '00:00:00';
|
||||||
}
|
}
|
||||||
let end = getDate(endDate, endTime);
|
let end = getDate(endDate, endTime);
|
||||||
let ratio =
|
let ratio =
|
||||||
$gpxStatistics.global.time.moving > 0
|
$gpxStatistics.global.time.moving > 0
|
||||||
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
||||||
: 1;
|
: 1;
|
||||||
let start = new Date(end.getTime() - ratio * movingTime * 1000);
|
let start = new Date(end.getTime() - ratio * movingTime * 1000);
|
||||||
startDate = toCalendarDate(start);
|
startDate = toCalendarDate(start);
|
||||||
startTime = toTimeString(start);
|
startTime = toTimeString(start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSpeed() {
|
function getSpeed() {
|
||||||
if (speed === undefined) {
|
if (speed === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let speedValue = speed;
|
let speedValue = speed;
|
||||||
if ($velocityUnits === 'pace') {
|
if ($velocityUnits === 'pace') {
|
||||||
speedValue = distancePerHourToSecondsPerDistance(speed);
|
speedValue = distancePerHourToSecondsPerDistance(speed);
|
||||||
}
|
}
|
||||||
if ($distanceUnits === 'imperial') {
|
if ($distanceUnits === 'imperial') {
|
||||||
speedValue = milesToKilometers(speedValue);
|
speedValue = milesToKilometers(speedValue);
|
||||||
} else if ($distanceUnits === 'nautical') {
|
} else if ($distanceUnits === 'nautical') {
|
||||||
speedValue = nauticalMilesToKilometers(speedValue);
|
speedValue = nauticalMilesToKilometers(speedValue);
|
||||||
}
|
}
|
||||||
return speedValue;
|
return speedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDataFromSpeed() {
|
function updateDataFromSpeed() {
|
||||||
let speedValue = getSpeed();
|
let speedValue = getSpeed();
|
||||||
if (speedValue === undefined) {
|
if (speedValue === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let distance =
|
let distance =
|
||||||
$gpxStatistics.global.distance.moving > 0
|
$gpxStatistics.global.distance.moving > 0
|
||||||
? $gpxStatistics.global.distance.moving
|
? $gpxStatistics.global.distance.moving
|
||||||
: $gpxStatistics.global.distance.total;
|
: $gpxStatistics.global.distance.total;
|
||||||
movingTime = (distance / speedValue) * 3600;
|
movingTime = (distance / speedValue) * 3600;
|
||||||
|
|
||||||
updateEnd();
|
updateEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDataFromTotalTime() {
|
function updateDataFromTotalTime() {
|
||||||
if (movingTime === undefined) {
|
if (movingTime === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let distance =
|
let distance =
|
||||||
$gpxStatistics.global.distance.moving > 0
|
$gpxStatistics.global.distance.moving > 0
|
||||||
? $gpxStatistics.global.distance.moving
|
? $gpxStatistics.global.distance.moving
|
||||||
: $gpxStatistics.global.distance.total;
|
: $gpxStatistics.global.distance.total;
|
||||||
setSpeed(distance / (movingTime / 3600));
|
setSpeed(distance / (movingTime / 3600));
|
||||||
updateEnd();
|
updateEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: canUpdate =
|
$: canUpdate =
|
||||||
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row gap-2 justify-center">
|
<div class="flex flex-row gap-2 justify-center">
|
||||||
<div class="flex flex-col gap-2 grow">
|
<div class="flex flex-col gap-2 grow">
|
||||||
<Label for="speed" class="flex flex-row">
|
<Label for="speed" class="flex flex-row">
|
||||||
<Zap size="16" class="mr-1" />
|
<Zap size="16" class="mr-1" />
|
||||||
{#if $velocityUnits === 'speed'}
|
{#if $velocityUnits === 'speed'}
|
||||||
{$_('quantities.speed')}
|
{$_('quantities.speed')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('quantities.pace')}
|
{$_('quantities.pace')}
|
||||||
{/if}
|
{/if}
|
||||||
</Label>
|
</Label>
|
||||||
<div class="flex flex-row gap-1 items-center">
|
<div class="flex flex-row gap-1 items-center">
|
||||||
{#if $velocityUnits === 'speed'}
|
{#if $velocityUnits === 'speed'}
|
||||||
<Input
|
<Input
|
||||||
id="speed"
|
id="speed"
|
||||||
type="number"
|
type="number"
|
||||||
step={0.01}
|
step={0.01}
|
||||||
min={0.01}
|
min={0.01}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
bind:value={speed}
|
bind:value={speed}
|
||||||
on:change={updateDataFromSpeed}
|
on:change={updateDataFromSpeed}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm shrink-0">
|
<span class="text-sm shrink-0">
|
||||||
{#if $distanceUnits === 'imperial'}
|
{#if $distanceUnits === 'imperial'}
|
||||||
{$_('units.miles_per_hour')}
|
{$_('units.miles_per_hour')}
|
||||||
{:else if $distanceUnits === 'metric'}
|
{:else if $distanceUnits === 'metric'}
|
||||||
{$_('units.kilometers_per_hour')}
|
{$_('units.kilometers_per_hour')}
|
||||||
{:else if $distanceUnits === 'nautical'}
|
{:else if $distanceUnits === 'nautical'}
|
||||||
{$_('units.knots')}
|
{$_('units.knots')}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<TimePicker
|
<TimePicker
|
||||||
bind:value={speed}
|
bind:value={speed}
|
||||||
showHours={false}
|
showHours={false}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
onChange={updateDataFromSpeed}
|
onChange={updateDataFromSpeed}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm shrink-0">
|
<span class="text-sm shrink-0">
|
||||||
{#if $distanceUnits === 'imperial'}
|
{#if $distanceUnits === 'imperial'}
|
||||||
{$_('units.minutes_per_mile')}
|
{$_('units.minutes_per_mile')}
|
||||||
{:else if $distanceUnits === 'metric'}
|
{:else if $distanceUnits === 'metric'}
|
||||||
{$_('units.minutes_per_kilometer')}
|
{$_('units.minutes_per_kilometer')}
|
||||||
{:else if $distanceUnits === 'nautical'}
|
{:else if $distanceUnits === 'nautical'}
|
||||||
{$_('units.minutes_per_nautical_mile')}
|
{$_('units.minutes_per_nautical_mile')}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 grow">
|
<div class="flex flex-col gap-2 grow">
|
||||||
<Label for="duration" class="flex flex-row">
|
<Label for="duration" class="flex flex-row">
|
||||||
<Timer size="16" class="mr-1" />
|
<Timer size="16" class="mr-1" />
|
||||||
{$_('toolbar.time.total_time')}
|
{$_('toolbar.time.total_time')}
|
||||||
</Label>
|
</Label>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
bind:value={movingTime}
|
bind:value={movingTime}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
onChange={updateDataFromTotalTime}
|
onChange={updateDataFromTotalTime}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Label class="flex flex-row">
|
<Label class="flex flex-row">
|
||||||
<CirclePlay size="16" class="mr-1" />
|
<CirclePlay size="16" class="mr-1" />
|
||||||
{$_('toolbar.time.start')}
|
{$_('toolbar.time.start')}
|
||||||
</Label>
|
</Label>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
bind:value={startDate}
|
bind:value={startDate}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
locale={get(locale) ?? 'en'}
|
locale={get(locale) ?? 'en'}
|
||||||
placeholder={$_('toolbar.time.pick_date')}
|
placeholder={$_('toolbar.time.pick_date')}
|
||||||
class="w-fit grow"
|
class="w-fit grow"
|
||||||
onValueChange={async () => {
|
onValueChange={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
updateEnd();
|
updateEnd();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={1}
|
step={1}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
bind:value={startTime}
|
bind:value={startTime}
|
||||||
class="w-fit"
|
class="w-fit"
|
||||||
on:change={updateEnd}
|
on:change={updateEnd}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Label class="flex flex-row">
|
<Label class="flex flex-row">
|
||||||
<CircleStop size="16" class="mr-1" />
|
<CircleStop size="16" class="mr-1" />
|
||||||
{$_('toolbar.time.end')}
|
{$_('toolbar.time.end')}
|
||||||
</Label>
|
</Label>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
bind:value={endDate}
|
bind:value={endDate}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
locale={get(locale) ?? 'en'}
|
locale={get(locale) ?? 'en'}
|
||||||
placeholder={$_('toolbar.time.pick_date')}
|
placeholder={$_('toolbar.time.pick_date')}
|
||||||
class="w-fit grow"
|
class="w-fit grow"
|
||||||
onValueChange={async () => {
|
onValueChange={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
updateStart();
|
updateStart();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={1}
|
step={1}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
bind:value={endTime}
|
bind:value={endTime}
|
||||||
class="w-fit"
|
class="w-fit"
|
||||||
on:change={updateStart}
|
on:change={updateStart}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
|
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
|
||||||
<div class="mt-0.5 flex flex-row gap-1 items-center">
|
<div class="mt-0.5 flex flex-row gap-1 items-center">
|
||||||
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
|
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
|
||||||
<Label for="artificial-time">
|
<Label for="artificial-time">
|
||||||
{$_('toolbar.time.artificial')}
|
{$_('toolbar.time.artificial')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
class="grow whitespace-normal h-fit"
|
class="grow whitespace-normal h-fit"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
let effectiveSpeed = getSpeed();
|
let effectiveSpeed = getSpeed();
|
||||||
if (
|
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
|
||||||
startDate === undefined ||
|
return;
|
||||||
startTime === undefined ||
|
}
|
||||||
effectiveSpeed === undefined
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(effectiveSpeed - $gpxStatistics.global.speed.moving) < 0.01) {
|
if (Math.abs(effectiveSpeed - $gpxStatistics.global.speed.moving) < 0.01) {
|
||||||
effectiveSpeed = $gpxStatistics.global.speed.moving;
|
effectiveSpeed = $gpxStatistics.global.speed.moving;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ratio = 1;
|
let ratio = 1;
|
||||||
if (
|
if (
|
||||||
$gpxStatistics.global.speed.moving > 0 &&
|
$gpxStatistics.global.speed.moving > 0 &&
|
||||||
$gpxStatistics.global.speed.moving !== effectiveSpeed
|
$gpxStatistics.global.speed.moving !== effectiveSpeed
|
||||||
) {
|
) {
|
||||||
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
|
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = $selection.getSelected()[0];
|
let item = $selection.getSelected()[0];
|
||||||
let fileId = item.getFileId();
|
let fileId = item.getFileId();
|
||||||
dbUtils.applyToFile(fileId, (file) => {
|
dbUtils.applyToFile(fileId, (file) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
if (artificial) {
|
if (artificial) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(getDate(startDate, startTime), movingTime);
|
||||||
getDate(startDate, startTime),
|
} else {
|
||||||
movingTime
|
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
||||||
);
|
}
|
||||||
} else {
|
} else if (item instanceof ListTrackItem) {
|
||||||
file.changeTimestamps(
|
if (artificial) {
|
||||||
getDate(startDate, startTime),
|
file.createArtificialTimestamps(
|
||||||
effectiveSpeed,
|
getDate(startDate, startTime),
|
||||||
ratio
|
movingTime,
|
||||||
);
|
item.getTrackIndex()
|
||||||
}
|
);
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else {
|
||||||
if (artificial) {
|
file.changeTimestamps(
|
||||||
file.createArtificialTimestamps(
|
getDate(startDate, startTime),
|
||||||
getDate(startDate, startTime),
|
effectiveSpeed,
|
||||||
movingTime,
|
ratio,
|
||||||
item.getTrackIndex()
|
item.getTrackIndex()
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
file.changeTimestamps(
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
getDate(startDate, startTime),
|
if (artificial) {
|
||||||
effectiveSpeed,
|
file.createArtificialTimestamps(
|
||||||
ratio,
|
getDate(startDate, startTime),
|
||||||
item.getTrackIndex()
|
movingTime,
|
||||||
);
|
item.getTrackIndex(),
|
||||||
}
|
item.getSegmentIndex()
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
);
|
||||||
if (artificial) {
|
} else {
|
||||||
file.createArtificialTimestamps(
|
file.changeTimestamps(
|
||||||
getDate(startDate, startTime),
|
getDate(startDate, startTime),
|
||||||
movingTime,
|
effectiveSpeed,
|
||||||
item.getTrackIndex(),
|
ratio,
|
||||||
item.getSegmentIndex()
|
item.getTrackIndex(),
|
||||||
);
|
item.getSegmentIndex()
|
||||||
} else {
|
);
|
||||||
file.changeTimestamps(
|
}
|
||||||
getDate(startDate, startTime),
|
}
|
||||||
effectiveSpeed,
|
});
|
||||||
ratio,
|
}}
|
||||||
item.getTrackIndex(),
|
>
|
||||||
item.getSegmentIndex()
|
<CalendarClock size="16" class="mr-1 shrink-0" />
|
||||||
);
|
{$_('toolbar.time.update')}
|
||||||
}
|
</Button>
|
||||||
}
|
<Button variant="outline" on:click={setGPXData}>
|
||||||
});
|
<CircleX size="16" />
|
||||||
}}
|
</Button>
|
||||||
>
|
</div>
|
||||||
<CalendarClock size="16" class="mr-1 shrink-0" />
|
<Help link={getURLForLanguage($locale, '/help/toolbar/time')}>
|
||||||
{$_('toolbar.time.update')}
|
{#if canUpdate}
|
||||||
</Button>
|
{$_('toolbar.time.help')}
|
||||||
<Button variant="outline" on:click={setGPXData}>
|
{:else}
|
||||||
<CircleX size="16" />
|
{$_('toolbar.time.help_invalid_selection')}
|
||||||
</Button>
|
{/if}
|
||||||
</div>
|
</Help>
|
||||||
<Help link="./help/toolbar/time">
|
|
||||||
{#if canUpdate}
|
|
||||||
{$_('toolbar.time.help')}
|
|
||||||
{:else}
|
|
||||||
{$_('toolbar.time.help_invalid_selection')}
|
|
||||||
{/if}
|
|
||||||
</Help>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(input[type='time']) {
|
div :global(input[type='time']) {
|
||||||
/*
|
/*
|
||||||
Style copy-pasted from shadcn-svelte Input.
|
Style copy-pasted from shadcn-svelte Input.
|
||||||
Needed to use native time input to avoid a bug with 2-level bind:value.
|
Needed to use native time input to avoid a bug with 2-level bind:value.
|
||||||
*/
|
*/
|
||||||
@apply flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
@apply flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,272 +1,272 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
|
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { Waypoint } from 'gpx';
|
import { Waypoint } from 'gpx';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { ListWaypointItem } from '$lib/components/file-list/FileList';
|
import { ListWaypointItem } from '$lib/components/file-list/FileList';
|
||||||
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
|
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { CirclePlus, CircleX, Save } from 'lucide-svelte';
|
import { CirclePlus, CircleX, Save } from 'lucide-svelte';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
|
|
||||||
let name: string;
|
let name: string;
|
||||||
let description: string;
|
let description: string;
|
||||||
let link: string;
|
let link: string;
|
||||||
let longitude: number;
|
let longitude: number;
|
||||||
let latitude: number;
|
let latitude: number;
|
||||||
|
|
||||||
let selectedSymbol = {
|
let selectedSymbol = {
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const { verticalFileView } = settings;
|
const { verticalFileView } = settings;
|
||||||
|
|
||||||
$: canCreate = $selection.size > 0;
|
$: canCreate = $selection.size > 0;
|
||||||
|
|
||||||
$: if ($verticalFileView && $selection) {
|
$: if ($verticalFileView && $selection) {
|
||||||
selectedWaypoint.update(() => {
|
selectedWaypoint.update(() => {
|
||||||
if ($selection.size === 1) {
|
if ($selection.size === 1) {
|
||||||
let item = $selection.getSelected()[0];
|
let item = $selection.getSelected()[0];
|
||||||
if (item instanceof ListWaypointItem) {
|
if (item instanceof ListWaypointItem) {
|
||||||
let file = getFile(item.getFileId());
|
let file = getFile(item.getFileId());
|
||||||
let waypoint = file?.wpt[item.getWaypointIndex()];
|
let waypoint = file?.wpt[item.getWaypointIndex()];
|
||||||
if (waypoint) {
|
if (waypoint) {
|
||||||
return [waypoint, item.getFileId()];
|
return [waypoint, item.getFileId()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let unsubscribe: (() => void) | undefined = undefined;
|
let unsubscribe: (() => void) | undefined = undefined;
|
||||||
function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
|
function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
|
||||||
if ($selectedWaypoint) {
|
if ($selectedWaypoint) {
|
||||||
if (fileStore) {
|
if (fileStore) {
|
||||||
if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
|
if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
|
||||||
$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
|
$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
|
||||||
name = $selectedWaypoint[0].name ?? '';
|
name = $selectedWaypoint[0].name ?? '';
|
||||||
description = $selectedWaypoint[0].desc ?? '';
|
description = $selectedWaypoint[0].desc ?? '';
|
||||||
if (
|
if (
|
||||||
$selectedWaypoint[0].cmt !== undefined &&
|
$selectedWaypoint[0].cmt !== undefined &&
|
||||||
$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
|
$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
|
||||||
) {
|
) {
|
||||||
description += '\n\n' + $selectedWaypoint[0].cmt;
|
description += '\n\n' + $selectedWaypoint[0].cmt;
|
||||||
}
|
}
|
||||||
link = $selectedWaypoint[0].link?.attributes?.href ?? '';
|
link = $selectedWaypoint[0].link?.attributes?.href ?? '';
|
||||||
let symbol = $selectedWaypoint[0].sym ?? '';
|
let symbol = $selectedWaypoint[0].sym ?? '';
|
||||||
let symbolKey = getSymbolKey(symbol);
|
let symbolKey = getSymbolKey(symbol);
|
||||||
if (symbolKey) {
|
if (symbolKey) {
|
||||||
selectedSymbol = {
|
selectedSymbol = {
|
||||||
value: symbol,
|
value: symbol,
|
||||||
label: $_(`gpx.symbol.${symbolKey}`)
|
label: $_(`gpx.symbol.${symbolKey}`)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
selectedSymbol = {
|
selectedSymbol = {
|
||||||
value: symbol,
|
value: symbol,
|
||||||
label: ''
|
label: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
|
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
|
||||||
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
|
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
|
||||||
} else {
|
} else {
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWaypointData() {
|
function resetWaypointData() {
|
||||||
name = '';
|
name = '';
|
||||||
description = '';
|
description = '';
|
||||||
link = '';
|
link = '';
|
||||||
selectedSymbol = {
|
selectedSymbol = {
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: ''
|
||||||
};
|
};
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
unsubscribe = undefined;
|
unsubscribe = undefined;
|
||||||
}
|
}
|
||||||
if ($selectedWaypoint) {
|
if ($selectedWaypoint) {
|
||||||
let fileStore = get(fileObservers).get($selectedWaypoint[1]);
|
let fileStore = get(fileObservers).get($selectedWaypoint[1]);
|
||||||
if (fileStore) {
|
if (fileStore) {
|
||||||
unsubscribe = fileStore.subscribe(updateWaypointData);
|
unsubscribe = fileStore.subscribe(updateWaypointData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resetWaypointData();
|
resetWaypointData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOrUpdateWaypoint() {
|
function createOrUpdateWaypoint() {
|
||||||
if (typeof latitude === 'string') {
|
if (typeof latitude === 'string') {
|
||||||
latitude = parseFloat(latitude);
|
latitude = parseFloat(latitude);
|
||||||
}
|
}
|
||||||
if (typeof longitude === 'string') {
|
if (typeof longitude === 'string') {
|
||||||
longitude = parseFloat(longitude);
|
longitude = parseFloat(longitude);
|
||||||
}
|
}
|
||||||
latitude = parseFloat(latitude.toFixed(6));
|
latitude = parseFloat(latitude.toFixed(6));
|
||||||
longitude = parseFloat(longitude.toFixed(6));
|
longitude = parseFloat(longitude.toFixed(6));
|
||||||
|
|
||||||
dbUtils.addOrUpdateWaypoint(
|
dbUtils.addOrUpdateWaypoint(
|
||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: latitude,
|
lat: latitude,
|
||||||
lon: longitude
|
lon: longitude
|
||||||
},
|
},
|
||||||
name: name.length > 0 ? name : undefined,
|
name: name.length > 0 ? name : undefined,
|
||||||
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: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined
|
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined
|
||||||
},
|
},
|
||||||
$selectedWaypoint
|
$selectedWaypoint
|
||||||
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
|
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
resetWaypointData();
|
resetWaypointData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCoordinates(e: any) {
|
function setCoordinates(e: any) {
|
||||||
latitude = e.lngLat.lat.toFixed(6);
|
latitude = e.lngLat.lat.toFixed(6);
|
||||||
longitude = e.lngLat.lng.toFixed(6);
|
longitude = e.lngLat.lng.toFixed(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
|
$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
|
||||||
return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
|
return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let m = get(map);
|
let m = get(map);
|
||||||
m?.on('click', setCoordinates);
|
m?.on('click', setCoordinates);
|
||||||
setCrosshairCursor();
|
setCrosshairCursor();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
let m = get(map);
|
let m = get(map);
|
||||||
m?.off('click', setCoordinates);
|
m?.off('click', setCoordinates);
|
||||||
resetCursor();
|
resetCursor();
|
||||||
|
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
unsubscribe = undefined;
|
unsubscribe = undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
||||||
<Label for="description">{$_('menu.metadata.description')}</Label>
|
<Label for="description">{$_('menu.metadata.description')}</Label>
|
||||||
<Textarea bind:value={description} id="description" />
|
<Textarea bind:value={description} id="description" />
|
||||||
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
||||||
<Select.Root bind:selected={selectedSymbol}>
|
<Select.Root bind:selected={selectedSymbol}>
|
||||||
<Select.Trigger id="symbol" class="w-full h-8">
|
<Select.Trigger id="symbol" class="w-full h-8">
|
||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||||
{#each sortedSymbols as [key, symbol]}
|
{#each sortedSymbols as [key, symbol]}
|
||||||
<Select.Item value={symbol.value}>
|
<Select.Item value={symbol.value}>
|
||||||
<span>
|
<span>
|
||||||
{#if symbol.icon}
|
{#if symbol.icon}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={symbol.icon}
|
this={symbol.icon}
|
||||||
size="14"
|
size="14"
|
||||||
class="inline-block align-sub mr-0.5"
|
class="inline-block align-sub mr-0.5"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="w-4 inline-block" />
|
<span class="w-4 inline-block" />
|
||||||
{/if}
|
{/if}
|
||||||
{$_(`gpx.symbol.${key}`)}
|
{$_(`gpx.symbol.${key}`)}
|
||||||
</span>
|
</span>
|
||||||
</Select.Item>
|
</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
||||||
<Input bind:value={link} id="link" class="h-8" />
|
<Input bind:value={link} id="link" class="h-8" />
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
||||||
<Input
|
<Input
|
||||||
bind:value={latitude}
|
bind:value={latitude}
|
||||||
type="number"
|
type="number"
|
||||||
id="latitude"
|
id="latitude"
|
||||||
step={1e-6}
|
step={1e-6}
|
||||||
min={-90}
|
min={-90}
|
||||||
max={90}
|
max={90}
|
||||||
class="text-xs h-8 "
|
class="text-xs h-8 "
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
|
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
|
||||||
<Input
|
<Input
|
||||||
bind:value={longitude}
|
bind:value={longitude}
|
||||||
type="number"
|
type="number"
|
||||||
id="longitude"
|
id="longitude"
|
||||||
step={1e-6}
|
step={1e-6}
|
||||||
min={-180}
|
min={-180}
|
||||||
max={180}
|
max={180}
|
||||||
class="text-xs h-8"
|
class="text-xs h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
class="grow whitespace-normal h-fit"
|
class="grow whitespace-normal h-fit"
|
||||||
on:click={createOrUpdateWaypoint}
|
on:click={createOrUpdateWaypoint}
|
||||||
>
|
>
|
||||||
{#if $selectedWaypoint}
|
{#if $selectedWaypoint}
|
||||||
<Save size="16" class="mr-1 shrink-0" />
|
<Save size="16" class="mr-1 shrink-0" />
|
||||||
{$_('menu.metadata.save')}
|
{$_('menu.metadata.save')}
|
||||||
{:else}
|
{:else}
|
||||||
<CirclePlus size="16" class="mr-1 shrink-0" />
|
<CirclePlus size="16" class="mr-1 shrink-0" />
|
||||||
{$_('toolbar.waypoint.create')}
|
{$_('toolbar.waypoint.create')}
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
resetWaypointData();
|
resetWaypointData();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircleX size="16" />
|
<CircleX size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Help link="./help/toolbar/poi">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/poi')}>
|
||||||
{#if $selectedWaypoint || canCreate}
|
{#if $selectedWaypoint || canCreate}
|
||||||
{$_('toolbar.waypoint.help')}
|
{$_('toolbar.waypoint.help')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.waypoint.help_no_selection')}
|
{$_('toolbar.waypoint.help_no_selection')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -25,7 +25,7 @@
|
|||||||
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
||||||
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { RoutingControls } from './RoutingControls';
|
import { RoutingControls } from './RoutingControls';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { fileObservers } from '$lib/db';
|
import { fileObservers } from '$lib/db';
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
type ListItem
|
type ListItem
|
||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import { flyAndScale, resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { TrackPoint } from 'gpx';
|
import { TrackPoint } from 'gpx';
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
||||||
<Help link="./help/toolbar/routing">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/routing')}>
|
||||||
{#if !validSelection}
|
{#if !validSelection}
|
||||||
{$_('toolbar.routing.help_no_file')}
|
{$_('toolbar.routing.help_no_file')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -17,11 +17,12 @@
|
|||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { onDestroy, tick } from 'svelte';
|
import { onDestroy, tick } from 'svelte';
|
||||||
import { Crop } from 'lucide-svelte';
|
import { Crop } from 'lucide-svelte';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
import { SplitControls } from './SplitControls';
|
import { SplitControls } from './SplitControls';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
let splitControls: SplitControls | undefined = undefined;
|
let splitControls: SplitControls | undefined = undefined;
|
||||||
let canCrop = false;
|
let canCrop = false;
|
||||||
@@ -135,7 +136,7 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</Label>
|
</Label>
|
||||||
<Help link="./help/toolbar/scissors">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/scissors')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.scissors.help')}
|
{$_('toolbar.scissors.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -40,7 +40,7 @@ By right-clicking on a file tab, you can access the same actions as in the [edit
|
|||||||
### Vertical layout
|
### Vertical layout
|
||||||
|
|
||||||
As mentioned in the [view options section](./menu/view), you can switch between a horizontal and a vertical layout for the file list.
|
As mentioned in the [view options section](./menu/view), you can switch between a horizontal and a vertical layout for the file list.
|
||||||
The vertical file list is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](../gpx).
|
The vertical file list is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](./gpx).
|
||||||
Indeed, this layout allows you to inspect the content of the files through collapsible sections.
|
Indeed, this layout allows you to inspect the content of the files through collapsible sections.
|
||||||
|
|
||||||
You can also apply [edit actions](./menu/edit) and [tools](./toolbar/) to internal file items.
|
You can also apply [edit actions](./menu/edit) and [tools](./toolbar/) to internal file items.
|
||||||
|
Reference in New Issue
Block a user