diff --git a/web/src/lib/components/list/list_card.svelte b/web/src/lib/components/list/list_card.svelte index 435b77b4..90016736 100644 --- a/web/src/lib/components/list/list_card.svelte +++ b/web/src/lib/components/list/list_card.svelte @@ -1,6 +1,11 @@
{#if list.avatar} avatar {:else}
@@ -36,13 +56,36 @@
+
+ {formatDistance( + cumulativeDistance, + )} + {formatElevation( + cumulativeElevationGain, + )} + {formatTimeHHMM( + cumulativeDuration, + )} +
+

{list.expand?.trails.length ?? 0} + {$_("trail", { + values: { n: list.expand?.trails.length ?? 0 }, + })}

{!active ? list.description?.substring(0, 100) : list.description} - {#if ((list.description?.length ?? 0) > 100) && !active} + {#if (list.description?.length ?? 0) > 100 && !active} ... {/if}

diff --git a/web/src/lib/components/trail/map_with_elevation.svelte b/web/src/lib/components/trail/map_with_elevation.svelte index b51b7414..d5295445 100644 --- a/web/src/lib/components/trail/map_with_elevation.svelte +++ b/web/src/lib/components/trail/map_with_elevation.svelte @@ -2,43 +2,41 @@ import { page } from "$app/stores"; import { Settings } from "$lib/models/settings"; import type { Trail } from "$lib/models/trail"; - import { createMarkerFromWaypoint } from "$lib/util/leaflet_util"; + import { createMarkerFromWaypoint, endIcon, startIcon } from "$lib/util/leaflet_util"; import "$lib/vendor/leaflet-elevation/src/index.css"; import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule"; - import type { Layer, Map, Marker, Polyline } from "leaflet"; + import type { Map, Marker } from "leaflet"; import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css"; import "leaflet/dist/leaflet.css"; import { createEventDispatcher, onMount } from "svelte"; import { _ } from "svelte-i18n"; import Dropdown from "../base/dropdown.svelte"; - export let trails: Trail[]; + export let trail: Trail | null; export let markers: Marker[] = []; export let map: Map | null = null; export let options: any = {}; export let graticule: AutoGraticule | null = null; export let crosshair: boolean = false; - export let activeTrailIndex: number = 0; const dispatch = createEventDispatcher(); let L: any; - let gpxGroup: any; + let controlElevation: any; let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude"; - $: gpxData = trails.map((t) => t.expand.gpx_data); - $: if (gpxData && gpxGroup) { - gpxGroup._elevation.updateOptions({ + $: gpxData = trail?.expand.gpx_data; + $: if (gpxData && controlElevation) { + controlElevation.updateOptions({ autofitBounds: options.autofitBounds ?? true, }); - gpxGroup.clear(); - gpxGroup._tracks = gpxData; - gpxGroup.addTracks(); + controlElevation.clear(); + controlElevation.load(gpxData); } - $: if (options && gpxGroup) { - gpxGroup._elevation.updateOptions(options); + $: if (options) { + controlElevation?.updateOptions(options); } $: hotlineSwitcherItems = [ @@ -68,21 +66,14 @@ L = (await import("leaflet")).default; await import("leaflet-gpx"); await import("leaflet.awesome-markers"); + //@ts-ignore await import("$lib/vendor/leaflet-elevation/src/index.js"); - await import("$lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup"); - const AutoGraticule = ( await import("$lib/vendor/leaflet-graticule/leaflet-auto-graticule") ).default; - map = L.map("map", { - preferCanvas: true, - plugins: ["/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js"], - }).setView( - [ - trails.at(activeTrailIndex)?.lat ?? 0, - trails.at(activeTrailIndex)?.lon ?? 0, - ], + map = L.map("map", { preferCanvas: true }).setView( + [trail?.lat ?? 0, trail?.lon ?? 0], 3, ); map!.attributionControl.setPrefix(false); @@ -112,9 +103,7 @@ const baseMaps: Record = { OpenStreetMaps: baseLayer, OpenTopoMaps: topoLayer, - ...($page.data.settings as Settings)?.tilesets?.reduce< - Record - >((t, current) => { + ...($page.data.settings as Settings)?.tilesets?.reduce< Record>((t, current) => { t[current.name] = L.tileLayer(current.url); return t; }, {}), @@ -170,7 +159,7 @@ lazy: false, distance: false, direction: true, - offset: 1000, + offset: 500, }, // Toggle "leaflet-edgescale" integration edgeScale: false, @@ -182,6 +171,14 @@ wptIcons: false, wptLabels: false, preferCanvas: true, + trkStart: { + interactive: false, + className: "hihi", + icon: startIcon() + }, + trkEnd: { + icon: endIcon(), + }, graticule: false, drawing: false, }; @@ -191,34 +188,13 @@ options, ); - gpxGroup = L.gpxGroup(gpxData, { - points: [], - // points_options: opts.points, - elevation: true, - elevation_options: elevation_options, - flyToBounds: true, - distanceMarkers: true, - }); + controlElevation = L.control.elevation(elevation_options).addTo(map); - const markerLayerGroup = L.layerGroup().addTo(map); - - gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => { - 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); + for (const waypoint of trail?.expand.waypoints ?? []) { + const marker = createMarkerFromWaypoint(L, waypoint); + marker.addTo(map!); + markers.push(marker); + } if (elevation_options.graticule) { graticule = new AutoGraticule(); @@ -227,15 +203,14 @@ }); function switchHotline(metric: "altitude" | "slope" | "speed" | false) { - gpxGroup._elevation.updateOptions({ + controlElevation.updateOptions({ hotline: metric, autofitBounds: false, }); selectedMetric = metric; localStorage.setItem("gradient", metric.toString()); - gpxGroup.clear(); - gpxGroup._tracks = gpxData; - gpxGroup.addTracks(); + controlElevation.clear(); + controlElevation.load(gpxData); } @@ -266,4 +241,4 @@
- + \ No newline at end of file diff --git a/web/src/lib/components/trail/map_with_elevation_multiple.svelte b/web/src/lib/components/trail/map_with_elevation_multiple.svelte new file mode 100644 index 00000000..66a677cf --- /dev/null +++ b/web/src/lib/components/trail/map_with_elevation_multiple.svelte @@ -0,0 +1,321 @@ + + +
+
+
+ switchHotline(e.detail.value)} + let:toggleMenu={openDropdown} + > + + +
+
+
+ +
+
+
diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json index 814b85e3..2a547cf0 100644 --- a/web/src/lib/i18n/locales/de.json +++ b/web/src/lib/i18n/locales/de.json @@ -106,6 +106,7 @@ "license": "Lizenz", "link-copied": "Link kopiert", "list": "{n, plural, =1 {Liste} other {Listen}}", + "list-saved-successfully": "", "location": "Standort", "login": "Login", "login-details": "Login Details", diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index d81b7fc1..6cb7aa5c 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -106,6 +106,7 @@ "license": "License", "link-copied": "Link copied!", "list": "{n, plural, =1 {List} other {Lists}}", + "list-saved-successfully": "List saved successfully", "location": "Location", "login": "Login", "login-details": "Login details", diff --git a/web/src/lib/i18n/locales/fr.json b/web/src/lib/i18n/locales/fr.json index 41ca8d20..4cedcf5c 100644 --- a/web/src/lib/i18n/locales/fr.json +++ b/web/src/lib/i18n/locales/fr.json @@ -106,6 +106,7 @@ "license": "Licence", "link-copied": "", "list": "{n, plural, =1 {Liste} other {Listes}}", + "list-saved-successfully": "", "location": "Localisation", "login": "Connexion", "login-details": "", diff --git a/web/src/lib/i18n/locales/hu.json b/web/src/lib/i18n/locales/hu.json index 200271b0..45359d74 100644 --- a/web/src/lib/i18n/locales/hu.json +++ b/web/src/lib/i18n/locales/hu.json @@ -106,6 +106,7 @@ "license": "License", "link-copied": "", "list": "{n, plural, =1 {Lista} other {Listák}}", + "list-saved-successfully": "", "location": "Helyszín", "login": "Bejelentkezés", "login-details": "", diff --git a/web/src/lib/i18n/locales/it.json b/web/src/lib/i18n/locales/it.json index cfc45d05..3d41523a 100644 --- a/web/src/lib/i18n/locales/it.json +++ b/web/src/lib/i18n/locales/it.json @@ -106,6 +106,7 @@ "license": "Licenza", "link-copied": "Link copiato", "list": "{n, plural, =1 {Lista} other {Liste}}", + "list-saved-successfully": "", "location": "Posizione", "login": "Login", "login-details": "", diff --git a/web/src/lib/i18n/locales/nl.json b/web/src/lib/i18n/locales/nl.json index 80c64503..a261d707 100644 --- a/web/src/lib/i18n/locales/nl.json +++ b/web/src/lib/i18n/locales/nl.json @@ -106,6 +106,7 @@ "license": "Licentie", "link-copied": "", "list": "{n, plural, =1 {Lijst} other {Lijsten}}", + "list-saved-successfully": "", "location": "Locatie", "login": "Inloggen", "login-details": "", diff --git a/web/src/lib/i18n/locales/pl.json b/web/src/lib/i18n/locales/pl.json index 827ee802..ad9cd8d4 100644 --- a/web/src/lib/i18n/locales/pl.json +++ b/web/src/lib/i18n/locales/pl.json @@ -106,6 +106,7 @@ "license": "Licencja", "link-copied": "", "list": "{n, plural, =1 {Lista} other {Listy}}", + "list-saved-successfully": "", "location": "Lokalizacja", "login": "Zaloguj się", "login-details": "", diff --git a/web/src/lib/i18n/locales/pt.json b/web/src/lib/i18n/locales/pt.json index 172e74cd..c523fcca 100644 --- a/web/src/lib/i18n/locales/pt.json +++ b/web/src/lib/i18n/locales/pt.json @@ -106,6 +106,7 @@ "license": "Licença", "link-copied": "", "list": "{n, plural, =1 {Lista} other {Listas}}", + "list-saved-successfully": "", "location": "Localização", "login": "Login", "login-details": "", diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json index 10392478..aea0cfc8 100644 --- a/web/src/lib/i18n/locales/zh.json +++ b/web/src/lib/i18n/locales/zh.json @@ -106,6 +106,7 @@ "license": "开源协议", "link-copied": "", "list": "{n, plural, =1 {列表} other {列表}}", + "list-saved-successfully": "", "location": "地点", "login": "登录", "login-details": "", diff --git a/web/src/lib/stores/list_store.ts b/web/src/lib/stores/list_store.ts index 0c9434d4..efc2845c 100644 --- a/web/src/lib/stores/list_store.ts +++ b/web/src/lib/stores/list_store.ts @@ -4,6 +4,7 @@ import { pb } from "$lib/pocketbase"; import { getFileURL } from "$lib/util/file_util"; import { ClientResponseError } from "pocketbase"; import { writable, type Writable } from "svelte/store"; +import { fetchGPX } from "./trail_store"; export const lists: Writable = writable([]) export const list: Writable = writable(new List("", [])) @@ -38,6 +39,12 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config? }) const response = await r.json() + for (const trail of response.expand.trails) { + const gpxData: string = await fetchGPX(trail); + trail.expand.gpx_data = gpxData; + } + + if (!r.ok) { throw new ClientResponseError(response) } diff --git a/web/src/lib/stores/trail_store.ts b/web/src/lib/stores/trail_store.ts index 78946e5e..b3f1e387 100644 --- a/web/src/lib/stores/trail_store.ts +++ b/web/src/lib/stores/trail_store.ts @@ -157,7 +157,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques trail.set(response); - return response; + return response as Trail; } export async function trails_create(trail: Trail, photos: File[], gpx: File | Blob | null, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { diff --git a/web/src/lib/util/leaflet_util.ts b/web/src/lib/util/leaflet_util.ts index 74406547..5d61eb8f 100644 --- a/web/src/lib/util/leaflet_util.ts +++ b/web/src/lib/util/leaflet_util.ts @@ -1,17 +1,24 @@ import type { Waypoint } from "$lib/models/waypoint"; -import type { Icon, LatLng, LeafletEvent, Map, Marker } from "leaflet"; +import type { LeafletEvent, Map, Marker } from "leaflet"; + +export const startIcon = () => L.divIcon({ + html: '', + className: 'start-icon' +}); +export const endIcon = () => L.divIcon({ + html: '', + className: 'end-icon' +}); export function createMarkerFromWaypoint(L: any, waypoint: Waypoint, onDragEnd?: (event: LeafletEvent) => void): Marker { - const fontAwesomeIcon = L.AwesomeMarkers.icon({ - icon: waypoint.icon, - prefix: "fa", - markerColor: "cadetblue", - iconColor: "white", - }) as Icon; + const icon = L.divIcon({ + html: ``, + className: 'waypoint-icon' + }); const marker = L.marker([waypoint.lat, waypoint.lon], { title: waypoint.name, - icon: fontAwesomeIcon, + icon: icon, draggable: onDragEnd != null, meta: { waypointName: waypoint.name diff --git a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js index 979190c0..fc8743f8 100644 --- a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js +++ b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js @@ -110,6 +110,8 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ this._hotline = L.featureGroup(); this._elevation = L.control.elevation(this.options.elevation_options); + this._routes = []; + this.options.points.forEach((poi) => L .marker(poi.latlng, { icon: L.icon(this.options.points_options.icon) }) @@ -123,9 +125,9 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ }, addTo: function (map) { + this._hotline.addTo(map); this._layers.addTo(map); this._markers.addTo(map); - this._hotline.addTo(map); this._map = map; @@ -137,23 +139,37 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ this._tracks.forEach(this._addTrack, this); }, + + select(index) { + if (index > this._routes.length - 1) { + return + } + + this.setSelection(this._routes[index]) + }, _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])) + .then(geojson => this._loadGeoJSON(geojson, this._hashCode(track), track.split('/').pop().split('#')[0].split('?')[0])) } }, + _hashCode: (s) => s.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0), + clear: function () { this._elevation.clear() + this._elevation.remove(); this._clearLayers(); this._clearLayers(this._markers); this._clearLayers(this._hotline) this._count = 0; this._loadedCount = 0; this._tracks = [] + this._routes = [] + + this.fire('clear') }, _clearLayers(l) { @@ -164,8 +180,9 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ } }, - _loadGeoJSON: function (geojson, fallbackName) { - if (geojson) { + _loadGeoJSON: function (geojson, hash, fallbackName) { + if (geojson) { + geojson.hash = hash geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName; this._loadRoute(geojson); } @@ -180,14 +197,14 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ 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, + hash: data.hash, filter: feature => feature.geometry.type != "Point", }); @@ -196,6 +213,8 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ route.addTo(this._layers); route.eachLayer((layer) => this._onEachRouteLayer(route, layer)); + + this._onEachRouteLoaded(route); }); @@ -204,6 +223,8 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ _onEachRouteLayer: function (route, layer) { var polyline = layer; + this._routes.push(route) + route.on('selected', L.bind(this._onRouteSelected, this, route, polyline)); polyline.on('mouseover', L.bind(this._onRouteMouseOver, this, route, polyline)); @@ -211,15 +232,14 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ polyline.on('click', L.bind(this._onRouteClick, this, route, polyline)); const startIcon = L.divIcon({ - html: '', + html: '', className: 'start-icon' }); const endIcon = L.divIcon({ - html: '', + html: '', className: 'end-icon' }); - const latlngs = polyline.getLatLngs(); - + const latlngs = polyline.getLatLngs(); if (this._loadedCount == 0) { L.marker(latlngs[0], { icon: startIcon }).addTo(this._markers) @@ -280,26 +300,35 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ } }, - _onSelectionChanged: function (e) { + _onSelectionChanged: async function (e) { var elevation = this._elevation; var eleDiv = elevation.getContainer(); var route = this.getSelection(); var hotline = this._hotline; - elevation.clear(); + hotline.clearLayers(); if (route && route.isSelected()) { + elevation.clear(); + if (!eleDiv) { elevation.addTo(this._map); + this._map.invalidateSize() } - route.getLayers().forEach(function (layer) { + for (const layer of route.getLayers()) { if (layer instanceof L.Polyline) { - elevation.addData(layer, false); + await elevation.addData(layer, false); } - }); + } + await elevation._initHotLine(route, this._hotline) + + this._map.flyToBounds(route.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true }); + + } else { if (eleDiv) { elevation.remove(); + this._map.invalidateSize() } } }, diff --git a/web/src/lib/vendor/leaflet-elevation/src/control.js b/web/src/lib/vendor/leaflet-elevation/src/control.js index d74f7113..a9e1321d 100644 --- a/web/src/lib/vendor/leaflet-elevation/src/control.js +++ b/web/src/lib/vendor/leaflet-elevation/src/control.js @@ -38,18 +38,17 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ /* * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data */ - addData(d, layer) { - this.import(this.__D3) - .then(() => { - if (this._modulesLoaded) { - layer = layer ?? (d.on && d); - this._addData(d); - this._addLayer(layer); - this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info }); - } else { - this.once('modules_loaded', () => this.addData(d, layer)); - } - }); + async addData(d, layer) { + await this.import(this.__D3) + + if (this._modulesLoaded) { + layer = layer ?? (d.on && d); + this._addData(d); + this._addLayer(layer); + this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info }); + } else { + this.once('modules_loaded', () => this.addData(d, layer)); + } }, /** @@ -450,10 +449,14 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation'; return this.options.hotline ? this.import(/* @vite-ignore */this.__LHOTLINE) .then(() => { + const map = this._map + map.createPane('hotline'); + map.getPane('hotline').style.pointerEvents = 'none'; + layer.eachLayer((trkseg) => { if (trkseg.feature.geometry.type != "Point") { let line = L.hotline(this._data.map(m => [m.x, m.y, m[prop] || 0]), { - renderer: L.Hotline.renderer(), + renderer: L.Hotline.renderer({pane: "hotline"}), min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0, max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1, palette: { @@ -465,8 +468,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ outlineColor: '#000000', outlineWidth: 1 }).addTo(target); - console.log(this._data); - + let alpha = trkseg.options.style && trkseg.options.style.opacity || 1; trkseg.on('add remove', ({ type }) => { trkseg.setStyle({ opacity: (type == 'add' ? 0 : alpha) }); diff --git a/web/src/routes/lists/+page.svelte b/web/src/routes/lists/+page.svelte index f48904cf..0ead40e5 100644 --- a/web/src/routes/lists/+page.svelte +++ b/web/src/routes/lists/+page.svelte @@ -1,12 +1,14 @@ @@ -213,16 +94,16 @@ {$_("list", { values: { n: 2 } })} | wanderer
    -
    +
    -
    +
    {#each $lists as item, i}
  • {/each}
-
+
+ +
({ initialValues: data.list!, validationSchema: listSchema, onSubmit: async (submittedList) => { - (document.getElementById("avatar") as HTMLInputElement).value = ""; + const avatarFile = ( + document.getElementById("avatar") as HTMLInputElement + ).files![0]; + loading = true; + if ($form.id) { + await lists_update($form, avatarFile); + } else { + await lists_create($form, avatarFile); + } + loading = false; + + show_toast({ + type: "success", + icon: "check", + text: $_("list-saved-successfully"), + }); }, }); @@ -77,7 +100,7 @@ async function handleSearchClick(item: SearchItem) { const trail = await trails_show(item.value, true); - $form.trails?.push(trail); + $form.trails?.push(trail.id!); $form.expand!.trails = [...$form.expand!.trails, trail]; } @@ -155,9 +178,13 @@ items={searchDropdownItems} > {#if $form.expand?.trails.length} - {#each $form.expand?.trails ?? [] as trail} + + {#each $form.expand?.trails ?? [] as trail, i} +
map.selectTrail(i)} >
No routes added {/if} - {$_("save-list")} - +
diff --git a/web/src/routes/map/trail/[id]/+page.svelte b/web/src/routes/map/trail/[id]/+page.svelte index 874ceb3f..bca934cc 100644 --- a/web/src/routes/map/trail/[id]/+page.svelte +++ b/web/src/routes/map/trail/[id]/+page.svelte @@ -18,7 +18,7 @@
- +
diff --git a/web/src/routes/map/trail/[id]/print/+page.svelte b/web/src/routes/map/trail/[id]/print/+page.svelte index caaa89c2..221b9ada 100644 --- a/web/src/routes/map/trail/[id]/print/+page.svelte +++ b/web/src/routes/map/trail/[id]/print/+page.svelte @@ -509,7 +509,7 @@ >
{/if}