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
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
66 changes: 44 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,40 @@ 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,
getColor: lineColor,
getRadius: 2,
getPosition: table.getChild("geometry")!,
radiusUnits: "pixels",
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