adds trail preview on high map zoom
This commit is contained in:
@@ -42,10 +42,10 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
|
|||||||
category = trailCategory.GetString("name")
|
category = trailCategory.GetString("name")
|
||||||
}
|
}
|
||||||
|
|
||||||
// polyline, err := getPolyline(app, r)
|
polyline, err := getPolyline(app, r)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// polyline = ""
|
polyline = ""
|
||||||
// }
|
}
|
||||||
|
|
||||||
domain := ""
|
domain := ""
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("isLocal") {
|
||||||
@@ -78,7 +78,7 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
|
|||||||
"thumbnail": thumbnail,
|
"thumbnail": thumbnail,
|
||||||
"gpx": r.GetString("gpx"),
|
"gpx": r.GetString("gpx"),
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
// "polyline": polyline,
|
"polyline": polyline,
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"iri": r.GetString("iri"),
|
"iri": r.GetString("iri"),
|
||||||
"_geo": map[string]float64{
|
"_geo": map[string]float64{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
createPopupFromTrail,
|
createPopupFromTrail,
|
||||||
FontawesomeMarker,
|
FontawesomeMarker,
|
||||||
} from "$lib/util/maplibre_util";
|
} from "$lib/util/maplibre_util";
|
||||||
|
import { decodePolyline } from "$lib/util/polyline_util";
|
||||||
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
||||||
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";
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
import { ClusterLayer } from "$lib/vendor/maplibre-layer-manager/cluster-layer";
|
import { ClusterLayer } from "$lib/vendor/maplibre-layer-manager/cluster-layer";
|
||||||
import { baseMapStyles } from "$lib/vendor/maplibre-layer-manager/layers";
|
import { baseMapStyles } from "$lib/vendor/maplibre-layer-manager/layers";
|
||||||
import { LayerManager } from "$lib/vendor/maplibre-layer-manager/maplibre-layer-manager";
|
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 { TerrainLayer } from "$lib/vendor/maplibre-layer-manager/terrain-layer";
|
||||||
import { TrailLayer } from "$lib/vendor/maplibre-layer-manager/trail-layer";
|
import { TrailLayer } from "$lib/vendor/maplibre-layer-manager/trail-layer";
|
||||||
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
||||||
@@ -114,21 +116,21 @@
|
|||||||
let mapLoaded: boolean = false;
|
let mapLoaded: boolean = false;
|
||||||
|
|
||||||
const trailColors = [
|
const trailColors = [
|
||||||
"#3549BB",
|
"#3549bb", // blue
|
||||||
"#592E9E",
|
"#ff7f0e", // orange
|
||||||
"#47A2CD",
|
"#2ca02c", // green
|
||||||
"#62D5BC",
|
"#d62728", // red
|
||||||
"#7DDE95",
|
"#9467bd", // purple
|
||||||
"#759E2E",
|
"#8c564b", // brown
|
||||||
"#BBA535",
|
"#e377c2", // pink
|
||||||
"#CD6F47",
|
"#373642", // gray
|
||||||
"#D5627B",
|
"#fae455", // yellow
|
||||||
"#DE7DC5",
|
"#17becf", // teal
|
||||||
];
|
];
|
||||||
|
|
||||||
let clusterPopup: M.Popup | null = null;
|
let clusterPopup: M.Popup | null = null;
|
||||||
|
|
||||||
let [data, clusterData] = $derived(getData(trails));
|
let [data, clusterData, previewData] = $derived(getData(trails));
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (data && map && mapLoaded) {
|
if (data && map && mapLoaded) {
|
||||||
untrack(() => initMap(map?.loaded() ?? false));
|
untrack(() => initMap(map?.loaded() ?? false));
|
||||||
@@ -182,20 +184,25 @@
|
|||||||
|
|
||||||
function getData(
|
function getData(
|
||||||
trails: Trail[],
|
trails: Trail[],
|
||||||
): [FeatureCollection[], FeatureCollection] {
|
): [FeatureCollection[], FeatureCollection, FeatureCollection] {
|
||||||
let clusterData: FeatureCollection = {
|
let clusterData: FeatureCollection = {
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
features: [],
|
features: [],
|
||||||
};
|
};
|
||||||
|
let previewData: FeatureCollection = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [],
|
||||||
|
};
|
||||||
let r: FeatureCollection[] = [];
|
let r: FeatureCollection[] = [];
|
||||||
|
|
||||||
for (const t of trails) {
|
trails.forEach((t, i) => {
|
||||||
if (t.expand?.gpx) {
|
if (t.expand?.gpx) {
|
||||||
r.push(t.expand.gpx.toGeoJSON());
|
r.push(t.expand.gpx.toGeoJSON());
|
||||||
} else if (t.expand?.gpx_data) {
|
} else if (t.expand?.gpx_data) {
|
||||||
r.push(GPX.parse(t.expand.gpx_data).toGeoJSON());
|
r.push(GPX.parse(t.expand.gpx_data).toGeoJSON());
|
||||||
}
|
}
|
||||||
if (clusterTrails && t.lat !== null && t.lon !== null) {
|
if (clusterTrails) {
|
||||||
|
if (t.lat !== null && t.lon !== null) {
|
||||||
clusterData.features.push({
|
clusterData.features.push({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
@@ -208,9 +215,30 @@
|
|||||||
},
|
},
|
||||||
} as Feature);
|
} 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) {
|
function initMap(mapLoaded: boolean) {
|
||||||
@@ -231,6 +259,7 @@
|
|||||||
});
|
});
|
||||||
if (clusterTrails) {
|
if (clusterTrails) {
|
||||||
addClusterLayer(clusterData);
|
addClusterLayer(clusterData);
|
||||||
|
addPreviewLayer(previewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(layerManager.layers).forEach(([id, layer]) => {
|
Object.entries(layerManager.layers).forEach(([id, layer]) => {
|
||||||
@@ -330,11 +359,14 @@
|
|||||||
if (!geojson || !map) {
|
if (!geojson || !map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trailLayer = new TrailLayer(
|
const trailLayer = new TrailLayer(
|
||||||
id,
|
id,
|
||||||
geojson,
|
geojson,
|
||||||
trailColors[index % trailColors.length],
|
trailColors[
|
||||||
|
clusterTrails
|
||||||
|
? hashStringToIndex(id ?? "", trailColors.length)
|
||||||
|
: 0
|
||||||
|
],
|
||||||
{
|
{
|
||||||
onEnter: (e) =>
|
onEnter: (e) =>
|
||||||
highlightTrail(id, trails[activeTrail ?? -1]?.id == id),
|
highlightTrail(id, trails[activeTrail ?? -1]?.id == id),
|
||||||
@@ -359,34 +391,15 @@
|
|||||||
if (!geojson || !map || !map.style) {
|
if (!geojson || !map || !map.style) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
layerManager.addLayer(
|
layerManager.addLayer("clusters", new ClusterLayer(map, geojson));
|
||||||
"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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addClusterHighlightLayer(geojson: FeatureCollection) {
|
function addPreviewLayer(geojson: FeatureCollection) {
|
||||||
if (!geojson || !map || !map.style) {
|
if (!geojson || !map || !map.style) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
layerManager.addLayer(
|
layerManager.addLayer("preview", new PreviewLayer(geojson));
|
||||||
"cluster-highlight",
|
|
||||||
new TrailLayer("cluster-highlight", geojson, trailColors[0]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCrosshairToCursorPosition(e: M.MapMouseEvent) {
|
function moveCrosshairToCursorPosition(e: M.MapMouseEvent) {
|
||||||
@@ -490,10 +503,6 @@
|
|||||||
clusterPopup = createPopupFromTrail(trail);
|
clusterPopup = createPopupFromTrail(trail);
|
||||||
clusterPopup.setLngLat([trail.lon!, trail.lat!]).addTo(map);
|
clusterPopup.setLngLat([trail.lon!, trail.lat!]).addTo(map);
|
||||||
|
|
||||||
const gpx = await fetchGPX(trail);
|
|
||||||
|
|
||||||
addClusterHighlightLayer(GPX.parse(gpx).toGeoJSON());
|
|
||||||
|
|
||||||
clusterPopup.on("close", () => {
|
clusterPopup.on("close", () => {
|
||||||
unHighlightCluster(false);
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} on:keyup={handleKeyup} />
|
<svelte:window on:keydown={handleKeydown} on:keyup={handleKeyup} />
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export async function calculateRouteBetween(startLat: number, startLon: number,
|
|||||||
const points = decodePolyline(shape);
|
const points = decodePolyline(shape);
|
||||||
const startTime = new Date().getTime();
|
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
|
return waypoints
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function decodePolyline(str: string, precision: number = 6) {
|
|||||||
lat += latitude_change;
|
lat += latitude_change;
|
||||||
lng += longitude_change;
|
lng += longitude_change;
|
||||||
|
|
||||||
coordinates.push([lat / factor, lng / factor]);
|
coordinates.push([lng / factor, lat / factor]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return coordinates;
|
return coordinates;
|
||||||
|
|||||||
@@ -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; }> = {
|
listeners: Record<string, { onMouseUp?: (e: MapMouseEvent) => void; onMouseDown?: (e: MapMouseEvent) => void; onEnter?: (e: MapMouseEvent) => void; onLeave?: (e: MapMouseEvent) => void; onMouseMove?: (e: MapMouseEvent) => void; }> = {
|
||||||
"clusters": {
|
"clusters": {
|
||||||
onMouseDown: this.zoomOnCluster.bind(this),
|
onMouseUp: this.zoomOnCluster.bind(this),
|
||||||
onEnter: () => this.map!.getCanvas().style.cursor = "pointer",
|
onEnter: () => this.map!.getCanvas().style.cursor = "pointer",
|
||||||
onLeave: () => this.map!.getCanvas().style.cursor = ""
|
onLeave: () => this.map!.getCanvas().style.cursor = ""
|
||||||
},
|
},
|
||||||
"unclustered-point": {
|
"unclustered-point": {
|
||||||
|
onMouseUp: this.zoomOnUnclusteredPoint.bind(this),
|
||||||
onEnter: () => this.map!.getCanvas().style.cursor = "pointer",
|
onEnter: () => this.map!.getCanvas().style.cursor = "pointer",
|
||||||
onLeave: () => this.map!.getCanvas().style.cursor = ""
|
onLeave: () => this.map!.getCanvas().style.cursor = ""
|
||||||
}
|
}
|
||||||
@@ -45,6 +46,7 @@ export class ClusterLayer implements BaseLayer {
|
|||||||
type: "circle",
|
type: "circle",
|
||||||
source: "cluster-trails",
|
source: "cluster-trails",
|
||||||
filter: ["has", "point_count"],
|
filter: ["has", "point_count"],
|
||||||
|
maxzoom: 10,
|
||||||
paint: {
|
paint: {
|
||||||
"circle-color": "#242734",
|
"circle-color": "#242734",
|
||||||
"circle-radius": [
|
"circle-radius": [
|
||||||
@@ -71,6 +73,7 @@ export class ClusterLayer implements BaseLayer {
|
|||||||
type: "symbol",
|
type: "symbol",
|
||||||
source: "cluster-trails",
|
source: "cluster-trails",
|
||||||
filter: ["has", "point_count"],
|
filter: ["has", "point_count"],
|
||||||
|
maxzoom: 10,
|
||||||
paint: {
|
paint: {
|
||||||
"text-color": "#fff",
|
"text-color": "#fff",
|
||||||
},
|
},
|
||||||
@@ -84,6 +87,7 @@ export class ClusterLayer implements BaseLayer {
|
|||||||
id: "unclustered-point",
|
id: "unclustered-point",
|
||||||
type: "circle",
|
type: "circle",
|
||||||
source: "cluster-trails",
|
source: "cluster-trails",
|
||||||
|
maxzoom: 10,
|
||||||
filter: ["!", ["has", "point_count"]],
|
filter: ["!", ["has", "point_count"]],
|
||||||
paint: {
|
paint: {
|
||||||
"circle-color": "#242734",
|
"circle-color": "#242734",
|
||||||
@@ -108,6 +112,17 @@ export class ClusterLayer implements BaseLayer {
|
|||||||
this.map.flyTo({
|
this.map.flyTo({
|
||||||
center: (features[0].geometry as any).coordinates,
|
center: (features[0].geometry as any).coordinates,
|
||||||
zoom,
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
63
web/src/lib/vendor/maplibre-layer-manager/preview-layer.ts
vendored
Normal file
63
web/src/lib/vendor/maplibre-layer-manager/preview-layer.ts
vendored
Normal 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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,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 = 100;
|
const MIN_ZOOM = 10;
|
||||||
|
|
||||||
let loading: boolean = $state(true);
|
let loading: boolean = $state(true);
|
||||||
let loadingNextPage: boolean = false;
|
let loadingNextPage: boolean = false;
|
||||||
@@ -421,6 +421,7 @@
|
|||||||
oninit={handleMapInit}
|
oninit={handleMapInit}
|
||||||
{trails}
|
{trails}
|
||||||
showElevation={false}
|
showElevation={false}
|
||||||
|
showTerrain={true}
|
||||||
showInfoPopup={true}
|
showInfoPopup={true}
|
||||||
activeTrail={-1}
|
activeTrail={-1}
|
||||||
fitBounds="off"
|
fitBounds="off"
|
||||||
|
|||||||
Reference in New Issue
Block a user