mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-10-14 11:38:18 +00:00
progress
This commit is contained in:
@@ -11,13 +11,14 @@
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { Trash2 } from 'lucide-svelte';
|
||||
import { map } from '$lib/stores';
|
||||
import { Trash2 } from '@lucide/svelte';
|
||||
import { map } from '$lib/components/map/map.svelte';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
import { dbUtils } from '$lib/db';
|
||||
import type { GeoJSONSource } from 'mapbox-gl';
|
||||
|
||||
let cleanType = CleanType.INSIDE;
|
||||
let deleteTrackpoints = true;
|
||||
@@ -25,13 +26,13 @@
|
||||
let rectangleCoordinates: mapboxgl.LngLat[] = [];
|
||||
|
||||
function updateRectangle() {
|
||||
if ($map) {
|
||||
if (map.current) {
|
||||
if (rectangleCoordinates.length != 2) {
|
||||
if ($map.getLayer('rectangle')) {
|
||||
$map.removeLayer('rectangle');
|
||||
if (map.current.getLayer('rectangle')) {
|
||||
map.current.removeLayer('rectangle');
|
||||
}
|
||||
} else {
|
||||
let data = {
|
||||
let data: GeoJSON.Feature = {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
@@ -45,18 +46,19 @@
|
||||
],
|
||||
],
|
||||
},
|
||||
properties: {},
|
||||
};
|
||||
let source = $map.getSource('rectangle');
|
||||
let source: GeoJSONSource | undefined = map.current.getSource('rectangle');
|
||||
if (source) {
|
||||
source.setData(data);
|
||||
} else {
|
||||
$map.addSource('rectangle', {
|
||||
map.current.addSource('rectangle', {
|
||||
type: 'geojson',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
if (!$map.getLayer('rectangle')) {
|
||||
$map.addLayer({
|
||||
if (!map.current.getLayer('rectangle')) {
|
||||
map.current.addLayer({
|
||||
id: 'rectangle',
|
||||
type: 'fill',
|
||||
source: 'rectangle',
|
||||
@@ -91,35 +93,34 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setCrosshairCursor();
|
||||
if (map.current) {
|
||||
setCrosshairCursor(map.current.getCanvas());
|
||||
map.current.on('mousedown', onMouseDown);
|
||||
map.current.on('mousemove', onMouseMove);
|
||||
map.current.on('mouseup', onMouseUp);
|
||||
map.current.on('touchstart', onMouseDown);
|
||||
map.current.on('touchmove', onMouseMove);
|
||||
map.current.on('touchend', onMouseUp);
|
||||
map.current.dragPan.disable();
|
||||
}
|
||||
});
|
||||
|
||||
$: if ($map) {
|
||||
$map.on('mousedown', onMouseDown);
|
||||
$map.on('mousemove', onMouseMove);
|
||||
$map.on('mouseup', onMouseUp);
|
||||
$map.on('touchstart', onMouseDown);
|
||||
$map.on('touchmove', onMouseMove);
|
||||
$map.on('touchend', onMouseUp);
|
||||
$map.dragPan.disable();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
resetCursor();
|
||||
if ($map) {
|
||||
$map.off('mousedown', onMouseDown);
|
||||
$map.off('mousemove', onMouseMove);
|
||||
$map.off('mouseup', onMouseUp);
|
||||
$map.off('touchstart', onMouseDown);
|
||||
$map.off('touchmove', onMouseMove);
|
||||
$map.off('touchend', onMouseUp);
|
||||
$map.dragPan.enable();
|
||||
if (map.current) {
|
||||
resetCursor(map.current.getCanvas());
|
||||
map.current.off('mousedown', onMouseDown);
|
||||
map.current.off('mousemove', onMouseMove);
|
||||
map.current.off('mouseup', onMouseUp);
|
||||
map.current.off('touchstart', onMouseDown);
|
||||
map.current.off('touchmove', onMouseMove);
|
||||
map.current.off('touchend', onMouseUp);
|
||||
map.current.dragPan.enable();
|
||||
|
||||
if ($map.getLayer('rectangle')) {
|
||||
$map.removeLayer('rectangle');
|
||||
if (map.current.getLayer('rectangle')) {
|
||||
map.current.removeLayer('rectangle');
|
||||
}
|
||||
if ($map.getSource('rectangle')) {
|
||||
$map.removeSource('rectangle');
|
||||
if (map.current.getSource('rectangle')) {
|
||||
map.current.removeSource('rectangle');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -132,23 +133,23 @@
|
||||
<div class="flex flex-row items-center gap-[6.4px] h-3">
|
||||
<Checkbox id="delete-trkpt" bind:checked={deleteTrackpoints} class="scale-90" />
|
||||
<Label for="delete-trkpt">
|
||||
{$_('toolbar.clean.delete_trackpoints')}
|
||||
{i18n._('toolbar.clean.delete_trackpoints')}
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-[6.4px] h-3">
|
||||
<Checkbox id="delete-wpt" bind:checked={deleteWaypoints} class="scale-90" />
|
||||
<Label for="delete-wpt">
|
||||
{$_('toolbar.clean.delete_waypoints')}
|
||||
{i18n._('toolbar.clean.delete_waypoints')}
|
||||
</Label>
|
||||
</div>
|
||||
<RadioGroup.Root bind:value={cleanType}>
|
||||
<Label class="flex flex-row items-center gap-2">
|
||||
<RadioGroup.Item value={CleanType.INSIDE} />
|
||||
{$_('toolbar.clean.delete_inside')}
|
||||
{i18n._('toolbar.clean.delete_inside')}
|
||||
</Label>
|
||||
<Label class="flex flex-row items-center gap-2">
|
||||
<RadioGroup.Item value={CleanType.OUTSIDE} />
|
||||
{$_('toolbar.clean.delete_outside')}
|
||||
{i18n._('toolbar.clean.delete_outside')}
|
||||
</Label>
|
||||
</RadioGroup.Root>
|
||||
</fieldset>
|
||||
@@ -156,7 +157,7 @@
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
disabled={!validSelection || rectangleCoordinates.length != 2}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
dbUtils.cleanSelection(
|
||||
[
|
||||
{
|
||||
@@ -176,13 +177,13 @@
|
||||
}}
|
||||
>
|
||||
<Trash2 size="16" class="mr-1" />
|
||||
{$_('toolbar.clean.button')}
|
||||
{i18n._('toolbar.clean.button')}
|
||||
</Button>
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/clean')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/clean')}>
|
||||
{#if validSelection}
|
||||
{$_('toolbar.clean.help')}
|
||||
{i18n._('toolbar.clean.help')}
|
||||
{:else}
|
||||
{$_('toolbar.clean.help_no_selection')}
|
||||
{i18n._('toolbar.clean.help_no_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
@@ -2,34 +2,38 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { MountainSnow } from 'lucide-svelte';
|
||||
import { MountainSnow } from '@lucide/svelte';
|
||||
import { dbUtils } from '$lib/db';
|
||||
import { map } from '$lib/stores';
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { map } from '$lib/components/map/map.svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
|
||||
$: validSelection = $selection.size > 0;
|
||||
let props: {
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let validSelection = $derived($selection.size > 0);
|
||||
</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
|
||||
variant="outline"
|
||||
class="whitespace-normal h-fit"
|
||||
disabled={!validSelection}
|
||||
on:click={async () => {
|
||||
if ($map) {
|
||||
dbUtils.addElevationToSelection($map);
|
||||
onclick={async () => {
|
||||
if (map.current) {
|
||||
dbUtils.addElevationToSelection(map.current);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MountainSnow size="16" class="mr-1 shrink-0" />
|
||||
{$_('toolbar.elevation.button')}
|
||||
{i18n._('toolbar.elevation.button')}
|
||||
</Button>
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/elevation')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/elevation')}>
|
||||
{#if validSelection}
|
||||
{$_('toolbar.elevation.help')}
|
||||
{i18n._('toolbar.elevation.help')}
|
||||
{:else}
|
||||
{$_('toolbar.elevation.help_no_selection')}
|
||||
{i18n._('toolbar.elevation.help_no_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Ungroup } from 'lucide-svelte';
|
||||
import { Ungroup } from '@lucide/svelte';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
import {
|
||||
ListFileItem,
|
||||
@@ -11,7 +11,7 @@
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { dbUtils, getFile } from '$lib/db';
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
|
||||
$: validSelection =
|
||||
@@ -39,15 +39,15 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||
<Button variant="outline" disabled={!validSelection} on:click={dbUtils.extractSelection}>
|
||||
<Button variant="outline" disabled={!validSelection} onclick={dbUtils.extractSelection}>
|
||||
<Ungroup size="16" class="mr-1" />
|
||||
{$_('toolbar.extract.button')}
|
||||
{i18n._('toolbar.extract.button')}
|
||||
</Button>
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/extract')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/extract')}>
|
||||
{#if validSelection}
|
||||
{$_('toolbar.extract.help')}
|
||||
{i18n._('toolbar.extract.help')}
|
||||
{:else}
|
||||
{$_('toolbar.extract.help_invalid_selection')}
|
||||
{i18n._('toolbar.extract.help_invalid_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
@@ -13,9 +13,9 @@
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { dbUtils, getFile } from '$lib/db';
|
||||
import { Group } from 'lucide-svelte';
|
||||
import { Group } from '@lucide/svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||
import { gpxStatistics } from '$lib/stores';
|
||||
@@ -61,17 +61,17 @@
|
||||
<RadioGroup.Root bind:value={mergeType}>
|
||||
<Label class="flex flex-row items-center gap-1.5 leading-5">
|
||||
<RadioGroup.Item value={MergeType.TRACES} />
|
||||
{$_('toolbar.merge.merge_traces')}
|
||||
{i18n._('toolbar.merge.merge_traces')}
|
||||
</Label>
|
||||
<Label class="flex flex-row items-center gap-1.5 leading-5">
|
||||
<RadioGroup.Item value={MergeType.CONTENTS} />
|
||||
{$_('toolbar.merge.merge_contents')}
|
||||
{i18n._('toolbar.merge.merge_contents')}
|
||||
</Label>
|
||||
</RadioGroup.Root>
|
||||
{#if mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0}
|
||||
<div class="flex flex-row items-center gap-1.5">
|
||||
<Checkbox id="remove-gaps" bind:checked={removeGaps} />
|
||||
<Label for="remove-gaps">{$_('toolbar.merge.remove_gaps')}</Label>
|
||||
<Label for="remove-gaps">{i18n._('toolbar.merge.remove_gaps')}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
@@ -79,7 +79,7 @@
|
||||
class="whitespace-normal h-fit"
|
||||
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
||||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
dbUtils.mergeSelection(
|
||||
mergeType === MergeType.TRACES,
|
||||
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
|
||||
@@ -87,31 +87,31 @@
|
||||
}}
|
||||
>
|
||||
<Group size="16" class="mr-1 shrink-0" />
|
||||
{$_('toolbar.merge.merge_selection')}
|
||||
{i18n._('toolbar.merge.merge_selection')}
|
||||
</Button>
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/merge')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/merge')}>
|
||||
{#if mergeType === MergeType.TRACES && canMergeTraces}
|
||||
{$_('toolbar.merge.help_merge_traces')}
|
||||
{i18n._('toolbar.merge.help_merge_traces')}
|
||||
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
||||
{$_('toolbar.merge.help_cannot_merge_traces')}
|
||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||
{i18n._('toolbar.merge.help_cannot_merge_traces')}
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||
<Shortcut
|
||||
ctrl={true}
|
||||
click={true}
|
||||
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||
/>
|
||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
||||
{$_('toolbar.merge.help_merge_contents')}
|
||||
{i18n._('toolbar.merge.help_merge_contents')}
|
||||
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
||||
{$_('toolbar.merge.help_cannot_merge_contents')}
|
||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||
{i18n._('toolbar.merge.help_cannot_merge_contents')}
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||
<Shortcut
|
||||
ctrl={true}
|
||||
click={true}
|
||||
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||
/>
|
||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
@@ -9,26 +9,30 @@
|
||||
ListTrackSegmentItem,
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { Filter } from 'lucide-svelte';
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { Funnel } from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||
import { dbUtils, fileObservers } from '$lib/db';
|
||||
import { map } from '$lib/stores';
|
||||
import { map } from '$lib/components/map/map.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||
import { derived } from 'svelte/store';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import type { GeoJSONSource } from 'mapbox-gl';
|
||||
|
||||
let sliderValue = [50];
|
||||
let maxPoints = 0;
|
||||
let currentPoints = 0;
|
||||
let props: { class?: string } = $props();
|
||||
|
||||
let sliderValue = $state([50]);
|
||||
let maxPoints = $state(0);
|
||||
let currentPoints = $state(0);
|
||||
const minTolerance = 0.1;
|
||||
const maxTolerance = 10000;
|
||||
|
||||
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||
|
||||
$: tolerance =
|
||||
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)));
|
||||
let validSelection = $derived(
|
||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||
);
|
||||
let tolerance = $derived(
|
||||
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
|
||||
);
|
||||
|
||||
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
|
||||
let unsubscribes = new Map<string, () => void>();
|
||||
@@ -63,18 +67,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
if ($map) {
|
||||
let source = $map.getSource('simplified');
|
||||
if (map.current) {
|
||||
let source: GeoJSONSource | undefined = map.current.getSource('simplified');
|
||||
if (source) {
|
||||
source.setData(data);
|
||||
} else {
|
||||
$map.addSource('simplified', {
|
||||
map.current.addSource('simplified', {
|
||||
type: 'geojson',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
if (!$map.getLayer('simplified')) {
|
||||
$map.addLayer({
|
||||
if (!map.current.getLayer('simplified')) {
|
||||
map.current.addLayer({
|
||||
id: 'simplified',
|
||||
type: 'line',
|
||||
source: 'simplified',
|
||||
@@ -84,62 +88,66 @@
|
||||
},
|
||||
});
|
||||
} else {
|
||||
$map.moveLayer('simplified');
|
||||
map.current.moveLayer('simplified');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($fileObservers) {
|
||||
unsubscribes.forEach((unsubscribe, fileId) => {
|
||||
if (!$fileObservers.has(fileId)) {
|
||||
unsubscribe();
|
||||
unsubscribes.delete(fileId);
|
||||
}
|
||||
});
|
||||
$fileObservers.forEach((fileStore, fileId) => {
|
||||
if (!unsubscribes.has(fileId)) {
|
||||
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
|
||||
fs,
|
||||
sel,
|
||||
]).subscribe(([fs, sel]) => {
|
||||
if (fs) {
|
||||
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
let segmentItem = new ListTrackSegmentItem(
|
||||
fileId,
|
||||
trackIndex,
|
||||
segmentIndex
|
||||
);
|
||||
if (sel.hasAnyParent(segmentItem)) {
|
||||
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
||||
simplified.set(segmentItem.getFullId(), [
|
||||
segmentItem,
|
||||
statistics.local.points.length,
|
||||
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
||||
]);
|
||||
update();
|
||||
} else if (simplified.has(segmentItem.getFullId())) {
|
||||
simplified.delete(segmentItem.getFullId());
|
||||
update();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
unsubscribes.set(fileId, unsubscribe);
|
||||
}
|
||||
});
|
||||
}
|
||||
$effect(() => {
|
||||
if ($fileObservers) {
|
||||
unsubscribes.forEach((unsubscribe, fileId) => {
|
||||
if (!$fileObservers.has(fileId)) {
|
||||
unsubscribe();
|
||||
unsubscribes.delete(fileId);
|
||||
}
|
||||
});
|
||||
$fileObservers.forEach((fileStore, fileId) => {
|
||||
if (!unsubscribes.has(fileId)) {
|
||||
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
|
||||
fs,
|
||||
sel,
|
||||
]).subscribe(([fs, sel]) => {
|
||||
if (fs) {
|
||||
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
let segmentItem = new ListTrackSegmentItem(
|
||||
fileId,
|
||||
trackIndex,
|
||||
segmentIndex
|
||||
);
|
||||
if (sel.hasAnyParent(segmentItem)) {
|
||||
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
||||
simplified.set(segmentItem.getFullId(), [
|
||||
segmentItem,
|
||||
statistics.local.points.length,
|
||||
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
||||
]);
|
||||
update();
|
||||
} else if (simplified.has(segmentItem.getFullId())) {
|
||||
simplified.delete(segmentItem.getFullId());
|
||||
update();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
unsubscribes.set(fileId, unsubscribe);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$: if (tolerance) {
|
||||
update();
|
||||
}
|
||||
$effect(() => {
|
||||
if (tolerance) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if ($map) {
|
||||
if ($map.getLayer('simplified')) {
|
||||
$map.removeLayer('simplified');
|
||||
if (map.current) {
|
||||
if (map.current.getLayer('simplified')) {
|
||||
map.current.removeLayer('simplified');
|
||||
}
|
||||
if ($map.getSource('simplified')) {
|
||||
$map.removeSource('simplified');
|
||||
if (map.current.getSource('simplified')) {
|
||||
map.current.removeSource('simplified');
|
||||
}
|
||||
}
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
@@ -160,28 +168,28 @@
|
||||
}
|
||||
</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">
|
||||
<Slider bind:value={sliderValue} min={0} max={100} step={1} />
|
||||
<Slider bind:value={sliderValue} min={0} max={100} step={1} type="multiple" />
|
||||
</div>
|
||||
<Label class="flex flex-row justify-between">
|
||||
<span>{$_('toolbar.reduce.tolerance')}</span>
|
||||
<span>{i18n._('toolbar.reduce.tolerance')}</span>
|
||||
<WithUnits value={tolerance / 1000} type="distance" decimals={4} class="font-normal" />
|
||||
</Label>
|
||||
<Label class="flex flex-row justify-between">
|
||||
<span>{$_('toolbar.reduce.number_of_points')}</span>
|
||||
<span>{i18n._('toolbar.reduce.number_of_points')}</span>
|
||||
<span class="font-normal">{currentPoints}/{maxPoints}</span>
|
||||
</Label>
|
||||
<Button variant="outline" disabled={!validSelection} on:click={reduce}>
|
||||
<Filter size="16" class="mr-1" />
|
||||
{$_('toolbar.reduce.button')}
|
||||
<Button variant="outline" disabled={!validSelection} onclick={reduce}>
|
||||
<Funnel size="16" class="mr-1" />
|
||||
{i18n._('toolbar.reduce.button')}
|
||||
</Button>
|
||||
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/minify')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/minify')}>
|
||||
{#if validSelection}
|
||||
{$_('toolbar.reduce.help')}
|
||||
{i18n._('toolbar.reduce.help')}
|
||||
{:else}
|
||||
{$_('toolbar.reduce.help_no_selection')}
|
||||
{i18n._('toolbar.reduce.help_no_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
@@ -14,10 +14,9 @@
|
||||
nauticalMilesToKilometers,
|
||||
} from '$lib/units';
|
||||
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 { _, locale } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
import {
|
||||
ListFileItem,
|
||||
@@ -183,9 +182,9 @@
|
||||
<Label for="speed" class="flex flex-row">
|
||||
<Zap size="16" class="mr-1" />
|
||||
{#if $velocityUnits === 'speed'}
|
||||
{$_('quantities.speed')}
|
||||
{i18n._('quantities.speed')}
|
||||
{:else}
|
||||
{$_('quantities.pace')}
|
||||
{i18n._('quantities.pace')}
|
||||
{/if}
|
||||
</Label>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
@@ -197,15 +196,15 @@
|
||||
min={0.01}
|
||||
disabled={!canUpdate}
|
||||
bind:value={speed}
|
||||
on:change={updateDataFromSpeed}
|
||||
onchange={updateDataFromSpeed}
|
||||
/>
|
||||
<span class="text-sm shrink-0">
|
||||
{#if $distanceUnits === 'imperial'}
|
||||
{$_('units.miles_per_hour')}
|
||||
{i18n._('units.miles_per_hour')}
|
||||
{:else if $distanceUnits === 'metric'}
|
||||
{$_('units.kilometers_per_hour')}
|
||||
{i18n._('units.kilometers_per_hour')}
|
||||
{:else if $distanceUnits === 'nautical'}
|
||||
{$_('units.knots')}
|
||||
{i18n._('units.knots')}
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
@@ -217,11 +216,11 @@
|
||||
/>
|
||||
<span class="text-sm shrink-0">
|
||||
{#if $distanceUnits === 'imperial'}
|
||||
{$_('units.minutes_per_mile')}
|
||||
{i18n._('units.minutes_per_mile')}
|
||||
{:else if $distanceUnits === 'metric'}
|
||||
{$_('units.minutes_per_kilometer')}
|
||||
{i18n._('units.minutes_per_kilometer')}
|
||||
{:else if $distanceUnits === 'nautical'}
|
||||
{$_('units.minutes_per_nautical_mile')}
|
||||
{i18n._('units.minutes_per_nautical_mile')}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -230,7 +229,7 @@
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<Label for="duration" class="flex flex-row">
|
||||
<Timer size="16" class="mr-1" />
|
||||
{$_('toolbar.time.total_time')}
|
||||
{i18n._('toolbar.time.total_time')}
|
||||
</Label>
|
||||
<TimePicker
|
||||
bind:value={movingTime}
|
||||
@@ -241,14 +240,14 @@
|
||||
</div>
|
||||
<Label class="flex flex-row">
|
||||
<CirclePlay size="16" class="mr-1" />
|
||||
{$_('toolbar.time.start')}
|
||||
{i18n._('toolbar.time.start')}
|
||||
</Label>
|
||||
<div class="flex flex-row gap-2">
|
||||
<DatePicker
|
||||
bind:value={startDate}
|
||||
disabled={!canUpdate}
|
||||
locale={get(locale) ?? 'en'}
|
||||
placeholder={$_('toolbar.time.pick_date')}
|
||||
locale={i18n.lang}
|
||||
placeholder={i18n._('toolbar.time.pick_date')}
|
||||
class="w-fit grow"
|
||||
onValueChange={async () => {
|
||||
await tick();
|
||||
@@ -266,14 +265,14 @@
|
||||
</div>
|
||||
<Label class="flex flex-row">
|
||||
<CircleStop size="16" class="mr-1" />
|
||||
{$_('toolbar.time.end')}
|
||||
{i18n._('toolbar.time.end')}
|
||||
</Label>
|
||||
<div class="flex flex-row gap-2">
|
||||
<DatePicker
|
||||
bind:value={endDate}
|
||||
disabled={!canUpdate}
|
||||
locale={get(locale) ?? 'en'}
|
||||
placeholder={$_('toolbar.time.pick_date')}
|
||||
locale={i18n.lang}
|
||||
placeholder={i18n._('toolbar.time.pick_date')}
|
||||
class="w-fit grow"
|
||||
onValueChange={async () => {
|
||||
await tick();
|
||||
@@ -293,7 +292,7 @@
|
||||
<div class="mt-0.5 flex flex-row gap-1 items-center">
|
||||
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
|
||||
<Label for="artificial-time">
|
||||
{$_('toolbar.time.artificial')}
|
||||
{i18n._('toolbar.time.artificial')}
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -303,7 +302,7 @@
|
||||
variant="outline"
|
||||
disabled={!canUpdate}
|
||||
class="grow whitespace-normal h-fit"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
let effectiveSpeed = getSpeed();
|
||||
if (
|
||||
startDate === undefined ||
|
||||
@@ -378,22 +377,24 @@
|
||||
}}
|
||||
>
|
||||
<CalendarClock size="16" class="mr-1 shrink-0" />
|
||||
{$_('toolbar.time.update')}
|
||||
{i18n._('toolbar.time.update')}
|
||||
</Button>
|
||||
<Button variant="outline" on:click={setGPXData}>
|
||||
<Button variant="outline" onclick={setGPXData}>
|
||||
<CircleX size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/time')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/time')}>
|
||||
{#if canUpdate}
|
||||
{$_('toolbar.time.help')}
|
||||
{i18n._('toolbar.time.help')}
|
||||
{:else}
|
||||
{$_('toolbar.time.help_invalid_selection')}
|
||||
{i18n._('toolbar.time.help_invalid_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
@reference "../../../../app.css";
|
||||
|
||||
div :global(input[type='time']) {
|
||||
/*
|
||||
Style copy-pasted from shadcn-svelte Input.
|
||||
|
@@ -12,7 +12,7 @@
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
import { Waypoint } from 'gpx';
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { ListWaypointItem } from '$lib/components/file-list/FileList';
|
||||
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -20,7 +20,7 @@
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { map } from '$lib/stores';
|
||||
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { MapPin, CircleX, Save } from 'lucide-svelte';
|
||||
import { MapPin, CircleX, Save } from '@lucide/svelte';
|
||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||
|
||||
let name: string;
|
||||
@@ -28,11 +28,7 @@
|
||||
let link: string;
|
||||
let longitude: number;
|
||||
let latitude: number;
|
||||
|
||||
let selectedSymbol = {
|
||||
value: '',
|
||||
label: '',
|
||||
};
|
||||
let symbolKey: string;
|
||||
|
||||
const { treeFileView } = settings;
|
||||
|
||||
@@ -70,18 +66,7 @@
|
||||
}
|
||||
link = $selectedWaypoint[0].link?.attributes?.href ?? '';
|
||||
let symbol = $selectedWaypoint[0].sym ?? '';
|
||||
let symbolKey = getSymbolKey(symbol);
|
||||
if (symbolKey) {
|
||||
selectedSymbol = {
|
||||
value: symbol,
|
||||
label: $_(`gpx.symbol.${symbolKey}`),
|
||||
};
|
||||
} else {
|
||||
selectedSymbol = {
|
||||
value: symbol,
|
||||
label: '',
|
||||
};
|
||||
}
|
||||
symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
|
||||
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
|
||||
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
|
||||
} else {
|
||||
@@ -97,10 +82,7 @@
|
||||
name = '';
|
||||
description = '';
|
||||
link = '';
|
||||
selectedSymbol = {
|
||||
value: '',
|
||||
label: '',
|
||||
};
|
||||
symbolKey = '';
|
||||
longitude = 0;
|
||||
latitude = 0;
|
||||
}
|
||||
@@ -140,7 +122,7 @@
|
||||
desc: description.length > 0 ? description : undefined,
|
||||
cmt: description.length > 0 ? description : undefined,
|
||||
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
||||
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined,
|
||||
sym: symbols[symbolKey]?.value ?? '',
|
||||
},
|
||||
$selectedWaypoint
|
||||
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
|
||||
@@ -157,7 +139,7 @@
|
||||
}
|
||||
|
||||
$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
|
||||
return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
|
||||
return i18n._(`gpx.symbol.${a[0]}`).localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
@@ -180,27 +162,31 @@
|
||||
|
||||
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
|
||||
<fieldset class="flex flex-col gap-2">
|
||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||
<Label for="name">{i18n._('menu.metadata.name')}</Label>
|
||||
<Input
|
||||
bind:value={name}
|
||||
id="name"
|
||||
class="font-semibold h-8"
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
/>
|
||||
<Label for="description">{$_('menu.metadata.description')}</Label>
|
||||
<Label for="description">{i18n._('menu.metadata.description')}</Label>
|
||||
<Textarea
|
||||
bind:value={description}
|
||||
id="description"
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
/>
|
||||
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
||||
<Select.Root bind:selected={selectedSymbol}>
|
||||
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
|
||||
<Select.Root bind:value={symbolKey} type="single">
|
||||
<Select.Trigger
|
||||
id="symbol"
|
||||
class="w-full h-8"
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
>
|
||||
<Select.Value />
|
||||
{#if symbolKey in symbols}
|
||||
{i18n._(`gpx.symbol.${symbolKey}`)}
|
||||
{:else}
|
||||
{symbolKey}
|
||||
{/if}
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||
{#each sortedSymbols as [key, symbol]}
|
||||
@@ -215,13 +201,13 @@
|
||||
{:else}
|
||||
<span class="w-4 inline-block"></span>
|
||||
{/if}
|
||||
{$_(`gpx.symbol.${key}`)}
|
||||
{i18n._(`gpx.symbol.${key}`)}
|
||||
</span>
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
||||
<Label for="link">{i18n._('toolbar.waypoint.link')}</Label>
|
||||
<Input
|
||||
bind:value={link}
|
||||
id="link"
|
||||
@@ -230,7 +216,7 @@
|
||||
/>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="grow">
|
||||
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
||||
<Label for="latitude">{i18n._('toolbar.waypoint.latitude')}</Label>
|
||||
<Input
|
||||
bind:value={latitude}
|
||||
type="number"
|
||||
@@ -243,7 +229,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
|
||||
<Label for="longitude">{i18n._('toolbar.waypoint.longitude')}</Label>
|
||||
<Input
|
||||
bind:value={longitude}
|
||||
type="number"
|
||||
@@ -262,19 +248,19 @@
|
||||
variant="outline"
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
class="grow whitespace-normal h-fit"
|
||||
on:click={createOrUpdateWaypoint}
|
||||
onclick={createOrUpdateWaypoint}
|
||||
>
|
||||
{#if $selectedWaypoint}
|
||||
<Save size="16" class="mr-1 shrink-0" />
|
||||
{$_('menu.metadata.save')}
|
||||
{i18n._('menu.metadata.save')}
|
||||
{:else}
|
||||
<MapPin size="16" class="mr-1 shrink-0" />
|
||||
{$_('toolbar.waypoint.create')}
|
||||
{i18n._('toolbar.waypoint.create')}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
selectedWaypoint.set(undefined);
|
||||
resetWaypointData();
|
||||
}}
|
||||
@@ -282,11 +268,11 @@
|
||||
<CircleX size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/poi')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
|
||||
{#if $selectedWaypoint || canCreate}
|
||||
{$_('toolbar.waypoint.help')}
|
||||
{i18n._('toolbar.waypoint.help')}
|
||||
{:else}
|
||||
{$_('toolbar.waypoint.help_no_selection')}
|
||||
{i18n._('toolbar.waypoint.help_no_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
@@ -20,13 +20,13 @@
|
||||
Repeat,
|
||||
SquareArrowUpLeft,
|
||||
SquareArrowOutDownRight,
|
||||
} from 'lucide-svelte';
|
||||
} from '@lucide/svelte';
|
||||
|
||||
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
|
||||
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
||||
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
||||
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing.svelte';
|
||||
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { RoutingControls } from './RoutingControls';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { fileObservers } from '$lib/db';
|
||||
@@ -39,7 +39,7 @@
|
||||
ListTrackSegmentItem,
|
||||
type ListItem,
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { TrackPoint } from 'gpx';
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
export let popupElement: HTMLElement | undefined = undefined;
|
||||
let selectedItem: ListItem | null = null;
|
||||
|
||||
const { privateRoads, routing } = settings;
|
||||
const { privateRoads, routing, routingProfile } = settings;
|
||||
|
||||
$: if ($map && popup && popupElement) {
|
||||
// remove controls for deleted files
|
||||
@@ -111,15 +111,12 @@
|
||||
|
||||
{#if minimizable && minimized}
|
||||
<div class="-m-1.5 -mb-2">
|
||||
<Button variant="ghost" class="px-1 h-[26px]" on:click={() => (minimized = false)}>
|
||||
<Button variant="ghost" class="px-1 h-[26px]" onclick={() => (minimized = false)}>
|
||||
<SquareArrowOutDownRight size="18" />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"
|
||||
in:flyAndScale={{ x: -2, y: 0, duration: 50 }}
|
||||
>
|
||||
<div class="flex flex-col gap-3 w-full max-w-80 animate-in animate-out {$$props.class ?? ''}">
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label class="flex flex-row justify-between items-center gap-2">
|
||||
<span class="flex flex-row items-center gap-1">
|
||||
@@ -128,9 +125,9 @@
|
||||
{:else}
|
||||
<RouteOff size="16" />
|
||||
{/if}
|
||||
{$_('toolbar.routing.use_routing')}
|
||||
{i18n._('toolbar.routing.use_routing')}
|
||||
</span>
|
||||
<Tooltip label={$_('toolbar.routing.use_routing_tooltip')}>
|
||||
<Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}>
|
||||
<Switch class="scale-90" bind:checked={$routing} />
|
||||
<Shortcut slot="extra" key="F5" />
|
||||
</Tooltip>
|
||||
@@ -139,25 +136,27 @@
|
||||
<div class="flex flex-col gap-3" in:slide>
|
||||
<Label class="flex flex-row justify-between items-center gap-2">
|
||||
<span class="shrink-0 flex flex-row items-center gap-1">
|
||||
{#if $routingProfileSelectItem.value.includes('bike') || $routingProfileSelectItem.value.includes('motorcycle')}
|
||||
{#if $routingProfile.includes('bike') || $routingProfile.includes('motorcycle')}
|
||||
<Bike size="16" />
|
||||
{:else if $routingProfileSelectItem.value.includes('foot')}
|
||||
{:else if $routingProfile.includes('foot')}
|
||||
<Footprints size="16" />
|
||||
{:else if $routingProfileSelectItem.value.includes('water')}
|
||||
{:else if $routingProfile.includes('water')}
|
||||
<Waves size="16" />
|
||||
{:else if $routingProfileSelectItem.value.includes('railway')}
|
||||
{:else if $routingProfile.includes('railway')}
|
||||
<TrainFront size="16" />
|
||||
{/if}
|
||||
{$_('toolbar.routing.activity')}
|
||||
{i18n._('toolbar.routing.activity')}
|
||||
</span>
|
||||
<Select.Root bind:selected={$routingProfileSelectItem}>
|
||||
<Select.Root type="single" bind:value={$routingProfile}>
|
||||
<Select.Trigger class="h-8 grow">
|
||||
<Select.Value />
|
||||
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.keys(brouterProfiles) as profile}
|
||||
<Select.Item value={profile}
|
||||
>{$_(`toolbar.routing.activities.${profile}`)}</Select.Item
|
||||
>{i18n._(
|
||||
`toolbar.routing.activities.${profile}`
|
||||
)}</Select.Item
|
||||
>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
@@ -166,7 +165,7 @@
|
||||
<Label class="flex flex-row justify-between items-center gap-2">
|
||||
<span class="flex flex-row gap-1">
|
||||
<TriangleAlert size="16" />
|
||||
{$_('toolbar.routing.allow_private')}
|
||||
{i18n._('toolbar.routing.allow_private')}
|
||||
</span>
|
||||
<Switch class="scale-90" bind:checked={$privateRoads} />
|
||||
</Label>
|
||||
@@ -175,20 +174,20 @@
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap justify-center gap-1">
|
||||
<ButtonWithTooltip
|
||||
label={$_('toolbar.routing.reverse.tooltip')}
|
||||
label={i18n._('toolbar.routing.reverse.tooltip')}
|
||||
variant="outline"
|
||||
class="flex flex-row gap-1 text-xs px-2"
|
||||
disabled={!validSelection}
|
||||
on:click={dbUtils.reverseSelection}
|
||||
onclick={dbUtils.reverseSelection}
|
||||
>
|
||||
<ArrowRightLeft size="12" />{$_('toolbar.routing.reverse.button')}
|
||||
<ArrowRightLeft size="12" />{i18n._('toolbar.routing.reverse.button')}
|
||||
</ButtonWithTooltip>
|
||||
<ButtonWithTooltip
|
||||
label={$_('toolbar.routing.route_back_to_start.tooltip')}
|
||||
label={i18n._('toolbar.routing.route_back_to_start.tooltip')}
|
||||
variant="outline"
|
||||
class="flex flex-row gap-1 text-xs px-2"
|
||||
disabled={!validSelection}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
const selected = getOrderedSelection();
|
||||
if (selected.length > 0) {
|
||||
const firstFileId = selected[0].getFileId();
|
||||
@@ -217,30 +216,30 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Home size="12" />{$_('toolbar.routing.route_back_to_start.button')}
|
||||
<Home size="12" />{i18n._('toolbar.routing.route_back_to_start.button')}
|
||||
</ButtonWithTooltip>
|
||||
<ButtonWithTooltip
|
||||
label={$_('toolbar.routing.round_trip.tooltip')}
|
||||
label={i18n._('toolbar.routing.round_trip.tooltip')}
|
||||
variant="outline"
|
||||
class="flex flex-row gap-1 text-xs px-2"
|
||||
disabled={!validSelection}
|
||||
on:click={dbUtils.createRoundTripForSelection}
|
||||
onclick={dbUtils.createRoundTripForSelection}
|
||||
>
|
||||
<Repeat size="12" />{$_('toolbar.routing.round_trip.button')}
|
||||
<Repeat size="12" />{i18n._('toolbar.routing.round_trip.button')}
|
||||
</ButtonWithTooltip>
|
||||
</div>
|
||||
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/routing')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/routing')}>
|
||||
{#if !validSelection}
|
||||
{$_('toolbar.routing.help_no_file')}
|
||||
{i18n._('toolbar.routing.help_no_file')}
|
||||
{:else}
|
||||
{$_('toolbar.routing.help')}
|
||||
{i18n._('toolbar.routing.help')}
|
||||
{/if}
|
||||
</Help>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="px-1 h-6"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
if (minimizable) {
|
||||
minimized = true;
|
||||
}
|
||||
|
@@ -3,9 +3,9 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||
import { canChangeStart } from './RoutingControls';
|
||||
import { CirclePlay, Trash2 } from 'lucide-svelte';
|
||||
import { CirclePlay, Trash2 } from '@lucide/svelte';
|
||||
|
||||
import { _ } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
|
||||
export let element: HTMLElement;
|
||||
</script>
|
||||
@@ -17,19 +17,19 @@
|
||||
<Button
|
||||
class="w-full px-2 py-1 h-6 justify-start"
|
||||
variant="ghost"
|
||||
on:click={() => element.dispatchEvent(new CustomEvent('change-start'))}
|
||||
onclick={() => element.dispatchEvent(new CustomEvent('change-start'))}
|
||||
>
|
||||
<CirclePlay size="16" class="mr-1" />
|
||||
{$_('toolbar.routing.start_loop_here')}
|
||||
{i18n._('toolbar.routing.start_loop_here')}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
class="w-full px-2 py-1 h-6 justify-start"
|
||||
variant="ghost"
|
||||
on:click={() => element.dispatchEvent(new CustomEvent('delete'))}
|
||||
onclick={() => element.dispatchEvent(new CustomEvent('delete'))}
|
||||
>
|
||||
<Trash2 size="16" class="mr-1" />
|
||||
{$_('menu.delete')}
|
||||
{i18n._('menu.delete')}
|
||||
<Shortcut shift={true} click={true} />
|
||||
</Button>
|
||||
</Card.Content>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
|
||||
import { get, writable, type Readable } from 'svelte/store';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { route } from './Routing';
|
||||
import { route } from './routing.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { dbUtils, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
|
||||
import {
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { currentTool, streetViewEnabled, Tool } from '$lib/stores';
|
||||
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils';
|
||||
|
||||
const { streetViewSource } = settings;
|
||||
// const { streetViewSource } = settings;
|
||||
export const canChangeStart = writable(false);
|
||||
|
||||
function stopPropagation(e: any) {
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import type { Coordinates } from 'gpx';
|
||||
import { TrackPoint, distance } from 'gpx';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import { settings } from '$lib/db';
|
||||
import { _, locale, isLoadingLocale } from '$lib/i18n';
|
||||
import { settings } from '$lib/logic/settings.svelte';
|
||||
import { getElevation } from '$lib/utils';
|
||||
|
||||
const { routing, routingProfile, privateRoads } = settings;
|
||||
@@ -17,38 +15,10 @@ export const brouterProfiles: { [key: string]: string } = {
|
||||
water: 'river',
|
||||
railway: 'rail',
|
||||
};
|
||||
export const routingProfileSelectItem = writable({
|
||||
value: '',
|
||||
label: '',
|
||||
});
|
||||
|
||||
derived([routingProfile, locale, isLoadingLocale], ([profile, l, i]) => [profile, l, i]).subscribe(
|
||||
([profile, l, i]) => {
|
||||
if (
|
||||
!i &&
|
||||
profile !== '' &&
|
||||
(profile !== get(routingProfileSelectItem).value ||
|
||||
get(_)(`toolbar.routing.activities.${profile}`) !==
|
||||
get(routingProfileSelectItem).label) &&
|
||||
l !== null
|
||||
) {
|
||||
routingProfileSelectItem.update((item) => {
|
||||
item.value = profile;
|
||||
item.label = get(_)(`toolbar.routing.activities.${profile}`);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
routingProfileSelectItem.subscribe((item) => {
|
||||
if (item.value !== '' && item.value !== get(routingProfile)) {
|
||||
routingProfile.set(item.value);
|
||||
}
|
||||
});
|
||||
|
||||
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||
if (get(routing)) {
|
||||
return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
|
||||
if (routing.value) {
|
||||
return getRoute(points, brouterProfiles[routingProfile.value], privateRoads.value);
|
||||
} else {
|
||||
return getIntermediatePoints(points);
|
||||
}
|
@@ -1,12 +1,5 @@
|
||||
<script lang="ts" context="module">
|
||||
export enum SplitType {
|
||||
FILES = 'files',
|
||||
TRACKS = 'tracks',
|
||||
SEGMENTS = 'segments',
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { splitAs, SplitType } from '$lib/components/toolbar/tools/scissors/utils.svelte';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { ListRootItem } from '$lib/components/file-list/FileList';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
@@ -15,31 +8,39 @@
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
|
||||
import { map } from '$lib/components/map/map.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { _, locale } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { onDestroy, tick } from 'svelte';
|
||||
import { Crop } from 'lucide-svelte';
|
||||
import { Crop } from '@lucide/svelte';
|
||||
import { dbUtils } from '$lib/db';
|
||||
import { SplitControls } from './SplitControls';
|
||||
import { SplitControls } from './SplitControls.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
|
||||
let props: {
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let splitControls: SplitControls | undefined = undefined;
|
||||
let canCrop = false;
|
||||
let canCrop = $state(false);
|
||||
|
||||
$: if ($map) {
|
||||
if (splitControls) {
|
||||
splitControls.destroy();
|
||||
$effect(() => {
|
||||
if (map.current) {
|
||||
if (splitControls) {
|
||||
splitControls.destroy();
|
||||
}
|
||||
splitControls = new SplitControls(map.current);
|
||||
}
|
||||
splitControls = new SplitControls($map);
|
||||
}
|
||||
});
|
||||
|
||||
$: validSelection =
|
||||
let validSelection = $derived(
|
||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||
$gpxStatistics.local.points.length > 0;
|
||||
$gpxStatistics.local.points.length > 0
|
||||
);
|
||||
|
||||
let maxSliderValue = 1;
|
||||
let sliderValues = [0, 1];
|
||||
let maxSliderValue = $state(1);
|
||||
let sliderValues = $state([0, 1]);
|
||||
|
||||
function updateCanCrop() {
|
||||
canCrop = sliderValues[0] != 0 || sliderValues[1] != maxSliderValue;
|
||||
@@ -73,32 +74,29 @@
|
||||
sliderValues = [0, maxSliderValue];
|
||||
}
|
||||
|
||||
$: if ($gpxStatistics.local.points.length - 1 != maxSliderValue) {
|
||||
updateSliderLimits();
|
||||
}
|
||||
$effect(() => {
|
||||
if ($gpxStatistics.local.points.length - 1 != maxSliderValue) {
|
||||
updateSliderLimits();
|
||||
}
|
||||
});
|
||||
|
||||
$: if (sliderValues) {
|
||||
updateCanCrop();
|
||||
updateSlicedGPXStatistics();
|
||||
}
|
||||
$effect(() => {
|
||||
if (sliderValues) {
|
||||
updateCanCrop();
|
||||
updateSlicedGPXStatistics();
|
||||
}
|
||||
});
|
||||
|
||||
$: if (
|
||||
$slicedGPXStatistics !== undefined &&
|
||||
($slicedGPXStatistics[1] !== sliderValues[0] || $slicedGPXStatistics[2] !== sliderValues[1])
|
||||
) {
|
||||
updateSliderValues();
|
||||
updateCanCrop();
|
||||
}
|
||||
|
||||
const splitTypes = [
|
||||
{ value: SplitType.FILES, label: $_('gpx.files') },
|
||||
{ value: SplitType.TRACKS, label: $_('gpx.tracks') },
|
||||
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') },
|
||||
];
|
||||
|
||||
let splitType = splitTypes.find((type) => type.value === $splitAs) ?? splitTypes[0];
|
||||
|
||||
$: splitAs.set(splitType.value);
|
||||
$effect(() => {
|
||||
if (
|
||||
$slicedGPXStatistics !== undefined &&
|
||||
($slicedGPXStatistics[1] !== sliderValues[0] ||
|
||||
$slicedGPXStatistics[2] !== sliderValues[1])
|
||||
) {
|
||||
updateSliderValues();
|
||||
updateCanCrop();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
$slicedGPXStatistics = undefined;
|
||||
@@ -109,43 +107,44 @@
|
||||
});
|
||||
</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">
|
||||
<Slider
|
||||
bind:value={sliderValues}
|
||||
max={maxSliderValue}
|
||||
step={1}
|
||||
type="multiple"
|
||||
disabled={!validSelection}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!validSelection || !canCrop}
|
||||
on:click={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
|
||||
onclick={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
|
||||
>
|
||||
<Crop size="16" class="mr-1" />{$_('toolbar.scissors.crop')}
|
||||
<Crop size="16" class="mr-1" />{i18n._('toolbar.scissors.crop')}
|
||||
</Button>
|
||||
<Separator />
|
||||
<Label class="flex flex-row flex-wrap gap-3 items-center">
|
||||
<span class="shrink-0">
|
||||
{$_('toolbar.scissors.split_as')}
|
||||
{i18n._('toolbar.scissors.split_as')}
|
||||
</span>
|
||||
<Select.Root bind:selected={splitType}>
|
||||
<Select.Root bind:value={splitAs.current} type="single">
|
||||
<Select.Trigger class="h-8 w-fit grow">
|
||||
<Select.Value />
|
||||
{i18n._('gpx.' + splitAs)}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each splitTypes as { value, label }}
|
||||
<Select.Item {value}>{label}</Select.Item>
|
||||
{#each Object.values(SplitType) as splitType}
|
||||
<Select.Item value={splitType}>{i18n._('gpx.' + splitType)}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Label>
|
||||
<Help link={getURLForLanguage($locale, '/help/toolbar/scissors')}>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/scissors')}>
|
||||
{#if validSelection}
|
||||
{$_('toolbar.scissors.help')}
|
||||
{i18n._('toolbar.scissors.help')}
|
||||
{:else}
|
||||
{$_('toolbar.scissors.help_invalid_selection')}
|
||||
{i18n._('toolbar.scissors.help_invalid_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
||||
|
@@ -7,8 +7,9 @@ import {
|
||||
selection,
|
||||
} from '$lib/components/file-list/Selection';
|
||||
import { ListTrackSegmentItem } from '$lib/components/file-list/FileList';
|
||||
import { currentTool, gpxStatistics, Tool } from '$lib/stores';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { gpxStatistics } from '$lib/stores';
|
||||
import { tool, Tool } from '$lib/components/toolbar/utils.svelte';
|
||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/utils.svelte';
|
||||
import { Scissors } from 'lucide-static';
|
||||
|
||||
export class SplitControls {
|
||||
@@ -26,11 +27,13 @@ export class SplitControls {
|
||||
|
||||
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
|
||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||
$effect(() => {
|
||||
tool.current, this.addIfNeeded.bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
addIfNeeded() {
|
||||
let scissors = get(currentTool) === Tool.SCISSORS;
|
||||
let scissors = tool.current === Tool.SCISSORS;
|
||||
if (!scissors) {
|
||||
if (this.active) {
|
||||
this.remove();
|
||||
@@ -166,6 +169,7 @@ export class SplitControls {
|
||||
marker.getElement().addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dbUtils.split(
|
||||
splitAs.current,
|
||||
control.fileId,
|
||||
control.trackIndex,
|
||||
control.segmentIndex,
|
@@ -0,0 +1,9 @@
|
||||
export enum SplitType {
|
||||
FILES = 'files',
|
||||
TRACKS = 'tracks',
|
||||
SEGMENTS = 'segments',
|
||||
}
|
||||
|
||||
export let splitAs = $state({
|
||||
current: SplitType.FILES,
|
||||
});
|
Reference in New Issue
Block a user