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

Reduce GC pressure and virtual function overhead. Micro-bench shows 70% cpu time reduction. #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
8 changes: 7 additions & 1 deletion example/polylabel_example.dart
Original file line number Diff line number Diff line change
@@ -4,7 +4,13 @@ import 'package:polylabel/polylabel.dart';

void main() {
final polygon = [
[Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1), Point(0, 0)]
[
Point<double>(0, 0),
Point<double>(1, 0),
Point<double>(1, 1),
Point<double>(0, 1),
Point<double>(0, 0)
]
];
final result = polylabel(polygon);
print(result); // PolylabelResult(Point(0.5, 0.5), distance: 0.5)
61 changes: 36 additions & 25 deletions lib/src/data.dart
Original file line number Diff line number Diff line change
@@ -1,61 +1,72 @@
import 'dart:math' show min, sqrt, sqrt2, Point;

typedef Polygon = List<List<Point>>;
typedef Polygon = List<List<Point<double>>>;

class PolylabelResult {
final Point point;
final num distance;
final Point<double> point;
final double distance;

const PolylabelResult(this.point, this.distance);

@override
String toString() => '$runtimeType($point, distance: $distance)';
String toString() => 'PolylabelResult($point, distance: $distance)';
}

class Cell {
final Point c; // cell center
final num h; // half the cell size
final num d; // distance from cell center to polygon
late num max; // max distance to polygon within a cell
// Cell center (x, y).
final double x;
final double y;

Cell(this.c, this.h, Polygon polygon) : d = pointToPolygonDist(c, polygon) {
max = d + h * sqrt2;
final double h; // half the cell size
final double d; // distance from cell center to polygon

late final double max;

Cell(this.x, this.y, this.h, Polygon polygon)
: d = pointToPolygonDist(x, y, polygon) {
max = d + h * sqrt2; // max distance to polygon within a cell
}
}

/// Signed distance from point to polygon outline (negative if point is outside)
num pointToPolygonDist(Point point, Polygon polygon) {
double pointToPolygonDist(final double x, final double y, Polygon polygon) {
bool inside = false;
num minDistSq = double.infinity;
double minDistSq = double.infinity;

for (var k = 0; k < polygon.length; k++) {
for (int k = 0; k < polygon.length; k++) {
final ring = polygon[k];
final len = ring.length;

for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
for (int i = 0, j = len - 1; i < len; j = i++) {
final a = ring[i];
final b = ring[j];

if ((a.y > point.y != b.y > point.y) &&
(point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x)) {
if ((a.y > y != b.y > y) &&
(x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x)) {
inside = !inside;
}

minDistSq = min(minDistSq, getSegDistSq(point, a, b));
minDistSq = min<double>(minDistSq, getSegDistSq(x, y, a, b));
}
}

return minDistSq == 0 ? 0 : (inside ? 1 : -1) * sqrt(minDistSq);
}

/// Get squared distance from a point to a segment
num getSegDistSq(Point p, Point a, Point b) {
num x = a.x;
num y = a.y;
num dx = b.x - x;
num dy = b.y - y;
double getSegDistSq(
final double px,
final double py,
Point<double> a,
Point<double> b,
) {
double x = a.x;
double y = a.y;
double dx = b.x - x;
double dy = b.y - y;

if (dx != 0 || dy != 0) {
final t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
final t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);

if (t > 1) {
x = b.x;
@@ -66,8 +77,8 @@ num getSegDistSq(Point p, Point a, Point b) {
}
}

dx = p.x - x;
dy = p.y - y;
dx = px - x;
dy = py - y;

return dx * dx + dy * dy;
}
91 changes: 55 additions & 36 deletions lib/src/polylabel_base.dart
Original file line number Diff line number Diff line change
@@ -4,97 +4,116 @@ import 'package:collection/collection.dart';
import 'data.dart';

/// Finds the polygon pole of inaccessibility.
///
/// Precision is given in "polygon point" units. Generally, a larger number
/// means a looser acceptance threshold and thus a lower precision.
PolylabelResult polylabel(
List<List<Point>> polygon, {
Polygon polygon, {
double precision = 1.0,
bool debug = false,
}) {
// find the bounding box of the outer ring
num minX = 0, minY = 0, maxX = 0, maxY = 0;
for (var i = 0; i < polygon[0].length; i++) {
var p = polygon[0][i];
if (i == 0 || p.x < minX) minX = p.x;
if (i == 0 || p.y < minY) minY = p.y;
if (i == 0 || p.x > maxX) maxX = p.x;
if (i == 0 || p.y > maxY) maxY = p.y;
if (polygon.isEmpty || polygon[0].isEmpty) {
return _empty;
}

num width = maxX - minX;
num height = maxY - minY;
num cellSize = min(width, height);
num h = cellSize / 2;
final ring = polygon[0];

// find the bounding box of the outer ring
double minX = ring[0].x, minY = ring[0].y, maxX = ring[0].x, maxY = ring[0].y;

for (int i = 0; i < ring.length; i++) {
final p = ring[i];

if (p.x < minX) minX = p.x;
if (p.y < minY) minY = p.y;
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}

final width = maxX - minX;
final height = maxY - minY;
final cellSize = min<double>(width, height);
if (cellSize == 0) {
return PolylabelResult(Point(minX, minY), 0);
return _empty;
}

// a priority queue of cells in order of their "potential" (max distance to polygon)
final cellQueue = PriorityQueue<Cell>((a, b) => b.max.compareTo(a.max));

// cover polygon with initial cells
final h = cellSize / 2;
for (var x = minX; x < maxX; x += cellSize) {
for (var y = minY; y < maxY; y += cellSize) {
cellQueue.add(Cell(Point(x + h, y + h), h, polygon));
final dx = x + h;
final dy = y + h;
cellQueue.add(Cell(dx, dy, h, polygon));
}
}

// take centroid as the first best guess
var bestCell = _getCentroidCell(polygon);
Cell bestCell = _getCentroidCell(polygon);

// second guess: bounding box centroid
var bboxCell = Cell(Point(minX + width / 2, minY + height / 2), 0, polygon);
if (bboxCell.d > bestCell.d) bestCell = bboxCell;

int numProbes = cellQueue.length;
final bboxCell = Cell(minX + width / 2, minY + height / 2, 0, polygon);
if (bboxCell.d > bestCell.d) {
bestCell = bboxCell;
}

while (cellQueue.isNotEmpty) {
// pick the most promising cell from the queue
final cell = cellQueue.removeFirst();

// update the best cell if we found a better one
// update the best cell if we found a better one (i.e maximizing the distance).
if (cell.d > bestCell.d) {
bestCell = cell;
if (debug) {
print(
'found best ${(1e4 * cell.d).round() / 1e4} after $numProbes probes',
'found best ${(1e4 * cell.d).round() / 1e4} after ${cellQueue.length} probes',
);
}
}

// do not drill down further if there's no chance of a better solution
if (cell.max - bestCell.d <= precision) continue;
if (cell.max - bestCell.d <= precision) {
continue;
}

// split the cell into four cells
h = cell.h / 2;
cellQueue.add(Cell(Point(cell.c.x - h, cell.c.y - h), h, polygon));
cellQueue.add(Cell(Point(cell.c.x + h, cell.c.y - h), h, polygon));
cellQueue.add(Cell(Point(cell.c.x - h, cell.c.y + h), h, polygon));
cellQueue.add(Cell(Point(cell.c.x + h, cell.c.y + h), h, polygon));
numProbes += 4;
final h = cell.h / 2;
cellQueue.add(Cell(cell.x - h, cell.y - h, h, polygon));
cellQueue.add(Cell(cell.x + h, cell.y - h, h, polygon));
cellQueue.add(Cell(cell.x - h, cell.y + h, h, polygon));
cellQueue.add(Cell(cell.x + h, cell.y + h, h, polygon));
}

if (debug) {
print('best distance: ${bestCell.d}');
}

return PolylabelResult(bestCell.c, bestCell.d);
return PolylabelResult(Point<double>(bestCell.x, bestCell.y), bestCell.d);
}

/// Get polygon centroid
Cell _getCentroidCell(Polygon polygon) {
num area = 0;
num x = 0;
num y = 0;
double area = 0;
double x = 0;
double y = 0;
final ring = polygon[0];
final len = ring.length;

for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
for (int i = 0, j = len - 1; i < len; j = i++) {
final a = ring[i];
final b = ring[j];
final f = a.x * b.y - b.x * a.y;
x += (a.x + b.x) * f;
y += (a.y + b.y) * f;
area += f * 3;
}
if (area == 0) return Cell(ring[0], 0, polygon);
return Cell(Point(x / area, y / area), 0, polygon);

if (area == 0) {
return Cell(ring[0].x, ring[0].y, 0, polygon);
}
return Cell(x / area, y / area, 0, polygon);
}

const _empty = PolylabelResult(Point<double>(0, 0), 0);
11 changes: 6 additions & 5 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -2,14 +2,15 @@ name: polylabel
description: >-
A fast algorithm for finding polygon pole of inaccessibility implemented as a Dart library.
Useful for optimal placement of a text label on a polygon.
version: 1.0.1
version: 2.0.0
repository: https://github.com/beroso/dart_polylabel

environment:
sdk: '>=2.13.0 <3.0.0'
sdk: '>=3.00.0 <4.0.0'

dev_dependencies:
lints: ^1.0.0
test: ^1.16.0
dependencies:
collection: ^1.15.0

dev_dependencies:
lints: ^2.1.1
test: ^1.16.0
20 changes: 14 additions & 6 deletions test/polylabel_test.dart
Original file line number Diff line number Diff line change
@@ -6,15 +6,16 @@ import 'package:test/test.dart';

import 'fixtures/fixture_reader.dart';

List<List<Point>> toPolygon(List original) {
List<List<Point<double>>> toPolygon(List original) {
return original
.map((polygon) => (polygon as List)
.map((p) => Point(p.first as num, p.last as num))
.map((p) =>
Point((p.first as num).toDouble(), (p.last as num).toDouble()))
.toList())
.toList();
}

List<List<Point>> loadData(String fixtureFile) {
List<List<Point<double>>> loadData(String fixtureFile) {
return toPolygon(jsonDecode(fixture(fixtureFile)));
}

@@ -24,9 +25,16 @@ void main() {
final water2 = loadData('water2.json');

test('finds pole of inaccessibility for water1 and precision 1', () {
final p = polylabel(water1, precision: 1);
expect(p.point, Point(3865.85009765625, 2124.87841796875));
expect(p.distance, 288.8493574779127);
final watch = Stopwatch()..start();

const N = 50;
for (int i = 0; i < N; ++i) {
final p = polylabel(water1, precision: 1);
expect(p.point, Point(3865.85009765625, 2124.87841796875));
expect(p.distance, 288.8493574779127);
}

print('Elapsed time for $N iterations of water1: ${watch.elapsed}');
});

test('finds pole of inaccessibility for water1 and precision 50', () {