Skip to content

Commit c49a479

Browse files
authored
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.
1 parent 8b523a8 commit c49a479

File tree

2 files changed

+120
-52
lines changed

2 files changed

+120
-52
lines changed

app/src/MapView.tsx

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515
removeProtocol,
1616
setRTLTextPlugin,
1717
} from "maplibre-gl";
18-
import type { MapGeoJSONFeature, StyleSpecification } from "maplibre-gl";
18+
import type {
19+
MapGeoJSONFeature,
20+
MapTouchEvent,
21+
StyleSpecification,
22+
} from "maplibre-gl";
1923
import "maplibre-gl/dist/maplibre-gl.css";
2024
import type { LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
2125
import { FileSource, PMTiles, Protocol } from "pmtiles";
@@ -48,6 +52,38 @@ function getSourceLayer(l: LayerSpecification): string {
4852
return "";
4953
}
5054

55+
const featureIdToOsmId = (raw: string | number) => {
56+
return Number(BigInt(raw) & ((BigInt(1) << BigInt(44)) - BigInt(1)));
57+
};
58+
59+
const featureIdToOsmType = (i: string | number) => {
60+
const t = (BigInt(i) >> BigInt(44)) & BigInt(3);
61+
if (t === BigInt(1)) return "node";
62+
if (t === BigInt(2)) return "way";
63+
if (t === BigInt(3)) return "relation";
64+
return "not_osm";
65+
};
66+
67+
const displayId = (featureId?: string | number) => {
68+
if (featureId) {
69+
const osmType = featureIdToOsmType(featureId);
70+
if (osmType !== "not_osm") {
71+
const osmId = featureIdToOsmId(featureId);
72+
return (
73+
<a
74+
class="underline text-purple"
75+
target="_blank"
76+
rel="noreferrer"
77+
href={`https://openstreetmap.org/${osmType}/${osmId}`}
78+
>
79+
{osmType} {osmId}
80+
</a>
81+
);
82+
}
83+
}
84+
return featureId;
85+
};
86+
5187
const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => {
5288
return (
5389
<div class="features-properties">
@@ -62,7 +98,7 @@ const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => {
6298
<tbody>
6399
<tr>
64100
<td>id</td>
65-
<td>{f.id}</td>
101+
<td>{displayId(f.id)}</td>
66102
</tr>
67103
<For each={Object.entries(f.properties)}>
68104
{([key, value]) => (
@@ -174,6 +210,8 @@ function StyleJsonPane(props: { theme: string; lang: string }) {
174210
);
175211
}
176212

213+
type MapLibreViewRef = { fit: () => void };
214+
177215
function MapLibreView(props: {
178216
theme: string;
179217
lang: string;
@@ -182,16 +220,20 @@ function MapLibreView(props: {
182220
tiles?: string;
183221
npmLayers: LayerSpecification[];
184222
droppedArchive?: PMTiles;
223+
ref?: (ref: MapLibreViewRef) => void;
185224
}) {
186225
let mapContainer: HTMLDivElement | undefined;
187226
let mapRef: MaplibreMap | undefined;
188227
let protocolRef: Protocol | undefined;
189228
let hiddenRef: HTMLDivElement | undefined;
229+
let longPressTimeout: ReturnType<typeof setTimeout>;
190230

191231
const [error, setError] = createSignal<string | undefined>();
192232
const [timelinessInfo, setTimelinessInfo] = createSignal<string>();
193233

194234
onMount(() => {
235+
props.ref?.({ fit });
236+
195237
if (getRTLTextPluginStatus() === "unavailable") {
196238
setRTLTextPlugin(
197239
"https://unpkg.com/@mapbox/[email protected]/mapbox-gl-rtl-text.min.js",
@@ -272,18 +314,43 @@ function MapLibreView(props: {
272314
}
273315
});
274316

275-
map.on("contextmenu", (e) => {
317+
const showContextMenu = (e: MapTouchEvent) => {
276318
const features = map.queryRenderedFeatures(e.point);
277319
if (hiddenRef && features.length) {
320+
hiddenRef.innerHTML = "";
278321
render(() => <FeaturesProperties features={features} />, hiddenRef);
279322
popup.setHTML(hiddenRef.innerHTML);
280323
popup.setLngLat(e.lngLat);
281324
popup.addTo(map);
282325
} else {
283326
popup.remove();
284327
}
328+
};
329+
330+
map.on("contextmenu", (e: MapTouchEvent) => {
331+
showContextMenu(e);
285332
});
286333

334+
map.on("touchstart", (e: MapTouchEvent) => {
335+
longPressTimeout = setTimeout(() => {
336+
showContextMenu(e);
337+
}, 500);
338+
});
339+
340+
const clearLongPress = () => {
341+
clearTimeout(longPressTimeout);
342+
};
343+
344+
map.on("touchend", clearLongPress);
345+
map.on("touchcancel", clearLongPress);
346+
map.on("touchmove", clearLongPress);
347+
map.on("pointerdrag", clearLongPress);
348+
map.on("pointermove", clearLongPress);
349+
map.on("moveend", clearLongPress);
350+
map.on("gesturestart", clearLongPress);
351+
map.on("gesturechange", clearLongPress);
352+
map.on("gestureend", clearLongPress);
353+
287354
mapRef = map;
288355

289356
return () => {
@@ -293,24 +360,25 @@ function MapLibreView(props: {
293360
};
294361
});
295362

296-
createEffect(() => {
363+
const fit = async () => {
297364
if (protocolRef) {
298-
const archive = props.droppedArchive;
299-
if (archive) {
365+
let archive = props.droppedArchive;
366+
if (!archive && props.tiles) {
367+
archive = new PMTiles(props.tiles);
300368
protocolRef.add(archive);
301-
(async () => {
302-
const header = await archive.getHeader();
303-
mapRef?.fitBounds(
304-
[
305-
[header.minLon, header.minLat],
306-
[header.maxLon, header.maxLat],
307-
],
308-
{ animate: false },
309-
);
310-
})();
369+
}
370+
if (archive) {
371+
const header = await archive.getHeader();
372+
mapRef?.fitBounds(
373+
[
374+
[header.minLon, header.minLat],
375+
[header.maxLon, header.maxLat],
376+
],
377+
{ animate: false },
378+
);
311379
}
312380
}
313-
});
381+
};
314382

315383
createEffect(() => {
316384
if (mapRef) {
@@ -319,30 +387,31 @@ function MapLibreView(props: {
319387
}
320388
});
321389

322-
createEffect(() => {
323-
(async () => {
324-
if (mapRef) {
325-
let minZoom: number | undefined;
326-
let maxZoom: number | undefined;
327-
if (props.droppedArchive) {
328-
const header = await props.droppedArchive.getHeader();
329-
minZoom = header.minZoom;
330-
maxZoom = header.maxZoom;
331-
}
332-
mapRef.setStyle(
333-
getMaplibreStyle(
334-
props.theme,
335-
props.lang,
336-
props.localSprites,
337-
props.tiles,
338-
props.npmLayers,
339-
props.droppedArchive,
340-
minZoom,
341-
maxZoom,
342-
),
343-
);
390+
createEffect(async () => {
391+
// HACK: do this to ensure a tracking scope is created
392+
// because async effects are not correct
393+
[props.theme, props.lang, props.localSprites, props.tiles, props.npmLayers];
394+
if (mapRef) {
395+
let minZoom: number | undefined;
396+
let maxZoom: number | undefined;
397+
if (props.droppedArchive) {
398+
const header = await props.droppedArchive.getHeader();
399+
minZoom = header.minZoom;
400+
maxZoom = header.maxZoom;
344401
}
345-
})();
402+
mapRef.setStyle(
403+
getMaplibreStyle(
404+
props.theme,
405+
props.lang,
406+
props.localSprites,
407+
props.tiles,
408+
props.npmLayers,
409+
props.droppedArchive,
410+
minZoom,
411+
maxZoom,
412+
),
413+
);
414+
}
346415
});
347416

348417
return (
@@ -381,6 +450,7 @@ function MapView() {
381450
const [knownNpmVersions, setKnownNpmVersions] = createSignal<string[]>([]);
382451
const [npmLayers, setNpmLayers] = createSignal<LayerSpecification[]>([]);
383452
const [droppedArchive, setDroppedArchive] = createSignal<PMTiles>();
453+
const [maplibreView, setMaplibreView] = createSignal<MapLibreViewRef>();
384454

385455
createEffect(() => {
386456
const record = {
@@ -454,13 +524,17 @@ function MapView() {
454524

455525
language_script_pairs.sort((a, b) => a.full_name.localeCompare(b.full_name));
456526

527+
const fit = () => {
528+
maplibreView()?.fit();
529+
};
530+
457531
return (
458532
<div class="flex flex-col h-dvh w-full">
459533
<Nav page={0} />
460534
<div class="max-w-[1500px] mx-auto">
461-
<form onSubmit={loadTiles} class="flex">
535+
<form onSubmit={loadTiles} class="flex space-x-2">
462536
<input
463-
class="border-2 border-gray p-1 flex-1 mr-2 text-xs lg:text-base"
537+
class="border-2 border-gray p-1 flex-1 text-xs lg:text-base"
464538
type="text"
465539
name="tiles"
466540
value={tiles()}
@@ -470,6 +544,9 @@ function MapView() {
470544
<button class="btn-primary" type="submit">
471545
load
472546
</button>
547+
<button class="btn-primary" type="submit" onClick={fit}>
548+
fit bounds
549+
</button>
473550
</form>
474551
<div class="flex my-2 space-y-2 lg:space-y-0 space-x-2 flex-col lg:flex-row items-center">
475552
<div class="flex items-center">
@@ -568,6 +645,7 @@ function MapView() {
568645
ondrop={drop}
569646
>
570647
<MapLibreView
648+
ref={setMaplibreView}
571649
tiles={tiles()}
572650
localSprites={localSprites()}
573651
showBoxes={showBoxes()}

app/src/index.css

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@
22
@tailwind components;
33
@tailwind utilities;
44

5-
.maplibregl-popup,
6-
.maplibregl-popup-close-button {
7-
color: black;
8-
}
9-
10-
.features-properties {
11-
max-height: 45vh;
12-
overflow-y: scroll;
13-
}
14-
155
@layer components {
166
.btn-primary {
177
@apply rounded bg-purple px-2 py-1 text-xs font-semibold text-white shadow-sm hover:bg-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-purple;

0 commit comments

Comments
 (0)