Skip to content

Commit 00d99d0

Browse files
authored
refactor: everything (#50)
1 parent 16bf481 commit 00d99d0

39 files changed

+1519
-2508
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
"@geoarrow/deck.gl-layers": "^0.3.0",
3838
"@geoarrow/geoarrow-js": "github:smohiudd/geoarrow-js#feature/wkb",
3939
"@tanstack/react-query": "^5.81.5",
40+
"@turf/bbox": "^7.2.0",
4041
"@turf/bbox-polygon": "^7.2.0",
41-
"@turf/centroid": "^7.2.0",
4242
"apache-arrow": "^19.0.0",
4343
"deck.gl": "^9.1.12",
4444
"duckdb-wasm-kit": "^0.1.38",

src/app.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import { Box, Container, SimpleGrid } from "@chakra-ui/react";
1+
import { Box, Container, GridItem, SimpleGrid } from "@chakra-ui/react";
22
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
33
import { MapProvider } from "react-map-gl/dist/esm/exports-maplibre";
4+
import Header from "./components/header";
5+
import Map from "./components/map";
6+
import Panel from "./components/panel";
47
import { Toaster } from "./components/ui/toaster";
5-
import Header from "./header";
6-
import Map from "./map";
7-
import Panel from "./panel";
8-
import { StacMapProvider } from "./provider";
8+
import { StacMapProvider } from "./provider/stac-map";
99

1010
export default function App() {
11-
const queryClient = new QueryClient({
12-
defaultOptions: { queries: { staleTime: Infinity } },
13-
});
11+
const queryClient = new QueryClient({});
1412

1513
return (
1614
<QueryClientProvider client={queryClient}>
@@ -19,14 +17,18 @@ export default function App() {
1917
<Box zIndex={0} position={"absolute"} top={0} left={0}>
2018
<Map></Map>
2119
</Box>
22-
<Container zIndex={1} fluid h={"dvh"} pointerEvents={"none"}>
23-
<Box pointerEvents={"auto"}>
24-
<Header></Header>
25-
</Box>
26-
<SimpleGrid columns={3}>
27-
<Box pointerEvents={"auto"}>
28-
<Panel></Panel>
29-
</Box>
20+
<Container zIndex={1} fluid h={"dvh"} py={4} pointerEvents={"none"}>
21+
<SimpleGrid columns={3} gap={4}>
22+
<GridItem colSpan={1}>
23+
<Box pointerEvents={"auto"}>
24+
<Panel></Panel>
25+
</Box>
26+
</GridItem>
27+
<GridItem colSpan={2}>
28+
<Box pointerEvents={"auto"}>
29+
<Header></Header>
30+
</Box>
31+
</GridItem>
3032
</SimpleGrid>
3133
</Container>
3234
<Toaster></Toaster>

src/components/assets.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
Badge,
3+
ButtonGroup,
4+
DataList,
5+
HStack,
6+
Heading,
7+
IconButton,
8+
Stack,
9+
Text,
10+
} from "@chakra-ui/react";
11+
import { LuDownload } from "react-icons/lu";
12+
import type { StacAsset } from "stac-ts";
13+
14+
export default function Assets({
15+
assets,
16+
}: {
17+
assets: { [k: string]: StacAsset };
18+
}) {
19+
return (
20+
<Stack>
21+
<Heading size={"sm"}>Assets</Heading>
22+
<DataList.Root>
23+
{Object.entries(assets).map(([key, asset]) => (
24+
<DataList.Item key={asset.href}>
25+
<DataList.ItemLabel>
26+
<HStack>
27+
{asset.title || key}
28+
29+
{asset.roles &&
30+
asset.roles.map((role) => <Badge key={role}>{role}</Badge>)}
31+
</HStack>
32+
</DataList.ItemLabel>
33+
<DataList.ItemValue>
34+
<HStack>
35+
<ButtonGroup size={"xs"} variant={"subtle"}>
36+
<IconButton asChild>
37+
<a href={asset.href} target="_blank">
38+
<LuDownload></LuDownload>
39+
</a>
40+
</IconButton>
41+
</ButtonGroup>
42+
{asset.type && (
43+
<Text fontSize={"xs"} fontWeight={"light"}>
44+
{asset.type}
45+
</Text>
46+
)}
47+
</HStack>
48+
</DataList.ItemValue>
49+
</DataList.Item>
50+
))}
51+
</DataList.Root>
52+
</Stack>
53+
);
54+
}

src/components/catalog.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { SkeletonText, Stack } from "@chakra-ui/react";
2+
import { LuFolder } from "react-icons/lu";
3+
import type { StacCatalog } from "stac-ts";
4+
import useStacMap from "../hooks/stac-map";
5+
import Collections from "./collections";
6+
import { ValueInfo } from "./value";
7+
8+
export default function Catalog({ catalog }: { catalog: StacCatalog }) {
9+
const { collections } = useStacMap();
10+
11+
return (
12+
<Stack gap={6}>
13+
<ValueInfo value={catalog} icon={<LuFolder></LuFolder>}></ValueInfo>
14+
{(collections && (
15+
<Collections collections={collections}></Collections>
16+
)) || <SkeletonText noOfLines={3}></SkeletonText>}
17+
</Stack>
18+
);
19+
}

src/components/collection.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { DataList, Text } from "@chakra-ui/react";
2+
import { LuFolderPlus } from "react-icons/lu";
3+
import type {
4+
StacCollection,
5+
SpatialExtent as StacSpatialExtent,
6+
TemporalExtent as StacTemporalExtent,
7+
} from "stac-ts";
8+
import { ValueInfo } from "./value";
9+
10+
export default function Collection({
11+
collection,
12+
}: {
13+
collection: StacCollection;
14+
}) {
15+
return (
16+
<ValueInfo value={collection} icon={<LuFolderPlus></LuFolderPlus>}>
17+
<CollectionInfo collection={collection}></CollectionInfo>
18+
</ValueInfo>
19+
);
20+
}
21+
22+
function CollectionInfo({ collection }: { collection: StacCollection }) {
23+
return (
24+
<DataList.Root orientation={"horizontal"} size={"sm"} py={4}>
25+
{collection.extent?.spatial?.bbox?.[0] && (
26+
<DataList.Item>
27+
<DataList.ItemLabel>Spatial extent</DataList.ItemLabel>
28+
<DataList.ItemValue>
29+
<SpatialExtent
30+
bbox={collection.extent.spatial.bbox[0]}
31+
></SpatialExtent>
32+
</DataList.ItemValue>
33+
</DataList.Item>
34+
)}
35+
{collection.extent?.temporal?.interval?.[0] && (
36+
<DataList.Item>
37+
<DataList.ItemLabel>Temporal extent</DataList.ItemLabel>
38+
<DataList.ItemValue>
39+
<TemporalExtent
40+
interval={collection.extent.temporal.interval[0]}
41+
></TemporalExtent>
42+
</DataList.ItemValue>
43+
</DataList.Item>
44+
)}
45+
</DataList.Root>
46+
);
47+
}
48+
49+
function SpatialExtent({ bbox }: { bbox: StacSpatialExtent }) {
50+
return <Text>[{bbox.map((n) => Number(n.toFixed(4))).join(", ")}]</Text>;
51+
}
52+
53+
function TemporalExtent({ interval }: { interval: StacTemporalExtent }) {
54+
return (
55+
<Text>
56+
<DateString datetime={interval[0]}></DateString>{" "}
57+
<DateString datetime={interval[1]}></DateString>
58+
</Text>
59+
);
60+
}
61+
62+
function DateString({ datetime }: { datetime: string | null }) {
63+
if (datetime) {
64+
return new Date(datetime).toLocaleDateString();
65+
} else {
66+
return "unbounded";
67+
}
68+
}

src/components/collections.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Heading, Link, List, Stack } from "@chakra-ui/react";
2+
import type { StacCollection } from "stac-ts";
3+
import useStacMap from "../hooks/stac-map";
4+
5+
export default function Collections({
6+
collections,
7+
}: {
8+
collections: StacCollection[];
9+
}) {
10+
return (
11+
<Stack>
12+
<Heading size={"md"}>Collections</Heading>
13+
<List.Root variant={"plain"} gap={1}>
14+
{collections.map((collection) => (
15+
<CollectionListItem
16+
key={collection.id}
17+
collection={collection}
18+
></CollectionListItem>
19+
))}
20+
</List.Root>
21+
</Stack>
22+
);
23+
}
24+
25+
function CollectionListItem({ collection }: { collection: StacCollection }) {
26+
const { setHref } = useStacMap();
27+
const selfHref = collection.links.find((link) => link.rel === "self")?.href;
28+
29+
return (
30+
<List.Item>
31+
<Link onClick={() => selfHref && setHref(selfHref)}>
32+
{collection.title || collection.id}
33+
</Link>
34+
</List.Item>
35+
);
36+
}

src/header.tsx renamed to src/components/header.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Box, Button, HStack, Input, Menu, Portal } from "@chakra-ui/react";
22
import { useEffect, useState } from "react";
3-
import { ColorModeButton } from "./components/ui/color-mode";
4-
import { useStacMap } from "./hooks";
3+
import useStacMap from "../hooks/stac-map";
4+
import { ColorModeButton } from "./ui/color-mode";
55

66
const EXAMPLES = [
77
["eoAPI DevSeed", "https://stac.eoapi.dev/"],
@@ -23,7 +23,7 @@ const EXAMPLES = [
2323

2424
export default function Header() {
2525
return (
26-
<HStack py={4}>
26+
<HStack>
2727
<HrefInput></HrefInput>
2828
<Examples></Examples>
2929
<ColorModeButton></ColorModeButton>
@@ -51,7 +51,7 @@ function HrefInput() {
5151
w={"full"}
5252
>
5353
<Input
54-
bg={"bg.muted/60"}
54+
bg={"bg.muted/90"}
5555
placeholder="Enter a STAC url"
5656
value={value}
5757
onChange={(e) => setValue(e.target.value)}

src/components/item-collection.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {
2+
createTreeCollection,
3+
DataList,
4+
FormatNumber,
5+
Stack,
6+
Text,
7+
TreeView,
8+
} from "@chakra-ui/react";
9+
import type { ReactNode } from "react";
10+
import { LuCircle, LuCircleDot, LuFiles } from "react-icons/lu";
11+
import useStacMap from "../hooks/stac-map";
12+
import type { StacGeoparquetMetadata, StacItemCollection } from "../types/stac";
13+
import { ValueInfo } from "./value";
14+
15+
export default function ItemCollection({
16+
itemCollection,
17+
}: {
18+
itemCollection: StacItemCollection;
19+
}) {
20+
const { stacGeoparquetMetadata } = useStacMap();
21+
22+
return (
23+
<ValueInfo value={itemCollection} type="Item collection" icon={<LuFiles />}>
24+
{stacGeoparquetMetadata && (
25+
<StacGeoparquetInfo
26+
metadata={stacGeoparquetMetadata}
27+
></StacGeoparquetInfo>
28+
)}
29+
</ValueInfo>
30+
);
31+
}
32+
33+
interface Node {
34+
id: string;
35+
value: ReactNode;
36+
children?: Node[];
37+
}
38+
39+
function StacGeoparquetInfo({
40+
metadata,
41+
}: {
42+
metadata: StacGeoparquetMetadata;
43+
}) {
44+
const collection = createTreeCollection<Node>({
45+
rootNode: {
46+
id: "root",
47+
value: "Metadata",
48+
children: metadata.keyValue.map((kv) => intoNode(kv.key, kv.value)),
49+
},
50+
});
51+
52+
return (
53+
<Stack gap={4}>
54+
<DataList.Root orientation={"horizontal"}>
55+
<DataList.Item>
56+
<DataList.ItemLabel>Count</DataList.ItemLabel>
57+
<DataList.ItemValue>
58+
<FormatNumber value={metadata.count}></FormatNumber>
59+
</DataList.ItemValue>
60+
</DataList.Item>
61+
</DataList.Root>
62+
<TreeView.Root collection={collection} variant={"subtle"}>
63+
<TreeView.Label fontWeight={"light"}>Key-value metadata</TreeView.Label>
64+
<TreeView.Tree>
65+
<TreeView.Node
66+
indentGuide={
67+
<TreeView.BranchIndentGuide></TreeView.BranchIndentGuide>
68+
}
69+
render={({ node, nodeState }) =>
70+
nodeState.isBranch ? (
71+
<TreeView.BranchControl>
72+
<LuCircleDot></LuCircleDot>
73+
<TreeView.BranchText>{node.value}</TreeView.BranchText>
74+
</TreeView.BranchControl>
75+
) : (
76+
<TreeView.Item>
77+
<LuCircle></LuCircle>
78+
<TreeView.ItemText>{node.value}</TreeView.ItemText>
79+
</TreeView.Item>
80+
)
81+
}
82+
></TreeView.Node>
83+
</TreeView.Tree>
84+
</TreeView.Root>
85+
</Stack>
86+
);
87+
}
88+
89+
// eslint-disable-next-line
90+
function intoNode(key: string, value: any) {
91+
const children: Node[] = [];
92+
93+
switch (typeof value) {
94+
case "string":
95+
case "number":
96+
case "bigint":
97+
case "boolean":
98+
children.push({
99+
id: `${key}-value`,
100+
value: value.toString(),
101+
});
102+
break;
103+
case "symbol":
104+
case "undefined":
105+
case "function":
106+
children.push({
107+
id: `${key}-value`,
108+
value: (
109+
<Text fontWeight="lighter" fontStyle={"italic"}>
110+
opaque
111+
</Text>
112+
),
113+
});
114+
break;
115+
case "object":
116+
if (Array.isArray(value)) {
117+
children.push(
118+
...value.map((v, index) => intoNode(index.toString(), v)),
119+
);
120+
} else {
121+
children.push(...Object.entries(value).map(([k, v]) => intoNode(k, v)));
122+
}
123+
}
124+
125+
return {
126+
id: key,
127+
value: key,
128+
children,
129+
};
130+
}

0 commit comments

Comments
 (0)