diff --git a/db/util/meilisearch.go b/db/util/meilisearch.go index 7c613948..1b70110c 100644 --- a/db/util/meilisearch.go +++ b/db/util/meilisearch.go @@ -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"), diff --git a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte index 1afa8724..7baf8569 100644 --- a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte +++ b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte @@ -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; + } diff --git a/web/src/lib/stores/valhalla_store.svelte.ts b/web/src/lib/stores/valhalla_store.svelte.ts index 5fff3cab..baf5d66c 100644 --- a/web/src/lib/stores/valhalla_store.svelte.ts +++ b/web/src/lib/stores/valhalla_store.svelte.ts @@ -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 } diff --git a/web/src/lib/util/polyline_util.ts b/web/src/lib/util/polyline_util.ts index 28ec3f6d..44468794 100644 --- a/web/src/lib/util/polyline_util.ts +++ b/web/src/lib/util/polyline_util.ts @@ -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; diff --git a/web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts index 717c0eb4..ef5885bb 100644 --- a/web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts +++ b/web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts @@ -11,11 +11,12 @@ export class ClusterLayer implements BaseLayer { listeners: Record 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 }); } } \ No newline at end of file diff --git a/web/src/lib/vendor/maplibre-layer-manager/preview-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/preview-layer.ts new file mode 100644 index 00000000..2d9d6e08 --- /dev/null +++ b/web/src/lib/vendor/maplibre-layer-manager/preview-layer.ts @@ -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", + }, + } + ] + + }; + } +} \ No newline at end of file diff --git a/web/src/routes/map/+page.svelte b/web/src/routes/map/+page.svelte index cea10ba2..9b0e2b36 100644 --- a/web/src/routes/map/+page.svelte +++ b/web/src/routes/map/+page.svelte @@ -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"