diff --git a/examples/website/pointcloud/app-arrow.tsx b/examples/website/pointcloud/app-arrow.tsx new file mode 100644 index 0000000000..b56093a993 --- /dev/null +++ b/examples/website/pointcloud/app-arrow.tsx @@ -0,0 +1,258 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React, {useState, useEffect} from 'react'; +import {render} from 'react-dom'; + +import DeckGL from '@deck.gl/react'; +import {COORDINATE_SYSTEM, OrbitView, LinearInterpolator} from '@deck.gl/core'; +import {PointCloudLayer} from '@deck.gl/layers'; + +import {load} from '@loaders.gl/core'; +import {getDeckBinaryDataFromArrowMesh, getBoundingBoxFromArrowPositions} from '@loaders.gl/geoarrow'; +import type {Mesh} from '@loaders.gl/schema'; +import {convertTableToMesh} from '@loaders.gl/schema-utils'; + +import {DracoArrowLoader} from '@loaders.gl/draco'; +import {LASArrowLoader} from '@loaders.gl/las'; +import {PLYArrowLoader} from '@loaders.gl/ply'; +import {PCDArrowLoader} from '@loaders.gl/pcd'; +import {OBJArrowLoader} from '@loaders.gl/obj'; + +import {ExamplePanel, Example, MetadataViewer} from './components/example-panel'; +import {EXAMPLES} from './examples'; + +// Additional format support can be added here, see +const POINT_CLOUD_LOADERS = [DracoArrowLoader, LASArrowLoader, PLYArrowLoader, PCDArrowLoader, OBJArrowLoader]; + +const INITIAL_VIEW_STATE = { + target: [0, 0, 0], + rotationX: 0, + rotationOrbit: 0, + orbitAxis: 'Y', + fov: 50, + minZoom: 0, + maxZoom: 10, + zoom: 1 +}; + +const transitionInterpolator = new LinearInterpolator(['rotationOrbit']); + +/** Application props (used by website MDX pages to configure example */ +type AppProps = { + /** Controls which examples are shown */ + format?: string; + /** Show tile borders */ + showTileBorders?: boolean; + /** On tiles load */ + onTilesLoad?: Function; + /** Any informational text to display in the overlay */ + children?: React.Children; +}; + +/** Application state */ +type AppState = { + /** Currently active tile source */ + pointData: any; + /** Metadata loaded from active tile source */ + metadata: string; + /**Current view state */ + viewState: Record; +}; + +export default function App(props: AppProps = {}) { + const [state, setState] = useState({ + viewState: INITIAL_VIEW_STATE, + pointData: null, + metadata: null + // TODO - handle errors + // error: null + }); + + const {pointData, selectedExample} = state; + + const layers = [ + pointData && + new PointCloudLayer({ + // Layers can't reinitialize with new binary data + id: `point-cloud-layer-${selectedExample}`, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + data: pointData, + getNormal: [0, 1, 0], + getColor: [200, 200, 255], + opacity: 0.5, + pointSize: 0.5 + }) + ]; + + return ( +
+ + {props.children} + {/* error ?
{error}
: '' */} + +

Schema and Metadata

+ +
+ + +
+ ); + + /* */ + + function onViewStateChange({viewState}) { + setState((state) => ({...state, viewState})); + } + + function rotateCamera() { + console.log('rotateCamera', state.viewState) + setState((state) => ({ + ...state, + viewState: { + ...state.viewState, + rotationOrbit: state.viewState.rotationOrbit + 10, + transitionDuration: 600, + transitionInterpolator, + onTransitionEnd: rotateCamera + } + })); + } + + async function onExampleChange({ + example, + exampleName + }: { + example: Example; + exampleName: string; + }): Promise { + // TODO - timing could be done automatically by `load`. + + setState((state) => ({ + ...state, + pointData: null, + metadata: null, + loadTimeMs: undefined, + loadStartMs: Date.now() + })); + + const {url} = example; + try { + const arrowTable = await load(url, POINT_CLOUD_LOADERS); + const pointCloud = convertTableToMesh(arrowTable); + const {schema, header, loaderData, attributes} = pointCloud; + + const viewState = getViewState(state, arrowTable, loaderData, attributes); + + const metadata = JSON.stringify({schema, header, loaderData}, null, 2); + + const pointData = getDeckBinaryDataFromArrowMesh(arrowTable.data); + + setState((state) => ({ + ...state, + loadTimeMs: Date.now() - state.loadStartMs, + loadStartMs: undefined, + // TODO - Some popular "point cloud" formats (PLY) can also generate indexed meshes + // in which case the vertex count is not correct for display as points + // Proposal: Consider adding a `mesh.points` or `mesh.pointcloud` option to mesh loaders + // in which case the loader throws away indices and just return the vertices? + pointData, + viewState, + metadata + })); + + rotateCamera(); + } catch (error) { + console.error('Failed to load data', url, error); + setState((state) => ({...state, error: `Could not load ${exampleName}: ${error.message}`})); + } + } +} + +/** + * Component that renders formatted stats for the point cloud + * @param props + * @returns + */ +function PointCloudStats(props: {vertexCount: number; loadTimeMs: number; loadStartMs: number}) { + const {vertexCount, loadTimeMs, loadStartMs} = props; + let message; + if (vertexCount >= 1e7) { + message = `${(vertexCount / 1e6).toFixed(0)}M`; + } else if (vertexCount >= 1e6) { + message = `${(vertexCount / 1e6).toFixed(1)}M`; + } else if (vertexCount >= 1e4) { + message = `${(vertexCount / 1e3).toFixed(0)}K`; + } else if (vertexCount >= 1e3) { + message = `${(vertexCount / 1e3).toFixed(1)}K`; + } else { + message = `${vertexCount}`; + } + + let loadMessage = ''; + if (loadTimeMs) { + loadMessage = `Load time: ${(loadTimeMs / 1000).toFixed(1)}s`; + } else if (loadStartMs) { + loadMessage = 'Loading...'; + } + + return ( +
+      
{Number.isFinite(vertexCount) ? `Points: ${message}` : null}
+
+ {loadMessage} +
+
+ ); +} + +// function getTooltip(info) { +// if (info.tile) { +// const {x, y, z} = info.tile.index; +// return `tile: x: ${x}, y: ${y}, z: ${z}`; +// } +// return null; +// } + +// HELPER FUNCTIONS + +function getViewState(state: AppState, arrowTable: ArrowTable, loaderData, attributes) { + // metadata from LAZ file header + const [mins, maxs] = + loaderData?.header?.mins && loaderData?.header?.maxs + ? [loaderData?.header?.mins, loaderData?.header?.maxs] + : getBoundingBoxFromArrowPositions(arrowTable.data.getChild('POSITION')); + + let {viewState} = state; + + // File contains bounding box info + return { + ...INITIAL_VIEW_STATE, + ...viewState, + target: [(mins[0] + maxs[0]) / 2, (mins[1] + maxs[1]) / 2, (mins[2] + maxs[2]) / 2], + zoom: Math.log2(window.innerWidth / (maxs[0] - mins[0])) - 1 + }; +} + +export function renderToDOM(container) { + render(, container); +} diff --git a/examples/website/pointcloud/package.json b/examples/website/pointcloud/package.json index a2a984b412..bc54ae04cb 100644 --- a/examples/website/pointcloud/package.json +++ b/examples/website/pointcloud/package.json @@ -10,9 +10,9 @@ "serve": "vite preview" }, "dependencies": { - "@deck.gl/react": "^9.0.1", "@deck.gl/core": "^9.0.1", "@deck.gl/layers": "^9.0.1", + "@deck.gl/react": "^9.0.1", "@loaders.gl/core": "4.4.0-alpha.0", "@loaders.gl/draco": "4.4.0-alpha.0", "@loaders.gl/las": "4.4.0-alpha.0", diff --git a/modules/draco/src/lib/draco-parser.ts b/modules/draco/src/lib/draco-parser.ts index caea06a984..9e1c9ac67b 100644 --- a/modules/draco/src/lib/draco-parser.ts +++ b/modules/draco/src/lib/draco-parser.ts @@ -257,6 +257,7 @@ export default class DracoParser { case 'triangle-strip': return { topology: 'triangle-strip', + // TODO - mode is wrong? mode: 4, // GL.TRIANGLES attributes, indices: { @@ -268,6 +269,7 @@ export default class DracoParser { default: return { topology: 'triangle-list', + // TODO - mode is wrong? mode: 5, // GL.TRIANGLE_STRIP attributes, indices: { diff --git a/modules/draco/test/draco-writer.spec.ts b/modules/draco/test/draco-writer.spec.ts index ef8d70bf11..93f135f68c 100644 --- a/modules/draco/test/draco-writer.spec.ts +++ b/modules/draco/test/draco-writer.spec.ts @@ -3,7 +3,7 @@ import {validateWriter, validateMeshCategoryData} from 'test/common/conformance' import {DracoLoader, DracoWriterOptions, DracoWriter, DracoWriterWorker} from '@loaders.gl/draco'; import {encode, fetchFile, parse} from '@loaders.gl/core'; -// import {getMeshSize} from '@loaders.gl/schema'; +// import {getMeshSize} from '@loaders.gl/schema-utils'; import draco3d from 'draco3d'; import {isBrowser, processOnWorker, WorkerFarm} from '@loaders.gl/worker-utils'; import {cloneTypeArray} from './test-utils/copyTypedArray'; diff --git a/modules/flatgeobuf/src/floatgeobuf-source.ts b/modules/flatgeobuf/src/flatgeobuf-source.ts similarity index 90% rename from modules/flatgeobuf/src/floatgeobuf-source.ts rename to modules/flatgeobuf/src/flatgeobuf-source.ts index 4a9ced9cb4..af4ba627ea 100644 --- a/modules/flatgeobuf/src/floatgeobuf-source.ts +++ b/modules/flatgeobuf/src/flatgeobuf-source.ts @@ -13,7 +13,7 @@ import {Source, DataSource, VectorSource} from '@loaders.gl/loader-utils'; import {FlatGeobufLoader} from './flatgeobuf-loader'; import {FlatGeobufFormat} from './flatgeobuf-format'; -export type FlatGeobuSourceOptions = DataSourceOptions & { +export type FlatGeobufSourceOptions = DataSourceOptions & { flatgeobuf?: {}; }; @@ -34,7 +34,7 @@ export const FlatGeobufSource = { }, testURL: (url: string): boolean => url.toLowerCase().includes('FeatureServer'), - createDataSource: (url: string, options: FlatGeobuSourceOptions): FlatGeobufVectorSource => + createDataSource: (url: string, options: FlatGeobufSourceOptions): FlatGeobufVectorSource => new FlatGeobufVectorSource(url, options) } as const satisfies Source; @@ -42,12 +42,12 @@ export const FlatGeobufSource = { * FlatGeobufVectorSource */ export class FlatGeobufVectorSource - extends DataSource + extends DataSource implements VectorSource { protected formatSpecificMetadata: Promise | null = null; - constructor(data: string, options: FlatGeobuSourceOptions) { + constructor(data: string, options: FlatGeobufSourceOptions) { super(data, options, FlatGeobufSource.defaultOptions); // this.formatSpecificMetadata = this._getFormatSpecificMetadata(); } diff --git a/modules/flatgeobuf/src/index.ts b/modules/flatgeobuf/src/index.ts index f6817cc4b9..365dd751dc 100644 --- a/modules/flatgeobuf/src/index.ts +++ b/modules/flatgeobuf/src/index.ts @@ -3,5 +3,9 @@ // Copyright (c) vis.gl contributors export {FlatGeobufFormat} from './flatgeobuf-format'; + export type {FlatGeobufLoaderOptions} from './flatgeobuf-loader'; export {FlatGeobufLoader, FlatGeobufWorkerLoader} from './flatgeobuf-loader'; + +export type {FlatGeobufSourceOptions} from './flatgeobuf-source'; +export {FlatGeobufSource as _FlatGeobufSource} from './flatgeobuf-source'; diff --git a/modules/geoarrow/src/index.ts b/modules/geoarrow/src/index.ts index 62f717bbc0..a2300f7d44 100644 --- a/modules/geoarrow/src/index.ts +++ b/modules/geoarrow/src/index.ts @@ -2,6 +2,11 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors +// MESH CATEGORY + +export {getBoundingBoxFromArrowPositions} from './mesharrow/get-bounding-box'; +export {getDeckBinaryDataFromArrowMesh} from './mesharrow/get-deck-binary-data'; + // GIS CATEGORY - GEOARROW export type {GeoArrowMetadata, GeoArrowEncoding} from './metadata/geoarrow-metadata'; diff --git a/modules/geoarrow/src/mesharrow/arrow-fixed-size-list-utils.ts b/modules/geoarrow/src/mesharrow/arrow-fixed-size-list-utils.ts new file mode 100644 index 0000000000..a9bf91d99d --- /dev/null +++ b/modules/geoarrow/src/mesharrow/arrow-fixed-size-list-utils.ts @@ -0,0 +1,62 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {TypedArray} from '@math.gl/types'; +import * as arrow from 'apache-arrow'; +import {getDataTypeFromArray, deserializeArrowType} from '@loaders.gl/schema-utils'; + +export function isFixedSizeList(vector: arrow.Vector): vector is arrow.Vector { + return vector.type instanceof arrow.FixedSizeList; +} + +export function getFixedSizeListSize(vector: arrow.Vector): number { + return vector.type instanceof arrow.FixedSizeList ? vector.type.listSize : 1; +} + +/** Get Arrow FixedSizeList vector from a typed array */ +export function getFixedSizeListVector( + typedArray: TypedArray, + stride: number +): arrow.Vector { + const data = getFixedSizeListData(typedArray, stride); + return new arrow.Vector([data]); +} + +/** Get Arrow FixedSizeList vector from a typed array */ +export function getFixedSizeListData( + typedArray: TypedArray, + stride: number +): arrow.Data { + const listType = getFixedSizeListType(typedArray, stride); + const nestedType = listType.children[0].type; + const buffers: Partial> = { + // valueOffsets: undefined, + [arrow.BufferType.DATA]: typedArray // values + // nullBitmap: undefined, + // typeIds: undefined + }; + + // Note: The contiguous array of data is held by the nested "primitive type" column + const nestedData = new arrow.Data(nestedType, 0, typedArray.length, 0, buffers); + + // Wrapped in a FixedSizeList column that provides a "strided" view of the data + const data = new arrow.Data( + listType, + 0, + typedArray.length / stride, + 0, + undefined, + [nestedData] + ); + + return data; +} + +/** Get Arrow FixedSizeList vector from a typed array */ +export function getFixedSizeListType(typedArray: TypedArray, stride: number): arrow.FixedSizeList { + const {type} = getDataTypeFromArray(typedArray); + const arrowType = deserializeArrowType(type); + const listType = new arrow.FixedSizeList(stride, new arrow.Field('value', arrowType)); + return listType; +} diff --git a/modules/geoarrow/src/mesharrow/arrow-list-of-fixed-size-list-utils.ts b/modules/geoarrow/src/mesharrow/arrow-list-of-fixed-size-list-utils.ts new file mode 100644 index 0000000000..0ff28e33f2 --- /dev/null +++ b/modules/geoarrow/src/mesharrow/arrow-list-of-fixed-size-list-utils.ts @@ -0,0 +1,47 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {TypedArray} from '@math.gl/types'; +import * as arrow from 'apache-arrow'; +import {getDataTypeFromArray, deserializeArrowType} from '@loaders.gl/schema-utils'; + +export function isListFixedSizeList( + vector: arrow.Vector +): vector is arrow.Vector { + return vector.type instanceof arrow.FixedSizeList; +} + +export function getListFixedSizeListSize(vector: arrow.Vector): number { + return vector.type instanceof arrow.FixedSizeList ? vector.type.listSize : 1; +} + +/** Get Arrow FixedSizeList vector from a typed array */ +export function getListFixedSizeListVector( + indexes: Uint32Array, + typedArray: TypedArray, + stride: number +): arrow.Vector { + const data = getFixedSizeListData(typedArray, stride); + return new arrow.Vector([data]); +} + +/** Get Arrow FixedSizeList vector from a typed array */ +export function getFixedSizeListData( + typedArray: TypedArray, + stride: number +): arrow.Data { + const listType = getFixedSizeListType(typedArray, stride); + const data = new arrow.Data(listType, 0, typedArray.length / stride, 0, [ + typedArray + ]); + return data; +} + +/** Get Arrow FixedSizeList vector from a typed array */ +export function getFixedSizeListType(typedArray: TypedArray, stride: number): arrow.FixedSizeList { + const {type} = getDataTypeFromArray(typedArray); + const arrowType = deserializeArrowType(type); + const listType = new arrow.FixedSizeList(stride, new arrow.Field('value', arrowType)); + return listType; +} diff --git a/modules/geoarrow/src/mesharrow/get-bounding-box.ts b/modules/geoarrow/src/mesharrow/get-bounding-box.ts new file mode 100644 index 0000000000..72345201e9 --- /dev/null +++ b/modules/geoarrow/src/mesharrow/get-bounding-box.ts @@ -0,0 +1,39 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import * as arrow from 'apache-arrow'; + +export type BoundingBox = [[number, number, number], [number, number, number]]; + +/** basic helper method to calculate a models upper and lower bounds */ +export function getBoundingBoxFromArrowPositions( + column: arrow.Vector +): BoundingBox { + const mins: [number, number, number] = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE]; + const maxs: [number, number, number] = [Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE]; + + const valueColumn = column.getChildAt(0)!; + for (const data of valueColumn.data) { + const pointSize = 3; // attributes.POSITION.size; + const pointData = data.buffers[arrow.BufferType.DATA]; + const pointCount = pointData.length / pointSize; + + for (let i = 0; i < pointCount; i += pointSize) { + const x = pointData[i]; + const y = pointData[i + 1]; + const z = pointData[i + 2]; + + if (x < mins[0]) mins[0] = x; + else if (x > maxs[0]) maxs[0] = x; + + if (y < mins[1]) mins[1] = y; + else if (y > maxs[1]) maxs[1] = y; + + if (z < mins[2]) mins[2] = z; + else if (z > maxs[2]) maxs[2] = z; + } + } + + return [mins, maxs]; +} diff --git a/modules/geoarrow/src/mesharrow/get-deck-binary-data.ts b/modules/geoarrow/src/mesharrow/get-deck-binary-data.ts new file mode 100644 index 0000000000..629dfd0209 --- /dev/null +++ b/modules/geoarrow/src/mesharrow/get-deck-binary-data.ts @@ -0,0 +1,43 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {TypedArray} from '@math.gl/types'; +import {getSizeAndValueFromMeshArrowVector} from './mesh-accessors'; +import * as arrow from 'apache-arrow'; + +/** */ +export type DeckBinaryData = { + length: number; + attributes: Record< + string, + { + value: TypedArray; + size: number; + } + >; +}; + +/** */ +export function getDeckBinaryDataFromArrowMesh(table: arrow.Table): DeckBinaryData { + const positionVector = table.getChild('POSITION'); + if (!positionVector) { + throw new Error('POSITION attribute not found'); + } + + const getPosition = getSizeAndValueFromMeshArrowVector(positionVector); + + const deckAttributes: DeckBinaryData['attributes'] = { + getPosition + }; + + const colorVector = table.getChild('COLOR_0'); + if (colorVector) { + deckAttributes.getColor = getSizeAndValueFromMeshArrowVector(colorVector); + } + // Check PointCloudLayer docs for other supported props? + return { + length: table.numRows, + attributes: deckAttributes + }; +} diff --git a/modules/geoarrow/src/mesharrow/mesh-accessors.ts b/modules/geoarrow/src/mesharrow/mesh-accessors.ts new file mode 100644 index 0000000000..1c687663a6 --- /dev/null +++ b/modules/geoarrow/src/mesharrow/mesh-accessors.ts @@ -0,0 +1,27 @@ +// loaders.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {TypedArray} from '@math.gl/types'; +import {getFixedSizeListSize} from './arrow-fixed-size-list-utils'; +import * as arrow from 'apache-arrow'; + +export function getSizeAndValueFromMeshArrowVector(attributeVector: arrow.Vector): { + size: number; + value: TypedArray; +} { + const size = getFixedSizeListSize(attributeVector); + const typedArrays = getTypedArraysFromMeshArrowVector(attributeVector); + return {size, value: typedArrays[0]}; +} + +export function getTypedArraysFromMeshArrowVector(attributeVector: arrow.Vector): TypedArray[] { + const typedArrays: TypedArray[] = []; + for (const attributeData of attributeVector.data) { + const valueData = attributeData?.children[0]; + const typedArray = valueData.values; + typedArrays.push(typedArray); + } + + return typedArrays; +} diff --git a/modules/geoarrow/tsconfig.json b/modules/geoarrow/tsconfig.json index 5ab893e6d7..7a730e0090 100644 --- a/modules/geoarrow/tsconfig.json +++ b/modules/geoarrow/tsconfig.json @@ -8,5 +8,6 @@ "outDir": "dist" }, "references": [ + {"path": "../schema-utils"} ] } diff --git a/modules/las/src/index.ts b/modules/las/src/index.ts index f3e992dbb0..ea9311cc44 100644 --- a/modules/las/src/index.ts +++ b/modules/las/src/index.ts @@ -4,6 +4,8 @@ // LASLoader +export {LASFormat} from './las-format'; + export type {LASLoaderOptions} from './las-loader'; export {LASWorkerLoader, LASLoader} from './las-loader'; -// export {LASArrowLoader} from './las-arrow-loader'; +export {LASArrowLoader} from './las-arrow-loader'; diff --git a/modules/obj/src/index.ts b/modules/obj/src/index.ts index db6fae1d70..d18f7f1242 100644 --- a/modules/obj/src/index.ts +++ b/modules/obj/src/index.ts @@ -7,6 +7,7 @@ export {OBJFormat} from './obj-format'; export type {OBJLoaderOptions} from './obj-loader'; export {OBJWorkerLoader, OBJLoader} from './obj-loader'; +export {OBJArrowLoader} from './obj-arrow-loader'; // MTLLoader diff --git a/modules/ply/src/index.ts b/modules/ply/src/index.ts index 83b1e67637..ed32958c40 100644 --- a/modules/ply/src/index.ts +++ b/modules/ply/src/index.ts @@ -4,5 +4,7 @@ // PLYLoader export {PLYFormat} from './ply-format'; + export type {PLYLoaderOptions} from './ply-loader'; export {PLYWorkerLoader, PLYLoader} from './ply-loader'; +export {PLYArrowLoader} from './ply-arrow-loader';