@@ -15,7 +15,11 @@ import {
15
15
removeProtocol ,
16
16
setRTLTextPlugin ,
17
17
} from "maplibre-gl" ;
18
- import type { MapGeoJSONFeature , StyleSpecification } from "maplibre-gl" ;
18
+ import type {
19
+ MapGeoJSONFeature ,
20
+ MapTouchEvent ,
21
+ StyleSpecification ,
22
+ } from "maplibre-gl" ;
19
23
import "maplibre-gl/dist/maplibre-gl.css" ;
20
24
import type { LayerSpecification } from "@maplibre/maplibre-gl-style-spec" ;
21
25
import { FileSource , PMTiles , Protocol } from "pmtiles" ;
@@ -48,6 +52,38 @@ function getSourceLayer(l: LayerSpecification): string {
48
52
return "" ;
49
53
}
50
54
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
+
51
87
const FeaturesProperties = ( props : { features : MapGeoJSONFeature [ ] } ) => {
52
88
return (
53
89
< div class = "features-properties" >
@@ -62,7 +98,7 @@ const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => {
62
98
< tbody >
63
99
< tr >
64
100
< td > id</ td >
65
- < td > { f . id } </ td >
101
+ < td > { displayId ( f . id ) } </ td >
66
102
</ tr >
67
103
< For each = { Object . entries ( f . properties ) } >
68
104
{ ( [ key , value ] ) => (
@@ -174,6 +210,8 @@ function StyleJsonPane(props: { theme: string; lang: string }) {
174
210
) ;
175
211
}
176
212
213
+ type MapLibreViewRef = { fit : ( ) => void } ;
214
+
177
215
function MapLibreView ( props : {
178
216
theme : string ;
179
217
lang : string ;
@@ -182,16 +220,20 @@ function MapLibreView(props: {
182
220
tiles ?: string ;
183
221
npmLayers : LayerSpecification [ ] ;
184
222
droppedArchive ?: PMTiles ;
223
+ ref ?: ( ref : MapLibreViewRef ) => void ;
185
224
} ) {
186
225
let mapContainer : HTMLDivElement | undefined ;
187
226
let mapRef : MaplibreMap | undefined ;
188
227
let protocolRef : Protocol | undefined ;
189
228
let hiddenRef : HTMLDivElement | undefined ;
229
+ let longPressTimeout : ReturnType < typeof setTimeout > ;
190
230
191
231
const [ error , setError ] = createSignal < string | undefined > ( ) ;
192
232
const [ timelinessInfo , setTimelinessInfo ] = createSignal < string > ( ) ;
193
233
194
234
onMount ( ( ) => {
235
+ props . ref ?.( { fit } ) ;
236
+
195
237
if ( getRTLTextPluginStatus ( ) === "unavailable" ) {
196
238
setRTLTextPlugin (
197
239
"https://unpkg.com/@mapbox/[email protected] /mapbox-gl-rtl-text.min.js" ,
@@ -272,18 +314,43 @@ function MapLibreView(props: {
272
314
}
273
315
} ) ;
274
316
275
- map . on ( "contextmenu" , ( e ) => {
317
+ const showContextMenu = ( e : MapTouchEvent ) => {
276
318
const features = map . queryRenderedFeatures ( e . point ) ;
277
319
if ( hiddenRef && features . length ) {
320
+ hiddenRef . innerHTML = "" ;
278
321
render ( ( ) => < FeaturesProperties features = { features } /> , hiddenRef ) ;
279
322
popup . setHTML ( hiddenRef . innerHTML ) ;
280
323
popup . setLngLat ( e . lngLat ) ;
281
324
popup . addTo ( map ) ;
282
325
} else {
283
326
popup . remove ( ) ;
284
327
}
328
+ } ;
329
+
330
+ map . on ( "contextmenu" , ( e : MapTouchEvent ) => {
331
+ showContextMenu ( e ) ;
285
332
} ) ;
286
333
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
+
287
354
mapRef = map ;
288
355
289
356
return ( ) => {
@@ -293,24 +360,25 @@ function MapLibreView(props: {
293
360
} ;
294
361
} ) ;
295
362
296
- createEffect ( ( ) => {
363
+ const fit = async ( ) => {
297
364
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 ) ;
300
368
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
+ ) ;
311
379
}
312
380
}
313
- } ) ;
381
+ } ;
314
382
315
383
createEffect ( ( ) => {
316
384
if ( mapRef ) {
@@ -319,30 +387,31 @@ function MapLibreView(props: {
319
387
}
320
388
} ) ;
321
389
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 ;
344
401
}
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
+ }
346
415
} ) ;
347
416
348
417
return (
@@ -381,6 +450,7 @@ function MapView() {
381
450
const [ knownNpmVersions , setKnownNpmVersions ] = createSignal < string [ ] > ( [ ] ) ;
382
451
const [ npmLayers , setNpmLayers ] = createSignal < LayerSpecification [ ] > ( [ ] ) ;
383
452
const [ droppedArchive , setDroppedArchive ] = createSignal < PMTiles > ( ) ;
453
+ const [ maplibreView , setMaplibreView ] = createSignal < MapLibreViewRef > ( ) ;
384
454
385
455
createEffect ( ( ) => {
386
456
const record = {
@@ -454,13 +524,17 @@ function MapView() {
454
524
455
525
language_script_pairs . sort ( ( a , b ) => a . full_name . localeCompare ( b . full_name ) ) ;
456
526
527
+ const fit = ( ) => {
528
+ maplibreView ( ) ?. fit ( ) ;
529
+ } ;
530
+
457
531
return (
458
532
< div class = "flex flex-col h-dvh w-full" >
459
533
< Nav page = { 0 } />
460
534
< div class = "max-w-[1500px] mx-auto" >
461
- < form onSubmit = { loadTiles } class = "flex" >
535
+ < form onSubmit = { loadTiles } class = "flex space-x-2 " >
462
536
< 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"
464
538
type = "text"
465
539
name = "tiles"
466
540
value = { tiles ( ) }
@@ -470,6 +544,9 @@ function MapView() {
470
544
< button class = "btn-primary" type = "submit" >
471
545
load
472
546
</ button >
547
+ < button class = "btn-primary" type = "submit" onClick = { fit } >
548
+ fit bounds
549
+ </ button >
473
550
</ form >
474
551
< div class = "flex my-2 space-y-2 lg:space-y-0 space-x-2 flex-col lg:flex-row items-center" >
475
552
< div class = "flex items-center" >
@@ -568,6 +645,7 @@ function MapView() {
568
645
ondrop = { drop }
569
646
>
570
647
< MapLibreView
648
+ ref = { setMaplibreView }
571
649
tiles = { tiles ( ) }
572
650
localSprites = { localSprites ( ) }
573
651
showBoxes = { showBoxes ( ) }
0 commit comments