map performance improvements

This commit is contained in:
Christian Beutel
2025-04-24 19:22:32 +02:00
parent 59051babed
commit e1bc3be698
4 changed files with 164 additions and 75 deletions

View File

@@ -5,6 +5,7 @@
import type { Trail } from "$lib/models/trail"; import type { Trail } from "$lib/models/trail";
import type { Waypoint } from "$lib/models/waypoint"; import type { Waypoint } from "$lib/models/waypoint";
import { theme } from "$lib/stores/theme_store"; import { theme } from "$lib/stores/theme_store";
import { fetchGPX } from "$lib/stores/trail_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";
import { import {
@@ -17,12 +18,8 @@
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control"; import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule"; 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 { import { T } from "@threlte/core";
Feature, import type { Feature, FeatureCollection, GeoJSON } from "geojson";
FeatureCollection,
GeoJSON,
Geometry,
} 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, untrack } from "svelte"; import { onDestroy, onMount, untrack } from "svelte";
@@ -46,7 +43,7 @@
elevationProfileContainer?: string | HTMLDivElement | undefined; elevationProfileContainer?: string | HTMLDivElement | undefined;
mapOptions?: Partial<M.MapOptions> | undefined; mapOptions?: Partial<M.MapOptions> | undefined;
activeTrail?: number | null; activeTrail?: number | null;
minZoom?: number; clusterTrails?: boolean;
onsegmentdragend?: (data: { onsegmentdragend?: (data: {
segment: number; segment: number;
event: M.MapMouseEvent; event: M.MapMouseEvent;
@@ -57,6 +54,10 @@
onmoveend?: (map: M.Map) => void; onmoveend?: (map: M.Map) => void;
onzoom?: (map: M.Map) => void; onzoom?: (map: M.Map) => void;
onclick?: (event: M.MapMouseEvent & Object) => void; onclick?: (event: M.MapMouseEvent & Object) => void;
onUnclusteredClick?: (
event: M.MapMouseEvent & Object,
trail: Trail,
) => void;
oninit?: (map: M.Map) => void; oninit?: (map: M.Map) => void;
} }
@@ -76,7 +77,7 @@
elevationProfileContainer = undefined, elevationProfileContainer = undefined,
mapOptions = undefined, mapOptions = undefined,
activeTrail = $bindable(0), activeTrail = $bindable(0),
minZoom = 0, clusterTrails = false,
onmarkerdragend, onmarkerdragend,
onsegmentdragend, onsegmentdragend,
onselect, onselect,
@@ -85,6 +86,7 @@
onmoveend, onmoveend,
onzoom, onzoom,
onclick, onclick,
onUnclusteredClick,
oninit, oninit,
}: Props = $props(); }: Props = $props();
@@ -99,6 +101,7 @@
endMarker: M.Marker | null; endMarker: M.Marker | null;
source: M.GeoJSONSource | null; source: M.GeoJSONSource | null;
layer: M.LineLayerSpecification | null; layer: M.LineLayerSpecification | null;
highlighted: boolean;
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;
@@ -130,7 +133,9 @@
"#DE7DC5", "#DE7DC5",
]; ];
let data = $derived(getData(trails)); let clusterPopup: M.Popup | null = null;
let [data, clusterData] = $derived(getData(trails));
$effect(() => { $effect(() => {
if (data && map) { if (data && map) {
untrack(() => initMap(map?.loaded() ?? false)); untrack(() => initMap(map?.loaded() ?? false));
@@ -182,27 +187,21 @@
}); });
}); });
function getData(trails: Trail[]) { function getData(trails: Trail[]): [GeoJSON[], FeatureCollection] {
if (!trails.length) { let cD: FeatureCollection = { type: "FeatureCollection", features: [] };
return []; let r: GeoJSON[] = [];
}
let r: GeoJSON[] =
(map?.getZoom() ?? 0) > minZoom
? []
: [{ type: "FeatureCollection", features: [] }];
trails.forEach((t) => { trails.forEach((t) => {
if ((map?.getZoom() ?? 0) > minZoom) { if (t.expand?.gpx_data) {
if (t.polyline) { r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
r.push(polylineToGeoJSON(t.polyline, 5)); }
} else if (t.expand?.gpx_data) { if (clusterTrails && t.lat !== null && t.lon !== null) {
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON); cD.features.push({
}
} else if (t.lat !== null && t.lon !== null) {
(r[0] as FeatureCollection).features.push({
id: t.id, id: t.id,
type: "Feature", type: "Feature",
properties: {}, properties: {
trail: t.id,
},
geometry: { geometry: {
type: "Point", type: "Point",
coordinates: [t.lon ?? 0, t.lat ?? 0], coordinates: [t.lon ?? 0, t.lat ?? 0],
@@ -211,7 +210,7 @@
} }
}); });
return r; return [r, cD];
} }
function initMap(mapLoaded: boolean) { function initMap(mapLoaded: boolean) {
@@ -226,13 +225,12 @@
epc?.hideProfile(); epc?.hideProfile();
} }
if ((map?.getZoom() ?? 0) > minZoom) { trails.forEach((t, i) => {
trails.forEach((t, i) => { const layerId = t.id!;
const layerId = t.id!; addTrailLayer(t, layerId, i, data[i]);
addTrailLayer(t, layerId, i, data[i]); });
}); if (clusterTrails) {
} else { addClusterLayer(clusterData);
addClusterLayer(data[0] as FeatureCollection);
} }
Object.keys(layers).forEach((layerId) => { Object.keys(layers).forEach((layerId) => {
@@ -279,7 +277,12 @@
maxY = Math.max(maxY, yMax); maxY = Math.max(maxY, yMax);
} }
if(minX < Infinity && minY < Infinity && maxX > -Infinity && maxY > -Infinity) { if (
minX < Infinity &&
minY < Infinity &&
maxX > -Infinity &&
maxY > -Infinity
) {
return new M.LngLatBounds([minX, minY, maxX, maxY]); return new M.LngLatBounds([minX, minY, maxX, maxY]);
} else { } else {
return new M.LngLatBounds([0, 0, 0, 0]); return new M.LngLatBounds([0, 0, 0, 0]);
@@ -342,6 +345,7 @@
endMarker: null, endMarker: null,
source: null, source: null,
layer: null, layer: null,
highlighted: false,
listener: { listener: {
onMouseUp: null, onMouseUp: null,
onMouseDown: null, onMouseDown: null,
@@ -384,7 +388,6 @@
id: id, id: id,
type: "line", type: "line",
source: id, source: id,
minzoom: minZoom,
paint: { paint: {
"line-color": trailColors[index % trailColors.length], "line-color": trailColors[index % trailColors.length],
"line-width": 5, "line-width": 5,
@@ -407,7 +410,7 @@
map.on("mousedown", id, layers[id].listener.onMouseDown); map.on("mousedown", id, layers[id].listener.onMouseDown);
} }
if (!drawing) { if (!drawing && !clusterTrails) {
addStartEndMarkers(trail, id, geojson); addStartEndMarkers(trail, id, geojson);
} }
} }
@@ -421,7 +424,6 @@
type: "geojson", type: "geojson",
data: geojson, data: geojson,
cluster: true, cluster: true,
clusterMaxZoom: minZoom - 1,
clusterRadius: 50, clusterRadius: 50,
}); });
} else { } else {
@@ -438,11 +440,17 @@
"circle-radius": [ "circle-radius": [
"step", "step",
["get", "point_count"], ["get", "point_count"],
10,
10,
15,
20, 20,
20,
50,
25,
100, 100,
30, 30,
750, 200,
40, 35,
], ],
"circle-stroke-width": 3, "circle-stroke-width": 3,
"circle-stroke-color": "#fff", "circle-stroke-color": "#fff",
@@ -461,7 +469,6 @@
"text-size": 12, "text-size": 12,
}, },
}); });
map.addLayer({ map.addLayer({
id: "unclustered-point", id: "unclustered-point",
type: "circle", type: "circle",
@@ -477,6 +484,34 @@
} }
} }
function addClusterHighlightLayer(geojson: GeoJSON) {
if (!geojson || !map || !map.style) {
return;
}
if (!map.getSource("cluster-highlight")) {
map.addSource("cluster-highlight", {
type: "geojson",
data: geojson,
});
} else {
(map.getSource("cluster-highlight") as M.GeoJSONSource).setData(
geojson,
);
}
if (!map.getLayer("cluster-highlight")) {
map.addLayer({
id: "cluster-highlight",
type: "line",
source: "cluster-highlight",
paint: {
"line-color": trailColors[0],
"line-width": 5,
},
});
}
}
function moveCrosshairToCursorPosition(e: M.MapMouseEvent) { function moveCrosshairToCursorPosition(e: M.MapMouseEvent) {
epc?.moveCrosshair(e.lngLat.lat, e.lngLat.lng); epc?.moveCrosshair(e.lngLat.lat, e.lngLat.lng);
moveElevationMarkerToCursorPosition(e); moveElevationMarkerToCursorPosition(e);
@@ -534,7 +569,7 @@
"interpolate", "interpolate",
["exponential", 1.5], ["exponential", 1.5],
["zoom"], ["zoom"],
minZoom, 0,
80, 80,
18, 18,
200, 200,
@@ -544,7 +579,7 @@
"interpolate", "interpolate",
["exponential", 1.5], ["exponential", 1.5],
["zoom"], ["zoom"],
minZoom, 0,
0.5, 0.5,
18, 18,
0.8, 0.8,
@@ -590,6 +625,34 @@
// map?.setPaintProperty(id, "line-color", "#648ad5"); // map?.setPaintProperty(id, "line-color", "#648ad5");
} }
export async function highlightCluster(trail: Trail) {
if (!map || !map.style) {
return;
}
clusterPopup = createPopupFromTrail(trail);
clusterPopup.setLngLat([trail.lon!, trail.lat!]).addTo(map);
const geojson = await fetchGPX(trail);
addClusterHighlightLayer(toGeoJson(geojson));
clusterPopup.on("close", () => {
unHighlightCluster(false);
});
}
export async function unHighlightCluster(closePopup: boolean = true) {
if (!map || !map.style) {
return;
}
if (map?.getLayer("cluster-highlight")) {
map?.removeLayer("cluster-highlight");
}
if (closePopup) {
clusterPopup?.remove();
}
}
function adjustTrailFocus(activeTrail: number | null) { function adjustTrailFocus(activeTrail: number | null) {
if (activeTrail !== null && trails[activeTrail] !== undefined) { if (activeTrail !== null && trails[activeTrail] !== undefined) {
if ( if (
@@ -658,7 +721,7 @@
} }
map.getCanvas().style.cursor = "inherit"; map.getCanvas().style.cursor = "inherit";
if (activeTrail !== null && trails[activeTrail]) { if (activeTrail !== null && trails[activeTrail] && !clusterTrails) {
addStartEndMarkers( addStartEndMarkers(
trails[activeTrail], trails[activeTrail],
trails[activeTrail].id, trails[activeTrail].id,
@@ -710,7 +773,7 @@
layers[id].endMarker.setLngLat( layers[id].endMarker.setLngLat(
startEndPoint[startEndPoint.length - 1] as M.LngLatLike, startEndPoint[startEndPoint.length - 1] as M.LngLatLike,
); );
if (map.getZoom() > minZoom) { if (!clusterTrails) {
layers[id].endMarker.addTo(map); layers[id].endMarker.addTo(map);
} }
@@ -727,16 +790,6 @@
layers[id].endMarker?.remove(); layers[id].endMarker?.remove();
} }
export function togglePopup(id: string, currentState?: boolean) {
if (
currentState !== undefined &&
layers[id]?.startMarker?.getPopup().isOpen() != currentState
) {
return;
}
layers[id]?.startMarker?.togglePopup();
}
function showWaypoints() { function showWaypoints() {
if (!map) { if (!map) {
return; return;
@@ -1014,17 +1067,6 @@
}); });
map.on("zoom", (e) => { map.on("zoom", (e) => {
const zoom = e.target.getZoom();
Object.values(layers).forEach((l) => {
if (zoom > minZoom && map && !drawing) {
l.startMarker?.addTo(map);
l.endMarker?.addTo(map);
} else {
l.startMarker?.remove();
l.endMarker?.remove();
}
});
onzoom?.(e.target); onzoom?.(e.target);
}); });
@@ -1035,6 +1077,51 @@
onclick?.(e); onclick?.(e);
}); });
map.on("click", "clusters", async (e) => {
if (!map) {
return;
}
const features = map.queryRenderedFeatures(e.point, {
layers: ["clusters"],
});
const clusterId = features[0].properties.cluster_id;
const zoom = await (
map.getSource("trails") as M.GeoJSONSource
).getClusterExpansionZoom(clusterId);
map.flyTo({
center: (features[0].geometry as any).coordinates,
zoom,
});
});
map.on(
"click",
"unclustered-point",
async (e: M.MapMouseEvent & Object) => {
const trail = trails.find(
(t) => t.id == (e as any).features[0].properties.trail,
);
if (!trail || !map) {
return;
}
highlightCluster(trail);
},
);
map.on("mouseenter", "clusters", () => {
map!.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "clusters", () => {
map!.getCanvas().style.cursor = "";
});
map.on("mouseenter", "unclustered-point", () => {
map!.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "unclustered-point", () => {
map!.getCanvas().style.cursor = "";
});
map.on("load", () => { map.on("load", () => {
initMap(true); initMap(true);
oninit?.(map!); oninit?.(map!);

View File

@@ -32,6 +32,7 @@ export type TrailSearchResult = {
location: string; location: string;
name: string; name: string;
public: boolean; public: boolean;
polyline?: string;
} }
export type ListSearchResult = { export type ListSearchResult = {

View File

@@ -66,7 +66,7 @@ export function splitMultiLineStringToLineStrings(geojson: GeoJsonObject): Featu
...feature.properties, ...feature.properties,
coordinateProperties: { coordinateProperties: {
...feature.properties.coordinateProperties, ...feature.properties.coordinateProperties,
times: feature.properties.coordinateProperties.times[lineIndex] times: feature.properties.coordinateProperties?.times?.[lineIndex]
}, },
featureId: features.length, featureId: features.length,
segmentId: lineIndex, segmentId: lineIndex,

View File

@@ -12,7 +12,7 @@
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte"; import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
import type { Settings } from "$lib/models/settings"; import type { Settings } from "$lib/models/settings";
import { import {
defaultTrailSearchAttributes, defaultTrailSearchAttributes,
type Trail, type Trail,
type TrailBoundingBox, type TrailBoundingBox,
type TrailFilter, type TrailFilter,
@@ -24,11 +24,12 @@
type LocationSearchResult, type LocationSearchResult,
type TrailSearchResult, type TrailSearchResult,
} from "$lib/stores/search_store"; } from "$lib/stores/search_store";
import { trails_search_bounding_box } from "$lib/stores/trail_store"; import {
trails_search_bounding_box
} from "$lib/stores/trail_store";
import { getIconForLocation } from "$lib/util/icon_util"; import { getIconForLocation } from "$lib/util/icon_util";
import type { Snapshot } from "@sveltejs/kit"; import type { Snapshot } from "@sveltejs/kit";
import * as M from "maplibre-gl"; import * as M from "maplibre-gl";
import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
@@ -45,7 +46,7 @@
const maxBoundingBox: TrailBoundingBox = page.data.boundingBox; const maxBoundingBox: TrailBoundingBox = page.data.boundingBox;
const settings: Settings = page.data.settings; const settings: Settings = page.data.settings;
const MIN_ZOOM = 6; const MIN_ZOOM = 100;
let loading: boolean = $state(true); let loading: boolean = $state(true);
let loadingNextPage: boolean = false; let loadingNextPage: boolean = false;
@@ -57,7 +58,7 @@
export const snapshot: Snapshot<TrailFilter> = { export const snapshot: Snapshot<TrailFilter> = {
capture: () => filter, capture: () => filter,
restore: (value) => { restore: (value) => {
filter = value; filter = value;
handleFilterUpdate(); handleFilterUpdate();
}, },
@@ -140,11 +141,11 @@
} }
function handleTrailCardMouseEnter(trail: Trail) { function handleTrailCardMouseEnter(trail: Trail) {
mapWithElevation?.togglePopup(trail.id!, false); mapWithElevation?.highlightCluster(trail);
} }
function handleTrailCardMouseLeave(trail: Trail) { function handleTrailCardMouseLeave(trail: Trail) {
mapWithElevation?.togglePopup(trail.id!, true); mapWithElevation?.unHighlightCluster();
} }
async function handleFilterUpdate() { async function handleFilterUpdate() {
@@ -356,7 +357,7 @@
showInfoPopup={true} showInfoPopup={true}
activeTrail={-1} activeTrail={-1}
fitBounds="off" fitBounds="off"
minZoom={MIN_ZOOM} clusterTrails={true}
bind:map bind:map
bind:this={mapWithElevation} bind:this={mapWithElevation}
></MapWithElevationMaplibre> ></MapWithElevationMaplibre>