Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function App() {
value,
error,
items: linkedItems,
table,
geoparqetTable,
stacGeoparquetItem,
} = useStacValue({
href,
Expand Down Expand Up @@ -104,7 +104,7 @@ export default function App() {
>
<Map
value={value}
table={table}
geoparquetTable={geoparqetTable}
collections={collections}
filteredCollections={filteredCollections}
items={filteredItems}
Expand Down
19 changes: 15 additions & 4 deletions src/hooks/stac-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import type { UseFileUploadReturn } from "@chakra-ui/react";
import { AsyncDuckDBConnection } from "@duckdb/duckdb-wasm";
import { useQueries, useQuery } from "@tanstack/react-query";
import type { Table } from "apache-arrow";
import type { StacItem } from "stac-ts";
import { useDuckDb } from "duckdb-wasm-kit";
import type { DatetimeBounds, StacValue } from "../types/stac";
Expand All @@ -11,6 +12,13 @@ import {
getStacGeoparquetItem,
getStacGeoparquetTable,
} from "../utils/stac-geoparquet";
import { ValidGeometryType } from "../utils/stac-geoparquet";

export interface GeoparquetTable {
// eslint-disable-next-line
table: Table<any> | undefined;
geometryType: ValidGeometryType | undefined;
}

export default function useStacValue({
href,
Expand Down Expand Up @@ -76,9 +84,13 @@ export default function useStacValue({
enabled: enableStacGeoparquet && !!stacGeoparquetItemId,
});
const value = jsonResult.data || stacGeoparquetResult.data || undefined;
const table = enableStacGeoparquet
? stacGeoparquetTableResult.data || undefined
const table: GeoparquetTable | undefined = enableStacGeoparquet
? {
table: stacGeoparquetTableResult.data?.table || undefined,
geometryType: stacGeoparquetTableResult.data?.geometryType || undefined,
}
: undefined;

const error =
jsonResult.error ||
stacGeoparquetResult.error ||
Expand All @@ -102,11 +114,10 @@ export default function useStacValue({
};
},
});

return {
value,
error,
table,
geoparqetTable: table,
stacGeoparquetItem: stacGeoparquetItem.data,
items: itemsResult.data.length > 0 ? itemsResult.data : undefined,
};
Expand Down
68 changes: 46 additions & 22 deletions src/layers/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ import { type DeckProps, Layer } from "@deck.gl/core";
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 {
GeoArrowPolygonLayer,
GeoArrowScatterplotLayer,
} from "@geoarrow/deck.gl-layers";
import bbox from "@turf/bbox";
import bboxPolygon from "@turf/bbox-polygon";
import "maplibre-gl/dist/maplibre-gl.css";
import type { Table } from "apache-arrow";
import type { SpatialExtent, StacCollection, StacItem } from "stac-ts";
import type { BBox, Feature, FeatureCollection } from "geojson";
import { useColorModeValue } from "../components/ui/color-mode";
import type { GeoparquetTable } from "../hooks/stac-value";
import type { BBox2D, Color } from "../types/map";
import type { StacValue } from "../types/stac";
import { ValidGeometryType } from "../utils/stac-geoparquet";

export default function Map({
value,
Expand All @@ -29,7 +33,7 @@ export default function Map({
setBbox,
picked,
setPicked,
table,
geoparquetTable,
setStacGeoparquetItemId,
cogTileHref,
}: {
Expand All @@ -42,7 +46,7 @@ export default function Map({
setBbox: (bbox: BBox2D | undefined) => void;
picked: StacValue | undefined;
setPicked: (picked: StacValue | undefined) => void;
table: Table | undefined;
geoparquetTable: GeoparquetTable | undefined;
setStacGeoparquetItemId: (id: string | undefined) => void;
cogTileHref: string | undefined;
}) {
Expand All @@ -51,6 +55,8 @@ export default function Map({
"positron-gl-style",
"dark-matter-gl-style"
);
const table = geoparquetTable?.table;
const geometryType = geoparquetTable?.geometryType;
const valueGeoJson = useMemo(() => {
if (value) {
return valueToGeoJson(value);
Expand Down Expand Up @@ -119,7 +125,6 @@ export default function Map({
},
})
);

layers = [
...layers,
new GeoJsonLayer({
Expand Down Expand Up @@ -166,23 +171,42 @@ export default function Map({
},
}),
];

if (table)
layers.push(
new GeoArrowPolygonLayer({
id: "table",
data: table,
filled: true,
getFillColor: fillColor,
getLineColor: lineColor,
getLineWidth: 2,
lineWidthUnits: "pixels",
pickable: true,
onClick: (info) => {
setStacGeoparquetItemId(table.getChild("id")?.get(info.index));
},
})
);
if (table) {
if (geometryType === ValidGeometryType.Polygon) {
layers.push(
new GeoArrowPolygonLayer({
id: "table-polygon",
data: table,
filled: true,
getFillColor: fillColor,
getLineColor: lineColor,
getLineWidth: 2,
lineWidthUnits: "pixels",
pickable: true,
onClick: (info) => {
setStacGeoparquetItemId(table.getChild("id")?.get(info.index));
},
})
);
} else if (geometryType === ValidGeometryType.Point) {
layers.push(
new GeoArrowScatterplotLayer({
id: "table-point",
data: table,
getFillColor: fillColor,
getPosition: table.getChild("geometry")!,
getLineColor: lineColor,
getLineWidth: 2,
opacity: 1,
radiusMinPixels: 0.5,
pickable: true,
onClick: (info) => {
setStacGeoparquetItemId(table.getChild("id")?.get(info.index));
},
})
);
}
}

useEffect(() => {
if (value && mapRef.current) {
Expand Down
84 changes: 69 additions & 15 deletions src/utils/stac-geoparquet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,33 @@ import {
import * as stacWasm from "stac-wasm";
import type { DatetimeBounds, StacItemCollection } from "../types/stac";

// @ts-expect-error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
export enum ValidGeometryType {
Point = "point",
Polygon = "polygon",
}

const isValidGeometryType = (
geometryType: string
): geometryType is ValidGeometryType => {
return Object.values(ValidGeometryType).includes(
geometryType as ValidGeometryType
);
};

export async function getStacGeoparquet(
href: string,
connection: AsyncDuckDBConnection
) {
const { startDatetimeColumnName, endDatetimeColumnName } =
await getStacGeoparquetDatetimeColumns(href, connection);

const summaryResult = await connection.query(
`SELECT COUNT(*) as count, MIN(bbox.xmin) as xmin, MIN(bbox.ymin) as ymin, MAX(bbox.xmax) as xmax, MAX(bbox.ymax) as ymax, MIN(${startDatetimeColumnName}) as start_datetime, MAX(${endDatetimeColumnName}) as end_datetime FROM read_parquet('${href}')`
);
const query =
startDatetimeColumnName && endDatetimeColumnName
? `SELECT COUNT(*) as count, MIN(bbox.xmin) as xmin, MIN(bbox.ymin) as ymin, MAX(bbox.xmax) as xmax, MAX(bbox.ymax) as ymax, MIN(${startDatetimeColumnName}) as start_datetime, MAX(${endDatetimeColumnName}) as end_datetime FROM read_parquet('${href}')`
: `SELECT COUNT(*) as count, MIN(bbox.xmin) as xmin, MIN(bbox.ymin) as ymin, MAX(bbox.xmax) as xmax, MAX(bbox.ymax) as ymax FROM read_parquet('${href}')`;

const summaryResult = await connection.query(query);
const summaryRow = summaryResult.toArray().map((row) => row.toJSON())[0];

const kvMetadataResult = await connection.query(
Expand Down Expand Up @@ -65,12 +82,21 @@ export async function getStacGeoparquetTable(
const { startDatetimeColumnName, endDatetimeColumnName } =
await getStacGeoparquetDatetimeColumns(href, connection);

let query = `SELECT ST_AsWKB(geometry) as geometry, id FROM read_parquet('${href}')`;
let query = `SELECT ST_AsWKB(geometry) AS geometry, ST_GeometryType(geometry) AS geometry_type, id FROM read_parquet('${href}')`;
if (datetimeBounds) {
query += ` WHERE ${startDatetimeColumnName} >= DATETIME '${datetimeBounds.start.toISOString()}' AND ${endDatetimeColumnName} <= DATETIME '${datetimeBounds.end.toISOString()}'`;
}
const result = await connection.query(query);
const geometry: Uint8Array[] = result.getChildAt(0)?.toArray();
const geometryType: string = result
.getChildAt(1)
?.toArray()[0]
?.toLowerCase();
if (!isValidGeometryType(geometryType)) {
throw new Error(
`Invalid geometry type: ${geometryType}. We currently do not support this type.`
);
}
const wkb = new Uint8Array(geometry?.flatMap((array) => [...array]));
const valueOffsets = new Int32Array(geometry.length + 1);
for (let i = 0, len = geometry.length; i < len; i++) {
Expand All @@ -82,17 +108,35 @@ export async function getStacGeoparquetTable(
data: wkb,
valueOffsets,
});
const polygons = io.parseWkb(data, io.WKBType.Polygon, 2);
const table = new Table({
// @ts-expect-error: 2769
geometry: makeVector(polygons),
id: vectorFromArray(result.getChild("id")?.toArray()),
});
table.schema.fields[0].metadata.set(
"ARROW:extension:name",
"geoarrow.polygon"
);
return table;
let table: Table | undefined = undefined;
if (geometryType === ValidGeometryType.Polygon) {
const polygons = io.parseWkb(data, io.WKBType.Polygon, 2);
table = new Table({
// @ts-expect-error: 2769
geometry: makeVector(polygons),
id: vectorFromArray(result.getChild("id")?.toArray()),
});
table.schema.fields[0].metadata.set(
"ARROW:extension:name",
"geoarrow.polygon"
);
} else if (geometryType === ValidGeometryType.Point) {
const points = io.parseWkb(data, io.WKBType.Point, 2);
table = new Table({
// @ts-expect-error: 2769
geometry: points,
id: vectorFromArray(result.getChild("id")?.toArray()),
});
table.schema.fields[0].metadata.set(
"ARROW:extension:name",
"geoarrow.point"
);
}

return {
table: table,
geometryType: geometryType,
};
}

export async function getStacGeoparquetItem(
Expand All @@ -117,6 +161,16 @@ async function getStacGeoparquetDatetimeColumns(
);
const describe = describeResult.toArray().map((row) => row.toJSON());
const columnNames = describe.map((row) => row.column_name);
const containsDates: boolean = columnNames.some((columnName: string) => {
return columnName.includes("date");
});

if (!containsDates)
return {
startDatetimeColumnName: null,
endDatetimeColumnName: null,
};

const startDatetimeColumnName = columnNames.includes("start_datetime")
? "start_datetime"
: "datetime";
Expand Down