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
17 changes: 17 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import useStacValue from "./hooks/stac-value";
import type { BBox2D, Color } from "./types/map";
import type { DatetimeBounds, StacValue } from "./types/stac";
import {
isCog,
isCollectionInBbox,
isCollectionInDatetimeBounds,
isItemInBbox,
isItemInDatetimeBounds,
isVisual,
} from "./utils/stac";

// TODO make this configurable by the user.
Expand All @@ -30,6 +32,7 @@ export default function App() {
const [datetimeBounds, setDatetimeBounds] = useState<DatetimeBounds>();
const [filter, setFilter] = useState(true);
const [stacGeoparquetItemId, setStacGeoparquetItemId] = useState<string>();
const [cogTileHref, setCogTileHref] = useState<string>();

// Derived state
const {
Expand Down Expand Up @@ -116,6 +119,17 @@ export default function App() {
setItems(undefined);
setDatetimeBounds(undefined);

let cogTileHref = undefined;
if (value && value.assets) {
for (const asset of Object.values(value.assets)) {
if (isCog(asset) && isVisual(asset)) {
cogTileHref = asset.href as string;
break;
}
}
}
setCogTileHref(cogTileHref);

if (value && (value.title || value.id)) {
document.title = "stac-map | " + (value.title || value.id);
} else {
Expand Down Expand Up @@ -151,6 +165,7 @@ export default function App() {
picked={picked}
setPicked={setPicked}
setStacGeoparquetItemId={setStacGeoparquetItemId}
cogTileHref={cogTileHref}
></Map>
</FileUpload.Dropzone>
</FileUpload.RootProvider>
Expand Down Expand Up @@ -184,6 +199,8 @@ export default function App() {
filteredItems={filteredItems}
setItems={setItems}
setDatetimeBounds={setDatetimeBounds}
cogTileHref={cogTileHref}
setCogTileHref={setCogTileHref}
></Overlay>
</Container>
<Toaster></Toaster>
Expand Down
55 changes: 50 additions & 5 deletions src/components/assets.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,66 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { LuDownload } from "react-icons/lu";
import {
Button,
ButtonGroup,
Card,
Checkbox,
Collapsible,
DataList,
HStack,
Image,
Span,
} from "@chakra-ui/react";
import type { StacAsset } from "stac-ts";
import Properties from "./properties";
import type { StacAssets } from "../types/stac";
import { isCog, isVisual } from "../utils/stac";

export default function Assets({ assets }: { assets: StacAssets }) {
export default function Assets({
assets,
cogTileHref,
setCogTileHref,
}: {
assets: StacAssets;
cogTileHref: string | undefined;
setCogTileHref: (href: string | undefined) => void;
}) {
return (
<DataList.Root>
{Object.keys(assets).map((key) => (
<DataList.Item key={"asset-" + key}>
<DataList.ItemLabel>{key}</DataList.ItemLabel>
<DataList.ItemValue>
<Asset asset={assets[key]} />
<Asset
asset={assets[key]}
cogTileHref={cogTileHref}
setCogTileHref={setCogTileHref}
/>
</DataList.ItemValue>
</DataList.Item>
))}
</DataList.Root>
);
}

function Asset({ asset }: { asset: StacAsset }) {
function Asset({
asset,
cogTileHref,
setCogTileHref,
}: {
asset: StacAsset;
cogTileHref: string | undefined;
setCogTileHref: (href: string | undefined) => void;
}) {
const [imageError, setImageError] = useState(false);
const [checked, setChecked] = useState(false);
// eslint-disable-next-line
const { href, roles, type, title, ...properties } = asset;

useEffect(() => {
setChecked(cogTileHref === asset.href);
}, [cogTileHref, asset.href]);

return (
<Card.Root size={"sm"} w="full">
<Card.Header>
Expand Down Expand Up @@ -64,7 +92,24 @@ function Asset({ asset }: { asset: StacAsset }) {
</Collapsible.Content>
</Collapsible.Root>
)}
<HStack justify={"right"}>
<HStack>
{isCog(asset) && isVisual(asset) && (
<Checkbox.Root
checked={checked}
onCheckedChange={(e) => {
setChecked(!!e.checked);
if (e.checked) setCogTileHref(asset.href);
else setCogTileHref(undefined);
}}
>
<Checkbox.HiddenInput />
<Checkbox.Control />
<Checkbox.Label>Visualize</Checkbox.Label>
</Checkbox.Root>
)}

<Span flex={"1"} />

<ButtonGroup size="sm" variant="outline">
<Button asChild>
<a href={asset.href} target="_blank">
Expand Down
38 changes: 35 additions & 3 deletions src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
useControl,
} from "react-map-gl/maplibre";
import { type DeckProps, Layer } from "@deck.gl/core";
import { GeoJsonLayer } from "@deck.gl/layers";
import { TileLayer } from "@deck.gl/geo-layers";
import { BitmapLayer, GeoJsonLayer } from "@deck.gl/layers";
import { MapboxOverlay } from "@deck.gl/mapbox";
import { GeoArrowPolygonLayer } from "@geoarrow/deck.gl-layers";
import bbox from "@turf/bbox";
Expand All @@ -31,6 +32,7 @@ export default function Map({
setPicked,
table,
setStacGeoparquetItemId,
cogTileHref,
}: {
value: StacValue | undefined;
collections: StacCollection[] | undefined;
Expand All @@ -44,6 +46,7 @@ export default function Map({
setPicked: (picked: StacValue | undefined) => void;
table: Table | undefined;
setStacGeoparquetItemId: (id: string | undefined) => void;
cogTileHref: string | undefined;
}) {
const mapRef = useRef<MapRef>(null);
const mapStyle = useColorModeValue(
Expand Down Expand Up @@ -87,7 +90,36 @@ export default function Map({
fillColor[3],
];

const layers: Layer[] = [
let layers: Layer[] = [];

if (cogTileHref)
layers.push(
new TileLayer({
id: "cog-tiles",
extent: value && getBbox(value, collections),
maxRequests: 10,
data:
cogTileHref &&
`https://titiler.xyz/cog/tiles/WebMercatorQuad/{z}/{x}/{y}.png?url=${cogTileHref}`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make the titiler endpoint configurable so other users can switch to their own endpoint 🙏

renderSubLayers: (props) => {
const { boundingBox } = props.tile;

return new BitmapLayer(props, {
data: undefined,
image: props.data,
bounds: [
boundingBox[0][0],
boundingBox[0][1],
boundingBox[1][0],
boundingBox[1][1],
],
});
},
})
);

layers = [
...layers,
new GeoJsonLayer({
id: "picked",
data: pickedGeoJson,
Expand Down Expand Up @@ -121,7 +153,7 @@ export default function Map({
new GeoJsonLayer({
id: "value",
data: valueGeoJson,
filled: !items,
filled: !items && !cogTileHref,
getFillColor: collections ? inverseFillColor : fillColor,
getLineColor: collections ? inverseLineColor : lineColor,
getLineWidth: 2,
Expand Down
10 changes: 9 additions & 1 deletion src/components/value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface SharedValueProps {
bbox: BBox2D | undefined;
setItems: (items: StacItem[] | undefined) => void;
setDatetimeBounds: (bounds: DatetimeBounds | undefined) => void;
cogTileHref: string | undefined;
setCogTileHref: (href: string | undefined) => void;
}

interface ValueProps extends SharedValueProps {
Expand All @@ -84,6 +86,8 @@ export function Value({
setFilter,
bbox,
setDatetimeBounds,
cogTileHref,
setCogTileHref,
}: ValueProps) {
const [search, setSearch] = useState<StacSearch>();
const [numberOfCollections, setNumberOfCollections] = useState<number>();
Expand Down Expand Up @@ -369,7 +373,11 @@ export function Value({
AccordionIcon={LuFiles}
accordionValue="assets"
>
<Assets assets={assets} />
<Assets
assets={assets}
cogTileHref={cogTileHref}
setCogTileHref={setCogTileHref}
/>
</Section>
)}

Expand Down
19 changes: 18 additions & 1 deletion src/utils/stac.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UseFileUploadReturn } from "@chakra-ui/react";
import type { StacCollection, StacItem, StacLink } from "stac-ts";
import type { StacAsset, StacCollection, StacItem, StacLink } from "stac-ts";
import type { BBox2D } from "../types/map";
import type { DatetimeBounds, StacAssets, StacValue } from "../types/stac";

Expand Down Expand Up @@ -250,3 +250,20 @@ export function getImportantLinks(links: StacLink[]) {
}
return { rootLink, collectionsLink, nextLink, prevLink, filteredLinks };
}

export function isCog(asset: StacAsset) {
return (
asset.type === "image/tiff; application=geotiff; profile=cloud-optimized"
);
}

export function isVisual(asset: StacAsset) {
if (asset.roles) {
for (const role of asset.roles) {
if (role === "visual" || role === "thumbnail") {
return true;
}
}
}
return false;
}