forked from mapbox/polylabel
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
64fe157
commit cbff791
Showing
9 changed files
with
309 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
mason_packages | ||
node_modules | ||
build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[submodule ".mason"] | ||
path = .mason | ||
url = https://github.com/mapbox/mason |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |