adds route edit menu

This commit is contained in:
Christian Beutel
2025-07-19 19:40:11 +02:00
parent 6a908a3ace
commit ef1571c54e
23 changed files with 791 additions and 386 deletions

View File

@@ -9,6 +9,7 @@
currentMin?: any; currentMin?: any;
currentMax?: any; currentMax?: any;
onset?: (data: [number, number]) => void; onset?: (data: [number, number]) => void;
onupdate?: (data: [number, number]) => void;
} }
let { let {
@@ -17,6 +18,7 @@
currentMin = $bindable(minValue), currentMin = $bindable(minValue),
currentMax = $bindable(maxValue), currentMax = $bindable(maxValue),
onset, onset,
onupdate,
}: Props = $props(); }: Props = $props();
let sliderContainer: any = $state(); let sliderContainer: any = $state();
@@ -25,6 +27,7 @@
const updateValues = (values: string[]) => { const updateValues = (values: string[]) => {
currentMin = parseFloat(values[0]); currentMin = parseFloat(values[0]);
currentMax = parseFloat(values[1]); currentMax = parseFloat(values[1]);
onupdate?.([currentMin, currentMax]);
}; };
noUiSlider.create(sliderContainer, { noUiSlider.create(sliderContainer, {

View File

@@ -716,6 +716,7 @@
if (!map) { if (!map) {
return; return;
} }
hideWaypoints();
activeTrail ??= 0; activeTrail ??= 0;
map.getCanvas().style.cursor = "crosshair"; map.getCanvas().style.cursor = "crosshair";
if (trails[activeTrail]) { if (trails[activeTrail]) {
@@ -727,6 +728,7 @@
if (!map) { if (!map) {
return; return;
} }
showWaypoints();
map.getCanvas().style.cursor = "inherit"; map.getCanvas().style.cursor = "inherit";
if (activeTrail !== null && trails[activeTrail] && !clusterTrails) { if (activeTrail !== null && trails[activeTrail] && !clusterTrails) {

View File

@@ -0,0 +1,411 @@
<script lang="ts">
import type {
RoutingOptions,
ValhallaBicycleCostingOptions,
} from "$lib/models/valhalla";
import { formatSpeed } from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import { slide } from "svelte/transition";
import Select, { type SelectItem } from "../base/select.svelte";
import Slider from "../base/slider.svelte";
import Toggle from "../base/toggle.svelte";
import Button from "../base/button.svelte";
import DoubleSlider from "../base/double_slider.svelte";
interface Props {
options: RoutingOptions;
onReverse: () => void;
onReset: () => void;
onCropToggle: (active: boolean) => void;
onUpdateCropRange: (data: [number, number]) => void;
onCrop: () => void;
onRecalculateElevationData: () => void;
}
let {
options = $bindable(),
onReverse,
onReset,
onCropToggle,
onUpdateCropRange,
onCrop,
onRecalculateElevationData,
}: Props = $props();
const modesOfTransport: SelectItem[] = [
{ text: $_("hiking"), value: "pedestrian" },
{ text: $_("cycling"), value: "bicycle" },
{ text: $_("driving"), value: "auto" },
];
const bikeTypes: SelectItem[] = [
{ text: $_("hybrid"), value: "Hybrid" },
{ text: $_("road"), value: "Road" },
{ text: $_("cross"), value: "Cross" },
{ text: $_("mountain"), value: "Mountain" },
];
if (!options.pedestrianOptions) {
options.pedestrianOptions = {
max_hiking_difficulty: 6,
walking_speed: 5.1,
use_hills: 1,
shortest: false,
};
}
if (!options.bicycleOptions) {
options.bicycleOptions = {
bicycle_type: "Hybrid",
cycling_speed: 20,
use_roads: 0.5,
use_hills: 0.5,
avoid_bad_surfaces: 0.25,
shortest: false,
};
}
if (!options.autoOptions) {
options.autoOptions = {
width: 1.6,
height: 1.9,
top_speed: 140,
fixed_speed: 0,
shortest: false,
};
}
$effect(() => {
if (!options.autoRouting) {
showSettings = false;
}
});
let showSettings = $state(false);
let editRoute = $state(false);
let crop = $state(false);
let recalculateElevationData = $state(false);
// svelte-ignore non_reactive_update
let cycleSpeedSlider: Slider;
function adjustSpeeddependingOnBikeType(
type: ValhallaBicycleCostingOptions["bicycle_type"],
) {
switch (type) {
case "City":
case "Hybrid":
cycleSpeedSlider?.set(18);
break;
case "Road":
cycleSpeedSlider?.set(25);
break;
case "Cross":
cycleSpeedSlider?.set(20);
break;
case "Mountain":
cycleSpeedSlider?.set(16);
break;
default:
break;
}
}
</script>
<div class="flex gap-x-2 items-start">
<div class="flex flex-col gap-y-1 p-1 bg-background rounded-md my-2">
<button
class="btn-icon"
class:bg-secondary-hover={editRoute}
aria-label="edit route"
onclick={() => {
recalculateElevationData = false;
crop = false;
editRoute = !editRoute;
}}><i class="fa fa-pen text-sm"></i></button
>
<button
class="btn-icon"
class:bg-secondary-hover={crop}
aria-label="crop route"
onclick={() => {
recalculateElevationData = false;
crop = !crop;
editRoute = false;
onCropToggle(crop);
}}><i class="fa fa-scissors text-sm"></i></button
>
<button
class="btn-icon"
class:bg-secondary-hover={recalculateElevationData}
aria-label="recalculate elevation data"
onclick={() => {
recalculateElevationData = !recalculateElevationData;
crop = false;
editRoute = false;
}}><i class="fa fa-mountain text-sm"></i></button
>
</div>
{#if editRoute}
<div class=" pt-2 pb-3 px-4 my-2 rounded-xl bg-background shadow-xl">
<Toggle
bind:value={options.autoRouting}
label={$_("enable-auto-routing")}
></Toggle>
<Select
items={modesOfTransport}
bind:value={options.modeOfTransport}
disabled={!options.autoRouting}
label={$_("activity", { values: { n: 1 } })}
></Select>
<div class="flex items-center gap-4 mt-4">
<button
class="btn-icon tooltip"
type="button"
onclick={() => onReverse()}
aria-label="Reverse trail direction"
data-title={$_("reverse-direction")}
><i class="fa fa-arrow-right-arrow-left"></i></button
>
<button
class="btn-icon tooltip"
type="button"
onclick={() => onReset()}
aria-label="Reset route"
data-title={$_("reset")}><i class="fa fa-trash"></i></button
>
<button
class="btn-icon tooltip"
type="button"
disabled={!options.autoRouting}
onclick={() => (showSettings = !showSettings)}
data-title={$_("more-route-settings")}
aria-label="Toggle routing settings"
><i
class="fa fa-cogs"
class:text-gray-500={!options.autoRouting}
></i></button
>
</div>
{#if showSettings}
<div in:slide out:slide>
{#if options.modeOfTransport === "pedestrian" && options.pedestrianOptions}
<p class="text-sm font-medium pb-1">
{$_("walking-speed")}
</p>
<Slider
minValue={0.5}
maxValue={25}
bind:currentValue={
options.pedestrianOptions.walking_speed
}
></Slider>
<p class="text-sm text-end">
{formatSpeed(
options.pedestrianOptions.walking_speed! / 3.6,
)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("use-hills")}</p>
<Slider
minValue={0}
maxValue={1}
step={0.1}
bind:currentValue={
options.pedestrianOptions!.use_hills
}
></Slider>
<p class="text-sm text-end">
{options.pedestrianOptions.use_hills?.toFixed(2)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">
{$_("max-hiking-difficulty")}
</p>
<Slider
minValue={0}
maxValue={6}
step={1}
bind:currentValue={
options.pedestrianOptions!.max_hiking_difficulty
}
></Slider>
<p class="text-sm text-end">
{options.pedestrianOptions.max_hiking_difficulty?.toFixed(
0,
)}
</p>
<hr class="border-input-border my-3" />
<Toggle
label={$_("shortest")}
bind:value={options.pedestrianOptions.shortest}
></Toggle>
{:else if options.modeOfTransport === "bicycle" && options.bicycleOptions}
<Select
items={bikeTypes}
label={$_("bike-type")}
onchange={(v) => adjustSpeeddependingOnBikeType(v)}
bind:value={options.bicycleOptions.bicycle_type}
></Select>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium pb-1">
{$_("cycling-speed")}
</p>
<Slider
minValue={5}
maxValue={50}
bind:currentValue={
options.bicycleOptions.cycling_speed
}
bind:this={cycleSpeedSlider}
></Slider>
<p class="text-sm text-end">
{formatSpeed(
options.bicycleOptions.cycling_speed! / 3.6,
)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("use-hills")}</p>
<Slider
minValue={0}
maxValue={1}
step={0.05}
bind:currentValue={options.bicycleOptions.use_hills}
></Slider>
<p class="text-sm text-end">
{options.bicycleOptions.use_hills?.toFixed(2)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("use-roads")}</p>
<Slider
minValue={0}
maxValue={1}
step={0.05}
bind:currentValue={options.bicycleOptions.use_roads}
></Slider>
<p class="text-sm text-end">
{options.bicycleOptions.use_roads?.toFixed(2)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">
{$_("avoid-bad-surfaces")}
</p>
<Slider
minValue={0}
maxValue={1}
step={0.05}
bind:currentValue={
options.bicycleOptions.avoid_bad_surfaces
}
></Slider>
<p class="text-sm text-end">
{options.bicycleOptions.avoid_bad_surfaces?.toFixed(
2,
)}
</p>
<hr class="border-input-border my-3" />
<Toggle
label={$_("shortest")}
bind:value={options.bicycleOptions.shortest}
></Toggle>
{:else if options.modeOfTransport === "auto" && options.autoOptions}
<p class="text-sm font-medium pb-1">
{$_("fixed-speed")}
</p>
<Slider
minValue={0}
maxValue={252}
step={1}
bind:currentValue={options.autoOptions.fixed_speed}
></Slider>
<p class="text-sm text-end">
{formatSpeed(
options.autoOptions.fixed_speed! / 3.6,
)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("top-speed")}</p>
<Slider
minValue={10}
maxValue={252}
step={1}
bind:currentValue={options.autoOptions.top_speed}
></Slider>
<p class="text-sm text-end">
{formatSpeed(options.autoOptions.top_speed! / 3.6)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">
{$_("car")}
{$_("width")}
</p>
<Slider
minValue={1}
maxValue={10}
step={0.1}
bind:currentValue={options.autoOptions.width}
></Slider>
<p class="text-sm text-end">
{options.autoOptions.width?.toFixed(1)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">
{$_("car")}
{$_("height")}
</p>
<Slider
minValue={1}
maxValue={10}
step={0.1}
bind:currentValue={options.autoOptions.height}
></Slider>
<p class="text-sm text-end">
{options.autoOptions.height?.toFixed(1)}
</p>
<hr class="border-input-border my-3" />
<Toggle
label={$_("shortest")}
bind:value={options.autoOptions.shortest}
></Toggle>
{/if}
</div>
{/if}
</div>
{/if}
{#if crop}
<div
class="p-4 my-2 rounded-xl bg-background shadow-xl min-w-72 flex flex-col"
>
<DoubleSlider onupdate={onUpdateCropRange}></DoubleSlider>
<button
class="btn-secondary mb-2"
onclick={() => {
crop = false;
onCrop();
onCropToggle(false);
}}>{$_("crop")}</button
>
<button
class="btn-secondary"
onclick={() => {
crop = false;
onCropToggle(false);
}}>{$_("cancel")}</button
>
</div>
{/if}
{#if recalculateElevationData}
<div class="p-4 my-2 rounded-xl bg-background shadow-xl">
<Button secondary onclick={onRecalculateElevationData}
>{$_("recalculate-elevation-data")}</Button
>
<p
class="bg-background/50 rounded-xl text-sm text-gray-500 mt-2 max-w-72"
>
{$_("recalculating-elevation-data-hint")}
</p>
</div>
{/if}
</div>

View File

@@ -1,288 +0,0 @@
<script lang="ts">
import type {
RoutingOptions,
ValhallaBicycleCostingOptions,
} from "$lib/models/valhalla";
import { formatSpeed } from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import { slide } from "svelte/transition";
import Select, { type SelectItem } from "../base/select.svelte";
import Slider from "../base/slider.svelte";
import Toggle from "../base/toggle.svelte";
interface Props {
options: RoutingOptions;
onReverse: () => void;
onReset: () => void;
}
let { options = $bindable(), onReverse, onReset }: Props = $props();
const modesOfTransport: SelectItem[] = [
{ text: $_("hiking"), value: "pedestrian" },
{ text: $_("cycling"), value: "bicycle" },
{ text: $_("driving"), value: "auto" },
];
const bikeTypes: SelectItem[] = [
{ text: $_("hybrid"), value: "Hybrid" },
{ text: $_("road"), value: "Road" },
{ text: $_("cross"), value: "Cross" },
{ text: $_("mountain"), value: "Mountain" },
];
if (!options.pedestrianOptions) {
options.pedestrianOptions = {
max_hiking_difficulty: 6,
walking_speed: 5.1,
use_hills: 1,
shortest: false,
};
}
if (!options.bicycleOptions) {
options.bicycleOptions = {
bicycle_type: "Hybrid",
cycling_speed: 20,
use_roads: 0.5,
use_hills: 0.5,
avoid_bad_surfaces: 0.25,
shortest: false,
};
}
if (!options.autoOptions) {
options.autoOptions = {
width: 1.6,
height: 1.9,
top_speed: 140,
fixed_speed: 0,
shortest: false,
};
}
$effect(() => {
if (!options.autoRouting) {
showSettings = false;
}
});
let showSettings = $state(false);
// svelte-ignore non_reactive_update
let cycleSpeedSlider: Slider;
function adjustSpeeddependingOnBikeType(
type: ValhallaBicycleCostingOptions["bicycle_type"],
) {
switch (type) {
case "City":
case "Hybrid":
cycleSpeedSlider?.set(18);
break;
case "Road":
cycleSpeedSlider?.set(25);
break;
case "Cross":
cycleSpeedSlider?.set(20);
break;
case "Mountain":
cycleSpeedSlider?.set(16);
break;
default:
break;
}
}
</script>
<div class="px-6 py-4 my-2 rounded-xl bg-background space-y-4">
<Toggle bind:value={options.autoRouting} label={$_("enable-auto-routing")}
></Toggle>
<div class="flex items-center gap-4">
<Select
items={modesOfTransport}
bind:value={options.modeOfTransport}
disabled={!options.autoRouting}
></Select>
<button
class="btn-icon"
type="button"
disabled={!options.autoRouting}
onclick={() => (showSettings = !showSettings)}
aria-label="Toggle routing settings"
><i class="fa fa-cogs" class:text-gray-500={!options.autoRouting}
></i></button
>
</div>
<div class="flex items-center gap-4">
<button
class="btn-icon tooltip"
type="button"
onclick={() => onReverse()}
aria-label="Reverse trail direction"
data-title={$_("reverse-direction")}
><i class="fa fa-arrow-right-arrow-left"></i></button
>
<button
class="btn-icon tooltip"
type="button"
onclick={() => onReset()}
aria-label="Reset route"
data-title={$_("reset")}><i class="fa fa-trash"></i></button
>
</div>
{#if showSettings}
<div in:slide out:slide>
{#if options.modeOfTransport === "pedestrian" && options.pedestrianOptions}
<p class="text-sm font-medium pb-1">{$_("walking-speed")}</p>
<Slider
minValue={0.5}
maxValue={25}
bind:currentValue={options.pedestrianOptions.walking_speed}
></Slider>
<p class="text-sm text-end">
{formatSpeed(
options.pedestrianOptions.walking_speed! / 3.6,
)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("use-hills")}</p>
<Slider
minValue={0}
maxValue={1}
step={0.1}
bind:currentValue={options.pedestrianOptions!.use_hills}
></Slider>
<p class="text-sm text-end">
{options.pedestrianOptions.use_hills?.toFixed(2)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("max-hiking-difficulty")}</p>
<Slider
minValue={0}
maxValue={6}
step={1}
bind:currentValue={
options.pedestrianOptions!.max_hiking_difficulty
}
></Slider>
<p class="text-sm text-end">
{options.pedestrianOptions.max_hiking_difficulty?.toFixed(
0,
)}
</p>
<hr class="border-input-border my-3" />
<Toggle
label={$_("shortest")}
bind:value={options.pedestrianOptions.shortest}
></Toggle>
{:else if options.modeOfTransport === "bicycle" && options.bicycleOptions}
<Select
items={bikeTypes}
label={$_("bike-type")}
onchange={(v) => adjustSpeeddependingOnBikeType(v)}
bind:value={options.bicycleOptions.bicycle_type}
></Select>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium pb-1">{$_("cycling-speed")}</p>
<Slider
minValue={5}
maxValue={50}
bind:currentValue={options.bicycleOptions.cycling_speed}
bind:this={cycleSpeedSlider}
></Slider>
<p class="text-sm text-end">
{formatSpeed(options.bicycleOptions.cycling_speed! / 3.6)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("use-hills")}</p>
<Slider
minValue={0}
maxValue={1}
step={0.05}
bind:currentValue={options.bicycleOptions.use_hills}
></Slider>
<p class="text-sm text-end">
{options.bicycleOptions.use_hills?.toFixed(2)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("use-roads")}</p>
<Slider
minValue={0}
maxValue={1}
step={0.05}
bind:currentValue={options.bicycleOptions.use_roads}
></Slider>
<p class="text-sm text-end">
{options.bicycleOptions.use_roads?.toFixed(2)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("avoid-bad-surfaces")}</p>
<Slider
minValue={0}
maxValue={1}
step={0.05}
bind:currentValue={
options.bicycleOptions.avoid_bad_surfaces
}
></Slider>
<p class="text-sm text-end">
{options.bicycleOptions.avoid_bad_surfaces?.toFixed(2)}
</p>
<hr class="border-input-border my-3" />
<Toggle
label={$_("shortest")}
bind:value={options.bicycleOptions.shortest}
></Toggle>
{:else if options.modeOfTransport === "auto" && options.autoOptions}
<p class="text-sm font-medium pb-1">{$_("fixed-speed")}</p>
<Slider
minValue={0}
maxValue={252}
step={1}
bind:currentValue={options.autoOptions.fixed_speed}
></Slider>
<p class="text-sm text-end">
{formatSpeed(options.autoOptions.fixed_speed! / 3.6)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("top-speed")}</p>
<Slider
minValue={10}
maxValue={252}
step={1}
bind:currentValue={options.autoOptions.top_speed}
></Slider>
<p class="text-sm text-end">
{formatSpeed(options.autoOptions.top_speed! / 3.6)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("car")} {$_("width")}</p>
<Slider
minValue={1}
maxValue={10}
step={0.1}
bind:currentValue={options.autoOptions.width}
></Slider>
<p class="text-sm text-end">
{options.autoOptions.width?.toFixed(1)}
</p>
<hr class="border-input-border my-3" />
<p class="text-sm font-medium">{$_("car")} {$_("height")}</p>
<Slider
minValue={1}
maxValue={10}
step={0.1}
bind:currentValue={options.autoOptions.height}
></Slider>
<p class="text-sm text-end">
{options.autoOptions.height?.toFixed(1)}
</p>
<hr class="border-input-border my-3" />
<Toggle
label={$_("shortest")}
bind:value={options.autoOptions.shortest}
></Toggle>
{/if}
</div>
{/if}
</div>

View File

@@ -81,6 +81,7 @@
"create-new-list": "Neue Liste erstellen", "create-new-list": "Neue Liste erstellen",
"create-waypoint": "Wegpunkt erstellen", "create-waypoint": "Wegpunkt erstellen",
"creation-date": "Erstellungsdatum", "creation-date": "Erstellungsdatum",
"crop": "",
"cross": "Querfeldein", "cross": "Querfeldein",
"current-password": "Aktuelles Passwort", "current-password": "Aktuelles Passwort",
"cycling": "Radfahren", "cycling": "Radfahren",
@@ -235,6 +236,7 @@
"metric": "Metrisch", "metric": "Metrisch",
"moderate": "Mittel", "moderate": "Mittel",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Berg", "mountain": "Berg",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Muss mindestens {n} Zeichen lang sein", "must-be-at-least-n-characters-long": "Muss mindestens {n} Zeichen lang sein",
@@ -313,6 +315,8 @@
"radius": "Radius", "radius": "Radius",
"railway-station": "", "railway-station": "",
"read-more": "Mehr", "read-more": "Mehr",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Registrieren", "register": "Registrieren",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Route entfernt aus", "removed-trail-from": "Route entfernt aus",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Auf der Karte anzeigen", "show-on-map": "Auf der Karte anzeigen",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Speichere deine Abenteuer!", "slogan": "Speichere deine Abenteuer!",
"slope": "Steigung", "slope": "Steigung",
"someone": "Jemand", "someone": "Jemand",

View File

@@ -3,7 +3,7 @@
"Canoeing": "Canoeing", "Canoeing": "Canoeing",
"Climbing": "Climbing", "Climbing": "Climbing",
"Hiking": "Hiking", "Hiking": "Hiking",
"skiing": "Skiing", "Skiing": "",
"Walking": "Walking", "Walking": "Walking",
"about": "About", "about": "About",
"account-delete-confirm": "You are about to delete your account. All your trails will also be deleted. Do you want to proceed?", "account-delete-confirm": "You are about to delete your account. All your trails will also be deleted. Do you want to proceed?",
@@ -81,6 +81,7 @@
"create-new-list": "Create new list", "create-new-list": "Create new list",
"create-waypoint": "Create waypoint", "create-waypoint": "Create waypoint",
"creation-date": "Creation date", "creation-date": "Creation date",
"crop": "Crop",
"cross": "Cross", "cross": "Cross",
"current-password": "Current password", "current-password": "Current password",
"cycling": "Cycling", "cycling": "Cycling",
@@ -235,6 +236,7 @@
"metric": "Metric", "metric": "Metric",
"moderate": "Moderate", "moderate": "Moderate",
"more": "More", "more": "More",
"more-route-settings": "More route settings",
"mountain": "Mountain", "mountain": "Mountain",
"mountain-pass": "Mountain pass", "mountain-pass": "Mountain pass",
"must-be-at-least-n-characters-long": "Must be at least {n} characters long", "must-be-at-least-n-characters-long": "Must be at least {n} characters long",
@@ -313,6 +315,8 @@
"radius": "Radius", "radius": "Radius",
"railway-station": "Railway station", "railway-station": "Railway station",
"read-more": "Read more", "read-more": "Read more",
"recalculate-elevation-data": "Recalculate elevation data",
"recalculating-elevation-data-hint": "Recalculating elevation data will erase the existing elevation data, if any, and replace it with data from Valhalla.",
"register": "Register", "register": "Register",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Removed trail from", "removed-trail-from": "Removed trail from",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Show on map", "show-on-map": "Show on map",
"shower": "Shower", "shower": "Shower",
"skiing": "Skiing",
"slogan": "Save your adventures!", "slogan": "Save your adventures!",
"slope": "Slope", "slope": "Slope",
"someone": "Someone", "someone": "Someone",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Crear una nueva lista", "create-new-list": "Crear una nueva lista",
"create-waypoint": "Crear punto de ruta", "create-waypoint": "Crear punto de ruta",
"creation-date": "Fecha de creación", "creation-date": "Fecha de creación",
"crop": "",
"cross": "Cruzar", "cross": "Cruzar",
"current-password": "Contraseña actual", "current-password": "Contraseña actual",
"cycling": "Ciclismo", "cycling": "Ciclismo",
@@ -235,6 +236,7 @@
"metric": "Métrica", "metric": "Métrica",
"moderate": "Medio", "moderate": "Medio",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Montaña", "mountain": "Montaña",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Tiene que tener por lo menos {n} caracteres", "must-be-at-least-n-characters-long": "Tiene que tener por lo menos {n} caracteres",
@@ -313,6 +315,8 @@
"radius": "Radio", "radius": "Radio",
"railway-station": "", "railway-station": "",
"read-more": "Leer más", "read-more": "Leer más",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Registrar", "register": "Registrar",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Ruta borrada de", "removed-trail-from": "Ruta borrada de",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Mostrar en mapa", "show-on-map": "Mostrar en mapa",
"shower": "", "shower": "",
"skiing": "",
"slogan": "¡Guarda tus aventuras!", "slogan": "¡Guarda tus aventuras!",
"slope": "Pendiente", "slope": "Pendiente",
"someone": "Alguien", "someone": "Alguien",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Create new list", "create-new-list": "Create new list",
"create-waypoint": "Create waypoint", "create-waypoint": "Create waypoint",
"creation-date": "Creation date", "creation-date": "Creation date",
"crop": "",
"cross": "Cross", "cross": "Cross",
"current-password": "Current password", "current-password": "Current password",
"cycling": "Cycling", "cycling": "Cycling",
@@ -235,6 +236,7 @@
"metric": "Metric", "metric": "Metric",
"moderate": "Moderate", "moderate": "Moderate",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Mountain", "mountain": "Mountain",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Must be at least {n} characters long", "must-be-at-least-n-characters-long": "Must be at least {n} characters long",
@@ -313,6 +315,8 @@
"radius": "Radius", "radius": "Radius",
"railway-station": "", "railway-station": "",
"read-more": "Read more", "read-more": "Read more",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Register", "register": "Register",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Removed trail from", "removed-trail-from": "Removed trail from",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Show on map", "show-on-map": "Show on map",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Save your adventures!", "slogan": "Save your adventures!",
"slope": "Slope", "slope": "Slope",
"someone": "Someone", "someone": "Someone",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Créer une nouvelle liste", "create-new-list": "Créer une nouvelle liste",
"create-waypoint": "Créer un point de passage", "create-waypoint": "Créer un point de passage",
"creation-date": "Date de création", "creation-date": "Date de création",
"crop": "",
"cross": "Cross", "cross": "Cross",
"current-password": "Mot de passe actuel", "current-password": "Mot de passe actuel",
"cycling": "Vélo", "cycling": "Vélo",
@@ -235,6 +236,7 @@
"metric": "Métrique", "metric": "Métrique",
"moderate": "Moyenne", "moderate": "Moyenne",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Montagne", "mountain": "Montagne",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Doit être composé d'au moins {n} caractères", "must-be-at-least-n-characters-long": "Doit être composé d'au moins {n} caractères",
@@ -313,6 +315,8 @@
"radius": "Rayon", "radius": "Rayon",
"railway-station": "", "railway-station": "",
"read-more": "Voir plus", "read-more": "Voir plus",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Créer un compte", "register": "Créer un compte",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Enlever l'itinéraire de", "removed-trail-from": "Enlever l'itinéraire de",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Voir sur la carte", "show-on-map": "Voir sur la carte",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Sauvegarder vos aventures !", "slogan": "Sauvegarder vos aventures !",
"slope": "Pente", "slope": "Pente",
"someone": "Quelqu'un", "someone": "Quelqu'un",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Új lista létrehozása", "create-new-list": "Új lista létrehozása",
"create-waypoint": "Create waypoint", "create-waypoint": "Create waypoint",
"creation-date": "létrehozás dátuma", "creation-date": "létrehozás dátuma",
"crop": "",
"cross": "Cross", "cross": "Cross",
"current-password": "Current password", "current-password": "Current password",
"cycling": "Cycling", "cycling": "Cycling",
@@ -235,6 +236,7 @@
"metric": "Metrikus", "metric": "Metrikus",
"moderate": "Mérsékelt", "moderate": "Mérsékelt",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Mountain", "mountain": "Mountain",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Legalább {n} karakter hosszúnak kell lennie", "must-be-at-least-n-characters-long": "Legalább {n} karakter hosszúnak kell lennie",
@@ -313,6 +315,8 @@
"radius": "Átmérő", "radius": "Átmérő",
"railway-station": "", "railway-station": "",
"read-more": "Read more", "read-more": "Read more",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Regisztráció", "register": "Regisztráció",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Eltávolított nyomvonal a", "removed-trail-from": "Eltávolított nyomvonal a",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Mutatás térképen", "show-on-map": "Mutatás térképen",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Mentse el a kalandjait!", "slogan": "Mentse el a kalandjait!",
"slope": "Slope", "slope": "Slope",
"someone": "Someone", "someone": "Someone",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Crea nuova lista", "create-new-list": "Crea nuova lista",
"create-waypoint": "Create waypoint", "create-waypoint": "Create waypoint",
"creation-date": "Data di creazione", "creation-date": "Data di creazione",
"crop": "",
"cross": "Cross", "cross": "Cross",
"current-password": "Password attuale", "current-password": "Password attuale",
"cycling": "Ciclismo", "cycling": "Ciclismo",
@@ -235,6 +236,7 @@
"metric": "Metrico", "metric": "Metrico",
"moderate": "Moderato", "moderate": "Moderato",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Mountain", "mountain": "Mountain",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Deve essere lungo almeno {n} caratteri", "must-be-at-least-n-characters-long": "Deve essere lungo almeno {n} caratteri",
@@ -313,6 +315,8 @@
"radius": "Raggio", "radius": "Raggio",
"railway-station": "", "railway-station": "",
"read-more": "Per saperne di più", "read-more": "Per saperne di più",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Registrati", "register": "Registrati",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Percorso rimosso da", "removed-trail-from": "Percorso rimosso da",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Mostra sulla mappa", "show-on-map": "Mostra sulla mappa",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Salva le tue avventure!", "slogan": "Salva le tue avventure!",
"slope": "Pendenza", "slope": "Pendenza",
"someone": "Qualcuno", "someone": "Qualcuno",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Nieuwe lijst", "create-new-list": "Nieuwe lijst",
"create-waypoint": "Nieuw routepunt", "create-waypoint": "Nieuw routepunt",
"creation-date": "Aanmaakdatum", "creation-date": "Aanmaakdatum",
"crop": "",
"cross": "Oversteken", "cross": "Oversteken",
"current-password": "Huidig wachtwoord", "current-password": "Huidig wachtwoord",
"cycling": "Fietsen", "cycling": "Fietsen",
@@ -235,6 +236,7 @@
"metric": "Metrisch", "metric": "Metrisch",
"moderate": "Gemiddeld", "moderate": "Gemiddeld",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Berg", "mountain": "Berg",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Minimaal {n} tekens", "must-be-at-least-n-characters-long": "Minimaal {n} tekens",
@@ -313,6 +315,8 @@
"radius": "Straal", "radius": "Straal",
"railway-station": "", "railway-station": "",
"read-more": "Lees meer", "read-more": "Lees meer",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Registreren", "register": "Registreren",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Route verwijderd van", "removed-trail-from": "Route verwijderd van",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Tonen op kaart", "show-on-map": "Tonen op kaart",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Bewaar je avonturen!", "slogan": "Bewaar je avonturen!",
"slope": "helling", "slope": "helling",
"someone": "Iemand", "someone": "Iemand",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Stwórz nową listę", "create-new-list": "Stwórz nową listę",
"create-waypoint": "Utwórz punkt trasy", "create-waypoint": "Utwórz punkt trasy",
"creation-date": "Data dodania", "creation-date": "Data dodania",
"crop": "",
"cross": "Krzyż", "cross": "Krzyż",
"current-password": "Obecne hasło", "current-password": "Obecne hasło",
"cycling": "Rower", "cycling": "Rower",
@@ -235,6 +236,7 @@
"metric": "Metryczne", "metric": "Metryczne",
"moderate": "Średni", "moderate": "Średni",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Góra", "mountain": "Góra",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Długość musi wynosić przynajmniej {n} znaków", "must-be-at-least-n-characters-long": "Długość musi wynosić przynajmniej {n} znaków",
@@ -313,6 +315,8 @@
"radius": "Promień", "radius": "Promień",
"railway-station": "", "railway-station": "",
"read-more": "Czytaj dalej", "read-more": "Czytaj dalej",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Zarejestruj", "register": "Zarejestruj",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Usunięto szlak z", "removed-trail-from": "Usunięto szlak z",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Pokaż na mapie", "show-on-map": "Pokaż na mapie",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Zapisz swoją wyprawę!", "slogan": "Zapisz swoją wyprawę!",
"slope": "Nachylenie", "slope": "Nachylenie",
"someone": "Ktoś", "someone": "Ktoś",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Criar nova lista", "create-new-list": "Criar nova lista",
"create-waypoint": "Create waypoint", "create-waypoint": "Create waypoint",
"creation-date": "Data de criação", "creation-date": "Data de criação",
"crop": "",
"cross": "Cross", "cross": "Cross",
"current-password": "Senha atual", "current-password": "Senha atual",
"cycling": "Ciclismo", "cycling": "Ciclismo",
@@ -235,6 +236,7 @@
"metric": "Métrica", "metric": "Métrica",
"moderate": "Moderado", "moderate": "Moderado",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Mountain", "mountain": "Mountain",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Deve ter pelo menos {n} caracteres", "must-be-at-least-n-characters-long": "Deve ter pelo menos {n} caracteres",
@@ -313,6 +315,8 @@
"radius": "Raio", "radius": "Raio",
"railway-station": "", "railway-station": "",
"read-more": "Ler mais", "read-more": "Ler mais",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Registo", "register": "Registo",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Trilha removida de", "removed-trail-from": "Trilha removida de",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Mostrar no mapa", "show-on-map": "Mostrar no mapa",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Guarde as suas aventuras!", "slogan": "Guarde as suas aventuras!",
"slope": "Inclinação", "slope": "Inclinação",
"someone": "Someone", "someone": "Someone",

View File

@@ -81,6 +81,7 @@
"create-new-list": "Создать новый список", "create-new-list": "Создать новый список",
"create-waypoint": "Создать путевую точку", "create-waypoint": "Создать путевую точку",
"creation-date": "Дата создания", "creation-date": "Дата создания",
"crop": "",
"cross": "Циклокросс", "cross": "Циклокросс",
"current-password": "Текущий пароль", "current-password": "Текущий пароль",
"cycling": "Велосипед", "cycling": "Велосипед",
@@ -235,6 +236,7 @@
"metric": "Метрическая", "metric": "Метрическая",
"moderate": "Средний", "moderate": "Средний",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Горный", "mountain": "Горный",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "Минимум {n} символов", "must-be-at-least-n-characters-long": "Минимум {n} символов",
@@ -313,6 +315,8 @@
"radius": "Радиус", "radius": "Радиус",
"railway-station": "", "railway-station": "",
"read-more": "Подробнее", "read-more": "Подробнее",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "Регистрация", "register": "Регистрация",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "Трек удалён из", "removed-trail-from": "Трек удалён из",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "Показать на карте", "show-on-map": "Показать на карте",
"shower": "", "shower": "",
"skiing": "",
"slogan": "Сохраняйте ваши приключения!", "slogan": "Сохраняйте ваши приключения!",
"slope": "Уклон", "slope": "Уклон",
"someone": "Кто-то", "someone": "Кто-то",

View File

@@ -81,6 +81,7 @@
"create-new-list": "创建新列表", "create-new-list": "创建新列表",
"create-waypoint": "Create waypoint", "create-waypoint": "Create waypoint",
"creation-date": "创建日期", "creation-date": "创建日期",
"crop": "",
"cross": "Cross", "cross": "Cross",
"current-password": "当前密码", "current-password": "当前密码",
"cycling": "骑行", "cycling": "骑行",
@@ -235,6 +236,7 @@
"metric": "公制", "metric": "公制",
"moderate": "中等", "moderate": "中等",
"more": "More", "more": "More",
"more-route-settings": "",
"mountain": "Mountain", "mountain": "Mountain",
"mountain-pass": "", "mountain-pass": "",
"must-be-at-least-n-characters-long": "长度至少 {n} 字符", "must-be-at-least-n-characters-long": "长度至少 {n} 字符",
@@ -313,6 +315,8 @@
"radius": "半径", "radius": "半径",
"railway-station": "", "railway-station": "",
"read-more": "阅读更多", "read-more": "阅读更多",
"recalculate-elevation-data": "",
"recalculating-elevation-data-hint": "",
"register": "注册", "register": "注册",
"remote-users-cannot-edit": "Remote users cannot edit", "remote-users-cannot-edit": "Remote users cannot edit",
"removed-trail-from": "路线已删除自", "removed-trail-from": "路线已删除自",
@@ -366,6 +370,7 @@
"show-less": "Show less", "show-less": "Show less",
"show-on-map": "地图中展示", "show-on-map": "地图中展示",
"shower": "", "shower": "",
"skiing": "",
"slogan": "保存你的冒险!", "slogan": "保存你的冒险!",
"slope": "坡度", "slope": "坡度",
"someone": "Someone", "someone": "Someone",

View File

@@ -12,6 +12,7 @@ class GpxMetricsComputation {
totalElevationLossSmoothed = 0; totalElevationLossSmoothed = 0;
totalDistance = 0; totalDistance = 0;
totalDistanceSmoothed = 0; totalDistanceSmoothed = 0;
cumulativeDistance: number[] = []
constructor(thresholdXY_m: number, thresholdZ_m: number) { constructor(thresholdXY_m: number, thresholdZ_m: number) {
this.thresholdXY_m = thresholdXY_m; this.thresholdXY_m = thresholdXY_m;
@@ -35,6 +36,7 @@ class GpxMetricsComputation {
); );
this.totalDistance += distance; this.totalDistance += distance;
this.cumulativeDistance.push(this.totalDistance)
this.lastFilteredPointXY = point; this.lastFilteredPointXY = point;

View File

@@ -25,6 +25,7 @@ type GPXFeature = {
centroid: { lat: number; lon: number }; centroid: { lat: number; lon: number };
boundingBox: { minLat: number; maxLat: number; minLon: number; maxLon: number }; boundingBox: { minLat: number; maxLat: number; minLon: number; maxLon: number };
distance: number; distance: number;
cumulativeDistance: number[]
elevationGain?: number; elevationGain?: number;
elevationLoss?: number; elevationLoss?: number;
duration: number; duration: number;
@@ -146,6 +147,7 @@ export default class GPX {
centroid, centroid,
boundingBox, boundingBox,
distance: totalDistance, distance: totalDistance,
cumulativeDistance: metrics.cumulativeDistance,
elevationGain: totalElevationGain, elevationGain: totalElevationGain,
elevationLoss: totalElevationLoss, elevationLoss: totalElevationLoss,
duration: Math.abs(totalDuration), duration: Math.abs(totalDuration),
@@ -153,6 +155,20 @@ export default class GPX {
} }
} }
flatten() {
const points: Waypoint[] = [];
this.trk?.forEach(track => {
track.trkseg?.forEach(segment => {
segment.trkpt?.forEach(pt => {
points.push(pt);
});
});
});
return points;
}
private generateMinHash(points: Waypoint[]): string { private generateMinHash(points: Waypoint[]): string {
const hashes = points.map(pt => geohash.encode(pt.$.lat, pt.$.lon)); const hashes = points.map(pt => geohash.encode(pt.$.lat, pt.$.lon));
return hashes.sort().join('').slice(0, 10); return hashes.sort().join('').slice(0, 10);

View File

@@ -10,16 +10,28 @@ import { _ } from "svelte-i18n";
const emtpyTrack: Track = { trkseg: [] } const emtpyTrack: Track = { trkseg: [] }
export let route: GPX = new GPX({ trk: [emtpyTrack] });
export let anchors: ValhallaAnchor[] = []; class ValhallaStore {
route: GPX = $state(new GPX({ trk: [emtpyTrack] }));
anchors: ValhallaAnchor[] = $state([]);
}
export const valhallaStore = new ValhallaStore();
export function clearRoute() { export function clearRoute() {
route = new GPX({ trk: [emtpyTrack] }); valhallaStore.route = new GPX({ trk: [emtpyTrack] });
anchors = []; }
export function clearAnchors() {
for (const anchor of valhallaStore.anchors) {
anchor.marker?.remove();
}
valhallaStore.anchors = [];
} }
export function setRoute(newRoute: GPX) { export function setRoute(newRoute: GPX) {
route = newRoute valhallaStore.route = newRoute
} }
export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number, options: RoutingOptions) { export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number, options: RoutingOptions) {
@@ -81,41 +93,41 @@ export async function insertIntoRoute(waypoints: Waypoint[], index?: number) {
const segment = new TrackSegment({ trkpt: waypoints }) const segment = new TrackSegment({ trkpt: waypoints })
if (index) { if (index) {
route.trk?.at(0)?.trkseg?.splice(index, 0, segment); valhallaStore.route.trk?.at(0)?.trkseg?.splice(index, 0, segment);
} else { } else {
route.trk?.at(0)?.trkseg?.push(segment); valhallaStore.route.trk?.at(0)?.trkseg?.push(segment);
} }
route.features = route.getTotals(); valhallaStore.route.features = valhallaStore.route.getTotals();
} }
export async function editRoute(index: number, waypoints: Waypoint[]) { export async function editRoute(index: number, waypoints: Waypoint[]) {
const segment = route.trk?.at(0)?.trkseg?.at(index) const segment = valhallaStore.route.trk?.at(0)?.trkseg?.at(index)
if (segment) { if (segment) {
segment.trkpt = waypoints segment.trkpt = waypoints
} }
route.features = route.getTotals(); valhallaStore.route.features = valhallaStore.route.getTotals();
} }
export function deleteFromRoute(index: number) { export function deleteFromRoute(index: number) {
route.trk?.at(0)?.trkseg?.splice(index, 1); valhallaStore.route.trk?.at(0)?.trkseg?.splice(index, 1);
route.features = route.getTotals(); valhallaStore.route.features = valhallaStore.route.getTotals();
} }
export function reverseRoute() { export function reverseRoute() {
for (const trk of route.trk ?? []) { for (const trk of valhallaStore.route.trk ?? []) {
for (const seg of trk.trkseg ?? []) { for (const seg of trk.trkseg ?? []) {
seg.trkpt?.reverse() seg.trkpt?.reverse()
} }
trk.trkseg?.reverse() trk.trkseg?.reverse()
} }
route.trk?.reverse() valhallaStore.route.trk?.reverse()
route.features = route.getTotals(); valhallaStore.route.features = valhallaStore.route.getTotals();
anchors.reverse(); valhallaStore.anchors.reverse();
anchors.forEach((a, i) => { valhallaStore.anchors.forEach((a, i) => {
if (!a.marker) { if (!a.marker) {
return; return;
} }
@@ -126,15 +138,15 @@ export function reverseRoute() {
._content.getElementsByTagName("h5")[0]; ._content.getElementsByTagName("h5")[0];
if (anchorPopupHeading) { if (anchorPopupHeading) {
anchorPopupHeading.textContent = anchorPopupHeading.textContent =
get(_)("route-point") + " #" + (i + 1); get(_)("valhallaStore.route-point") + " #" + (i + 1);
} }
}); });
} }
export function resetRoute() { export function resetRoute() {
route = new GPX({ trk: [emtpyTrack] }); valhallaStore.route = new GPX({ trk: [{ ...emtpyTrack }] });
anchors.forEach((a) => { valhallaStore.anchors.forEach((a) => {
if (!a.marker) { if (!a.marker) {
return; return;
} }
@@ -142,13 +154,17 @@ export function resetRoute() {
a.marker.remove(); a.marker.remove();
}) })
anchors = [] valhallaStore.anchors = []
}
export async function recalculateHeight() {
await valhallaStore.route.correctElevation();
} }
export function normalizeRouteTime() { export function normalizeRouteTime() {
let currentTime = new Date(); let currentTime = new Date();
for (const seg of route.trk?.at(0)?.trkseg ?? []) { for (const seg of valhallaStore.route.trk?.at(0)?.trkseg ?? []) {
if (!seg.trkpt?.length) { if (!seg.trkpt?.length) {
continue continue

View File

@@ -1,6 +1,5 @@
import GPX from "$lib/models/gpx/gpx"; import GPX from "$lib/models/gpx/gpx";
import { Trail } from "$lib/models/trail"; import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import { gpx, kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON"; import { gpx, kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
import cryptoRandomString from "crypto-random-string"; import cryptoRandomString from "crypto-random-string";
//@ts-ignore //@ts-ignore
@@ -16,6 +15,7 @@ import * as xmldom from 'xmldom';
import { bbox, splitMultiLineStringToLineStrings } from "./geojson_util"; import { bbox, splitMultiLineStringToLineStrings } from "./geojson_util";
import { trails_show } from "$lib/stores/trail_store"; import { trails_show } from "$lib/stores/trail_store";
import { handleFromRecordWithIRI } from "./activitypub_util"; import { handleFromRecordWithIRI } from "./activitypub_util";
import { Waypoint } from "$lib/models/waypoint";
export async function gpx2trail(gpxString: string, fallbackName?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) { export async function gpx2trail(gpxString: string, fallbackName?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
@@ -25,11 +25,11 @@ export async function gpx2trail(gpxString: string, fallbackName?: string, f: (ur
if (gpx instanceof Error) { if (gpx instanceof Error) {
throw gpx; throw gpx;
} }
try { // try {
await gpx.correctElevation(f) // await gpx.correctElevation(f)
} catch(e) { // } catch(e) {
console.warn("Unable to correct elevation: " + e) // console.warn("Unable to correct elevation: " + e)
} // }
const trail = new Trail(""); const trail = new Trail("");
@@ -360,3 +360,45 @@ export function toGeoJson(gpxData: string) {
geojson.bbox = bbox(geojson) geojson.bbox = bbox(geojson)
return geojson return geojson
} }
export function cropGPX(start: GPXWaypoint, end: GPXWaypoint, gpx: GPX): GPX {
let foundStart = false;
let done = false;
const croppedTrk = gpx.trk?.map(track => {
const croppedSegments: TrackSegment[] = [];
track.trkseg?.forEach(seg => {
const newPoints: GPXWaypoint[] = [];
for (const pt of seg.trkpt ?? []) {
if (!foundStart) {
if (pt === start) {
foundStart = true;
newPoints.push(pt);
}
continue;
}
if (foundStart && !done) {
newPoints.push(pt);
if (pt === end) {
done = true;
break;
}
}
}
if (newPoints.length > 0) {
croppedSegments.push({ trkpt: newPoints });
}
});
return {
...track,
trkseg: croppedSegments,
};
}).filter(track => track.trkseg.length > 0);
return new GPX({ ...gpx, trk: croppedTrk ?? [], })
}

View File

@@ -13,9 +13,9 @@ import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
import { icons } from "./icon_util"; import { icons } from "./icon_util";
export class FontawesomeMarker extends M.Marker { export class FontawesomeMarker extends M.Marker {
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) { constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, style?: string, id?: string }, markerOptions?: M.MarkerOptions) {
const element = document.createElement('div') const element = document.createElement('div')
element.className = `cursor-pointer flex items-center justify-center w-${options.width ?? 7} aspect-square ${options.backgroundColor ?? "bg-gray-500"} rounded-full text-${options.fontSize ?? "normal"}` element.className = `cursor-pointer flex items-center justify-center w-${options.width ?? 7} aspect-square ${options.backgroundColor ?? "bg-gray-500"} rounded-full text-${options.fontSize ?? "normal"} ${options.style ?? ""}`
element.id = options.id ?? ""; element.id = options.id ?? "";
super({ element: element, ...markerOptions }); super({ element: element, ...markerOptions });

View File

@@ -4,7 +4,6 @@
import Datepicker from "$lib/components/base/datepicker.svelte"; import Datepicker from "$lib/components/base/datepicker.svelte";
import Select from "$lib/components/base/select.svelte"; import Select from "$lib/components/base/select.svelte";
import TextField from "$lib/components/base/text_field.svelte"; import TextField from "$lib/components/base/text_field.svelte";
import Textarea from "$lib/components/base/textarea.svelte";
import Toggle from "$lib/components/base/toggle.svelte"; import Toggle from "$lib/components/base/toggle.svelte";
import ListSelectModal from "$lib/components/list/list_select_modal.svelte"; import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte"; import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
@@ -17,6 +16,7 @@
import { TrailCreateSchema } from "$lib/models/api/trail_schema.js"; import { TrailCreateSchema } from "$lib/models/api/trail_schema.js";
import { WaypointCreateSchema } from "$lib/models/api/waypoint_schema.js"; import { WaypointCreateSchema } from "$lib/models/api/waypoint_schema.js";
import GPX from "$lib/models/gpx/gpx"; import GPX from "$lib/models/gpx/gpx";
import GPXWaypoint from "$lib/models/gpx/waypoint";
import type { List } from "$lib/models/list"; import type { List } from "$lib/models/list";
import { SummitLog } from "$lib/models/summit_log"; import { SummitLog } from "$lib/models/summit_log";
import { Trail } from "$lib/models/trail"; import { Trail } from "$lib/models/trail";
@@ -35,26 +35,27 @@
trails_update, trails_update,
} from "$lib/stores/trail_store.js"; } from "$lib/stores/trail_store.js";
import { import {
anchors, valhallaStore,
calculateRouteBetween, calculateRouteBetween,
clearAnchors,
clearRoute, clearRoute,
deleteFromRoute, deleteFromRoute,
editRoute, editRoute,
insertIntoRoute, insertIntoRoute,
normalizeRouteTime, normalizeRouteTime,
recalculateHeight,
resetRoute, resetRoute,
reverseRoute, reverseRoute,
route,
setRoute, setRoute,
} from "$lib/stores/valhalla_store"; } from "$lib/stores/valhalla_store.svelte.js";
import { waypoint } from "$lib/stores/waypoint_store"; import { waypoint } from "$lib/stores/waypoint_store";
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util"; import { getFileURL } from "$lib/util/file_util";
import { import {
formatDistance, formatDistance,
formatElevation, formatElevation,
formatTimeHHMM, formatTimeHHMM,
} from "$lib/util/format_util"; } from "$lib/util/format_util";
import { fromFile, gpx2trail } from "$lib/util/gpx_util"; import { cropGPX, fromFile, gpx2trail } from "$lib/util/gpx_util";
import { page } from "$app/state"; import { page } from "$app/state";
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg"; import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
@@ -63,10 +64,11 @@
type ComboboxItem, type ComboboxItem,
} from "$lib/components/base/combobox.svelte"; } from "$lib/components/base/combobox.svelte";
import type { DropdownItem } from "$lib/components/base/dropdown.svelte"; import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
import Editor from "$lib/components/base/editor.svelte";
import Search, { import Search, {
type SearchItem, type SearchItem,
} from "$lib/components/base/search.svelte"; } from "$lib/components/base/search.svelte";
import RoutingOptionsPopup from "$lib/components/trail/routing_options_popup.svelte"; import RouteEditor from "$lib/components/trail/route_editor.svelte";
import { TagCreateSchema } from "$lib/models/api/tag_schema.js"; import { TagCreateSchema } from "$lib/models/api/tag_schema.js";
import { convertDMSToDD } from "$lib/models/gpx/utils.js"; import { convertDMSToDD } from "$lib/models/gpx/utils.js";
import { Tag } from "$lib/models/tag.js"; import { Tag } from "$lib/models/tag.js";
@@ -76,10 +78,12 @@
} from "$lib/stores/search_store.js"; } from "$lib/stores/search_store.js";
import { tags_index } from "$lib/stores/tag_store.js"; import { tags_index } from "$lib/stores/tag_store.js";
import { theme } from "$lib/stores/theme_store.js"; import { theme } from "$lib/stores/theme_store.js";
import { currentUser } from "$lib/stores/user_store.js";
import { getIconForLocation } from "$lib/util/icon_util.js"; import { getIconForLocation } from "$lib/util/icon_util.js";
import { import {
createAnchorMarker, createAnchorMarker,
createEditTrailMapPopup, createEditTrailMapPopup,
FontawesomeMarker,
} from "$lib/util/maplibre_util"; } from "$lib/util/maplibre_util";
import EXIF from "$lib/vendor/exif-js/exif.js"; import EXIF from "$lib/vendor/exif-js/exif.js";
import { validator } from "@felte/validator-zod"; import { validator } from "@felte/validator-zod";
@@ -89,10 +93,8 @@
import { onMount, untrack } from "svelte"; import { onMount, untrack } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { backInOut } from "svelte/easing"; import { backInOut } from "svelte/easing";
import { scale } from "svelte/transition"; import { slide } from "svelte/transition";
import { z } from "zod"; import { z } from "zod";
import { currentUser } from "$lib/stores/user_store.js";
import Editor from "$lib/components/base/editor.svelte";
let { data } = $props(); let { data } = $props();
@@ -119,6 +121,13 @@
let searchDropdownItems: SearchItem[] = $state([]); let searchDropdownItems: SearchItem[] = $state([]);
let cropStartMarker: FontawesomeMarker;
let cropEndMarker: FontawesomeMarker;
let flatRoute: GPXWaypoint[] = $derived(valhallaStore.route.flatten())
let croppedGPX: GPX | null = null;
const ClientTrailCreateSchema = TrailCreateSchema.extend({ const ClientTrailCreateSchema = TrailCreateSchema.extend({
expand: z expand: z
.object({ .object({
@@ -199,13 +208,13 @@
if ( if (
(!form.lat || !form.lon) && (!form.lat || !form.lon) &&
route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0) valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)
) { ) {
form.lat = route.trk form.lat = valhallaStore.route.trk
?.at(0) ?.at(0)
?.trkseg?.at(0) ?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lat; ?.trkpt?.at(0)?.$.lat;
form.lon = route.trk form.lon = valhallaStore.route.trk
?.at(0) ?.at(0)
?.trkseg?.at(0) ?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lon; ?.trkpt?.at(0)?.$.lon;
@@ -344,6 +353,7 @@
} }
setRoute(parseResult.gpx); setRoute(parseResult.gpx);
initRouteAnchors(parseResult.gpx); initRouteAnchors(parseResult.gpx);
initCropMarkers();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -370,12 +380,10 @@
} }
function clearAnchorMarker() { function clearAnchorMarker() {
for (const anchor of anchors) { clearAnchors();
anchor.marker?.remove();
}
} }
function initRouteAnchors(gpx: GPX) { function initRouteAnchors(gpx: GPX, addToMap: boolean = false) {
const segments = gpx.trk?.at(0)?.trkseg ?? []; const segments = gpx.trk?.at(0)?.trkseg ?? [];
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
@@ -386,21 +394,67 @@
addAnchor( addAnchor(
points[0].$.lat!, points[0].$.lat!,
points[0].$.lon!, points[0].$.lon!,
anchors.length, valhallaStore.anchors.length,
false, addToMap,
); );
} }
if (i == segments.length - 1) { if (i == segments.length - 1) {
addAnchor( addAnchor(
points[points.length - 1].$.lat!, points[points.length - 1].$.lat!,
points[points.length - 1].$.lon!, points[points.length - 1].$.lon!,
anchors.length, valhallaStore.anchors.length,
false, addToMap,
); );
} }
} }
} }
function initCropMarkers() {
const routeStartPoint: M.LngLatLike = [
valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)?.$.lon ?? 0,
valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)?.$.lat ?? 0,
];
const routeEndPoint: M.LngLatLike = [
valhallaStore.route.trk?.at(-1)?.trkseg?.at(-1)?.trkpt?.at(-1)?.$.lon ?? 0,
valhallaStore.route.trk?.at(-1)?.trkseg?.at(-1)?.trkpt?.at(-1)?.$.lat ?? 0,
];
if (!cropStartMarker || !cropEndMarker) {
cropStartMarker = new FontawesomeMarker(
{
id: "crop-start-marker",
icon: "fa-regular fa-circle",
fontSize: "xs",
style: "z-10",
width: 4,
backgroundColor: "bg-primary",
fontColor: "white",
},
{},
);
cropEndMarker = new FontawesomeMarker(
{
id: "crop-end-marker",
icon: "fa fa-flag-checkered",
fontSize: "xs",
style: "z-10",
width: 4,
backgroundColor: "bg-primary",
fontColor: "white",
},
{},
);
cropStartMarker
.setOpacity("0")
.setLngLat(routeStartPoint)
.addTo(map!);
cropEndMarker.setOpacity("0").setLngLat(routeEndPoint).addTo(map!);
} else {
cropStartMarker.setLngLat(routeStartPoint);
cropEndMarker.setLngLat(routeEndPoint);
}
}
function openMarkerPopup(waypoint: Waypoint) { function openMarkerPopup(waypoint: Waypoint) {
waypoint.marker?.togglePopup(); waypoint.marker?.togglePopup();
} }
@@ -545,25 +599,25 @@
return; return;
} }
drawingActive = true; drawingActive = true;
if (!route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.length) { if (!valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.length) {
} }
for (const anchor of anchors) { for (const anchor of valhallaStore.anchors) {
anchor.marker?.addTo(map); anchor.marker?.addTo(map);
} }
} }
async function stopDrawing() { async function stopDrawing() {
drawingActive = false; drawingActive = false;
for (const anchor of anchors) { for (const anchor of valhallaStore.anchors) {
anchor.marker?.remove(); anchor.marker?.remove();
} }
if (route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)) { if (valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)) {
$formData.lat = route.trk $formData.lat = valhallaStore.route.trk
?.at(0) ?.at(0)
?.trkseg?.at(0) ?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lat; ?.trkpt?.at(0)?.$.lat;
$formData.lon = route.trk $formData.lon = valhallaStore.route.trk
?.at(0) ?.at(0)
?.trkseg?.at(0) ?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lon; ?.trkpt?.at(0)?.$.lon;
@@ -593,9 +647,9 @@
}); });
mapPopup.addTo(map!); mapPopup.addTo(map!);
} else { } else {
const anchorCount = anchors.length; const anchorCount = valhallaStore.anchors.length;
if (anchorCount == 0) { if (anchorCount == 0) {
addAnchor(e.lngLat.lat, e.lngLat.lng, anchors.length); addAnchor(e.lngLat.lat, e.lngLat.lng, valhallaStore.anchors.length);
} else { } else {
await addAnchorAndRecalculate(e.lngLat.lat, e.lngLat.lng); await addAnchorAndRecalculate(e.lngLat.lat, e.lngLat.lng);
} }
@@ -603,8 +657,8 @@
} }
async function addAnchorAndRecalculate(lat: number, lon: number) { async function addAnchorAndRecalculate(lat: number, lon: number) {
const previousAnchor = anchors[anchors.length - 1]; const previousAnchor = valhallaStore.anchors[valhallaStore.anchors.length - 1];
const anchor = addAnchor(lat, lon, anchors.length); const anchor = addAnchor(lat, lon, valhallaStore.anchors.length);
const markerText = startAnchorLoading(anchor); const markerText = startAnchorLoading(anchor);
try { try {
const routeWaypoints = await calculateRouteBetween( const routeWaypoints = await calculateRouteBetween(
@@ -645,10 +699,10 @@
lon, lon,
index + 1, index + 1,
() => { () => {
removeAnchor(anchors.findIndex((a) => a.id == anchor.id)); removeAnchor(valhallaStore.anchors.findIndex((a) => a.id == anchor.id));
}, },
() => { () => {
const thisAnchor = anchors.find((a) => a.id == anchor.id); const thisAnchor = valhallaStore.anchors.find((a) => a.id == anchor.id);
addAnchorAndRecalculate( addAnchorAndRecalculate(
thisAnchor?.lat ?? lat, thisAnchor?.lat ?? lat,
thisAnchor?.lon ?? lon, thisAnchor?.lon ?? lon,
@@ -666,7 +720,7 @@
anchor.lat = position.lat; anchor.lat = position.lat;
anchor.lon = position.lng; anchor.lon = position.lng;
await recalculateRoute( await recalculateRoute(
anchors.findIndex((a) => a.id == anchor.id), valhallaStore.anchors.findIndex((a) => a.id == anchor.id),
); );
draggingMarker = false; draggingMarker = false;
}, },
@@ -675,7 +729,7 @@
marker.addTo(map); marker.addTo(map);
} }
anchor.marker = marker; anchor.marker = marker;
anchors.splice(index, 0, anchor); valhallaStore.anchors.splice(index, 0, anchor);
return anchor; return anchor;
} }
@@ -709,10 +763,10 @@
if (!drawingActive) { if (!drawingActive) {
return; return;
} }
anchors[anchorIndex]?.marker?.remove(); valhallaStore.anchors[anchorIndex]?.marker?.remove();
anchors.splice(anchorIndex, 1); valhallaStore.anchors.splice(anchorIndex, 1);
for (let i = anchorIndex; i < anchors.length; i++) { for (let i = anchorIndex; i < valhallaStore.anchors.length; i++) {
const anchor = anchors[i]; const anchor = valhallaStore.anchors[i];
const markerIcon = anchor.marker?.getElement(); const markerIcon = anchor.marker?.getElement();
if (markerIcon) { if (markerIcon) {
const markerText = markerIcon.textContent ?? "0"; const markerText = markerIcon.textContent ?? "0";
@@ -730,7 +784,7 @@
if ($formData.expand?.gpx_data) { if ($formData.expand?.gpx_data) {
updateTrailWithRouteData(); updateTrailWithRouteData();
} }
} else if (anchorIndex == anchors.length) { } else if (anchorIndex == valhallaStore.anchors.length) {
deleteFromRoute(anchorIndex - 1); deleteFromRoute(anchorIndex - 1);
updateTrailWithRouteData(); updateTrailWithRouteData();
} else { } else {
@@ -740,17 +794,17 @@
} }
async function recalculateRoute(anchorIndex: number) { async function recalculateRoute(anchorIndex: number) {
const markerText = startAnchorLoading(anchors[anchorIndex]); const markerText = startAnchorLoading(valhallaStore.anchors[anchorIndex]);
const anchor = anchors[anchorIndex]; const anchor = valhallaStore.anchors[anchorIndex];
if (!anchor) { if (!anchor) {
return; return;
} }
let nextRouteSegment; let nextRouteSegment;
let previousRouteSegment; let previousRouteSegment;
try { try {
if (anchorIndex < anchors.length - 1) { if (anchorIndex < valhallaStore.anchors.length - 1) {
const nextAnchor = anchors[anchorIndex + 1]; const nextAnchor = valhallaStore.anchors[anchorIndex + 1];
nextRouteSegment = await calculateRouteBetween( nextRouteSegment = await calculateRouteBetween(
anchor.lat, anchor.lat,
@@ -761,7 +815,7 @@
); );
} }
if (anchorIndex > 0) { if (anchorIndex > 0) {
const previousAnchor = anchors[anchorIndex - 1]; const previousAnchor = valhallaStore.anchors[anchorIndex - 1];
previousRouteSegment = await calculateRouteBetween( previousRouteSegment = await calculateRouteBetween(
previousAnchor.lat, previousAnchor.lat,
previousAnchor.lon, previousAnchor.lon,
@@ -789,7 +843,7 @@
type: "error", type: "error",
}); });
} finally { } finally {
stopAnchorLoading(anchors[anchorIndex], markerText); stopAnchorLoading(valhallaStore.anchors[anchorIndex], markerText);
} }
} }
@@ -807,8 +861,8 @@
); );
const markerText = startAnchorLoading(anchor); const markerText = startAnchorLoading(anchor);
for (let i = data.segment + 2; i < anchors.length; i++) { for (let i = data.segment + 2; i < valhallaStore.anchors.length; i++) {
const anchor = anchors[i]; const anchor = valhallaStore.anchors[i];
const markerIcon = anchor.marker?.getElement(); const markerIcon = anchor.marker?.getElement();
if (markerIcon) { if (markerIcon) {
const markerText = markerIcon.textContent ?? "0"; const markerText = markerIcon.textContent ?? "0";
@@ -821,8 +875,8 @@
$_("route-point") + " #" + newIndex; $_("route-point") + " #" + newIndex;
} }
} }
const previousAnchor = anchors[data.segment]; const previousAnchor = valhallaStore.anchors[data.segment];
const nextAnchor = anchors[data.segment + 2]; const nextAnchor = valhallaStore.anchors[data.segment + 2];
try { try {
const previousRouteSegment = await calculateRouteBetween( const previousRouteSegment = await calculateRouteBetween(
@@ -868,14 +922,97 @@
updateTrailWithRouteData(); updateTrailWithRouteData();
} }
function updateTrailWithRouteData() { async function recalculateElevationData() {
overwriteGPX = true; await recalculateHeight();
const totals = route.features;
updateTrailWithRouteData();
}
function toggleCropMarkers(active: boolean) {
if (active) {
cropStartMarker?.setOpacity("1");
cropEndMarker?.setOpacity("1");
} else {
cropStartMarker?.setOpacity("0");
cropEndMarker?.setOpacity("0");
}
}
function updateCropMarkers(range: [start: number, end: number]) {
const [start, end] = range;
const targetStartDistance = valhallaStore.route.features.distance * (start / 100);
const [startLon, startLat, startIndex] = getCoordinateAtDistance(
flatRoute,
valhallaStore.route.features.cumulativeDistance,
targetStartDistance,
);
const targetEndDistance = valhallaStore.route.features.distance * (end / 100);
const [endLon, endLat, endIndex] = getCoordinateAtDistance(
flatRoute,
valhallaStore.route.features.cumulativeDistance,
targetEndDistance,
);
cropStartMarker.setLngLat([startLon, startLat]);
cropEndMarker.setLngLat([endLon, endLat]);
croppedGPX = cropGPX(flatRoute[startIndex], flatRoute[endIndex], valhallaStore.route);
const totals = croppedGPX.features;
$formData.distance = totals.distance; $formData.distance = totals.distance;
$formData.duration = totals.duration / 1000; $formData.duration = totals.duration / 1000;
$formData.elevation_gain = totals.elevationGain; $formData.elevation_gain = totals.elevationGain;
$formData.elevation_loss = totals.elevationLoss; $formData.elevation_loss = totals.elevationLoss;
$formData.expand!.gpx_data = route.toString(); }
function confirmCrop() {
if (!croppedGPX) {
return;
}
setRoute(croppedGPX);
updateTrailWithRouteData();
clearAnchorMarker();
initRouteAnchors(croppedGPX, true);
}
function getCoordinateAtDistance(
points: GPXWaypoint[],
cumulative: number[],
target: number,
) {
let low = 0,
high = cumulative.length - 1;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (cumulative[mid] < target) low = mid + 1;
else high = mid;
}
const i = Math.max(1, low);
const prevDist = cumulative[i - 1];
const nextDist = cumulative[i];
const ratio = (target - prevDist) / (nextDist - prevDist);
const prev = points[i - 1];
const next = points[i];
return [
prev.$.lon! + (next.$.lon! - prev.$.lon!) * ratio,
prev.$.lat! + (next.$.lat! - prev.$.lat!) * ratio,
i,
];
}
function updateTrailWithRouteData() {
overwriteGPX = true;
const totals = valhallaStore.route.features;
$formData.distance = totals.distance;
$formData.duration = totals.duration / 1000;
$formData.elevation_gain = totals.elevationGain;
$formData.elevation_loss = totals.elevationLoss;
$formData.expand!.gpx_data = valhallaStore.route.toString();
if (!$formData.id) { if (!$formData.id) {
$formData.id = cryptoRandomString({ length: 15 }); $formData.id = cryptoRandomString({ length: 15 });
@@ -1288,15 +1425,19 @@
<div class="relative"> <div class="relative">
{#if drawingActive} {#if drawingActive}
<div <div
in:scale={{ easing: backInOut }} in:slide={{ easing: backInOut, axis: "x" }}
out:scale={{ easing: backInOut }} out:slide={{ easing: backInOut, axis: "x" }}
class="absolute top-8 left-2 z-50" class="absolute top-8 left-2 z-50"
> >
<RoutingOptionsPopup <RouteEditor
bind:options={routingOptions} bind:options={routingOptions}
onReverse={reverseTrail} onReverse={reverseTrail}
onReset={resetTrail} onReset={resetTrail}
></RoutingOptionsPopup> onCropToggle={toggleCropMarkers}
onCrop={confirmCrop}
onUpdateCropRange={updateCropMarkers}
onRecalculateElevationData={recalculateElevationData}
></RouteEditor>
</div> </div>
{/if} {/if}
<div id="trail-map"> <div id="trail-map">

View File

@@ -9,12 +9,12 @@ export default defineConfig({
ssr: { noExternal: ['three'] }, ssr: { noExternal: ['three'] },
...(process.env.WANDERER_ENV == "dev" ? { ...(process.env.WANDERER_ENV == "dev" ? {
server: { server: {
https: { // https: {
key: fs.readFileSync('.svelte-kit/key.pem'), // key: fs.readFileSync('.svelte-kit/key.pem'),
cert: fs.readFileSync('.svelte-kit/cert.pem') // cert: fs.readFileSync('.svelte-kit/cert.pem')
}, // },
host: true, // true // host: true, // true
port: 443 // 443 // port: 443 // 443
} }
} : {}) } : {})
}); });