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 (
-
@@ -568,6 +645,7 @@ function MapView() {
ondrop={drop}
>