11'use client' ;
22
3- import { useCallback , useId , useRef } from 'react' ;
3+ import { useCallback , useEffect , useId , useRef , useState } from 'react' ;
44import {
55 Dialog ,
66 DialogContent ,
@@ -17,6 +17,23 @@ import { Database } from 'lucide-react';
1717import { Popup } from 'maplibre-gl' ;
1818import { buildMartinUrl } from '@/lib/api' ;
1919
20+ interface TileJSON {
21+ tilejson ?: string ;
22+ name ?: string ;
23+ description ?: string ;
24+ version ?: string ;
25+ attribution ?: string ;
26+ scheme ?: string ;
27+ tiles : string [ ] ;
28+ grids ?: string [ ] ;
29+ data ?: string [ ] ;
30+ minzoom ?: number ;
31+ maxzoom ?: number ;
32+ bounds ?: [ number , number , number , number ] ; // [west, south, east, north]
33+ center ?: [ number , number , number ] ; // [longitude, latitude, zoom]
34+ vector_layers ?: unknown [ ] ;
35+ }
36+
2037interface TileInspectDialogProps {
2138 name : string ;
2239 source : TileSource ;
@@ -27,6 +44,86 @@ export function TileInspectDialog({ name, source, onCloseAction }: TileInspectDi
2744 const id = useId ( ) ;
2845 const mapRef = useRef < MapRef > ( null ) ;
2946 const inspectControlRef = useRef < MaplibreInspect > ( null ) ;
47+ const [ tileJSON , setTileJSON ] = useState < TileJSON | null > ( null ) ;
48+
49+ const configureMapBounds = useCallback ( ( ) => {
50+ if ( ! mapRef . current || ! tileJSON ) {
51+ return ;
52+ }
53+
54+ const map = mapRef . current . getMap ( ) ;
55+
56+ // Set minzoom and maxzoom restrictions
57+ if ( tileJSON . minzoom !== undefined ) {
58+ map . setMinZoom ( tileJSON . minzoom ) ;
59+ }
60+ if ( tileJSON . maxzoom !== undefined ) {
61+ map . setMaxZoom ( tileJSON . maxzoom ) ;
62+ }
63+
64+ // Set bounds restrictions if available
65+ if ( tileJSON . bounds ) {
66+ const [ west , south , east , north ] = tileJSON . bounds ;
67+ map . setMaxBounds ( [
68+ [ west , south ] ,
69+ [ east , north ] ,
70+ ] ) ;
71+ }
72+
73+ // Fit to bounds or center if available
74+ if ( tileJSON . bounds ) {
75+ const [ west , south , east , north ] = tileJSON . bounds ;
76+ map . fitBounds (
77+ [
78+ [ west , south ] ,
79+ [ east , north ] ,
80+ ] ,
81+ {
82+ padding : 50 ,
83+ maxZoom : tileJSON . maxzoom ,
84+ } ,
85+ ) ;
86+ } else if ( tileJSON . center ) {
87+ const [ lng , lat , zoom ] = tileJSON . center ;
88+ map . setCenter ( [ lng , lat ] ) ;
89+ if ( zoom !== undefined ) {
90+ map . setZoom ( zoom ) ;
91+ }
92+ }
93+ } , [ tileJSON ] ) ;
94+
95+ // Fetch TileJSON when dialog opens
96+ useEffect ( ( ) => {
97+ let cancelled = false ;
98+
99+ fetch ( buildMartinUrl ( `/${ name } ` ) )
100+ . then ( ( response ) => {
101+ if ( ! response . ok ) {
102+ throw new Error ( `Failed to fetch TileJSON: ${ response . statusText } ` ) ;
103+ }
104+ return response . json ( ) ;
105+ } )
106+ . then ( ( data : TileJSON ) => {
107+ if ( ! cancelled ) {
108+ setTileJSON ( data ) ;
109+ }
110+ } )
111+ . catch ( ( error ) => {
112+ console . error ( 'Error fetching TileJSON:' , error ) ;
113+ // Continue without TileJSON restrictions if fetch fails
114+ } ) ;
115+
116+ return ( ) => {
117+ cancelled = true ;
118+ } ;
119+ } , [ name ] ) ;
120+
121+ // Reconfigure bounds when TileJSON loads after map is already initialized
122+ useEffect ( ( ) => {
123+ if ( tileJSON && mapRef . current ) {
124+ configureMapBounds ( ) ;
125+ }
126+ } , [ tileJSON , configureMapBounds ] ) ;
30127
31128 const addInspectorToMap = useCallback ( ( ) => {
32129 if ( ! mapRef . current ) {
@@ -54,12 +151,15 @@ export function TileInspectDialog({ name, source, onCloseAction }: TileInspectDi
54151 } ) ;
55152
56153 map . addControl ( inspectControlRef . current ) ;
57- } , [ name ] ) ;
154+
155+ // Configure bounds after adding inspector
156+ configureMapBounds ( ) ;
157+ } , [ name , configureMapBounds ] ) ;
58158 const isImageSource = [ 'image/gif' , 'image/jpeg' , 'image/png' , 'image/webp' ] . includes (
59159 source . content_type ,
60160 ) ;
61161 return (
62- < Dialog onOpenChange = { ( v ) => ! v && onCloseAction ( ) } open = { true } >
162+ < Dialog onOpenChange = { ( v : boolean ) => ! v && onCloseAction ( ) } open = { true } >
63163 < DialogContent className = "max-w-6xl w-full p-6 max-h-[90vh] overflow-auto" >
64164 < DialogHeader className = "mb-6" >
65165 < DialogTitle className = "text-2xl flex items-center justify-between" >
@@ -78,6 +178,31 @@ export function TileInspectDialog({ name, source, onCloseAction }: TileInspectDi
78178 < MapLibreMap
79179 ref = { mapRef }
80180 reuseMaps = { false }
181+ onLoad = { ( ) => {
182+ // Configure bounds for raster sources after map loads
183+ if ( tileJSON ) {
184+ configureMapBounds ( ) ;
185+ }
186+ } }
187+ initialViewState = {
188+ tileJSON ?. center
189+ ? {
190+ longitude : tileJSON . center [ 0 ] ,
191+ latitude : tileJSON . center [ 1 ] ,
192+ zoom : tileJSON . center [ 2 ] ?? 0 ,
193+ }
194+ : undefined
195+ }
196+ minZoom = { tileJSON ?. minzoom }
197+ maxZoom = { tileJSON ?. maxzoom }
198+ maxBounds = {
199+ tileJSON ?. bounds
200+ ? [
201+ [ tileJSON . bounds [ 0 ] , tileJSON . bounds [ 1 ] ] ,
202+ [ tileJSON . bounds [ 2 ] , tileJSON . bounds [ 3 ] ] ,
203+ ]
204+ : undefined
205+ }
81206 style = { {
82207 height : '500px' ,
83208 width : '100%' ,
@@ -91,6 +216,25 @@ export function TileInspectDialog({ name, source, onCloseAction }: TileInspectDi
91216 onLoad = { addInspectorToMap }
92217 ref = { mapRef }
93218 reuseMaps = { false }
219+ initialViewState = {
220+ tileJSON ?. center
221+ ? {
222+ longitude : tileJSON . center [ 0 ] ,
223+ latitude : tileJSON . center [ 1 ] ,
224+ zoom : tileJSON . center [ 2 ] ?? 0 ,
225+ }
226+ : undefined
227+ }
228+ minZoom = { tileJSON ?. minzoom }
229+ maxZoom = { tileJSON ?. maxzoom }
230+ maxBounds = {
231+ tileJSON ?. bounds
232+ ? [
233+ [ tileJSON . bounds [ 0 ] , tileJSON . bounds [ 1 ] ] ,
234+ [ tileJSON . bounds [ 2 ] , tileJSON . bounds [ 3 ] ] ,
235+ ]
236+ : undefined
237+ }
94238 style = { {
95239 height : '500px' ,
96240 width : '100%' ,
0 commit comments