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")
|
||||
}
|
||||
|
||||
// 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"),
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
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 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"
|
||||
|
||||
Reference in New Issue
Block a user