@@ -9,6 +9,7 @@ import maplibre from 'maplibre-gl';
9
9
import {
10
10
TerraDraw ,
11
11
TerraDrawRectangleMode ,
12
+ TerraDrawPolygonMode ,
12
13
TerraDrawRenderMode
13
14
} from 'terra-draw' ;
14
15
import { TerraDrawMapLibreGLAdapter } from 'terra-draw-maplibre-gl-adapter' ;
@@ -23,16 +24,14 @@ import { nominatimSearch } from '../../network/nominatim';
23
24
const LocationSelect = props => {
24
25
const { name, value, placeholder, onChange } = props ;
25
26
26
- // State
27
27
const [ queryType , setQueryType ] = useState ( 'q' ) ;
28
28
const [ isLoading , setIsLoading ] = useState ( false ) ;
29
29
const [ inputValue , setInputValue ] = useState ( '' ) ;
30
+ const [ activeMode , setActiveMode ] = useState ( 'render' ) ;
30
31
31
- // Refs
32
32
const mapRef = useRef ( null ) ;
33
33
const drawRef = useRef ( null ) ;
34
34
35
- // Query type options
36
35
const queryTypeOptions = [
37
36
{ value : 'q' , label : 'Any' } ,
38
37
{ value : 'city' , label : 'City' } ,
@@ -41,7 +40,6 @@ const LocationSelect = props => {
41
40
{ value : 'country' , label : 'Country' }
42
41
] ;
43
42
44
- // Update map with new data
45
43
const updateMap = useCallback (
46
44
data => {
47
45
const map = mapRef . current ;
@@ -52,13 +50,7 @@ const LocationSelect = props => {
52
50
if ( map . getSource ( 'feature' ) ) {
53
51
map . getSource ( 'feature' ) . setData ( data ) ;
54
52
} else {
55
- map . addSource ( 'feature' , {
56
- type : 'geojson' ,
57
- data : {
58
- type : 'Feature' ,
59
- geometry : data
60
- }
61
- } ) ;
53
+ map . addSource ( 'feature' , { type : 'geojson' , data } ) ;
62
54
}
63
55
64
56
if ( map . getLayer ( 'geometry' ) === undefined ) {
@@ -68,23 +60,18 @@ const LocationSelect = props => {
68
60
source : 'feature' ,
69
61
paint : {
70
62
'fill-color' : '#088' ,
71
- 'fill-opacity' : 0.6
63
+ 'fill-opacity' : 0.3
72
64
}
73
65
} ) ;
74
66
}
75
67
76
- onChange ( 'in_bbox' , undefined ) ;
77
- onChange ( 'geometry' , fromJS ( [ { label : data , value : data } ] ) ) ;
78
-
79
68
const bounds = bbox ( data ) ;
80
69
map . fitBounds ( [ bounds . slice ( 0 , 2 ) , bounds . slice ( 2 ) ] , { padding : 20 } ) ;
81
70
} ,
82
71
[ onChange ]
83
72
) ;
84
73
85
- // Initialize map and draw
86
74
useEffect ( ( ) => {
87
- // Create map
88
75
const map = new maplibre . Map ( {
89
76
container : 'geometry-map' ,
90
77
style : '/positron.json'
@@ -96,11 +83,11 @@ const LocationSelect = props => {
96
83
map . touchZoomRotate . disableRotation ( ) ;
97
84
map . keyboard . disableRotation ( ) ;
98
85
99
- // Create draw
100
86
const draw = new TerraDraw ( {
101
87
adapter : new TerraDrawMapLibreGLAdapter ( { map, lib : maplibre } ) ,
102
88
modes : [
103
89
new TerraDrawRectangleMode ( ) ,
90
+ new TerraDrawPolygonMode ( ) ,
104
91
new TerraDrawRenderMode ( { modeName : 'render' } )
105
92
]
106
93
} ) ;
@@ -110,67 +97,49 @@ const LocationSelect = props => {
110
97
draw . on ( 'finish' , ( id , context ) => {
111
98
const snapshot = draw . getSnapshot ( ) ;
112
99
const feature = snapshot . find ( f => f . id === id ) ;
113
- const bounds = bbox ( feature ) ; // even though the user drew a box, 'feature' is a Polygon
114
- const wsen = bounds . map ( v => v . toFixed ( 4 ) ) . join ( ',' ) ;
115
100
116
- onChange ( 'geometry' , undefined ) ;
117
- onChange ( 'in_bbox' , fromJS ( [ { label : wsen , value : wsen } ] ) ) ;
101
+ if ( feature . geometry . type === 'Polygon' ) {
102
+ if ( draw . getMode ( ) === 'rectangle' ) {
103
+ const bounds = bbox ( feature ) ;
104
+ const wsen = bounds . map ( v => v . toFixed ( 4 ) ) . join ( ',' ) ;
105
+ onChange ( 'geometry' , null ) ;
106
+ onChange ( 'in_bbox' , fromJS ( [ { label : wsen , value : wsen } ] ) ) ;
107
+ } else {
108
+ onChange (
109
+ 'geometry' ,
110
+ fromJS ( [ { label : feature . geometry , value : feature . geometry } ] )
111
+ ) ;
112
+ onChange ( 'in_bbox' , null ) ;
113
+ }
114
+ }
115
+
116
+ // Set mode back to render after completing a shape
117
+ draw . setMode ( 'render' ) ;
118
+ setActiveMode ( 'render' ) ;
119
+ updateMap ( feature . geometry ) ;
118
120
} ) ;
119
121
120
- // Store refs
121
122
mapRef . current = map ;
122
123
drawRef . current = draw ;
123
124
124
- // Set up event listeners
125
- const handleKeyDown = event => {
126
- if ( event . key === 'Shift' ) {
127
- draw . clear ( ) ;
128
- map . getSource ( 'feature' ) ?. setData ( {
129
- type : 'Feature' ,
130
- geometry : null
131
- } ) ;
132
- draw . setMode ( 'rectangle' ) ;
133
- }
134
- } ;
135
-
136
- const handleKeyUp = event => {
137
- if ( event . key === 'Shift' ) {
138
- draw . setMode ( 'render' ) ;
139
- }
140
- } ;
141
-
142
- document . addEventListener ( 'keydown' , handleKeyDown ) ;
143
- document . addEventListener ( 'keyup' , handleKeyUp ) ;
144
-
145
125
map . on ( 'style.load' , ( ) => {
146
126
map . setProjection ( { type : 'globe' } ) ;
147
127
148
128
// Display initial bbox or polygon (if it exists) on the map
149
129
if ( value && value . size > 0 ) {
150
130
const { value : geometry } = value . get ( 0 ) . toJS ( ) ;
151
- console . log ( 'geometry' , geometry ) ;
152
131
if ( geometry && typeof geometry === 'object' ) {
153
132
// geometry is a GeoJSON polygon
154
133
updateMap ( geometry ) ;
155
134
} else if ( geometry && typeof geometry === 'string' ) {
156
135
// geometry is a bbox string (WSEN, comma-separated)
157
136
const bounds = geometry . split ( ',' ) . map ( Number ) ;
158
- console . log ( 'bounds' , bounds ) ;
159
- console . log ( 'bboxPolygon' , bboxPolygon ( bounds ) ) ;
160
137
updateMap ( bboxPolygon ( bounds ) . geometry ) ;
161
138
}
162
139
}
163
140
} ) ;
164
141
165
- // Cleanup
166
- return ( ) => {
167
- document . removeEventListener ( 'keydown' , handleKeyDown ) ;
168
- document . removeEventListener ( 'keyup' , handleKeyUp ) ;
169
-
170
- if ( map ) {
171
- map . remove ( ) ;
172
- }
173
- } ;
142
+ return ( ) => map ?. remove ( ) ;
174
143
} , [ onChange , updateMap ] ) ;
175
144
176
145
// Check if one character input is allowed (for East Asian languages)
@@ -220,15 +189,13 @@ const LocationSelect = props => {
220
189
[ queryType , isOneCharInputAllowed ]
221
190
) ;
222
191
223
- // Create debounced version of loadOptions
224
192
const debouncedLoadOptions = useCallback (
225
193
debounce ( ( inputValue , callback ) => {
226
194
loadOptions ( inputValue ) . then ( callback ) ;
227
195
} , 500 ) ,
228
196
[ loadOptions ]
229
197
) ;
230
198
231
- // Handle selection change
232
199
const handleChange = useCallback (
233
200
selectedOption => {
234
201
if ( selectedOption ) {
@@ -252,16 +219,41 @@ const LocationSelect = props => {
252
219
[ updateMap ]
253
220
) ;
254
221
255
- // Handle query type change
256
222
const handleQueryTypeChange = useCallback ( selectedOption => {
257
223
setQueryType ( selectedOption . value ) ;
258
224
} , [ ] ) ;
259
225
260
- // Handle input change
261
226
const handleInputChange = useCallback ( newValue => {
262
227
setInputValue ( newValue ) ;
263
228
} , [ ] ) ;
264
229
230
+ const handleModeChange = useCallback ( mode => {
231
+ const draw = drawRef . current ;
232
+ if ( draw ) {
233
+ draw . clear ( ) ;
234
+ draw . setMode ( mode ) ;
235
+ setActiveMode ( mode ) ;
236
+ }
237
+ } , [ ] ) ;
238
+
239
+ const handleClear = useCallback ( ( ) => {
240
+ const draw = drawRef . current ;
241
+ const map = mapRef . current ;
242
+ if ( draw ) {
243
+ draw . clear ( ) ;
244
+ draw . setMode ( 'render' ) ;
245
+ setActiveMode ( 'render' ) ;
246
+ }
247
+ if ( map && map . getSource ( 'feature' ) ) {
248
+ map . getSource ( 'feature' ) . setData ( {
249
+ type : 'Feature' ,
250
+ geometry : null
251
+ } ) ;
252
+ }
253
+ onChange ( 'geometry' , null ) ;
254
+ onChange ( 'in_bbox' , null ) ;
255
+ } , [ onChange ] ) ;
256
+
265
257
return (
266
258
< div >
267
259
< div className = "grid grid--gut12" >
@@ -290,12 +282,59 @@ const LocationSelect = props => {
290
282
</ div >
291
283
</ div >
292
284
< div className = "grid grid--gut12 pt6" >
293
- < div className = "col col--12 map-select" >
285
+ < div className = "col col--12" >
286
+ < div className = "flex-parent flex-parent--row flex-parent--center-cross mb6" >
287
+ < div className = "flex-child flex-child--no-shrink mr6" >
288
+ < button
289
+ className = { `btn btn--s border border--1 border--darken5 border--darken25-on-hover round bg-darken10 bg-darken5-on-hover color-gray transition ${
290
+ activeMode === 'rectangle'
291
+ ? 'bg-darken25 bg-darken25-on-hover'
292
+ : ''
293
+ } `}
294
+ onClick = { ( ) => handleModeChange ( 'rectangle' ) }
295
+ >
296
+ < svg className = "icon h18 w18 inline-block align-middle" >
297
+ < use xlinkHref = "#icon-polygon" />
298
+ </ svg >
299
+ Box
300
+ </ button >
301
+ </ div >
302
+ < div className = "flex-child flex-child--no-shrink mr6" >
303
+ < button
304
+ className = { `btn btn--s border border--1 border--darken5 border--darken25-on-hover round bg-darken10 bg-darken5-on-hover color-gray transition ${
305
+ activeMode === 'polygon'
306
+ ? 'bg-darken25 bg-darken25-on-hover'
307
+ : ''
308
+ } `}
309
+ onClick = { ( ) => handleModeChange ( 'polygon' ) }
310
+ >
311
+ < svg className = "icon h18 w18 inline-block align-middle" >
312
+ < use xlinkHref = "#icon-pencil" />
313
+ </ svg >
314
+ Polygon
315
+ </ button >
316
+ </ div >
317
+ < div className = "flex-child flex-child--no-shrink" >
318
+ < button
319
+ className = "btn btn--s border border--1 border--darken5 border--darken25-on-hover round bg-darken10 bg-darken5-on-hover color-gray transition"
320
+ onClick = { handleClear }
321
+ title = "Clear selection"
322
+ >
323
+ < svg className = "icon h18 w18 inline-block align-middle" >
324
+ < use xlinkHref = "#icon-close" />
325
+ </ svg >
326
+ Clear
327
+ </ button >
328
+ </ div >
329
+ </ div >
294
330
< div id = "geometry-map" />
295
331
</ div >
296
332
</ div >
297
333
< p >
298
- Hold < kbd > Shift</ kbd > and click to draw a bounding box.
334
+ { activeMode == 'rectangle' &&
335
+ 'Click two corners to draw a bounding box.' }
336
+ { activeMode == 'polygon' &&
337
+ 'Click a series of points to draw a polygon; click back on the first point to finish.' }
299
338
</ p >
300
339
</ div >
301
340
) ;
0 commit comments