diff --git a/src/app.tsx b/src/app.tsx index e94f9b6..d9a417d 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -43,7 +43,7 @@ export default function App() { value, error, items: linkedItems, - table, + geoparqetTable, stacGeoparquetItem, } = useStacValue({ href, @@ -104,7 +104,7 @@ export default function App() { > | undefined; + geometryType: ValidGeometryType | undefined; +} export default function useStacValue({ href, @@ -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 || @@ -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, }; diff --git a/src/layers/map.tsx b/src/layers/map.tsx index 049142c..e1dfee5 100644 --- a/src/layers/map.tsx +++ b/src/layers/map.tsx @@ -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, @@ -29,7 +33,7 @@ export default function Map({ setBbox, picked, setPicked, - table, + geoparquetTable, setStacGeoparquetItemId, cogTileHref, }: { @@ -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; }) { @@ -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); @@ -119,7 +125,6 @@ export default function Map({ }, }) ); - layers = [ ...layers, new GeoJsonLayer({ @@ -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) { diff --git a/src/utils/stac-geoparquet.ts b/src/utils/stac-geoparquet.ts index 2e843ba..e92f0fb 100644 --- a/src/utils/stac-geoparquet.ts +++ b/src/utils/stac-geoparquet.ts @@ -11,6 +11,20 @@ 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 @@ -18,9 +32,12 @@ export async function getStacGeoparquet( 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( @@ -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++) { @@ -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( @@ -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";