adds maplibre for new routes

This commit is contained in:
Christian Beutel
2024-11-28 19:35:59 +01:00
parent b6b409e855
commit aea4e26084
7 changed files with 201 additions and 112 deletions

View File

@@ -9,21 +9,30 @@
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 type { GeoJsonObject } 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 { onDestroy, onMount } from "svelte"; import { createEventDispatcher, onDestroy, onMount } from "svelte";
export let trail: Trail; export let trail: Trail | null;
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 crosshairCursor: boolean = false; export let drawing: boolean = false;
let mapContainer: HTMLDivElement; let mapContainer: HTMLDivElement;
let epc: ElevationProfileControl; let epc: ElevationProfileControl;
let startMarker: M.Marker;
let endMarker: M.Marker;
$: data = toGeoJson(trail.expand.gpx_data!) as GeoJsonObject; const dispatch = createEventDispatcher();
$: data = trail?.expand.gpx_data
? (toGeoJson(trail.expand.gpx_data!) as GeoJSON)
: null;
$: if (data && map) {
initMap();
}
$: if ($theme == "dark") { $: if ($theme == "dark") {
epc?.toggleTheme({ epc?.toggleTheme({
@@ -41,11 +50,86 @@
}); });
} }
$: if (drawing && map) {
map.getCanvas().style.cursor = "crosshair";
} else if (!drawing && map) {
map.getCanvas().style.cursor = "inherit";
addStartEndMarkers();
}
function initMap() {
if (!map || !data) {
return;
}
for (const waypoint of trail?.expand.waypoints ?? []) {
const marker = createMarkerFromWaypoint(waypoint);
marker.addTo(map);
markers.push(marker);
}
epc.setData(data, trail!.expand.waypoints);
epc.showProfile();
const trailSource = map.getSource("trail-source");
if (!trailSource) {
map.on("load", () => {
map!.addSource("trail-source", {
type: "geojson",
data: data,
});
map!.addLayer({
id: "uploaded-polygons",
type: "line",
source: "trail-source",
paint: {
"line-color": "#648ad5",
"line-width": 5,
},
});
});
} else {
(trailSource as M.GeoJSONSource).setData(data);
}
if (!drawing) {
addStartEndMarkers();
}
}
function addStartEndMarkers() {
if (!map || !data) {
return;
}
const startEndPoint = findStartAndEndPoints(data);
startMarker ??= new FontawesomeMarker({ icon: "fa fa-bullseye" }, {});
startMarker.setLngLat(startEndPoint[0] as M.LngLatLike).addTo(map);
endMarker ??= new FontawesomeMarker(
{ icon: "fa fa-flag-checkered" },
{},
);
endMarker.setLngLat(startEndPoint[1] as M.LngLatLike).addTo(map);
map!.fitBounds(data.bbox as any, {
animate: false,
padding: {
top: 16,
left: 16,
right: 16,
bottom: map!.getContainer().clientHeight * 0.3 + 16,
},
});
}
onMount(async () => { onMount(async () => {
const initialState = { const initialState = {
lng: 0, lng: 0,
lat: 0, lat: 0,
zoom: 14, zoom: 1,
}; };
const ElevationProfileControl = ( const ElevationProfileControl = (
await import( await import(
@@ -60,28 +144,6 @@
zoom: initialState.zoom, zoom: initialState.zoom,
}); });
for (const waypoint of trail?.expand.waypoints ?? []) {
const marker = createMarkerFromWaypoint(waypoint);
marker.addTo(map);
markers.push(marker);
}
const startEndPoint = findStartAndEndPoints(data);
const startMarker = new FontawesomeMarker(
{ icon: "fa fa-bullseye" },
{},
)
.setLngLat(startEndPoint[0] as M.LngLatLike)
.addTo(map);
const endMarker = new FontawesomeMarker(
{ icon: "fa fa-flag-checkered" },
{},
)
.setLngLat(startEndPoint[1] as M.LngLatLike)
.addTo(map);
const elevationMarker = new FontawesomeMarker( const elevationMarker = new FontawesomeMarker(
{ {
icon: "fa-regular fa-circle", icon: "fa-regular fa-circle",
@@ -96,7 +158,7 @@
elevationMarker.setOpacity("0"); elevationMarker.setOpacity("0");
epc = new ElevationProfileControl({ epc = new ElevationProfileControl({
visible: true, visible: false,
profileBackgroundColor: $theme == "light" ? "#242734" : "#191b24", profileBackgroundColor: $theme == "light" ? "#242734" : "#191b24",
backgroundColor: "bg-menu-background/90", backgroundColor: "bg-menu-background/90",
unit: $page.data.settings?.unit ?? "metric", unit: $page.data.settings?.unit ?? "metric",
@@ -122,32 +184,9 @@
"top-left", "top-left",
); );
map.addControl(epc); map.addControl(epc);
epc.setData(data, trail.expand.waypoints);
map.on("load", () => { map.on("click", (e) => {
map!.addSource("trail-source", { dispatch("click", e);
type: "geojson",
data: data as any,
});
map!.addLayer({
id: "uploaded-polygons",
type: "line",
source: "trail-source",
paint: {
"line-color": "#648ad5",
"line-width": 5,
},
});
map!.fitBounds(data.bbox as any, {
animate: false,
padding: {
top: 16,
left: 16,
right: 16,
bottom: map!.getContainer().clientHeight * 0.3 + 16,
},
});
}); });
}); });
@@ -156,7 +195,7 @@
}); });
</script> </script>
<div id="map" class:cursor-pointer={crosshairCursor} bind:this={mapContainer}></div> <div id="map" bind:this={mapContainer}></div>
<style> <style>
#map { #map {

View File

@@ -1,4 +1,4 @@
import type { LatLng, Marker } from "leaflet"; import * as M from "maplibre-gl";
interface ValhallaRouteResponse { interface ValhallaRouteResponse {
trip: { trip: {
@@ -16,7 +16,7 @@ interface ValhallaAnchor {
id: string, id: string,
lat: number, lat: number,
lon: number, lon: number,
marker?: Marker marker?: M.Marker
} }
export { type ValhallaRouteResponse, type ValhallaHeightResponse, type ValhallaAnchor } export { type ValhallaAnchor, type ValhallaHeightResponse, type ValhallaRouteResponse };

View File

@@ -1,4 +1,5 @@
import type { Marker } from "leaflet"; import type { Marker } from "maplibre-gl";
import * as M from "maplibre-gl";
import { number, object, string } from "yup"; import { number, object, string } from "yup";
class Waypoint { class Waypoint {
@@ -8,13 +9,13 @@ class Waypoint {
lat: number; lat: number;
lon: number; lon: number;
icon?: string; icon?: string;
marker?: Marker; marker?: M.Marker;
photos: string[]; photos: string[];
_photos: File[]; _photos: File[];
author?: string; author?: string;
constructor(lat: number, lon: number, params?: { constructor(lat: number, lon: number, params?: {
id?: string, name?: string, description?: string, icon?: string, marker?: Marker, photos?: string[]; id?: string, name?: string, description?: string, icon?: string, marker?: M.Marker, photos?: string[];
}) { }) {
this.id = params?.id; this.id = params?.id;
this.name = params?.name ?? ""; this.name = params?.name ?? "";

View File

@@ -41,4 +41,72 @@ 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 {
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.textContent = "" + index
const marker = new M.Marker(
{
draggable: true,
element: anchorElement
}
);
marker.setLngLat([lon, lat]);
const deleteButton = document.createElement("button");
deleteButton.className = "fa fa-trash text-red-500 rounded-full aspect-square h-8 text-lg";
deleteButton.addEventListener("click", onDeleteClick)
const popup = new M.Popup({})
popup.setDOMContent(deleteButton)
marker.setPopup(popup);
marker.on("dragend", onDragEnd);
marker.getElement().addEventListener("click", (e) => {
e.stopPropagation();
marker.togglePopup();
})
return marker
}
// export function calculatePixelPerMeter(map: Map, meters: number) {
// const y = map.getSize().y;
// const x = map.getSize().x;
// const maxMeters = map.containerPointToLatLng([0, y]).distanceTo(map.containerPointToLatLng([x, y]));
// const pixelPerMeter = x / maxMeters;
// return pixelPerMeter * meters
// }
// export function calculateScaleFactor(map: Map) {
// function _pxTOmm() {
// let heightRef = document.createElement('div');
// heightRef.style.height = '1mm';
// heightRef.style.position = "absolute";
// heightRef.id = 'heightRef';
// document.body.appendChild(heightRef);
// const pxPermm = heightRef.getBoundingClientRect().height;
// document.body.removeChild(heightRef);
// return function pxTOmm(px: number) {
// return px / pxPermm;
// }
// }
// var centerOfMap = map.getSize().y / 2;
// var realWorlMetersPer100Pixels = map.distance(
// map.containerPointToLatLng([0, centerOfMap]),
// map.containerPointToLatLng([100, centerOfMap])
// );
// const screenMetersPer100Pixels = _pxTOmm()(100) / 1000;
// const scaleFactor = realWorlMetersPer100Pixels / screenMetersPer100Pixels
// return scaleFactor
// }

View File

@@ -347,15 +347,4 @@
.leaflet-popup-content { .leaflet-popup-content {
width: max-content !important; width: max-content !important;
max-width: 100%; max-width: 100%;
}
.leaflet-anchor {
background-color: #fff;
border: 2px solid black;
width: 24px;
height: 24px;
border-radius: 50%;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
text-align: center;
} }

View File

@@ -25,7 +25,10 @@ export function haversineCumulatedDistanceWgs84(path: Position[]): number[] {
export function smoothElevations(positions: Position[], windowSize: number): Position[] { export function smoothElevations(positions: Position[], windowSize: number): Position[] {
// Ensure windowSize is valid (at least 1) // Ensure windowSize is valid (at least 1)
if (windowSize < 1) throw new Error("Window size must be at least 1."); if (windowSize < 1) {
console.warn("Window size must be at least 1.");
return positions
};
// Create a new array with smoothed elevations // Create a new array with smoothed elevations
return positions.map((pos, i, arr) => { return positions.map((pos, i, arr) => {

View File

@@ -10,7 +10,7 @@
import ListSelectModal from "$lib/components/list/list_select_modal.svelte"; import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte"; import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
import SummitLogModal from "$lib/components/summit_log/summit_log_modal.svelte"; import SummitLogModal from "$lib/components/summit_log/summit_log_modal.svelte";
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte"; import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
import PhotoPicker from "$lib/components/trail/photo_picker.svelte"; import PhotoPicker from "$lib/components/trail/photo_picker.svelte";
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte"; import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte"; import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
@@ -58,13 +58,14 @@
gpx2trail, gpx2trail,
isFITFile, isFITFile,
} from "$lib/util/gpx_util"; } from "$lib/util/gpx_util";
import { import {
createAnchorMarker, createAnchorMarker,
createMarkerFromWaypoint, createMarkerFromWaypoint,
} from "$lib/util/leaflet_util"; } from "$lib/util/maplibre_util";
import { createForm } from "$lib/vendor/svelte-form-lib"; import { createForm } from "$lib/vendor/svelte-form-lib";
import cryptoRandomString from "crypto-random-string"; import cryptoRandomString from "crypto-random-string";
import type { DivIcon, LeafletMouseEvent, Map } from "leaflet"; import * as M from "maplibre-gl";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { backInOut } from "svelte/easing"; import { backInOut } from "svelte/easing";
@@ -73,8 +74,7 @@
export let data: { trail: Trail }; export let data: { trail: Trail };
let L: any; let map: M.Map;
let map: Map;
let openWaypointModal: () => void; let openWaypointModal: () => void;
let openSummitLogModal: () => void; let openSummitLogModal: () => void;
@@ -215,9 +215,6 @@
}); });
onMount(async () => { onMount(async () => {
L = (await import("leaflet")).default;
await import("leaflet.awesome-markers");
clearAnchorMarker(); clearAnchorMarker();
clearRoute(); clearRoute();
@@ -296,10 +293,12 @@
elevation_loss: $form.elevation_loss, elevation_loss: $form.elevation_loss,
duration: $form.duration ? $form.duration * 60 : undefined, duration: $form.duration ? $form.duration * 60 : undefined,
}); });
log.expand.gpx_data = gpxData; log.expand.gpx_data = gpxData;
const blob = new Blob([gpxData], { type: selectedFile.type }); const blob = new Blob([gpxData], { type: selectedFile.type });
log._gpx = new File([blob], selectedFile.name, { type: selectedFile.type }); log._gpx = new File([blob], selectedFile.name, {
type: selectedFile.type,
});
$form.expand.summit_logs.push(log); $form.expand.summit_logs.push(log);
@@ -370,7 +369,7 @@
} }
function openMarkerPopup(waypoint: Waypoint) { function openMarkerPopup(waypoint: Waypoint) {
waypoint.marker?.openPopup(); waypoint.marker?.togglePopup();
} }
function handleWaypointMenuClick( function handleWaypointMenuClick(
@@ -411,9 +410,9 @@
savedWaypoint.id = cryptoRandomString({ length: 15 }); savedWaypoint.id = cryptoRandomString({ length: 15 });
$form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint]; $form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint];
} }
const marker = createMarkerFromWaypoint(L, savedWaypoint, (event) => { const marker = createMarkerFromWaypoint(savedWaypoint, (event) => {
var marker = event.target; var marker = event;
var position = marker.getLatLng(); var position = marker.getLngLat();
const editableWaypointIndex = $form.expand.waypoints.findIndex( const editableWaypointIndex = $form.expand.waypoints.findIndex(
(w) => w.id == savedWaypoint.id, (w) => w.id == savedWaypoint.id,
); );
@@ -503,26 +502,26 @@
} }
} }
async function handleMapClick(e: LeafletMouseEvent) { async function handleMapClick(e: M.MapMouseEvent) {
if (!drawingActive) { if (!drawingActive) {
return; return;
} }
const anchorCount = anchors.length; const anchorCount = anchors.length;
if (anchorCount == 0) { if (anchorCount == 0) {
addAnchor(e.latlng.lat, e.latlng.lng); addAnchor(e.lngLat.lat, e.lngLat.lng);
} else { } else {
const previousAnchor = anchors[anchorCount - 1]; const previousAnchor = anchors[anchorCount - 1];
try { try {
const routeWaypoints = await calculateRouteBetween( const routeWaypoints = await calculateRouteBetween(
previousAnchor.lat, previousAnchor.lat,
previousAnchor.lon, previousAnchor.lon,
e.latlng.lat, e.lngLat.lat,
e.latlng.lng, e.lngLat.lng,
selectedModeOfTransport, selectedModeOfTransport,
autoRouting, autoRouting,
); );
appendToRoute(routeWaypoints); appendToRoute(routeWaypoints);
addAnchor(e.latlng.lat, e.latlng.lng); addAnchor(e.lngLat.lat, e.lngLat.lng);
updateTrailWithRouteData(); updateTrailWithRouteData();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -542,7 +541,6 @@
lon: lon, lon: lon,
}; };
const marker = createAnchorMarker( const marker = createAnchorMarker(
L,
lat, lat,
lon, lon,
anchors.length + 1, anchors.length + 1,
@@ -553,7 +551,7 @@
if (!drawingActive) { if (!drawingActive) {
return; return;
} }
const position = marker.getLatLng(); const position = marker.getLngLat();
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));
@@ -574,14 +572,11 @@
anchors.splice(anchorIndex, 1); anchors.splice(anchorIndex, 1);
for (let i = anchorIndex; i < anchors.length; i++) { for (let i = anchorIndex; i < anchors.length; i++) {
const anchor = anchors[i]; const anchor = anchors[i];
const markerIcon = anchor.marker?.getIcon() as DivIcon | undefined; const markerIcon = anchor.marker?.getElement();
if (markerIcon) { if (markerIcon) {
const markerText = const markerText = markerIcon.textContent ?? "0";
(markerIcon.options.html as HTMLSpanElement).textContent ??
"0";
const markerIndex = parseInt(markerText); const markerIndex = parseInt(markerText);
(markerIcon.options.html as HTMLSpanElement).textContent = markerIcon.textContent = markerIndex - 1 + "";
markerIndex - 1 + "";
} }
} }
if (anchorIndex == 0) { if (anchorIndex == 0) {
@@ -924,19 +919,13 @@
></Select> ></Select>
</div> </div>
{/if} {/if}
<MapWithElevation <MapWithElevationMaplibre
trail={$form} trail={$form}
crosshair={drawingActive} drawing={drawingActive}
options={{
autofitBounds: !drawingActive,
mapTooltip: !drawingActive,
speed: !drawingActive,
speedFactor: drawingActive ? 0 : 1,
showStartEnd: !drawingActive,
}}
bind:map bind:map
on:click={(e) => handleMapClick(e.detail)} on:click={(e) => handleMapClick(e.detail)}
></MapWithElevation> ></MapWithElevationMaplibre>
</div> </div>
</main> </main>
<WaypointModal <WaypointModal