mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	refactor map popups and add inspect trackpoint feature
This commit is contained in:
		@@ -20,7 +20,7 @@
 | 
			
		||||
		Construction
 | 
			
		||||
	} from 'lucide-svelte';
 | 
			
		||||
	import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
 | 
			
		||||
	import { _, locale } from 'svelte-i18n';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import {
 | 
			
		||||
		getCadenceWithUnits,
 | 
			
		||||
		getConvertedDistance,
 | 
			
		||||
@@ -36,10 +36,10 @@
 | 
			
		||||
		getVelocityWithUnits
 | 
			
		||||
	} from '$lib/units';
 | 
			
		||||
	import type { Writable } from 'svelte/store';
 | 
			
		||||
	import { DateFormatter } from '@internationalized/date';
 | 
			
		||||
	import type { GPXStatistics } from 'gpx';
 | 
			
		||||
	import { settings } from '$lib/db';
 | 
			
		||||
	import { mode } from 'mode-watcher';
 | 
			
		||||
	import { df } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
	export let gpxStatistics: Writable<GPXStatistics>;
 | 
			
		||||
	export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
 | 
			
		||||
@@ -49,15 +49,6 @@
 | 
			
		||||
 | 
			
		||||
	const { distanceUnits, velocityUnits, temperatureUnits } = settings;
 | 
			
		||||
 | 
			
		||||
	let df: DateFormatter;
 | 
			
		||||
 | 
			
		||||
	$: if ($locale) {
 | 
			
		||||
		df = new DateFormatter($locale, {
 | 
			
		||||
			dateStyle: 'medium',
 | 
			
		||||
			timeStyle: 'medium'
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let canvas: HTMLCanvasElement;
 | 
			
		||||
	let overlay: HTMLCanvasElement;
 | 
			
		||||
	let chart: Chart;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								website/src/lib/components/MapPopup.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								website/src/lib/components/MapPopup.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
<svelte:options accessors />
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { TrackPoint, Waypoint } from 'gpx';
 | 
			
		||||
	import type { Writable } from 'svelte/store';
 | 
			
		||||
	import WaypointPopup from '$lib/components/gpx-layer/WaypointPopup.svelte';
 | 
			
		||||
	import TrackpointPopup from '$lib/components/gpx-layer/TrackpointPopup.svelte';
 | 
			
		||||
	import OverpassPopup from '$lib/components/layer-control/OverpassPopup.svelte';
 | 
			
		||||
	import type { PopupItem } from './MapPopup';
 | 
			
		||||
 | 
			
		||||
	export let item: Writable<PopupItem | null>;
 | 
			
		||||
	export let container: HTMLDivElement | null = null;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div bind:this={container}>
 | 
			
		||||
	{#if $item}
 | 
			
		||||
		{#if $item.item instanceof Waypoint}
 | 
			
		||||
			<WaypointPopup waypoint={$item} />
 | 
			
		||||
		{:else if $item.item instanceof TrackPoint}
 | 
			
		||||
			<TrackpointPopup trackpoint={$item} />
 | 
			
		||||
		{:else}
 | 
			
		||||
			<OverpassPopup poi={$item} />
 | 
			
		||||
		{/if}
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										78
									
								
								website/src/lib/components/MapPopup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								website/src/lib/components/MapPopup.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import { TrackPoint, Waypoint } from "gpx";
 | 
			
		||||
import mapboxgl from "mapbox-gl";
 | 
			
		||||
import { tick } from "svelte";
 | 
			
		||||
import { get, writable, type Writable } from "svelte/store";
 | 
			
		||||
import MapPopupComponent from "./MapPopup.svelte";
 | 
			
		||||
 | 
			
		||||
export type PopupItem<T = Waypoint | TrackPoint | any> = {
 | 
			
		||||
    item: T;
 | 
			
		||||
    fileId?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class MapPopup {
 | 
			
		||||
    map: mapboxgl.Map;
 | 
			
		||||
    popup: mapboxgl.Popup;
 | 
			
		||||
    item: Writable<PopupItem | null> = writable(null);
 | 
			
		||||
    maybeHideBinded = this.maybeHide.bind(this);
 | 
			
		||||
 | 
			
		||||
    constructor(map: mapboxgl.Map, options?: mapboxgl.PopupOptions) {
 | 
			
		||||
        this.map = map;
 | 
			
		||||
        this.popup = new mapboxgl.Popup(options);
 | 
			
		||||
 | 
			
		||||
        let component = new MapPopupComponent({
 | 
			
		||||
            target: document.body,
 | 
			
		||||
            props: {
 | 
			
		||||
                item: this.item
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        tick().then(() => this.popup.setDOMContent(component.container));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setItem(item: PopupItem | null) {
 | 
			
		||||
        this.item.set(item);
 | 
			
		||||
        if (item === null) {
 | 
			
		||||
            this.hide();
 | 
			
		||||
        } else {
 | 
			
		||||
            tick().then(() => this.show());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    show() {
 | 
			
		||||
        const i = get(this.item);
 | 
			
		||||
        if (i === null) {
 | 
			
		||||
            this.hide();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.popup.setLngLat(this.getCoordinates()).addTo(this.map);
 | 
			
		||||
        this.map.on('mousemove', this.maybeHideBinded);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    maybeHide(e: mapboxgl.MapMouseEvent) {
 | 
			
		||||
        const i = get(this.item);
 | 
			
		||||
        if (i === null) {
 | 
			
		||||
            this.hide();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (this.map.project(this.getCoordinates()).dist(this.map.project(e.lngLat)) > 60) {
 | 
			
		||||
            this.hide();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hide() {
 | 
			
		||||
        this.popup.remove();
 | 
			
		||||
        this.map.off('mousemove', this.maybeHideBinded);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    remove() {
 | 
			
		||||
        this.popup.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCoordinates() {
 | 
			
		||||
        const i = get(this.item);
 | 
			
		||||
        if (i === null) {
 | 
			
		||||
            return new mapboxgl.LngLat(0, 0);
 | 
			
		||||
        }
 | 
			
		||||
        return (i.item instanceof Waypoint || i.item instanceof TrackPoint) ? i.item.getCoordinates() : new mapboxgl.LngLat(i.item.lon, i.item.lat);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -49,17 +49,11 @@
 | 
			
		||||
		gpxLayers,
 | 
			
		||||
		map
 | 
			
		||||
	} from '$lib/stores';
 | 
			
		||||
	import {
 | 
			
		||||
		GPXTreeElement,
 | 
			
		||||
		Track,
 | 
			
		||||
		TrackSegment,
 | 
			
		||||
		type AnyGPXTreeElement,
 | 
			
		||||
		Waypoint,
 | 
			
		||||
		GPXFile
 | 
			
		||||
	} from 'gpx';
 | 
			
		||||
	import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import MetadataDialog from './MetadataDialog.svelte';
 | 
			
		||||
	import StyleDialog from './StyleDialog.svelte';
 | 
			
		||||
	import { waypointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
 | 
			
		||||
 | 
			
		||||
	export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
 | 
			
		||||
	export let item: ListItem;
 | 
			
		||||
@@ -179,7 +173,7 @@
 | 
			
		||||
						if (layer && file) {
 | 
			
		||||
							let waypoint = file.wpt[item.getWaypointIndex()];
 | 
			
		||||
							if (waypoint) {
 | 
			
		||||
								layer.showWaypointPopup(waypoint);
 | 
			
		||||
								waypointPopup?.setItem({ item: waypoint, fileId: item.getFileId() });
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
@@ -188,7 +182,7 @@
 | 
			
		||||
					if (item instanceof ListWaypointItem) {
 | 
			
		||||
						let layer = gpxLayers.get(item.getFileId());
 | 
			
		||||
						if (layer) {
 | 
			
		||||
							layer.hideWaypointPopup();
 | 
			
		||||
							waypointPopup?.setItem(null);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,13 @@ import { currentTool, map, Tool } from "$lib/stores";
 | 
			
		||||
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
 | 
			
		||||
import { get, type Readable } from "svelte/store";
 | 
			
		||||
import mapboxgl from "mapbox-gl";
 | 
			
		||||
import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointPopup";
 | 
			
		||||
import { waypointPopup, deleteWaypoint, trackpointPopup } from "./GPXLayerPopup";
 | 
			
		||||
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
 | 
			
		||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
 | 
			
		||||
import type { Waypoint } from "gpx";
 | 
			
		||||
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
 | 
			
		||||
import { getClosestLinePoint, getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
 | 
			
		||||
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
 | 
			
		||||
import { MapPin, Square } from "lucide-static";
 | 
			
		||||
import { getSymbolKey, symbols } from "$lib/assets/symbols";
 | 
			
		||||
import { tick } from "svelte";
 | 
			
		||||
 | 
			
		||||
const colors = [
 | 
			
		||||
    '#ff0000',
 | 
			
		||||
@@ -44,6 +42,31 @@ function decrementColor(color: string) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const inspectKey = 'Shift';
 | 
			
		||||
let inspectKeyDown: KeyDown | null = null;
 | 
			
		||||
class KeyDown {
 | 
			
		||||
    key: string;
 | 
			
		||||
    down: boolean = false;
 | 
			
		||||
    constructor(key: string) {
 | 
			
		||||
        this.key = key;
 | 
			
		||||
        document.addEventListener('keydown', this.onKeyDown);
 | 
			
		||||
        document.addEventListener('keyup', this.onKeyUp);
 | 
			
		||||
    }
 | 
			
		||||
    onKeyDown = (e: KeyboardEvent) => {
 | 
			
		||||
        if (e.key === this.key) {
 | 
			
		||||
            this.down = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    onKeyUp = (e: KeyboardEvent) => {
 | 
			
		||||
        if (e.key === this.key) {
 | 
			
		||||
            this.down = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    isDown() {
 | 
			
		||||
        return this.down;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
 | 
			
		||||
    let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
 | 
			
		||||
    return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
 | 
			
		||||
@@ -81,9 +104,9 @@ export class GPXLayer {
 | 
			
		||||
    updateBinded: () => void = this.update.bind(this);
 | 
			
		||||
    layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
 | 
			
		||||
    layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
 | 
			
		||||
    layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
 | 
			
		||||
    layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
 | 
			
		||||
    layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
 | 
			
		||||
    maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this);
 | 
			
		||||
 | 
			
		||||
    constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
 | 
			
		||||
        this.map = map;
 | 
			
		||||
@@ -114,6 +137,10 @@ export class GPXLayer {
 | 
			
		||||
        this.draggable = get(currentTool) === Tool.WAYPOINT;
 | 
			
		||||
 | 
			
		||||
        this.map.on('style.import.load', this.updateBinded);
 | 
			
		||||
 | 
			
		||||
        if (inspectKeyDown === null) {
 | 
			
		||||
            inspectKeyDown = new KeyDown(inspectKey);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update() {
 | 
			
		||||
@@ -158,6 +185,7 @@ export class GPXLayer {
 | 
			
		||||
                this.map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
 | 
			
		||||
                this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
 | 
			
		||||
                this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
 | 
			
		||||
                this.map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (get(directionMarkers)) {
 | 
			
		||||
@@ -225,11 +253,11 @@ export class GPXLayer {
 | 
			
		||||
                    }).setLngLat(waypoint.getCoordinates());
 | 
			
		||||
                    Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
 | 
			
		||||
                    let dragEndTimestamp = 0;
 | 
			
		||||
                    marker.getElement().addEventListener('mouseover', (e) => {
 | 
			
		||||
                    marker.getElement().addEventListener('mousemove', (e) => {
 | 
			
		||||
                        if (marker._isDragging) {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        this.showWaypointPopup(marker._waypoint);
 | 
			
		||||
                        waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
                    });
 | 
			
		||||
                    marker.getElement().addEventListener('click', (e) => {
 | 
			
		||||
@@ -252,14 +280,14 @@ export class GPXLayer {
 | 
			
		||||
                        } else if (get(currentTool) === Tool.WAYPOINT) {
 | 
			
		||||
                            selectedWaypoint.set([marker._waypoint, this.fileId]);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            this.showWaypointPopup(marker._waypoint);
 | 
			
		||||
                            waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
 | 
			
		||||
                        }
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
                    });
 | 
			
		||||
                    marker.on('dragstart', () => {
 | 
			
		||||
                        setGrabbingCursor();
 | 
			
		||||
                        marker.getElement().style.cursor = 'grabbing';
 | 
			
		||||
                        this.hideWaypointPopup();
 | 
			
		||||
                        waypointPopup?.hide();
 | 
			
		||||
                    });
 | 
			
		||||
                    marker.on('dragend', (e) => {
 | 
			
		||||
                        resetCursor();
 | 
			
		||||
@@ -308,6 +336,7 @@ export class GPXLayer {
 | 
			
		||||
            this.map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
 | 
			
		||||
            this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
 | 
			
		||||
            this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
 | 
			
		||||
            this.map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
 | 
			
		||||
            this.map.off('style.import.load', this.updateBinded);
 | 
			
		||||
 | 
			
		||||
            if (this.map.getLayer(this.fileId + '-direction')) {
 | 
			
		||||
@@ -354,6 +383,19 @@ export class GPXLayer {
 | 
			
		||||
        resetCursor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    layerOnMouseMove(e: any) {
 | 
			
		||||
        if (inspectKeyDown?.isDown()) {
 | 
			
		||||
            let trackIndex = e.features[0].properties.trackIndex;
 | 
			
		||||
            let segmentIndex = e.features[0].properties.segmentIndex;
 | 
			
		||||
 | 
			
		||||
            const file = get(this.file)?.file;
 | 
			
		||||
            if (file) {
 | 
			
		||||
                const closest = getClosestLinePoint(file.trk[trackIndex].trkseg[segmentIndex].trkpt, { lat: e.lngLat.lat, lon: e.lngLat.lng });
 | 
			
		||||
                trackpointPopup?.setItem({ item: closest, fileId: this.fileId });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    layerOnClick(e: any) {
 | 
			
		||||
        if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
 | 
			
		||||
            return;
 | 
			
		||||
@@ -392,48 +434,6 @@ export class GPXLayer {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showWaypointPopup(waypoint: Waypoint) {
 | 
			
		||||
        if (get(currentPopupWaypoint) !== null) {
 | 
			
		||||
            this.hideWaypointPopup();
 | 
			
		||||
        } else if (waypoint === get(currentPopupWaypoint)?.[0]) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let marker = this.markers[waypoint._data.index];
 | 
			
		||||
        if (marker) {
 | 
			
		||||
            currentPopupWaypoint.set([waypoint, this.fileId]);
 | 
			
		||||
            tick().then(() => {
 | 
			
		||||
                // Show popup once the content component has been rendered
 | 
			
		||||
                marker.setPopup(waypointPopup);
 | 
			
		||||
                marker.togglePopup();
 | 
			
		||||
                this.map.on('mousemove', this.maybeHideWaypointPopupBinded);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    maybeHideWaypointPopup(e: any) {
 | 
			
		||||
        let waypoint = get(currentPopupWaypoint)?.[0];
 | 
			
		||||
        if (waypoint) {
 | 
			
		||||
            let marker = this.markers[waypoint._data.index];
 | 
			
		||||
            if (marker) {
 | 
			
		||||
                if (this.map.project(marker.getLngLat()).dist(this.map.project(e.lngLat)) > 60) {
 | 
			
		||||
                    this.hideWaypointPopup();
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                this.hideWaypointPopup();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hideWaypointPopup() {
 | 
			
		||||
        let waypoint = get(currentPopupWaypoint)?.[0];
 | 
			
		||||
        if (waypoint) {
 | 
			
		||||
            let marker = this.markers[waypoint._data.index];
 | 
			
		||||
            marker?.getPopup()?.remove();
 | 
			
		||||
            currentPopupWaypoint.set(null);
 | 
			
		||||
            this.map.off('mousemove', this.maybeHideWaypointPopupBinded);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getGeoJSON(): GeoJSON.FeatureCollection {
 | 
			
		||||
        let file = get(this.file)?.file;
 | 
			
		||||
        if (!file) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								website/src/lib/components/gpx-layer/GPXLayerPopup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								website/src/lib/components/gpx-layer/GPXLayerPopup.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import { dbUtils } from "$lib/db";
 | 
			
		||||
import { MapPopup } from "$lib/components/MapPopup";
 | 
			
		||||
 | 
			
		||||
export let waypointPopup: MapPopup | null = null;
 | 
			
		||||
export let trackpointPopup: MapPopup | null = null;
 | 
			
		||||
 | 
			
		||||
export function createPopups(map: mapboxgl.Map) {
 | 
			
		||||
    removePopups();
 | 
			
		||||
    waypointPopup = new MapPopup(map, {
 | 
			
		||||
        closeButton: false,
 | 
			
		||||
        focusAfterOpen: false,
 | 
			
		||||
        maxWidth: undefined,
 | 
			
		||||
        offset: {
 | 
			
		||||
            'top': [0, 0],
 | 
			
		||||
            'top-left': [0, 0],
 | 
			
		||||
            'top-right': [0, 0],
 | 
			
		||||
            'bottom': [0, -30],
 | 
			
		||||
            'bottom-left': [0, -30],
 | 
			
		||||
            'bottom-right': [0, -30],
 | 
			
		||||
            'left': [10, -15],
 | 
			
		||||
            'right': [-10, -15],
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
    trackpointPopup = new MapPopup(map, {
 | 
			
		||||
        closeButton: false,
 | 
			
		||||
        focusAfterOpen: false,
 | 
			
		||||
        maxWidth: undefined,
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removePopups() {
 | 
			
		||||
    if (waypointPopup !== null) {
 | 
			
		||||
        waypointPopup.remove();
 | 
			
		||||
        waypointPopup = null;
 | 
			
		||||
    }
 | 
			
		||||
    if (trackpointPopup !== null) {
 | 
			
		||||
        trackpointPopup.remove();
 | 
			
		||||
        trackpointPopup = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function deleteWaypoint(fileId: string, waypointIndex: number) {
 | 
			
		||||
    dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { map, gpxLayers } from '$lib/stores';
 | 
			
		||||
	import { GPXLayer } from './GPXLayer';
 | 
			
		||||
	import WaypointPopup from './WaypointPopup.svelte';
 | 
			
		||||
	import { fileObservers } from '$lib/db';
 | 
			
		||||
	import { DistanceMarkers } from './DistanceMarkers';
 | 
			
		||||
	import { StartEndMarkers } from './StartEndMarkers';
 | 
			
		||||
	import { onDestroy } from 'svelte';
 | 
			
		||||
	import { createPopups, removePopups } from './GPXLayerPopup';
 | 
			
		||||
 | 
			
		||||
	let distanceMarkers: DistanceMarkers | undefined = undefined;
 | 
			
		||||
	let startEndMarkers: StartEndMarkers | undefined = undefined;
 | 
			
		||||
@@ -35,6 +35,7 @@
 | 
			
		||||
		if (startEndMarkers) {
 | 
			
		||||
			startEndMarkers.remove();
 | 
			
		||||
		}
 | 
			
		||||
		createPopups($map);
 | 
			
		||||
		distanceMarkers = new DistanceMarkers($map);
 | 
			
		||||
		startEndMarkers = new StartEndMarkers($map);
 | 
			
		||||
	}
 | 
			
		||||
@@ -42,17 +43,14 @@
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		gpxLayers.forEach((layer) => layer.remove());
 | 
			
		||||
		gpxLayers.clear();
 | 
			
		||||
 | 
			
		||||
		removePopups();
 | 
			
		||||
		if (distanceMarkers) {
 | 
			
		||||
			distanceMarkers.remove();
 | 
			
		||||
			distanceMarkers = undefined;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (startEndMarkers) {
 | 
			
		||||
			startEndMarkers.remove();
 | 
			
		||||
			startEndMarkers = undefined;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<WaypointPopup />
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								website/src/lib/components/gpx-layer/TrackpointPopup.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								website/src/lib/components/gpx-layer/TrackpointPopup.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import type { TrackPoint } from 'gpx';
 | 
			
		||||
	import type { PopupItem } from '$lib/components/MapPopup';
 | 
			
		||||
	import * as Card from '$lib/components/ui/card';
 | 
			
		||||
	import WithUnits from '$lib/components/WithUnits.svelte';
 | 
			
		||||
	import { Compass, Mountain, Timer } from 'lucide-svelte';
 | 
			
		||||
	import { df } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
	export let trackpoint: PopupItem<TrackPoint>;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Card.Root class="border-none shadow-md text-base p-2">
 | 
			
		||||
	<Card.Header class="p-0">
 | 
			
		||||
		<Card.Title class="text-md"></Card.Title>
 | 
			
		||||
	</Card.Header>
 | 
			
		||||
	<Card.Content class="flex flex-col p-0 text-xs gap-1">
 | 
			
		||||
		<div class="flex flex-row items-center gap-1">
 | 
			
		||||
			<Compass size="14" />
 | 
			
		||||
			{trackpoint.item.getLatitude().toFixed(6)}° {trackpoint.item
 | 
			
		||||
				.getLongitude()
 | 
			
		||||
				.toFixed(6)}°
 | 
			
		||||
		</div>
 | 
			
		||||
		{#if trackpoint.item.ele !== undefined}
 | 
			
		||||
			<div class="flex flex-row items-center gap-1">
 | 
			
		||||
				<Mountain size="14" />
 | 
			
		||||
				<WithUnits value={trackpoint.item.ele} type="elevation" />
 | 
			
		||||
			</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
		{#if trackpoint.item.time}
 | 
			
		||||
			<div class="flex flex-row items-center gap-1">
 | 
			
		||||
				<Timer size="14" />
 | 
			
		||||
				{df.format(trackpoint.item.time)}
 | 
			
		||||
			</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</Card.Content>
 | 
			
		||||
</Card.Root>
 | 
			
		||||
@@ -2,23 +2,19 @@
 | 
			
		||||
	import * as Card from '$lib/components/ui/card';
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import Shortcut from '$lib/components/Shortcut.svelte';
 | 
			
		||||
	import { waypointPopup, currentPopupWaypoint, deleteWaypoint } from './WaypointPopup';
 | 
			
		||||
	import { deleteWaypoint } from './GPXLayerPopup';
 | 
			
		||||
	import WithUnits from '$lib/components/WithUnits.svelte';
 | 
			
		||||
	import { Dot, ExternalLink, Trash2 } from 'lucide-svelte';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { Tool, currentTool } from '$lib/stores';
 | 
			
		||||
	import { getSymbolKey, symbols } from '$lib/assets/symbols';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import sanitizeHtml from 'sanitize-html';
 | 
			
		||||
	import type { Waypoint } from 'gpx';
 | 
			
		||||
	import type { PopupItem } from '$lib/components/MapPopup';
 | 
			
		||||
 | 
			
		||||
	let popupElement: HTMLDivElement;
 | 
			
		||||
	export let waypoint: PopupItem<Waypoint>;
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		waypointPopup.setDOMContent(popupElement);
 | 
			
		||||
		popupElement.classList.remove('hidden');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	$: symbolKey = $currentPopupWaypoint ? getSymbolKey($currentPopupWaypoint[0].sym) : undefined;
 | 
			
		||||
	$: symbolKey = waypoint ? getSymbolKey(waypoint.item.sym) : undefined;
 | 
			
		||||
 | 
			
		||||
	function sanitize(text: string | undefined): string {
 | 
			
		||||
		if (text === undefined) {
 | 
			
		||||
@@ -34,68 +30,61 @@
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div bind:this={popupElement} class="hidden">
 | 
			
		||||
	{#if $currentPopupWaypoint}
 | 
			
		||||
		<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
 | 
			
		||||
			<Card.Header class="p-0">
 | 
			
		||||
				<Card.Title class="text-md">
 | 
			
		||||
					{#if $currentPopupWaypoint[0].link && $currentPopupWaypoint[0].link.attributes && $currentPopupWaypoint[0].link.attributes.href}
 | 
			
		||||
						<a href={$currentPopupWaypoint[0].link.attributes.href} target="_blank">
 | 
			
		||||
							{$currentPopupWaypoint[0].name ?? $currentPopupWaypoint[0].link.attributes.href}
 | 
			
		||||
							<ExternalLink size="12" class="inline-block mb-1.5" />
 | 
			
		||||
						</a>
 | 
			
		||||
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
 | 
			
		||||
	<Card.Header class="p-0">
 | 
			
		||||
		<Card.Title class="text-md">
 | 
			
		||||
			{#if waypoint.item.link && waypoint.item.link.attributes && waypoint.item.link.attributes.href}
 | 
			
		||||
				<a href={waypoint.item.link.attributes.href} target="_blank">
 | 
			
		||||
					{waypoint.item.name ?? waypoint.item.link.attributes.href}
 | 
			
		||||
					<ExternalLink size="12" class="inline-block mb-1.5" />
 | 
			
		||||
				</a>
 | 
			
		||||
			{:else}
 | 
			
		||||
				{waypoint.item.name ?? $_('gpx.waypoint.item')}
 | 
			
		||||
			{/if}
 | 
			
		||||
		</Card.Title>
 | 
			
		||||
	</Card.Header>
 | 
			
		||||
	<Card.Content class="flex flex-col p-0 text-sm">
 | 
			
		||||
		<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
 | 
			
		||||
			{#if symbolKey}
 | 
			
		||||
				<span>
 | 
			
		||||
					{#if symbols[symbolKey].icon}
 | 
			
		||||
						<svelte:component
 | 
			
		||||
							this={symbols[symbolKey].icon}
 | 
			
		||||
							size="12"
 | 
			
		||||
							class="inline-block mb-0.5"
 | 
			
		||||
						/>
 | 
			
		||||
					{:else}
 | 
			
		||||
						{$currentPopupWaypoint[0].name ?? $_('gpx.waypoint')}
 | 
			
		||||
						<span class="w-4 inline-block" />
 | 
			
		||||
					{/if}
 | 
			
		||||
				</Card.Title>
 | 
			
		||||
			</Card.Header>
 | 
			
		||||
			<Card.Content class="flex flex-col p-0 text-sm">
 | 
			
		||||
				<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
 | 
			
		||||
					{#if symbolKey}
 | 
			
		||||
						<span>
 | 
			
		||||
							{#if symbols[symbolKey].icon}
 | 
			
		||||
								<svelte:component
 | 
			
		||||
									this={symbols[symbolKey].icon}
 | 
			
		||||
									size="12"
 | 
			
		||||
									class="inline-block mb-0.5"
 | 
			
		||||
								/>
 | 
			
		||||
							{:else}
 | 
			
		||||
								<span class="w-4 inline-block" />
 | 
			
		||||
							{/if}
 | 
			
		||||
							{$_(`gpx.symbol.${symbolKey}`)}
 | 
			
		||||
						</span>
 | 
			
		||||
						<Dot size="16" />
 | 
			
		||||
					{/if}
 | 
			
		||||
					{$currentPopupWaypoint[0].getLatitude().toFixed(6)}° {$currentPopupWaypoint[0]
 | 
			
		||||
						.getLongitude()
 | 
			
		||||
						.toFixed(6)}°
 | 
			
		||||
					{#if $currentPopupWaypoint[0].ele !== undefined}
 | 
			
		||||
						<Dot size="16" />
 | 
			
		||||
						<WithUnits value={$currentPopupWaypoint[0].ele} type="elevation" />
 | 
			
		||||
					{/if}
 | 
			
		||||
				</div>
 | 
			
		||||
				{#if $currentPopupWaypoint[0].desc}
 | 
			
		||||
					<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].desc)}</span>
 | 
			
		||||
				{/if}
 | 
			
		||||
				{#if $currentPopupWaypoint[0].cmt && $currentPopupWaypoint[0].cmt !== $currentPopupWaypoint[0].desc}
 | 
			
		||||
					<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].cmt)}</span>
 | 
			
		||||
				{/if}
 | 
			
		||||
				{#if $currentTool === Tool.WAYPOINT}
 | 
			
		||||
					<Button
 | 
			
		||||
						class="mt-2 w-full px-2 py-1 h-8 justify-start"
 | 
			
		||||
						variant="outline"
 | 
			
		||||
						on:click={() =>
 | 
			
		||||
							deleteWaypoint($currentPopupWaypoint[1], $currentPopupWaypoint[0]._data.index)}
 | 
			
		||||
					>
 | 
			
		||||
						<Trash2 size="16" class="mr-1" />
 | 
			
		||||
						{$_('menu.delete')}
 | 
			
		||||
						<Shortcut shift={true} click={true} />
 | 
			
		||||
					</Button>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</Card.Content>
 | 
			
		||||
		</Card.Root>
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
					{$_(`gpx.symbol.${symbolKey}`)}
 | 
			
		||||
				</span>
 | 
			
		||||
				<Dot size="16" />
 | 
			
		||||
			{/if}
 | 
			
		||||
			{waypoint.item.getLatitude().toFixed(6)}° {waypoint.item.getLongitude().toFixed(6)}°
 | 
			
		||||
			{#if waypoint.item.ele !== undefined}
 | 
			
		||||
				<Dot size="16" />
 | 
			
		||||
				<WithUnits value={waypoint.item.ele} type="elevation" />
 | 
			
		||||
			{/if}
 | 
			
		||||
		</div>
 | 
			
		||||
		{#if waypoint.item.desc}
 | 
			
		||||
			<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.desc)}</span>
 | 
			
		||||
		{/if}
 | 
			
		||||
		{#if waypoint.item.cmt && waypoint.item.cmt !== waypoint.item.desc}
 | 
			
		||||
			<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.cmt)}</span>
 | 
			
		||||
		{/if}
 | 
			
		||||
		{#if $currentTool === Tool.WAYPOINT}
 | 
			
		||||
			<Button
 | 
			
		||||
				class="mt-2 w-full px-2 py-1 h-8 justify-start"
 | 
			
		||||
				variant="outline"
 | 
			
		||||
				on:click={() => deleteWaypoint(waypoint.fileId, waypoint.item._data.index)}
 | 
			
		||||
			>
 | 
			
		||||
				<Trash2 size="16" class="mr-1" />
 | 
			
		||||
				{$_('menu.delete')}
 | 
			
		||||
				<Shortcut shift={true} click={true} />
 | 
			
		||||
			</Button>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</Card.Content>
 | 
			
		||||
</Card.Root>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
	div :global(a) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
import { dbUtils } from "$lib/db";
 | 
			
		||||
import type { Waypoint } from "gpx";
 | 
			
		||||
import mapboxgl from "mapbox-gl";
 | 
			
		||||
import { writable } from "svelte/store";
 | 
			
		||||
 | 
			
		||||
export const currentPopupWaypoint = writable<[Waypoint, string] | null>(null);
 | 
			
		||||
 | 
			
		||||
export const waypointPopup = new mapboxgl.Popup({
 | 
			
		||||
    closeButton: false,
 | 
			
		||||
    focusAfterOpen: false,
 | 
			
		||||
    maxWidth: undefined,
 | 
			
		||||
    offset: {
 | 
			
		||||
        'top': [0, 0],
 | 
			
		||||
        'top-left': [0, 0],
 | 
			
		||||
        'top-right': [0, 0],
 | 
			
		||||
        'bottom': [0, -30],
 | 
			
		||||
        'bottom-left': [0, -30],
 | 
			
		||||
        'bottom-right': [0, -30],
 | 
			
		||||
        'left': [10, -15],
 | 
			
		||||
        'right': [-10, -15],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function deleteWaypoint(fileId: string, waypointIndex: number) {
 | 
			
		||||
    dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
 | 
			
		||||
}
 | 
			
		||||
@@ -13,7 +13,6 @@
 | 
			
		||||
	import { get, writable } from 'svelte/store';
 | 
			
		||||
	import { customBasemapUpdate, getLayers } from './utils';
 | 
			
		||||
	import { OverpassLayer } from './OverpassLayer';
 | 
			
		||||
	import OverpassPopup from './OverpassPopup.svelte';
 | 
			
		||||
 | 
			
		||||
	let container: HTMLDivElement;
 | 
			
		||||
	let overpassLayer: OverpassLayer;
 | 
			
		||||
@@ -34,7 +33,7 @@
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			let basemap = basemaps.hasOwnProperty($currentBasemap)
 | 
			
		||||
				? basemaps[$currentBasemap]
 | 
			
		||||
				: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
 | 
			
		||||
				: ($customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]);
 | 
			
		||||
			$map.removeImport('basemap');
 | 
			
		||||
			if (typeof basemap === 'string') {
 | 
			
		||||
				$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
 | 
			
		||||
@@ -211,8 +210,6 @@
 | 
			
		||||
	</div>
 | 
			
		||||
</CustomControl>
 | 
			
		||||
 | 
			
		||||
<OverpassPopup />
 | 
			
		||||
 | 
			
		||||
<svelte:window
 | 
			
		||||
	on:click={(e) => {
 | 
			
		||||
		if (open && !cancelEvents && !container.contains(e.target)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
import SphericalMercator from "@mapbox/sphericalmercator";
 | 
			
		||||
import { getLayers } from "./utils";
 | 
			
		||||
import mapboxgl from "mapbox-gl";
 | 
			
		||||
import { get, writable } from "svelte/store";
 | 
			
		||||
import { liveQuery } from "dexie";
 | 
			
		||||
import { db, settings } from "$lib/db";
 | 
			
		||||
import { overpassQueryData } from "$lib/assets/layers";
 | 
			
		||||
import { tick } from "svelte";
 | 
			
		||||
import { MapPopup } from "$lib/components/MapPopup";
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
    currentOverpassQueries
 | 
			
		||||
@@ -15,15 +14,6 @@ const mercator = new SphericalMercator({
 | 
			
		||||
    size: 256,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const overpassPopupPOI = writable<Record<string, any> | null>(null);
 | 
			
		||||
 | 
			
		||||
export const overpassPopup = new mapboxgl.Popup({
 | 
			
		||||
    closeButton: false,
 | 
			
		||||
    focusAfterOpen: false,
 | 
			
		||||
    maxWidth: undefined,
 | 
			
		||||
    offset: 15,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
 | 
			
		||||
 | 
			
		||||
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
 | 
			
		||||
@@ -36,6 +26,7 @@ export class OverpassLayer {
 | 
			
		||||
    queryZoom = 12;
 | 
			
		||||
    expirationTime = 7 * 24 * 3600 * 1000;
 | 
			
		||||
    map: mapboxgl.Map;
 | 
			
		||||
    popup: MapPopup;
 | 
			
		||||
 | 
			
		||||
    currentQueries: Set<string> = new Set();
 | 
			
		||||
    nextQueries: Map<string, { x: number, y: number, queries: string[] }> = new Map();
 | 
			
		||||
@@ -44,10 +35,15 @@ export class OverpassLayer {
 | 
			
		||||
    queryIfNeededBinded = this.queryIfNeeded.bind(this);
 | 
			
		||||
    updateBinded = this.update.bind(this);
 | 
			
		||||
    onHoverBinded = this.onHover.bind(this);
 | 
			
		||||
    maybeHidePopupBinded = this.maybeHidePopup.bind(this);
 | 
			
		||||
 | 
			
		||||
    constructor(map: mapboxgl.Map) {
 | 
			
		||||
        this.map = map;
 | 
			
		||||
        this.popup = new MapPopup(map, {
 | 
			
		||||
            closeButton: false,
 | 
			
		||||
            focusAfterOpen: false,
 | 
			
		||||
            maxWidth: undefined,
 | 
			
		||||
            offset: 15,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    add() {
 | 
			
		||||
@@ -127,30 +123,12 @@ export class OverpassLayer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onHover(e: any) {
 | 
			
		||||
        overpassPopupPOI.set({
 | 
			
		||||
            ...e.features[0].properties,
 | 
			
		||||
            sym: overpassQueryData[e.features[0].properties.query].symbol ?? ''
 | 
			
		||||
        this.popup.setItem({
 | 
			
		||||
            item: {
 | 
			
		||||
                ...e.features[0].properties,
 | 
			
		||||
                sym: overpassQueryData[e.features[0].properties.query].symbol ?? ''
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        tick().then(() => {
 | 
			
		||||
            // Show the popup once the content component has been rendered
 | 
			
		||||
            overpassPopup.setLngLat(e.features[0].geometry.coordinates);
 | 
			
		||||
            overpassPopup.addTo(this.map);
 | 
			
		||||
            this.map.on('mousemove', this.maybeHidePopupBinded);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    maybeHidePopup(e: any) {
 | 
			
		||||
        let poi = get(overpassPopupPOI);
 | 
			
		||||
        if (poi && this.map.project([poi.lon, poi.lat]).dist(this.map.project(e.lngLat)) > 60) {
 | 
			
		||||
            this.hideWaypointPopup();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hideWaypointPopup() {
 | 
			
		||||
        overpassPopupPOI.set(null);
 | 
			
		||||
        overpassPopup.remove();
 | 
			
		||||
 | 
			
		||||
        this.map.off('mousemove', this.maybeHidePopupBinded);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    query(bbox: [number, number, number, number]) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,102 +1,92 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import * as Card from '$lib/components/ui/card';
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import { overpassPopup, overpassPopupPOI } from './OverpassLayer';
 | 
			
		||||
	import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
	import { PencilLine, MapPin } from 'lucide-svelte';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import { dbUtils } from '$lib/db';
 | 
			
		||||
	import type { PopupItem } from '$lib/components/MapPopup';
 | 
			
		||||
 | 
			
		||||
	let popupElement: HTMLDivElement;
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		overpassPopup.setDOMContent(popupElement);
 | 
			
		||||
		popupElement.classList.remove('hidden');
 | 
			
		||||
	});
 | 
			
		||||
	export let poi: PopupItem<any>;
 | 
			
		||||
 | 
			
		||||
	let tags = {};
 | 
			
		||||
	let name = '';
 | 
			
		||||
	$: if ($overpassPopupPOI) {
 | 
			
		||||
		tags = JSON.parse($overpassPopupPOI.tags);
 | 
			
		||||
	$: if (poi) {
 | 
			
		||||
		tags = JSON.parse(poi.item.tags);
 | 
			
		||||
		if (tags.name !== undefined && tags.name !== '') {
 | 
			
		||||
			name = tags.name;
 | 
			
		||||
		} else {
 | 
			
		||||
			name = $_(`layers.label.${$overpassPopupPOI.query}`);
 | 
			
		||||
			name = $_(`layers.label.${poi.item.query}`);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div bind:this={popupElement} class="hidden">
 | 
			
		||||
	{#if $overpassPopupPOI}
 | 
			
		||||
		<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
 | 
			
		||||
			<Card.Header class="p-0">
 | 
			
		||||
				<Card.Title class="text-md">
 | 
			
		||||
					<div class="flex flex-row gap-3">
 | 
			
		||||
						<div class="flex flex-col">
 | 
			
		||||
							{name}
 | 
			
		||||
							<div class="text-muted-foreground text-sm font-normal">
 | 
			
		||||
								{$overpassPopupPOI.lat.toFixed(6)}° {$overpassPopupPOI.lon.toFixed(6)}°
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						<Button
 | 
			
		||||
							class="ml-auto p-1.5 h-8"
 | 
			
		||||
							variant="outline"
 | 
			
		||||
							href="https://www.openstreetmap.org/edit?editor=id&node={$overpassPopupPOI.id}"
 | 
			
		||||
							target="_blank"
 | 
			
		||||
						>
 | 
			
		||||
							<PencilLine size="16" />
 | 
			
		||||
						</Button>
 | 
			
		||||
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
 | 
			
		||||
	<Card.Header class="p-0">
 | 
			
		||||
		<Card.Title class="text-md">
 | 
			
		||||
			<div class="flex flex-row gap-3">
 | 
			
		||||
				<div class="flex flex-col">
 | 
			
		||||
					{name}
 | 
			
		||||
					<div class="text-muted-foreground text-sm font-normal">
 | 
			
		||||
						{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
 | 
			
		||||
					</div>
 | 
			
		||||
				</Card.Title>
 | 
			
		||||
			</Card.Header>
 | 
			
		||||
			{#if tags.image || tags['image:0']}
 | 
			
		||||
				<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
 | 
			
		||||
					<!-- svelte-ignore a11y-missing-attribute -->
 | 
			
		||||
					<img src={tags.image ?? tags['image:0']} />
 | 
			
		||||
				</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
 | 
			
		||||
				<div class="grid grid-cols-[auto_auto] gap-x-3">
 | 
			
		||||
					{#each Object.entries(tags) as [key, value]}
 | 
			
		||||
						{#if key !== 'name' && !key.includes('image')}
 | 
			
		||||
							<span class="font-mono">{key}</span>
 | 
			
		||||
							{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
 | 
			
		||||
								<a href={value} target="_blank" class="text-link underline">{value}</a>
 | 
			
		||||
							{:else if key === 'phone' || key === 'contact:phone'}
 | 
			
		||||
								<a href={'tel:' + value} class="text-link underline">{value}</a>
 | 
			
		||||
							{:else if key === 'email' || key === 'contact:email'}
 | 
			
		||||
								<a href={'mailto:' + value} class="text-link underline">{value}</a>
 | 
			
		||||
							{:else}
 | 
			
		||||
								<span>{value}</span>
 | 
			
		||||
							{/if}
 | 
			
		||||
						{/if}
 | 
			
		||||
					{/each}
 | 
			
		||||
				</div>
 | 
			
		||||
				<Button
 | 
			
		||||
					class="mt-2"
 | 
			
		||||
					class="ml-auto p-1.5 h-8"
 | 
			
		||||
					variant="outline"
 | 
			
		||||
					disabled={$selection.size === 0}
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						let desc = Object.entries(tags)
 | 
			
		||||
							.map(([key, value]) => `${key}: ${value}`)
 | 
			
		||||
							.join('\n');
 | 
			
		||||
						dbUtils.addOrUpdateWaypoint({
 | 
			
		||||
							attributes: {
 | 
			
		||||
								lat: $overpassPopupPOI.lat,
 | 
			
		||||
								lon: $overpassPopupPOI.lon
 | 
			
		||||
							},
 | 
			
		||||
							name: name,
 | 
			
		||||
							desc: desc,
 | 
			
		||||
							cmt: desc,
 | 
			
		||||
							sym: $overpassPopupPOI.sym
 | 
			
		||||
						});
 | 
			
		||||
					}}
 | 
			
		||||
					href="https://www.openstreetmap.org/edit?editor=id&node={poi.item.id}"
 | 
			
		||||
					target="_blank"
 | 
			
		||||
				>
 | 
			
		||||
					<MapPin size="16" class="mr-1" />
 | 
			
		||||
					{$_('toolbar.waypoint.add')}
 | 
			
		||||
					<PencilLine size="16" />
 | 
			
		||||
				</Button>
 | 
			
		||||
			</Card.Content>
 | 
			
		||||
		</Card.Root>
 | 
			
		||||
			</div>
 | 
			
		||||
		</Card.Title>
 | 
			
		||||
	</Card.Header>
 | 
			
		||||
	{#if tags.image || tags['image:0']}
 | 
			
		||||
		<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
 | 
			
		||||
			<!-- svelte-ignore a11y-missing-attribute -->
 | 
			
		||||
			<img src={tags.image ?? tags['image:0']} />
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
	<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
 | 
			
		||||
		<div class="grid grid-cols-[auto_auto] gap-x-3">
 | 
			
		||||
			{#each Object.entries(tags) as [key, value]}
 | 
			
		||||
				{#if key !== 'name' && !key.includes('image')}
 | 
			
		||||
					<span class="font-mono">{key}</span>
 | 
			
		||||
					{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
 | 
			
		||||
						<a href={value} target="_blank" class="text-link underline">{value}</a>
 | 
			
		||||
					{:else if key === 'phone' || key === 'contact:phone'}
 | 
			
		||||
						<a href={'tel:' + value} class="text-link underline">{value}</a>
 | 
			
		||||
					{:else if key === 'email' || key === 'contact:email'}
 | 
			
		||||
						<a href={'mailto:' + value} class="text-link underline">{value}</a>
 | 
			
		||||
					{:else}
 | 
			
		||||
						<span>{value}</span>
 | 
			
		||||
					{/if}
 | 
			
		||||
				{/if}
 | 
			
		||||
			{/each}
 | 
			
		||||
		</div>
 | 
			
		||||
		<Button
 | 
			
		||||
			class="mt-2"
 | 
			
		||||
			variant="outline"
 | 
			
		||||
			disabled={$selection.size === 0}
 | 
			
		||||
			on:click={() => {
 | 
			
		||||
				let desc = Object.entries(tags)
 | 
			
		||||
					.map(([key, value]) => `${key}: ${value}`)
 | 
			
		||||
					.join('\n');
 | 
			
		||||
				dbUtils.addOrUpdateWaypoint({
 | 
			
		||||
					attributes: {
 | 
			
		||||
						lat: poi.item.lat,
 | 
			
		||||
						lon: poi.item.lon
 | 
			
		||||
					},
 | 
			
		||||
					name: name,
 | 
			
		||||
					desc: desc,
 | 
			
		||||
					cmt: desc,
 | 
			
		||||
					sym: poi.item.sym
 | 
			
		||||
				});
 | 
			
		||||
			}}
 | 
			
		||||
		>
 | 
			
		||||
			<MapPin size="16" class="mr-1" />
 | 
			
		||||
			{$_('toolbar.waypoint.add')}
 | 
			
		||||
		</Button>
 | 
			
		||||
	</Card.Content>
 | 
			
		||||
</Card.Root>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import mapboxgl from "mapbox-gl";
 | 
			
		||||
import tilebelt from "@mapbox/tilebelt";
 | 
			
		||||
import { PUBLIC_MAPBOX_TOKEN } from "$env/static/public";
 | 
			
		||||
import PNGReader from "png.js";
 | 
			
		||||
import type { DateFormatter } from "@internationalized/date";
 | 
			
		||||
 | 
			
		||||
export function cn(...inputs: ClassValue[]) {
 | 
			
		||||
    return twMerge(clsx(inputs));
 | 
			
		||||
@@ -215,4 +216,16 @@ export function getURLForLanguage(lang: string | null | undefined, path: string)
 | 
			
		||||
            return `${base}${newPath}`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDateFormatter(locale: string) {
 | 
			
		||||
    return new Intl.DateTimeFormat(locale, {
 | 
			
		||||
        dateStyle: 'medium',
 | 
			
		||||
        timeStyle: 'medium'
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let df: DateFormatter = getDateFormatter('en');
 | 
			
		||||
locale.subscribe((l) => {
 | 
			
		||||
    df = getDateFormatter(l ?? 'en');
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user