Skip to content

Commit 7ace8c7

Browse files
authored
1 parent 03519bc commit 7ace8c7

File tree

7 files changed

+250
-124
lines changed

7 files changed

+250
-124
lines changed

src/app.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { StacCollection, StacItem } from "stac-ts";
44
import Map from "./components/map";
55
import Overlay from "./components/overlay";
66
import { Toaster } from "./components/ui/toaster";
7+
import useStacChildren from "./hooks/stac-children";
78
import useStacValue from "./hooks/stac-value";
89
import type { BBox2D, Color } from "./types/map";
910
import type { DatetimeBounds, StacValue } from "./types/stac";
@@ -17,6 +18,7 @@ export default function App() {
1718
// State
1819
const [href, setHref] = useState<string | undefined>(getInitialHref());
1920
const fileUpload = useFileUpload({ maxFiles: 1 });
21+
const [userCollections, setCollections] = useState<StacCollection[]>();
2022
const [userItems, setItems] = useState<StacItem[]>();
2123
const [picked, setPicked] = useState<StacValue>();
2224
const [bbox, setBbox] = useState<BBox2D>();
@@ -28,8 +30,6 @@ export default function App() {
2830
const {
2931
value,
3032
error,
31-
collections,
32-
catalogs,
3333
items: linkedItems,
3434
table,
3535
stacGeoparquetItem,
@@ -39,6 +39,12 @@ export default function App() {
3939
datetimeBounds: filter ? datetimeBounds : undefined,
4040
stacGeoparquetItemId,
4141
});
42+
const collectionsLink = value?.links?.find((link) => link.rel === "data");
43+
const { catalogs, collections: linkedCollections } = useStacChildren({
44+
value,
45+
enabled: !!value && !collectionsLink,
46+
});
47+
const collections = collectionsLink ? userCollections : linkedCollections;
4248
const items = userItems || linkedItems;
4349
const filteredCollections = useMemo(() => {
4450
if (filter && collections) {
@@ -161,6 +167,7 @@ export default function App() {
161167
value={value}
162168
error={error}
163169
catalogs={catalogs}
170+
setCollections={setCollections}
164171
collections={collections}
165172
filteredCollections={filteredCollections}
166173
filter={filter}

src/components/overlay.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default function Overlay({
2626
value,
2727
error,
2828
catalogs,
29+
setCollections,
2930
collections,
3031
filteredCollections,
3132
filter,
@@ -43,6 +44,7 @@ export default function Overlay({
4344
error: Error | undefined;
4445
value: StacValue | undefined;
4546
catalogs: StacCatalog[] | undefined;
47+
setCollections: (collections: StacCollection[] | undefined) => void;
4648
collections: StacCollection[] | undefined;
4749
filteredCollections: StacCollection[] | undefined;
4850
fileUpload: UseFileUploadReturn;
@@ -86,6 +88,7 @@ export default function Overlay({
8688
value={picked || value}
8789
error={error}
8890
catalogs={catalogs}
91+
setCollections={setCollections}
8992
collections={collections}
9093
filteredCollections={filteredCollections}
9194
fileUpload={fileUpload}

src/components/panel.tsx

Lines changed: 163 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,27 @@ import {
99
LuFolderSearch,
1010
LuLink,
1111
LuList,
12+
LuPause,
13+
LuPlay,
1214
LuSearch,
15+
LuStepForward,
1316
} from "react-icons/lu";
1417
import {
1518
Accordion,
1619
Alert,
1720
Box,
21+
Button,
22+
ButtonGroup,
23+
Card,
24+
Heading,
1825
HStack,
1926
Icon,
2027
SkeletonText,
28+
Stack,
2129
type UseFileUploadReturn,
2230
} from "@chakra-ui/react";
23-
import { useQuery } from "@tanstack/react-query";
24-
import type { StacCatalog, StacCollection, StacItem } from "stac-ts";
31+
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
32+
import type { StacCatalog, StacCollection, StacItem, StacLink } from "stac-ts";
2533
import Assets from "./assets";
2634
import Catalogs from "./catalogs";
2735
import CollectionSearch from "./collection-search";
@@ -37,6 +45,7 @@ import type { BBox2D } from "../types/map";
3745
import type {
3846
DatetimeBounds,
3947
StacAssets,
48+
StacCollections,
4049
StacSearch,
4150
StacValue,
4251
} from "../types/stac";
@@ -46,6 +55,7 @@ export default function Panel({
4655
value,
4756
error,
4857
catalogs,
58+
setCollections,
4959
collections,
5060
filteredCollections,
5161
items,
@@ -62,6 +72,7 @@ export default function Panel({
6272
value: StacValue | undefined;
6373
error: Error | undefined;
6474
catalogs: StacCatalog[] | undefined;
75+
setCollections: (collections: StacCollection[] | undefined) => void;
6576
collections: StacCollection[] | undefined;
6677
filteredCollections: StacCollection[] | undefined;
6778
items: StacItem[] | undefined;
@@ -76,15 +87,8 @@ export default function Panel({
7687
setDatetimeBounds: (bounds: DatetimeBounds | undefined) => void;
7788
}) {
7889
const [search, setSearch] = useState<StacSearch>();
79-
const rootHref = value?.links?.find((link) => link.rel === "root")?.href;
80-
const rootData = useQuery<StacValue>({
81-
queryKey: ["stac-value", rootHref],
82-
enabled: !!rootHref,
83-
queryFn: () => fetchStac(rootHref),
84-
});
85-
const searchLinks = rootData.data?.links?.filter(
86-
(link) => link.rel === "search"
87-
);
90+
const [numberOfCollections, setNumberOfCollections] = useState<number>();
91+
const [fetchAllCollections, setFetchAllCollections] = useState(false);
8892
const { links, assets, properties } = useMemo(() => {
8993
if (value) {
9094
if (value.type === "Feature") {
@@ -101,50 +105,165 @@ export default function Panel({
101105
return { links: undefined, assets: undefined, properties: undefined };
102106
}
103107
}, [value]);
108+
const { rootLink, collectionsLink, nextLink, prevLink, filteredLinks } =
109+
useMemo(() => {
110+
let rootLink: StacLink | undefined = undefined;
111+
let collectionsLink: StacLink | undefined = undefined;
112+
let nextLink: StacLink | undefined = undefined;
113+
let prevLink: StacLink | undefined = undefined;
114+
const filteredLinks = [];
115+
if (links) {
116+
for (const link of links) {
117+
switch (link.rel) {
118+
case "root":
119+
rootLink = link;
120+
break;
121+
case "data":
122+
collectionsLink = link;
123+
break;
124+
case "next":
125+
nextLink = link;
126+
break;
127+
case "previous":
128+
prevLink = link;
129+
break;
130+
}
131+
// We already show children and items in their own pane
132+
if (link.rel !== "child" && link.rel !== "item")
133+
filteredLinks.push(link);
134+
}
135+
}
136+
return { rootLink, collectionsLink, nextLink, prevLink, filteredLinks };
137+
}, [links]);
138+
const rootData = useQuery<StacValue | undefined>({
139+
queryKey: ["stac-value", rootLink?.href],
140+
enabled: !!rootLink,
141+
queryFn: () => rootLink && fetchStac(rootLink.href),
142+
});
143+
const searchLinks = useMemo(() => {
144+
return rootData.data?.links?.filter((link) => link.rel === "search");
145+
}, [rootData.data]);
146+
const collectionsResult = useInfiniteQuery({
147+
queryKey: ["stac-collections", collectionsLink?.href],
148+
queryFn: async ({ pageParam }) => {
149+
if (pageParam) {
150+
return await fetch(pageParam).then((response) => {
151+
if (response.ok) return response.json();
152+
else
153+
throw new Error(
154+
`Error while fetching collections from ${pageParam}`
155+
);
156+
});
157+
} else {
158+
return null;
159+
}
160+
},
161+
initialPageParam: collectionsLink?.href,
162+
getNextPageParam: (lastPage: StacCollections | null) =>
163+
lastPage?.links?.find((link) => link.rel == "next")?.href,
164+
enabled: !!collectionsLink,
165+
});
166+
useEffect(() => {
167+
setCollections(
168+
collectionsResult.data?.pages.flatMap((page) => page?.collections || [])
169+
);
170+
if (collectionsResult.data?.pages.at(0)?.numberMatched)
171+
setNumberOfCollections(collectionsResult.data?.pages[0]?.numberMatched);
172+
}, [collectionsResult.data, setCollections]);
173+
useEffect(() => {
174+
if (
175+
fetchAllCollections &&
176+
!collectionsResult.isFetching &&
177+
collectionsResult.hasNextPage
178+
)
179+
collectionsResult.fetchNextPage();
180+
}, [fetchAllCollections, collectionsResult]);
181+
useEffect(() => {
182+
setFetchAllCollections(false);
183+
setNumberOfCollections(undefined);
184+
}, [value]);
104185

105186
// Handled by the value
106187
if (properties?.description) delete properties["description"];
107-
const thumbnailAsset =
108-
assets &&
109-
((Object.keys(assets).includes("thumbnail") && assets["thumbnail"]) ||
110-
Object.values(assets).find((asset) =>
111-
asset.roles?.includes("thumbnail")
112-
));
113-
const nextLink = links?.find((link) => link.rel === "next");
114-
const prevLink = links?.find((link) => link.rel === "previous");
115-
// We already provide linked children and items in their own pane.
116-
const filteredLinks = links?.filter(
117-
(link) => link.rel !== "child" && link.rel !== "item"
118-
);
188+
const thumbnailAsset = useMemo(() => {
189+
return (
190+
assets &&
191+
((Object.keys(assets).includes("thumbnail") && assets["thumbnail"]) ||
192+
Object.values(assets).find((asset) =>
193+
asset.roles?.includes("thumbnail")
194+
))
195+
);
196+
}, [assets]);
119197

120198
useEffect(() => {
121199
setItems(undefined);
122200
}, [search, setItems]);
123201

124202
return (
125203
<Box p={4} overflow={"scroll"} maxH={"80dvh"}>
126-
{(href && value && (
127-
<Value
128-
value={value}
129-
thumbnailAsset={thumbnailAsset}
130-
href={href}
131-
setHref={setHref}
132-
nextLink={nextLink}
133-
prevLink={prevLink}
134-
/>
135-
)) ||
136-
(error && (
137-
<Alert.Root status={"error"}>
138-
<Alert.Indicator />
139-
<Alert.Content>
140-
<Alert.Title>Error while fetching STAC value</Alert.Title>
141-
<Alert.Description>{error.toString()}</Alert.Description>
142-
</Alert.Content>
143-
</Alert.Root>
204+
<Stack gap={4}>
205+
{(href && value && (
206+
<Value
207+
value={value}
208+
thumbnailAsset={thumbnailAsset}
209+
href={href}
210+
setHref={setHref}
211+
nextLink={nextLink}
212+
prevLink={prevLink}
213+
/>
144214
)) ||
145-
(href && <SkeletonText />) || (
146-
<Introduction setHref={setHref} fileUpload={fileUpload} />
215+
(error && (
216+
<Alert.Root status={"error"}>
217+
<Alert.Indicator />
218+
<Alert.Content>
219+
<Alert.Title>Error while fetching STAC value</Alert.Title>
220+
<Alert.Description>{error.toString()}</Alert.Description>
221+
</Alert.Content>
222+
</Alert.Root>
223+
)) ||
224+
(href && <SkeletonText />) || (
225+
<Introduction setHref={setHref} fileUpload={fileUpload} />
226+
)}
227+
228+
{collectionsResult.hasNextPage && (
229+
<Card.Root size={"sm"} variant={"outline"}>
230+
<Card.Header>
231+
<Heading size={"sm"}>Collection pagination</Heading>
232+
</Card.Header>
233+
<Card.Body>
234+
<ButtonGroup size={"xs"} variant={"surface"}>
235+
<Button
236+
disabled={fetchAllCollections || collectionsResult.isFetching}
237+
onClick={() => {
238+
if (
239+
!collectionsResult.isFetching &&
240+
collectionsResult.hasNextPage
241+
)
242+
collectionsResult.fetchNextPage();
243+
}}
244+
>
245+
Fetch more collections <LuStepForward />
246+
</Button>
247+
<Button
248+
onClick={() =>
249+
setFetchAllCollections((previous) => !previous)
250+
}
251+
>
252+
{(fetchAllCollections && (
253+
<>
254+
Pause fetching collections <LuPause />
255+
</>
256+
)) || (
257+
<>
258+
Fetch all collections <LuPlay />
259+
</>
260+
)}
261+
</Button>
262+
</ButtonGroup>
263+
</Card.Body>
264+
</Card.Root>
147265
)}
266+
</Stack>
148267

149268
{value && (
150269
<Accordion.Root multiple size={"sm"} variant={"enclosed"} mt={4}>
@@ -164,7 +283,7 @@ export default function Panel({
164283
<>
165284
Collections{" "}
166285
{(filteredCollections &&
167-
`(${filteredCollections?.length}/${collections.length})`) ||
286+
`(${filteredCollections?.length}/${numberOfCollections || collections.length})`) ||
168287
`(${collections.length})`}
169288
</>
170289
}

src/hooks/stac-children.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useMemo } from "react";
2+
import { useQueries } from "@tanstack/react-query";
3+
import type { StacValue } from "../types/stac";
4+
import { getStacJsonValue } from "../utils/stac";
5+
6+
export default function useStacChildren({
7+
value,
8+
enabled,
9+
}: {
10+
value: StacValue | undefined;
11+
enabled: boolean;
12+
}) {
13+
const results = useQueries({
14+
queries:
15+
value?.links
16+
?.filter((link) => link.rel === "child")
17+
.map((link) => {
18+
return {
19+
queryKey: ["stac-value", link.href],
20+
queryFn: () => getStacJsonValue(link.href),
21+
enabled: enabled,
22+
};
23+
}) || [],
24+
combine: (results) => {
25+
return {
26+
data: results.map((result) => result.data),
27+
};
28+
},
29+
});
30+
31+
return useMemo(() => {
32+
const collections = [];
33+
const catalogs = [];
34+
for (const value of results.data) {
35+
switch (value?.type) {
36+
case "Catalog":
37+
catalogs.push(value);
38+
break;
39+
case "Collection":
40+
collections.push(value);
41+
break;
42+
}
43+
}
44+
return {
45+
collections: collections.length > 0 ? collections : undefined,
46+
catalogs: catalogs.length > 0 ? catalogs : undefined,
47+
};
48+
}, [results.data]);
49+
}

0 commit comments

Comments
 (0)