1
1
import {
2
+ Accordion ,
2
3
Alert ,
3
- Box ,
4
4
Button ,
5
5
ButtonGroup ,
6
6
createListCollection ,
7
7
Field ,
8
8
Group ,
9
+ Heading ,
9
10
HStack ,
10
11
IconButton ,
11
12
Input ,
13
+ Link ,
12
14
Portal ,
13
15
Progress ,
14
16
Select ,
15
17
Stack ,
16
18
Switch ,
19
+ Text ,
17
20
} from "@chakra-ui/react" ;
21
+ import type { BBox } from "geojson" ;
18
22
import { useEffect , useState } from "react" ;
19
23
import { LuPause , LuPlay , LuSearch , LuX } from "react-icons/lu" ;
20
24
import { useMap } from "react-map-gl/maplibre" ;
@@ -23,8 +27,14 @@ import useStacMap from "../../hooks/stac-map";
23
27
import useStacSearch from "../../hooks/stac-search" ;
24
28
import type { StacSearch } from "../../types/stac" ;
25
29
import DownloadButtons from "../download" ;
30
+ import { SpatialExtent } from "../extents" ;
26
31
import { toaster } from "../ui/toaster" ;
27
32
33
+ interface NormalizedBbox {
34
+ bbox : BBox ;
35
+ isCrossingAntimeridian : boolean ;
36
+ }
37
+
28
38
export default function ItemSearch ( {
29
39
collection,
30
40
links,
@@ -35,6 +45,7 @@ export default function ItemSearch({
35
45
const { setItems } = useStacMap ( ) ;
36
46
const [ search , setSearch ] = useState < StacSearch > ( ) ;
37
47
const [ link , setLink ] = useState < StacLink | undefined > ( links [ 0 ] ) ;
48
+ const [ normalizedBbox , setNormalizedBbox ] = useState < NormalizedBbox > ( ) ;
38
49
const [ datetime , setDatetime ] = useState < string > ( ) ;
39
50
const [ useViewportBounds , setUseViewportBounds ] = useState ( true ) ;
40
51
const { map } = useMap ( ) ;
@@ -48,36 +59,111 @@ export default function ItemSearch({
48
59
} ) ,
49
60
} ) ;
50
61
51
- return (
52
- < Stack gap = { 4 } >
53
- < Alert . Root status = { "warning" } size = { "sm" } >
54
- < Alert . Indicator > </ Alert . Indicator >
55
- < Alert . Content >
56
- < Alert . Title > Under construction</ Alert . Title >
57
- < Alert . Description >
58
- Item search is under active development and is relatively
59
- under-powered.
60
- </ Alert . Description >
61
- </ Alert . Content >
62
- </ Alert . Root >
62
+ useEffect ( ( ) => {
63
+ function getNormalizedMapBounds ( ) {
64
+ return normalizeBbox ( map ?. getBounds ( ) . toArray ( ) . flat ( ) as BBox ) ;
65
+ }
66
+
67
+ const listener = ( ) => {
68
+ setNormalizedBbox ( getNormalizedMapBounds ( ) ) ;
69
+ } ;
63
70
64
- < Switch . Root
65
- disabled = { ! map }
66
- checked = { ! ! map && useViewportBounds }
67
- onCheckedChange = { ( e ) => setUseViewportBounds ( e . checked ) }
71
+ if ( useViewportBounds && map ) {
72
+ map . on ( "moveend" , listener ) ;
73
+ setNormalizedBbox ( getNormalizedMapBounds ( ) ) ;
74
+ } else {
75
+ map ?. off ( "moveend" , listener ) ;
76
+ setNormalizedBbox ( undefined ) ;
77
+ }
78
+ } , [ map , useViewportBounds ] ) ;
79
+
80
+ return (
81
+ < Stack >
82
+ < Accordion . Root
83
+ defaultValue = { [ "spatial" , "temporal" ] }
84
+ multiple
85
+ variant = { "enclosed" }
68
86
>
69
- < Switch . HiddenInput > </ Switch . HiddenInput >
70
- < Switch . Label > Use viewport bounds</ Switch . Label >
71
- < Switch . Control > </ Switch . Control >
72
- </ Switch . Root >
87
+ < Accordion . Item value = "spatial" >
88
+ < Accordion . ItemTrigger >
89
+ < Heading flex = "1" size = { "lg" } >
90
+ Spatial
91
+ </ Heading >
92
+ < Accordion . ItemIndicator />
93
+ </ Accordion . ItemTrigger >
94
+ < Accordion . ItemContent pb = { 4 } >
95
+ < Stack >
96
+ < Switch . Root
97
+ disabled = { ! map }
98
+ checked = { ! ! map && useViewportBounds }
99
+ onCheckedChange = { ( e ) => setUseViewportBounds ( e . checked ) }
100
+ size = { "sm" }
101
+ >
102
+ < Switch . HiddenInput > </ Switch . HiddenInput >
103
+ < Switch . Label > Use viewport bounds</ Switch . Label >
104
+ < Switch . Control > </ Switch . Control >
105
+ </ Switch . Root >
106
+ { normalizedBbox && (
107
+ < Text fontSize = { "sm" } fontWeight = { "lighter" } >
108
+ < SpatialExtent bbox = { normalizedBbox ?. bbox } > </ SpatialExtent >
109
+ </ Text >
110
+ ) }
111
+ { normalizedBbox ?. isCrossingAntimeridian && (
112
+ < Alert . Root status = { "warning" } size = { "sm" } >
113
+ < Alert . Indicator > </ Alert . Indicator >
114
+ < Alert . Content >
115
+ < Alert . Title > Antimeridian-crossing viewport</ Alert . Title >
116
+ < Alert . Description >
117
+ The viewport bounds cross the{ " " }
118
+ < Link
119
+ href = "https://en.wikipedia.org/wiki/180th_meridian"
120
+ target = "_blank"
121
+ >
122
+ antimeridian
123
+ </ Link >
124
+ , and may servers do not support antimeridian-crossing
125
+ bounding boxes. The search bounding box has been reduced
126
+ to only one side of the antimeridian.
127
+ </ Alert . Description >
128
+ </ Alert . Content >
129
+ </ Alert . Root >
130
+ ) }
131
+ </ Stack >
132
+ </ Accordion . ItemContent >
133
+ </ Accordion . Item >
73
134
74
- < Datetime
75
- interval = { collection . extent ?. temporal ?. interval [ 0 ] }
76
- setDatetime = { setDatetime }
77
- > </ Datetime >
135
+ < Accordion . Item value = "temporal" >
136
+ < Accordion . ItemTrigger >
137
+ < Heading flex = "1" size = { "lg" } >
138
+ Temporal
139
+ </ Heading >
140
+ < Accordion . ItemIndicator />
141
+ </ Accordion . ItemTrigger >
142
+ < Accordion . ItemContent pb = { 4 } >
143
+ < Datetime
144
+ interval = { collection . extent ?. temporal ?. interval [ 0 ] }
145
+ setDatetime = { setDatetime }
146
+ > </ Datetime >
147
+ </ Accordion . ItemContent >
148
+ </ Accordion . Item >
149
+ </ Accordion . Root >
78
150
79
151
< HStack >
80
- < Box flex = { 1 } > </ Box >
152
+ < Button
153
+ variant = { "solid" }
154
+ size = { "md" }
155
+ onClick = { ( ) =>
156
+ setSearch ( {
157
+ collections : [ collection . id ] ,
158
+ datetime,
159
+ bbox : normalizedBbox ?. bbox ,
160
+ } )
161
+ }
162
+ disabled = { ! ! search }
163
+ >
164
+ < LuSearch > </ LuSearch >
165
+ Search
166
+ </ Button >
81
167
82
168
< Select . Root
83
169
collection = { methods }
@@ -110,31 +196,6 @@ export default function ItemSearch({
110
196
</ Select . Positioner >
111
197
</ Portal >
112
198
</ Select . Root >
113
-
114
- < Button
115
- variant = { "surface" }
116
- onClick = { ( ) =>
117
- setSearch ( {
118
- collections : [ collection . id ] ,
119
- datetime,
120
- bbox :
121
- useViewportBounds && map
122
- ? normalizeBbox (
123
- map . getBounds ( ) . toArray ( ) . flat ( ) as [
124
- number ,
125
- number ,
126
- number ,
127
- number ,
128
- ] ,
129
- )
130
- : undefined ,
131
- } )
132
- }
133
- disabled = { ! ! search }
134
- >
135
- < LuSearch > </ LuSearch >
136
- Search
137
- </ Button >
138
199
</ HStack >
139
200
140
201
{ search && link && (
@@ -257,7 +318,7 @@ function Datetime({
257
318
} , [ startDatetime , endDatetime , setDatetime ] ) ;
258
319
259
320
return (
260
- < Stack >
321
+ < Stack gap = { 4 } >
261
322
< DatetimeInput
262
323
label = "Start datetime"
263
324
datetime = { startDatetime }
@@ -268,17 +329,16 @@ function Datetime({
268
329
datetime = { endDatetime }
269
330
setDatetime = { setEndDatetime }
270
331
> </ DatetimeInput >
271
- < HStack >
272
- < Button
273
- variant = { "outline" }
274
- onClick = { ( ) => {
275
- setStartDatetime ( interval ?. [ 0 ] ? new Date ( interval [ 0 ] ) : undefined ) ;
276
- setEndDatetime ( interval ?. [ 1 ] ? new Date ( interval [ 1 ] ) : undefined ) ;
277
- } }
278
- >
279
- Set to collection extents
280
- </ Button >
281
- </ HStack >
332
+ < Button
333
+ size = { "sm" }
334
+ variant = { "outline" }
335
+ onClick = { ( ) => {
336
+ setStartDatetime ( interval ?. [ 0 ] ? new Date ( interval [ 0 ] ) : undefined ) ;
337
+ setEndDatetime ( interval ?. [ 1 ] ? new Date ( interval [ 1 ] ) : undefined ) ;
338
+ } }
339
+ >
340
+ Set to collection extents
341
+ </ Button >
282
342
</ Stack >
283
343
) ;
284
344
}
@@ -354,25 +414,29 @@ function DatetimeInput({
354
414
) ;
355
415
}
356
416
357
- function normalizeBbox ( bbox : [ number , number , number , number ] ) {
417
+ function normalizeBbox ( bbox : BBox ) : NormalizedBbox {
358
418
if ( bbox [ 2 ] - bbox [ 0 ] >= 360 ) {
359
- return [ - 180 , bbox [ 1 ] , 180 , bbox [ 3 ] ] ;
419
+ return {
420
+ bbox : [ - 180 , bbox [ 1 ] , 180 , bbox [ 3 ] ] ,
421
+ isCrossingAntimeridian : false ,
422
+ } ;
360
423
} else if ( bbox [ 0 ] < - 180 ) {
361
424
return normalizeBbox ( [ bbox [ 0 ] + 360 , bbox [ 1 ] , bbox [ 2 ] + 360 , bbox [ 3 ] ] ) ;
362
425
} else if ( bbox [ 0 ] > 180 ) {
363
426
return normalizeBbox ( [ bbox [ 0 ] - 360 , bbox [ 1 ] , bbox [ 2 ] - 360 , bbox [ 3 ] ] ) ;
364
427
} else if ( bbox [ 2 ] > 180 ) {
365
- // Antimeridian-crossing
366
- toaster . create ( {
367
- type : "info" ,
368
- title : "Viewport crosses the antimeridian" ,
369
- description :
370
- "The viewport crosses the antimeridian, and many STAC API servers do not support bounding boxes that cross +/- 180° longitude. We're narrowing the viewport to only search to only one side." ,
371
- } ) ;
372
428
if ( ( bbox [ 0 ] + bbox [ 2 ] ) / 2 > 180 ) {
373
- return [ - 180 , bbox [ 1 ] , bbox [ 2 ] - 360 , bbox [ 3 ] ] ;
429
+ return {
430
+ bbox : [ - 180 , bbox [ 1 ] , bbox [ 2 ] - 360 , bbox [ 3 ] ] ,
431
+ isCrossingAntimeridian : true ,
432
+ } ;
374
433
} else {
375
- return [ bbox [ 0 ] , bbox [ 1 ] , 180 , bbox [ 3 ] ] ;
434
+ return {
435
+ bbox : [ bbox [ 0 ] , bbox [ 1 ] , 180 , bbox [ 3 ] ] ,
436
+ isCrossingAntimeridian : true ,
437
+ } ;
376
438
}
439
+ } else {
440
+ return { bbox : bbox , isCrossingAntimeridian : false } ;
377
441
}
378
442
}
0 commit comments