Skip to content

Commit

Permalink
fix(GeoArrow): handle tessellation error & improve mean centers (#2803)
Browse files Browse the repository at this point in the history
  • Loading branch information
lixun910 authored Nov 28, 2023
1 parent 77f3c1c commit 8a35c9a
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 49 deletions.
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];
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

0 comments on commit 8a35c9a

Please sign in to comment.