refactor map popups and add inspect trackpoint feature

This commit is contained in:
vcoppe
2024-10-08 15:49:14 +02:00
parent d823f44558
commit 711825f5a3
14 changed files with 396 additions and 289 deletions

View File

@@ -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;

View 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>

View 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);
}
}

View File

@@ -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);
}
}
}}

View File

@@ -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) {

View 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, []));
}

View File

@@ -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 />

View 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)}&deg; {trackpoint.item
.getLongitude()
.toFixed(6)}&deg;
</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>

View File

@@ -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)}&deg; {$currentPopupWaypoint[0]
.getLongitude()
.toFixed(6)}&deg;
{#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)}&deg; {waypoint.item.getLongitude().toFixed(6)}&deg;
{#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) {

View File

@@ -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, []));
}

View File

@@ -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)) {

View File

@@ -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]) {

View File

@@ -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)}&deg; {$overpassPopupPOI.lon.toFixed(6)}&deg;
</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)}&deg; {poi.item.lon.toFixed(6)}&deg;
</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>

View File

@@ -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');
});