@@ -6,14 +6,15 @@ import type { Map as MapLibreMap } from 'maplibre-gl';
66export default function PlaygroundPage ( ) {
77 const mapRef = useRef < MapLibreMap | null > ( null ) ;
88 const mapEl = useRef < HTMLDivElement | null > ( null ) ;
9- const [ baseUrl , setBaseUrl ] = useState < string > (
10- process . env . NEXT_PUBLIC_API_BASE || 'http://localhost:8000'
11- ) ;
9+ const envApi = process . env . NEXT_PUBLIC_API_BASE || 'http://localhost:8000' ;
10+ const [ baseUrl , setBaseUrl ] = useState < string > ( envApi ) ;
1211 const [ pin , setPin ] = useState < string > ( '' ) ;
1312 const [ city , setCity ] = useState < string > ( '' ) ;
1413 const [ limit , setLimit ] = useState < number > ( 200 ) ;
1514 const [ loading , setLoading ] = useState ( false ) ;
1615 const [ error , setError ] = useState < string | null > ( null ) ;
16+ const [ useBbox , setUseBbox ] = useState < boolean > ( false ) ;
17+ const [ cluster , setCluster ] = useState < boolean > ( true ) ;
1718
1819 useEffect ( ( ) => {
1920 let canceled = false ;
@@ -44,22 +45,81 @@ export default function PlaygroundPage() {
4445 url . searchParams . set ( 'limit' , String ( limit ) ) ;
4546 if ( pin ) url . searchParams . set ( 'pin' , pin ) ;
4647 if ( city ) url . searchParams . set ( 'city' , city ) ;
48+ if ( useBbox && mapRef . current ) {
49+ const b = mapRef . current . getBounds ( ) ;
50+ const bbox = `${ b . getWest ( ) } ,${ b . getSouth ( ) } ,${ b . getEast ( ) } ,${ b . getNorth ( ) } ` ;
51+ url . searchParams . set ( 'bbox' , bbox ) ;
52+ }
4753 const r = await fetch ( url . toString ( ) ) ;
4854 const gj = await r . json ( ) ;
4955 if ( ! r . ok ) throw new Error ( gj ?. error || `HTTP ${ r . status } ` ) ;
5056 const map = mapRef . current ;
5157 if ( ! map ) return ;
5258 const srcId = 'addresses' ;
53- const layerId = 'addresses-layer' ;
59+ const pointLayerId = 'addresses-points' ;
60+ const clusterLayerId = 'addresses-clusters' ;
61+ const clusterCountId = 'addresses-cluster-count' ;
62+
63+ // Clean existing layers/sources
64+ for ( const id of [ clusterCountId , clusterLayerId , pointLayerId ] ) {
65+ if ( map . getLayer ( id ) ) map . removeLayer ( id ) ;
66+ }
67+ if ( map . getSource ( srcId ) ) map . removeSource ( srcId ) ;
5468
55- if ( map . getLayer ( layerId ) ) map . removeLayer ( layerId ) ;
56- if ( map . getSource ( srcId ) ) ( map . getSource ( srcId ) as any ) . setData ( gj ) ;
57- else map . addSource ( srcId , { type : 'geojson' , data : gj } as any ) ;
69+ // Add source with optional clustering
70+ const sourceOpts : any = { type : 'geojson' , data : gj } ;
71+ if ( cluster ) {
72+ sourceOpts . cluster = true ;
73+ sourceOpts . clusterMaxZoom = 14 ;
74+ sourceOpts . clusterRadius = 50 ;
75+ }
76+ map . addSource ( srcId , sourceOpts ) ;
77+
78+ if ( cluster ) {
79+ map . addLayer ( {
80+ id : clusterLayerId ,
81+ type : 'circle' ,
82+ source : srcId ,
83+ filter : [ 'has' , 'point_count' ] ,
84+ paint : {
85+ 'circle-color' : [
86+ 'step' ,
87+ [ 'get' , 'point_count' ] ,
88+ '#99c6f3' ,
89+ 10 ,
90+ '#66a6e8' ,
91+ 50 ,
92+ '#2f7dd1' ,
93+ ] ,
94+ 'circle-radius' : [
95+ 'step' ,
96+ [ 'get' , 'point_count' ] ,
97+ 12 ,
98+ 10 ,
99+ 16 ,
100+ 50 ,
101+ 22 ,
102+ ] ,
103+ } ,
104+ } as any ) ;
105+ map . addLayer ( {
106+ id : clusterCountId ,
107+ type : 'symbol' ,
108+ source : srcId ,
109+ filter : [ 'has' , 'point_count' ] ,
110+ layout : {
111+ 'text-field' : [ 'get' , 'point_count_abbreviated' ] ,
112+ 'text-size' : 12 ,
113+ } ,
114+ paint : { 'text-color' : '#fff' } ,
115+ } as any ) ;
116+ }
58117
59118 map . addLayer ( {
60- id : layerId ,
119+ id : pointLayerId ,
61120 type : 'circle' ,
62121 source : srcId ,
122+ filter : cluster ? [ '!' , [ 'has' , 'point_count' ] ] : undefined ,
63123 paint : {
64124 'circle-radius' : 5 ,
65125 'circle-color' : '#d61f69' ,
@@ -100,13 +160,16 @@ export default function PlaygroundPage() {
100160 < input type = "number" min = { 1 } max = { 10000 } value = { limit } onChange = { ( e ) => setLimit ( parseInt ( e . target . value || '100' , 10 ) ) } />
101161 </ div >
102162
103- < div style = { { marginTop : 8 } } >
163+ < div style = { { marginTop : 8 , display : 'flex' , gap : 16 , alignItems : 'center' , flexWrap : 'wrap' } } >
164+ < label > < input type = "checkbox" checked = { useBbox } onChange = { ( e ) => setUseBbox ( e . target . checked ) } /> Use map view as bbox</ label >
165+ < label > < input type = "checkbox" checked = { cluster } onChange = { ( e ) => setCluster ( e . target . checked ) } /> Cluster</ label >
104166 < button className = "btn" disabled = { loading } onClick = { load } > { loading ? 'Loading…' : 'Load on Map' } </ button >
105- { error && < span style = { { color : 'crimson' , marginLeft : 12 } } > Error: { error } </ span > }
167+ < button className = "btn outline" onClick = { ( ) => setBaseUrl ( envApi ) } > Use Live API</ button >
168+ < button className = "btn outline" onClick = { ( ) => setBaseUrl ( 'http://localhost:8000' ) } > Use Local</ button >
169+ { error && < span style = { { color : 'crimson' } } > Error: { error } </ span > }
106170 </ div >
107171
108172 < div ref = { mapEl } style = { { height : 540 , marginTop : 16 , borderRadius : 8 , overflow : 'hidden' , border : '1px solid #eee' } } />
109173 </ section >
110174 ) ;
111175}
112-
0 commit comments