adds route editing capabilities
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
|
import directionCaret from "$lib/assets/svgs/caret-right-solid.svg";
|
||||||
|
import type { Settings } from "$lib/models/settings";
|
||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
import { theme } from "$lib/stores/theme_store";
|
import { theme } from "$lib/stores/theme_store";
|
||||||
import { findStartAndEndPoints } from "$lib/util/geojson_util";
|
import { findStartAndEndPoints } from "$lib/util/geojson_util";
|
||||||
import { toGeoJson } from "$lib/util/gpx_util";
|
import { toGeoJson } from "$lib/util/gpx_util";
|
||||||
@@ -10,17 +13,16 @@
|
|||||||
FontawesomeMarker,
|
FontawesomeMarker,
|
||||||
} from "$lib/util/maplibre_util";
|
} from "$lib/util/maplibre_util";
|
||||||
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
||||||
|
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
||||||
|
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
||||||
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
||||||
import type { GeoJSON } from "geojson";
|
import type { GeoJSON } from "geojson";
|
||||||
import * as M from "maplibre-gl";
|
import * as M from "maplibre-gl";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
||||||
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
|
||||||
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
|
||||||
import type { Settings } from "$lib/models/settings";
|
|
||||||
import directionCaret from "$lib/assets/svgs/caret-right-solid.svg";
|
|
||||||
|
|
||||||
export let trails: Trail[] = [];
|
export let trails: Trail[] = [];
|
||||||
|
export let waypoints: Waypoint[] = [];
|
||||||
export let markers: M.Marker[] = [];
|
export let markers: M.Marker[] = [];
|
||||||
export let map: M.Map | null = null;
|
export let map: M.Map | null = null;
|
||||||
export let drawing: boolean = false;
|
export let drawing: boolean = false;
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
undefined;
|
undefined;
|
||||||
export let mapOptions: Partial<M.MapOptions> | undefined = undefined;
|
export let mapOptions: Partial<M.MapOptions> | undefined = undefined;
|
||||||
|
|
||||||
export let activeTrail: number = 0;
|
export let activeTrail: number | null = null;
|
||||||
export let minZoom: number = 0;
|
export let minZoom: number = 0;
|
||||||
|
|
||||||
let mapContainer: HTMLDivElement;
|
let mapContainer: HTMLDivElement;
|
||||||
@@ -56,11 +58,19 @@
|
|||||||
listener: {
|
listener: {
|
||||||
onEnter: ((e: M.MapMouseEvent) => void) | null;
|
onEnter: ((e: M.MapMouseEvent) => void) | null;
|
||||||
onLeave: ((e: M.MapMouseEvent) => void) | null;
|
onLeave: ((e: M.MapMouseEvent) => void) | null;
|
||||||
onClick: ((e: M.MapMouseEvent) => void) | null;
|
onMouseUp: ((e: M.MapMouseEvent) => void) | null;
|
||||||
|
onMouseDown: ((e: M.MapMouseEvent) => void) | null;
|
||||||
|
onMouseMove: ((e: M.MapMouseEvent) => void) | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
|
let elevationMarker: FontawesomeMarker;
|
||||||
|
|
||||||
|
let draggingSegment: number | null = null;
|
||||||
|
|
||||||
|
let hoveringTrail: boolean = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const trailColors = [
|
const trailColors = [
|
||||||
@@ -79,7 +89,19 @@
|
|||||||
$: data = getData(trails);
|
$: data = getData(trails);
|
||||||
|
|
||||||
$: if (data && map) {
|
$: if (data && map) {
|
||||||
initMap();
|
initMap(map.loaded());
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (activeTrail !== null && trails[activeTrail] !== undefined) {
|
||||||
|
if (
|
||||||
|
!drawing &&
|
||||||
|
fitBounds !== "off" &&
|
||||||
|
data.some((d) => d.bbox !== undefined)
|
||||||
|
) {
|
||||||
|
focusTrail(trails[activeTrail]);
|
||||||
|
}
|
||||||
|
} else if (activeTrail === null && trails.length) {
|
||||||
|
unFocusTrail();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($theme == "dark") {
|
$: if ($theme == "dark") {
|
||||||
@@ -99,14 +121,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: if (drawing && map) {
|
$: if (drawing && map) {
|
||||||
map.getCanvas().style.cursor = "crosshair";
|
startDrawing();
|
||||||
} else if (!drawing && map) {
|
} else if (!drawing && map) {
|
||||||
map.getCanvas().style.cursor = "inherit";
|
stopDrawing();
|
||||||
addStartEndMarkers(
|
|
||||||
trails[activeTrail],
|
|
||||||
trails[activeTrail]?.id ?? activeTrail.toString(),
|
|
||||||
data?.at(activeTrail),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (showGrid) {
|
$: if (showGrid) {
|
||||||
@@ -133,6 +150,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (waypoints) {
|
||||||
|
showWaypoints();
|
||||||
|
refreshElevationProfile();
|
||||||
|
}
|
||||||
|
|
||||||
function getData(trails: Trail[]) {
|
function getData(trails: Trail[]) {
|
||||||
if (!trails.length) {
|
if (!trails.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -157,27 +179,25 @@
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMap() {
|
function initMap(mapLoaded: boolean) {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data[activeTrail] && showElevation) {
|
refreshElevationProfile();
|
||||||
epc?.setData(
|
if (showElevation) {
|
||||||
data[activeTrail]!,
|
|
||||||
trails.at(activeTrail)!.expand?.waypoints,
|
|
||||||
);
|
|
||||||
epc?.showProfile();
|
epc?.showProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
trails.forEach((t, i) => {
|
trails.forEach((t, i) => {
|
||||||
const layerId = t.id ?? i.toString();
|
const layerId = t.id!;
|
||||||
addTrailLayer(t, layerId, i, data[i]);
|
addTrailLayer(t, layerId, i, data[i]);
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(layers).forEach((layerId) => {
|
Object.keys(layers).forEach((layerId) => {
|
||||||
const isStillVisible = trails.some((t) => t.id === layerId);
|
const isStillVisible = trails.some((t) => t.id === layerId);
|
||||||
if (!isStillVisible) {
|
if (!isStillVisible) {
|
||||||
|
removeCaretLayer();
|
||||||
removeTrailLayer(layerId);
|
removeTrailLayer(layerId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -186,8 +206,18 @@
|
|||||||
!drawing &&
|
!drawing &&
|
||||||
fitBounds !== "off" &&
|
fitBounds !== "off" &&
|
||||||
data.some((d) => d.bbox !== undefined)
|
data.some((d) => d.bbox !== undefined)
|
||||||
) {
|
) {
|
||||||
flyToBounds(fitBounds == "animate");
|
if (activeTrail !== null && mapLoaded) {
|
||||||
|
focusTrail(trails[activeTrail]);
|
||||||
|
} else {
|
||||||
|
flyToBounds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshElevationProfile() {
|
||||||
|
if (activeTrail !== null && data[activeTrail]) {
|
||||||
|
epc?.setData(data[activeTrail]!, waypoints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,17 +239,18 @@
|
|||||||
return new M.LngLatBounds([minX, minY, maxX, maxY]);
|
return new M.LngLatBounds([minX, minY, maxX, maxY]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function flyToBounds(animate: boolean = true) {
|
function flyToBounds() {
|
||||||
const bounds = data[activeTrail]
|
const bounds =
|
||||||
? (data[activeTrail].bbox as M.LngLatBoundsLike)
|
activeTrail !== null && data[activeTrail]
|
||||||
: getBounds();
|
? (data[activeTrail].bbox as M.LngLatBoundsLike)
|
||||||
|
: getBounds();
|
||||||
|
|
||||||
if (!bounds) {
|
if (!bounds || !map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
map!.fitBounds(bounds, {
|
map!.fitBounds(bounds, {
|
||||||
animate: animate,
|
animate: fitBounds == "animate",
|
||||||
padding: {
|
padding: {
|
||||||
top: 16,
|
top: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -250,7 +281,8 @@
|
|||||||
|
|
||||||
map?.off("mouseenter", id, layers[id].listener.onEnter!);
|
map?.off("mouseenter", id, layers[id].listener.onEnter!);
|
||||||
map?.off("mouseleave", id, layers[id].listener.onLeave!);
|
map?.off("mouseleave", id, layers[id].listener.onLeave!);
|
||||||
map?.off("click", id, layers[id].listener.onClick!);
|
map?.off("mouseup", id, layers[id].listener.onMouseUp!);
|
||||||
|
map?.off("mousemove", id, layers[id].listener.onMouseMove!);
|
||||||
|
|
||||||
delete layers[id];
|
delete layers[id];
|
||||||
}
|
}
|
||||||
@@ -263,9 +295,11 @@
|
|||||||
source: null,
|
source: null,
|
||||||
layer: null,
|
layer: null,
|
||||||
listener: {
|
listener: {
|
||||||
onClick: null,
|
onMouseUp: null,
|
||||||
|
onMouseDown: null,
|
||||||
onEnter: null,
|
onEnter: null,
|
||||||
onLeave: null,
|
onLeave: null,
|
||||||
|
onMouseMove: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -290,10 +324,6 @@
|
|||||||
data: geojson,
|
data: geojson,
|
||||||
});
|
});
|
||||||
layers[id].source = map.getSource(id) as M.GeoJSONSource;
|
layers[id].source = map.getSource(id) as M.GeoJSONSource;
|
||||||
// map.addSource("trail-source", {
|
|
||||||
// type: "vector",
|
|
||||||
// url: `http://localhost:8080/data/out.json`,
|
|
||||||
// });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -313,28 +343,21 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
layers[id].layer = map.getLayer(id) as M.LineLayerSpecification;
|
layers[id].layer = map.getLayer(id) as M.LineLayerSpecification;
|
||||||
layers[id].listener.onEnter = (e) => highlightTrail(id);
|
layers[id].listener.onEnter = (e) =>
|
||||||
|
highlightTrail(id, trails[activeTrail ?? -1]?.id == id);
|
||||||
layers[id].listener.onLeave = (e) => unHighlightTrail(id);
|
layers[id].listener.onLeave = (e) => unHighlightTrail(id);
|
||||||
layers[id].listener.onClick = (e) => focusTrail(trail, e);
|
layers[id].listener.onMouseUp = (e) => {
|
||||||
|
activeTrail = trails.indexOf(trail);
|
||||||
|
};
|
||||||
|
layers[id].listener.onMouseMove =
|
||||||
|
moveElevationMarkerToCursorPosition;
|
||||||
|
layers[id].listener.onMouseDown = (e) => handDragStart(e, id);
|
||||||
|
|
||||||
map.on("mouseenter", id, layers[id].listener.onEnter);
|
map.on("mouseenter", id, layers[id].listener.onEnter);
|
||||||
map.on("mouseleave", id, layers[id].listener.onLeave);
|
map.on("mouseleave", id, layers[id].listener.onLeave);
|
||||||
map.on("click", id, layers[id].listener.onClick);
|
map.on("mouseup", id, layers[id].listener.onMouseUp);
|
||||||
|
map.on("mousemove", id, layers[id].listener.onMouseMove);
|
||||||
// map.addLayer({
|
map.on("mousedown", id, layers[id].listener.onMouseDown);
|
||||||
// id: "trail-layer",
|
|
||||||
// type: "line",
|
|
||||||
// source: "trail-source",
|
|
||||||
// "source-layer": "Herzogstand", // Replace with the actual layer name in the .mbtiles file
|
|
||||||
// layout: {
|
|
||||||
// "line-join": "round",
|
|
||||||
// "line-cap": "round",
|
|
||||||
// },
|
|
||||||
// paint: {
|
|
||||||
// "line-color": "#648ad5",
|
|
||||||
// "line-width": 5,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!drawing) {
|
if (!drawing) {
|
||||||
@@ -342,10 +365,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveElevationMarkerToCursorPosition(e: M.MapMouseEvent) {
|
||||||
|
elevationMarker.setLngLat(e.lngLat);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handDragStart(e: M.MapMouseEvent, id: string) {
|
||||||
|
if (
|
||||||
|
!drawing ||
|
||||||
|
(e.originalEvent.target as HTMLElement | null)?.classList.contains(
|
||||||
|
"route-anchor",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const features = map?.queryRenderedFeatures(e.point, {
|
||||||
|
layers: [id],
|
||||||
|
});
|
||||||
|
const segmentId = features?.at(0)?.properties.segmentId;
|
||||||
|
if (segmentId !== null) {
|
||||||
|
draggingSegment = segmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
map?.on("mousemove", moveElevationMarkerToCursorPosition);
|
||||||
|
map?.once("mouseup", handleDragEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd(e: M.MapMouseEvent) {
|
||||||
|
map?.off("mousemove", moveElevationMarkerToCursorPosition);
|
||||||
|
dispatch("segmentDragEnd", { segment: draggingSegment, event: e });
|
||||||
|
draggingSegment = null;
|
||||||
|
}
|
||||||
|
|
||||||
function addCaretLayer(id?: string) {
|
function addCaretLayer(id?: string) {
|
||||||
if (!map || !id) {
|
if (!map || !id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (map.getLayer("direction-carets")) {
|
||||||
|
removeCaretLayer();
|
||||||
|
}
|
||||||
|
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: "direction-carets",
|
id: "direction-carets",
|
||||||
type: "symbol",
|
type: "symbol",
|
||||||
@@ -382,46 +442,63 @@
|
|||||||
map.removeLayer("direction-carets");
|
map.removeLayer("direction-carets");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightTrail(id: string) {
|
export function highlightTrail(
|
||||||
|
id: string,
|
||||||
|
showElevationMarker: boolean = false,
|
||||||
|
) {
|
||||||
|
if (showElevationMarker) {
|
||||||
|
elevationMarker.setOpacity("1");
|
||||||
|
}
|
||||||
map?.setPaintProperty(id, "line-width", 7);
|
map?.setPaintProperty(id, "line-width", 7);
|
||||||
|
hoveringTrail = true;
|
||||||
// map?.setPaintProperty(id, "line-color", "#2766e3");
|
// map?.setPaintProperty(id, "line-color", "#2766e3");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unHighlightTrail(id: string) {
|
export function unHighlightTrail(id: string | undefined) {
|
||||||
|
if (!id || draggingSegment !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elevationMarker.setOpacity("0");
|
||||||
|
hoveringTrail = false;
|
||||||
map?.setPaintProperty(id, "line-width", 5);
|
map?.setPaintProperty(id, "line-width", 5);
|
||||||
// map?.setPaintProperty(id, "line-color", "#648ad5");
|
// map?.setPaintProperty(id, "line-color", "#648ad5");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusTrail(trail: Trail, e?: M.MapMouseEvent) {
|
function focusTrail(trail: Trail) {
|
||||||
const currentlyFocussedTrail = trails[activeTrail];
|
|
||||||
if (currentlyFocussedTrail) {
|
activeTrail = trails.findIndex(t => t.id == trail.id);
|
||||||
unFocusTrail(currentlyFocussedTrail);
|
if (activeTrail < 0) {
|
||||||
}
|
activeTrail = null;
|
||||||
e?.preventDefault();
|
|
||||||
dispatch("select", trail);
|
|
||||||
const index = trails.findIndex((t) => t.id == trail.id);
|
|
||||||
if (index == -1) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeTrail = index;
|
|
||||||
|
const currentlyFocussedTrail = trails[activeTrail];
|
||||||
highlightTrail(trail.id!);
|
if (currentlyFocussedTrail && currentlyFocussedTrail != trail) {
|
||||||
if (data[activeTrail] && showElevation) {
|
unFocusTrail(currentlyFocussedTrail);
|
||||||
epc?.setData(
|
}
|
||||||
data[activeTrail]!,
|
dispatch("select", trail);
|
||||||
trails.at(activeTrail)!.expand?.waypoints,
|
|
||||||
);
|
try {
|
||||||
epc?.showProfile();
|
highlightTrail(trail.id!);
|
||||||
|
refreshElevationProfile();
|
||||||
|
if (showElevation) {
|
||||||
|
epc?.showProfile();
|
||||||
|
}
|
||||||
|
showWaypoints();
|
||||||
|
addCaretLayer(trail.id!);
|
||||||
|
flyToBounds();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
}
|
}
|
||||||
showWaypoints();
|
|
||||||
addCaretLayer(trail.id!);
|
|
||||||
flyToBounds();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unFocusTrail(trail: Trail) {
|
function unFocusTrail(trail?: Trail) {
|
||||||
dispatch("unselect", trail);
|
if (trail) {
|
||||||
activeTrail = -1;
|
dispatch("unselect", trail);
|
||||||
unHighlightTrail(trail.id!);
|
unHighlightTrail(trail.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTrail = null;
|
||||||
flyToBounds();
|
flyToBounds();
|
||||||
|
|
||||||
if (showElevation) {
|
if (showElevation) {
|
||||||
@@ -431,12 +508,38 @@
|
|||||||
removeCaretLayer();
|
removeCaretLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startDrawing() {
|
||||||
|
if (!map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeTrail ??= 0;
|
||||||
|
map.getCanvas().style.cursor = "crosshair";
|
||||||
|
if (trails[activeTrail]) {
|
||||||
|
removeStartEndMarkers(trails[activeTrail].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrawing() {
|
||||||
|
if (!map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.getCanvas().style.cursor = "inherit";
|
||||||
|
|
||||||
|
if (activeTrail !== null && trails[activeTrail]) {
|
||||||
|
addStartEndMarkers(
|
||||||
|
trails[activeTrail],
|
||||||
|
trails[activeTrail].id,
|
||||||
|
data?.at(activeTrail),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addStartEndMarkers(
|
function addStartEndMarkers(
|
||||||
trail: Trail,
|
trail: Trail,
|
||||||
id: string,
|
id: string | undefined,
|
||||||
geojson: GeoJSON | null | undefined,
|
geojson: GeoJSON | null | undefined,
|
||||||
) {
|
) {
|
||||||
if (!map || !trail) {
|
if (!map || !trail || !id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
createEmptyLayer(id);
|
createEmptyLayer(id);
|
||||||
@@ -470,12 +573,22 @@
|
|||||||
{ icon: "fa fa-flag-checkered" },
|
{ icon: "fa fa-flag-checkered" },
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
layers[id].endMarker.setLngLat(startEndPoint[1] as M.LngLatLike);
|
layers[id].endMarker.setLngLat(
|
||||||
|
startEndPoint[startEndPoint.length - 1] as M.LngLatLike,
|
||||||
|
);
|
||||||
if (map.getZoom() > minZoom) {
|
if (map.getZoom() > minZoom) {
|
||||||
layers[id].endMarker.addTo(map);
|
layers[id].endMarker.addTo(map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeStartEndMarkers(id: string | undefined) {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layers[id].startMarker?.remove();
|
||||||
|
layers[id].endMarker?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
export function togglePopup(id: string, currentState?: boolean) {
|
export function togglePopup(id: string, currentState?: boolean) {
|
||||||
if (
|
if (
|
||||||
currentState !== undefined &&
|
currentState !== undefined &&
|
||||||
@@ -490,7 +603,9 @@
|
|||||||
if (!map) {
|
if (!map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const waypoint of trails[activeTrail]?.expand?.waypoints ?? []) {
|
|
||||||
|
hideWaypoints();
|
||||||
|
for (const waypoint of waypoints) {
|
||||||
const marker = createMarkerFromWaypoint(waypoint, onMarkerDragEnd);
|
const marker = createMarkerFromWaypoint(waypoint, onMarkerDragEnd);
|
||||||
marker.addTo(map);
|
marker.addTo(map);
|
||||||
markers.push(marker);
|
markers.push(marker);
|
||||||
@@ -570,7 +685,7 @@
|
|||||||
};
|
};
|
||||||
map = new M.Map(finalMapOptions);
|
map = new M.Map(finalMapOptions);
|
||||||
|
|
||||||
const elevationMarker = new FontawesomeMarker(
|
elevationMarker = new FontawesomeMarker(
|
||||||
{
|
{
|
||||||
id: "elevation-marker",
|
id: "elevation-marker",
|
||||||
icon: "fa-regular fa-circle",
|
icon: "fa-regular fa-circle",
|
||||||
@@ -655,10 +770,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
map.on("styledata", () => {
|
map.on("styledata", () => {
|
||||||
trails.forEach((t, i) => {
|
|
||||||
addTrailLayer(t, t.id ?? i.toString(), i, data?.at(i));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (showTerrain) {
|
if (showTerrain) {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
@@ -695,7 +806,7 @@
|
|||||||
map.on("zoom", (e) => {
|
map.on("zoom", (e) => {
|
||||||
const zoom = e.target.getZoom();
|
const zoom = e.target.getZoom();
|
||||||
Object.values(layers).forEach((l) => {
|
Object.values(layers).forEach((l) => {
|
||||||
if (zoom > minZoom && map) {
|
if (zoom > minZoom && map && !drawing) {
|
||||||
l.endMarker?.addTo(map);
|
l.endMarker?.addTo(map);
|
||||||
} else {
|
} else {
|
||||||
l.endMarker?.remove();
|
l.endMarker?.remove();
|
||||||
@@ -706,11 +817,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
map.on("click", (e) => {
|
map.on("click", (e) => {
|
||||||
|
if (hoveringTrail && drawing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch("click", e);
|
dispatch("click", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on("load", () => {
|
map.on("load", () => {
|
||||||
addCaretLayer(trails[activeTrail]?.id);
|
initMap(true);
|
||||||
dispatch("init", map);
|
dispatch("init", map);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -429,6 +429,8 @@
|
|||||||
<div class="relative h-72 rounded-xl overflow-hidden">
|
<div class="relative h-72 rounded-xl overflow-hidden">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[trail]}
|
trails={[trail]}
|
||||||
|
activeTrail={0}
|
||||||
|
waypoints={trail.expand?.waypoints}
|
||||||
showElevation={false}
|
showElevation={false}
|
||||||
showStyleSwitcher={false}
|
showStyleSwitcher={false}
|
||||||
showFullscreen={true}
|
showFullscreen={true}
|
||||||
|
|||||||
@@ -68,13 +68,14 @@ export async function calculateRouteBetween(startLat: number, startLon: number,
|
|||||||
return waypoints
|
return waypoints
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function appendToRoute(waypoints: Waypoint[]) {
|
export async function insertIntoRoute(waypoints: Waypoint[], index?: number) {
|
||||||
const segment = new TrackSegment({ trkpt: [] })
|
const segment = new TrackSegment({ trkpt: waypoints })
|
||||||
|
|
||||||
for (const wpt of waypoints) {
|
if (index) {
|
||||||
segment.trkpt!.push(wpt)
|
route.trk?.at(0)?.trkseg?.splice(index, 0, segment);
|
||||||
|
} else {
|
||||||
|
route.trk?.at(0)?.trkseg?.push(segment);
|
||||||
}
|
}
|
||||||
route.trk?.at(0)?.trkseg?.push(segment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editRoute(index: number, waypoints: Waypoint[]) {
|
export async function editRoute(index: number, waypoints: Waypoint[]) {
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ export function bbox(
|
|||||||
result[3] = coord[1];
|
result[3] = coord[1];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findStartAndEndPoints(geojson: GeoJsonObject): Position[] {
|
export function findStartAndEndPoints(geojson: GeoJsonObject): Position[] {
|
||||||
const startEndPoints: Position[] = [];
|
const startEndPoints: Position[] = [];
|
||||||
|
|
||||||
// Check if it's a FeatureCollection
|
// Check if it's a FeatureCollection
|
||||||
if ((geojson as any).features) {
|
if ((geojson as any).features) {
|
||||||
(geojson as any).features.forEach((feature: any) => {
|
(geojson as any).features.forEach((feature: any) => {
|
||||||
@@ -50,6 +50,43 @@ export function findStartAndEndPoints(geojson: GeoJsonObject): Position[] {
|
|||||||
return startEndPoints;
|
return startEndPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function splitMultiLineStringToLineStrings(geojson: GeoJsonObject): FeatureCollection {
|
||||||
|
const features: Feature[] = [];
|
||||||
|
|
||||||
|
(geojson as any).features.forEach((feature: any) => {
|
||||||
|
if (feature.geometry.type === "MultiLineString") {
|
||||||
|
feature.geometry.coordinates.forEach((lineString: any, lineIndex: number) => {
|
||||||
|
features.push({
|
||||||
|
type: "Feature",
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: lineString,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
...feature.properties,
|
||||||
|
featureId: features.length,
|
||||||
|
segmentId: lineIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (feature.geometry.type === "LineString") {
|
||||||
|
features.push({
|
||||||
|
...feature,
|
||||||
|
properties: {
|
||||||
|
...feature.properties,
|
||||||
|
featureId: features.length,
|
||||||
|
segmentId: 0
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: features,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function extractStartAndEndPointsFromGeometry(geometry: any, startEndPoints: Position[]) {
|
function extractStartAndEndPointsFromGeometry(geometry: any, startEndPoints: Position[]) {
|
||||||
if (geometry.type === "LineString") {
|
if (geometry.type === "LineString") {
|
||||||
const coords = geometry.coordinates as number[][];
|
const coords = geometry.coordinates as number[][];
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import GPXWaypoint from "$lib/models/gpx/waypoint";
|
|||||||
import EasyFit from "$lib/vendor/easy-fit/easy-fit";
|
import EasyFit from "$lib/vendor/easy-fit/easy-fit";
|
||||||
import type { GeoJSON, Feature, FeatureCollection, GeoJsonProperties, Position } from 'geojson';
|
import type { GeoJSON, Feature, FeatureCollection, GeoJsonProperties, Position } from 'geojson';
|
||||||
import * as xmldom from 'xmldom';
|
import * as xmldom from 'xmldom';
|
||||||
import { bbox } from "./geojson_util";
|
import { bbox, splitMultiLineStringToLineStrings } from "./geojson_util";
|
||||||
|
|
||||||
|
|
||||||
export async function gpx2trail(gpxString: string, fallbackName?: string) {
|
export async function gpx2trail(gpxString: string, fallbackName?: string) {
|
||||||
@@ -130,7 +130,7 @@ export async function fromFile(file: File | Blob) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {gpxData, gpxFile};
|
return { gpxData, gpxFile };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromKML(kmlData: string) {
|
export function fromKML(kmlData: string) {
|
||||||
@@ -317,9 +317,10 @@ export function isFITFile(buffer: ArrayBuffer) {
|
|||||||
|
|
||||||
export function toGeoJson(gpxData: string) {
|
export function toGeoJson(gpxData: string) {
|
||||||
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
|
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
|
||||||
const geojson = gpx(
|
let geojson = gpx(
|
||||||
parser.parseFromString(gpxData, "text/xml"),
|
parser.parseFromString(gpxData, "text/xml"),
|
||||||
) as GeoJSON;
|
) as GeoJSON;
|
||||||
|
geojson = splitMultiLineStringToLineStrings(geojson);
|
||||||
geojson.bbox = bbox(geojson)
|
geojson.bbox = bbox(geojson)
|
||||||
return geojson
|
return geojson
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { Trail } from "$lib/models/trail";
|
|
||||||
import type { Waypoint } from "$lib/models/waypoint";
|
|
||||||
import M from "maplibre-gl";
|
|
||||||
import { getFileURL } from "./file_util";
|
|
||||||
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import { _ } from "svelte-i18n";
|
|
||||||
import { haversineDistance } from "$lib/models/gpx/utils";
|
|
||||||
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
|
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
|
||||||
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
|
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
|
||||||
|
import { haversineDistance } from "$lib/models/gpx/utils";
|
||||||
|
import type { Trail } from "$lib/models/trail";
|
||||||
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
import { theme } from "$lib/stores/theme_store";
|
import { theme } from "$lib/stores/theme_store";
|
||||||
|
import M from "maplibre-gl";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import { getFileURL } from "./file_util";
|
||||||
|
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
|
||||||
|
import { icons } from "./icon_util";
|
||||||
|
|
||||||
export class FontawesomeMarker extends M.Marker {
|
export class FontawesomeMarker extends M.Marker {
|
||||||
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
|
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
|
||||||
@@ -34,13 +35,32 @@ export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker
|
|||||||
color: "#6b7280"
|
color: "#6b7280"
|
||||||
|
|
||||||
})
|
})
|
||||||
const popup = new M.Popup({ offset: 25, closeButton: false }).setHTML(
|
|
||||||
"<b>" +
|
const content = document.createElement("div");
|
||||||
waypoint.name +
|
|
||||||
"</b>" +
|
const spanElement = document.createElement("span");
|
||||||
(waypoint.description && waypoint.description.length > 0
|
const iconElement = document.createElement("i");
|
||||||
? "<br>" + waypoint.description
|
const iconName = icons.includes(waypoint.icon ?? "") ? waypoint.icon : "circle";
|
||||||
: ""),
|
iconElement.classList.add("fa", `fa-${iconName}`)
|
||||||
|
spanElement.appendChild(iconElement);
|
||||||
|
|
||||||
|
const nameElement = document.createElement("b");
|
||||||
|
nameElement.textContent = waypoint.name ?? "-";
|
||||||
|
if(waypoint.name?.length) {
|
||||||
|
nameElement.classList.add("ml-2")
|
||||||
|
}
|
||||||
|
spanElement.appendChild(nameElement);
|
||||||
|
content.appendChild(spanElement);
|
||||||
|
|
||||||
|
if (waypoint.description && waypoint.description.length > 0) {
|
||||||
|
const descriptionElement = document.createElement("p");
|
||||||
|
descriptionElement.textContent = waypoint.description;
|
||||||
|
content.appendChild(descriptionElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const popup = new M.Popup({ offset: 25, closeButton: false }).setDOMContent(
|
||||||
|
content
|
||||||
);
|
);
|
||||||
marker
|
marker
|
||||||
.setLngLat([waypoint.lon, waypoint.lat])
|
.setLngLat([waypoint.lon, waypoint.lat])
|
||||||
@@ -53,10 +73,11 @@ export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker
|
|||||||
return marker;
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAnchorMarker(lat: number, lon: number, index: number, onDeleteClick: () => void, onDragEnd: (event: M.Marker) => void): FontawesomeMarker {
|
export function createAnchorMarker(lat: number, lon: number, index: number,
|
||||||
|
onDeleteClick: () => void, onDragStart: (event: Event) => void, onDragEnd: (event: Event) => void): FontawesomeMarker {
|
||||||
|
|
||||||
const anchorElement = document.createElement("span")
|
const anchorElement = document.createElement("span")
|
||||||
anchorElement.className = "cursor-pointer rounded-full w-6 h-6 border border-black text-center bg-background-inverse text-content-inverse"
|
anchorElement.className = "route-anchor cursor-pointer rounded-full w-6 h-6 border border-black text-center bg-background-inverse text-content-inverse"
|
||||||
anchorElement.textContent = "" + index
|
anchorElement.textContent = "" + index
|
||||||
const marker = new M.Marker(
|
const marker = new M.Marker(
|
||||||
{
|
{
|
||||||
@@ -69,12 +90,14 @@ export function createAnchorMarker(lat: number, lon: number, index: number, onDe
|
|||||||
const deleteButton = document.createElement("button");
|
const deleteButton = document.createElement("button");
|
||||||
deleteButton.className = "fa fa-trash text-red-500 rounded-full aspect-square h-8 text-lg";
|
deleteButton.className = "fa fa-trash text-red-500 rounded-full aspect-square h-8 text-lg";
|
||||||
deleteButton.addEventListener("click", onDeleteClick)
|
deleteButton.addEventListener("click", onDeleteClick)
|
||||||
const popup = new M.Popup({})
|
const popup = new M.Popup({ closeButton: false })
|
||||||
popup.setDOMContent(deleteButton)
|
popup.setDOMContent(deleteButton)
|
||||||
marker.setPopup(popup);
|
marker.setPopup(popup);
|
||||||
|
|
||||||
|
marker.on("dragstart", onDragStart);
|
||||||
marker.on("dragend", onDragEnd);
|
marker.on("dragend", onDragEnd);
|
||||||
marker.getElement().addEventListener("click", (e) => {
|
marker.getElement().addEventListener("click", (e) => {
|
||||||
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
marker.togglePopup();
|
marker.togglePopup();
|
||||||
})
|
})
|
||||||
@@ -86,8 +109,8 @@ export function createPopupFromTrail(trail: Trail) {
|
|||||||
const thumbnail = trail.photos.length
|
const thumbnail = trail.photos.length
|
||||||
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
|
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
|
||||||
: get(theme) === "light"
|
: get(theme) === "light"
|
||||||
? emptyStateTrailLight
|
? emptyStateTrailLight
|
||||||
: emptyStateTrailDark;
|
: emptyStateTrailDark;
|
||||||
const popup = new M.Popup({ maxWidth: "320px" });
|
const popup = new M.Popup({ maxWidth: "320px" });
|
||||||
popup.setHTML(
|
popup.setHTML(
|
||||||
`<a href="/map/trail/${trail.id}" data-sveltekit-preload-data="off">
|
`<a href="/map/trail/${trail.id}" data-sveltekit-preload-data="off">
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ export class ElevationProfile {
|
|||||||
wpDiv.style.top = `8px`; // Position horizontally
|
wpDiv.style.top = `8px`; // Position horizontally
|
||||||
|
|
||||||
// Add custom HTML content (e.g., icon + label)
|
// Add custom HTML content (e.g., icon + label)
|
||||||
wpDiv.innerHTML = `<div class="tooltip" data-title="${this.waypoints[index].name ?? "?"}"><i class="fa fa-${this.waypoints.at(index)?.icon ?? 'circle'}"></i></div>`;
|
wpDiv.innerHTML = `<div class="tooltip" data-title="${this.waypoints[index]?.name ?? "?"}"><i class="fa fa-${this.waypoints.at(index)?.icon ?? 'circle'}"></i></div>`;
|
||||||
|
|
||||||
waypointContainer.appendChild(wpDiv); // Add to container
|
waypointContainer.appendChild(wpDiv); // Add to container
|
||||||
});
|
});
|
||||||
@@ -864,7 +864,7 @@ export class ElevationProfile {
|
|||||||
async setData(data: GeoJsonObject, waypoints?: Waypoint[]) {
|
async setData(data: GeoJsonObject, waypoints?: Waypoint[]) {
|
||||||
// Concatenates the positions that may come from multiple LineStrings or MultiLineString
|
// Concatenates the positions that may come from multiple LineStrings or MultiLineString
|
||||||
const { positions, times } = geoJsonObjectToPositionsAndTimes(data);
|
const { positions, times } = geoJsonObjectToPositionsAndTimes(data);
|
||||||
|
|
||||||
this.times = times;
|
this.times = times;
|
||||||
|
|
||||||
this.elevatedPositions = smoothElevations(positions, Math.ceil(positions.length / 100));
|
this.elevatedPositions = smoothElevations(positions, Math.ceil(positions.length / 100));
|
||||||
@@ -896,7 +896,7 @@ export class ElevationProfile {
|
|||||||
this.cumulatedDPlus = [];
|
this.cumulatedDPlus = [];
|
||||||
this.grade = [];
|
this.grade = [];
|
||||||
this.waypoints = waypoints ?? [];
|
this.waypoints = waypoints ?? [];
|
||||||
// this.waypointPositions = [];
|
this.waypointPositions = [];
|
||||||
|
|
||||||
let cumulatedDPlus = 0;
|
let cumulatedDPlus = 0;
|
||||||
let cumulatedTime = 0;
|
let cumulatedTime = 0;
|
||||||
|
|||||||
@@ -53,8 +53,6 @@
|
|||||||
: null;
|
: null;
|
||||||
let selectedTrail: Trail | null = null;
|
let selectedTrail: Trail | null = null;
|
||||||
|
|
||||||
let activeTrailIndex: number = -1;
|
|
||||||
|
|
||||||
let loading: boolean = false;
|
let loading: boolean = false;
|
||||||
let loadingNextPage: boolean = false;
|
let loadingNextPage: boolean = false;
|
||||||
let filterExpanded: boolean = false;
|
let filterExpanded: boolean = false;
|
||||||
@@ -63,6 +61,12 @@
|
|||||||
|
|
||||||
let userQuery = "";
|
let userQuery = "";
|
||||||
|
|
||||||
|
$: selectedTrailIndex = selectedTrail
|
||||||
|
? (selectedList?.expand?.trails?.indexOf(selectedTrail) ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$: selectedTrailWaypoints = selectedTrail?.expand?.waypoints;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($page.url.searchParams.get("list") && selectedList) {
|
if ($page.url.searchParams.get("list") && selectedList) {
|
||||||
setCurrentList(selectedList);
|
setCurrentList(selectedList);
|
||||||
@@ -121,7 +125,6 @@
|
|||||||
loadAllListsOnNextBack = false;
|
loadAllListsOnNextBack = false;
|
||||||
}
|
}
|
||||||
if (selectedTrail) {
|
if (selectedTrail) {
|
||||||
mapWithElevation.unFocusTrail(selectedTrail);
|
|
||||||
selectedTrail = null;
|
selectedTrail = null;
|
||||||
} else if (selectedList) {
|
} else if (selectedList) {
|
||||||
selectedList = null;
|
selectedList = null;
|
||||||
@@ -135,7 +138,6 @@
|
|||||||
|
|
||||||
function selectTrail(trail: Trail) {
|
function selectTrail(trail: Trail) {
|
||||||
selectedTrail = trail;
|
selectedTrail = trail;
|
||||||
mapWithElevation.focusTrail(trail);
|
|
||||||
window.scrollTo({ top: 0 });
|
window.scrollTo({ top: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,14 +355,15 @@
|
|||||||
<div id="trail-map" class:hidden={!showMap}>
|
<div id="trail-map" class:hidden={!showMap}>
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={selectedList?.expand?.trails ?? []}
|
trails={selectedList?.expand?.trails ?? []}
|
||||||
|
waypoints={selectedTrailWaypoints}
|
||||||
bind:map
|
bind:map
|
||||||
bind:this={mapWithElevation}
|
bind:this={mapWithElevation}
|
||||||
bind:markers
|
bind:markers
|
||||||
|
activeTrail={selectedTrailIndex}
|
||||||
fitBounds="animate"
|
fitBounds="animate"
|
||||||
on:select={(e) => {
|
on:select={(e) => {
|
||||||
selectedTrail = e.detail;
|
selectedTrail = e.detail;
|
||||||
}}
|
}}
|
||||||
bind:activeTrail={activeTrailIndex}
|
|
||||||
showInfoPopup={true}
|
showInfoPopup={true}
|
||||||
showTerrain={true}
|
showTerrain={true}
|
||||||
></MapWithElevationMaplibre>
|
></MapWithElevationMaplibre>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<div id="trail-details" class="sticky top-[62px]">
|
<div id="trail-details" class="sticky top-[62px]">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[$trail]}
|
trails={[$trail]}
|
||||||
|
waypoints={$trail.expand?.waypoints}
|
||||||
|
activeTrail={0}
|
||||||
bind:markers
|
bind:markers
|
||||||
showTerrain={true}
|
showTerrain={true}
|
||||||
></MapWithElevationMaplibre>
|
></MapWithElevationMaplibre>
|
||||||
|
|||||||
@@ -516,6 +516,8 @@
|
|||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[$trail]}
|
trails={[$trail]}
|
||||||
|
waypoints={$trail.expand?.waypoints}
|
||||||
|
activeTrail={0}
|
||||||
on:zoom={(e) => updateScale(e.detail)}
|
on:zoom={(e) => updateScale(e.detail)}
|
||||||
bind:map
|
bind:map
|
||||||
{showGrid}
|
{showGrid}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
const file = files[i];
|
const file = files[i];
|
||||||
try {
|
try {
|
||||||
uploadProgress.set((progress += 100 / (files.length * 2)));
|
uploadProgress.set((progress += 100 / (files.length * 2)));
|
||||||
await trails_upload(file);
|
// await trails_upload(file);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorsThrown += 1;
|
errorsThrown += 1;
|
||||||
show_toast({
|
show_toast({
|
||||||
@@ -98,14 +98,7 @@
|
|||||||
uploading = false;
|
uploading = false;
|
||||||
jsConfetti.addConfetti({
|
jsConfetti.addConfetti({
|
||||||
confettiRadius: 4,
|
confettiRadius: 4,
|
||||||
confettiColors: [
|
emojis: ['🍃', '🍁'],
|
||||||
"#F4C842",
|
|
||||||
"#3C9D9B",
|
|
||||||
"#D1C4E9",
|
|
||||||
"#FF6F61",
|
|
||||||
"#A9D9C1",
|
|
||||||
"#F2F2F2",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,11 @@
|
|||||||
} from "$lib/stores/trail_store.js";
|
} from "$lib/stores/trail_store.js";
|
||||||
import {
|
import {
|
||||||
anchors,
|
anchors,
|
||||||
appendToRoute,
|
|
||||||
calculateRouteBetween,
|
calculateRouteBetween,
|
||||||
clearRoute,
|
clearRoute,
|
||||||
deleteFromRoute,
|
deleteFromRoute,
|
||||||
editRoute,
|
editRoute,
|
||||||
|
insertIntoRoute,
|
||||||
route,
|
route,
|
||||||
setRoute,
|
setRoute,
|
||||||
} from "$lib/stores/valhalla_store";
|
} from "$lib/stores/valhalla_store";
|
||||||
@@ -54,6 +54,9 @@
|
|||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import { fromFile, gpx2trail } from "$lib/util/gpx_util";
|
import { fromFile, gpx2trail } from "$lib/util/gpx_util";
|
||||||
|
|
||||||
|
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
|
||||||
|
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
|
||||||
|
import { theme } from "$lib/stores/theme_store.js";
|
||||||
import {
|
import {
|
||||||
createAnchorMarker,
|
createAnchorMarker,
|
||||||
createMarkerFromWaypoint,
|
createMarkerFromWaypoint,
|
||||||
@@ -67,9 +70,6 @@
|
|||||||
import { backInOut } from "svelte/easing";
|
import { backInOut } from "svelte/easing";
|
||||||
import { scale } from "svelte/transition";
|
import { scale } from "svelte/transition";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
|
|
||||||
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
|
|
||||||
import { theme } from "$lib/stores/theme_store.js";
|
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@
|
|||||||
|
|
||||||
let drawingActive = false;
|
let drawingActive = false;
|
||||||
let overwriteGPX = false;
|
let overwriteGPX = false;
|
||||||
|
let draggingMarker = false;
|
||||||
|
|
||||||
const ClientTrailCreateSchema = TrailCreateSchema.extend({
|
const ClientTrailCreateSchema = TrailCreateSchema.extend({
|
||||||
expand: z
|
expand: z
|
||||||
@@ -171,7 +172,7 @@
|
|||||||
?.trkpt?.at(0)?.$.lon;
|
?.trkpt?.at(0)?.$.lon;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.id) {
|
if ($page.params.id === "new") {
|
||||||
const createdTrail = await trails_create(
|
const createdTrail = await trails_create(
|
||||||
form as Trail,
|
form as Trail,
|
||||||
photoFiles,
|
photoFiles,
|
||||||
@@ -213,6 +214,19 @@
|
|||||||
if ($formData.expand!.gpx_data) {
|
if ($formData.expand!.gpx_data) {
|
||||||
const gpx = await GPX.parse($formData.expand!.gpx_data);
|
const gpx = await GPX.parse($formData.expand!.gpx_data);
|
||||||
if (!(gpx instanceof Error)) {
|
if (!(gpx instanceof Error)) {
|
||||||
|
if (gpx.rte && !gpx.trk) {
|
||||||
|
gpx.trk = [
|
||||||
|
{
|
||||||
|
trkseg: [
|
||||||
|
{
|
||||||
|
trkpt: gpx.rte?.at(0)?.rtept,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
gpx.rte = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
setRoute(gpx);
|
setRoute(gpx);
|
||||||
initRouteAnchors(gpx);
|
initRouteAnchors(gpx);
|
||||||
}
|
}
|
||||||
@@ -245,7 +259,7 @@
|
|||||||
const prevId = $formData.id;
|
const prevId = $formData.id;
|
||||||
const parseResult = await gpx2trail(gpxData, selectedFile.name);
|
const parseResult = await gpx2trail(gpxData, selectedFile.name);
|
||||||
setFields(parseResult.trail);
|
setFields(parseResult.trail);
|
||||||
$formData.id = prevId;
|
$formData.id = prevId ?? cryptoRandomString({ length: 15 });
|
||||||
$formData.expand!.gpx_data = gpxData;
|
$formData.expand!.gpx_data = gpxData;
|
||||||
setFields(
|
setFields(
|
||||||
"category",
|
"category",
|
||||||
@@ -273,12 +287,20 @@
|
|||||||
|
|
||||||
$formData.expand!.summit_logs.push(log);
|
$formData.expand!.summit_logs.push(log);
|
||||||
|
|
||||||
|
if (parseResult.gpx.rte?.length && !parseResult.gpx.trk) {
|
||||||
|
parseResult.gpx.trk = [
|
||||||
|
{
|
||||||
|
trkseg: [
|
||||||
|
{
|
||||||
|
trkpt: parseResult.gpx.rte?.at(0)?.rtept,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
parseResult.gpx.rte = undefined;
|
||||||
|
}
|
||||||
setRoute(parseResult.gpx);
|
setRoute(parseResult.gpx);
|
||||||
initRouteAnchors(parseResult.gpx);
|
initRouteAnchors(parseResult.gpx);
|
||||||
|
|
||||||
for (const waypoint of $formData.expand!.waypoints) {
|
|
||||||
saveWaypoint(waypoint);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
@@ -329,12 +351,18 @@
|
|||||||
const points = segment.trkpt ?? [];
|
const points = segment.trkpt ?? [];
|
||||||
|
|
||||||
if (points.length > 0) {
|
if (points.length > 0) {
|
||||||
addAnchor(points[0].$.lat!, points[0].$.lon!, false);
|
addAnchor(
|
||||||
|
points[0].$.lat!,
|
||||||
|
points[0].$.lon!,
|
||||||
|
anchors.length,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (i == segments.length - 1) {
|
if (i == segments.length - 1) {
|
||||||
addAnchor(
|
addAnchor(
|
||||||
points[points.length - 1].$.lat!,
|
points[points.length - 1].$.lat!,
|
||||||
points[points.length - 1].$.lon!,
|
points[points.length - 1].$.lon!,
|
||||||
|
anchors.length,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -369,6 +397,7 @@
|
|||||||
$formData.expand!.waypoints.splice(index, 1);
|
$formData.expand!.waypoints.splice(index, 1);
|
||||||
$formData.waypoints.splice(index, 1);
|
$formData.waypoints.splice(index, 1);
|
||||||
$formData.expand!.waypoints = $formData.expand!.waypoints;
|
$formData.expand!.waypoints = $formData.expand!.waypoints;
|
||||||
|
// updateTrailOnMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveWaypoint(savedWaypoint: Waypoint) {
|
function saveWaypoint(savedWaypoint: Waypoint) {
|
||||||
@@ -377,7 +406,6 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (editedWaypointIndex >= 0) {
|
if (editedWaypointIndex >= 0) {
|
||||||
$formData.expand!.waypoints[editedWaypointIndex].marker?.remove();
|
|
||||||
$formData.expand!.waypoints[editedWaypointIndex] = savedWaypoint;
|
$formData.expand!.waypoints[editedWaypointIndex] = savedWaypoint;
|
||||||
} else {
|
} else {
|
||||||
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
||||||
@@ -385,11 +413,9 @@
|
|||||||
...$formData.expand!.waypoints,
|
...$formData.expand!.waypoints,
|
||||||
savedWaypoint,
|
savedWaypoint,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
const marker = createMarkerFromWaypoint(savedWaypoint, moveMarker);
|
|
||||||
|
|
||||||
marker.addTo(map);
|
// updateTrailOnMap();
|
||||||
savedWaypoint.marker = marker;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveMarker(marker: M.Marker, wpId?: string) {
|
function moveMarker(marker: M.Marker, wpId?: string) {
|
||||||
@@ -405,6 +431,7 @@
|
|||||||
editableWaypoint.lat = position.lat;
|
editableWaypoint.lat = position.lat;
|
||||||
editableWaypoint.lon = position.lng;
|
editableWaypoint.lon = position.lng;
|
||||||
$formData.expand!.waypoints = [...$formData.expand!.waypoints];
|
$formData.expand!.waypoints = [...$formData.expand!.waypoints];
|
||||||
|
// updateTrailOnMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeSummitLogModalOpen() {
|
function beforeSummitLogModalOpen() {
|
||||||
@@ -493,7 +520,7 @@
|
|||||||
}
|
}
|
||||||
const anchorCount = anchors.length;
|
const anchorCount = anchors.length;
|
||||||
if (anchorCount == 0) {
|
if (anchorCount == 0) {
|
||||||
addAnchor(e.lngLat.lat, e.lngLat.lng);
|
addAnchor(e.lngLat.lat, e.lngLat.lng, anchors.length);
|
||||||
} else {
|
} else {
|
||||||
const previousAnchor = anchors[anchorCount - 1];
|
const previousAnchor = anchors[anchorCount - 1];
|
||||||
try {
|
try {
|
||||||
@@ -505,8 +532,8 @@
|
|||||||
selectedModeOfTransport,
|
selectedModeOfTransport,
|
||||||
autoRouting,
|
autoRouting,
|
||||||
);
|
);
|
||||||
appendToRoute(routeWaypoints);
|
insertIntoRoute(routeWaypoints);
|
||||||
addAnchor(e.lngLat.lat, e.lngLat.lng);
|
addAnchor(e.lngLat.lat, e.lngLat.lng, anchors.length);
|
||||||
updateTrailWithRouteData();
|
updateTrailWithRouteData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -519,7 +546,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAnchor(lat: number, lon: number, addtoMap: boolean = true) {
|
function addAnchor(
|
||||||
|
lat: number,
|
||||||
|
lon: number,
|
||||||
|
index: number,
|
||||||
|
addtoMap: boolean = true,
|
||||||
|
) {
|
||||||
const anchor: ValhallaAnchor = {
|
const anchor: ValhallaAnchor = {
|
||||||
id: cryptoRandomString({ length: 15 }),
|
id: cryptoRandomString({ length: 15 }),
|
||||||
lat: lat,
|
lat: lat,
|
||||||
@@ -528,10 +560,13 @@
|
|||||||
const marker = createAnchorMarker(
|
const marker = createAnchorMarker(
|
||||||
lat,
|
lat,
|
||||||
lon,
|
lon,
|
||||||
anchors.length + 1,
|
index + 1,
|
||||||
() => {
|
() => {
|
||||||
removeAnchor(anchors.findIndex((a) => a.id == anchor.id));
|
removeAnchor(anchors.findIndex((a) => a.id == anchor.id));
|
||||||
},
|
},
|
||||||
|
(e) => {
|
||||||
|
draggingMarker = true;
|
||||||
|
},
|
||||||
(_) => {
|
(_) => {
|
||||||
if (!drawingActive) {
|
if (!drawingActive) {
|
||||||
return;
|
return;
|
||||||
@@ -540,13 +575,16 @@
|
|||||||
anchor.lat = position.lat;
|
anchor.lat = position.lat;
|
||||||
anchor.lon = position.lng;
|
anchor.lon = position.lng;
|
||||||
recalculateRoute(anchors.findIndex((a) => a.id == anchor.id));
|
recalculateRoute(anchors.findIndex((a) => a.id == anchor.id));
|
||||||
|
draggingMarker = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (addtoMap) {
|
if (addtoMap) {
|
||||||
marker.addTo(map);
|
marker.addTo(map);
|
||||||
}
|
}
|
||||||
anchor.marker = marker;
|
anchor.marker = marker;
|
||||||
anchors.push(anchor);
|
anchors.splice(index, 0, anchor);
|
||||||
|
|
||||||
|
return anchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAnchor(anchorIndex: number) {
|
function removeAnchor(anchorIndex: number) {
|
||||||
@@ -583,21 +621,75 @@
|
|||||||
}
|
}
|
||||||
let nextRouteSegment;
|
let nextRouteSegment;
|
||||||
let previousRouteSegment;
|
let previousRouteSegment;
|
||||||
if (anchorIndex < anchors.length - 1) {
|
try {
|
||||||
const nextAnchor = anchors[anchorIndex + 1];
|
if (anchorIndex < anchors.length - 1) {
|
||||||
|
const nextAnchor = anchors[anchorIndex + 1];
|
||||||
|
|
||||||
nextRouteSegment = await calculateRouteBetween(
|
nextRouteSegment = await calculateRouteBetween(
|
||||||
anchor.lat,
|
anchor.lat,
|
||||||
anchor.lon,
|
anchor.lon,
|
||||||
nextAnchor.lat,
|
nextAnchor.lat,
|
||||||
nextAnchor.lon,
|
nextAnchor.lon,
|
||||||
selectedModeOfTransport,
|
selectedModeOfTransport,
|
||||||
autoRouting,
|
autoRouting,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
if (anchorIndex > 0) {
|
||||||
|
const previousAnchor = anchors[anchorIndex - 1];
|
||||||
|
previousRouteSegment = await calculateRouteBetween(
|
||||||
|
previousAnchor.lat,
|
||||||
|
previousAnchor.lon,
|
||||||
|
anchor.lat,
|
||||||
|
anchor.lon,
|
||||||
|
selectedModeOfTransport,
|
||||||
|
autoRouting,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextRouteSegment) {
|
||||||
|
editRoute(anchorIndex, nextRouteSegment);
|
||||||
|
}
|
||||||
|
if (previousRouteSegment) {
|
||||||
|
editRoute(anchorIndex - 1, previousRouteSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTrailWithRouteData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
show_toast({
|
||||||
|
text: "Error calculating route",
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (anchorIndex > 0) {
|
}
|
||||||
const previousAnchor = anchors[anchorIndex - 1];
|
|
||||||
previousRouteSegment = await calculateRouteBetween(
|
async function handleSegmentDragEnd(data: {
|
||||||
|
segment: number;
|
||||||
|
event: M.MapMouseEvent;
|
||||||
|
}) {
|
||||||
|
if (draggingMarker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const anchor = addAnchor(
|
||||||
|
data.event.lngLat.lat,
|
||||||
|
data.event.lngLat.lng,
|
||||||
|
data.segment + 1,
|
||||||
|
);
|
||||||
|
for (let i = data.segment + 2; i < anchors.length; i++) {
|
||||||
|
const anchor = anchors[i];
|
||||||
|
const markerIcon = anchor.marker?.getElement();
|
||||||
|
if (markerIcon) {
|
||||||
|
const markerText = markerIcon.textContent ?? "0";
|
||||||
|
const markerIndex = parseInt(markerText);
|
||||||
|
markerIcon.textContent = markerIndex + 1 + "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const previousAnchor = anchors[data.segment];
|
||||||
|
const nextAnchor = anchors[data.segment + 2];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const previousRouteSegment = await calculateRouteBetween(
|
||||||
previousAnchor.lat,
|
previousAnchor.lat,
|
||||||
previousAnchor.lon,
|
previousAnchor.lon,
|
||||||
anchor.lat,
|
anchor.lat,
|
||||||
@@ -605,14 +697,26 @@
|
|||||||
selectedModeOfTransport,
|
selectedModeOfTransport,
|
||||||
autoRouting,
|
autoRouting,
|
||||||
);
|
);
|
||||||
|
const nextRouteSegment = await calculateRouteBetween(
|
||||||
|
anchor.lat,
|
||||||
|
anchor.lon,
|
||||||
|
nextAnchor.lat,
|
||||||
|
nextAnchor.lon,
|
||||||
|
selectedModeOfTransport,
|
||||||
|
autoRouting,
|
||||||
|
);
|
||||||
|
|
||||||
|
editRoute(data.segment, previousRouteSegment);
|
||||||
|
insertIntoRoute(nextRouteSegment, data.segment + 1);
|
||||||
|
updateTrailWithRouteData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
show_toast({
|
||||||
|
text: "Error calculating route",
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (nextRouteSegment) {
|
|
||||||
editRoute(anchorIndex, nextRouteSegment);
|
|
||||||
}
|
|
||||||
if (previousRouteSegment) {
|
|
||||||
editRoute(anchorIndex - 1, previousRouteSegment);
|
|
||||||
}
|
|
||||||
updateTrailWithRouteData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTrailWithRouteData() {
|
function updateTrailWithRouteData() {
|
||||||
@@ -623,10 +727,13 @@
|
|||||||
$formData.elevation_gain = totals.elevationGain;
|
$formData.elevation_gain = totals.elevationGain;
|
||||||
$formData.elevation_loss = totals.elevationLoss;
|
$formData.elevation_loss = totals.elevationLoss;
|
||||||
$formData.expand!.gpx_data = route.toString();
|
$formData.expand!.gpx_data = route.toString();
|
||||||
|
if (!$formData.id) {
|
||||||
|
$formData.id = cryptoRandomString({ length: 15 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTrailOnMap() {
|
function updateTrailOnMap() {
|
||||||
mapTrail = [{ ...($formData as Trail) }];
|
mapTrail = [$formData as Trail];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -902,11 +1009,14 @@
|
|||||||
<div id="trail-map">
|
<div id="trail-map">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={mapTrail}
|
trails={mapTrail}
|
||||||
|
waypoints={$formData.expand?.waypoints}
|
||||||
drawing={drawingActive}
|
drawing={drawingActive}
|
||||||
showTerrain={true}
|
showTerrain={true}
|
||||||
onMarkerDragEnd={moveMarker}
|
onMarkerDragEnd={moveMarker}
|
||||||
|
activeTrail={0}
|
||||||
bind:map
|
bind:map
|
||||||
on:click={(e) => handleMapClick(e.detail)}
|
on:click={(e) => handleMapClick(e.detail)}
|
||||||
|
on:segmentDragEnd={(e) => handleSegmentDragEnd(e.detail)}
|
||||||
></MapWithElevationMaplibre>
|
></MapWithElevationMaplibre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user