Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
dde9be6
refactor!: rewrite with zustand
gadomski Jan 14, 2026
7bd7ef4
feat: collections stash
gadomski Jan 15, 2026
2a82c7d
fix: working back towards filter and search
gadomski Jan 15, 2026
3d76bbd
fix: back to filtering
gadomski Jan 15, 2026
466716c
fix: leftover variable
gadomski Jan 15, 2026
70869a3
tests: add a few
gadomski Jan 15, 2026
b5c941d
fix: clear filter
gadomski Jan 15, 2026
9677fe0
fix: fetching
gadomski Jan 15, 2026
e19dd51
feat: re-add examples
gadomski Jan 15, 2026
ce77a52
feat: skeleton when loading
gadomski Jan 15, 2026
a1c0bba
feat: add lineClamp
gadomski Jan 15, 2026
3941f00
feat: description clamping
gadomski Jan 15, 2026
5888bb4
feat: add value and fill color
gadomski Jan 15, 2026
9499dd6
feat: add icon
gadomski Jan 15, 2026
bb63fde
feat: add root and parent links
gadomski Jan 15, 2026
58ced5f
feat: fix dependenceis?
gadomski Jan 15, 2026
80cc215
refactor: getLinkHref
gadomski Jan 15, 2026
13302f0
fix: lints
gadomski Jan 15, 2026
055173a
chore: whitespace
gadomski Jan 15, 2026
23d853f
feat: add json
gadomski Jan 15, 2026
4efe488
fix: re-add STAC browser
gadomski Jan 15, 2026
d5867a1
fix: make stac browser configurable
gadomski Jan 15, 2026
420eda7
feat: ready for search
gadomski Jan 15, 2026
f025071
feat: add section-header
gadomski Jan 15, 2026
f114d7f
feat: stuff
gadomski Jan 15, 2026
c750083
fix: filling of value
gadomski Jan 15, 2026
e978f43
feat: search action bar
gadomski Jan 15, 2026
4081e1f
fix: clear everything when we set href
gadomski Jan 15, 2026
e9c86b2
feat: add filter viewport
gadomski Jan 15, 2026
a245e9a
feat: hover collections
gadomski Jan 15, 2026
e1eab51
feat: link map back to sidebar
gadomski Jan 15, 2026
b38a52e
fix: fill
gadomski Jan 15, 2026
24dbc42
fix: add skeleton to root load
gadomski Jan 15, 2026
184ae47
feat: refactor panel
gadomski Jan 15, 2026
4086e3f
fix: re-organize map
gadomski Jan 16, 2026
fb24950
feat: hover collections properly
gadomski Jan 16, 2026
4b03fee
fix: split value panel
gadomski Jan 16, 2026
8bca2fe
feat: add children
gadomski Jan 16, 2026
aeae52b
feat: childrenworking like collections
gadomski Jan 16, 2026
9f3b61f
fix: hovering collections when value is loaded
gadomski Jan 16, 2026
71bec49
fix: reset hoveredCollection
gadomski Jan 16, 2026
bb6bd39
feat: set cursor
gadomski Jan 16, 2026
928f185
fix: section
gadomski Jan 16, 2026
4759618
feat: better viz
gadomski Jan 16, 2026
6e36515
fix: format
gadomski Jan 16, 2026
d454cd8
fix: cast value.description
gadomski Jan 16, 2026
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
1 change: 0 additions & 1 deletion .env

This file was deleted.

6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# STAC Natural Query API endpoint
VITE_STAC_NATURAL_QUERY_API=https://api.stac-semantic-search.k8s.labs.ds.io

# Base URL for STAC Browser external links
# Default: https://radiantearth.github.io/stac-browser/#/external/
VITE_STAC_BROWSER_URL="https://radiantearth.github.io/stac-browser/#/external/"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ dist-ssr
*.sw?
tests/**/__screenshots__/
codebook.toml
.env
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@
"dependencies": {
"@chakra-ui/react": "^3.31.0",
"@deck.gl/core": "^9.2.5",
"@deck.gl/extensions": "^9.2.5",
"@deck.gl/geo-layers": "^9.2.5",
"@deck.gl/layers": "^9.2.5",
"@deck.gl/mapbox": "^9.2.5",
"@deck.gl/mesh-layers": "^9.2.5",
"@developmentseed/deck.gl-geotiff": "^0.1.0",
"@developmentseed/deck.gl-raster": "^0.1.0",
"@duckdb/duckdb-wasm": "^1.32.0",
"@emotion/react": "^11.13.5",
"@geoarrow/deck.gl-layers": "^0.3.0",
Expand All @@ -60,8 +61,10 @@
"react-icons": "^5.5.0",
"react-map-gl": "^8.1.0",
"react-markdown": "^10.1.0",
"shiki": "^3.21.0",
"stac-ts": "^1.0.4",
"stac-wasm": "^0.0.3"
"stac-wasm": "^0.0.3",
"zustand": "^5.0.10"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
Expand Down
169 changes: 36 additions & 133 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,123 +1,47 @@
import { useEffect, useMemo, useState } from "react";
import { Box, Container, FileUpload, useFileUpload } from "@chakra-ui/react";
import type { StacCollection, StacItem } from "stac-ts";
import { useEffect } from "react";
import { MapProvider } from "react-map-gl/maplibre";
import { Box, Container } from "@chakra-ui/react";
import Map from "./components/map";
import Overlay from "./components/overlay";
import { Toaster } from "./components/ui/toaster";
import useHrefParam from "./hooks/href-param";
import useStacChildren from "./hooks/stac-children";
import useStacFilters from "./hooks/stac-filters";
import useStacValue from "./hooks/stac-value";
import Map from "./layers/map";
import Overlay from "./layers/overlay";
import type { BBox2D, Color } from "./types/map";
import type { DatetimeBounds, StacValue } from "./types/stac";
import getDateTimes from "./utils/datetimes";
import { getCogHref } from "./utils/stac";
import getDocumentTitle from "./utils/title";

// TODO make this configurable by the user.
const lineColor: Color = [207, 63, 2, 100];
const fillColor: Color = [207, 63, 2, 50];
import { useStore } from "./store";
import { getCurrentHref } from "./utils/href";

export default function App() {
// User state
const { href, setHref } = useHrefParam();
const fileUpload = useFileUpload({
maxFiles: 1,
onFileChange: (details) => {
if (details.acceptedFiles.length === 1) {
setHref(details.acceptedFiles[0].name);
}
},
});
const [userCollections, setCollections] = useState<StacCollection[]>();
const [userItems, setItems] = useState<StacItem[]>();
const [picked, setPicked] = useState<StacValue>();
const [bbox, setBbox] = useState<BBox2D>();
const [datetimeBounds, setDatetimeBounds] = useState<DatetimeBounds>();
const [filter, setFilter] = useState(true);
const [stacGeoparquetItemId, setStacGeoparquetItemId] = useState<string>();
const [cogHref, setcogHref] = useState<string>();

// Derived state
const {
value,
error,
items: linkedItems,
geoparqetTable,
stacGeoparquetItem,
} = useStacValue({
href,
fileUpload,
datetimeBounds: filter ? datetimeBounds : undefined,
stacGeoparquetItemId,
});
const collectionsLink = value?.links?.find((link) => link.rel === "data");
const { catalogs, collections: linkedCollections } = useStacChildren({
value,
enabled: !!value && !collectionsLink,
});
const collections = collectionsLink ? userCollections : linkedCollections;
const items = userItems || linkedItems;
const { filteredCollections, filteredItems } = useStacFilters({
collections,
items,
filter,
bbox,
datetimeBounds,
});

const datetimes = useMemo(
() => (value ? getDateTimes(value, items, collections) : null),
[value, items, collections]
);
const href = useStore((state) => state.href);
const setHref = useStore((state) => state.setHref);

// Effects
useEffect(() => {
document.title = getDocumentTitle(value);
}, [value]);
if (href && getCurrentHref() != href) {
history.pushState(null, "", "?href=" + href);
} else if (href === "") {
history.pushState(null, "", location.pathname);
}
}, [href]);

useEffect(() => {
setPicked(undefined);
setItems(undefined);
setDatetimeBounds(undefined);
setcogHref(value && getCogHref(value));
}, [value]);

useEffect(() => {
setcogHref(picked && getCogHref(picked));
}, [picked]);
function handlePopState() {
setHref(getCurrentHref() ?? "");
}
window.addEventListener("popstate", handlePopState);

if (getCurrentHref()) {
try {
new URL(getCurrentHref());
} catch {
history.pushState(null, "", location.pathname);
}
}

useEffect(() => {
setPicked(stacGeoparquetItem);
}, [stacGeoparquetItem]);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, [setHref]);

return (
<>
<MapProvider>
<Box h={"100dvh"}>
<FileUpload.RootProvider value={fileUpload} unstyled={true}>
<FileUpload.Dropzone
disableClick={true}
style={{
height: "100dvh",
width: "100dvw",
}}
>
<Map
value={value}
geoparquetTable={geoparqetTable}
collections={collections}
filteredCollections={filteredCollections}
items={filteredItems}
fillColor={fillColor}
lineColor={lineColor}
setBbox={setBbox}
picked={picked}
setPicked={setPicked}
setStacGeoparquetItemId={setStacGeoparquetItemId}
cogHref={cogHref}
></Map>
</FileUpload.Dropzone>
</FileUpload.RootProvider>
<Map />
</Box>
<Container
zIndex={1}
Expand All @@ -129,30 +53,9 @@ export default function App() {
left={0}
pt={4}
>
<Overlay
href={href}
setHref={setHref}
fileUpload={fileUpload}
value={value}
error={error}
catalogs={catalogs}
setCollections={setCollections}
collections={filteredCollections}
totalNumOfCollections={collections?.length}
filter={filter}
setFilter={setFilter}
bbox={bbox}
setPicked={setPicked}
picked={picked}
items={filteredItems}
setItems={setItems}
setDatetimeBounds={setDatetimeBounds}
cogHref={cogHref}
setcogHref={setcogHref}
datetimes={datetimes}
></Overlay>
<Overlay />
</Container>
<Toaster></Toaster>
</>
<Toaster />
</MapProvider>
);
}
111 changes: 111 additions & 0 deletions src/components/assets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useEffect, useState } from "react";
import { LuDownload, LuFileImage } from "react-icons/lu";
import {
Badge,
ButtonGroup,
Clipboard,
Group,
Heading,
HStack,
IconButton,
RadioCard,
Stack,
} from "@chakra-ui/react";
import type { StacAsset } from "stac-ts";
import { useStore } from "../store";

export default function Assets({
assets,
}: {
assets: { [k: string]: StacAsset };
}) {
const setGeotiffHref = useStore((store) => store.setGeotiffHref);
let defaultValue = null;
for (const [key, asset] of Object.entries(assets)) {
if (!defaultValue && isGeotiff(asset)) {
defaultValue = key;
}
if (defaultValue && isGeotiff(asset) && asset.roles?.includes("visual")) {
defaultValue = key;
}
}
const [value, setValue] = useState<string | null>(defaultValue);

useEffect(() => {
if (value) {
setGeotiffHref(assets[value]?.href);
} else {
setGeotiffHref(null);
}
}, [assets, value, setGeotiffHref]);

return (
<Stack>
<Heading size={"md"}>
<HStack>
<LuFileImage /> Assets
</HStack>
</Heading>
<RadioCard.Root
value={value}
onValueChange={(e) => setValue(e.value)}
size={"sm"}
>
<Group orientation="vertical">
{Object.entries(assets).map(([key, asset]) => (
<Asset key={key} assetKey={key} asset={asset} />
))}
</Group>
</RadioCard.Root>
</Stack>
);
}

function Asset({ assetKey, asset }: { assetKey: string; asset: StacAsset }) {
const scheme = asset.href.split(":").at(0);

return (
<RadioCard.Item
value={assetKey}
width={"full"}
disabled={!isGeotiff(asset)}
>
<RadioCard.ItemHiddenInput />
<RadioCard.ItemControl>
<RadioCard.ItemContent>
<RadioCard.ItemText>{asset.title || assetKey}</RadioCard.ItemText>
<RadioCard.ItemDescription>
<Badge>{scheme}</Badge>
{asset.type && <Badge>{asset.type}</Badge>}
</RadioCard.ItemDescription>
</RadioCard.ItemContent>
<RadioCard.ItemIndicator />
</RadioCard.ItemControl>
<RadioCard.ItemAddon>
<ButtonGroup size={"xs"} variant={"plain"}>
<Clipboard.Root value={asset.href}>
<Clipboard.Trigger asChild>
<IconButton>
<Clipboard.Indicator />
</IconButton>
</Clipboard.Trigger>
</Clipboard.Root>
{scheme?.startsWith("http") && (
<IconButton asChild>
<a href={asset.href}>
<LuDownload />
</a>
</IconButton>
)}
</ButtonGroup>
</RadioCard.ItemAddon>
</RadioCard.Item>
);
}

function isGeotiff(asset: StacAsset) {
return (
asset.type?.startsWith("image/tiff; application=geotiff") &&
asset.href.startsWith("http")
);
}
Loading