waypoint tool

This commit is contained in:
vcoppe
2024-06-12 18:48:03 +02:00
parent 7dd8855604
commit 71ff8ad727
11 changed files with 399 additions and 59 deletions

View File

@@ -2,13 +2,14 @@ import { currentTool, Tool } from "$lib/stores";
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
import { get, type Readable } from "svelte/store";
import mapboxgl from "mapbox-gl";
import { currentWaypoint, waypointPopup } from "./WaypointPopup";
import { currentPopupWaypoint, waypointPopup } from "./WaypointPopup";
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
import type { Waypoint } from "gpx";
import { produce } from "immer";
import { resetCursor, setCursor, setGrabbingCursor, setPointerCursor } from "$lib/utils";
import { font } from "$lib/assets/layers";
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
let defaultWeight = 5;
let defaultOpacity = 0.6;
@@ -190,13 +191,14 @@ export class GPXLayer {
return;
}
if ((e.shiftKey || e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
if (get(verticalFileView)) {
if ((e.shiftKey || e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
} else {
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
}
} else {
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
}
if (!get(verticalFileView) && !get(selection).has(new ListFileItem(this.fileId))) {
addSelectItem(new ListFileItem(this.fileId));
selectedWaypoint.set([marker._waypoint, this.fileId]);
}
e.stopPropagation();
});
@@ -318,14 +320,14 @@ export class GPXLayer {
showWaypointPopup(waypoint: Waypoint) {
let marker = this.markers[waypoint._data.index];
if (marker) {
currentWaypoint.set(waypoint);
currentPopupWaypoint.set(waypoint);
marker.setPopup(waypointPopup);
marker.togglePopup();
}
}
hideWaypointPopup() {
let waypoint = get(currentWaypoint);
let waypoint = get(currentPopupWaypoint);
if (waypoint) {
let marker = this.markers[waypoint._data.index];
marker?.getPopup()?.remove();

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { waypointPopup, currentWaypoint } from './WaypointPopup';
import { waypointPopup, currentPopupWaypoint } from './WaypointPopup';
import WithUnits from '$lib/components/WithUnits.svelte';
import { Dot } from 'lucide-svelte';
import { onMount } from 'svelte';
@@ -16,26 +16,26 @@
</script>
<div bind:this={popupElement} class="hidden">
{#if $currentWaypoint}
{#if $currentPopupWaypoint}
<Card.Root class="border-none shadow-md text-base max-w-72 p-2">
<Card.Header class="p-0">
<Card.Title class="text-md">{$currentWaypoint.name}</Card.Title>
<Card.Title class="text-md">{$currentPopupWaypoint.name}</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-sm">
<div class="flex flex-row items-center text-muted-foreground">
{$currentWaypoint.getLatitude().toFixed(6)}&deg; {$currentWaypoint
{$currentPopupWaypoint.getLatitude().toFixed(6)}&deg; {$currentPopupWaypoint
.getLongitude()
.toFixed(6)}&deg;
{#if $currentWaypoint.ele !== undefined}
{#if $currentPopupWaypoint.ele !== undefined}
<Dot size="16" />
<WithUnits value={$currentWaypoint.ele} type="elevation" />
<WithUnits value={$currentPopupWaypoint.ele} type="elevation" />
{/if}
</div>
{#if $currentWaypoint.desc}
<span>{$currentWaypoint.desc}</span>
{#if $currentPopupWaypoint.desc}
<span>{$currentPopupWaypoint.desc}</span>
{/if}
{#if $currentWaypoint.cmt}
<span>{$currentWaypoint.cmt}</span>
{#if $currentPopupWaypoint.cmt}
<span>{$currentPopupWaypoint.cmt}</span>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -2,7 +2,7 @@ import type { Waypoint } from "gpx";
import mapboxgl from "mapbox-gl";
import { writable } from "svelte/store";
export const currentWaypoint = writable<Waypoint | null>(null);
export const currentPopupWaypoint = writable<Waypoint | null>(null);
export const waypointPopup = new mapboxgl.Popup({
closeButton: false,

View File

@@ -8,7 +8,6 @@
SquareDashedMousePointer,
Ungroup,
MapPin,
Palette,
Filter,
Scissors
} from 'lucide-svelte';
@@ -28,7 +27,7 @@
</ToolbarItem>
<ToolbarItem tool={Tool.WAYPOINT}>
<MapPin slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.waypoint_tooltip')}</span>
<span slot="tooltip">{$_('toolbar.waypoint.tooltip')}</span>
</ToolbarItem>
<ToolbarItem tool={Tool.SCISSORS}>
<Scissors slot="icon" size="18" />

View File

@@ -1,40 +1,232 @@
<script lang="ts">
import * as Alert from '$lib/components/ui/alert';
import { CircleHelp } from 'lucide-svelte';
import { selection } from '$lib/components/file-list/Selection';
import type { Waypoint } from 'gpx';
import { _ } from 'svelte-i18n';
import { ListWaypointItem } from '$lib/components/file-list/FileList';
import { fileObservers } from '$lib/db';
import { get } from 'svelte/store';
<script lang="ts" context="module">
import { writable } from 'svelte/store';
let waypoint: Waypoint | undefined = undefined;
$: if ($selection) {
waypoint = undefined;
$selection.forEach((item) => {
if (item instanceof ListWaypointItem) {
if (waypoint) return;
let fileStore = get(fileObservers).get(item.getFileId());
if (fileStore) {
waypoint = get(fileStore)?.file.wpt[item.getWaypointIndex()];
}
}
});
}
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
</script>
<div class="flex flex-col gap-3 max-w-96">
{#if waypoint}
<span>{waypoint.name}</span>
<span>{waypoint.desc ?? ''}</span>
<span>{waypoint.cmt ?? ''}</span>
{/if}
<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 { selection } from '$lib/components/file-list/Selection';
import { Waypoint } from 'gpx';
import { _ } from 'svelte-i18n';
import { ListWaypointItem } from '$lib/components/file-list/FileList';
import { dbUtils, fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
import { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte';
import { onDestroy, onMount } from 'svelte';
import { map } from '$lib/stores';
import { resetCursor, setCrosshairCursor } from '$lib/utils';
import { CircleX } from 'lucide-svelte';
<Alert.Root class="max-w-64">
<CircleHelp size="16" />
<Alert.Description>
<div>{$_('toolbar.waypoint.help')}</div>
</Alert.Description>
</Alert.Root>
let name: string;
let description: string;
let comment: string;
let longitude: number;
let latitude: number;
const { verticalFileView } = settings;
$: canCreate = $selection.size > 0;
$: if ($verticalFileView && $selection) {
selectedWaypoint.update(() => {
if ($selection.size === 1) {
let item = $selection.getSelected()[0];
if (item instanceof ListWaypointItem) {
let fileStore = get(fileObservers).get(item.getFileId());
if (fileStore) {
let waypoint = get(fileStore)?.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 ?? '';
comment = $selectedWaypoint[0].cmt ?? '';
longitude = $selectedWaypoint[0].getLongitude();
latitude = $selectedWaypoint[0].getLatitude();
} else {
selectedWaypoint.set(undefined);
}
} else {
selectedWaypoint.set(undefined);
}
}
}
function resetWaypointData() {
name = '';
description = '';
comment = '';
longitude = 0;
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);
}
if (typeof longitude === 'string') {
longitude = parseFloat(longitude);
}
latitude = parseFloat(latitude.toFixed(6));
longitude = parseFloat(longitude.toFixed(6));
if ($selectedWaypoint) {
dbUtils.applyToFile($selectedWaypoint[1], (file) => {
let waypoint = $selectedWaypoint[0].clone();
waypoint.name = name;
waypoint.desc = description;
waypoint.cmt = comment;
waypoint.setCoordinates({
lat: latitude,
lon: longitude
});
return file.replaceWaypoints(
$selectedWaypoint[0]._data.index,
$selectedWaypoint[0]._data.index,
[waypoint]
)[0];
});
} else {
let fileIds = new Set<string>();
$selection.getSelected().forEach((item) => {
fileIds.add(item.getFileId());
});
let waypoint = new Waypoint({
name,
desc: description,
cmt: comment,
attributes: {
lat: latitude,
lon: longitude
}
});
// TODO get elevation for waypoint
dbUtils.applyToFiles(
Array.from(fileIds),
(file) => file.replaceWaypoints(file.wpt.length, file.wpt.length, [waypoint])[0]
);
}
selectedWaypoint.set(undefined);
resetWaypointData();
}
function setCoordinates(e: any) {
latitude = e.lngLat.lat;
longitude = e.lngLat.lng;
}
onMount(() => {
let m = get(map);
m?.on('click', setCoordinates);
setCrosshairCursor();
});
onDestroy(() => {
let m = get(map);
m?.off('click', setCoordinates);
resetCursor();
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
}
});
</script>
<div class="flex flex-col gap-3 w-80">
<fieldset class="flex flex-col gap-2">
<Label for="name">{$_('toolbar.waypoint.name')}</Label>
<Input bind:value={name} id="name" class="font-semibold h-8" />
<Label for="description">{$_('toolbar.waypoint.description')}</Label>
<Textarea bind:value={description} id="description" />
<Label for="comment">{$_('toolbar.waypoint.comment')}</Label>
<Textarea bind:value={comment} id="comment" />
<div class="flex flex-row gap-2">
<div>
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
<Input
bind:value={latitude}
type="number"
id="latitude"
step={1e-6}
min={-90}
max={90}
class="text-xs h-8"
/>
</div>
<div>
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
<Input
bind:value={longitude}
type="number"
id="longitude"
step={1e-6}
min={-180}
max={180}
class="text-xs h-8"
/>
</div>
</div>
</fieldset>
<div class="flex flex-row gap-2">
<Button
variant="outline"
disabled={!canCreate && !$selectedWaypoint}
class="grow"
on:click={createOrUpdateWaypoint}
>
{#if $selectedWaypoint}
{$_('toolbar.waypoint.update')}
{:else}
{$_('toolbar.waypoint.create')}
{/if}
</Button>
<Button
variant="outline"
on:click={() => {
selectedWaypoint.set(undefined);
resetWaypointData();
}}
>
<CircleX size="16" />
</Button>
</div>
<Help>
{#if $selectedWaypoint || canCreate}
{$_('toolbar.waypoint.help')}
{:else}
{$_('toolbar.waypoint.help_no_selection')}
{/if}
</Help>
</div>

View File

@@ -85,7 +85,6 @@
{/if}
{$_('toolbar.routing.use_routing')}
</span>
<Switch class="scale-90" bind:checked={$routing} />
</Label>
<span slot="tooltip">{$_('toolbar.routing.use_routing_tooltip')}</span>

View File

@@ -0,0 +1,29 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<input
class={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View File

@@ -0,0 +1,28 @@
import Root from "./textarea.svelte";
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
};

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements";
import type { TextareaEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<textarea
class={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

View File

@@ -130,7 +130,18 @@
"help_cannot_merge_contents": "Your selection needs to contain several file items to merge their contents"
},
"extract_tooltip": "Extract contents",
"waypoint_tooltip": "Create and edit points of interest",
"waypoint": {
"tooltip": "Create and edit points of interest",
"name": "Name",
"description": "Description",
"comment": "Comment",
"longitude": "Longitude",
"latitude": "Latitude",
"create": "Create",
"update": "Update",
"help": "Fill in the form to create a new point of interest, or click on an existing one to edit it. Click on the map to fill the coordinates, or drag points of interest to move them.",
"help_no_selection": "Select a file item to create or edit points of interest"
},
"reduce": {
"tooltip": "Reduce the number of GPS points",
"tolerance": "Tolerance",