adds trail preview on high map zoom

This commit is contained in:
Christian Beutel
2025-08-22 15:41:34 +02:00
parent 33776ad42c
commit 380c1802f2
7 changed files with 164 additions and 67 deletions

View File

@@ -42,10 +42,10 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
category = trailCategory.GetString("name")
}
// polyline, err := getPolyline(app, r)
// if err != nil {
// polyline = ""
// }
polyline, err := getPolyline(app, r)
if err != nil {
polyline = ""
}
domain := ""
if !author.GetBool("isLocal") {
@@ -78,9 +78,9 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
"thumbnail": thumbnail,
"gpx": r.GetString("gpx"),
"tags": tags,
// "polyline": polyline,
"domain": domain,
"iri": r.GetString("iri"),
"polyline": polyline,
"domain": domain,
"iri": r.GetString("iri"),
"_geo": map[string]float64{
"lat": r.GetFloat("lat"),
"lng": r.GetFloat("lon"),

View File

@@ -12,6 +12,7 @@
createPopupFromTrail,
FontawesomeMarker,
} from "$lib/util/maplibre_util";
import { decodePolyline } from "$lib/util/polyline_util";
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";
@@ -19,6 +20,7 @@
import { ClusterLayer } from "$lib/vendor/maplibre-layer-manager/cluster-layer";
import { baseMapStyles } from "$lib/vendor/maplibre-layer-manager/layers";
import { LayerManager } from "$lib/vendor/maplibre-layer-manager/maplibre-layer-manager";
import { PreviewLayer } from "$lib/vendor/maplibre-layer-manager/preview-layer";
import { TerrainLayer } from "$lib/vendor/maplibre-layer-manager/terrain-layer";
import { TrailLayer } from "$lib/vendor/maplibre-layer-manager/trail-layer";
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
@@ -114,21 +116,21 @@
let mapLoaded: boolean = false;
const trailColors = [
"#3549BB",
"#592E9E",
"#47A2CD",
"#62D5BC",
"#7DDE95",
"#759E2E",
"#BBA535",
"#CD6F47",
"#D5627B",
"#DE7DC5",
"#3549bb", // blue
"#ff7f0e", // orange
"#2ca02c", // green
"#d62728", // red
"#9467bd", // purple
"#8c564b", // brown
"#e377c2", // pink
"#373642", // gray
"#fae455", // yellow
"#17becf", // teal
];
let clusterPopup: M.Popup | null = null;
let [data, clusterData] = $derived(getData(trails));
let [data, clusterData, previewData] = $derived(getData(trails));
$effect(() => {
if (data && map && mapLoaded) {
untrack(() => initMap(map?.loaded() ?? false));
@@ -182,35 +184,61 @@
function getData(
trails: Trail[],
): [FeatureCollection[], FeatureCollection] {
): [FeatureCollection[], FeatureCollection, FeatureCollection] {
let clusterData: FeatureCollection = {
type: "FeatureCollection",
features: [],
};
let previewData: FeatureCollection = {
type: "FeatureCollection",
features: [],
};
let r: FeatureCollection[] = [];
for (const t of trails) {
trails.forEach((t, i) => {
if (t.expand?.gpx) {
r.push(t.expand.gpx.toGeoJSON());
} else if (t.expand?.gpx_data) {
r.push(GPX.parse(t.expand.gpx_data).toGeoJSON());
}
if (clusterTrails && t.lat !== null && t.lon !== null) {
clusterData.features.push({
id: t.id,
type: "Feature",
properties: {
trail: t.id,
},
geometry: {
type: "Point",
coordinates: [t.lon ?? 0, t.lat ?? 0],
},
} as Feature);
}
}
if (clusterTrails) {
if (t.lat !== null && t.lon !== null) {
clusterData.features.push({
id: t.id,
type: "Feature",
properties: {
trail: t.id,
},
geometry: {
type: "Point",
coordinates: [t.lon ?? 0, t.lat ?? 0],
},
} as Feature);
}
return [r, clusterData];
if (t.polyline) {
previewData.features.push({
id: t.id,
type: "Feature",
properties: {
trail: t.id,
color: trailColors[
hashStringToIndex(
t.id ?? "",
trailColors.length,
)
],
},
geometry: {
type: "LineString",
coordinates: decodePolyline(t.polyline, 5),
},
});
}
}
});
return [r, clusterData, previewData];
}
function initMap(mapLoaded: boolean) {
@@ -231,6 +259,7 @@
});
if (clusterTrails) {
addClusterLayer(clusterData);
addPreviewLayer(previewData);
}
Object.entries(layerManager.layers).forEach(([id, layer]) => {
@@ -330,11 +359,14 @@
if (!geojson || !map) {
return;
}
const trailLayer = new TrailLayer(
id,
geojson,
trailColors[index % trailColors.length],
trailColors[
clusterTrails
? hashStringToIndex(id ?? "", trailColors.length)
: 0
],
{
onEnter: (e) =>
highlightTrail(id, trails[activeTrail ?? -1]?.id == id),
@@ -359,34 +391,15 @@
if (!geojson || !map || !map.style) {
return;
}
layerManager.addLayer(
"clusters",
new ClusterLayer(map, geojson, {
"unclustered-point": {
onMouseDown: (e) => {
const trail = trails.find(
(t) =>
t.id == (e as any).features[0].properties.trail,
);
if (!trail || !map) {
return;
}
highlightCluster(trail);
},
},
}),
);
layerManager.addLayer("clusters", new ClusterLayer(map, geojson));
}
function addClusterHighlightLayer(geojson: FeatureCollection) {
function addPreviewLayer(geojson: FeatureCollection) {
if (!geojson || !map || !map.style) {
return;
}
layerManager.addLayer(
"cluster-highlight",
new TrailLayer("cluster-highlight", geojson, trailColors[0]),
);
layerManager.addLayer("preview", new PreviewLayer(geojson));
}
function moveCrosshairToCursorPosition(e: M.MapMouseEvent) {
@@ -490,10 +503,6 @@
clusterPopup = createPopupFromTrail(trail);
clusterPopup.setLngLat([trail.lon!, trail.lat!]).addTo(map);
const gpx = await fetchGPX(trail);
addClusterHighlightLayer(GPX.parse(gpx).toGeoJSON());
clusterPopup.on("close", () => {
unHighlightCluster(false);
});
@@ -927,6 +936,15 @@
}
}
}
function hashStringToIndex(str: string, max: number) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash) % max;
}
</script>
<svelte:window on:keydown={handleKeydown} on:keyup={handleKeyup} />

View File

@@ -104,7 +104,7 @@ export async function calculateRouteBetween(startLat: number, startLon: number,
const points = decodePolyline(shape);
const startTime = new Date().getTime();
const waypoints = points.map((p, i) => new Waypoint({ $: { lat: p[0], lon: p[1] }, ele: heightResponse.height[i], time: new Date(startTime + (((duration * 1000) / points.length) * i)) }))
const waypoints = points.map((p, i) => new Waypoint({ $: { lat: p[1], lon: p[0] }, ele: heightResponse.height[i], time: new Date(startTime + (((duration * 1000) / points.length) * i)) }))
return waypoints
}

View File

@@ -61,7 +61,7 @@ export function decodePolyline(str: string, precision: number = 6) {
lat += latitude_change;
lng += longitude_change;
coordinates.push([lat / factor, lng / factor]);
coordinates.push([lng / factor, lat / factor]);
}
return coordinates;

View File

@@ -11,11 +11,12 @@ export class ClusterLayer implements BaseLayer {
listeners: Record<string, { onMouseUp?: (e: MapMouseEvent) => void; onMouseDown?: (e: MapMouseEvent) => void; onEnter?: (e: MapMouseEvent) => void; onLeave?: (e: MapMouseEvent) => void; onMouseMove?: (e: MapMouseEvent) => void; }> = {
"clusters": {
onMouseDown: this.zoomOnCluster.bind(this),
onMouseUp: this.zoomOnCluster.bind(this),
onEnter: () => this.map!.getCanvas().style.cursor = "pointer",
onLeave: () => this.map!.getCanvas().style.cursor = ""
},
"unclustered-point": {
onMouseUp: this.zoomOnUnclusteredPoint.bind(this),
onEnter: () => this.map!.getCanvas().style.cursor = "pointer",
onLeave: () => this.map!.getCanvas().style.cursor = ""
}
@@ -45,6 +46,7 @@ export class ClusterLayer implements BaseLayer {
type: "circle",
source: "cluster-trails",
filter: ["has", "point_count"],
maxzoom: 10,
paint: {
"circle-color": "#242734",
"circle-radius": [
@@ -71,6 +73,7 @@ export class ClusterLayer implements BaseLayer {
type: "symbol",
source: "cluster-trails",
filter: ["has", "point_count"],
maxzoom: 10,
paint: {
"text-color": "#fff",
},
@@ -84,6 +87,7 @@ export class ClusterLayer implements BaseLayer {
id: "unclustered-point",
type: "circle",
source: "cluster-trails",
maxzoom: 10,
filter: ["!", ["has", "point_count"]],
paint: {
"circle-color": "#242734",
@@ -108,6 +112,17 @@ export class ClusterLayer implements BaseLayer {
this.map.flyTo({
center: (features[0].geometry as any).coordinates,
zoom,
maxDuration: 3000
});
}
private zoomOnUnclusteredPoint(e: MapMouseEvent) {
const coordinates = (e as any).features[0].geometry.coordinates.slice();
this.map.flyTo({
center: coordinates,
zoom: 12,
maxDuration: 3000
});
}
}

View File

@@ -0,0 +1,63 @@
import type { StyleSpecification } from "maplibre-gl";
import type { BaseLayer } from "./layers";
export class PreviewLayer implements BaseLayer {
spec: StyleSpecification;
constructor(geojson: GeoJSON.FeatureCollection) {
const startPoints: GeoJSON.FeatureCollection = {
type: "FeatureCollection",
features: geojson.features.map((f, i) => ({
type: "Feature",
properties: {
...f.properties,
id: i
},
geometry: {
type: "Point",
coordinates: (f.geometry as any).coordinates[0]
}
}))
};
this.spec = {
version: 8,
name: "preview",
sources: {
"preview": {
type: "geojson",
data: geojson,
},
"preview-start-point": {
type: "geojson",
data: startPoints,
}
},
layers: [
{
id: "preview",
type: "line",
source: "preview",
minzoom: 10,
paint: {
"line-color": ["get", "color"],
"line-width": 5,
},
},
{
id: "preview-start-point",
type: "circle",
source: "preview-start-point",
minzoom: 10,
paint: {
"circle-color": "#242734",
"circle-radius": 6,
"circle-stroke-width": 2,
"circle-stroke-color": "#fff",
},
}
]
};
}
}

View File

@@ -46,7 +46,7 @@
const maxBoundingBox: TrailBoundingBox = page.data.boundingBox;
const settings: Settings = page.data.settings;
const MIN_ZOOM = 100;
const MIN_ZOOM = 10;
let loading: boolean = $state(true);
let loadingNextPage: boolean = false;
@@ -421,6 +421,7 @@
oninit={handleMapInit}
{trails}
showElevation={false}
showTerrain={true}
showInfoPopup={true}
activeTrail={-1}
fitBounds="off"