adds list editor
This commit is contained in:
@@ -46,6 +46,7 @@
|
|||||||
function handleItemClick(item: SearchItem) {
|
function handleItemClick(item: SearchItem) {
|
||||||
searching = false;
|
searching = false;
|
||||||
dispatch("click", item);
|
dispatch("click", item);
|
||||||
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{#if value.length > 0}
|
{#if value.length > 0}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="btn-icon absolute top-1/2 -translate-y-1/2 right-0 mr-2"
|
class="btn-icon absolute top-1/2 -translate-y-1/2 right-0 mr-2"
|
||||||
on:click={clear}
|
on:click={clear}
|
||||||
in:fade={{ duration: 150 }}
|
in:fade={{ duration: 150 }}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-6 p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer"
|
class="flex items-start gap-6 p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer"
|
||||||
class:bg-menu-item-background-hover={active}
|
class:bg-menu-item-background-hover={active}
|
||||||
>
|
>
|
||||||
{#if list.avatar}
|
{#if list.avatar}
|
||||||
@@ -29,11 +29,22 @@
|
|||||||
<i class="fa fa-table-list text-5xl"></i>
|
<i class="fa fa-table-list text-5xl"></i>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="self-start min-w-0 w-full">
|
<div class="self-start min-w-0 w-full transition-transform">
|
||||||
<div class="flex justify-between items-center ">
|
<div class="flex justify-between items-center">
|
||||||
<h5 class="text-xl font-semibold overflow-hidden overflow-ellipsis">{list.name}</h5>
|
<h5 class="text-xl font-semibold overflow-hidden overflow-ellipsis">
|
||||||
|
{list.name}
|
||||||
|
</h5>
|
||||||
<Dropdown items={dropdownItems} on:change></Dropdown>
|
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-500 text-sm mr-8">{list.description}</p>
|
<p
|
||||||
|
class="text-gray-500 text-sm mr-8 whitespace-pre-wrap {active
|
||||||
|
? ''
|
||||||
|
: 'max-h-24 overflow-hidden text-ellipsis'}"
|
||||||
|
>
|
||||||
|
{!active ? list.description?.substring(0, 100) : list.description}
|
||||||
|
{#if ((list.description?.length ?? 0) > 100) && !active}
|
||||||
|
...
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,38 +5,40 @@
|
|||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
|
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
|
||||||
import type { Map, Marker } from "leaflet";
|
import type { Layer, Map, Marker, Polyline } from "leaflet";
|
||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import Dropdown from "../base/dropdown.svelte";
|
import Dropdown from "../base/dropdown.svelte";
|
||||||
|
|
||||||
export let trail: Trail | null;
|
export let trails: Trail[];
|
||||||
export let markers: Marker[] = [];
|
export let markers: Marker[] = [];
|
||||||
export let map: Map | null = null;
|
export let map: Map | null = null;
|
||||||
export let options: any = {};
|
export let options: any = {};
|
||||||
export let graticule: AutoGraticule | null = null;
|
export let graticule: AutoGraticule | null = null;
|
||||||
export let crosshair: boolean = false;
|
export let crosshair: boolean = false;
|
||||||
|
export let activeTrailIndex: number = 0;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let L: any;
|
let L: any;
|
||||||
let controlElevation: any;
|
let gpxGroup: any;
|
||||||
|
|
||||||
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
|
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
|
||||||
|
|
||||||
$: gpxData = trail?.expand.gpx_data;
|
$: gpxData = trails.map((t) => t.expand.gpx_data);
|
||||||
$: if (gpxData && controlElevation) {
|
$: if (gpxData && gpxGroup) {
|
||||||
controlElevation.updateOptions({
|
gpxGroup._elevation.updateOptions({
|
||||||
autofitBounds: options.autofitBounds ?? true,
|
autofitBounds: options.autofitBounds ?? true,
|
||||||
});
|
});
|
||||||
controlElevation.clear();
|
gpxGroup.clear();
|
||||||
controlElevation.load(gpxData);
|
gpxGroup._tracks = gpxData;
|
||||||
|
gpxGroup.addTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (options) {
|
$: if (options && gpxGroup) {
|
||||||
controlElevation?.updateOptions(options);
|
gpxGroup._elevation.updateOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: hotlineSwitcherItems = [
|
$: hotlineSwitcherItems = [
|
||||||
@@ -66,14 +68,21 @@
|
|||||||
L = (await import("leaflet")).default;
|
L = (await import("leaflet")).default;
|
||||||
await import("leaflet-gpx");
|
await import("leaflet-gpx");
|
||||||
await import("leaflet.awesome-markers");
|
await import("leaflet.awesome-markers");
|
||||||
//@ts-ignore
|
|
||||||
await import("$lib/vendor/leaflet-elevation/src/index.js");
|
await import("$lib/vendor/leaflet-elevation/src/index.js");
|
||||||
|
await import("$lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup");
|
||||||
|
|
||||||
const AutoGraticule = (
|
const AutoGraticule = (
|
||||||
await import("$lib/vendor/leaflet-graticule/leaflet-auto-graticule")
|
await import("$lib/vendor/leaflet-graticule/leaflet-auto-graticule")
|
||||||
).default;
|
).default;
|
||||||
|
|
||||||
map = L.map("map", { preferCanvas: true }).setView(
|
map = L.map("map", {
|
||||||
[trail?.lat ?? 0, trail?.lon ?? 0],
|
preferCanvas: true,
|
||||||
|
plugins: ["/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js"],
|
||||||
|
}).setView(
|
||||||
|
[
|
||||||
|
trails.at(activeTrailIndex)?.lat ?? 0,
|
||||||
|
trails.at(activeTrailIndex)?.lon ?? 0,
|
||||||
|
],
|
||||||
3,
|
3,
|
||||||
);
|
);
|
||||||
map!.attributionControl.setPrefix(false);
|
map!.attributionControl.setPrefix(false);
|
||||||
@@ -103,7 +112,9 @@
|
|||||||
const baseMaps: Record<string, L.TileLayer> = {
|
const baseMaps: Record<string, L.TileLayer> = {
|
||||||
OpenStreetMaps: baseLayer,
|
OpenStreetMaps: baseLayer,
|
||||||
OpenTopoMaps: topoLayer,
|
OpenTopoMaps: topoLayer,
|
||||||
...($page.data.settings as Settings)?.tilesets?.reduce< Record<string, string>>((t, current) => {
|
...($page.data.settings as Settings)?.tilesets?.reduce<
|
||||||
|
Record<string, string>
|
||||||
|
>((t, current) => {
|
||||||
t[current.name] = L.tileLayer(current.url);
|
t[current.name] = L.tileLayer(current.url);
|
||||||
return t;
|
return t;
|
||||||
}, {}),
|
}, {}),
|
||||||
@@ -159,7 +170,7 @@
|
|||||||
lazy: false,
|
lazy: false,
|
||||||
distance: false,
|
distance: false,
|
||||||
direction: true,
|
direction: true,
|
||||||
offset: 500,
|
offset: 1000,
|
||||||
},
|
},
|
||||||
// Toggle "leaflet-edgescale" integration
|
// Toggle "leaflet-edgescale" integration
|
||||||
edgeScale: false,
|
edgeScale: false,
|
||||||
@@ -171,26 +182,6 @@
|
|||||||
wptIcons: false,
|
wptIcons: false,
|
||||||
wptLabels: false,
|
wptLabels: false,
|
||||||
preferCanvas: true,
|
preferCanvas: true,
|
||||||
trkStart: {
|
|
||||||
interactive: false,
|
|
||||||
className: "hihi",
|
|
||||||
icon: L.AwesomeMarkers.icon({
|
|
||||||
icon: "circle-half-stroke",
|
|
||||||
prefix: "fa",
|
|
||||||
markerColor: "cadetblue",
|
|
||||||
iconColor: "white",
|
|
||||||
className: "awesome-marker pointer-events-none",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
trkEnd: {
|
|
||||||
icon: L.AwesomeMarkers.icon({
|
|
||||||
icon: "flag-checkered",
|
|
||||||
prefix: "fa",
|
|
||||||
markerColor: "cadetblue",
|
|
||||||
iconColor: "white",
|
|
||||||
className: "awesome-marker pointer-events-none",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
graticule: false,
|
graticule: false,
|
||||||
drawing: false,
|
drawing: false,
|
||||||
};
|
};
|
||||||
@@ -200,13 +191,34 @@
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
controlElevation = L.control.elevation(elevation_options).addTo(map);
|
gpxGroup = L.gpxGroup(gpxData, {
|
||||||
|
points: [],
|
||||||
|
// points_options: opts.points,
|
||||||
|
elevation: true,
|
||||||
|
elevation_options: elevation_options,
|
||||||
|
flyToBounds: true,
|
||||||
|
distanceMarkers: true,
|
||||||
|
});
|
||||||
|
|
||||||
for (const waypoint of trail?.expand.waypoints ?? []) {
|
const markerLayerGroup = L.layerGroup().addTo(map);
|
||||||
const marker = createMarkerFromWaypoint(L, waypoint);
|
|
||||||
marker.addTo(map!);
|
gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => {
|
||||||
markers.push(marker);
|
markerLayerGroup.clearLayers();
|
||||||
}
|
|
||||||
|
activeTrailIndex = polyline.options.index ?? 0;
|
||||||
|
|
||||||
|
if (polyline._selected) {
|
||||||
|
for (const waypoint of trails.at(activeTrailIndex)?.expand
|
||||||
|
.waypoints ?? []) {
|
||||||
|
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||||
|
marker.addTo(markerLayerGroup!);
|
||||||
|
markers.push(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gpxGroup.addTo(map);
|
||||||
|
// controlElevation = L.control.elevation(elevation_options).addTo(map);
|
||||||
|
|
||||||
if (elevation_options.graticule) {
|
if (elevation_options.graticule) {
|
||||||
graticule = new AutoGraticule();
|
graticule = new AutoGraticule();
|
||||||
@@ -215,14 +227,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
|
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
|
||||||
controlElevation.updateOptions({
|
gpxGroup._elevation.updateOptions({
|
||||||
hotline: metric,
|
hotline: metric,
|
||||||
autofitBounds: false,
|
autofitBounds: false,
|
||||||
});
|
});
|
||||||
selectedMetric = metric;
|
selectedMetric = metric;
|
||||||
localStorage.setItem("gradient", metric.toString());
|
localStorage.setItem("gradient", metric.toString());
|
||||||
controlElevation.clear();
|
gpxGroup.clear();
|
||||||
controlElevation.load(gpxData);
|
gpxGroup._tracks = gpxData;
|
||||||
|
gpxGroup.addTracks();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="flex gap-8 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors"
|
class="flex gap-8 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
|
||||||
>
|
>
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<img class="h-28 w-28 object-cover rounded-xl" src={thumbnail} alt="" />
|
<img class="h-28 w-28 object-cover rounded-xl" src={thumbnail} alt="" />
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "Route entfernt aus",
|
"removed-trail-from": "Route entfernt aus",
|
||||||
"required": "Pflichtfeld",
|
"required": "Pflichtfeld",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "Route speichern",
|
"save-trail": "Route speichern",
|
||||||
"save-your-trail-first": "Route zuerst speichern",
|
"save-your-trail-first": "Route zuerst speichern",
|
||||||
"search-cities": "Städte suchen",
|
"search-cities": "Städte suchen",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "Removed trail from",
|
"removed-trail-from": "Removed trail from",
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"save-list": "Save List",
|
||||||
"save-trail": "Save Trail",
|
"save-trail": "Save Trail",
|
||||||
"save-your-trail-first": "Save your trail first",
|
"save-your-trail-first": "Save your trail first",
|
||||||
"search-cities": "Search cities",
|
"search-cities": "Search cities",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "Enlever l'itinéraire de",
|
"removed-trail-from": "Enlever l'itinéraire de",
|
||||||
"required": "Requis",
|
"required": "Requis",
|
||||||
"save": "Sauvegarder",
|
"save": "Sauvegarder",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "Sauvegarder l'itinéraire",
|
"save-trail": "Sauvegarder l'itinéraire",
|
||||||
"save-your-trail-first": "Enregistrez d'abord votre trace",
|
"save-your-trail-first": "Enregistrez d'abord votre trace",
|
||||||
"search-cities": "Recherche une ville",
|
"search-cities": "Recherche une ville",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "Eltávolított nyomvonal a",
|
"removed-trail-from": "Eltávolított nyomvonal a",
|
||||||
"required": "Kötelező",
|
"required": "Kötelező",
|
||||||
"save": "Mentés",
|
"save": "Mentés",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "Útvonal mentése",
|
"save-trail": "Útvonal mentése",
|
||||||
"save-your-trail-first": "Először mentsd el a nyomvonaladat",
|
"save-your-trail-first": "Először mentsd el a nyomvonaladat",
|
||||||
"search-cities": "Városok keresése",
|
"search-cities": "Városok keresése",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "Percorso rimosso da",
|
"removed-trail-from": "Percorso rimosso da",
|
||||||
"required": "Obbligatorio",
|
"required": "Obbligatorio",
|
||||||
"save": "Salva",
|
"save": "Salva",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "Salva percorso",
|
"save-trail": "Salva percorso",
|
||||||
"save-your-trail-first": "Salva prima il tuo percorso",
|
"save-your-trail-first": "Salva prima il tuo percorso",
|
||||||
"search-cities": "Cerca città",
|
"search-cities": "Cerca città",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "De wandelroute is verwijderd van",
|
"removed-trail-from": "De wandelroute is verwijderd van",
|
||||||
"required": "Verplicht",
|
"required": "Verplicht",
|
||||||
"save": "Bewaren",
|
"save": "Bewaren",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "Wandelroute bewaren",
|
"save-trail": "Wandelroute bewaren",
|
||||||
"save-your-trail-first": "Bewaar eerst je wandelroute",
|
"save-your-trail-first": "Bewaar eerst je wandelroute",
|
||||||
"search-cities": "Zoeken naar steden",
|
"search-cities": "Zoeken naar steden",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "Usunięto ścieżkę z",
|
"removed-trail-from": "Usunięto ścieżkę z",
|
||||||
"required": "Wymagane",
|
"required": "Wymagane",
|
||||||
"save": "Zapisz",
|
"save": "Zapisz",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "Zapisz ścieżkę",
|
"save-trail": "Zapisz ścieżkę",
|
||||||
"save-your-trail-first": "Najpierw zapisz swój ślad",
|
"save-your-trail-first": "Najpierw zapisz swój ślad",
|
||||||
"search-cities": "Szukaj miasta",
|
"search-cities": "Szukaj miasta",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "Trilha removida de",
|
"removed-trail-from": "Trilha removida de",
|
||||||
"required": "Obrigatório",
|
"required": "Obrigatório",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "Guardar trilho",
|
"save-trail": "Guardar trilho",
|
||||||
"save-your-trail-first": "Salve sua trilha primeiro",
|
"save-your-trail-first": "Salve sua trilha primeiro",
|
||||||
"search-cities": "Procurar cidades",
|
"search-cities": "Procurar cidades",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"removed-trail-from": "路线已删除自",
|
"removed-trail-from": "路线已删除自",
|
||||||
"required": "必填",
|
"required": "必填",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
|
"save-list": "",
|
||||||
"save-trail": "保存路线",
|
"save-trail": "保存路线",
|
||||||
"save-your-trail-first": "先保存你的路线",
|
"save-your-trail-first": "先保存你的路线",
|
||||||
"search-cities": "搜索城市",
|
"search-cities": "搜索城市",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export class List {
|
|||||||
constructor(name: string, trails: Trail[], params?: { description?: string, avatar?: string, author?: string }) {
|
constructor(name: string, trails: Trail[], params?: { description?: string, avatar?: string, author?: string }) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.expand = { trails: trails };
|
this.expand = { trails: trails };
|
||||||
|
this.trails = trails.map(t => t.id!);
|
||||||
this.description = params?.description;
|
this.description = params?.description;
|
||||||
this.avatar = params?.description;
|
this.avatar = params?.description;
|
||||||
this.author = params?.author;
|
this.author = params?.author;
|
||||||
|
|||||||
@@ -32,6 +32,23 @@ export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | UR
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function lists_show(id: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
|
const r = await f(`/api/v1/list/${id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
const response = await r.json()
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
throw new ClientResponseError(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
list.set(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export async function lists_create(list: List, avatar?: File) {
|
export async function lists_create(list: List, avatar?: File) {
|
||||||
if (!pb.authStore.model) {
|
if (!pb.authStore.model) {
|
||||||
throw new Error("Unauthenticated");
|
throw new Error("Unauthenticated");
|
||||||
|
|||||||
330
web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js
vendored
Normal file
330
web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js
vendored
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/adoroszlai/joebed/tree/gh-pages
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014- Doroszlai Attila, 2019- Raruto
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
L.Mixin.Selectable = {
|
||||||
|
includes: L.Mixin.Events,
|
||||||
|
|
||||||
|
setSelected: function (s) {
|
||||||
|
var selected = !!s;
|
||||||
|
if (this._selected !== selected) {
|
||||||
|
this._selected = selected;
|
||||||
|
this.fire('selected');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelected: function () {
|
||||||
|
return !!this._selected;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
L.Mixin.Selection = {
|
||||||
|
includes: L.Mixin.Events,
|
||||||
|
|
||||||
|
getSelection: function () {
|
||||||
|
return this._selected;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelection: function (item) {
|
||||||
|
if (this._selected === item) {
|
||||||
|
if (item !== null) {
|
||||||
|
item.setSelected(!item.isSelected());
|
||||||
|
if (!item.isSelected()) {
|
||||||
|
this._selected = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this._selected) {
|
||||||
|
this._selected.setSelected(false);
|
||||||
|
}
|
||||||
|
this._selected = item;
|
||||||
|
if (this._selected) {
|
||||||
|
this._selected.setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.fire('selection_changed', { polyline: item });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
L.GeoJSON.include(L.Mixin.Selectable);
|
||||||
|
|
||||||
|
export const GpxGroup = L.GpxGroup = L.Class.extend({
|
||||||
|
options: {
|
||||||
|
highlight: {
|
||||||
|
opacity: 1,
|
||||||
|
weight: 6,
|
||||||
|
},
|
||||||
|
points: [],
|
||||||
|
points_options: {
|
||||||
|
icon: {
|
||||||
|
iconUrl: '../images/elevation-poi.png',
|
||||||
|
iconSize: [12, 12],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flyToBounds: true,
|
||||||
|
elevation: true,
|
||||||
|
elevation_options: {
|
||||||
|
theme: 'lightblue-theme',
|
||||||
|
detached: true,
|
||||||
|
elevationDiv: '#elevation',
|
||||||
|
},
|
||||||
|
distanceMarkers: true,
|
||||||
|
distanceMarkers_options: {
|
||||||
|
lazy: true,
|
||||||
|
distance: false,
|
||||||
|
direction: true,
|
||||||
|
offset: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function (tracks, options) {
|
||||||
|
|
||||||
|
L.Util.setOptions(this, options);
|
||||||
|
|
||||||
|
this._count = 0;
|
||||||
|
this._loadedCount = 0;
|
||||||
|
this._tracks = tracks;
|
||||||
|
this._layers = L.featureGroup();
|
||||||
|
this._markers = L.featureGroup();
|
||||||
|
this._hotline = L.featureGroup();
|
||||||
|
this._elevation = L.control.elevation(this.options.elevation_options);
|
||||||
|
|
||||||
|
this.options.points.forEach((poi) =>
|
||||||
|
L
|
||||||
|
.marker(poi.latlng, { icon: L.icon(this.options.points_options.icon) })
|
||||||
|
.bindTooltip(poi.name, { direction: 'auto' }).addTo(this._markers)
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getBounds: function () {
|
||||||
|
return this._layers.getBounds();
|
||||||
|
},
|
||||||
|
|
||||||
|
addTo: function (map) {
|
||||||
|
this._layers.addTo(map);
|
||||||
|
this._markers.addTo(map);
|
||||||
|
this._hotline.addTo(map);
|
||||||
|
|
||||||
|
this._map = map;
|
||||||
|
|
||||||
|
this.on('selection_changed', this._onSelectionChanged, this);
|
||||||
|
this.addTracks();
|
||||||
|
},
|
||||||
|
|
||||||
|
addTracks() {
|
||||||
|
this._tracks.forEach(this._addTrack, this);
|
||||||
|
|
||||||
|
},
|
||||||
|
_addTrack: function (track) {
|
||||||
|
if (track instanceof Object) {
|
||||||
|
this._loadGeoJSON(track);
|
||||||
|
} else {
|
||||||
|
this._elevation._parseFromString(track)
|
||||||
|
.then(geojson => this._loadGeoJSON(geojson, track.split('/').pop().split('#')[0].split('?')[0]))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function () {
|
||||||
|
this._elevation.clear()
|
||||||
|
this._clearLayers();
|
||||||
|
this._clearLayers(this._markers);
|
||||||
|
this._clearLayers(this._hotline)
|
||||||
|
this._count = 0;
|
||||||
|
this._loadedCount = 0;
|
||||||
|
this._tracks = []
|
||||||
|
},
|
||||||
|
|
||||||
|
_clearLayers(l) {
|
||||||
|
l = l || this._layers;
|
||||||
|
if (l && l.eachLayer) {
|
||||||
|
l.eachLayer(f => f.remove())
|
||||||
|
l.clearLayers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_loadGeoJSON: function (geojson, fallbackName) {
|
||||||
|
if (geojson) {
|
||||||
|
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
|
||||||
|
this._loadRoute(geojson);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_loadRoute: function (data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
var line_style = {
|
||||||
|
color: this._uniqueColors(this._tracks.length)[this._count++],
|
||||||
|
opacity: 0.75,
|
||||||
|
weight: 5,
|
||||||
|
distanceMarkers: this.options.distanceMarkers_options,
|
||||||
|
};
|
||||||
|
|
||||||
|
var route = L.geoJson(data, {
|
||||||
|
name: data.name || '',
|
||||||
|
style: (feature) => line_style,
|
||||||
|
distanceMarkers: line_style.distanceMarkers,
|
||||||
|
originalStyle: line_style,
|
||||||
|
isGroupLayer: true,
|
||||||
|
index: this._count - 1,
|
||||||
|
filter: feature => feature.geometry.type != "Point",
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this._elevation.import([this._elevation.__LGEOMUTIL, this._elevation.__LDISTANCEM]).then(() => {
|
||||||
|
route.addTo(this._layers);
|
||||||
|
|
||||||
|
route.eachLayer((layer) => this._onEachRouteLayer(route, layer));
|
||||||
|
this._onEachRouteLoaded(route);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEachRouteLayer: function (route, layer) {
|
||||||
|
var polyline = layer;
|
||||||
|
|
||||||
|
route.on('selected', L.bind(this._onRouteSelected, this, route, polyline));
|
||||||
|
|
||||||
|
polyline.on('mouseover', L.bind(this._onRouteMouseOver, this, route, polyline));
|
||||||
|
polyline.on('mouseout', L.bind(this._onRouteMouseOut, this, route, polyline));
|
||||||
|
polyline.on('click', L.bind(this._onRouteClick, this, route, polyline));
|
||||||
|
|
||||||
|
const startIcon = L.divIcon({
|
||||||
|
html: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-bullseye"></i>',
|
||||||
|
className: 'start-icon'
|
||||||
|
});
|
||||||
|
const endIcon = L.divIcon({
|
||||||
|
html: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-flag-checkered"></i>',
|
||||||
|
className: 'end-icon'
|
||||||
|
});
|
||||||
|
const latlngs = polyline.getLatLngs();
|
||||||
|
|
||||||
|
|
||||||
|
if (this._loadedCount == 0) {
|
||||||
|
L.marker(latlngs[0], { icon: startIcon }).addTo(this._markers)
|
||||||
|
}
|
||||||
|
|
||||||
|
L.marker(latlngs[latlngs.length - 1], { icon: endIcon }).addTo(this._markers)
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEachRouteLoaded: function (route) {
|
||||||
|
this.fire('route_loaded', { route: route });
|
||||||
|
|
||||||
|
if (++this._loadedCount === this._tracks.length) {
|
||||||
|
this.fire('loaded');
|
||||||
|
if (this.options.flyToBounds) {
|
||||||
|
this._map.flyToBounds(this.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
highlight: function (route, polyline) {
|
||||||
|
polyline.setStyle(this.options.highlight);
|
||||||
|
polyline.options.highlighted = true
|
||||||
|
if (this.options.distanceMarkers) {
|
||||||
|
polyline.addDistanceMarkers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unhighlight: function (route, polyline) {
|
||||||
|
polyline.setStyle(route.options.originalStyle);
|
||||||
|
polyline.options.highlighted = false
|
||||||
|
if (this.options.distanceMarkers) {
|
||||||
|
polyline.removeDistanceMarkers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteMouseOver: function (route, polyline) {
|
||||||
|
if (!route.isSelected()) {
|
||||||
|
this.highlight(route, polyline);
|
||||||
|
}
|
||||||
|
this.fire('route_mouseover', { route: route, polyline: polyline });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteMouseOut: function (route, polyline) {
|
||||||
|
if (!route.isSelected()) {
|
||||||
|
this.unhighlight(route, polyline);
|
||||||
|
}
|
||||||
|
this.fire('route_mouseout', { route: route, polyline: polyline });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteClick: function (route, polyline) {
|
||||||
|
this.highlight(route, polyline)
|
||||||
|
this.setSelection(route);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteSelected: function (route, polyline) {
|
||||||
|
if (!route.isSelected()) {
|
||||||
|
this.unhighlight(route, polyline);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSelectionChanged: function (e) {
|
||||||
|
var elevation = this._elevation;
|
||||||
|
var eleDiv = elevation.getContainer();
|
||||||
|
var route = this.getSelection();
|
||||||
|
var hotline = this._hotline;
|
||||||
|
|
||||||
|
elevation.clear();
|
||||||
|
|
||||||
|
if (route && route.isSelected()) {
|
||||||
|
if (!eleDiv) {
|
||||||
|
elevation.addTo(this._map);
|
||||||
|
}
|
||||||
|
route.getLayers().forEach(function (layer) {
|
||||||
|
if (layer instanceof L.Polyline) {
|
||||||
|
elevation.addData(layer, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (eleDiv) {
|
||||||
|
elevation.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_uniqueColors: function (count) {
|
||||||
|
return count === 1 ? ['#0058ca'] : new Array(count).fill(null).map((_, i) => this._hsvToHex(i * (1 / count), 1, 0.7));
|
||||||
|
},
|
||||||
|
|
||||||
|
_hsvToHex: function (h, s, v) {
|
||||||
|
var i = Math.floor(h * 6);
|
||||||
|
var f = h * 6 - i;
|
||||||
|
var p = v * (1 - s);
|
||||||
|
var q = v * (1 - f * s);
|
||||||
|
var t = v * (1 - (1 - f) * s);
|
||||||
|
var rgb = { 0: [v, t, p], 1: [q, v, p], 2: [p, v, t], 3: [p, q, v], 4: [t, p, v], 5: [v, p, q] }[i % 6];
|
||||||
|
return rgb.map(d => d * 255).reduce((hex, byte) => hex + ((byte >> 4) & 0x0F).toString(16) + (byte & 0x0F).toString(16), "#");
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFrom: function (map) {
|
||||||
|
this._layers.removeFrom(map);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
L.GpxGroup.include(L.Mixin.Events);
|
||||||
|
L.GpxGroup.include(L.Mixin.Selection);
|
||||||
|
|
||||||
|
L.gpxGroup = (tracks, options) => new L.GpxGroup(tracks, options);
|
||||||
@@ -446,7 +446,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
|||||||
}) : Promise.resolve();
|
}) : Promise.resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
_initHotLine(layer) {
|
_initHotLine(layer, target = this._hotline) {
|
||||||
let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
|
let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
|
||||||
return this.options.hotline ? this.import(/* @vite-ignore */this.__LHOTLINE)
|
return this.options.hotline ? this.import(/* @vite-ignore */this.__LHOTLINE)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -464,7 +464,9 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
|||||||
weight: 5,
|
weight: 5,
|
||||||
outlineColor: '#000000',
|
outlineColor: '#000000',
|
||||||
outlineWidth: 1
|
outlineWidth: 1
|
||||||
}).addTo(this._hotline);
|
}).addTo(target);
|
||||||
|
console.log(this._data);
|
||||||
|
|
||||||
let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
|
let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
|
||||||
trkseg.on('add remove', ({ type }) => {
|
trkseg.on('add remove', ({ type }) => {
|
||||||
trkseg.setStyle({ opacity: (type == 'add' ? 0 : alpha) });
|
trkseg.setStyle({ opacity: (type == 'add' ? 0 : alpha) });
|
||||||
@@ -536,16 +538,20 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
|||||||
|
|
||||||
L.Canvas.include({
|
L.Canvas.include({
|
||||||
_fillStroke(ctx, layer) {
|
_fillStroke(ctx, layer) {
|
||||||
if (control._layers.hasLayer(layer)) {
|
let options = layer.options;
|
||||||
|
|
||||||
let options = layer.options;
|
|
||||||
|
|
||||||
options.color = color.line || color.area || theme;
|
if (control._layers.hasLayer(layer) || options.isGroupLayer) {
|
||||||
|
|
||||||
|
|
||||||
|
if (!options.isGroupLayer) {
|
||||||
|
options.color = color.line || color.area || theme;
|
||||||
|
}
|
||||||
options.stroke = !!options.color;
|
options.stroke = !!options.color;
|
||||||
|
|
||||||
oldProto.call(this, ctx, layer);
|
oldProto.call(this, ctx, layer);
|
||||||
|
|
||||||
if (options.stroke && options.weight !== 0) {
|
if (!options.highlighted && options.stroke && options.weight !== 0) {
|
||||||
let oldVal = ctx.globalCompositeOperation || 'source-over';
|
let oldVal = ctx.globalCompositeOperation || 'source-over';
|
||||||
ctx.globalCompositeOperation = 'destination-over'
|
ctx.globalCompositeOperation = 'destination-over'
|
||||||
ctx.strokeStyle = color.outline || '#FFF';
|
ctx.strokeStyle = color.outline || '#FFF';
|
||||||
|
|||||||
101
web/src/lib/vendor/leaflet-elevation/src/utils.js
vendored
101
web/src/lib/vendor/leaflet-elevation/src/utils.js
vendored
@@ -3,19 +3,28 @@
|
|||||||
**/
|
**/
|
||||||
export const Colors = {
|
export const Colors = {
|
||||||
'lightblue': { area: '#3366CC', alpha: 0.45, stroke: '#3366CC' },
|
'lightblue': { area: '#3366CC', alpha: 0.45, stroke: '#3366CC' },
|
||||||
'magenta' : { area: '#FF005E' },
|
'magenta': { area: '#FF005E' },
|
||||||
'yellow' : { area: '#FF0' },
|
'yellow': { area: '#FF0' },
|
||||||
'purple' : { area: '#732C7B' },
|
'purple': { area: '#732C7B' },
|
||||||
'steelblue': { area: '#4682B4' },
|
'steelblue': { area: '#4682B4' },
|
||||||
'red' : { area: '#F00' },
|
'red': { area: '#F00' },
|
||||||
'lime' : { area: '#9CC222', line: '#566B13' },
|
'lime': { area: '#9CC222', line: '#566B13' },
|
||||||
'gray': { area: '#000000', line: '#3366CC', alpha: 0.00001, stroke: '#000000' }
|
'gray': { area: '#000000', line: '#3366CC', alpha: 0.00001, stroke: '#000000' }
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEC = 1000;
|
export const LineColors = [
|
||||||
const MIN = SEC * 60;
|
'#0058ca',
|
||||||
|
'#E36414',
|
||||||
|
'5f0f40' ,
|
||||||
|
'#9A031E',
|
||||||
|
'#fb8b24',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const SEC = 1000;
|
||||||
|
const MIN = SEC * 60;
|
||||||
const HOUR = MIN * 60;
|
const HOUR = MIN * 60;
|
||||||
const DAY = HOUR * 24;
|
const DAY = HOUR * 24;
|
||||||
|
|
||||||
export function resolveURL(src, baseUrl) {
|
export function resolveURL(src, baseUrl) {
|
||||||
console.log(baseUrl, src);
|
console.log(baseUrl, src);
|
||||||
@@ -27,19 +36,19 @@ export function resolveURL(src, baseUrl) {
|
|||||||
*/
|
*/
|
||||||
export function formatTime(t) {
|
export function formatTime(t) {
|
||||||
let d = Math.floor(t / DAY);
|
let d = Math.floor(t / DAY);
|
||||||
let h = Math.floor( (t - d * DAY) / HOUR);
|
let h = Math.floor((t - d * DAY) / HOUR);
|
||||||
let m = Math.floor( (t - d * DAY - h * HOUR) / MIN);
|
let m = Math.floor((t - d * DAY - h * HOUR) / MIN);
|
||||||
let s = Math.round( (t - d * DAY - h * HOUR - m * MIN) / SEC);
|
let s = Math.round((t - d * DAY - h * HOUR - m * MIN) / SEC);
|
||||||
if ( s === 60 ) { m++; s = 0; }
|
if (s === 60) { m++; s = 0; }
|
||||||
if ( m === 60 ) { h++; m = 0; }
|
if (m === 60) { h++; m = 0; }
|
||||||
if ( h === 24 ) { d++; h = 0; }
|
if (h === 24) { d++; h = 0; }
|
||||||
return (d ? d + "d " : '') + h.toString().padStart(2, 0) + ':' + m.toString().padStart(2, 0) + "'" + s.toString().padStart(2, 0) + '"';
|
return (d ? d + "d " : '') + h.toString().padStart(2, 0) + ':' + m.toString().padStart(2, 0) + "'" + s.toString().padStart(2, 0) + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a time (millis) to human readable date string (dd-mm-yyyy hh:mm:ss)
|
* Convert a time (millis) to human readable date string (dd-mm-yyyy hh:mm:ss)
|
||||||
*/
|
*/
|
||||||
export function formatDate(format) {
|
export function formatDate(format) {
|
||||||
if (!format) {
|
if (!format) {
|
||||||
return (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
|
return (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
|
||||||
} else if (format == 'time') {
|
} else if (format == 'time') {
|
||||||
@@ -53,7 +62,7 @@ export function formatTime(t) {
|
|||||||
/**
|
/**
|
||||||
* Generate download data event.
|
* Generate download data event.
|
||||||
*/
|
*/
|
||||||
export function saveFile(dataURI, fileName) {
|
export function saveFile(dataURI, fileName) {
|
||||||
let a = create('a', '', { href: dataURI, target: '_new', download: fileName || "", style: "display:none;" });
|
let a = create('a', '', { href: dataURI, target: '_new', download: fileName || "", style: "display:none;" });
|
||||||
let b = document.body;
|
let b = document.body;
|
||||||
b.appendChild(a);
|
b.appendChild(a);
|
||||||
@@ -65,7 +74,7 @@ export function formatTime(t) {
|
|||||||
/**
|
/**
|
||||||
* Convert SVG Path into Path2D and then update canvas
|
* Convert SVG Path into Path2D and then update canvas
|
||||||
*/
|
*/
|
||||||
export function drawCanvas(ctx, path) {
|
export function drawCanvas(ctx, path) {
|
||||||
path.classed('canvas-path', true);
|
path.classed('canvas-path', true);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -73,8 +82,8 @@ export function formatTime(t) {
|
|||||||
let p = new Path2D(path.attr('d'));
|
let p = new Path2D(path.attr('d'));
|
||||||
|
|
||||||
ctx.strokeStyle = path.__strokeStyle || path.attr('stroke');
|
ctx.strokeStyle = path.__strokeStyle || path.attr('stroke');
|
||||||
ctx.fillStyle = path.__fillStyle || path.attr('fill');
|
ctx.fillStyle = path.__fillStyle || path.attr('fill');
|
||||||
ctx.lineWidth = 1.25;
|
ctx.lineWidth = 1.25;
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
|
|
||||||
// stroke opacity
|
// stroke opacity
|
||||||
@@ -82,7 +91,7 @@ export function formatTime(t) {
|
|||||||
ctx.stroke(p);
|
ctx.stroke(p);
|
||||||
|
|
||||||
// fill opacity
|
// fill opacity
|
||||||
ctx.globalAlpha = path.attr('fill-opacity') || 0.45;
|
ctx.globalAlpha = path.attr('fill-opacity') || 0.45;
|
||||||
ctx.fill(p);
|
ctx.fill(p);
|
||||||
|
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
@@ -94,7 +103,7 @@ export function formatTime(t) {
|
|||||||
* Loop and extract GPX Extensions handled by "@tmcw/toGeoJSON" (eg. "coordinateProperties" > "times")
|
* Loop and extract GPX Extensions handled by "@tmcw/toGeoJSON" (eg. "coordinateProperties" > "times")
|
||||||
*/
|
*/
|
||||||
export function coordPropsToMeta(coordProps, name, parser) {
|
export function coordPropsToMeta(coordProps, name, parser) {
|
||||||
return coordProps && (({props, point, id, isMulti }) => {
|
return coordProps && (({ props, point, id, isMulti }) => {
|
||||||
if (props) {
|
if (props) {
|
||||||
for (const key of coordProps) {
|
for (const key of coordProps) {
|
||||||
if (key in props) {
|
if (key in props) {
|
||||||
@@ -109,7 +118,7 @@ export function coordPropsToMeta(coordProps, name, parser) {
|
|||||||
/**
|
/**
|
||||||
* Extract numeric property (id) from GeoJSON object
|
* Extract numeric property (id) from GeoJSON object
|
||||||
*/
|
*/
|
||||||
export const parseNumeric = (property, id) => parseInt((typeof property === 'object' ? property[id] : property));
|
export const parseNumeric = (property, id) => parseInt((typeof property === 'object' ? property[id] : property));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract datetime property (id) from GeoJSON object
|
* Extract datetime property (id) from GeoJSON object
|
||||||
@@ -119,25 +128,25 @@ export const parseDate = (property, id) => new Date(Date.parse((typeof property
|
|||||||
/**
|
/**
|
||||||
* A little bit shorter than L.DomUtil
|
* A little bit shorter than L.DomUtil
|
||||||
*/
|
*/
|
||||||
export const addClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.addClass(n, s));
|
export const addClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.addClass(n, s));
|
||||||
export const removeClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.removeClass(n, s));
|
export const removeClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.removeClass(n, s));
|
||||||
export const toggleClass = (n, str, cond) => (cond ? addClass : removeClass)(n, str);
|
export const toggleClass = (n, str, cond) => (cond ? addClass : removeClass)(n, str);
|
||||||
export const replaceClass = (n, rem, add) => (rem && removeClass(n, rem)) || (add && addClass(n, add));
|
export const replaceClass = (n, rem, add) => (rem && removeClass(n, rem)) || (add && addClass(n, add));
|
||||||
export const style = (n, k, v) => (typeof v === "undefined" && L.DomUtil.getStyle(n, k)) || n.style.setProperty(k, v);
|
export const style = (n, k, v) => (typeof v === "undefined" && L.DomUtil.getStyle(n, k)) || n.style.setProperty(k, v);
|
||||||
export const toggleStyle = (n, k, v, cond) => style(n, k, cond ? v : '');
|
export const toggleStyle = (n, k, v, cond) => style(n, k, cond ? v : '');
|
||||||
export const setAttributes = (n, attrs) => { for (let k in attrs) { n.setAttribute(k, attrs[k]); } };
|
export const setAttributes = (n, attrs) => { for (let k in attrs) { n.setAttribute(k, attrs[k]); } };
|
||||||
export const toggleEvent = (el, e, fn, cond) => el[cond ? 'on' : 'off'](e, fn);
|
export const toggleEvent = (el, e, fn, cond) => el[cond ? 'on' : 'off'](e, fn);
|
||||||
export const create = (tag, str, attrs, n) => { let elem = L.DomUtil.create(tag, str || ""); if (attrs) setAttributes(elem, attrs); if (n) append(n, elem); return elem; };
|
export const create = (tag, str, attrs, n) => { let elem = L.DomUtil.create(tag, str || ""); if (attrs) setAttributes(elem, attrs); if (n) append(n, elem); return elem; };
|
||||||
export const append = (n, c) => n.appendChild(c);
|
export const append = (n, c) => n.appendChild(c);
|
||||||
export const insert = (n, c, pos) => n.insertAdjacentElement(pos, c);
|
export const insert = (n, c, pos) => n.insertAdjacentElement(pos, c);
|
||||||
export const select = (str, n) => (n || document).querySelector(str);
|
export const select = (str, n) => (n || document).querySelector(str);
|
||||||
export const each = (obj, fn) => { for (let i in obj) fn(obj[i], i); };
|
export const each = (obj, fn) => { for (let i in obj) fn(obj[i], i); };
|
||||||
export const randomId = () => Math.random().toString(36).substr(2, 9);
|
export const randomId = () => Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: use generators instead? (ie. "yield")
|
* TODO: use generators instead? (ie. "yield")
|
||||||
*/
|
*/
|
||||||
export const iMax = (iVal, max = -Infinity) => (iVal > max ? iVal : max);
|
export const iMax = (iVal, max = -Infinity) => (iVal > max ? iVal : max);
|
||||||
export const iMin = (iVal, min = +Infinity) => (iVal < min ? iVal : min);
|
export const iMin = (iVal, min = +Infinity) => (iVal < min ? iVal : min);
|
||||||
export const iAvg = (iVal, avg = 0, idx = 1) => (iVal + avg * (idx - 1)) / idx;
|
export const iAvg = (iVal, avg = 0, idx = 1) => (iVal + avg * (idx - 1)) / idx;
|
||||||
export const iSum = (iVal, sum = 0) => iVal + sum;
|
export const iSum = (iVal, sum = 0) => iVal + sum;
|
||||||
@@ -145,19 +154,19 @@ export const iSum = (iVal, sum = 0) => iVal + sum;
|
|||||||
/**
|
/**
|
||||||
* Alias for some leaflet core functions
|
* Alias for some leaflet core functions
|
||||||
*/
|
*/
|
||||||
export const { on, off } = L.DomEvent;
|
export const { on, off } = L.DomEvent;
|
||||||
export const { throttle, wrapNum } = L.Util;
|
export const { throttle, wrapNum } = L.Util;
|
||||||
export const { hasClass } = L.DomUtil;
|
export const { hasClass } = L.DomUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limit floating point precision
|
* Limit floating point precision
|
||||||
*/
|
*/
|
||||||
export const round = L.Util.formatNum;
|
export const round = L.Util.formatNum;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limit a number between min / max values
|
* Limit a number between min / max values
|
||||||
*/
|
*/
|
||||||
export const clamp = (val, range) => range ? (val < range[0] ? range[0] : val > range[1] ? range[1] : val) : val;
|
export const clamp = (val, range) => range ? (val < range[0] ? range[0] : val > range[1] ? range[1] : val) : val;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limit a delta difference between two values
|
* Limit a delta difference between two values
|
||||||
@@ -170,7 +179,7 @@ export const wrapDelta = (curr, prev, deltaMax) => Math.abs(curr - prev) > delta
|
|||||||
* @see https://web.dev/structured-clone/#features-and-limitations
|
* @see https://web.dev/structured-clone/#features-and-limitations
|
||||||
*/
|
*/
|
||||||
export function cloneDeep(o, skipProps = [], cache = []) {
|
export function cloneDeep(o, skipProps = [], cache = []) {
|
||||||
switch(!o || typeof o) {
|
switch (!o || typeof o) {
|
||||||
case 'object':
|
case 'object':
|
||||||
const hit = cache.filter(c => o === c.original)[0];
|
const hit = cache.filter(c => o === c.original)[0];
|
||||||
if (hit) return hit.copy; // handle circular structures
|
if (hit) return hit.copy; // handle circular structures
|
||||||
@@ -186,10 +195,10 @@ export function cloneDeep(o, skipProps = [], cache = []) {
|
|||||||
propdesc.get || propdesc.set
|
propdesc.get || propdesc.set
|
||||||
? propdesc // just copy accessor properties
|
? propdesc // just copy accessor properties
|
||||||
: { // deep copy data properties
|
: { // deep copy data properties
|
||||||
writable: propdesc.writable,
|
writable: propdesc.writable,
|
||||||
configurable: propdesc.configurable,
|
configurable: propdesc.configurable,
|
||||||
enumerable: propdesc.enumerable,
|
enumerable: propdesc.enumerable,
|
||||||
value: skipProps.includes(prop) ? propdesc.value : cloneDeep(propdesc.value, skipProps, cache),
|
value: skipProps.includes(prop) ? propdesc.value : cloneDeep(propdesc.value, skipProps, cache),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -198,7 +207,7 @@ export function cloneDeep(o, skipProps = [], cache = []) {
|
|||||||
case 'symbol':
|
case 'symbol':
|
||||||
console.warn('cloneDeep: ' + typeof o + 's not fully supported:', o);
|
console.warn('cloneDeep: ' + typeof o + 's not fully supported:', o);
|
||||||
case true:
|
case true:
|
||||||
// null, undefined or falsy primitive
|
// null, undefined or falsy primitive
|
||||||
default:
|
default:
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|||||||
221
web/src/routes/lists/edit/[id]/+page.svelte
Normal file
221
web/src/routes/lists/edit/[id]/+page.svelte
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { listSchema, type List } from "$lib/models/list";
|
||||||
|
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import Button from "$lib/components/base/button.svelte";
|
||||||
|
import Search, {
|
||||||
|
type SearchItem,
|
||||||
|
} from "$lib/components/base/search.svelte";
|
||||||
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
|
import Textarea from "$lib/components/base/textarea.svelte";
|
||||||
|
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
|
||||||
|
import TrailListItem from "$lib/components/trail/trail_list_item.svelte";
|
||||||
|
import { trails_show } from "$lib/stores/trail_store";
|
||||||
|
import { getFileURL } from "$lib/util/file_util.js";
|
||||||
|
import {
|
||||||
|
formatDistance,
|
||||||
|
formatElevation,
|
||||||
|
formatTimeHHMM,
|
||||||
|
} from "$lib/util/format_util";
|
||||||
|
import type { Trail } from "$lib/models/trail.js";
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
let previewURL = "";
|
||||||
|
let searchDropdownItems: SearchItem[] = [];
|
||||||
|
|
||||||
|
const { form, errors, handleChange, handleSubmit } = createForm<List>({
|
||||||
|
initialValues: data.list!,
|
||||||
|
validationSchema: listSchema,
|
||||||
|
onSubmit: async (submittedList) => {
|
||||||
|
(document.getElementById("avatar") as HTMLInputElement).value = "";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAvatarBrowser() {
|
||||||
|
document.getElementById("avatar")!.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAvatarSelection() {
|
||||||
|
const files = (document.getElementById("avatar") as HTMLInputElement)
|
||||||
|
.files;
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previewURL = URL.createObjectURL(files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(q: string) {
|
||||||
|
const r = await fetch("/api/v1/search/multi", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
indexUid: "trails",
|
||||||
|
q: q,
|
||||||
|
limit: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await r.json();
|
||||||
|
|
||||||
|
searchDropdownItems = response.results[0].hits.map(
|
||||||
|
(t: Record<string, any>) => ({
|
||||||
|
text: t.name,
|
||||||
|
description: `${t.location ?? "-"}`,
|
||||||
|
value: t.id,
|
||||||
|
icon: "route",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearchClick(item: SearchItem) {
|
||||||
|
const trail = await trails_show(item.value, true);
|
||||||
|
$form.trails?.push(trail);
|
||||||
|
$form.expand!.trails = [...$form.expand!.trails, trail];
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTrail(trail: Trail) {
|
||||||
|
$form.trails?.filter((id) => id !== trail.id);
|
||||||
|
$form.expand!.trails = $form.expand!.trails.filter(
|
||||||
|
(t) => t.id !== trail.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="grid grid-cols-1 md:grid-cols-[440px_1fr]">
|
||||||
|
<form
|
||||||
|
id="list-form"
|
||||||
|
class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0"
|
||||||
|
on:submit={handleSubmit}
|
||||||
|
>
|
||||||
|
<h2 class="text-2xl font-semibold">
|
||||||
|
{$page.params.id === "new" ? $_("new-list") : $_("edit-list")}
|
||||||
|
</h2>
|
||||||
|
<label for="avatar" class="text-sm font-medium block">
|
||||||
|
{$_("avatar")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="avatar"
|
||||||
|
type="file"
|
||||||
|
id="avatar"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none;"
|
||||||
|
on:change={handleAvatarSelection}
|
||||||
|
/>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
{#if previewURL.length > 0}
|
||||||
|
<img
|
||||||
|
class="w-32 aspect-square rounded-full object-cover border border-gray-100"
|
||||||
|
alt="avatar"
|
||||||
|
src={previewURL}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-32 aspect-square rounded-full object-cover border border-gray-200"
|
||||||
|
>
|
||||||
|
<i class="fa fa-table-list text-5xl"></i>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
type="button"
|
||||||
|
on:click={openAvatarBrowser}>{$_("change")}...</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name="name"
|
||||||
|
label={$_("name")}
|
||||||
|
bind:value={$form.name}
|
||||||
|
error={$errors.name}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
name="description"
|
||||||
|
label={$_("description")}
|
||||||
|
bind:value={$form.description}
|
||||||
|
error={$errors.description}
|
||||||
|
on:change={handleChange}
|
||||||
|
></Textarea>
|
||||||
|
<h3 class="text-xl font-semibold">
|
||||||
|
{$_("trail", { values: { n: 2 } })}
|
||||||
|
</h3>
|
||||||
|
<Search
|
||||||
|
on:update={(e) => search(e.detail)}
|
||||||
|
on:click={(e) => handleSearchClick(e.detail)}
|
||||||
|
placeholder="{$_('search-trails')}..."
|
||||||
|
items={searchDropdownItems}
|
||||||
|
></Search>
|
||||||
|
{#if $form.expand?.trails.length}
|
||||||
|
{#each $form.expand?.trails ?? [] as trail}
|
||||||
|
<div
|
||||||
|
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
|
||||||
|
>
|
||||||
|
<div class="shrink-0">
|
||||||
|
<img
|
||||||
|
class="h-12 w-12 object-cover rounded-xl"
|
||||||
|
src={trail.photos.length
|
||||||
|
? getFileURL(
|
||||||
|
trail,
|
||||||
|
trail.photos[trail.thumbnail],
|
||||||
|
)
|
||||||
|
: "/imgs/default_thumbnail.webp"}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="basis-full">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h4 class="font-semibold text-lg">
|
||||||
|
{trail.name}
|
||||||
|
</h4>
|
||||||
|
<span class="text-sm"
|
||||||
|
><i class="fa fa-gauge mr-2"></i>{$_(
|
||||||
|
trail.difficulty ?? "?",
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-1 gap-4 text-sm text-gray-500">
|
||||||
|
<span
|
||||||
|
><i class="fa fa-left-right mr-2"
|
||||||
|
></i>{formatDistance(trail.distance)}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-up-down mr-2"
|
||||||
|
></i>{formatElevation(
|
||||||
|
trail.elevation_gain,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-clock mr-2"
|
||||||
|
></i>{formatTimeHHMM(trail.duration)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-icon text-red-500"
|
||||||
|
on:click={() => deleteTrail(trail)}
|
||||||
|
><i class="fa fa-trash"></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<span class="text-center text-sm text-gray-500 my-8"
|
||||||
|
>No routes added</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<Button primary={true} large={true} type="submit" extraClasses="mb-2"
|
||||||
|
>{$_("save-list")}</Button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
<MapWithElevation trails={$form.expand?.trails ?? []}></MapWithElevation>
|
||||||
|
</main>
|
||||||
27
web/src/routes/lists/edit/[id]/+page.ts
Normal file
27
web/src/routes/lists/edit/[id]/+page.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { List } from "$lib/models/list";
|
||||||
|
import { lists_show } from "$lib/stores/list_store";
|
||||||
|
import { error, type Load } from "@sveltejs/kit";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
|
export const load: Load = async ({ params, fetch, data }) => {
|
||||||
|
if (!params.id) {
|
||||||
|
return error(400, "Bad Request")
|
||||||
|
}
|
||||||
|
|
||||||
|
let list: List;
|
||||||
|
if (params.id === "new") {
|
||||||
|
list = new List("", []);
|
||||||
|
return { list: list }
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
list = await lists_show(params.id, fetch);
|
||||||
|
return { list: list }
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ClientResponseError) {
|
||||||
|
return error(e.status as any, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
||||||
<div id="trail-details" class=" sticky top-[62px]">
|
<div id="trail-details" class=" sticky top-[62px]">
|
||||||
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
<MapWithElevation trails={[$trail]} bind:markers></MapWithElevation>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -509,7 +509,7 @@
|
|||||||
>
|
>
|
||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<MapWithElevation
|
<MapWithElevation
|
||||||
trail={$trail}
|
trails={[$trail]}
|
||||||
options={{
|
options={{
|
||||||
theme: "gray-theme",
|
theme: "gray-theme",
|
||||||
slope: false,
|
slope: false,
|
||||||
|
|||||||
@@ -889,7 +889,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<MapWithElevation
|
<MapWithElevation
|
||||||
trail={$form}
|
trails={[$form]}
|
||||||
crosshair={drawingActive}
|
crosshair={drawingActive}
|
||||||
options={{
|
options={{
|
||||||
autofitBounds: !drawingActive,
|
autofitBounds: !drawingActive,
|
||||||
|
|||||||
Reference in New Issue
Block a user