Skip to content

Commit

Permalink
Add C++ port (mapbox#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
jfirebaugh authored and mourner committed Dec 16, 2016
1 parent 64fe157 commit cbff791
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mason_packages
node_modules
build
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule ".mason"]
path = .mason
url = https://github.com/mapbox/mason
1 change: 1 addition & 0 deletions .mason
Submodule .mason added at 76de7e
39 changes: 35 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@
language: node_js
node_js:
- "4"
- "stable"
sudo: false
dist: trusty
cache: apt

matrix:
include:
- language: generic
env: CXX=g++-5
addons:
apt:
sources: [ 'ubuntu-toolchain-r-test' ]
packages: [ 'g++-5' ]
script:
- make test

- language: generic
env: CXX=clang++-3.8
addons:
apt:
sources: [ 'ubuntu-toolchain-r-test' ]
packages: [ 'clang-3.8', 'libstdc++-5-dev', 'libstdc++6' ]
script:
- make test

- language: node
node_js: 4
script:
- npm install
- npm test

- language: node
node_js: stable
script:
- npm install
- npm test
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CXXFLAGS += -I. -std=c++14 -Wall -Wextra -Wshadow -Werror -g -fPIC

MASON ?= .mason/mason
VARIANT = variant 1.1.4
GEOMETRY = geometry 0.9.0
RAPIDJSON = rapidjson 1.1.0

DEPS = `$(MASON) cflags $(VARIANT)` \
`$(MASON) cflags $(GEOMETRY)` \
`$(MASON) cflags $(RAPIDJSON)`

mason_packages/headers/geometry:
$(MASON) install $(VARIANT)
$(MASON) install $(GEOMETRY)
$(MASON) install $(RAPIDJSON)

build:
mkdir -p build

build/test: test/test.cpp polylabel.hpp build mason_packages/headers/geometry
$(CXX) $(CFLAGS) $(CXXFLAGS) $(DEPS) $< -o $@

test: build/test
./build/test
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "polylabel",
"version": "1.0.2",
"description": "A JS library for finding optimal label position inside a polygon",
"main": "index.js",
"main": "polylabel.js",
"scripts": {
"pretest": "eslint *.js test/*.js",
"test": "tape test/test.js"
Expand Down
178 changes: 178 additions & 0 deletions polylabel.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#pragma once

#include <mapbox/geometry/polygon.hpp>
#include <mapbox/geometry/envelope.hpp>
#include <mapbox/geometry/point.hpp>
#include <mapbox/geometry/point_arithmetic.hpp>

#include <algorithm>
#include <cmath>
#include <iostream>
#include <queue>

namespace mapbox {

namespace detail {

// get squared distance from a point to a segment
template <class T>
T getSegDistSq(const geometry::point<T>& p,
const geometry::point<T>& a,
const geometry::point<T>& b) {
auto x = a.x;
auto y = a.y;
auto dx = b.x - x;
auto dy = b.y - y;

if (dx != 0 || dy != 0) {

auto 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;
}

// signed distance from point to polygon outline (negative if point is outside)
template <class T>
auto pointToPolygonDist(const geometry::point<T>& point, const geometry::polygon<T>& polygon) {
bool inside = false;
auto minDistSq = std::numeric_limits<double>::infinity();

for (const auto& ring : polygon) {
for (std::size_t i = 0, len = ring.size(), j = len - 1; i < len; j = i++) {
const auto& a = ring[i];
const auto& 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 = std::min(minDistSq, getSegDistSq(point, a, b));
}
}

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

template <class T>
struct Cell {
Cell(const geometry::point<T>& c_, T h_, const geometry::polygon<T>& polygon)
: c(c_),
h(h_),
d(pointToPolygonDist(c, polygon)),
max(d + h * std::sqrt(2))
{}

geometry::point<T> c; // cell center
T h; // half the cell size
T d; // distance from cell center to polygon
T max; // max distance to polygon within a cell
};

// get polygon centroid
template <class T>
Cell<T> getCentroidCell(const geometry::polygon<T>& polygon) {
T area = 0;
geometry::point<T> c { 0, 0 };
const auto& ring = polygon.at(0);

for (std::size_t i = 0, len = ring.size(), j = len - 1; i < len; j = i++) {
const geometry::point<T>& a = ring[i];
const geometry::point<T>& b = ring[j];
auto f = a.x * b.y - b.x * a.y;
c.x += (a.x + b.x) * f;
c.y += (a.y + b.y) * f;
area += f * 3;
}

return Cell<T>(area == 0 ? ring.at(0) : c / area, 0, polygon);
}

} // namespace detail

template <class T>
geometry::point<T> polylabel(const geometry::polygon<T>& polygon, T precision = 1, bool debug = false) {
using namespace detail;

// find the bounding box of the outer ring
const geometry::box<T> envelope = geometry::envelope(polygon.at(0));

const geometry::point<T> size {
envelope.max.x - envelope.min.x,
envelope.max.y - envelope.min.y
};

const T cellSize = std::min(size.x, size.y);
T h = cellSize / 2;

// a priority queue of cells in order of their "potential" (max distance to polygon)
auto compareMax = [] (const Cell<T>& a, const Cell<T>& b) {
return a.max < b.max;
};
using Queue = std::priority_queue<Cell<T>, std::vector<Cell<T>>, decltype(compareMax)>;
Queue cellQueue(compareMax);

if (cellSize == 0) {
return envelope.min;
}

// cover polygon with initial cells
for (T x = envelope.min.x; x < envelope.max.x; x += cellSize) {
for (T y = envelope.min.y; y < envelope.max.y; y += cellSize) {
cellQueue.push(Cell<T>({x + h, y + h}, h, polygon));
}
}

// take centroid as the first best guess
auto bestCell = getCentroidCell(polygon);

// special case for rectangular polygons
Cell<T> bboxCell(envelope.min + size / 2.0, 0, polygon);
if (bboxCell.d > bestCell.d) {
bestCell = bboxCell;
}

auto numProbes = cellQueue.size();
while (!cellQueue.empty()) {
// pick the most promising cell from the queue
auto cell = cellQueue.top();
cellQueue.pop();

// update the best cell if we found a better one
if (cell.d > bestCell.d) {
bestCell = cell;
if (debug) std::cout << "found best " << std::round(1e4 * cell.d) / 1e4 << " after " << numProbes << " probes" << std::endl;
}

// 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.push(Cell<T>({cell.c.x - h, cell.c.y - h}, h, polygon));
cellQueue.push(Cell<T>({cell.c.x + h, cell.c.y - h}, h, polygon));
cellQueue.push(Cell<T>({cell.c.x - h, cell.c.y + h}, h, polygon));
cellQueue.push(Cell<T>({cell.c.x + h, cell.c.y + h}, h, polygon));
numProbes += 4;
}

if (debug) {
std::cout << "num probes: " << numProbes << std::endl;
std::cout << "best distance: " << bestCell.d << std::endl;
}

return bestCell.c;
}

} // namespace mapbox
File renamed without changes.
64 changes: 64 additions & 0 deletions test/test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include <polylabel.hpp>

#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>

#include <cassert>
#include <fstream>
#include <sstream>
#include <iostream>

using namespace mapbox;

// Use the CrtAllocator, because the MemoryPoolAllocator is broken on ARM
// https://github.com/miloyip/rapidjson/issues/200, 301, 388
using rapidjson_allocator = rapidjson::CrtAllocator;
using rapidjson_document = rapidjson::GenericDocument<rapidjson::UTF8<>, rapidjson_allocator>;
using rapidjson_value = rapidjson::GenericValue<rapidjson::UTF8<>, rapidjson_allocator>;

geometry::polygon<double> fixture(const std::string& path) {
std::ifstream t(path.c_str());
std::stringstream buffer;
buffer << t.rdbuf();

rapidjson_document d;
d.Parse(buffer.str().c_str());

geometry::polygon<double> result;

for (const auto& ringArray : d.GetArray()) {
geometry::linear_ring<double> ring;
for (const auto& point : ringArray.GetArray()) {
ring.push_back({
point[0].GetDouble(),
point[1].GetDouble()
});
}
result.push_back(std::move(ring));
}

return result;
}

int main() {
geometry::polygon<double> water1 = fixture("./test/fixtures/water1.json");
geometry::polygon<double> water2 = fixture("./test/fixtures/water2.json");

// finds pole of inaccessibility for water1 and precision 1
assert(polylabel(water1, 1.0) == geometry::point<double>(3865.85009765625, 2124.87841796875));

// finds pole of inaccessibility for water1 and precision 50
assert(polylabel(water1, 50.0) == geometry::point<double>(3854.296875, 2123.828125));

// finds pole of inaccessibility for water2 and default precision 1
assert(polylabel(water2) == geometry::point<double>(3263.5, 3263.5));

// works on degenerate polygons
assert(polylabel(geometry::polygon<double>({{{0, 0}, {1, 0}, {2, 0}, {0, 0}}}))
== geometry::point<double>(0, 0));
assert(polylabel(geometry::polygon<double>({{{0, 0}, {1, 0}, {1, 1}, {1, 0}, {0, 0}}}))
== geometry::point<double>(0, 0));

return 0;
}

0 comments on commit cbff791

Please sign in to comment.