Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(geoarrow): Add support for Arrow meshes #3159

Merged
merged 5 commits into from
Nov 7, 2024
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
258 changes: 258 additions & 0 deletions examples/website/pointcloud/app-arrow.tsx
Original file line number Diff line number Diff line change
@@ -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<string, number>;
};

export default function App(props: AppProps = {}) {
const [state, setState] = useState<AppState>({
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 (
<div style={{position: 'relative', height: '100%'}}>
<ExamplePanel
examples={EXAMPLES}
format={props.format}
onExampleChange={onExampleChange}
>
{props.children}
{/* error ? <div style={{color: 'red'}}>{error}</div> : '' */}
<PointCloudStats
vertexCount={pointData?.length || 0}
loadTimeMs={state.loadTimeMs}
loadStartMs={state.loadStartMs}
/>
<h3>Schema and Metadata</h3>
<MetadataViewer metadata={state.metadata} />
</ExamplePanel>

<DeckGL
layers={layers}
views={new OrbitView({})}
viewState={state.viewState}
controller={{inertia: true}}
onViewStateChange={onViewStateChange}
// TODO - move to view
parameters={{
clearColor: [0.07, 0.14, 0.19, 1]
}}
></DeckGL>
</div>
);

/* <Attributions attributions={metadata?.attributions} /> */

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<void> {
// 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 (
<pre style={{textAlign: 'center', margin: 0}}>
<div>{Number.isFinite(vertexCount) ? `Points: ${message}` : null}</div>
<div>
{loadMessage}
</div>
</pre>
);
}

// 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(<App />, container);
}
2 changes: 1 addition & 1 deletion examples/website/pointcloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions modules/draco/src/lib/draco-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -268,6 +269,7 @@ export default class DracoParser {
default:
return {
topology: 'triangle-list',
// TODO - mode is wrong?
mode: 5, // GL.TRIANGLE_STRIP
attributes,
indices: {
Expand Down
2 changes: 1 addition & 1 deletion modules/draco/test/draco-writer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {};
};

Expand All @@ -34,20 +34,20 @@ 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<FlatGeobufVectorSource>;

/**
* FlatGeobufVectorSource
*/
export class FlatGeobufVectorSource
extends DataSource<string, FlatGeobuSourceOptions>
extends DataSource<string, FlatGeobufSourceOptions>
implements VectorSource
{
protected formatSpecificMetadata: Promise<any> | null = null;

constructor(data: string, options: FlatGeobuSourceOptions) {
constructor(data: string, options: FlatGeobufSourceOptions) {
super(data, options, FlatGeobufSource.defaultOptions);
// this.formatSpecificMetadata = this._getFormatSpecificMetadata();
}
Expand Down
4 changes: 4 additions & 0 deletions modules/flatgeobuf/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
5 changes: 5 additions & 0 deletions modules/geoarrow/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading
Loading