This commit is contained in:
vcoppe
2025-10-05 19:34:05 +02:00
parent 1cc07901f6
commit 0733562c0d
70 changed files with 2641 additions and 2968 deletions

View File

@@ -15,21 +15,25 @@
import { onDestroy, onMount } from 'svelte';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
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 { map } from '$lib/components/map/utils.svelte';
import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let cleanType = CleanType.INSIDE;
let deleteTrackpoints = true;
let deleteWaypoints = true;
let rectangleCoordinates: mapboxgl.LngLat[] = [];
let props: {
class?: string;
} = $props();
function updateRectangle() {
if (map.current) {
let cleanType = $state(CleanType.INSIDE);
let deleteTrackpoints = $state(true);
let deleteWaypoints = $state(true);
let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
$effect(() => {
if (map.value) {
if (rectangleCoordinates.length != 2) {
if (map.current.getLayer('rectangle')) {
map.current.removeLayer('rectangle');
if (map.value.getLayer('rectangle')) {
map.value.removeLayer('rectangle');
}
} else {
let data: GeoJSON.Feature = {
@@ -48,17 +52,17 @@
},
properties: {},
};
let source: GeoJSONSource | undefined = map.current.getSource('rectangle');
let source: GeoJSONSource | undefined = map.value.getSource('rectangle');
if (source) {
source.setData(data);
} else {
map.current.addSource('rectangle', {
map.value.addSource('rectangle', {
type: 'geojson',
data: data,
});
}
if (!map.current.getLayer('rectangle')) {
map.current.addLayer({
if (!map.value.getLayer('rectangle')) {
map.value.addLayer({
id: 'rectangle',
type: 'fill',
source: 'rectangle',
@@ -70,11 +74,7 @@
}
}
}
}
$: if (rectangleCoordinates) {
updateRectangle();
}
});
let mousedown = false;
function onMouseDown(e: any) {
@@ -93,42 +93,42 @@
}
onMount(() => {
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.value) {
setCrosshairCursor(map.value.getCanvas());
map.value.on('mousedown', onMouseDown);
map.value.on('mousemove', onMouseMove);
map.value.on('mouseup', onMouseUp);
map.value.on('touchstart', onMouseDown);
map.value.on('touchmove', onMouseMove);
map.value.on('touchend', onMouseUp);
map.value.dragPan.disable();
}
});
onDestroy(() => {
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.value) {
resetCursor(map.value.getCanvas());
map.value.off('mousedown', onMouseDown);
map.value.off('mousemove', onMouseMove);
map.value.off('mouseup', onMouseUp);
map.value.off('touchstart', onMouseDown);
map.value.off('touchmove', onMouseMove);
map.value.off('touchend', onMouseUp);
map.value.dragPan.enable();
if (map.current.getLayer('rectangle')) {
map.current.removeLayer('rectangle');
if (map.value.getLayer('rectangle')) {
map.value.removeLayer('rectangle');
}
if (map.current.getSource('rectangle')) {
map.current.removeSource('rectangle');
if (map.value.getSource('rectangle')) {
map.value.removeSource('rectangle');
}
}
});
$: validSelection = $selection.size > 0;
let validSelection = $derived(selection.value.size > 0);
</script>
<div class="flex flex-col gap-3 w-full max-w-80 items-center {$$props.class ?? ''}">
<div class="flex flex-col gap-3 w-full max-w-80 items-center {props.class ?? ''}">
<fieldset class="flex flex-col gap-3">
<div class="flex flex-row items-center gap-[6.4px] h-3">
<Checkbox id="delete-trkpt" bind:checked={deleteTrackpoints} class="scale-90" />
@@ -158,7 +158,7 @@
class="w-full"
disabled={!validSelection || rectangleCoordinates.length != 2}
onclick={() => {
dbUtils.cleanSelection(
fileActions.cleanSelection(
[
{
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),

View File

@@ -1,18 +1,18 @@
<script lang="ts">
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 { dbUtils } from '$lib/db';
import { map } from '$lib/components/map/map.svelte';
import { map } from '$lib/components/map/utils.svelte';
import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let props: {
class?: string;
} = $props();
let validSelection = $derived($selection.size > 0);
let validSelection = $derived(selection.value.size > 0);
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
@@ -21,8 +21,8 @@
class="whitespace-normal h-fit"
disabled={!validSelection}
onclick={async () => {
if (map.current) {
dbUtils.addElevationToSelection(map.current);
if (map.value) {
fileActions.addElevationToSelection(map.value);
}
}}
>

View File

@@ -1,45 +1,51 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Ungroup } from '@lucide/svelte';
import { selection } from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListWaypointsItem,
} from '$lib/components/file-list/FileList';
} from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { dbUtils, getFile } from '$lib/db';
import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
$: validSelection =
$selection.size > 0 &&
$selection.getSelected().every((item) => {
if (
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem ||
item instanceof ListTrackSegmentItem
) {
return false;
}
let file = getFile(item.getFileId());
if (file) {
if (item instanceof ListFileItem) {
return file.getSegments().length > 1;
} else if (item instanceof ListTrackItem) {
if (item.getTrackIndex() < file.trk.length) {
return file.trk[item.getTrackIndex()].getSegments().length > 1;
let props: {
class?: string;
} = $props();
let validSelection = $derived(
selection.value.size > 0 &&
selection.value.getSelected().every((item) => {
if (
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem ||
item instanceof ListTrackSegmentItem
) {
return false;
}
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
if (item instanceof ListFileItem) {
return file.getSegments().length > 1;
} else if (item instanceof ListTrackItem) {
if (item.getTrackIndex() < file.trk.length) {
return file.trk[item.getTrackIndex()].getSegments().length > 1;
}
}
}
}
return false;
});
return false;
})
);
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<Button variant="outline" disabled={!validSelection} onclick={dbUtils.extractSelection}>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<Button variant="outline" disabled={!validSelection} onclick={fileActions.extractSelection}>
<Ungroup size="16" class="mr-1" />
{i18n._('toolbar.extract.button')}
</Button>

View File

@@ -6,58 +6,58 @@
</script>
<script lang="ts">
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { selection } from '$lib/components/file-list/Selection';
import { Button } from '$lib/components/ui/button';
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 { i18n } from '$lib/i18n.svelte';
import { dbUtils, getFile } from '$lib/db';
import { Group } from '@lucide/svelte';
import { getURLForLanguage } from '$lib/utils';
import Shortcut from '$lib/components/Shortcut.svelte';
import { gpxStatistics } from '$lib/stores';
import { selection } from '$lib/logic/selection.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let canMergeTraces = false;
let canMergeContents = false;
let removeGaps = false;
let props: {
class?: string;
} = $props();
$: if ($selection.size > 1) {
canMergeTraces = true;
} else if ($selection.size === 1) {
let selected = $selection.getSelected()[0];
if (selected instanceof ListFileItem) {
let file = getFile(selected.getFileId());
if (file) {
canMergeTraces = file.getSegments().length > 1;
} else {
canMergeTraces = false;
let canMergeTraces = $derived.by(() => {
if (selection.value.size > 1) {
return true;
} else if (selection.value.size === 1) {
let selected = selection.value.getSelected()[0];
if (selected instanceof ListFileItem) {
let file = fileStateCollection.getFile(selected.getFileId());
if (file) {
return file.getSegments().length > 1;
}
} else if (selected instanceof ListTrackItem) {
let trackIndex = selected.getTrackIndex();
let file = fileStateCollection.getFile(selected.getFileId());
if (file && trackIndex < file.trk.length) {
return file.trk[trackIndex].getSegments().length > 1;
}
}
} else if (selected instanceof ListTrackItem) {
let trackIndex = selected.getTrackIndex();
let file = getFile(selected.getFileId());
if (file && trackIndex < file.trk.length) {
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
} else {
canMergeTraces = false;
}
} else {
canMergeContents = false;
return false;
}
}
});
$: canMergeContents =
$selection.size > 1 &&
$selection
.getSelected()
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
let canMergeContents = $derived(
selection.value.size > 1 &&
selection.value
.getSelected()
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
);
let mergeType = MergeType.TRACES;
let removeGaps = $state(false);
let mergeType = $state(MergeType.TRACES);
</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}>
<Label class="flex flex-row items-center gap-1.5 leading-5">
<RadioGroup.Item value={MergeType.TRACES} />
@@ -80,7 +80,7 @@
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
onclick={() => {
dbUtils.mergeSelection(
fileActions.mergeSelection(
mergeType === MergeType.TRACES,
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
);

View File

@@ -2,22 +2,22 @@
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import { selection } from '$lib/components/file-list/Selection';
import {
ListItem,
ListRootItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
} from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
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/components/map/map.svelte';
import { map } from '$lib/components/map/utils.svelte';
import { onDestroy } from 'svelte';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { getURLForLanguage } from '$lib/utils';
import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let props: { class?: string } = $props();
@@ -28,7 +28,7 @@
const maxTolerance = 10000;
let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
let tolerance = $derived(
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
@@ -67,18 +67,18 @@
});
});
if (map.current) {
let source: GeoJSONSource | undefined = map.current.getSource('simplified');
if (map.value) {
let source: GeoJSONSource | undefined = map.value.getSource('simplified');
if (source) {
source.setData(data);
} else {
map.current.addSource('simplified', {
map.value.addSource('simplified', {
type: 'geojson',
data: data,
});
}
if (!map.current.getLayer('simplified')) {
map.current.addLayer({
if (!map.value.getLayer('simplified')) {
map.value.addLayer({
id: 'simplified',
type: 'line',
source: 'simplified',
@@ -88,52 +88,52 @@
},
});
} else {
map.current.moveLayer('simplified');
map.value.moveLayer('simplified');
}
}
}
$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);
}
});
}
});
// $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);
// }
// });
// }
// });
$effect(() => {
if (tolerance) {
@@ -142,12 +142,12 @@
});
onDestroy(() => {
if (map.current) {
if (map.current.getLayer('simplified')) {
map.current.removeLayer('simplified');
if (map.value) {
if (map.value.getLayer('simplified')) {
map.value.removeLayer('simplified');
}
if (map.current.getSource('simplified')) {
map.current.removeSource('simplified');
if (map.value.getSource('simplified')) {
map.value.removeSource('simplified');
}
}
unsubscribes.forEach((unsubscribe) => unsubscribe());
@@ -164,7 +164,7 @@
.map((point) => point.point)
);
});
dbUtils.reduce(itemsAndPoints);
fileActions.reduce(itemsAndPoints);
}
</script>

View File

@@ -5,7 +5,6 @@
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
import { dbUtils, settings } from '$lib/db';
import { gpxStatistics } from '$lib/stores';
import {
distancePerHourToSecondsPerDistance,
@@ -17,15 +16,22 @@
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from '@lucide/svelte';
import { tick } from 'svelte';
import { i18n } from '$lib/i18n.svelte';
import { selection } from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
} from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
import { settings } from '$lib/logic/settings.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
import { fileActionManager } from '$lib/logic/file-action-manager.svelte';
let props: {
class?: string;
} = $props();
let startDate: DateValue | undefined = undefined;
let startTime: string | undefined = undefined;
@@ -47,7 +53,7 @@
function setSpeed(value: number) {
let speedValue = getConvertedVelocity(value);
if ($velocityUnits === 'speed') {
if (velocityUnits.value === 'speed') {
speedValue = parseFloat(speedValue.toFixed(2));
}
speed = speedValue;
@@ -80,9 +86,9 @@
}
}
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
setGPXData();
}
// $: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
// setGPXData();
// }
function getDate(date: DateValue, time: string): Date {
if (date === undefined) {
@@ -133,12 +139,12 @@
}
let speedValue = speed;
if ($velocityUnits === 'pace') {
if (velocityUnits.value === 'pace') {
speedValue = distancePerHourToSecondsPerDistance(speed);
}
if ($distanceUnits === 'imperial') {
if (distanceUnits.value === 'imperial') {
speedValue = milesToKilometers(speedValue);
} else if ($distanceUnits === 'nautical') {
} else if (distanceUnits.value === 'nautical') {
speedValue = nauticalMilesToKilometers(speedValue);
}
return speedValue;
@@ -171,24 +177,26 @@
updateEnd();
}
$: canUpdate =
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
let canUpdate = $derived(
selection.value.size === 1 &&
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
</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">
<div class="flex flex-row gap-2 justify-center">
<div class="flex flex-col gap-2 grow">
<Label for="speed" class="flex flex-row">
<Zap size="16" class="mr-1" />
{#if $velocityUnits === 'speed'}
{#if velocityUnits.value === 'speed'}
{i18n._('quantities.speed')}
{:else}
{i18n._('quantities.pace')}
{/if}
</Label>
<div class="flex flex-row gap-1 items-center">
{#if $velocityUnits === 'speed'}
{#if velocityUnits.value === 'speed'}
<Input
id="speed"
type="number"
@@ -199,11 +207,11 @@
onchange={updateDataFromSpeed}
/>
<span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'}
{#if distanceUnits.value === 'imperial'}
{i18n._('units.miles_per_hour')}
{:else if $distanceUnits === 'metric'}
{:else if distanceUnits.value === 'metric'}
{i18n._('units.kilometers_per_hour')}
{:else if $distanceUnits === 'nautical'}
{:else if distanceUnits.value === 'nautical'}
{i18n._('units.knots')}
{/if}
</span>
@@ -215,11 +223,11 @@
onChange={updateDataFromSpeed}
/>
<span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'}
{#if distanceUnits.value === 'imperial'}
{i18n._('units.minutes_per_mile')}
{:else if $distanceUnits === 'metric'}
{:else if distanceUnits.value === 'metric'}
{i18n._('units.minutes_per_kilometer')}
{:else if $distanceUnits === 'nautical'}
{:else if distanceUnits.value === 'nautical'}
{i18n._('units.minutes_per_nautical_mile')}
{/if}
</span>
@@ -260,7 +268,7 @@
disabled={!canUpdate}
bind:value={startTime}
class="w-fit"
on:change={updateEnd}
onchange={updateEnd}
/>
</div>
<Label class="flex flex-row">
@@ -285,7 +293,7 @@
disabled={!canUpdate}
bind:value={endTime}
class="w-fit"
on:change={updateStart}
onchange={updateStart}
/>
</div>
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
@@ -324,9 +332,9 @@
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
}
let item = $selection.getSelected()[0];
let item = selection.value.getSelected()[0];
let fileId = item.getFileId();
dbUtils.applyToFile(fileId, (file) => {
fileActionManager.applyToFile(fileId, (file) => {
if (item instanceof ListFileItem) {
if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps(

View File

@@ -21,65 +21,75 @@
SquareArrowUpLeft,
SquareArrowOutDownRight,
} from '@lucide/svelte';
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing.svelte';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/utils.svelte';
import { i18n } from '$lib/i18n.svelte';
import { RoutingControls } from './RoutingControls';
import mapboxgl from 'mapbox-gl';
import { fileObservers } from '$lib/db';
// import { RoutingControls } from './RoutingControls';
import { slide } from 'svelte/transition';
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
type ListItem,
} from '$lib/components/file-list/FileList';
} from '$lib/components/file-list/file-list';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { onDestroy, onMount } from 'svelte';
import { TrackPoint } from 'gpx';
import { settings } from '$lib/logic/settings.svelte';
import { map } from '$lib/components/map/utils.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions.svelte';
let {
minimized = $bindable(false),
minimizable = true,
popup = undefined,
popupElement = undefined,
class: className = '',
}: {
minimized?: boolean;
minimizable?: boolean;
popup?: mapboxgl.Popup;
popupElement?: HTMLDivElement;
class?: string;
} = $props();
export let minimized = false;
export let minimizable = true;
export let popup: mapboxgl.Popup | undefined = undefined;
export let popupElement: HTMLElement | undefined = undefined;
let selectedItem: ListItem | null = null;
const { privateRoads, routing, routingProfile } = settings;
$: if ($map && popup && popupElement) {
// remove controls for deleted files
routingControls.forEach((controls, fileId) => {
if (!$fileObservers.has(fileId)) {
controls.destroy();
routingControls.delete(fileId);
// $: if (map && popup && popupElement) {
// // remove controls for deleted files
// routingControls.forEach((controls, fileId) => {
// if (!$fileObservers.has(fileId)) {
// controls.destroy();
// routingControls.delete(fileId);
if (selectedItem && selectedItem.getFileId() === fileId) {
selectedItem = null;
}
} else if ($map !== controls.map) {
controls.updateMap($map);
}
});
// add controls for new files
$fileObservers.forEach((file, fileId) => {
if (!routingControls.has(fileId)) {
routingControls.set(
fileId,
new RoutingControls($map, fileId, file, popup, popupElement)
);
}
});
}
// if (selectedItem && selectedItem.getFileId() === fileId) {
// selectedItem = null;
// }
// } else if ($map !== controls.map) {
// controls.updateMap($map);
// }
// });
// // add controls for new files
// fileStateCollection.files.forEach((file, fileId) => {
// if (!routingControls.has(fileId)) {
// routingControls.set(
// fileId,
// new RoutingControls($map, fileId, file, popup, popupElement)
// );
// }
// });
// }
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
let validSelection = $derived(
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
function createFileWithPoint(e: any) {
if ($selection.size === 0) {
if (selection.value.size === 0) {
let file = newGPXFile();
file.replaceTrackPoints(0, 0, 0, 0, [
new TrackPoint({
@@ -90,22 +100,22 @@
}),
]);
file._data.id = getFileIds(1)[0];
dbUtils.add(file);
selectFileWhenLoaded(file._data.id);
fileActions.add(file);
// selectFileWhenLoaded(file._data.id);
}
}
onMount(() => {
setCrosshairCursor();
$map?.on('click', createFileWithPoint);
// setCrosshairCursor();
map.value?.on('click', createFileWithPoint);
});
onDestroy(() => {
resetCursor();
$map?.off('click', createFileWithPoint);
// resetCursor();
map.value?.off('click', createFileWithPoint);
routingControls.forEach((controls) => controls.destroy());
routingControls.clear();
// routingControls.forEach((controls) => controls.destroy());
// routingControls.clear();
});
</script>
@@ -116,11 +126,11 @@
</Button>
</div>
{:else}
<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 w-full max-w-80 animate-in animate-out {className ?? ''}">
<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">
{#if $routing}
{#if routing.value}
<Route size="16" />
{:else}
<RouteOff size="16" />
@@ -128,28 +138,28 @@
{i18n._('toolbar.routing.use_routing')}
</span>
<Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}>
<Switch class="scale-90" bind:checked={$routing} />
<Switch class="scale-90" bind:checked={routing.value} />
<Shortcut slot="extra" key="F5" />
</Tooltip>
</Label>
{#if $routing}
{#if routing.value}
<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 $routingProfile.includes('bike') || $routingProfile.includes('motorcycle')}
{#if routingProfile.value.includes('bike') || routingProfile.value.includes('motorcycle')}
<Bike size="16" />
{:else if $routingProfile.includes('foot')}
{:else if routingProfile.value.includes('foot')}
<Footprints size="16" />
{:else if $routingProfile.includes('water')}
{:else if routingProfile.value.includes('water')}
<Waves size="16" />
{:else if $routingProfile.includes('railway')}
{:else if routingProfile.value.includes('railway')}
<TrainFront size="16" />
{/if}
{i18n._('toolbar.routing.activity')}
</span>
<Select.Root type="single" bind:value={$routingProfile}>
<Select.Root type="single" bind:value={routingProfile.value}>
<Select.Trigger class="h-8 grow">
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
{i18n._(`toolbar.routing.activities.${routingProfile.value}`)}
</Select.Trigger>
<Select.Content>
{#each Object.keys(brouterProfiles) as profile}
@@ -167,7 +177,7 @@
<TriangleAlert size="16" />
{i18n._('toolbar.routing.allow_private')}
</span>
<Switch class="scale-90" bind:checked={$privateRoads} />
<Switch class="scale-90" bind:checked={privateRoads.value} />
</Label>
</div>
{/if}
@@ -178,7 +188,7 @@
variant="outline"
class="flex flex-row gap-1 text-xs px-2"
disabled={!validSelection}
onclick={dbUtils.reverseSelection}
onclick={fileActions.reverseSelection}
>
<ArrowRightLeft size="12" />{i18n._('toolbar.routing.reverse.button')}
</ButtonWithTooltip>
@@ -188,10 +198,10 @@
class="flex flex-row gap-1 text-xs px-2"
disabled={!validSelection}
onclick={() => {
const selected = getOrderedSelection();
const selected = selection.getOrderedSelection();
if (selected.length > 0) {
const firstFileId = selected[0].getFileId();
const firstFile = getFile(firstFileId);
const firstFile = fileStateCollection.getFile(firstFileId);
if (firstFile) {
let start = (() => {
if (selected[0] instanceof ListFileItem) {
@@ -208,9 +218,9 @@
if (start !== undefined) {
const lastFileId = selected[selected.length - 1].getFileId();
routingControls
.get(lastFileId)
?.appendAnchorWithCoordinates(start.getCoordinates());
// routingControls
// .get(lastFileId)
// ?.appendAnchorWithCoordinates(start.getCoordinates());
}
}
}

View File

@@ -1,18 +1,15 @@
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.svelte';
import { route } from './utils.svelte';
import { toast } from 'svelte-sonner';
import { i18n } from '$lib/i18n.svelte';
import { dbUtils, settings, type GPXFileWithStatistics } from '$lib/db';
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListTrackItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
import { currentTool, streetViewEnabled, Tool } from '$lib/stores';
} from '$lib/components/file-list/file-list';
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils';
import type { GPXFileWithStatistics } from '$lib/logic/statistics';
// const { streetViewSource } = settings;
export const canChangeStart = writable(false);

View File

@@ -1,8 +1,7 @@
<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';
import { ListRootItem } from '$lib/components/file-list/file-list';
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
@@ -15,8 +14,9 @@
import { onDestroy, tick } from 'svelte';
import { Crop } from '@lucide/svelte';
import { dbUtils } from '$lib/db';
import { SplitControls } from './SplitControls.svelte';
import { SplitControls } from './split-controls';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
let props: {
class?: string;
@@ -35,7 +35,7 @@
});
let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
$gpxStatistics.local.points.length > 0
);

View File

@@ -1,16 +1,12 @@
import { TrackPoint, TrackSegment } from 'gpx';
import { get } from 'svelte/store';
import mapboxgl from 'mapbox-gl';
import { dbUtils, getFile } from '$lib/db';
import {
applyToOrderedSelectedItemsFromFile,
selection,
} from '$lib/components/file-list/Selection';
import { ListTrackSegmentItem } from '$lib/components/file-list/FileList';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
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';
import { applyToOrderedSelectedItemsFromFile, selection } from '$lib/logic/selection.svelte';
export class SplitControls {
active: boolean = false;
@@ -25,10 +21,9 @@ export class SplitControls {
constructor(map: mapboxgl.Map) {
this.map = map;
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
$effect(() => {
tool.current, this.addIfNeeded.bind(this);
tool.current, selection.value, this.addIfNeeded.bind(this);
});
}
@@ -64,7 +59,7 @@ export class SplitControls {
if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (
get(selection).hasAnyParent(
selection.value.hasAnyParent(
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
)
) {

View File

@@ -1,19 +1,11 @@
<script lang="ts" context="module">
import { writable } from 'svelte/store';
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
</script>
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { selection } from '$lib/components/file-list/Selection';
import { Waypoint } from 'gpx';
import { i18n } from '$lib/i18n.svelte';
import { ListWaypointItem } from '$lib/components/file-list/FileList';
import { ListWaypointItem } from '$lib/components/file-list/file-list';
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
import { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte';
@@ -22,61 +14,21 @@
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { MapPin, CircleX, Save } from '@lucide/svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection } from '$lib/logic/selection.svelte';
import { selectedWaypoint } from './utils.svelte';
let name: string;
let description: string;
let link: string;
let longitude: number;
let latitude: number;
let symbolKey: string;
let props: {
class?: string;
} = $props();
const { treeFileView } = settings;
let name = $state('');
let description = $state('');
let link = $state('');
let symbolKey = $state('');
let longitude = $state(0);
let latitude = $state(0);
$: canCreate = $selection.size > 0;
$: if ($treeFileView && $selection) {
selectedWaypoint.update(() => {
if ($selection.size === 1) {
let item = $selection.getSelected()[0];
if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId());
let waypoint = file?.wpt[item.getWaypointIndex()];
if (waypoint) {
return [waypoint, item.getFileId()];
}
}
}
return undefined;
});
}
let unsubscribe: (() => void) | undefined = undefined;
function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
if ($selectedWaypoint) {
if (fileStore) {
if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
name = $selectedWaypoint[0].name ?? '';
description = $selectedWaypoint[0].desc ?? '';
if (
$selectedWaypoint[0].cmt !== undefined &&
$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
) {
description += '\n\n' + $selectedWaypoint[0].cmt;
}
link = $selectedWaypoint[0].link?.attributes?.href ?? '';
let symbol = $selectedWaypoint[0].sym ?? '';
symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
} else {
selectedWaypoint.set(undefined);
}
} else {
selectedWaypoint.set(undefined);
}
}
}
let canCreate = $derived(selection.value.size > 0);
function resetWaypointData() {
name = '';
@@ -87,21 +39,6 @@
latitude = 0;
}
$: {
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
}
if ($selectedWaypoint) {
let fileStore = get(fileObservers).get($selectedWaypoint[1]);
if (fileStore) {
unsubscribe = fileStore.subscribe(updateWaypointData);
}
} else {
resetWaypointData();
}
}
function createOrUpdateWaypoint() {
if (typeof latitude === 'string') {
latitude = parseFloat(latitude);
@@ -124,12 +61,12 @@
link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: symbols[symbolKey]?.value ?? '',
},
$selectedWaypoint
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
selectedWaypoint.wpt && selectedWaypoint.fileId
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
: undefined
);
selectedWaypoint.set(undefined);
selectedWaypoint.reset();
resetWaypointData();
}
@@ -138,49 +75,46 @@
longitude = e.lngLat.lng.toFixed(6);
}
$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
return i18n._(`gpx.symbol.${a[0]}`).localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
});
let sortedSymbols = $derived(
Object.entries(symbols).sort((a, b) => {
return i18n
._(`gpx.symbol.${a[0]}`)
.localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
})
);
onMount(() => {
let m = get(map);
m?.on('click', setCoordinates);
setCrosshairCursor();
map.value?.on('click', setCoordinates);
// setCrosshairCursor();
});
onDestroy(() => {
let m = get(map);
m?.off('click', setCoordinates);
resetCursor();
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
}
map.value?.off('click', setCoordinates);
// resetCursor();
});
</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">
<Label for="name">{i18n._('menu.metadata.name')}</Label>
<Input
bind:value={name}
id="name"
class="font-semibold h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
<Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea
bind:value={description}
id="description"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
<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}
disabled={!canCreate && !selectedWaypoint.wpt}
>
{#if symbolKey in symbols}
{i18n._(`gpx.symbol.${symbolKey}`)}
@@ -212,7 +146,7 @@
bind:value={link}
id="link"
class="h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
<div class="flex flex-row gap-2">
<div class="grow">
@@ -225,7 +159,7 @@
min={-90}
max={90}
class="text-xs h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
</div>
<div class="grow">
@@ -238,7 +172,7 @@
min={-180}
max={180}
class="text-xs h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
</div>
</div>
@@ -246,11 +180,11 @@
<div class="flex flex-row gap-2 items-center">
<Button
variant="outline"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
class="grow whitespace-normal h-fit"
onclick={createOrUpdateWaypoint}
>
{#if $selectedWaypoint}
{#if selectedWaypoint.wpt}
<Save size="16" class="mr-1 shrink-0" />
{i18n._('menu.metadata.save')}
{:else}
@@ -261,7 +195,7 @@
<Button
variant="outline"
onclick={() => {
selectedWaypoint.set(undefined);
selectedWaypoint.reset();
resetWaypointData();
}}
>
@@ -269,7 +203,7 @@
</Button>
</div>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
{#if $selectedWaypoint || canCreate}
{#if selectedWaypoint.wpt || canCreate}
{i18n._('toolbar.waypoint.help')}
{:else}
{i18n._('toolbar.waypoint.help_no_selection')}

View File

@@ -0,0 +1,67 @@
import { ListWaypointItem } from '$lib/components/file-list/file-list';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { selection } from '$lib/logic/selection.svelte';
import { settings } from '$lib/logic/settings.svelte';
import type { Waypoint } from 'gpx';
export class WaypointSelection {
private _selection: [Waypoint, string] | undefined;
constructor() {
this._selection = $derived.by(() => {
if (settings.treeFileView.value && selection.value.size === 1) {
let item = selection.value.getSelected()[0];
if (item instanceof ListWaypointItem) {
let file = fileStateCollection.getFile(item.getFileId());
let waypoint = file?.wpt[item.getWaypointIndex()];
if (waypoint) {
return [waypoint, item.getFileId()];
}
}
}
return undefined;
});
}
reset() {
this._selection = undefined;
}
get wpt(): Waypoint | undefined {
return this._selection ? this._selection[0] : undefined;
}
get fileId(): string | undefined {
return this._selection ? this._selection[1] : undefined;
}
// TODO update the waypoint data if the file changes
// function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
// if (selectedWaypoint.wpt) {
// if (fileStore) {
// if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
// $selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
// name = $selectedWaypoint[0].name ?? '';
// description = $selectedWaypoint[0].desc ?? '';
// if (
// $selectedWaypoint[0].cmt !== undefined &&
// $selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
// ) {
// description += '\n\n' + $selectedWaypoint[0].cmt;
// }
// link = $selectedWaypoint[0].link?.attributes?.href ?? '';
// let symbol = $selectedWaypoint[0].sym ?? '';
// symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
// longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
// latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
// } else {
// selectedWaypoint.reset();
// }
// } else {
// selectedWaypoint.reset();
// }
// }
// }
}
export const selectedWaypoint = new WaypointSelection();