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(polygon): Improve speed of Polygon.getSignedArea() #19

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ jobs:
uses: coverallsapp/github-action@09b709cf6a16e30b0808ba050c7a6e8a5ef13f8d # v1.2.5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Bench
run: |
yarn test bench
31 changes: 29 additions & 2 deletions modules/polygon/src/polygon-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,47 @@ export const DimIndex: Record<string, number> = {
* @returns Signed area of the polygon.
* https://en.wikipedia.org/wiki/Shoelace_formula
*/
export function getPolygonSignedArea(points: NumericArray, options: PolygonParams = {}): number {
export function getPolygonSignedArea(
points: NumericArray,
options: PolygonParams & {fast?: boolean} = {}
): number {
const {start = 0, end = points.length, plane = 'xy'} = options;
const dim = options.size || 2;
let area = 0;
const i0 = DimIndex[plane[0]];
const i1 = DimIndex[plane[1]];

let area = 0;
for (let i = start, j = end - dim; i < end; i += dim) {
area += (points[i + i0] - points[j + i0]) * (points[i + i1] + points[j + i1]);
j = i;
}
return area / 2;
}

export function getPolygonSignedAreaFlatFast(
points: NumericArray,
options: PolygonParams = {}
): number {
// const {start = 0} = options; TODO - fast
if (options.start) {
throw new Error('start option is not supported in fast mode');
}
const {end = points.length, plane = 'xy'} = options;
const dim = options.size || 2;
const i0 = DimIndex[plane[0]];
const i1 = DimIndex[plane[1]];

const Area = function (curr: number, prev: number): number {
return (points[curr + i0] - points[prev + i0]) * (points[curr + i1] - points[prev + i1]);
};

let area = Area(0, end - dim);
for (let i = dim; i < end; i += dim) {
area += Area(i, i - dim);
}
return area / 2;
}

/**
* Calls the visitor callback for each segment in the polygon.
* @param points An array that represents points of the polygon
Expand Down
11 changes: 7 additions & 4 deletions modules/polygon/src/polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {NumericArray} from '@math.gl/core';

import {
getPolygonSignedArea,
getPolygonSignedAreaFlatFast,
forEachSegmentInPolygon,
modifyPolygonWindingDirection,
getPolygonSignedAreaPoints,
Expand Down Expand Up @@ -46,10 +47,12 @@ export class Polygon {
* Returns signed area of the polygon.
* @returns Signed area of the polygon.
*/
getSignedArea(): number {
if (this.isFlatArray) return getPolygonSignedArea(this.points as NumericArray, this.options);

return getPolygonSignedAreaPoints(this.points as number[][], this.options);
getSignedArea(fast?: boolean): number {
return this.isFlatArray
? fast
? getPolygonSignedAreaFlatFast(this.points as NumericArray, this.options)
: getPolygonSignedArea(this.points as NumericArray, this.options)
: getPolygonSignedAreaPoints(this.points as number[][], this.options);
}

/**
Expand Down
74 changes: 53 additions & 21 deletions modules/polygon/test/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@
// @ts-nocheck
import {earcut, Polygon, modifyPolygonWindingDirection, WINDING} from '@math.gl/polygon';
import {toNested} from './utils';
import {fstat} from 'fs';

const polygonSmall = [0, 0, 1, 1, 0, 2, -1, 1, -1.25, 0.5, 0, 0];
const flatPolygonSmall = [0, 0, 1, 1, 0, 2, -1, 1, -1.25, 0.5, 0, 0];

const polygonMedium = [
const flatPolygonMedium = [
4.2625, 2.24125, 3.0025, 3.20125, 2.5225, 4.22125, 0.9225, 4.32125, -0.3775, 3.30125, -0.7975,
2.14125, -1.8575, 2.72125, -1.8575, 0.64125, -0.3775, -0.89875, -0.3775, -0.89875, 1.2825,
0.92125, 1.4025, -0.89875, 2.9025, -0.31875, 4.0825, 0.62125, 4.2625, 2.24125
];
const polygonMediumNested = toNested(polygonMedium);
const nestedPolygonMedium = toNested(flatPolygonMedium);

const flatPolygonLarge = new Array(10000)
.fill(0)
.map((_, i) =>
i % 2 === 0 ? Math.cos((Math.PI * 2 * i) / 5000) : Math.sin((Math.PI * 2 * i) / 5000)
);
const nestedPolygonLarge = toNested(flatPolygonLarge);

// A helper function to swap winding direction on each iteration.
let winding = WINDING.CLOCKWISE;
Expand All @@ -28,44 +36,68 @@ function nextWinding() {
export function polygonBench(suite, addReferenceBenchmarks) {
suite
.group('Polygon')
.add('Polygon#new()', () => new Polygon(polygonSmall))
.add('Polygon#modifyWindingDirection() S', () => {
const polygon = new Polygon(polygonSmall);
.add('Polygon#new()', () => new Polygon(flatPolygonSmall))
.add('Polygon#modifyWindingDirection() flat small', () => {
const polygon = new Polygon(flatPolygonSmall);
polygon.modifyWindingDirection(nextWinding());
})
.add('modifyPolygonWindingDirection() S', () => {
modifyPolygonWindingDirection(polygonSmall, nextWinding(), {
.add('modifyPolygonWindingDirection() flat small', () => {
modifyPolygonWindingDirection(flatPolygonSmall, nextWinding(), {
isClosed: true,
start: 0,
end: polygonSmall.length,
end: flatPolygonSmall.length,
size: 2
});
})
.add('Polygon#modifyWindingDirection() M', () => {
const polygon = new Polygon(polygonMedium);
.add('Polygon#modifyWindingDirection() flat medium', () => {
const polygon = new Polygon(flatPolygonMedium);
polygon.modifyWindingDirection(nextWinding());
})
.add('modifyPolygonWindingDirection() M', () => {
modifyPolygonWindingDirection(polygonMedium, nextWinding(), {
.add('modifyPolygonWindingDirection() flat medium', () => {
modifyPolygonWindingDirection(flatPolygonMedium, nextWinding(), {
isClosed: true,
start: 0,
end: polygonMedium.length,
end: flatPolygonMedium.length,
size: 2
});
})
.add('Polygon#getSignedArea()', () => {
const polygon = new Polygon(polygonMedium);
.add('Polygon#getSignedArea() flat medium', () => {
const polygon = new Polygon(flatPolygonMedium);
polygon.getSignedArea();
})
.add('Polygon#getSignedArea() nested', () => {
const polygon = new Polygon(polygonMediumNested);
.add('Polygon#getSignedArea(fast) flat medium', () => {
const polygon = new Polygon(flatPolygonMedium);
polygon.getSignedArea(true);
})
.add('Polygon#getSignedArea() nested medium', () => {
const polygon = new Polygon(nestedPolygonMedium);
polygon.getSignedArea();
})
.add('Polygon#getSignedArea(fast) nested medium', () => {
const polygon = new Polygon(nestedPolygonMedium);
polygon.getSignedArea(true);
})
.add('Polygon#getSignedArea() flat large', () => {
const polygon = new Polygon(flatPolygonLarge);
polygon.getSignedArea();
})
.add('earcut with precomputed areas ', () => {
earcut(polygonMedium, [], 2, [-21.3664]);
.add('Polygon#getSignedArea(fast) flat large ', () => {
const polygon = new Polygon(flatPolygonLarge);
polygon.getSignedArea(true);
})
.add('Polygon#getSignedArea() nested large', () => {
const polygon = new Polygon(nestedPolygonLarge);
polygon.getSignedArea();
})
.add('Polygon#getSignedArea(fast) nested large', () => {
const polygon = new Polygon(nestedPolygonLarge);
polygon.getSignedArea(true);
})
.add('earcut with precomputed areas', () => {
earcut(flatPolygonMedium, [], 2, [-21.3664]);
})
.add('earcut', () => {
earcut(polygonMedium, [], 2);
earcut(flatPolygonMedium, [], 2);
});

return suite;
Expand Down
6 changes: 3 additions & 3 deletions test/bench/modules.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {cullingBench} from '../../modules/culling/test/bench';
import {polygonBench} from '../../modules/polygon/test/bench';

export default function addBenchmarks(suite: Bench, addReferenceBenchmarks: boolean): Bench {
coreBench(suite, addReferenceBenchmarks);
geospatialBench(suite, addReferenceBenchmarks);
cullingBench(suite, addReferenceBenchmarks);
// coreBench(suite, addReferenceBenchmarks);
// geospatialBench(suite, addReferenceBenchmarks);
// cullingBench(suite, addReferenceBenchmarks);
polygonBench(suite, addReferenceBenchmarks);

return suite;
Expand Down
Loading