From c49a479ed260d90263edffae4cd17079615d7ace Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Wed, 8 Jan 2025 17:14:57 +0800 Subject: [PATCH] App improvements (#350) * Display OSM IDs on right click/long press. * Make the zooming to tile extents explicit with a button. * Fix changing of theme/lang when drag-and-drop files are loaded. --- app/src/MapView.tsx | 162 ++++++++++++++++++++++++++++++++------------ app/src/index.css | 10 --- 2 files changed, 120 insertions(+), 52 deletions(-) diff --git a/app/src/MapView.tsx b/app/src/MapView.tsx index 0b8d23e7..9c270314 100644 --- a/app/src/MapView.tsx +++ b/app/src/MapView.tsx @@ -15,7 +15,11 @@ import { removeProtocol, setRTLTextPlugin, } from "maplibre-gl"; -import type { MapGeoJSONFeature, StyleSpecification } from "maplibre-gl"; +import type { + MapGeoJSONFeature, + MapTouchEvent, + StyleSpecification, +} from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; import type { LayerSpecification } from "@maplibre/maplibre-gl-style-spec"; import { FileSource, PMTiles, Protocol } from "pmtiles"; @@ -48,6 +52,38 @@ function getSourceLayer(l: LayerSpecification): string { return ""; } +const featureIdToOsmId = (raw: string | number) => { + return Number(BigInt(raw) & ((BigInt(1) << BigInt(44)) - BigInt(1))); +}; + +const featureIdToOsmType = (i: string | number) => { + const t = (BigInt(i) >> BigInt(44)) & BigInt(3); + if (t === BigInt(1)) return "node"; + if (t === BigInt(2)) return "way"; + if (t === BigInt(3)) return "relation"; + return "not_osm"; +}; + +const displayId = (featureId?: string | number) => { + if (featureId) { + const osmType = featureIdToOsmType(featureId); + if (osmType !== "not_osm") { + const osmId = featureIdToOsmId(featureId); + return ( + + {osmType} {osmId} + + ); + } + } + return featureId; +}; + const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => { return (
@@ -62,7 +98,7 @@ const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => { id - {f.id} + {displayId(f.id)} {([key, value]) => ( @@ -174,6 +210,8 @@ function StyleJsonPane(props: { theme: string; lang: string }) { ); } +type MapLibreViewRef = { fit: () => void }; + function MapLibreView(props: { theme: string; lang: string; @@ -182,16 +220,20 @@ function MapLibreView(props: { tiles?: string; npmLayers: LayerSpecification[]; droppedArchive?: PMTiles; + ref?: (ref: MapLibreViewRef) => void; }) { let mapContainer: HTMLDivElement | undefined; let mapRef: MaplibreMap | undefined; let protocolRef: Protocol | undefined; let hiddenRef: HTMLDivElement | undefined; + let longPressTimeout: ReturnType; const [error, setError] = createSignal(); const [timelinessInfo, setTimelinessInfo] = createSignal(); onMount(() => { + props.ref?.({ fit }); + if (getRTLTextPluginStatus() === "unavailable") { setRTLTextPlugin( "https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js", @@ -272,9 +314,10 @@ function MapLibreView(props: { } }); - map.on("contextmenu", (e) => { + const showContextMenu = (e: MapTouchEvent) => { const features = map.queryRenderedFeatures(e.point); if (hiddenRef && features.length) { + hiddenRef.innerHTML = ""; render(() => , hiddenRef); popup.setHTML(hiddenRef.innerHTML); popup.setLngLat(e.lngLat); @@ -282,8 +325,32 @@ function MapLibreView(props: { } else { popup.remove(); } + }; + + map.on("contextmenu", (e: MapTouchEvent) => { + showContextMenu(e); }); + map.on("touchstart", (e: MapTouchEvent) => { + longPressTimeout = setTimeout(() => { + showContextMenu(e); + }, 500); + }); + + const clearLongPress = () => { + clearTimeout(longPressTimeout); + }; + + map.on("touchend", clearLongPress); + map.on("touchcancel", clearLongPress); + map.on("touchmove", clearLongPress); + map.on("pointerdrag", clearLongPress); + map.on("pointermove", clearLongPress); + map.on("moveend", clearLongPress); + map.on("gesturestart", clearLongPress); + map.on("gesturechange", clearLongPress); + map.on("gestureend", clearLongPress); + mapRef = map; return () => { @@ -293,24 +360,25 @@ function MapLibreView(props: { }; }); - createEffect(() => { + const fit = async () => { if (protocolRef) { - const archive = props.droppedArchive; - if (archive) { + let archive = props.droppedArchive; + if (!archive && props.tiles) { + archive = new PMTiles(props.tiles); protocolRef.add(archive); - (async () => { - const header = await archive.getHeader(); - mapRef?.fitBounds( - [ - [header.minLon, header.minLat], - [header.maxLon, header.maxLat], - ], - { animate: false }, - ); - })(); + } + if (archive) { + const header = await archive.getHeader(); + mapRef?.fitBounds( + [ + [header.minLon, header.minLat], + [header.maxLon, header.maxLat], + ], + { animate: false }, + ); } } - }); + }; createEffect(() => { if (mapRef) { @@ -319,30 +387,31 @@ function MapLibreView(props: { } }); - createEffect(() => { - (async () => { - if (mapRef) { - let minZoom: number | undefined; - let maxZoom: number | undefined; - if (props.droppedArchive) { - const header = await props.droppedArchive.getHeader(); - minZoom = header.minZoom; - maxZoom = header.maxZoom; - } - mapRef.setStyle( - getMaplibreStyle( - props.theme, - props.lang, - props.localSprites, - props.tiles, - props.npmLayers, - props.droppedArchive, - minZoom, - maxZoom, - ), - ); + createEffect(async () => { + // HACK: do this to ensure a tracking scope is created + // because async effects are not correct + [props.theme, props.lang, props.localSprites, props.tiles, props.npmLayers]; + if (mapRef) { + let minZoom: number | undefined; + let maxZoom: number | undefined; + if (props.droppedArchive) { + const header = await props.droppedArchive.getHeader(); + minZoom = header.minZoom; + maxZoom = header.maxZoom; } - })(); + mapRef.setStyle( + getMaplibreStyle( + props.theme, + props.lang, + props.localSprites, + props.tiles, + props.npmLayers, + props.droppedArchive, + minZoom, + maxZoom, + ), + ); + } }); return ( @@ -381,6 +450,7 @@ function MapView() { const [knownNpmVersions, setKnownNpmVersions] = createSignal([]); const [npmLayers, setNpmLayers] = createSignal([]); const [droppedArchive, setDroppedArchive] = createSignal(); + const [maplibreView, setMaplibreView] = createSignal(); createEffect(() => { const record = { @@ -454,13 +524,17 @@ function MapView() { language_script_pairs.sort((a, b) => a.full_name.localeCompare(b.full_name)); + const fit = () => { + maplibreView()?.fit(); + }; + return (