Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
"@geoarrow/deck.gl-layers": "^0.3.0",
"@geoarrow/geoarrow-js": "github:smohiudd/geoarrow-js#feature/wkb",
"@tanstack/react-query": "^5.81.5",
"@turf/bbox": "^7.2.0",
"@turf/bbox-polygon": "^7.2.0",
"@turf/centroid": "^7.2.0",
"apache-arrow": "^19.0.0",
"deck.gl": "^9.1.12",
"duckdb-wasm-kit": "^0.1.38",
Expand Down
34 changes: 18 additions & 16 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Box, Container, SimpleGrid } from "@chakra-ui/react";
import { Box, Container, GridItem, SimpleGrid } from "@chakra-ui/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MapProvider } from "react-map-gl/dist/esm/exports-maplibre";
import Header from "./components/header";
import Map from "./components/map";
import Panel from "./components/panel";
import { Toaster } from "./components/ui/toaster";
import Header from "./header";
import Map from "./map";
import Panel from "./panel";
import { StacMapProvider } from "./provider";
import { StacMapProvider } from "./provider/stac-map";

export default function App() {
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity } },
});
const queryClient = new QueryClient({});

return (
<QueryClientProvider client={queryClient}>
Expand All @@ -19,14 +17,18 @@ export default function App() {
<Box zIndex={0} position={"absolute"} top={0} left={0}>
<Map></Map>
</Box>
<Container zIndex={1} fluid h={"dvh"} pointerEvents={"none"}>
<Box pointerEvents={"auto"}>
<Header></Header>
</Box>
<SimpleGrid columns={3}>
<Box pointerEvents={"auto"}>
<Panel></Panel>
</Box>
<Container zIndex={1} fluid h={"dvh"} py={4} pointerEvents={"none"}>
<SimpleGrid columns={3} gap={4}>
<GridItem colSpan={1}>
<Box pointerEvents={"auto"}>
<Panel></Panel>
</Box>
</GridItem>
<GridItem colSpan={2}>
<Box pointerEvents={"auto"}>
<Header></Header>
</Box>
</GridItem>
</SimpleGrid>
</Container>
<Toaster></Toaster>
Expand Down
54 changes: 54 additions & 0 deletions src/components/assets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Badge,
ButtonGroup,
DataList,
HStack,
Heading,
IconButton,
Stack,
Text,
} from "@chakra-ui/react";
import { LuDownload } from "react-icons/lu";
import type { StacAsset } from "stac-ts";

export default function Assets({
assets,
}: {
assets: { [k: string]: StacAsset };
}) {
return (
<Stack>
<Heading size={"sm"}>Assets</Heading>
<DataList.Root>
{Object.entries(assets).map(([key, asset]) => (
<DataList.Item key={asset.href}>
<DataList.ItemLabel>
<HStack>
{asset.title || key}

{asset.roles &&
asset.roles.map((role) => <Badge key={role}>{role}</Badge>)}
</HStack>
</DataList.ItemLabel>
<DataList.ItemValue>
<HStack>
<ButtonGroup size={"xs"} variant={"subtle"}>
<IconButton asChild>
<a href={asset.href} target="_blank">
<LuDownload></LuDownload>
</a>
</IconButton>
</ButtonGroup>
{asset.type && (
<Text fontSize={"xs"} fontWeight={"light"}>
{asset.type}
</Text>
)}
</HStack>
</DataList.ItemValue>
</DataList.Item>
))}
</DataList.Root>
</Stack>
);
}
19 changes: 19 additions & 0 deletions src/components/catalog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SkeletonText, Stack } from "@chakra-ui/react";
import { LuFolder } from "react-icons/lu";
import type { StacCatalog } from "stac-ts";
import useStacMap from "../hooks/stac-map";
import Collections from "./collections";
import { ValueInfo } from "./value";

export default function Catalog({ catalog }: { catalog: StacCatalog }) {
const { collections } = useStacMap();

return (
<Stack gap={6}>
<ValueInfo value={catalog} icon={<LuFolder></LuFolder>}></ValueInfo>
{(collections && (
<Collections collections={collections}></Collections>
)) || <SkeletonText noOfLines={3}></SkeletonText>}
</Stack>
);
}
68 changes: 68 additions & 0 deletions src/components/collection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { DataList, Text } from "@chakra-ui/react";
import { LuFolderPlus } from "react-icons/lu";
import type {
StacCollection,
SpatialExtent as StacSpatialExtent,
TemporalExtent as StacTemporalExtent,
} from "stac-ts";
import { ValueInfo } from "./value";

export default function Collection({
collection,
}: {
collection: StacCollection;
}) {
return (
<ValueInfo value={collection} icon={<LuFolderPlus></LuFolderPlus>}>
<CollectionInfo collection={collection}></CollectionInfo>
</ValueInfo>
);
}

function CollectionInfo({ collection }: { collection: StacCollection }) {
return (
<DataList.Root orientation={"horizontal"} size={"sm"} py={4}>
{collection.extent?.spatial?.bbox?.[0] && (
<DataList.Item>
<DataList.ItemLabel>Spatial extent</DataList.ItemLabel>
<DataList.ItemValue>
<SpatialExtent
bbox={collection.extent.spatial.bbox[0]}
></SpatialExtent>
</DataList.ItemValue>
</DataList.Item>
)}
{collection.extent?.temporal?.interval?.[0] && (
<DataList.Item>
<DataList.ItemLabel>Temporal extent</DataList.ItemLabel>
<DataList.ItemValue>
<TemporalExtent
interval={collection.extent.temporal.interval[0]}
></TemporalExtent>
</DataList.ItemValue>
</DataList.Item>
)}
</DataList.Root>
);
}

function SpatialExtent({ bbox }: { bbox: StacSpatialExtent }) {
return <Text>[{bbox.map((n) => Number(n.toFixed(4))).join(", ")}]</Text>;
}

function TemporalExtent({ interval }: { interval: StacTemporalExtent }) {
return (
<Text>
<DateString datetime={interval[0]}></DateString> —{" "}
<DateString datetime={interval[1]}></DateString>
</Text>
);
}

function DateString({ datetime }: { datetime: string | null }) {
if (datetime) {
return new Date(datetime).toLocaleDateString();
} else {
return "unbounded";
}
}
36 changes: 36 additions & 0 deletions src/components/collections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Heading, Link, List, Stack } from "@chakra-ui/react";
import type { StacCollection } from "stac-ts";
import useStacMap from "../hooks/stac-map";

export default function Collections({
collections,
}: {
collections: StacCollection[];
}) {
return (
<Stack>
<Heading size={"md"}>Collections</Heading>
<List.Root variant={"plain"} gap={1}>
{collections.map((collection) => (
<CollectionListItem
key={collection.id}
collection={collection}
></CollectionListItem>
))}
</List.Root>
</Stack>
);
}

function CollectionListItem({ collection }: { collection: StacCollection }) {
const { setHref } = useStacMap();
const selfHref = collection.links.find((link) => link.rel === "self")?.href;

return (
<List.Item>
<Link onClick={() => selfHref && setHref(selfHref)}>
{collection.title || collection.id}
</Link>
</List.Item>
);
}
8 changes: 4 additions & 4 deletions src/header.tsx → src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, Button, HStack, Input, Menu, Portal } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { ColorModeButton } from "./components/ui/color-mode";
import { useStacMap } from "./hooks";
import useStacMap from "../hooks/stac-map";
import { ColorModeButton } from "./ui/color-mode";

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

export default function Header() {
return (
<HStack py={4}>
<HStack>
<HrefInput></HrefInput>
<Examples></Examples>
<ColorModeButton></ColorModeButton>
Expand Down Expand Up @@ -51,7 +51,7 @@ function HrefInput() {
w={"full"}
>
<Input
bg={"bg.muted/60"}
bg={"bg.muted/90"}
placeholder="Enter a STAC url"
value={value}
onChange={(e) => setValue(e.target.value)}
Expand Down
130 changes: 130 additions & 0 deletions src/components/item-collection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
createTreeCollection,
DataList,
FormatNumber,
Stack,
Text,
TreeView,
} from "@chakra-ui/react";
import type { ReactNode } from "react";
import { LuCircle, LuCircleDot, LuFiles } from "react-icons/lu";
import useStacMap from "../hooks/stac-map";
import type { StacGeoparquetMetadata, StacItemCollection } from "../types/stac";
import { ValueInfo } from "./value";

export default function ItemCollection({
itemCollection,
}: {
itemCollection: StacItemCollection;
}) {
const { stacGeoparquetMetadata } = useStacMap();

return (
<ValueInfo value={itemCollection} type="Item collection" icon={<LuFiles />}>
{stacGeoparquetMetadata && (
<StacGeoparquetInfo
metadata={stacGeoparquetMetadata}
></StacGeoparquetInfo>
)}
</ValueInfo>
);
}

interface Node {
id: string;
value: ReactNode;
children?: Node[];
}

function StacGeoparquetInfo({
metadata,
}: {
metadata: StacGeoparquetMetadata;
}) {
const collection = createTreeCollection<Node>({
rootNode: {
id: "root",
value: "Metadata",
children: metadata.keyValue.map((kv) => intoNode(kv.key, kv.value)),
},
});

return (
<Stack gap={4}>
<DataList.Root orientation={"horizontal"}>
<DataList.Item>
<DataList.ItemLabel>Count</DataList.ItemLabel>
<DataList.ItemValue>
<FormatNumber value={metadata.count}></FormatNumber>
</DataList.ItemValue>
</DataList.Item>
</DataList.Root>
<TreeView.Root collection={collection} variant={"subtle"}>
<TreeView.Label fontWeight={"light"}>Key-value metadata</TreeView.Label>
<TreeView.Tree>
<TreeView.Node
indentGuide={
<TreeView.BranchIndentGuide></TreeView.BranchIndentGuide>
}
render={({ node, nodeState }) =>
nodeState.isBranch ? (
<TreeView.BranchControl>
<LuCircleDot></LuCircleDot>
<TreeView.BranchText>{node.value}</TreeView.BranchText>
</TreeView.BranchControl>
) : (
<TreeView.Item>
<LuCircle></LuCircle>
<TreeView.ItemText>{node.value}</TreeView.ItemText>
</TreeView.Item>
)
}
></TreeView.Node>
</TreeView.Tree>
</TreeView.Root>
</Stack>
);
}

// eslint-disable-next-line
function intoNode(key: string, value: any) {
const children: Node[] = [];

switch (typeof value) {
case "string":
case "number":
case "bigint":
case "boolean":
children.push({
id: `${key}-value`,
value: value.toString(),
});
break;
case "symbol":
case "undefined":
case "function":
children.push({
id: `${key}-value`,
value: (
<Text fontWeight="lighter" fontStyle={"italic"}>
opaque
</Text>
),
});
break;
case "object":
if (Array.isArray(value)) {
children.push(
...value.map((v, index) => intoNode(index.toString(), v)),
);
} else {
children.push(...Object.entries(value).map(([k, v]) => intoNode(k, v)));
}
}

return {
id: key,
value: key,
children,
};
}
Loading