diff --git a/website/src/lib/components/ElevationProfile.svelte b/website/src/lib/components/ElevationProfile.svelte index 7c3fecf6..d15d9cb2 100644 --- a/website/src/lib/components/ElevationProfile.svelte +++ b/website/src/lib/components/ElevationProfile.svelte @@ -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; 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; diff --git a/website/src/lib/components/MapPopup.svelte b/website/src/lib/components/MapPopup.svelte new file mode 100644 index 00000000..e89800b1 --- /dev/null +++ b/website/src/lib/components/MapPopup.svelte @@ -0,0 +1,25 @@ + + + + +
+ {#if $item} + {#if $item.item instanceof Waypoint} + + {:else if $item.item instanceof TrackPoint} + + {:else} + + {/if} + {/if} +
diff --git a/website/src/lib/components/MapPopup.ts b/website/src/lib/components/MapPopup.ts new file mode 100644 index 00000000..6e43125c --- /dev/null +++ b/website/src/lib/components/MapPopup.ts @@ -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 = { + item: T; + fileId?: string; +}; + +export class MapPopup { + map: mapboxgl.Map; + popup: mapboxgl.Popup; + item: Writable = 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); + } +} \ No newline at end of file diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index 811fa855..87bab802 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -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 | 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); } } }} diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index 644330cd..d419c6a4 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -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 ` @@ -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) { 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) { diff --git a/website/src/lib/components/gpx-layer/GPXLayerPopup.ts b/website/src/lib/components/gpx-layer/GPXLayerPopup.ts new file mode 100644 index 00000000..8d26c886 --- /dev/null +++ b/website/src/lib/components/gpx-layer/GPXLayerPopup.ts @@ -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, [])); +} \ No newline at end of file diff --git a/website/src/lib/components/gpx-layer/GPXLayers.svelte b/website/src/lib/components/gpx-layer/GPXLayers.svelte index b18cbf86..6ab1e0e8 100644 --- a/website/src/lib/components/gpx-layer/GPXLayers.svelte +++ b/website/src/lib/components/gpx-layer/GPXLayers.svelte @@ -1,11 +1,11 @@ - - diff --git a/website/src/lib/components/gpx-layer/TrackpointPopup.svelte b/website/src/lib/components/gpx-layer/TrackpointPopup.svelte new file mode 100644 index 00000000..7bc6a78e --- /dev/null +++ b/website/src/lib/components/gpx-layer/TrackpointPopup.svelte @@ -0,0 +1,36 @@ + + + + + + + +
+ + {trackpoint.item.getLatitude().toFixed(6)}° {trackpoint.item + .getLongitude() + .toFixed(6)}° +
+ {#if trackpoint.item.ele !== undefined} +
+ + +
+ {/if} + {#if trackpoint.item.time} +
+ + {df.format(trackpoint.item.time)} +
+ {/if} + + diff --git a/website/src/lib/components/gpx-layer/WaypointPopup.svelte b/website/src/lib/components/gpx-layer/WaypointPopup.svelte index 97edfc89..6fc8ebc9 100644 --- a/website/src/lib/components/gpx-layer/WaypointPopup.svelte +++ b/website/src/lib/components/gpx-layer/WaypointPopup.svelte @@ -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; - 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 @@ } - + {#if waypoint.item.desc} + {@html sanitize(waypoint.item.desc)} + {/if} + {#if waypoint.item.cmt && waypoint.item.cmt !== waypoint.item.desc} + {@html sanitize(waypoint.item.cmt)} + {/if} + {#if $currentTool === Tool.WAYPOINT} + + {/if} + +