From 19415f7bcb3551cd5e0cbf4ddc6ce4c2ed10b799 Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Tue, 10 Sep 2024 17:44:18 +0200 Subject: [PATCH] adds mapwithelevationmultiple to map page --- web/src/css/components.css | 2 +- .../trail/map_with_elevation.svelte | 2 +- .../trail/map_with_elevation_multiple.svelte | 39 ++- .../libs/leaflet-gpxgroup.js | 63 +++-- web/src/routes/lists/+page.svelte | 2 +- web/src/routes/map/+page.svelte | 264 ++++-------------- 6 files changed, 129 insertions(+), 243 deletions(-) diff --git a/web/src/css/components.css b/web/src/css/components.css index 515a97fd..6771ab31 100644 --- a/web/src/css/components.css +++ b/web/src/css/components.css @@ -40,7 +40,6 @@ .tooltip { position: relative; - z-index: 10; } .tooltip:before, @@ -52,6 +51,7 @@ transform: translate3d(0, -10px, 0); transition: all .15s ease-in-out; + z-index: 10; } .tooltip:before { diff --git a/web/src/lib/components/trail/map_with_elevation.svelte b/web/src/lib/components/trail/map_with_elevation.svelte index d5295445..65c731a2 100644 --- a/web/src/lib/components/trail/map_with_elevation.svelte +++ b/web/src/lib/components/trail/map_with_elevation.svelte @@ -214,7 +214,7 @@ } -
+
{ - - const trail = trails.find((t) => gpxGroup?._hashCode(t.expand.gpx_data) == route.options.hash) + const trail = trails.find( + (t) => + gpxGroup?._hashCode(t.expand.gpx_data) == + route.options.hash, + ); if (!trail) { return; @@ -275,6 +279,14 @@ gpxGroup.select(index); } + export function openPopup(index: number) { + gpxGroup.openPopup(index); + } + + export function closePopup(index: number) { + gpxGroup.closePopup(index); + } + function switchHotline(metric: "altitude" | "slope" | "speed" | false) { if (metric === selectedMetric) { return; @@ -293,10 +305,10 @@ } -
+
@@ -314,8 +326,5 @@
-
- -
-
+
diff --git a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js index fc8743f8..3449803d 100644 --- a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js +++ b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js @@ -147,6 +147,23 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ this.setSelection(this._routes[index]) }, + + openPopup(index) { + if (index > this._routes.length - 1) { + return + } + + this._routes[index].openPopup(); + }, + + closePopup(index) { + if (index > this._routes.length - 1) { + return + } + + this._routes[index].closePopup(); + }, + _addTrack: function (track) { if (track instanceof Object) { this._loadGeoJSON(track); @@ -156,7 +173,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ } }, - _hashCode: (s) => s.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0), + _hashCode: (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0), clear: function () { this._elevation.clear() @@ -181,8 +198,8 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ }, _loadGeoJSON: function (geojson, hash, fallbackName) { - if (geojson) { - geojson.hash = hash + if (geojson) { + geojson.hash = hash geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName; this._loadRoute(geojson); } @@ -190,14 +207,13 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ _loadRoute: function (data) { if (!data) return; - var line_style = { - color: this._uniqueColors(this._tracks.length)[this._count++], + color: this._hashToColor(data.hash), opacity: 0.75, weight: 5, distanceMarkers: this.options.distanceMarkers_options, }; - + var route = L.geoJson(data, { name: data.name || '', style: (feature) => line_style, @@ -214,7 +230,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ route.eachLayer((layer) => this._onEachRouteLayer(route, layer)); - + this._onEachRouteLoaded(route); }); @@ -239,9 +255,9 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ html: '', className: 'end-icon' }); - const latlngs = polyline.getLatLngs(); + const latlngs = polyline.getLatLngs(); - if (this._loadedCount == 0) { + if (this._loadedCount == 0 || !this.options.itinerary) { L.marker(latlngs[0], { icon: startIcon }).addTo(this._markers) } @@ -333,18 +349,25 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ } }, - _uniqueColors: function (count) { - return count === 1 ? ['#0058ca'] : new Array(count).fill(null).map((_, i) => this._hsvToHex(i * (1 / count), 1, 0.7)); - }, + _hashToColor(hash) { + // Ensure hash is a positive integer + hash = Math.abs(hash); - _hsvToHex: function (h, s, v) { - var i = Math.floor(h * 6); - var f = h * 6 - i; - var p = v * (1 - s); - var q = v * (1 - f * s); - var t = v * (1 - (1 - f) * s); - var rgb = { 0: [v, t, p], 1: [q, v, p], 2: [p, v, t], 3: [p, q, v], 4: [t, p, v], 5: [v, p, q] }[i % 6]; - return rgb.map(d => d * 255).reduce((hex, byte) => hex + ((byte >> 4) & 0x0F).toString(16) + (byte & 0x0F).toString(16), "#"); + // Define the maximum brightness (e.g., 200 ensures the color is not too light) + const maxBrightness = 200; + + // Convert the hash to a color in a reduced RGB range to prevent light colors + const r = (hash >> 16) & 0xFF; // Extract the red component + const g = (hash >> 8) & 0xFF; // Extract the green component + const b = hash & 0xFF; // Extract the blue component + + // Adjust the RGB values to be within the allowed range (0 to maxBrightness) + const red = Math.floor((r / 255) * maxBrightness); + const green = Math.floor((g / 255) * maxBrightness); + const blue = Math.floor((b / 255) * maxBrightness); + + // Format as HEX color + return `#${(1 << 24 | red << 16 | green << 8 | blue).toString(16).slice(1).toUpperCase()}`; }, removeFrom: function (map) { diff --git a/web/src/routes/lists/+page.svelte b/web/src/routes/lists/+page.svelte index 0ead40e5..f99674ea 100644 --- a/web/src/routes/lists/+page.svelte +++ b/web/src/routes/lists/+page.svelte @@ -135,7 +135,7 @@ {/each}
-
diff --git a/web/src/routes/map/+page.svelte b/web/src/routes/map/+page.svelte index 23cf4ea2..18cbf5c7 100644 --- a/web/src/routes/map/+page.svelte +++ b/web/src/routes/map/+page.svelte @@ -6,6 +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 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"; @@ -19,21 +20,12 @@ trails, trails_search_bounding_box, } from "$lib/stores/trail_store"; - import { currentUser } from "$lib/stores/user_store"; import { country_codes } from "$lib/util/country_code_util"; - import { getFileURL } from "$lib/util/file_util"; - import { - formatDistance, - formatElevation, - formatTimeHHMM, - } from "$lib/util/format_util"; import "$lib/vendor/leaflet-elevation/src/index.css"; import type { GPX, - Icon, LatLng, LatLngBoundsExpression, - LeafletEvent, Map, Marker, } from "leaflet"; @@ -42,11 +34,8 @@ import { onMount } from "svelte"; import { _ } from "svelte-i18n"; import { slide } from "svelte/transition"; - let L: any; let map: Map; - let gpxLayers: Record = {}; - let startMarkers: Record = {}; - + let mapWithElevation: MapWithElevationMultiple; let searchDropdownItems: SearchItem[] = []; let showFilter: boolean = false; @@ -56,103 +45,7 @@ const maxBoundingBox: TrailBoundingBox = $page.data.boundingBox; const settings: Settings = $page.data.settings; - onMount(async () => { - L = (await import("leaflet")).default; - await import("leaflet-gpx"); - await import("leaflet.awesome-markers"); - - map = L.map("map").setView([0, 0], 4); - map.attributionControl.setPrefix(false); - - const baseLayer = L.tileLayer( - "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - { - attribution: "© OpenStreetMap contributors", - }, - ); - - const topoLayer = L.tileLayer( - "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png ", - { - attribution: "© OpenStreetMap contributors", - }, - ); - - const baseMaps = { - OpenStreetMaps: baseLayer, - OpenTopoMaps: topoLayer, - }; - - L.control.layers(baseMaps).addTo(map); - - switch (localStorage.getItem("layer")) { - case "OpenTopoMaps": - topoLayer.addTo(map); - break; - default: - baseLayer.addTo(map); - break; - } - - map.on("baselayerchange", function (e) { - localStorage.setItem("layer", e.name); - }); - - map.on("moveend", async (e) => { - const bounds = map.getBounds(); - await searchTrails(bounds.getNorthEast(), bounds.getSouthWest()); - - $page.url.searchParams.set("tl_lat", bounds.getNorth().toString()); - $page.url.searchParams.set("tl_lon", bounds.getEast().toString()); - $page.url.searchParams.set("br_lat", bounds.getSouth().toString()); - $page.url.searchParams.set("br_lon", bounds.getWest().toString()); - - goto(`?${$page.url.searchParams.toString()}`); - }); - - const lat = $page.url.searchParams.get("lat"); - const lon = $page.url.searchParams.get("lon"); - - const tl_lat = $page.url.searchParams.get("tl_lat"); - const tl_lon = $page.url.searchParams.get("tl_lon"); - const br_lat = $page.url.searchParams.get("br_lat"); - const br_lon = $page.url.searchParams.get("br_lon"); - - if (lat && lon) { - map.setView([parseFloat(lat), parseFloat(lon)], 12); - } else if (tl_lat && tl_lon && br_lat && br_lon) { - const boundingBox: LatLngBoundsExpression = [ - [parseFloat(tl_lat), parseFloat(tl_lon)], - [parseFloat(br_lat), parseFloat(br_lon)], - ]; - - map.fitBounds(boundingBox); - } else if (settings && settings.mapFocus == "trails") { - const boundingBox: LatLngBoundsExpression = [ - [maxBoundingBox.max_lat, maxBoundingBox.min_lon], - [maxBoundingBox.min_lat, maxBoundingBox.max_lon], - ]; - map.fitBounds(boundingBox); - } else if ( - settings && - settings.mapFocus == "location" && - settings.location - ) { - map.setView([settings.location.lat, settings.location.lon], 12); - } else { - navigator.geolocation.getCurrentPosition( - (position) => { - const lat = position.coords.latitude; - const lon = position.coords.longitude; - - map.setView([lat, lon], 13); - }, - (error) => { - console.error("Error getting user location:", error); - }, - ); - } - }); + onMount(async () => {}); async function search(q: string) { const r = await fetch("/api/v1/search/multi", { @@ -209,103 +102,59 @@ southWest, filter, ); - - for (const newTrail of changes.added) { - const gpxLayer = await addGPXLayer(newTrail); - - if (gpxLayer) { - gpxLayers[newTrail.id!] = gpxLayer; - } - } - - for (const deletedTrail of changes.deleted) { - map.removeLayer(gpxLayers[deletedTrail.id!]); - delete gpxLayers[deletedTrail.id!]; - delete startMarkers[deletedTrail.id!]; - } } - function addGPXLayer(trail: Trail) { - return new Promise(function (resolve, reject) { - if (!trail.expand.gpx_data) { - reject(); - } - - const thumbnail = trail.photos.length - ? getFileURL(trail, trail.photos[trail.thumbnail]) - : "/imgs/default_thumbnail.webp"; - const gpxLayer = new L.GPX(trail.expand.gpx_data!, { - async: true, - polyline_options: { - className: "lightblue-theme elevation-polyline", - weight: 5, - }, - gpx_options: { - parseElements: ["track", "route"], - }, - marker_options: { - startIcon: L.AwesomeMarkers.icon({ - icon: "circle-half-stroke", - prefix: "fa", - markerColor: "cadetblue", - iconColor: "white", - }) as Icon, - startIconUrl: "", - endIconUrl: "", - shadowUrl: "", - }, - }) - .on("addpoint", function (e: any) { - if (e.point_type === "start") { - const marker: Marker = e.point as Marker; - startMarkers[trail.id!] = marker; - marker.bindPopup( - ` -
  • -
    -
    -
    -

    ${trail.name}

    -
    - ${trail.location ? `
    ${trail.location}
    ` : ""} -
    ${$_(trail.difficulty as string)}
    -
    -
    ${formatDistance( - trail.distance, - )} ${formatElevation( - trail.elevation_gain, - )} ${formatTimeHHMM( - trail.duration, - )}
    -
    -
  • -
    `, - ); - } - }) - .on("loaded", function (e: LeafletEvent) { - resolve(gpxLayer); - }) - .on("error", reject) - .addTo(map); - }); + function handleTrailCardMouseEnter(index: number) { + mapWithElevation.openPopup(index) } - function handleTrailCardMouseEnter(trail: Trail) { - const marker: Marker = startMarkers[trail.id!]; - marker?.openPopup(); - } - - function handleTrailCardMouseLeave(trail: Trail) { - const marker: Marker = startMarkers[trail.id!]; - marker?.closePopup(); + function handleTrailCardMouseLeave(index: number) { + mapWithElevation.closePopup(index) } async function handleFilterUpdate(filter: TrailFilter) { const bounds = map.getBounds(); await searchTrails(bounds.getNorthEast(), bounds.getSouthWest()); } + + async function handleMapMove() { + const bounds = map.getBounds(); + await searchTrails(bounds.getNorthEast(), bounds.getSouthWest()); + + $page.url.searchParams.set("tl_lat", bounds.getNorth().toString()); + $page.url.searchParams.set("tl_lon", bounds.getEast().toString()); + $page.url.searchParams.set("br_lat", bounds.getSouth().toString()); + $page.url.searchParams.set("br_lon", bounds.getWest().toString()); + + goto(`?${$page.url.searchParams.toString()}`); + } + + function handleMapInit() { + if (settings && settings.mapFocus == "trails") { + const boundingBox: LatLngBoundsExpression = [ + [maxBoundingBox.max_lat, maxBoundingBox.min_lon], + [maxBoundingBox.min_lat, maxBoundingBox.max_lon], + ]; + map.fitBounds(boundingBox); + } else if ( + settings && + settings.mapFocus == "location" && + settings.location + ) { + map.setView([settings.location.lat, settings.location.lon], 12); + } else { + navigator.geolocation.getCurrentPosition( + (position) => { + const lat = position.coords.latitude; + const lon = position.coords.longitude; + map.setView([lat, lon], 13); + }, + (error) => { + console.error("Error getting user location:", error); + }, + ); + } + } @@ -357,22 +206,27 @@ {#if $trails.length == 0} {/if} - {#each $trails as trail} + {#each $trails as trail,i} handleTrailCardMouseEnter(trail)} - on:mouseleave={() => handleTrailCardMouseLeave(trail)} + on:mouseenter={() => handleTrailCardMouseEnter(i)} + on:mouseleave={() => handleTrailCardMouseLeave(i)} > {/each} {/if}
    -
    +
    + +