adds map_with_elevation_multiple
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { List } from "$lib/models/list";
|
import type { List } from "$lib/models/list";
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
import {
|
||||||
|
formatDistance,
|
||||||
|
formatElevation,
|
||||||
|
formatTimeHHMM,
|
||||||
|
} from "$lib/util/format_util";
|
||||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
export let list: List;
|
export let list: List;
|
||||||
@@ -10,6 +15,21 @@
|
|||||||
{ text: $_("edit"), value: "edit" },
|
{ text: $_("edit"), value: "edit" },
|
||||||
{ text: $_("delete"), value: "delete" },
|
{ text: $_("delete"), value: "delete" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$: cumulativeDistance = list.expand?.trails.reduce(
|
||||||
|
(s, b) => s + b.distance!,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
$: cumulativeElevationGain = list.expand?.trails.reduce(
|
||||||
|
(s, b) => s + b.elevation_gain!,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
$: cumulativeDuration = list.expand?.trails.reduce(
|
||||||
|
(s, b) => s + b.duration!,
|
||||||
|
0,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -18,13 +38,13 @@
|
|||||||
>
|
>
|
||||||
{#if list.avatar}
|
{#if list.avatar}
|
||||||
<img
|
<img
|
||||||
class="w-16 md:w-24 aspect-square rounded-full object-cover"
|
class="w-16 md:w-20 aspect-square rounded-full object-cover"
|
||||||
src={getFileURL(list, list.avatar)}
|
src={getFileURL(list, list.avatar)}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="flex w-16 md:w-24 aspect-square shrink-0 items-center justify-center"
|
class="flex w-16 md:w-20 aspect-square shrink-0 items-center justify-center"
|
||||||
>
|
>
|
||||||
<i class="fa fa-table-list text-5xl"></i>
|
<i class="fa fa-table-list text-5xl"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,13 +56,36 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<Dropdown items={dropdownItems} on:change></Dropdown>
|
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex mt-1 gap-4 text-sm text-gray-500 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||||
|
cumulativeDistance,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-up-down mr-2"></i>{formatElevation(
|
||||||
|
cumulativeElevationGain,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
|
||||||
|
cumulativeDuration,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 mb-2"
|
||||||
|
>{list.expand?.trails.length ?? 0}
|
||||||
|
{$_("trail", {
|
||||||
|
values: { n: list.expand?.trails.length ?? 0 },
|
||||||
|
})}</p
|
||||||
|
>
|
||||||
<p
|
<p
|
||||||
class="text-gray-500 text-sm mr-8 whitespace-pre-wrap {active
|
class="text-gray-500 text-sm mr-8 whitespace-pre-wrap {active
|
||||||
? ''
|
? ''
|
||||||
: 'max-h-24 overflow-hidden text-ellipsis'}"
|
: 'max-h-24 overflow-hidden text-ellipsis'}"
|
||||||
>
|
>
|
||||||
{!active ? list.description?.substring(0, 100) : list.description}
|
{!active ? list.description?.substring(0, 100) : list.description}
|
||||||
{#if ((list.description?.length ?? 0) > 100) && !active}
|
{#if (list.description?.length ?? 0) > 100 && !active}
|
||||||
...
|
...
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -2,43 +2,41 @@
|
|||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import { Settings } from "$lib/models/settings";
|
import { Settings } from "$lib/models/settings";
|
||||||
import type { Trail } from "$lib/models/trail";
|
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 "$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 { Layer, Map, Marker, Polyline } from "leaflet";
|
import type { Map, Marker } 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 trails: Trail[];
|
export let trail: Trail | null;
|
||||||
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 gpxGroup: any;
|
let controlElevation: any;
|
||||||
|
|
||||||
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
|
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
|
||||||
|
|
||||||
$: gpxData = trails.map((t) => t.expand.gpx_data);
|
$: gpxData = trail?.expand.gpx_data;
|
||||||
$: if (gpxData && gpxGroup) {
|
$: if (gpxData && controlElevation) {
|
||||||
gpxGroup._elevation.updateOptions({
|
controlElevation.updateOptions({
|
||||||
autofitBounds: options.autofitBounds ?? true,
|
autofitBounds: options.autofitBounds ?? true,
|
||||||
});
|
});
|
||||||
gpxGroup.clear();
|
controlElevation.clear();
|
||||||
gpxGroup._tracks = gpxData;
|
controlElevation.load(gpxData);
|
||||||
gpxGroup.addTracks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (options && gpxGroup) {
|
$: if (options) {
|
||||||
gpxGroup._elevation.updateOptions(options);
|
controlElevation?.updateOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: hotlineSwitcherItems = [
|
$: hotlineSwitcherItems = [
|
||||||
@@ -68,21 +66,14 @@
|
|||||||
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", {
|
map = L.map("map", { preferCanvas: true }).setView(
|
||||||
preferCanvas: true,
|
[trail?.lat ?? 0, trail?.lon ?? 0],
|
||||||
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);
|
||||||
@@ -112,9 +103,7 @@
|
|||||||
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<
|
...($page.data.settings as Settings)?.tilesets?.reduce< Record<string, string>>((t, current) => {
|
||||||
Record<string, string>
|
|
||||||
>((t, current) => {
|
|
||||||
t[current.name] = L.tileLayer(current.url);
|
t[current.name] = L.tileLayer(current.url);
|
||||||
return t;
|
return t;
|
||||||
}, {}),
|
}, {}),
|
||||||
@@ -170,7 +159,7 @@
|
|||||||
lazy: false,
|
lazy: false,
|
||||||
distance: false,
|
distance: false,
|
||||||
direction: true,
|
direction: true,
|
||||||
offset: 1000,
|
offset: 500,
|
||||||
},
|
},
|
||||||
// Toggle "leaflet-edgescale" integration
|
// Toggle "leaflet-edgescale" integration
|
||||||
edgeScale: false,
|
edgeScale: false,
|
||||||
@@ -182,6 +171,14 @@
|
|||||||
wptIcons: false,
|
wptIcons: false,
|
||||||
wptLabels: false,
|
wptLabels: false,
|
||||||
preferCanvas: true,
|
preferCanvas: true,
|
||||||
|
trkStart: {
|
||||||
|
interactive: false,
|
||||||
|
className: "hihi",
|
||||||
|
icon: startIcon()
|
||||||
|
},
|
||||||
|
trkEnd: {
|
||||||
|
icon: endIcon(),
|
||||||
|
},
|
||||||
graticule: false,
|
graticule: false,
|
||||||
drawing: false,
|
drawing: false,
|
||||||
};
|
};
|
||||||
@@ -191,34 +188,13 @@
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
gpxGroup = L.gpxGroup(gpxData, {
|
controlElevation = L.control.elevation(elevation_options).addTo(map);
|
||||||
points: [],
|
|
||||||
// points_options: opts.points,
|
|
||||||
elevation: true,
|
|
||||||
elevation_options: elevation_options,
|
|
||||||
flyToBounds: true,
|
|
||||||
distanceMarkers: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const markerLayerGroup = L.layerGroup().addTo(map);
|
for (const waypoint of trail?.expand.waypoints ?? []) {
|
||||||
|
|
||||||
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);
|
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||||
marker.addTo(markerLayerGroup!);
|
marker.addTo(map!);
|
||||||
markers.push(marker);
|
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();
|
||||||
@@ -227,15 +203,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
|
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
|
||||||
gpxGroup._elevation.updateOptions({
|
controlElevation.updateOptions({
|
||||||
hotline: metric,
|
hotline: metric,
|
||||||
autofitBounds: false,
|
autofitBounds: false,
|
||||||
});
|
});
|
||||||
selectedMetric = metric;
|
selectedMetric = metric;
|
||||||
localStorage.setItem("gradient", metric.toString());
|
localStorage.setItem("gradient", metric.toString());
|
||||||
gpxGroup.clear();
|
controlElevation.clear();
|
||||||
gpxGroup._tracks = gpxData;
|
controlElevation.load(gpxData);
|
||||||
gpxGroup.addTracks();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
321
web/src/lib/components/trail/map_with_elevation_multiple.svelte
Normal file
321
web/src/lib/components/trail/map_with_elevation_multiple.svelte
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { Settings } from "$lib/models/settings";
|
||||||
|
import type { Trail } from "$lib/models/trail";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
import {
|
||||||
|
formatDistance,
|
||||||
|
formatElevation,
|
||||||
|
formatTimeHHMM,
|
||||||
|
} from "$lib/util/format_util";
|
||||||
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
|
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 map: any | null = null;
|
||||||
|
export let options: any = {};
|
||||||
|
export let activeTrailIndex: number | null = 0;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let L: any;
|
||||||
|
let gpxGroup: any;
|
||||||
|
|
||||||
|
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
|
||||||
|
|
||||||
|
$: gpxData = trails.map((t) => t.expand.gpx_data);
|
||||||
|
$: if (
|
||||||
|
gpxData &&
|
||||||
|
gpxGroup &&
|
||||||
|
map &&
|
||||||
|
(gpxData.length != gpxGroup._tracks.length ||
|
||||||
|
gpxGroup?._tracks[0] !== gpxData[0])
|
||||||
|
) {
|
||||||
|
if (gpxData.length == 0) {
|
||||||
|
map?.setView([0, 0], 4);
|
||||||
|
}
|
||||||
|
gpxGroup._elevation.updateOptions({
|
||||||
|
autofitBounds: options.autofitBounds ?? true,
|
||||||
|
});
|
||||||
|
gpxGroup.clear();
|
||||||
|
gpxGroup._tracks = gpxData;
|
||||||
|
gpxGroup.addTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (options && gpxGroup) {
|
||||||
|
gpxGroup._elevation.updateOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: hotlineSwitcherItems = [
|
||||||
|
{
|
||||||
|
text: $_("altitude"),
|
||||||
|
value: "altitude",
|
||||||
|
icon: selectedMetric == "altitude" ? "circle-dot" : "circle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: $_("slope"),
|
||||||
|
value: "slope",
|
||||||
|
icon: selectedMetric == "slope" ? "circle-dot" : "circle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: $_("speed"),
|
||||||
|
value: "speed",
|
||||||
|
icon: selectedMetric == "speed" ? "circle-dot" : "circle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: $_("off"),
|
||||||
|
value: false,
|
||||||
|
icon: selectedMetric == false ? "circle-dot" : "circle",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
L = (await import("leaflet")).default;
|
||||||
|
await import("leaflet-gpx");
|
||||||
|
await import("leaflet.awesome-markers");
|
||||||
|
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 ?? 0)?.lat ?? 0,
|
||||||
|
trails.at(activeTrailIndex ?? 0)?.lon ?? 0,
|
||||||
|
],
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
map!.attributionControl.setPrefix(false);
|
||||||
|
|
||||||
|
map!.on("zoomend", function () {
|
||||||
|
dispatch("zoomend", map);
|
||||||
|
});
|
||||||
|
|
||||||
|
map!.on("click", function (e: any) {
|
||||||
|
dispatch("click", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseLayer = L.tileLayer(
|
||||||
|
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
{
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const topoLayer = L.tileLayer(
|
||||||
|
"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
|
||||||
|
{
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseMaps: Record<string, L.TileLayer> = {
|
||||||
|
OpenStreetMaps: baseLayer,
|
||||||
|
OpenTopoMaps: topoLayer,
|
||||||
|
...($page.data.settings as Settings)?.tilesets?.reduce<
|
||||||
|
Record<string, string>
|
||||||
|
>((t, current) => {
|
||||||
|
t[current.name] = L.tileLayer(current.url);
|
||||||
|
return t;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
L.control.layers(baseMaps).addTo(map);
|
||||||
|
|
||||||
|
const layerPreference = localStorage.getItem("layer");
|
||||||
|
|
||||||
|
if (
|
||||||
|
layerPreference &&
|
||||||
|
Object.keys(baseMaps).includes(layerPreference)
|
||||||
|
) {
|
||||||
|
baseMaps[layerPreference].addTo(map!);
|
||||||
|
} else {
|
||||||
|
baseLayer.addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
map!.on("baselayerchange", function (e: any) {
|
||||||
|
localStorage.setItem("layer", e.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const localMetric =
|
||||||
|
(localStorage.getItem("gradient") as any) ?? "altitude";
|
||||||
|
selectedMetric = localMetric === "false" ? false : localMetric;
|
||||||
|
|
||||||
|
const default_elevation_options = {
|
||||||
|
height: 200,
|
||||||
|
|
||||||
|
theme: "lightblue-theme",
|
||||||
|
detached: true,
|
||||||
|
elevationDiv: "#elevation",
|
||||||
|
closeBtn: false,
|
||||||
|
followMarker: true,
|
||||||
|
autofitBounds: true,
|
||||||
|
imperial: $page.data.settings?.unit == "imperial" ?? false,
|
||||||
|
reverseCoords: false,
|
||||||
|
acceleration: false,
|
||||||
|
slope: true,
|
||||||
|
speed: true,
|
||||||
|
altitude: true,
|
||||||
|
time: true,
|
||||||
|
distance: true,
|
||||||
|
// Summary track info style: "inline" || "multiline" || false
|
||||||
|
summary: false,
|
||||||
|
downloadLink: false,
|
||||||
|
ruler: false,
|
||||||
|
legend: true,
|
||||||
|
// Toggle "leaflet-almostover" integration
|
||||||
|
almostOver: false,
|
||||||
|
// Toggle "leaflet-distance-markers" integration
|
||||||
|
distanceMarkers: {
|
||||||
|
lazy: false,
|
||||||
|
distance: false,
|
||||||
|
direction: true,
|
||||||
|
offset: 1000,
|
||||||
|
},
|
||||||
|
// Toggle "leaflet-edgescale" integration
|
||||||
|
edgeScale: false,
|
||||||
|
// Toggle "leaflet-hotline" integration
|
||||||
|
hotline: selectedMetric,
|
||||||
|
// Display track datetimes: true || false
|
||||||
|
timestamps: false,
|
||||||
|
waypoints: false,
|
||||||
|
wptIcons: false,
|
||||||
|
wptLabels: false,
|
||||||
|
preferCanvas: true,
|
||||||
|
graticule: false,
|
||||||
|
drawing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const elevation_options = Object.assign(
|
||||||
|
default_elevation_options,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
gpxGroup = L.gpxGroup(gpxData, {
|
||||||
|
points: [],
|
||||||
|
// points_options: opts.points,
|
||||||
|
elevation: true,
|
||||||
|
elevation_options: elevation_options,
|
||||||
|
flyToBounds: true,
|
||||||
|
distanceMarkers: true,
|
||||||
|
removeElevationOnDeselect: options.removeElevationOnDeselect,
|
||||||
|
});
|
||||||
|
|
||||||
|
const markerLayerGroup = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
|
gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => {
|
||||||
|
markerLayerGroup.clearLayers();
|
||||||
|
|
||||||
|
activeTrailIndex = null;
|
||||||
|
|
||||||
|
if (polyline._selected) {
|
||||||
|
activeTrailIndex = polyline.options.index;
|
||||||
|
for (const waypoint of trails.at(activeTrailIndex!)?.expand
|
||||||
|
.waypoints ?? []) {
|
||||||
|
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||||
|
marker.addTo(markerLayerGroup!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gpxGroup.on("clear", () => {
|
||||||
|
markerLayerGroup.clearLayers();
|
||||||
|
activeTrailIndex = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
gpxGroup.on("route_loaded", ({ route }: { route: any }) => {
|
||||||
|
|
||||||
|
const trail = trails.find((t) => gpxGroup?._hashCode(t.expand.gpx_data) == route.options.hash)
|
||||||
|
|
||||||
|
if (!trail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const thumbnail = trail.photos.length
|
||||||
|
? getFileURL(trail, trail.photos[trail.thumbnail])
|
||||||
|
: "/imgs/default_thumbnail.webp";
|
||||||
|
route.bindPopup(
|
||||||
|
`<a href="/trail/view/${trail.id}" data-sveltekit-preload-data="false">
|
||||||
|
<li class="flex items-center gap-4 cursor-pointer text-black max-w-80">
|
||||||
|
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${thumbnail}" alt="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
||||||
|
<div class="flex gap-x-4">
|
||||||
|
${trail.location ? `<h5><i class="fa fa-location-dot mr-2"></i>${trail.location}</h5>` : ""}
|
||||||
|
<h5><i class="fa fa-gauge mr-2"></i>${$_(trail.difficulty as string)}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-2 gap-4 text-sm text-gray-500 flex-wrap"><span class="shrink-0"><i
|
||||||
|
class="fa fa-left-right mr-2"></i>${formatDistance(
|
||||||
|
trail.distance,
|
||||||
|
)}</span> <span class="shrink-0"><i class="fa fa-up-down mr-2"></i>${formatElevation(
|
||||||
|
trail.elevation_gain,
|
||||||
|
)}</span> <span class="shrink-0"><i class="fa fa-clock mr-2"></i>${formatTimeHHMM(
|
||||||
|
trail.duration,
|
||||||
|
)}</span></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</a>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gpxGroup.addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function selectTrail(index: number) {
|
||||||
|
gpxGroup.select(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
|
||||||
|
if (metric === selectedMetric) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gpxGroup._elevation.updateOptions({
|
||||||
|
hotline: metric,
|
||||||
|
});
|
||||||
|
gpxGroup.options.flyToBounds = false;
|
||||||
|
selectedMetric = metric;
|
||||||
|
localStorage.setItem("gradient", metric.toString());
|
||||||
|
gpxGroup.clear();
|
||||||
|
gpxGroup._tracks = gpxData;
|
||||||
|
gpxGroup.addTracks();
|
||||||
|
|
||||||
|
gpxGroup.options.flyToBounds = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="map-container" class="flex flex-col h-full">
|
||||||
|
<div
|
||||||
|
id="map"
|
||||||
|
class="rounded-xl z-0 basis-full min-h-96 md:min-h-0"
|
||||||
|
style="position: relative; outline-style: none;"
|
||||||
|
>
|
||||||
|
<div class="absolute top-20 right-3 text-sm" style="z-index: 500">
|
||||||
|
<Dropdown
|
||||||
|
items={hotlineSwitcherItems}
|
||||||
|
on:change={(e) => switchHotline(e.detail.value)}
|
||||||
|
let:toggleMenu={openDropdown}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="rounded-md border-2 border-black border-opacity-30 bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-12 w-12"
|
||||||
|
on:click={openDropdown}
|
||||||
|
>
|
||||||
|
<i class="fa fa-route text-lg"></i>
|
||||||
|
</button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<slot />
|
||||||
|
<div class="basis-[300px] flex-grow flex-shrink-0" id="elevation"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "Lizenz",
|
"license": "Lizenz",
|
||||||
"link-copied": "Link kopiert",
|
"link-copied": "Link kopiert",
|
||||||
"list": "{n, plural, =1 {Liste} other {Listen}}",
|
"list": "{n, plural, =1 {Liste} other {Listen}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "Standort",
|
"location": "Standort",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"login-details": "Login Details",
|
"login-details": "Login Details",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "License",
|
"license": "License",
|
||||||
"link-copied": "Link copied!",
|
"link-copied": "Link copied!",
|
||||||
"list": "{n, plural, =1 {List} other {Lists}}",
|
"list": "{n, plural, =1 {List} other {Lists}}",
|
||||||
|
"list-saved-successfully": "List saved successfully",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"login-details": "Login details",
|
"login-details": "Login details",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "Licence",
|
"license": "Licence",
|
||||||
"link-copied": "",
|
"link-copied": "",
|
||||||
"list": "{n, plural, =1 {Liste} other {Listes}}",
|
"list": "{n, plural, =1 {Liste} other {Listes}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "Localisation",
|
"location": "Localisation",
|
||||||
"login": "Connexion",
|
"login": "Connexion",
|
||||||
"login-details": "",
|
"login-details": "",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "License",
|
"license": "License",
|
||||||
"link-copied": "",
|
"link-copied": "",
|
||||||
"list": "{n, plural, =1 {Lista} other {Listák}}",
|
"list": "{n, plural, =1 {Lista} other {Listák}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "Helyszín",
|
"location": "Helyszín",
|
||||||
"login": "Bejelentkezés",
|
"login": "Bejelentkezés",
|
||||||
"login-details": "",
|
"login-details": "",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "Licenza",
|
"license": "Licenza",
|
||||||
"link-copied": "Link copiato",
|
"link-copied": "Link copiato",
|
||||||
"list": "{n, plural, =1 {Lista} other {Liste}}",
|
"list": "{n, plural, =1 {Lista} other {Liste}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "Posizione",
|
"location": "Posizione",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"login-details": "",
|
"login-details": "",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "Licentie",
|
"license": "Licentie",
|
||||||
"link-copied": "",
|
"link-copied": "",
|
||||||
"list": "{n, plural, =1 {Lijst} other {Lijsten}}",
|
"list": "{n, plural, =1 {Lijst} other {Lijsten}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "Locatie",
|
"location": "Locatie",
|
||||||
"login": "Inloggen",
|
"login": "Inloggen",
|
||||||
"login-details": "",
|
"login-details": "",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "Licencja",
|
"license": "Licencja",
|
||||||
"link-copied": "",
|
"link-copied": "",
|
||||||
"list": "{n, plural, =1 {Lista} other {Listy}}",
|
"list": "{n, plural, =1 {Lista} other {Listy}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "Lokalizacja",
|
"location": "Lokalizacja",
|
||||||
"login": "Zaloguj się",
|
"login": "Zaloguj się",
|
||||||
"login-details": "",
|
"login-details": "",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "Licença",
|
"license": "Licença",
|
||||||
"link-copied": "",
|
"link-copied": "",
|
||||||
"list": "{n, plural, =1 {Lista} other {Listas}}",
|
"list": "{n, plural, =1 {Lista} other {Listas}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "Localização",
|
"location": "Localização",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"login-details": "",
|
"login-details": "",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"license": "开源协议",
|
"license": "开源协议",
|
||||||
"link-copied": "",
|
"link-copied": "",
|
||||||
"list": "{n, plural, =1 {列表} other {列表}}",
|
"list": "{n, plural, =1 {列表} other {列表}}",
|
||||||
|
"list-saved-successfully": "",
|
||||||
"location": "地点",
|
"location": "地点",
|
||||||
"login": "登录",
|
"login": "登录",
|
||||||
"login-details": "",
|
"login-details": "",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { pb } from "$lib/pocketbase";
|
|||||||
import { getFileURL } from "$lib/util/file_util";
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import { fetchGPX } from "./trail_store";
|
||||||
|
|
||||||
export const lists: Writable<List[]> = writable([])
|
export const lists: Writable<List[]> = writable([])
|
||||||
export const list: Writable<List> = writable(new List("", []))
|
export const list: Writable<List> = writable(new List("", []))
|
||||||
@@ -38,6 +39,12 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
|
|||||||
})
|
})
|
||||||
const response = await r.json()
|
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) {
|
if (!r.ok) {
|
||||||
throw new ClientResponseError(response)
|
throw new ClientResponseError(response)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques
|
|||||||
|
|
||||||
trail.set(response);
|
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<Response> = fetch) {
|
export async function trails_create(trail: Trail, photos: File[], gpx: File | Blob | null, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import type { Waypoint } from "$lib/models/waypoint";
|
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: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-bullseye -translate-x-1/2"></i>',
|
||||||
|
className: 'start-icon'
|
||||||
|
});
|
||||||
|
export const endIcon = () => L.divIcon({
|
||||||
|
html: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-flag-checkered -translate-x-1/2"></i>',
|
||||||
|
className: 'end-icon'
|
||||||
|
});
|
||||||
|
|
||||||
export function createMarkerFromWaypoint(L: any, waypoint: Waypoint, onDragEnd?: (event: LeafletEvent) => void): Marker {
|
export function createMarkerFromWaypoint(L: any, waypoint: Waypoint, onDragEnd?: (event: LeafletEvent) => void): Marker {
|
||||||
const fontAwesomeIcon = L.AwesomeMarkers.icon({
|
const icon = L.divIcon({
|
||||||
icon: waypoint.icon,
|
html: `<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-${waypoint.icon}"></i>`,
|
||||||
prefix: "fa",
|
className: 'waypoint-icon'
|
||||||
markerColor: "cadetblue",
|
});
|
||||||
iconColor: "white",
|
|
||||||
}) as Icon;
|
|
||||||
|
|
||||||
const marker = L.marker([waypoint.lat, waypoint.lon], {
|
const marker = L.marker([waypoint.lat, waypoint.lon], {
|
||||||
title: waypoint.name,
|
title: waypoint.name,
|
||||||
icon: fontAwesomeIcon,
|
icon: icon,
|
||||||
draggable: onDragEnd != null,
|
draggable: onDragEnd != null,
|
||||||
meta: {
|
meta: {
|
||||||
waypointName: waypoint.name
|
waypointName: waypoint.name
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
this._hotline = L.featureGroup();
|
this._hotline = L.featureGroup();
|
||||||
this._elevation = L.control.elevation(this.options.elevation_options);
|
this._elevation = L.control.elevation(this.options.elevation_options);
|
||||||
|
|
||||||
|
this._routes = [];
|
||||||
|
|
||||||
this.options.points.forEach((poi) =>
|
this.options.points.forEach((poi) =>
|
||||||
L
|
L
|
||||||
.marker(poi.latlng, { icon: L.icon(this.options.points_options.icon) })
|
.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) {
|
addTo: function (map) {
|
||||||
|
this._hotline.addTo(map);
|
||||||
this._layers.addTo(map);
|
this._layers.addTo(map);
|
||||||
this._markers.addTo(map);
|
this._markers.addTo(map);
|
||||||
this._hotline.addTo(map);
|
|
||||||
|
|
||||||
this._map = map;
|
this._map = map;
|
||||||
|
|
||||||
@@ -137,23 +139,37 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
this._tracks.forEach(this._addTrack, this);
|
this._tracks.forEach(this._addTrack, this);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
select(index) {
|
||||||
|
if (index > this._routes.length - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setSelection(this._routes[index])
|
||||||
|
},
|
||||||
_addTrack: function (track) {
|
_addTrack: function (track) {
|
||||||
if (track instanceof Object) {
|
if (track instanceof Object) {
|
||||||
this._loadGeoJSON(track);
|
this._loadGeoJSON(track);
|
||||||
} else {
|
} else {
|
||||||
this._elevation._parseFromString(track)
|
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 () {
|
clear: function () {
|
||||||
this._elevation.clear()
|
this._elevation.clear()
|
||||||
|
this._elevation.remove();
|
||||||
this._clearLayers();
|
this._clearLayers();
|
||||||
this._clearLayers(this._markers);
|
this._clearLayers(this._markers);
|
||||||
this._clearLayers(this._hotline)
|
this._clearLayers(this._hotline)
|
||||||
this._count = 0;
|
this._count = 0;
|
||||||
this._loadedCount = 0;
|
this._loadedCount = 0;
|
||||||
this._tracks = []
|
this._tracks = []
|
||||||
|
this._routes = []
|
||||||
|
|
||||||
|
this.fire('clear')
|
||||||
},
|
},
|
||||||
|
|
||||||
_clearLayers(l) {
|
_clearLayers(l) {
|
||||||
@@ -164,8 +180,9 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_loadGeoJSON: function (geojson, fallbackName) {
|
_loadGeoJSON: function (geojson, hash, fallbackName) {
|
||||||
if (geojson) {
|
if (geojson) {
|
||||||
|
geojson.hash = hash
|
||||||
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
|
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
|
||||||
this._loadRoute(geojson);
|
this._loadRoute(geojson);
|
||||||
}
|
}
|
||||||
@@ -187,7 +204,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
distanceMarkers: line_style.distanceMarkers,
|
distanceMarkers: line_style.distanceMarkers,
|
||||||
originalStyle: line_style,
|
originalStyle: line_style,
|
||||||
isGroupLayer: true,
|
isGroupLayer: true,
|
||||||
index: this._count - 1,
|
hash: data.hash,
|
||||||
filter: feature => feature.geometry.type != "Point",
|
filter: feature => feature.geometry.type != "Point",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,6 +213,8 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
route.addTo(this._layers);
|
route.addTo(this._layers);
|
||||||
|
|
||||||
route.eachLayer((layer) => this._onEachRouteLayer(route, layer));
|
route.eachLayer((layer) => this._onEachRouteLayer(route, layer));
|
||||||
|
|
||||||
|
|
||||||
this._onEachRouteLoaded(route);
|
this._onEachRouteLoaded(route);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,6 +223,8 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
_onEachRouteLayer: function (route, layer) {
|
_onEachRouteLayer: function (route, layer) {
|
||||||
var polyline = layer;
|
var polyline = layer;
|
||||||
|
|
||||||
|
this._routes.push(route)
|
||||||
|
|
||||||
route.on('selected', L.bind(this._onRouteSelected, this, route, polyline));
|
route.on('selected', L.bind(this._onRouteSelected, this, route, polyline));
|
||||||
|
|
||||||
polyline.on('mouseover', L.bind(this._onRouteMouseOver, this, route, polyline));
|
polyline.on('mouseover', L.bind(this._onRouteMouseOver, this, route, polyline));
|
||||||
@@ -211,16 +232,15 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
polyline.on('click', L.bind(this._onRouteClick, this, route, polyline));
|
polyline.on('click', L.bind(this._onRouteClick, this, route, polyline));
|
||||||
|
|
||||||
const startIcon = L.divIcon({
|
const startIcon = L.divIcon({
|
||||||
html: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-bullseye"></i>',
|
html: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-bullseye -translate-x-1/2"></i>',
|
||||||
className: 'start-icon'
|
className: 'start-icon'
|
||||||
});
|
});
|
||||||
const endIcon = L.divIcon({
|
const endIcon = L.divIcon({
|
||||||
html: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-flag-checkered"></i>',
|
html: '<i class="px-2 py-2 text-white bg-gray-500 rounded-lg fa fa-flag-checkered -translate-x-1/2"></i>',
|
||||||
className: 'end-icon'
|
className: 'end-icon'
|
||||||
});
|
});
|
||||||
const latlngs = polyline.getLatLngs();
|
const latlngs = polyline.getLatLngs();
|
||||||
|
|
||||||
|
|
||||||
if (this._loadedCount == 0) {
|
if (this._loadedCount == 0) {
|
||||||
L.marker(latlngs[0], { icon: startIcon }).addTo(this._markers)
|
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 elevation = this._elevation;
|
||||||
var eleDiv = elevation.getContainer();
|
var eleDiv = elevation.getContainer();
|
||||||
var route = this.getSelection();
|
var route = this.getSelection();
|
||||||
var hotline = this._hotline;
|
var hotline = this._hotline;
|
||||||
|
|
||||||
elevation.clear();
|
hotline.clearLayers();
|
||||||
|
|
||||||
if (route && route.isSelected()) {
|
if (route && route.isSelected()) {
|
||||||
|
elevation.clear();
|
||||||
|
|
||||||
if (!eleDiv) {
|
if (!eleDiv) {
|
||||||
elevation.addTo(this._map);
|
elevation.addTo(this._map);
|
||||||
|
this._map.invalidateSize()
|
||||||
}
|
}
|
||||||
route.getLayers().forEach(function (layer) {
|
for (const layer of route.getLayers()) {
|
||||||
if (layer instanceof L.Polyline) {
|
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 {
|
} else {
|
||||||
if (eleDiv) {
|
if (eleDiv) {
|
||||||
elevation.remove();
|
elevation.remove();
|
||||||
|
this._map.invalidateSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ 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
|
* Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
|
||||||
*/
|
*/
|
||||||
addData(d, layer) {
|
async addData(d, layer) {
|
||||||
this.import(this.__D3)
|
await this.import(this.__D3)
|
||||||
.then(() => {
|
|
||||||
if (this._modulesLoaded) {
|
if (this._modulesLoaded) {
|
||||||
layer = layer ?? (d.on && d);
|
layer = layer ?? (d.on && d);
|
||||||
this._addData(d);
|
this._addData(d);
|
||||||
@@ -49,7 +49,6 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
|||||||
} else {
|
} else {
|
||||||
this.once('modules_loaded', () => this.addData(d, layer));
|
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';
|
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(() => {
|
||||||
|
const map = this._map
|
||||||
|
map.createPane('hotline');
|
||||||
|
map.getPane('hotline').style.pointerEvents = 'none';
|
||||||
|
|
||||||
layer.eachLayer((trkseg) => {
|
layer.eachLayer((trkseg) => {
|
||||||
if (trkseg.feature.geometry.type != "Point") {
|
if (trkseg.feature.geometry.type != "Point") {
|
||||||
let line = L.hotline(this._data.map(m => [m.x, m.y, m[prop] || 0]), {
|
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,
|
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,
|
max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
|
||||||
palette: {
|
palette: {
|
||||||
@@ -465,7 +468,6 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
|||||||
outlineColor: '#000000',
|
outlineColor: '#000000',
|
||||||
outlineWidth: 1
|
outlineWidth: 1
|
||||||
}).addTo(target);
|
}).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 }) => {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||||
import ListCard from "$lib/components/list/list_card.svelte";
|
import ListCard from "$lib/components/list/list_card.svelte";
|
||||||
import ListModal from "$lib/components/list/list_modal.svelte";
|
import ListModal from "$lib/components/list/list_modal.svelte";
|
||||||
|
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte";
|
||||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||||
import { List } from "$lib/models/list";
|
import { List } from "$lib/models/list";
|
||||||
import type { Trail, TrailFilter } from "$lib/models/trail";
|
import type { TrailFilter } from "$lib/models/trail";
|
||||||
import {
|
import {
|
||||||
list,
|
list,
|
||||||
lists,
|
lists,
|
||||||
@@ -16,25 +18,14 @@
|
|||||||
lists_update,
|
lists_update,
|
||||||
} from "$lib/stores/list_store";
|
} from "$lib/stores/list_store";
|
||||||
import { fetchGPX } from "$lib/stores/trail_store";
|
import { fetchGPX } from "$lib/stores/trail_store";
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
|
||||||
import {
|
|
||||||
formatDistance,
|
|
||||||
formatElevation,
|
|
||||||
formatTimeHHMM,
|
|
||||||
} from "$lib/util/format_util";
|
|
||||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
import type {
|
import type {
|
||||||
GPX,
|
Map
|
||||||
Icon,
|
|
||||||
LatLngBoundsExpression,
|
|
||||||
LeafletEvent,
|
|
||||||
Map,
|
|
||||||
Marker,
|
|
||||||
} from "leaflet";
|
} 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 { onMount, tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let openListModal: () => void;
|
let openListModal: () => void;
|
||||||
@@ -43,89 +34,8 @@
|
|||||||
let filter: TrailFilter = $page.data.filter;
|
let filter: TrailFilter = $page.data.filter;
|
||||||
let listToBeDeleted: List | null = null;
|
let listToBeDeleted: List | null = null;
|
||||||
|
|
||||||
let L: any;
|
|
||||||
let map: Map;
|
let map: Map;
|
||||||
let showMap: boolean = false;
|
let showMap: boolean = true;
|
||||||
let gpxLayers: GPX[] = [];
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
L = (await import("leaflet")).default;
|
|
||||||
await import("leaflet-gpx");
|
|
||||||
await import("leaflet.awesome-markers");
|
|
||||||
|
|
||||||
map = L.map("map").setView([0, 0], 4);
|
|
||||||
map.attributionControl.setPrefix(false);
|
|
||||||
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
||||||
attribution: "© OpenStreetMap contributors",
|
|
||||||
}).addTo(map);
|
|
||||||
});
|
|
||||||
|
|
||||||
function addGPXLayer(trail: Trail) {
|
|
||||||
return new Promise<GPX>(function (resolve, reject) {
|
|
||||||
if (!trail.expand.gpx_data) {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnail = trail.photos.length
|
|
||||||
? getFileURL(trail, trail.photos[trail.thumbnail])
|
|
||||||
: "/imgs/default_thumbnail.webp";
|
|
||||||
const gpxLayer = new L.GPX(trail.expand.gpx_data!, {
|
|
||||||
async: true,
|
|
||||||
polyline_options: {
|
|
||||||
className: "lightblue-theme elevation-polyline",
|
|
||||||
weight: 5,
|
|
||||||
},
|
|
||||||
gpx_options: {
|
|
||||||
parseElements: ["track", "route"],
|
|
||||||
},
|
|
||||||
marker_options: {
|
|
||||||
startIcon: L.AwesomeMarkers.icon({
|
|
||||||
icon: "circle-half-stroke",
|
|
||||||
prefix: "fa",
|
|
||||||
markerColor: "cadetblue",
|
|
||||||
iconColor: "white",
|
|
||||||
}) as Icon,
|
|
||||||
startIconUrl: "",
|
|
||||||
endIconUrl: "",
|
|
||||||
shadowUrl: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.on("addpoint", function (e: any) {
|
|
||||||
if (e.point_type === "start") {
|
|
||||||
const marker: Marker = e.point as Marker;
|
|
||||||
marker.bindPopup(
|
|
||||||
`<a href="map/trail/${trail.id}">
|
|
||||||
<li class="flex items-center gap-4 cursor-pointer text-black">
|
|
||||||
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${thumbnail}" alt="">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
|
||||||
<div class="flex gap-x-4">
|
|
||||||
${trail.location ? `<h5><i class="fa fa-location-dot mr-2"></i>${trail.location}</h5>` : ""}
|
|
||||||
<h5><i class="fa fa-gauge mr-2"></i>${$_(trail.difficulty as string)}</h5>
|
|
||||||
</div>
|
|
||||||
<div class="flex mt-2 gap-4 text-sm text-gray-500"><span class="shrink-0"><i
|
|
||||||
class="fa fa-left-right mr-2"></i>${formatDistance(
|
|
||||||
trail.distance,
|
|
||||||
)}</span> <span class="shrink-0"><i class="fa fa-up-down mr-2"></i>${formatElevation(
|
|
||||||
trail.elevation_gain,
|
|
||||||
)}</span> <span class="shrink-0"><i class="fa fa-clock mr-2"></i>${formatTimeHHMM(
|
|
||||||
trail.duration,
|
|
||||||
)}</span></div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</a>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on("loaded", function (e: LeafletEvent) {
|
|
||||||
resolve(gpxLayer);
|
|
||||||
})
|
|
||||||
.on("error", reject)
|
|
||||||
.addTo(map);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleMap() {
|
async function toggleMap() {
|
||||||
showMap = !showMap;
|
showMap = !showMap;
|
||||||
@@ -136,11 +46,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeListModalOpen() {
|
|
||||||
list.set(new List("", []));
|
|
||||||
openListModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveList(e: CustomEvent<{ list: List; avatar?: File }>) {
|
async function saveList(e: CustomEvent<{ list: List; avatar?: File }>) {
|
||||||
const result = e.detail;
|
const result = e.detail;
|
||||||
if (result.list.id) {
|
if (result.list.id) {
|
||||||
@@ -158,8 +63,7 @@
|
|||||||
const item = e.detail;
|
const item = e.detail;
|
||||||
|
|
||||||
if (item.value == "edit") {
|
if (item.value == "edit") {
|
||||||
list.set(currentList);
|
goto("/lists/edit/" + currentList.id);
|
||||||
openListModal();
|
|
||||||
} else if (item.value == "delete") {
|
} else if (item.value == "delete") {
|
||||||
openConfirmModal();
|
openConfirmModal();
|
||||||
listToBeDeleted = currentList;
|
listToBeDeleted = currentList;
|
||||||
@@ -176,36 +80,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setCurrentList(item: List) {
|
async function setCurrentList(item: List) {
|
||||||
for (const layer of gpxLayers) {
|
|
||||||
map.removeLayer(layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
list.set(item);
|
|
||||||
|
|
||||||
if (item.expand && item.expand.trails.length > 0) {
|
if (item.expand && item.expand.trails.length > 0) {
|
||||||
let minLat = item.expand.trails[0].lat!;
|
|
||||||
let maxLat = item.expand.trails[0].lat!;
|
|
||||||
let minLon = item.expand.trails[0].lon!;
|
|
||||||
let maxLon = item.expand.trails[0].lon!;
|
|
||||||
|
|
||||||
for (const trail of item.expand.trails) {
|
for (const trail of item.expand.trails) {
|
||||||
minLat = Math.min(minLat, trail.lat!);
|
|
||||||
maxLat = Math.max(maxLat, trail.lat!);
|
|
||||||
minLon = Math.min(minLon, trail.lon!);
|
|
||||||
maxLon = Math.max(maxLon, trail.lon!);
|
|
||||||
|
|
||||||
const gpxData: string = await fetchGPX(trail);
|
const gpxData: string = await fetchGPX(trail);
|
||||||
trail.expand.gpx_data = gpxData;
|
trail.expand.gpx_data = gpxData;
|
||||||
gpxLayers.push(await addGPXLayer(trail));
|
|
||||||
}
|
}
|
||||||
const boundingBox: LatLngBoundsExpression = [
|
|
||||||
[maxLat, minLon],
|
|
||||||
[minLat, maxLon],
|
|
||||||
];
|
|
||||||
map.fitBounds(boundingBox);
|
|
||||||
} else {
|
|
||||||
map.setView([0, 0], 4);
|
|
||||||
}
|
}
|
||||||
|
list.set(item);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -213,16 +94,16 @@
|
|||||||
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
|
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<main
|
<main
|
||||||
class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-4 lg:gap-8 max-w-7xl mx-4 md:mx-auto"
|
class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 mx-4"
|
||||||
style="min-height: calc(100vh - 124px)"
|
style="height: calc(100vh - 124px)"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
class="list-list mx-2 md:mx-auto rounded-xl border border-input-border p-4 max-w-full"
|
class="list-list mx-4 md:mx-auto rounded-xl border border-input-border max-w-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
<div class="flex gap-x-4 items-center">
|
<div class="flex gap-x-4 items-center p-4 top-0 sticky bg-background z-50">
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center gap-4 hover:bg-menu-item-background-hover transition-colors rounded-xl p-4 cursor-pointer"
|
class="flex w-full items-center gap-4 hover:bg-menu-item-background-hover transition-colors rounded-xl p-4 cursor-pointer"
|
||||||
on:click={beforeListModalOpen}
|
on:click={() => goto("/lists/edit/new")}
|
||||||
>
|
>
|
||||||
<i class="fa fa-plus text-xl aspect-square"></i>
|
<i class="fa fa-plus text-xl aspect-square"></i>
|
||||||
<h5 id="create-list-button" class="text-xl font-semibold">
|
<h5 id="create-list-button" class="text-xl font-semibold">
|
||||||
@@ -235,7 +116,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-separator my-2" />
|
<hr class="border-separator mb-2" />
|
||||||
{#each $lists as item, i}
|
{#each $lists as item, i}
|
||||||
<li
|
<li
|
||||||
class="list-list-item"
|
class="list-list-item"
|
||||||
@@ -253,7 +134,10 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<div id="map" class="rounded-xl z-0" class:hidden={!showMap}></div>
|
<div class:hidden={!showMap}>
|
||||||
|
<MapWithElevationMultiple trails={$list.expand?.trails ?? []} bind:map
|
||||||
|
></MapWithElevationMultiple>
|
||||||
|
</div>
|
||||||
<div class="min-w-0" class:hidden={showMap}>
|
<div class="min-w-0" class:hidden={showMap}>
|
||||||
<TrailList
|
<TrailList
|
||||||
bind:filter
|
bind:filter
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
} from "$lib/components/base/search.svelte";
|
} from "$lib/components/base/search.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 Textarea from "$lib/components/base/textarea.svelte";
|
||||||
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
|
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte";
|
||||||
import TrailListItem from "$lib/components/trail/trail_list_item.svelte";
|
import type { Trail } from "$lib/models/trail.js";
|
||||||
import { trails_show } from "$lib/stores/trail_store";
|
import { trails_show } from "$lib/stores/trail_store";
|
||||||
import { getFileURL } from "$lib/util/file_util.js";
|
import { getFileURL } from "$lib/util/file_util.js";
|
||||||
import {
|
import {
|
||||||
@@ -19,18 +19,41 @@
|
|||||||
formatElevation,
|
formatElevation,
|
||||||
formatTimeHHMM,
|
formatTimeHHMM,
|
||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import type { Trail } from "$lib/models/trail.js";
|
import { lists_create, lists_update } from "$lib/stores/list_store.js";
|
||||||
|
import { show_toast } from "$lib/stores/toast_store.js";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
let previewURL = "";
|
let previewURL = "";
|
||||||
let searchDropdownItems: SearchItem[] = [];
|
let searchDropdownItems: SearchItem[] = [];
|
||||||
|
|
||||||
|
let activeTrailIndex: number | null = null;
|
||||||
|
|
||||||
|
let map: MapWithElevationMultiple;
|
||||||
|
|
||||||
|
let loading: boolean = false;
|
||||||
|
|
||||||
const { form, errors, handleChange, handleSubmit } = createForm<List>({
|
const { form, errors, handleChange, handleSubmit } = createForm<List>({
|
||||||
initialValues: data.list!,
|
initialValues: data.list!,
|
||||||
validationSchema: listSchema,
|
validationSchema: listSchema,
|
||||||
onSubmit: async (submittedList) => {
|
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) {
|
async function handleSearchClick(item: SearchItem) {
|
||||||
const trail = await trails_show(item.value, true);
|
const trail = await trails_show(item.value, true);
|
||||||
$form.trails?.push(trail);
|
$form.trails?.push(trail.id!);
|
||||||
$form.expand!.trails = [...$form.expand!.trails, trail];
|
$form.expand!.trails = [...$form.expand!.trails, trail];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +178,13 @@
|
|||||||
items={searchDropdownItems}
|
items={searchDropdownItems}
|
||||||
></Search>
|
></Search>
|
||||||
{#if $form.expand?.trails.length}
|
{#if $form.expand?.trails.length}
|
||||||
{#each $form.expand?.trails ?? [] as trail}
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
{#each $form.expand?.trails ?? [] as trail, i}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
|
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
|
||||||
|
class:border-primary={i == activeTrailIndex}
|
||||||
|
on:click={() => map.selectTrail(i)}
|
||||||
>
|
>
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<img
|
<img
|
||||||
@@ -213,9 +240,17 @@
|
|||||||
>No routes added</span
|
>No routes added</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<Button primary={true} large={true} type="submit" extraClasses="mb-2"
|
<Button
|
||||||
>{$_("save-list")}</Button
|
primary={true}
|
||||||
|
large={true}
|
||||||
|
type="submit"
|
||||||
|
extraClasses="mb-2"
|
||||||
|
{loading}>{$_("save-list")}</Button
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
<MapWithElevation trails={$form.expand?.trails ?? []}></MapWithElevation>
|
<MapWithElevationMultiple
|
||||||
|
trails={$form.expand?.trails ?? []}
|
||||||
|
bind:activeTrailIndex
|
||||||
|
bind:this={map}
|
||||||
|
></MapWithElevationMultiple>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -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 trails={[$trail]} bind:markers></MapWithElevation>
|
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -509,7 +509,7 @@
|
|||||||
>
|
>
|
||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<MapWithElevation
|
<MapWithElevation
|
||||||
trails={[$trail]}
|
trail={$trail}
|
||||||
options={{
|
options={{
|
||||||
theme: "gray-theme",
|
theme: "gray-theme",
|
||||||
slope: false,
|
slope: false,
|
||||||
|
|||||||
@@ -889,7 +889,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<MapWithElevation
|
<MapWithElevation
|
||||||
trails={[$form]}
|
trail={$form}
|
||||||
crosshair={drawingActive}
|
crosshair={drawingActive}
|
||||||
options={{
|
options={{
|
||||||
autofitBounds: !drawingActive,
|
autofitBounds: !drawingActive,
|
||||||
|
|||||||
Reference in New Issue
Block a user