Skip to content

Commit 16bf481

Browse files
authored
feat: item search (#49)
* feat: use visibility * feat: it's working * feat: button size sm * feat: add focus for item * feat: download * feat: remove css * fix: remove css
1 parent 4e5b7b8 commit 16bf481

File tree

14 files changed

+599
-108
lines changed

14 files changed

+599
-108
lines changed

src/components/stac/asset.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
Card,
3-
EmptyState,
43
HStack,
54
IconButton,
65
type IconButtonProps,
@@ -10,7 +9,7 @@ import {
109
Text,
1110
} from "@chakra-ui/react";
1211
import { useEffect, useState } from "react";
13-
import { LuDownload, LuImageMinus } from "react-icons/lu";
12+
import { LuDownload } from "react-icons/lu";
1413
import type { StacAsset } from "stac-ts";
1514

1615
export function Assets({ assets }: { assets: { [k: string]: StacAsset } }) {
@@ -31,9 +30,6 @@ export function AssetCard({
3130
asset: StacAsset;
3231
}) {
3332
const [showImage, setShowImage] = useState(false);
34-
const [description, setDescription] = useState(
35-
"Asset is not JPEG or PNG media type",
36-
);
3733

3834
const iconButtonProps: IconButtonProps = {
3935
size: "2xs",
@@ -52,33 +48,18 @@ export function AssetCard({
5248
</Text>
5349
</Card.Header>
5450
<Card.Body>
55-
{(showImage && (
51+
{showImage && (
5652
<Image
5753
src={asset.href}
5854
onError={() => {
59-
setDescription("Image failed to load");
6055
setShowImage(false);
6156
}}
6257
></Image>
63-
)) || (
64-
<EmptyState.Root size={"sm"}>
65-
<EmptyState.Content>
66-
<EmptyState.Indicator>
67-
<LuImageMinus></LuImageMinus>
68-
</EmptyState.Indicator>
69-
<EmptyState.Title fontSize={"sm"}>
70-
Cannot display
71-
</EmptyState.Title>
72-
<EmptyState.Description fontSize={"xs"}>
73-
{description}
74-
</EmptyState.Description>
75-
</EmptyState.Content>
76-
</EmptyState.Root>
7758
)}
59+
<Text fontSize={"2xs"}>{asset.type}</Text>
7860
</Card.Body>
7961
<Card.Footer>
8062
<Stack gap={4}>
81-
<Text fontSize={"2xs"}>{asset.type}</Text>
8263
<HStack>
8364
<IconButton {...iconButtonProps} asChild>
8465
<a href={asset.href}>

src/components/stac/hooks.ts

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import type { UseFileUploadReturn } from "@chakra-ui/react";
2-
import { useQuery } from "@tanstack/react-query";
2+
import {
3+
useInfiniteQuery,
4+
useQuery,
5+
type DefaultError,
6+
type InfiniteData,
7+
type QueryKey,
8+
} from "@tanstack/react-query";
39
import { isParquetFile, useDuckDb } from "duckdb-wasm-kit";
410
import { useEffect, useState } from "react";
5-
import type { StacCollection } from "stac-ts";
11+
import type { StacCollection, StacItem } from "stac-ts";
612
import { toaster } from "../ui/toaster";
713
import type {
814
NaturalLanguageCollectionSearchResult,
915
StacCollections,
1016
StacItemCollection,
17+
StacSearchRequest,
1118
StacValue,
1219
} from "./types";
1320

@@ -79,6 +86,7 @@ export function useStacValue(
7986
return null;
8087
}
8188
},
89+
enabled: !!href,
8290
});
8391

8492
useEffect(() => {
@@ -110,7 +118,6 @@ export function useStacValue(
110118

111119
export function useStacCollections(href: string | undefined) {
112120
const [currentHref, setCurrentHref] = useState<string | undefined>();
113-
const [pages, setPages] = useState<StacCollections[]>([]);
114121
const [collections, setCollections] = useState<
115122
StacCollection[] | undefined
116123
>();
@@ -131,11 +138,11 @@ export function useStacCollections(href: string | undefined) {
131138
return null;
132139
}
133140
},
141+
enabled: !!href,
134142
});
135143

136144
useEffect(() => {
137145
setCurrentHref(href);
138-
setPages([]);
139146
setCollections(undefined);
140147
}, [href]);
141148

@@ -151,7 +158,10 @@ export function useStacCollections(href: string | undefined) {
151158

152159
useEffect(() => {
153160
if (data) {
154-
setPages((pages) => [...pages, data]);
161+
setCollections((collections) => [
162+
...(collections ?? []),
163+
...data.collections,
164+
]);
155165
if (data) {
156166
const nextLink = data.links?.find((link) => link.rel == "next");
157167
if (nextLink) {
@@ -161,15 +171,103 @@ export function useStacCollections(href: string | undefined) {
161171
}
162172
}, [data]);
163173

174+
return { collections, isPending, error };
175+
}
176+
177+
export function useItemSearch(searchRequest: StacSearchRequest | undefined) {
178+
const [items, setItems] = useState<StacItem[] | undefined>();
179+
const [numberMatched, setNumberMatched] = useState<number | undefined>();
180+
181+
const { data, error, hasNextPage, isFetching, fetchNextPage } =
182+
useInfiniteQuery<
183+
StacItemCollection | null,
184+
DefaultError,
185+
InfiniteData<StacItemCollection | null>,
186+
QueryKey,
187+
StacSearchRequest | undefined
188+
>({
189+
queryKey: ["item-search", searchRequest],
190+
queryFn: async ({ pageParam }) => {
191+
if (pageParam) {
192+
const url = new URL(pageParam.link.href);
193+
const method = (pageParam.link.method as string | undefined) || "GET";
194+
const init: RequestInit = {
195+
method,
196+
headers: {
197+
Accept: "application/json",
198+
"Content-Type": "application/json",
199+
},
200+
};
201+
if (method === "GET") {
202+
if (pageParam.search.collections) {
203+
url.searchParams.set(
204+
"collections",
205+
pageParam.search.collections.join(","),
206+
);
207+
}
208+
} else {
209+
if (pageParam.link.body) {
210+
init.body = JSON.stringify(pageParam.link.body);
211+
} else {
212+
init.body = JSON.stringify(pageParam.search);
213+
}
214+
}
215+
return await fetch(url, init).then((response) => {
216+
if (response.ok) {
217+
return response.json();
218+
} else {
219+
throw new Error(
220+
`Error while searching ${url}: ${response.statusText}`,
221+
);
222+
}
223+
});
224+
} else {
225+
return null;
226+
}
227+
},
228+
initialPageParam: searchRequest,
229+
getNextPageParam: (lastPage: StacItemCollection | null) => {
230+
if (lastPage) {
231+
const nextLink = lastPage.links?.find((link) => link.rel == "next");
232+
if (nextLink && searchRequest) {
233+
return {
234+
search: searchRequest.search,
235+
link: nextLink,
236+
};
237+
}
238+
}
239+
},
240+
enabled: !!searchRequest,
241+
});
242+
164243
useEffect(() => {
165-
if (pages.length > 0) {
166-
setCollections(pages.flatMap((page) => page.collections));
244+
if (data) {
245+
setItems(data.pages.flatMap((page) => page?.features || []));
246+
if (data.pages.length > 0 && data.pages[0]?.numberMatched) {
247+
setNumberMatched(data.pages[0].numberMatched);
248+
}
167249
} else {
168-
setCollections(undefined);
250+
setItems(undefined);
169251
}
170-
}, [pages]);
252+
}, [data]);
171253

172-
return { collections, isPending, error };
254+
useEffect(() => {
255+
if (searchRequest && hasNextPage && !isFetching) {
256+
fetchNextPage();
257+
}
258+
}, [searchRequest, hasNextPage, isFetching, fetchNextPage]);
259+
260+
useEffect(() => {
261+
if (error) {
262+
toaster.create({
263+
type: "error",
264+
title: "Error while searching",
265+
description: error.message,
266+
});
267+
}
268+
}, [error]);
269+
270+
return { items, hasNextPage, numberMatched };
173271
}
174272

175273
export function useNaturalLanguageCollectionSearch(

src/components/stac/item-collection.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@ import {
33
Card,
44
CloseButton,
55
DataList,
6+
DownloadTrigger,
67
Drawer,
78
EmptyState,
89
FormatNumber,
910
HStack,
11+
IconButton,
1012
Portal,
1113
Stack,
1214
Stat,
1315
} from "@chakra-ui/react";
14-
import { LuEyeOff, LuFileJson } from "react-icons/lu";
16+
import { LuDownload, LuEyeOff, LuFileJson, LuFocus } from "react-icons/lu";
1517
import type { StacItem } from "stac-ts";
16-
import { useStacMap } from "../../hooks";
18+
import { useFitBbox, useStacMap } from "../../hooks";
1719
import Loading from "../loading";
1820
import Item from "./item";
1921
import { type StacGeoparquetMetadata } from "./stac-geoparquet";
2022
import type { StacItemCollection } from "./types";
23+
import { getItemCollectionExtent } from "./utils";
2124
import Value from "./value";
2225

2326
export default function ItemCollection({
@@ -30,9 +33,32 @@ export default function ItemCollection({
3033
stacGeoparquetMetadataIsPending,
3134
stacGeoparquetItem,
3235
} = useStacMap();
36+
const fitBbox = useFitBbox();
37+
3338
return (
3439
<Stack>
3540
<Value value={itemCollection} type="Item collection"></Value>
41+
<HStack>
42+
<DownloadTrigger
43+
asChild
44+
data={JSON.stringify(itemCollection)}
45+
fileName={itemCollection.id || "item-collection" + ".json"}
46+
mimeType="application/json"
47+
>
48+
<IconButton size={"sm"} variant={"subtle"}>
49+
<LuDownload></LuDownload>
50+
</IconButton>
51+
</DownloadTrigger>
52+
{itemCollection.features.length > 0 && (
53+
<IconButton
54+
size={"sm"}
55+
variant={"subtle"}
56+
onClick={() => fitBbox(getItemCollectionExtent(itemCollection))}
57+
>
58+
<LuFocus></LuFocus>
59+
</IconButton>
60+
)}
61+
</HStack>
3662
{stacGeoparquetMetadataIsPending && <Loading></Loading>}
3763
{stacGeoparquetMetadata && (
3864
<StacGeoparquetMetadata

src/components/stac/item.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { Button, Heading, Stack } from "@chakra-ui/react";
2-
import { LuExternalLink } from "react-icons/lu";
1+
import { Button, Heading, HStack, IconButton, Stack } from "@chakra-ui/react";
2+
import { LuExternalLink, LuFocus } from "react-icons/lu";
33
import type { StacItem } from "stac-ts";
4+
import { useFitBbox } from "../../hooks";
45
import { Assets } from "./asset";
56
import Value, {
67
SelfLinkButtons as BaseSelfLinkButtons,
78
type SelfLinkButtonsProps,
89
} from "./value";
910

1011
export default function Item({ item }: { item: StacItem }) {
12+
const fitBbox = useFitBbox();
13+
1114
return (
1215
<Stack>
1316
<Value
@@ -16,6 +19,18 @@ export default function Item({ item }: { item: StacItem }) {
1619
selfLinkButtonsType={SelfLinkButtons}
1720
></Value>
1821

22+
<HStack>
23+
{item.bbox && (
24+
<IconButton
25+
size={"xs"}
26+
variant={"surface"}
27+
onClick={() => item.bbox && fitBbox(item.bbox)}
28+
>
29+
<LuFocus></LuFocus>
30+
</IconButton>
31+
)}
32+
</HStack>
33+
1934
<Heading size={"md"} mt={4}>
2035
Assets
2136
</Heading>

0 commit comments

Comments
 (0)