Files
wanderer/web/src/lib/components/trail/map_with_elevation_maplibre.svelte
2024-11-28 22:01:34 +01:00

256 lines
7.4 KiB
Svelte

<script lang="ts">
import { page } from "$app/stores";
import type { Trail } from "$lib/models/trail";
import { theme } from "$lib/stores/theme_store";
import { findStartAndEndPoints } from "$lib/util/geojson_util";
import { toGeoJson } from "$lib/util/gpx_util";
import {
createMarkerFromWaypoint,
FontawesomeMarker,
} from "$lib/util/maplibre_util";
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
import type { GeoJSON } from "geojson";
import * as M from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { createEventDispatcher, onDestroy, onMount } from "svelte";
export let trail: Trail | null;
export let markers: M.Marker[] = [];
export let map: M.Map | null = null;
export let drawing: boolean = false;
let mapContainer: HTMLDivElement;
let epc: ElevationProfileControl;
let startMarker: M.Marker;
let endMarker: M.Marker;
const dispatch = createEventDispatcher();
$: data = trail?.expand.gpx_data
? (toGeoJson(trail.expand.gpx_data!) as GeoJSON)
: null;
$: if (data && map) {
initMap();
}
$: if ($theme == "dark") {
epc?.toggleTheme({
profileBackgroundColor: "#191b24",
elevationGridColor: "#ddd2",
labelColor: "#ddd8",
crosshairColor: "#fff5",
});
} else {
epc?.toggleTheme({
profileBackgroundColor: "#242734",
elevationGridColor: "#0002",
labelColor: "#0009",
crosshairColor: "#0005",
});
}
$: if (drawing && map) {
map.getCanvas().style.cursor = "crosshair";
} else if (!drawing && map) {
map.getCanvas().style.cursor = "inherit";
addStartEndMarkers();
}
function initMap() {
if (!map || !data) {
return;
}
epc.setData(data, trail!.expand.waypoints);
epc.showProfile();
const trailSource = map.getSource("trail-source");
if (!trailSource) {
addTrailLayer();
} else {
(trailSource as M.GeoJSONSource).setData(data);
}
if (!drawing) {
addStartEndMarkers();
}
}
function addTrailLayer() {
if (!data || !map) {
return;
}
const trailSource = map.getSource("trail-source");
if (!trailSource) {
try {
map.addSource("trail-source", {
type: "geojson",
data: data,
});
} catch (e) {
return;
}
}
const trailLayer = map.getLayer("trail-layer");
if (!trailLayer) {
map.addLayer({
id: "trail-layer",
type: "line",
source: "trail-source",
paint: {
"line-color": "#648ad5",
"line-width": 5,
},
});
}
}
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 () => {
const initialState = {
lng: 0,
lat: 0,
zoom: 1,
};
const ElevationProfileControl = (
await import(
"$lib/vendor/maplibre-elevation-profile/elevationprofile-control"
)
).ElevationProfileControl;
const mapStyles = [
{
text: "Carto Light",
value: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
thumbnail:
"https://basemaps.cartocdn.com/light_all/1/0/0@2x.png",
},
{
text: "Carto Dark",
value: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
thumbnail:
"https://basemaps.cartocdn.com/dark_all/1/0/0@2x.png",
},
];
const preferredMapStyleIndex = mapStyles.findIndex(
(s) => s.text === localStorage.getItem("layer"),
);
map = new M.Map({
container: mapContainer,
style:
mapStyles[preferredMapStyleIndex].value ?? mapStyles[0].value,
center: [initialState.lng, initialState.lat],
zoom: initialState.zoom,
});
const elevationMarker = new FontawesomeMarker(
{
icon: "fa-regular fa-circle",
fontSize: "xs",
width: 4,
backgroundColor: "primary",
fontColor: "white",
},
{},
);
elevationMarker.setLngLat([0, 0]).addTo(map);
elevationMarker.setOpacity("0");
epc = new ElevationProfileControl({
visible: false,
profileBackgroundColor: $theme == "light" ? "#242734" : "#191b24",
backgroundColor: "bg-menu-background/90",
unit: $page.data.settings?.unit ?? "metric",
profileLineWidth: 3,
displayDistanceGrid: true,
tooltipDisplayDPlus: false,
onEnter: () => {
elevationMarker.setOpacity("1");
},
onLeave: () => {
elevationMarker.setOpacity("0");
},
onMove: (data) => {
elevationMarker.setLngLat(data.position as M.LngLatLike);
},
});
const switcherControl = new StyleSwitcherControl({
styles: mapStyles,
onSwitch: (style) => {
map?.setStyle(style.value);
localStorage.setItem("layer", style.text);
},
selectedIndex:
preferredMapStyleIndex !== -1 ? preferredMapStyleIndex : 0,
});
map.addControl(new M.NavigationControl());
map.addControl(
new M.ScaleControl({
maxWidth: 120,
unit: $page.data.settings?.unit ?? "metric",
}),
"top-left",
);
map.addControl(epc);
map.addControl(switcherControl);
map.on("styledata", () => {
addTrailLayer();
});
map.on("click", (e) => {
dispatch("click", e);
});
for (const waypoint of trail?.expand.waypoints ?? []) {
const marker = createMarkerFromWaypoint(waypoint);
marker.addTo(map);
markers.push(marker);
}
});
onDestroy(() => {
map?.remove();
});
</script>
<div id="map" bind:this={mapContainer}></div>
<style>
#map {
width: 100%;
height: 100%;
}
</style>