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

fix(GeoArrow): handle tessellation error & improve mean centers #2803

Merged
merged 4 commits into from
Nov 28, 2023
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
127 changes: 81 additions & 46 deletions modules/arrow/src/geoarrow/convert-geoarrow-to-binary-geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import {GeoArrowEncoding} from '@loaders.gl/gis';
import {updateBoundsFromGeoArrowSamples} from './get-arrow-bounds';
import {TypedArray} from '@loaders.gl/loader-utils';

/**
* Binary geometry type
*/
enum BinaryGeometryType {
points = 'points',
lines = 'lines',
polygons = 'polygons'
}

/**
* Binary data from geoarrow column and can be used by e.g. deck.gl GeojsonLayer
*/
Expand Down Expand Up @@ -57,7 +66,9 @@ export type BinaryGeometriesFromArrowOptions = {
/** option to specify which chunk to get binary geometries from, for progressive rendering */
chunkIndex?: number;
/** option to get mean centers from geometries, for polygon filtering */
meanCenter?: boolean;
calculateMeanCenters?: boolean;
/** option to compute the triangle indices by tesselating polygons */
triangulate?: boolean;
};

/**
Expand Down Expand Up @@ -86,7 +97,7 @@ export function getBinaryGeometriesFromArrow(

chunks.forEach((chunk) => {
const {featureIds, flatCoordinateArray, nDim, geomOffset, triangles} =
getBinaryGeometriesFromChunk(chunk, geoEncoding);
getBinaryGeometriesFromChunk(chunk, geoEncoding, options);

const globalFeatureIds = new Uint32Array(featureIds.length);
for (let i = 0; i < featureIds.length; i++) {
Expand Down Expand Up @@ -145,7 +156,7 @@ export function getBinaryGeometriesFromArrow(
binaryGeometries,
bounds,
featureTypes,
...(options?.meanCenter
...(options?.calculateMeanCenters
? {meanCenters: getMeanCentersFromBinaryGeometries(binaryGeometries)}
: {})
};
Expand All @@ -159,21 +170,22 @@ export function getBinaryGeometriesFromArrow(
export function getMeanCentersFromBinaryGeometries(binaryGeometries: BinaryFeatures[]): number[][] {
const globalMeanCenters: number[][] = [];
binaryGeometries.forEach((binaryGeometry: BinaryFeatures) => {
let binaryGeometryType: string | null = null;
let binaryGeometryType: keyof typeof BinaryGeometryType | null = null;
if (binaryGeometry.points && binaryGeometry.points.positions.value.length > 0) {
binaryGeometryType = 'points';
binaryGeometryType = BinaryGeometryType.points;
} else if (binaryGeometry.lines && binaryGeometry.lines.positions.value.length > 0) {
binaryGeometryType = 'lines';
binaryGeometryType = BinaryGeometryType.lines;
} else if (binaryGeometry.polygons && binaryGeometry.polygons.positions.value.length > 0) {
binaryGeometryType = 'polygons';
binaryGeometryType = BinaryGeometryType.polygons;
}

const binaryContent = binaryGeometryType ? binaryGeometry[binaryGeometryType] : null;
if (binaryContent && binaryGeometryType !== null) {
const featureIds = binaryContent.featureIds.value;
const flatCoordinateArray = binaryContent.positions.value;
const nDim = binaryContent.positions.size;
const primitivePolygonIndices = binaryContent.primitivePolygonIndices?.value;
const primitivePolygonIndices =
binaryContent.type === 'Polygon' ? binaryContent.primitivePolygonIndices?.value : undefined;

const meanCenters = getMeanCentersFromGeometry(
featureIds,
Expand Down Expand Up @@ -201,30 +213,33 @@ function getMeanCentersFromGeometry(
featureIds: TypedArray,
flatCoordinateArray: TypedArray,
nDim: number,
geometryType: string,
geometryType: keyof typeof BinaryGeometryType,
primitivePolygonIndices?: TypedArray
) {
const meanCenters: number[][] = [];
const vertexCount = flatCoordinateArray.length;
let vertexIndex = 0;
let coordIdx = 0;
let primitiveIdx = 0;
while (vertexIndex < vertexCount) {
const featureId = featureIds[vertexIndex / nDim];
const center = [0, 0];
let vertexCountInFeature = 0;
while (vertexIndex < vertexCount && featureIds[vertexIndex / nDim] === featureId) {
while (vertexIndex < vertexCount && featureIds[coordIdx] === featureId) {
if (
geometryType === 'polygons' &&
primitivePolygonIndices &&
primitivePolygonIndices.indexOf(vertexIndex / nDim) >= 0
geometryType === BinaryGeometryType.polygons &&
primitivePolygonIndices?.[primitiveIdx] === coordIdx
) {
// skip the first point since it is the same as the last point in each ring for polygons
vertexIndex += nDim;
primitiveIdx++;
} else {
center[0] += flatCoordinateArray[vertexIndex];
center[1] += flatCoordinateArray[vertexIndex + 1];
vertexIndex += nDim;
vertexCountInFeature++;
}
coordIdx += 1;
}
center[0] /= vertexCountInFeature;
center[1] /= vertexCountInFeature;
Expand All @@ -237,11 +252,13 @@ function getMeanCentersFromGeometry(
* get binary geometries from geoarrow column
* @param chunk one chunk/batch of geoarrow column
* @param geoEncoding geo encoding of the geoarrow column
* @param options options for getting binary geometries
* @returns BinaryGeometryContent
*/
function getBinaryGeometriesFromChunk(
chunk: arrow.Data,
geoEncoding: GeoArrowEncoding
geoEncoding: GeoArrowEncoding,
options?: BinaryGeometriesFromArrowOptions
): BinaryGeometryContent {
switch (geoEncoding) {
case 'geoarrow.point':
Expand All @@ -252,7 +269,7 @@ function getBinaryGeometriesFromChunk(
return getBinaryLinesFromChunk(chunk, geoEncoding);
case 'geoarrow.polygon':
case 'geoarrow.multipolygon':
return getBinaryPolygonsFromChunk(chunk, geoEncoding);
return getBinaryPolygonsFromChunk(chunk, geoEncoding, options);
default:
throw Error('invalid geoarrow encoding');
}
Expand All @@ -271,47 +288,62 @@ export function getTriangleIndices(
primitivePolygonIndices: Int32Array,
flatCoordinateArray: Float64Array,
nDim: number
): Uint32Array {
let primitiveIndex = 0;
const triangles: number[] = [];
// loop polygonIndices to get triangles
for (let i = 0; i < polygonIndices.length - 1; i++) {
const startIdx = polygonIndices[i];
const endIdx = polygonIndices[i + 1];
// get subarray of flatCoordinateArray
const slicedFlatCoords = flatCoordinateArray.subarray(startIdx * nDim, endIdx * nDim);
// get holeIndices for earcut
const holeIndices: number[] = [];
while (primitivePolygonIndices[primitiveIndex] < endIdx) {
if (primitivePolygonIndices[primitiveIndex] > startIdx) {
holeIndices.push(primitivePolygonIndices[primitiveIndex] - startIdx);
): Uint32Array | null {
try {
let primitiveIndex = 0;
const triangles: number[] = [];
// loop polygonIndices to get triangles
for (let i = 0; i < polygonIndices.length - 1; i++) {
const startIdx = polygonIndices[i];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this code be split into smaller helper functions - e.g. one that prepares the input to earcut?

const endIdx = polygonIndices[i + 1];
// get subarray of flatCoordinateArray
const slicedFlatCoords = flatCoordinateArray.subarray(startIdx * nDim, endIdx * nDim);
// get holeIndices for earcut
const holeIndices: number[] = [];
while (primitivePolygonIndices[primitiveIndex] < endIdx) {
if (primitivePolygonIndices[primitiveIndex] > startIdx) {
holeIndices.push(primitivePolygonIndices[primitiveIndex] - startIdx);
}
primitiveIndex++;
}
const triangleIndices = earcut(
slicedFlatCoords,
holeIndices.length > 0 ? holeIndices : undefined,
nDim
);
if (triangleIndices.length === 0) {
throw Error('can not tesselate invalid polygon');
}
for (let j = 0; j < triangleIndices.length; j++) {
triangles.push(triangleIndices[j] + startIdx);
}
primitiveIndex++;
}
const triangleIndices = earcut(
slicedFlatCoords,
holeIndices.length > 0 ? holeIndices : undefined,
nDim
);
for (let j = 0; j < triangleIndices.length; j++) {
triangles.push(triangleIndices[j] + startIdx);
// convert traingles to Uint32Array
const trianglesUint32 = new Uint32Array(triangles.length);
for (let i = 0; i < triangles.length; i++) {
trianglesUint32[i] = triangles[i];
}
return trianglesUint32;
} catch (error) {
// TODO - add logging
// there is an expection when tesselating invalid polygon, e.g. polygon with self-intersection
// return null to skip tesselating
return null;
}
// convert traingles to Uint32Array
const trianglesUint32 = new Uint32Array(triangles.length);
for (let i = 0; i < triangles.length; i++) {
trianglesUint32[i] = triangles[i];
}
return trianglesUint32;
}

/**
* get binary polygons from geoarrow polygon column
* @param chunk one chunk of geoarrow polygon column
* @param geoEncoding the geo encoding of the geoarrow polygon column
* @param options options for getting binary geometries
* @returns BinaryGeometryContent
*/
function getBinaryPolygonsFromChunk(chunk: arrow.Data, geoEncoding: string): BinaryGeometryContent {
function getBinaryPolygonsFromChunk(
chunk: arrow.Data,
geoEncoding: string,
options?: BinaryGeometriesFromArrowOptions
): BinaryGeometryContent {
const isMultiPolygon = geoEncoding === 'geoarrow.multipolygon';

const polygonData = isMultiPolygon ? chunk.children[0] : chunk;
Expand Down Expand Up @@ -341,14 +373,17 @@ function getBinaryPolygonsFromChunk(chunk: arrow.Data, geoEncoding: string): Bin
}
}

const triangles = getTriangleIndices(geometryIndicies, geomOffset, flatCoordinateArray, nDim);
const triangles = options?.triangulate
? getTriangleIndices(geometryIndicies, geomOffset, flatCoordinateArray, nDim)
: null;

return {
featureIds,
flatCoordinateArray,
nDim,
geomOffset,
geometryIndicies,
triangles
...(options?.triangulate && triangles ? {triangles} : {})
};
}

Expand Down
2 changes: 1 addition & 1 deletion modules/arrow/src/triangulate-on-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type TriangulateInput = {

/** Result type for operation: 'triangulate' */
export type TriangulateResult = TriangulateInput & {
triangleIndices: Uint32Array;
triangleIndices?: Uint32Array;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion modules/arrow/src/workers/triangulation-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ function triangulateBatch(data: TriangulateInput): TriangulateResult {
data.flatCoordinateArray,
data.nDim
);
return {...data, triangleIndices};
return {...data, ...(triangleIndices ? {triangleIndices} : {})};
}
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ async function testGetBinaryGeometriesFromArrow(

t.notEqual(encoding, undefined, 'encoding is not undefined');
if (geoColumn && encoding) {
const options = {meanCenter: true};
const options = {calculateMeanCenters: true, triangulate: true};
const binaryData = getBinaryGeometriesFromArrow(geoColumn, encoding, options);
t.deepEqual(binaryData, expectedBinaryGeometries, 'binary geometries are correct');
}
Expand Down