adds multi trail support for maplibre
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
}
|
||||
|
||||
trail = (await gpx2trail(log.expand.gpx_data)).trail;
|
||||
trail.id = log.id;
|
||||
trail.expand.gpx_data = log.expand.gpx_data;
|
||||
|
||||
openMapModal();
|
||||
@@ -102,7 +103,7 @@
|
||||
>
|
||||
<div slot="content" id="summit-log-table-map" class="h-[32rem]">
|
||||
{#if trail}
|
||||
<MapWithElevationMaplibre {trail} bind:map
|
||||
<MapWithElevationMaplibre trails={[trail]} bind:map
|
||||
></MapWithElevationMaplibre>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { toGeoJson } from "$lib/util/gpx_util";
|
||||
import {
|
||||
createMarkerFromWaypoint,
|
||||
createPopupFromTrail,
|
||||
FontawesomeMarker,
|
||||
} from "$lib/util/maplibre_util";
|
||||
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
||||
@@ -15,21 +16,36 @@
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
||||
|
||||
export let trail: Trail | null;
|
||||
export let trails: Trail[] = [];
|
||||
export let markers: M.Marker[] = [];
|
||||
export let map: M.Map | null = null;
|
||||
export let drawing: boolean = false;
|
||||
export let showElevation: boolean = true;
|
||||
export let showInfoPopup: boolean = false;
|
||||
export let activeTrail: number = 0;
|
||||
export let minZoom: number = 0;
|
||||
|
||||
let mapContainer: HTMLDivElement;
|
||||
let epc: ElevationProfileControl;
|
||||
let startMarker: M.Marker;
|
||||
let endMarker: M.Marker;
|
||||
let epc: ElevationProfileControl | null = null;
|
||||
|
||||
let layers: Record<
|
||||
string,
|
||||
{
|
||||
startMarker: M.Marker | null;
|
||||
endMarker: M.Marker | null;
|
||||
source: M.GeoJSONSource | null;
|
||||
layer: M.LineLayerSpecification | null;
|
||||
listener: {
|
||||
onEnter: ((e: M.MapMouseEvent) => void) | null;
|
||||
onLeave: ((e: M.MapMouseEvent) => void) | null;
|
||||
onClick: ((e: M.MapMouseEvent) => void) | null;
|
||||
};
|
||||
}
|
||||
> = {};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: data = trail?.expand.gpx_data
|
||||
? (toGeoJson(trail.expand.gpx_data!) as GeoJSON)
|
||||
: null;
|
||||
$: data = getData(trails);
|
||||
|
||||
$: if (data && map) {
|
||||
initMap();
|
||||
@@ -55,84 +71,303 @@
|
||||
map.getCanvas().style.cursor = "crosshair";
|
||||
} else if (!drawing && map) {
|
||||
map.getCanvas().style.cursor = "inherit";
|
||||
addStartEndMarkers();
|
||||
addStartEndMarkers(
|
||||
trails[activeTrail],
|
||||
trails[activeTrail]?.id ?? activeTrail.toString(),
|
||||
data?.at(activeTrail),
|
||||
);
|
||||
}
|
||||
|
||||
function getData(trails: Trail[]) {
|
||||
if (!trails.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const r: GeoJSON[] = [];
|
||||
trails.forEach((t) => {
|
||||
if (t.expand.gpx_data) {
|
||||
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
|
||||
} else if (t.lat && t.lon) {
|
||||
r.push({
|
||||
id: "",
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [t.lon ?? 0, t.lat ?? 0],
|
||||
},
|
||||
} as GeoJSON);
|
||||
}
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
if (!map || !data) {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
epc.setData(data, trail!.expand.waypoints);
|
||||
epc.showProfile();
|
||||
|
||||
const trailSource = map.getSource("trail-source");
|
||||
if (!trailSource) {
|
||||
addTrailLayer();
|
||||
} else {
|
||||
(trailSource as M.GeoJSONSource).setData(data);
|
||||
if (data[activeTrail] && showElevation) {
|
||||
epc?.setData(
|
||||
data[activeTrail]!,
|
||||
trails.at(activeTrail)!.expand.waypoints,
|
||||
);
|
||||
epc?.showProfile();
|
||||
}
|
||||
|
||||
if (!drawing) {
|
||||
addStartEndMarkers();
|
||||
trails.forEach((t, i) => {
|
||||
const layerId = t.id ?? i.toString();
|
||||
addTrailLayer(t, layerId, data[i]);
|
||||
});
|
||||
|
||||
Object.keys(layers).forEach((layerId) => {
|
||||
const isStillVisible = trails.some((t) => t.id === layerId);
|
||||
if (!isStillVisible) {
|
||||
removeTrailLayer(layerId);
|
||||
}
|
||||
});
|
||||
|
||||
if (!drawing && data.some((d) => d.bbox !== undefined)) {
|
||||
flyToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
function addTrailLayer() {
|
||||
if (!data || !map) {
|
||||
function getBounds() {
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
for (const [xMin, yMin, xMax, yMax] of data
|
||||
.filter((d) => d.bbox !== undefined)
|
||||
.map((d) => d.bbox!)) {
|
||||
minX = Math.min(minX, xMin);
|
||||
minY = Math.min(minY, yMin);
|
||||
maxX = Math.max(maxX, xMax);
|
||||
maxY = Math.max(maxY, yMax);
|
||||
}
|
||||
|
||||
return new M.LngLatBounds([minX, minY, maxX, maxY]);
|
||||
}
|
||||
|
||||
function flyToBounds() {
|
||||
const bounds = data[activeTrail]
|
||||
? (data[activeTrail].bbox as M.LngLatBoundsLike)
|
||||
: getBounds();
|
||||
map!.fitBounds(bounds, {
|
||||
animate: true,
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom:
|
||||
16 +
|
||||
(epc?.isProfileShown
|
||||
? map!.getContainer().clientHeight * 0.3
|
||||
: 0),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function removeTrailLayer(id: string) {
|
||||
if (!layers[id]) {
|
||||
return;
|
||||
}
|
||||
const trailSource = map.getSource("trail-source");
|
||||
if (!trailSource) {
|
||||
const layer = layers[id];
|
||||
|
||||
if (layer.layer) {
|
||||
map?.removeLayer(id);
|
||||
}
|
||||
if (layer.source) {
|
||||
map?.removeSource(id);
|
||||
}
|
||||
layer.startMarker?.remove();
|
||||
layer.endMarker?.remove();
|
||||
|
||||
map?.off("mouseenter", id, layers[id].listener.onEnter!);
|
||||
map?.off("mouseleave", id, layers[id].listener.onLeave!);
|
||||
map?.off("click", id, layers[id].listener.onClick!);
|
||||
|
||||
delete layers[id];
|
||||
}
|
||||
|
||||
function createEmptyLayer(id: string) {
|
||||
if (!layers[id]) {
|
||||
layers[id] = {
|
||||
startMarker: null,
|
||||
endMarker: null,
|
||||
source: null,
|
||||
layer: null,
|
||||
listener: {
|
||||
onClick: null,
|
||||
onEnter: null,
|
||||
onLeave: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function addTrailLayer(
|
||||
trail: Trail,
|
||||
id: string,
|
||||
geojson: GeoJSON | null | undefined,
|
||||
) {
|
||||
if (!geojson || !map) {
|
||||
return;
|
||||
}
|
||||
|
||||
createEmptyLayer(id);
|
||||
|
||||
if (!layers[id].source) {
|
||||
try {
|
||||
map.addSource("trail-source", {
|
||||
map.addSource(id, {
|
||||
type: "geojson",
|
||||
data: data,
|
||||
data: geojson,
|
||||
});
|
||||
layers[id].source = map.getSource(id) as M.GeoJSONSource;
|
||||
// map.addSource("trail-source", {
|
||||
// type: "vector",
|
||||
// url: `http://localhost:8080/data/out.json`,
|
||||
// });
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
layers[id].source.setData(geojson);
|
||||
}
|
||||
|
||||
const trailLayer = map.getLayer("trail-layer");
|
||||
if (!trailLayer) {
|
||||
if (!layers[id].layer) {
|
||||
map.addLayer({
|
||||
id: "trail-layer",
|
||||
id: id,
|
||||
type: "line",
|
||||
source: "trail-source",
|
||||
source: id,
|
||||
minzoom: minZoom,
|
||||
paint: {
|
||||
"line-color": "#648ad5",
|
||||
"line-width": 5,
|
||||
},
|
||||
});
|
||||
layers[id].layer = map.getLayer(id) as M.LineLayerSpecification;
|
||||
layers[id].listener.onEnter = (e) => highlightTrail(id);
|
||||
layers[id].listener.onLeave = (e) => unHighlightTrail(id);
|
||||
layers[id].listener.onClick = (e) => focusTrail(trail, e);
|
||||
|
||||
map.on("mouseenter", id, layers[id].listener.onEnter);
|
||||
map.on("mouseleave", id, layers[id].listener.onLeave);
|
||||
map.on("click", id, layers[id].listener.onClick);
|
||||
|
||||
// map.addLayer({
|
||||
// id: "trail-layer",
|
||||
// type: "line",
|
||||
// source: "trail-source",
|
||||
// "source-layer": "Herzogstand", // Replace with the actual layer name in the .mbtiles file
|
||||
// layout: {
|
||||
// "line-join": "round",
|
||||
// "line-cap": "round",
|
||||
// },
|
||||
// paint: {
|
||||
// "line-color": "#648ad5",
|
||||
// "line-width": 5,
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
if (!drawing) {
|
||||
addStartEndMarkers(trail, id, geojson);
|
||||
}
|
||||
}
|
||||
|
||||
function addStartEndMarkers() {
|
||||
if (!map || !data) {
|
||||
export function highlightTrail(id: string) {
|
||||
map?.setPaintProperty(id, "line-width", 7);
|
||||
map?.setPaintProperty(id, "line-color", "#2766e3");
|
||||
}
|
||||
|
||||
export function unHighlightTrail(id: string) {
|
||||
map?.setPaintProperty(id, "line-width", 5);
|
||||
map?.setPaintProperty(id, "line-color", "#648ad5");
|
||||
}
|
||||
|
||||
export function focusTrail(trail: Trail, e?: M.MapMouseEvent) {
|
||||
const currentlyFocussedTrail = trails[activeTrail];
|
||||
if (currentlyFocussedTrail) {
|
||||
unFocusTrail(currentlyFocussedTrail);
|
||||
}
|
||||
e?.preventDefault();
|
||||
dispatch("select", trail);
|
||||
const index = trails.indexOf(trail);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
const startEndPoint = findStartAndEndPoints(data);
|
||||
activeTrail = index;
|
||||
highlightTrail(trail.id!);
|
||||
flyToBounds();
|
||||
if (data[activeTrail] && showElevation) {
|
||||
epc?.setData(
|
||||
data[activeTrail]!,
|
||||
trails.at(activeTrail)!.expand.waypoints,
|
||||
);
|
||||
epc?.showProfile();
|
||||
}
|
||||
}
|
||||
|
||||
startMarker ??= new FontawesomeMarker({ icon: "fa fa-bullseye" }, {});
|
||||
export function unFocusTrail(trail: Trail) {
|
||||
dispatch("unselect", trail);
|
||||
activeTrail = -1;
|
||||
unHighlightTrail(trail.id!);
|
||||
flyToBounds();
|
||||
|
||||
startMarker.setLngLat(startEndPoint[0] as M.LngLatLike).addTo(map);
|
||||
if (showElevation) {
|
||||
epc?.hideProfile();
|
||||
}
|
||||
}
|
||||
|
||||
endMarker ??= new FontawesomeMarker(
|
||||
function addStartEndMarkers(
|
||||
trail: Trail,
|
||||
id: string,
|
||||
geojson: GeoJSON | null | undefined,
|
||||
) {
|
||||
if (!map || !trail) {
|
||||
return;
|
||||
}
|
||||
createEmptyLayer(id);
|
||||
|
||||
layers[id].startMarker ??= new FontawesomeMarker(
|
||||
{ icon: "fa fa-bullseye" },
|
||||
{},
|
||||
);
|
||||
|
||||
if (!geojson) {
|
||||
if (trail.lon && trail.lat) {
|
||||
layers[id].startMarker
|
||||
.setLngLat([trail.lon, trail.lat])
|
||||
.addTo(map);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const startEndPoint = findStartAndEndPoints(geojson);
|
||||
|
||||
layers[id].startMarker
|
||||
.setLngLat(startEndPoint[0] as M.LngLatLike)
|
||||
.addTo(map);
|
||||
|
||||
if (showInfoPopup) {
|
||||
const popup = createPopupFromTrail(trail);
|
||||
layers[id].startMarker.setPopup(popup);
|
||||
}
|
||||
|
||||
layers[id].endMarker ??= new FontawesomeMarker(
|
||||
{ icon: "fa fa-flag-checkered" },
|
||||
{},
|
||||
);
|
||||
endMarker.setLngLat(startEndPoint[1] as M.LngLatLike).addTo(map);
|
||||
layers[id].endMarker.setLngLat(startEndPoint[1] as M.LngLatLike);
|
||||
if (map.getZoom() > minZoom) {
|
||||
layers[id].endMarker.addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
map!.fitBounds(data.bbox as any, {
|
||||
animate: false,
|
||||
padding: {
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: map!.getContainer().clientHeight * 0.3 + 16,
|
||||
},
|
||||
});
|
||||
export function togglePopup(id: string) {
|
||||
layers[id]?.startMarker?.togglePopup();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
@@ -148,6 +383,16 @@
|
||||
).ElevationProfileControl;
|
||||
|
||||
const mapStyles = [
|
||||
{
|
||||
text: "Open Street Maps",
|
||||
value: "/styles/osm.json",
|
||||
thumbnail: "https://tile.openstreetmap.org/1/0/0.png",
|
||||
},
|
||||
{
|
||||
text: "Open Topo Maps",
|
||||
value: "/styles/otm.json",
|
||||
thumbnail: "https://tile.opentopomap.org/1/0/0.png",
|
||||
},
|
||||
{
|
||||
text: "Carto Light",
|
||||
value: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
|
||||
@@ -186,28 +431,10 @@
|
||||
elevationMarker.setLngLat([0, 0]).addTo(map);
|
||||
elevationMarker.setOpacity("0");
|
||||
|
||||
epc = new ElevationProfileControl({
|
||||
visible: false,
|
||||
profileBackgroundColor: $theme == "light" ? "#242734" : "#191b24",
|
||||
backgroundColor: "bg-menu-background/90",
|
||||
unit: $page.data.settings?.unit ?? "metric",
|
||||
profileLineWidth: 3,
|
||||
displayDistanceGrid: true,
|
||||
tooltipDisplayDPlus: false,
|
||||
onEnter: () => {
|
||||
elevationMarker.setOpacity("1");
|
||||
},
|
||||
onLeave: () => {
|
||||
elevationMarker.setOpacity("0");
|
||||
},
|
||||
onMove: (data) => {
|
||||
elevationMarker.setLngLat(data.position as M.LngLatLike);
|
||||
},
|
||||
});
|
||||
|
||||
const switcherControl = new StyleSwitcherControl({
|
||||
styles: mapStyles,
|
||||
onSwitch: (style) => {
|
||||
layers = {};
|
||||
map?.setStyle(style.value);
|
||||
localStorage.setItem("layer", style.text);
|
||||
},
|
||||
@@ -222,18 +449,64 @@
|
||||
}),
|
||||
"top-left",
|
||||
);
|
||||
map.addControl(epc);
|
||||
map.addControl(switcherControl);
|
||||
|
||||
if (showElevation) {
|
||||
epc = new ElevationProfileControl({
|
||||
visible: false,
|
||||
profileBackgroundColor:
|
||||
$theme == "light" ? "#242734" : "#191b24",
|
||||
backgroundColor: "bg-menu-background/90",
|
||||
unit: $page.data.settings?.unit ?? "metric",
|
||||
profileLineWidth: 3,
|
||||
displayDistanceGrid: true,
|
||||
tooltipDisplayDPlus: false,
|
||||
zoom: false,
|
||||
onEnter: () => {
|
||||
elevationMarker.setOpacity("1");
|
||||
},
|
||||
onLeave: () => {
|
||||
elevationMarker.setOpacity("0");
|
||||
},
|
||||
onMove: (data) => {
|
||||
elevationMarker.setLngLat(data.position as M.LngLatLike);
|
||||
},
|
||||
});
|
||||
map.addControl(epc);
|
||||
}
|
||||
|
||||
map.on("styledata", () => {
|
||||
addTrailLayer();
|
||||
trails.forEach((t, i) => {
|
||||
addTrailLayer(t, t.id ?? i.toString(), data?.at(i));
|
||||
});
|
||||
});
|
||||
|
||||
map.on("moveend", (e) => {
|
||||
dispatch("moveend", e.target);
|
||||
});
|
||||
|
||||
map.on("zoom", (e) => {
|
||||
const zoom = e.target.getZoom();
|
||||
Object.values(layers).forEach((l) => {
|
||||
if (zoom > minZoom && map) {
|
||||
l.endMarker?.addTo(map);
|
||||
} else {
|
||||
l.endMarker?.remove();
|
||||
}
|
||||
});
|
||||
|
||||
dispatch("zoom", e.target);
|
||||
});
|
||||
|
||||
map.on("click", (e) => {
|
||||
dispatch("click", e);
|
||||
});
|
||||
|
||||
for (const waypoint of trail?.expand.waypoints ?? []) {
|
||||
map.on("load", () => {
|
||||
dispatch("init", map);
|
||||
});
|
||||
|
||||
for (const waypoint of trails[activeTrail]?.expand.waypoints ?? []) {
|
||||
const marker = createMarkerFromWaypoint(waypoint);
|
||||
marker.addTo(map);
|
||||
markers.push(marker);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_store";
|
||||
import { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store";
|
||||
import * as M from "maplibre-gl";
|
||||
|
||||
export const trails: Writable<Trail[]> = writable([])
|
||||
export const trail: Writable<Trail> = writable(new Trail(""));
|
||||
@@ -71,7 +72,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
|
||||
}
|
||||
}
|
||||
|
||||
export async function trails_search_bounding_box(northEast: LatLng, southWest: LatLng, filter?: TrailFilter) {
|
||||
export async function trails_search_bounding_box(northEast: M.LngLat, southWest: M.LngLat, filter?: TrailFilter, loadGPX: boolean = true) {
|
||||
|
||||
let filterText: string = "";
|
||||
|
||||
@@ -83,6 +84,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
q: "", options: {
|
||||
limit: 400,
|
||||
filter: [
|
||||
`_geoBoundingBox([${northEast.lat}, ${northEast.lng}], [${southWest.lat}, ${southWest.lng}])`,
|
||||
filterText
|
||||
@@ -92,7 +94,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
|
||||
});
|
||||
const result = await r.json();
|
||||
|
||||
const trailIds = result.hits.map((h: Record<string, any>) => h.id);
|
||||
const trailIds = result.hits?.map((h: Record<string, any>) => h.id) ?? [];
|
||||
|
||||
if (trailIds.length == 0) {
|
||||
const currentTrails: Trail[] = get(trails);
|
||||
@@ -101,6 +103,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
|
||||
}
|
||||
|
||||
r = await fetch('/api/v1/trail?' + new URLSearchParams({
|
||||
"per-page": "-1",
|
||||
filter: `'${trailIds.join(',')}'~id`,
|
||||
expand: "category,waypoints,summit_logs",
|
||||
sort: `+name`,
|
||||
@@ -110,14 +113,17 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
|
||||
const response = await r.json()
|
||||
|
||||
if (r.ok) {
|
||||
for (const trail of response.items) {
|
||||
const gpxData: string = await fetchGPX(trail);
|
||||
if (!trail.expand) {
|
||||
trail.expand = {};
|
||||
if (loadGPX) {
|
||||
for (const trail of response.items) {
|
||||
const gpxData: string = await fetchGPX(trail);
|
||||
if (!trail.expand) {
|
||||
trail.expand = {};
|
||||
}
|
||||
trail.expand.gpx_data = gpxData;
|
||||
}
|
||||
trail.expand.gpx_data = gpxData;
|
||||
}
|
||||
|
||||
|
||||
const comparison = compareObjectArrays<Trail>(get(trails), response.items)
|
||||
|
||||
if (comparison.added.length || comparison.deleted.length || comparison.updated.length) {
|
||||
|
||||
@@ -30,42 +30,91 @@ export function bbox(
|
||||
|
||||
export function findStartAndEndPoints(geojson: GeoJsonObject): Position[] {
|
||||
const startEndPoints: Position[] = [];
|
||||
|
||||
(geojson as FeatureCollection).features.forEach((feature) => {
|
||||
const geometry = feature.geometry;
|
||||
|
||||
if (geometry.type === "LineString") {
|
||||
const coords = geometry.coordinates as number[][];
|
||||
const start: [number, number] = [coords[0][0], coords[0][1]]; // First point
|
||||
const end: [number, number] = [
|
||||
coords[coords.length - 1][0],
|
||||
coords[coords.length - 1][1],
|
||||
]; // Last point
|
||||
startEndPoints.push(start);
|
||||
startEndPoints.push(end)
|
||||
} else if (geometry.type === "MultiLineString") {
|
||||
const coords = geometry.coordinates as number[][][];
|
||||
const start: [number, number] = [
|
||||
coords[0][0][0],
|
||||
coords[0][0][1],
|
||||
]; // First point of the first line
|
||||
const lastLine = coords[coords.length - 1];
|
||||
const end: [number, number] = [
|
||||
lastLine[lastLine.length - 1][0],
|
||||
lastLine[lastLine.length - 1][1],
|
||||
]; // Last point of the last line
|
||||
startEndPoints.push(start);
|
||||
startEndPoints.push(end)
|
||||
} else {
|
||||
console.warn(
|
||||
`Geometry type ${geometry.type} is not supported for start/end point extraction.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if it's a FeatureCollection
|
||||
if ((geojson as any).features) {
|
||||
(geojson as any).features.forEach((feature: any) => {
|
||||
const geometry = feature.geometry;
|
||||
extractStartAndEndPointsFromGeometry(geometry, startEndPoints);
|
||||
});
|
||||
} else if ((geojson as any).geometry) {
|
||||
// Single Feature
|
||||
const geometry = (geojson as any).geometry;
|
||||
extractStartAndEndPointsFromGeometry(geometry, startEndPoints);
|
||||
} else {
|
||||
console.warn(
|
||||
"Unsupported GeoJSON type. Expected FeatureCollection or Feature."
|
||||
);
|
||||
}
|
||||
|
||||
return startEndPoints;
|
||||
}
|
||||
|
||||
function extractStartAndEndPointsFromGeometry(geometry: any, startEndPoints: Position[]) {
|
||||
if (geometry.type === "LineString") {
|
||||
const coords = geometry.coordinates as number[][];
|
||||
const start: [number, number] = [coords[0][0], coords[0][1]]; // First point
|
||||
const end: [number, number] = [
|
||||
coords[coords.length - 1][0],
|
||||
coords[coords.length - 1][1],
|
||||
]; // Last point
|
||||
startEndPoints.push(start, end);
|
||||
} else if (geometry.type === "MultiLineString") {
|
||||
const coords = geometry.coordinates as number[][][];
|
||||
const start: [number, number] = [
|
||||
coords[0][0][0],
|
||||
coords[0][0][1],
|
||||
]; // First point of the first line
|
||||
const lastLine = coords[coords.length - 1];
|
||||
const end: [number, number] = [
|
||||
lastLine[lastLine.length - 1][0],
|
||||
lastLine[lastLine.length - 1][1],
|
||||
]; // Last point of the last line
|
||||
startEndPoints.push(start, end);
|
||||
} else if (geometry.type === "Point") {
|
||||
const coords = geometry.coordinates as number[];
|
||||
startEndPoints.push(coords as [number, number], coords as [number, number]);
|
||||
} else if (geometry.type === "MultiPoint") {
|
||||
const coords = geometry.coordinates as number[][];
|
||||
const start: [number, number] = [coords[0][0], coords[0][1]]; // First point
|
||||
const end: [number, number] = [
|
||||
coords[coords.length - 1][0],
|
||||
coords[coords.length - 1][1],
|
||||
]; // Last point
|
||||
startEndPoints.push(start, end);
|
||||
} else if (geometry.type === "Polygon") {
|
||||
const coords = geometry.coordinates as number[][][];
|
||||
const start: [number, number] = [
|
||||
coords[0][0][0],
|
||||
coords[0][0][1],
|
||||
]; // First point of the first ring
|
||||
const lastRing = coords[coords.length - 1];
|
||||
const end: [number, number] = [
|
||||
lastRing[lastRing.length - 1][0],
|
||||
lastRing[lastRing.length - 1][1],
|
||||
]; // Last point of the last ring
|
||||
startEndPoints.push(start, end);
|
||||
} else if (geometry.type === "MultiPolygon") {
|
||||
const coords = geometry.coordinates as number[][][][];
|
||||
const firstPolygon = coords[0];
|
||||
const start: [number, number] = [
|
||||
firstPolygon[0][0][0],
|
||||
firstPolygon[0][0][1],
|
||||
]; // First point of the first ring of the first polygon
|
||||
const lastPolygon = coords[coords.length - 1];
|
||||
const lastRing = lastPolygon[lastPolygon.length - 1];
|
||||
const end: [number, number] = [
|
||||
lastRing[lastRing.length - 1][0],
|
||||
lastRing[lastRing.length - 1][1],
|
||||
]; // Last point of the last ring of the last polygon
|
||||
startEndPoints.push(start, end);
|
||||
} else {
|
||||
console.warn(
|
||||
`Geometry type ${geometry.type} is not supported for start/end point extraction.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function coordEach(geojson: GeoJSON, callback: (
|
||||
currentCoord: number[],
|
||||
coordIndex: number,
|
||||
@@ -144,13 +193,13 @@ function coordEach(geojson: GeoJSON, callback: (
|
||||
for (j = 0; j < coords.length; j++) {
|
||||
if (
|
||||
callback(
|
||||
coords[j],
|
||||
coordIndex,
|
||||
featureIndex,
|
||||
multiFeatureIndex,
|
||||
geometryIndex
|
||||
coords[j],
|
||||
coordIndex,
|
||||
featureIndex,
|
||||
multiFeatureIndex,
|
||||
geometryIndex
|
||||
) === false
|
||||
)
|
||||
)
|
||||
return false;
|
||||
coordIndex++;
|
||||
if (geomType === "MultiPoint") multiFeatureIndex++;
|
||||
@@ -163,13 +212,13 @@ function coordEach(geojson: GeoJSON, callback: (
|
||||
for (k = 0; k < coords[j].length - wrapShrink; k++) {
|
||||
if (
|
||||
callback(
|
||||
coords[j][k],
|
||||
coordIndex,
|
||||
featureIndex,
|
||||
multiFeatureIndex,
|
||||
geometryIndex
|
||||
coords[j][k],
|
||||
coordIndex,
|
||||
featureIndex,
|
||||
multiFeatureIndex,
|
||||
geometryIndex
|
||||
) === false
|
||||
)
|
||||
)
|
||||
return false;
|
||||
coordIndex++;
|
||||
}
|
||||
@@ -185,13 +234,13 @@ function coordEach(geojson: GeoJSON, callback: (
|
||||
for (l = 0; l < coords[j][k].length - wrapShrink; l++) {
|
||||
if (
|
||||
callback(
|
||||
coords[j][k][l],
|
||||
coordIndex,
|
||||
featureIndex,
|
||||
multiFeatureIndex,
|
||||
geometryIndex
|
||||
coords[j][k][l],
|
||||
coordIndex,
|
||||
featureIndex,
|
||||
multiFeatureIndex,
|
||||
geometryIndex
|
||||
) === false
|
||||
)
|
||||
)
|
||||
return false;
|
||||
coordIndex++;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import M from "maplibre-gl";
|
||||
import { getFileURL } from "./file_util";
|
||||
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
|
||||
import { get } from "svelte/store";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export class FontawesomeMarker extends M.Marker {
|
||||
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string }, markerOptions?: M.MarkerOptions) {
|
||||
@@ -72,6 +77,38 @@ export function createAnchorMarker(lat: number, lon: number, index: number, onDe
|
||||
return marker
|
||||
}
|
||||
|
||||
export function createPopupFromTrail(trail: Trail) {
|
||||
const thumbnail = trail.photos.length
|
||||
? getFileURL(trail, trail.photos[trail.thumbnail])
|
||||
: "/imgs/default_thumbnail.webp";
|
||||
const popup = new M.Popup({maxWidth: "320px"});
|
||||
popup.setHTML(
|
||||
`<a href="/trail/view/${trail.id}" data-sveltekit-preload-data="off">
|
||||
<li class="flex items-center gap-4 cursor-pointer text-black max-w-80">
|
||||
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${thumbnail}" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
||||
<div class="flex gap-x-4">
|
||||
${trail.location ? `<h5><i class="fa fa-location-dot mr-2"></i>${trail.location}</h5>` : ""}
|
||||
<h5><i class="fa fa-gauge mr-2"></i>${get(_)(trail.difficulty as string)}</h5>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 mt-2 gap-x-4 gap-y-2 text-sm text-gray-500 flex-wrap"><span class="shrink-0"><i
|
||||
class="fa fa-left-right mr-2"></i>${formatDistance(
|
||||
trail.distance,
|
||||
)}</span><span class="shrink-0"><i class="fa fa-clock mr-2"></i>${formatTimeHHMM(
|
||||
trail.duration,
|
||||
)}</span><span class="shrink-0"><i class="fa fa-arrow-trend-up mr-2"></i>${formatElevation(
|
||||
trail.elevation_gain,
|
||||
)}</span></span> <span class="shrink-0"><i class="fa fa-arrow-trend-down mr-2"></i>${formatElevation(
|
||||
trail.elevation_loss,
|
||||
)}</span></div>
|
||||
</div>
|
||||
</li>
|
||||
</a>`)
|
||||
return popup;
|
||||
}
|
||||
|
||||
// export function calculatePixelPerMeter(map: Map, meters: number) {
|
||||
// const y = map.getSize().y;
|
||||
// const x = map.getSize().x;
|
||||
|
||||
@@ -58,7 +58,7 @@ export class ElevationProfileControl implements IControl {
|
||||
private map?: M.Map;
|
||||
private buttonContainer?: HTMLDivElement;
|
||||
private toggleButton?: HTMLButtonElement;
|
||||
private isProfileShown = false;
|
||||
public isProfileShown = false;
|
||||
private iconSpan?: HTMLSpanElement;
|
||||
|
||||
private profileContainer?: HTMLDivElement;
|
||||
|
||||
@@ -8,15 +8,15 @@ import type {
|
||||
Position,
|
||||
} from "geojson";
|
||||
|
||||
import { Chart, registerables, type ScriptableContext } from "chart.js";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import zoomPlugin from "chartjs-plugin-zoom";
|
||||
// @ts-ignore
|
||||
import { CrosshairPlugin } from "chartjs-plugin-crosshair";
|
||||
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import { haversineCumulatedDistanceWgs84, smoothElevations } from "./tools";
|
||||
import { haversineDistance } from "$lib/models/gpx/utils";
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import { formatTimeHHMM } from "$lib/util/format_util";
|
||||
import { haversineCumulatedDistanceWgs84, smoothElevations } from "./tools";
|
||||
|
||||
const FEET_PER_METER = 3.28084;
|
||||
const MILES_PER_METER = 0.000621371;
|
||||
@@ -288,6 +288,7 @@ export type ElevationProfileOptions = {
|
||||
* Default: `"#0005"` (partially transparent black)
|
||||
*/
|
||||
crosshairColor?: string;
|
||||
zoom?: boolean;
|
||||
/**
|
||||
* Callback function to call when the chart is zoomed or panned.
|
||||
* The argument `windowedLineString` is the GeoJSON LineString corresponding
|
||||
@@ -348,6 +349,7 @@ const elevationProfileDefaultOptions: ElevationProfileOptions = {
|
||||
paddingRight: 10,
|
||||
onClick: null,
|
||||
onMove: null,
|
||||
zoom: true
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -573,15 +575,15 @@ export class ElevationProfile {
|
||||
zoom: {
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true,
|
||||
enabled: this.settings.zoom,
|
||||
},
|
||||
pinch: {
|
||||
enabled: true,
|
||||
enabled: this.settings.zoom,
|
||||
},
|
||||
mode: "x",
|
||||
},
|
||||
pan: {
|
||||
enabled: true,
|
||||
enabled: this.settings.zoom,
|
||||
mode: "x",
|
||||
},
|
||||
limits: {
|
||||
@@ -701,7 +703,7 @@ export class ElevationProfile {
|
||||
},
|
||||
{
|
||||
id: "customZoomEvent",
|
||||
afterDataLimits: () => {
|
||||
afterDataLimits: (chart) => {
|
||||
if (typeof this.settings.onChangeView !== "function") return;
|
||||
try {
|
||||
this.settings.onChangeView.apply(this, [
|
||||
@@ -791,6 +793,7 @@ export class ElevationProfile {
|
||||
if (color !== prevColor) {
|
||||
const percentDone = this.cumulatedDistance[i] / this.cumulatedDistance[this.cumulatedDistance.length - 1]
|
||||
this.gradient.addColorStop(percentDone, color);
|
||||
|
||||
prevColor = color;
|
||||
}
|
||||
|
||||
@@ -974,7 +977,7 @@ export class ElevationProfile {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.grade.push(this.grade.at(-1) ?? 0);
|
||||
this.cumulatedDPlus.push(cumulatedDPlus);
|
||||
|
||||
|
||||
@@ -6,22 +6,18 @@
|
||||
import ListCard from "$lib/components/list/list_card.svelte";
|
||||
import ListPanel from "$lib/components/list/list_panel.svelte";
|
||||
import ListShareModal from "$lib/components/list/list_share_modal.svelte";
|
||||
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte";
|
||||
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
||||
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
|
||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||
import { List } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import {
|
||||
list,
|
||||
lists,
|
||||
lists_delete,
|
||||
lists_index,
|
||||
lists_index
|
||||
} from "$lib/stores/list_store";
|
||||
import { fetchGPX } from "$lib/stores/trail_store";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type { Map } from "leaflet";
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import * as M from "maplibre-gl";
|
||||
|
||||
import { onMount, tick } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
@@ -29,14 +25,16 @@
|
||||
let openConfirmModal: () => void;
|
||||
let openShareModal: () => void;
|
||||
|
||||
let map: Map;
|
||||
let mapWithElevationMultiple: MapWithElevationMultiple;
|
||||
let map: M.Map;
|
||||
let mapWithElevation: MapWithElevationMaplibre;
|
||||
let markers: any[];
|
||||
let showMap: boolean = true;
|
||||
|
||||
let selectedList: List | null = null;
|
||||
let selectedTrail: Trail | null = null;
|
||||
|
||||
let activeTrailIndex: number = -1;
|
||||
|
||||
onMount(() => {
|
||||
if ($page.url.searchParams.get("list")) {
|
||||
const listToFocus = $lists.find(
|
||||
@@ -86,32 +84,32 @@
|
||||
selectedList = item;
|
||||
}
|
||||
|
||||
function back() {
|
||||
async function back() {
|
||||
if (selectedTrail) {
|
||||
mapWithElevation.unFocusTrail(selectedTrail);
|
||||
selectedTrail = null;
|
||||
mapWithElevationMultiple.resetSelection();
|
||||
} else if (selectedList) {
|
||||
selectedList = null;
|
||||
map.flyTo([0, 0], 4, {
|
||||
duration: 0.25,
|
||||
easeLinearity: 0.25,
|
||||
noMoveStart: true,
|
||||
map.flyTo({
|
||||
animate: true,
|
||||
zoom: 1,
|
||||
center: [0, 0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function selectTrail(trail: Trail) {
|
||||
selectedTrail = trail;
|
||||
mapWithElevationMultiple.selectTrail(trail.id!);
|
||||
mapWithElevation.focusTrail(trail);
|
||||
window.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
function highlightTrail(trail: Trail) {
|
||||
mapWithElevationMultiple.highlightTrail(trail.id!);
|
||||
mapWithElevation.highlightTrail(trail.id!);
|
||||
}
|
||||
|
||||
function unHighlightTrail(trail: Trail) {
|
||||
mapWithElevationMultiple.unHighlightTrail(trail.id!);
|
||||
mapWithElevation.unHighlightTrail(trail.id!);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -170,17 +168,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="trail-map" class="md:sticky md:top-[62px]" class:hidden={!showMap}>
|
||||
<MapWithElevationMultiple
|
||||
<MapWithElevationMaplibre
|
||||
trails={selectedList?.expand?.trails ?? []}
|
||||
bind:map
|
||||
bind:this={mapWithElevationMultiple}
|
||||
bind:this={mapWithElevation}
|
||||
bind:markers
|
||||
on:select={(e) => {
|
||||
selectedTrail = e.detail
|
||||
selectedTrail = e.detail;
|
||||
}}
|
||||
bindRoutePopup={false}
|
||||
options={{ itinerary: true, flyToBounds: true }}
|
||||
></MapWithElevationMultiple>
|
||||
bind:activeTrail={activeTrailIndex}
|
||||
showInfoPopup={true}
|
||||
></MapWithElevationMaplibre>
|
||||
</div>
|
||||
<div class="min-w-0" class:hidden={showMap}>
|
||||
<TrailList trails={selectedList?.expand?.trails ?? []}></TrailList>
|
||||
|
||||
@@ -10,10 +10,16 @@
|
||||
} from "$lib/components/base/search.svelte";
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import Textarea from "$lib/components/base/textarea.svelte";
|
||||
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
||||
import type { Trail } from "$lib/models/trail.js";
|
||||
import { TrailShare } from "$lib/models/trail_share.js";
|
||||
import { lists_create, lists_update } from "$lib/stores/list_store.js";
|
||||
import { show_toast } from "$lib/stores/toast_store.js";
|
||||
import {
|
||||
trail_share_create,
|
||||
trail_share_index,
|
||||
} from "$lib/stores/trail_share_store.js";
|
||||
import { trails_show } from "$lib/stores/trail_store";
|
||||
import { getFileURL } from "$lib/util/file_util.js";
|
||||
import {
|
||||
@@ -21,21 +27,15 @@
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import {
|
||||
trail_share_create,
|
||||
trail_share_index,
|
||||
} from "$lib/stores/trail_share_store.js";
|
||||
import { TrailShare } from "$lib/models/trail_share.js";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
|
||||
export let data;
|
||||
|
||||
let previewURL = data.previewUrl ?? "";
|
||||
let searchDropdownItems: SearchItem[] = [];
|
||||
|
||||
let activeTrailIndex: number | null = null;
|
||||
let activeTrailIndex: number = -1;
|
||||
|
||||
let map: MapWithElevationMultiple;
|
||||
let map: MapWithElevationMaplibre;
|
||||
|
||||
let loading: boolean = false;
|
||||
|
||||
@@ -301,7 +301,9 @@
|
||||
trail.difficulty ?? "?",
|
||||
)}</span
|
||||
>
|
||||
<div class="grid grid-cols-2 mt-1 gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||
<div
|
||||
class="grid grid-cols-2 mt-1 gap-x-4 gap-y-2 text-sm text-gray-500"
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"
|
||||
></i>{formatDistance(trail.distance)}</span
|
||||
@@ -368,12 +370,11 @@
|
||||
>
|
||||
</form>
|
||||
<div id="trail-map" class="max-h-full">
|
||||
<MapWithElevationMultiple
|
||||
<MapWithElevationMaplibre
|
||||
trails={$form.expand?.trails ?? []}
|
||||
options={{ flyToBounds: true }}
|
||||
bind:activeTrailIndex
|
||||
bind:activeTrail={activeTrailIndex}
|
||||
bind:this={map}
|
||||
></MapWithElevationMultiple>
|
||||
></MapWithElevationMaplibre>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
type SearchItem,
|
||||
} from "$lib/components/base/search.svelte";
|
||||
import EmptyStateSearch from "$lib/components/empty_states/empty_state_search.svelte";
|
||||
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte";
|
||||
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
||||
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
||||
import type { Settings } from "$lib/models/settings";
|
||||
@@ -21,21 +21,13 @@
|
||||
trails_search_bounding_box,
|
||||
} from "$lib/stores/trail_store";
|
||||
import { country_codes } from "$lib/util/country_code_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type {
|
||||
GPX,
|
||||
LatLng,
|
||||
LatLngBoundsExpression,
|
||||
Map,
|
||||
Marker,
|
||||
} from "leaflet";
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import * as M from "maplibre-gl";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { slide } from "svelte/transition";
|
||||
let map: Map;
|
||||
let mapWithElevation: MapWithElevationMultiple;
|
||||
|
||||
let map: M.Map;
|
||||
let mapWithElevation: MapWithElevationMaplibre;
|
||||
let searchDropdownItems: SearchItem[] = [];
|
||||
|
||||
let showFilter: boolean = false;
|
||||
@@ -45,6 +37,8 @@
|
||||
const maxBoundingBox: TrailBoundingBox = $page.data.boundingBox;
|
||||
const settings: Settings = $page.data.settings;
|
||||
|
||||
const MIN_ZOOM = 6;
|
||||
|
||||
onMount(async () => {});
|
||||
|
||||
async function search(q: string) {
|
||||
@@ -93,23 +87,25 @@
|
||||
}
|
||||
|
||||
function handleSearchClick(item: SearchItem) {
|
||||
map.setView([item.value._geo.lat, item.value._geo.lng], 14);
|
||||
map.setCenter([item.value._geo.lng, item.value._geo.lat]);
|
||||
map.setZoom(14);
|
||||
}
|
||||
|
||||
async function searchTrails(northEast: LatLng, southWest: LatLng) {
|
||||
async function searchTrails(northEast: M.LngLat, southWest: M.LngLat) {
|
||||
const changes = await trails_search_bounding_box(
|
||||
northEast,
|
||||
southWest,
|
||||
filter,
|
||||
map.getZoom() > MIN_ZOOM
|
||||
);
|
||||
}
|
||||
|
||||
function handleTrailCardMouseEnter(trail: Trail) {
|
||||
mapWithElevation.openPopup(trail.id!)
|
||||
mapWithElevation.togglePopup(trail.id!);
|
||||
}
|
||||
|
||||
function handleTrailCardMouseLeave(trail: Trail) {
|
||||
mapWithElevation.closePopup(trail.id!)
|
||||
mapWithElevation.togglePopup(trail.id!);
|
||||
}
|
||||
|
||||
async function handleFilterUpdate(filter: TrailFilter) {
|
||||
@@ -136,29 +132,37 @@
|
||||
$page.url.searchParams.has("br_lat") &&
|
||||
$page.url.searchParams.has("br_lon")
|
||||
) {
|
||||
const boundingBox: LatLngBoundsExpression = [
|
||||
[parseFloat($page.url.searchParams.get("br_lat")!), parseFloat($page.url.searchParams.get("tl_lon")!)],
|
||||
[parseFloat($page.url.searchParams.get("tl_lat")!), parseFloat($page.url.searchParams.get("br_lon")!)],
|
||||
const boundingBox: M.LngLatBoundsLike = [
|
||||
[
|
||||
parseFloat($page.url.searchParams.get("br_lon")!),
|
||||
parseFloat($page.url.searchParams.get("tl_lat")!),
|
||||
],
|
||||
[
|
||||
parseFloat($page.url.searchParams.get("tl_lon")!),
|
||||
parseFloat($page.url.searchParams.get("br_lat")!),
|
||||
],
|
||||
];
|
||||
map.fitBounds(boundingBox);
|
||||
map.fitBounds(boundingBox, { animate: false });
|
||||
} else if (settings && settings.mapFocus == "trails") {
|
||||
const boundingBox: LatLngBoundsExpression = [
|
||||
[maxBoundingBox.max_lat, maxBoundingBox.min_lon],
|
||||
[maxBoundingBox.min_lat, maxBoundingBox.max_lon],
|
||||
const boundingBox: M.LngLatBoundsLike = [
|
||||
[maxBoundingBox.min_lon, maxBoundingBox.max_lat],
|
||||
[maxBoundingBox.max_lon, maxBoundingBox.min_lat],
|
||||
];
|
||||
map.fitBounds(boundingBox);
|
||||
map.fitBounds(boundingBox, { animate: false, padding: 32 });
|
||||
} else if (
|
||||
settings &&
|
||||
settings.mapFocus == "location" &&
|
||||
settings.location
|
||||
) {
|
||||
map.setView([settings.location.lat, settings.location.lon], 12);
|
||||
map.setCenter([settings.location.lon, settings.location.lat]);
|
||||
map.setZoom(12);
|
||||
} else {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
map.setView([lat, lon], 13);
|
||||
map.setCenter([lat, lon]);
|
||||
map.setZoom(12);
|
||||
},
|
||||
(error) => {
|
||||
console.error("Error getting user location:", error);
|
||||
@@ -235,14 +239,17 @@
|
||||
id="trail-map"
|
||||
class:hidden={!showMap && browser && window.innerWidth < 768}
|
||||
>
|
||||
<MapWithElevationMultiple
|
||||
<MapWithElevationMaplibre
|
||||
on:moveend={handleMapMove}
|
||||
on:init={handleMapInit}
|
||||
trails={$trails}
|
||||
options={{ flyToBounds: false }}
|
||||
showElevation={false}
|
||||
showInfoPopup={true}
|
||||
activeTrail={-1}
|
||||
minZoom={MIN_ZOOM}
|
||||
bind:map
|
||||
bind:this={mapWithElevation}
|
||||
></MapWithElevationMultiple>
|
||||
></MapWithElevationMaplibre>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
||||
<div id="trail-details" class="sticky top-[62px]">
|
||||
<MapWithElevationMaplibre trail={$trail} bind:markers></MapWithElevationMaplibre>
|
||||
<MapWithElevationMaplibre trails={[$trail]} bind:markers></MapWithElevationMaplibre>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<script>
|
||||
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
||||
import { onMount } from "svelte";
|
||||
export let data;
|
||||
|
||||
</script>
|
||||
|
||||
<MapWithElevationMaplibre trail={data.trail}></MapWithElevationMaplibre>
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { trails_show } from "$lib/stores/trail_store";
|
||||
|
||||
export const load = async ({ params, fetch }) => {
|
||||
const t: Trail = await trails_show("yesm2tqc6jok8jq", true, fetch)
|
||||
|
||||
|
||||
return { trail: t }
|
||||
};
|
||||
@@ -921,7 +921,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<MapWithElevationMaplibre
|
||||
trail={$form}
|
||||
trails={[$form]}
|
||||
drawing={drawingActive}
|
||||
bind:map
|
||||
on:click={(e) => handleMapClick(e.detail)}
|
||||
|
||||
21
web/static/styles/osm.json
Normal file
21
web/static/styles/osm.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"osm-tiles": {
|
||||
"type": "raster",
|
||||
"tiles": [
|
||||
"https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
],
|
||||
"tileSize": 256
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "osm-tiles",
|
||||
"type": "raster",
|
||||
"source": "osm-tiles",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 19
|
||||
}
|
||||
]
|
||||
}
|
||||
21
web/static/styles/otm.json
Normal file
21
web/static/styles/otm.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"osm-tiles": {
|
||||
"type": "raster",
|
||||
"tiles": [
|
||||
"https://tile.opentopomap.org/{z}/{x}/{y}.png"
|
||||
],
|
||||
"tileSize": 256
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "osm-tiles",
|
||||
"type": "raster",
|
||||
"source": "osm-tiles",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 19
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user