Skip to content

Commit

Permalink
🎉 Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
beroso committed Nov 10, 2021
0 parents commit 998e6a7
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Files and directories created by pub.
.dart_tool/
.packages

# Conventional directory for build outputs.
build/

# Omit committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

- Initial version.
11 changes: 11 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Copyright 2021 André Sousa

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->

# polylabel

Dart port of https://github.com/mapbox/polylabel.

## Usage

```dart
import 'dart:math';
import 'package:polylabel/polylabel.dart';
final polygon = [[Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1), Point(0, 0)]];
final result = polylabel(polygon); // PolylabelResult{x: 0.5, y: 0.5, distance: 0.5}
```
30 changes: 30 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.

include: package:lints/recommended.yaml

# Uncomment the following section to specify additional rules.

# linter:
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
6 changes: 6 additions & 0 deletions example/polylabel_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:polylabel/polylabel.dart';

// void main() {
// var awesome = Awesome();
// print('awesome: ${awesome.isAwesome}');
// }
6 changes: 6 additions & 0 deletions lib/polylabel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// Support for doing something awesome.
///
/// More dartdocs go here.
library polylabel;

export 'src/polylabel_base.dart';
175 changes: 175 additions & 0 deletions lib/src/polylabel_base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import 'dart:math' show min, sqrt, sqrt2, Point;
import 'package:collection/collection.dart';

typedef Polygon = List<List<Point>>;

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(this.c, this.h, Polygon polygon) : d = pointToPolygonDist(c, polygon) {
max = d + h * sqrt2;
}
}

class PolylabelResult {
final num x;
final num y;
final num distance;
PolylabelResult(this.x, this.y, this.distance);
}

/// Finds the polygon pole of inaccessibility, the most distant internal point
/// from the polygon outline (not to be confused with centroid).
///
/// Useful for optimal placement of a text label on a polygon.
PolylabelResult polylabel(
List<List<Point>> 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;
}

num width = maxX - minX;
num height = maxY - minY;
num cellSize = min(width, height);
num h = cellSize / 2;

if (cellSize == 0) {
return PolylabelResult(minX, minY, 0);
}

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

// cover polygon with initial cells
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));
}
}

// take centroid as the first best guess
var 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;

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
if (cell.d > bestCell.d) {
bestCell = cell;
if (debug) {
print(
'found best ${(1e4 * cell.d).round() / 1e4} after $numProbes probes',
);
}
}

// do not drill down further if there's no chance of a better solution
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;
}

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

return PolylabelResult(bestCell.c.x, bestCell.c.y, bestCell.d);
}

/// Compare two cells
int compareMax(Cell a, Cell b) {
return (b.max - a.max).toInt();
}

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

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

for (var i = 0, len = ring.length, 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)) {
inside = !inside;
}

minDistSq = min(minDistSq, getSegDistSq(point, a, b));
}
}

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

/// Get polygon centroid
Cell getCentroidCell(Polygon polygon) {
num area = 0;
num x = 0;
num y = 0;
final ring = polygon[0];

for (var i = 0, len = ring.length, 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);
}

/// 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;

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

if (t > 1) {
x = b.x;
y = b.y;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}

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

return dx * dx + dy * dy;
}
13 changes: 13 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: polylabel
description: Dart port of https://github.com/mapbox/polylabel.
version: 0.1.0
repository: https://github.com/beroso/dart_polylabel

environment:
sdk: '>=2.14.4 <3.0.0'

dev_dependencies:
lints: ^1.0.0
test: ^1.16.0
dependencies:
collection: ^1.15.0
16 changes: 16 additions & 0 deletions test/polylabel_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:polylabel/polylabel.dart';
import 'package:test/test.dart';

void main() {
// group('A group of tests', () {
// final awesome = Awesome();

// setUp(() {
// // Additional setup goes here.
// });

// test('First Test', () {
// expect(awesome.isAwesome, isTrue);
// });
// });
}

0 comments on commit 998e6a7

Please sign in to comment.