diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b76f03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +mason_packages +node_modules +build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2b08bbb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".mason"] + path = .mason + url = https://github.com/mapbox/mason diff --git a/.mason b/.mason new file mode 160000 index 0000000..76de7eb --- /dev/null +++ b/.mason @@ -0,0 +1 @@ +Subproject commit 76de7ebd9bfc838c23754c23f9cc745a4c84da9f diff --git a/.travis.yml b/.travis.yml index 8131feb..c645dce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3987c4f --- /dev/null +++ b/Makefile @@ -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 diff --git a/package.json b/package.json index d979510..09b00fb 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/polylabel.hpp b/polylabel.hpp new file mode 100644 index 0000000..3c6bb75 --- /dev/null +++ b/polylabel.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mapbox { + +namespace detail { + +// get squared distance from a point to a segment +template +T getSegDistSq(const geometry::point& p, + const geometry::point& a, + const geometry::point& 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 +auto pointToPolygonDist(const geometry::point& point, const geometry::polygon& polygon) { + bool inside = false; + auto minDistSq = std::numeric_limits::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 +struct Cell { + Cell(const geometry::point& c_, T h_, const geometry::polygon& polygon) + : c(c_), + h(h_), + d(pointToPolygonDist(c, polygon)), + max(d + h * std::sqrt(2)) + {} + + geometry::point 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 +Cell getCentroidCell(const geometry::polygon& polygon) { + T area = 0; + geometry::point 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& a = ring[i]; + const geometry::point& 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(area == 0 ? ring.at(0) : c / area, 0, polygon); +} + +} // namespace detail + +template +geometry::point polylabel(const geometry::polygon& polygon, T precision = 1, bool debug = false) { + using namespace detail; + + // find the bounding box of the outer ring + const geometry::box envelope = geometry::envelope(polygon.at(0)); + + const geometry::point 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& a, const Cell& b) { + return a.max < b.max; + }; + using Queue = std::priority_queue, std::vector>, 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({x + h, y + h}, h, polygon)); + } + } + + // take centroid as the first best guess + auto bestCell = getCentroidCell(polygon); + + // special case for rectangular polygons + Cell 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({cell.c.x - h, cell.c.y - h}, h, polygon)); + cellQueue.push(Cell({cell.c.x + h, cell.c.y - h}, h, polygon)); + cellQueue.push(Cell({cell.c.x - h, cell.c.y + h}, h, polygon)); + cellQueue.push(Cell({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 \ No newline at end of file diff --git a/index.js b/polylabel.js similarity index 100% rename from index.js rename to polylabel.js diff --git a/test/test.cpp b/test/test.cpp new file mode 100644 index 0000000..7af0d0e --- /dev/null +++ b/test/test.cpp @@ -0,0 +1,64 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +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_allocator>; +using rapidjson_value = rapidjson::GenericValue, rapidjson_allocator>; + +geometry::polygon 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 result; + + for (const auto& ringArray : d.GetArray()) { + geometry::linear_ring 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 water1 = fixture("./test/fixtures/water1.json"); + geometry::polygon water2 = fixture("./test/fixtures/water2.json"); + + // finds pole of inaccessibility for water1 and precision 1 + assert(polylabel(water1, 1.0) == geometry::point(3865.85009765625, 2124.87841796875)); + + // finds pole of inaccessibility for water1 and precision 50 + assert(polylabel(water1, 50.0) == geometry::point(3854.296875, 2123.828125)); + + // finds pole of inaccessibility for water2 and default precision 1 + assert(polylabel(water2) == geometry::point(3263.5, 3263.5)); + + // works on degenerate polygons + assert(polylabel(geometry::polygon({{{0, 0}, {1, 0}, {2, 0}, {0, 0}}})) + == geometry::point(0, 0)); + assert(polylabel(geometry::polygon({{{0, 0}, {1, 0}, {1, 1}, {1, 0}, {0, 0}}})) + == geometry::point(0, 0)); + + return 0; +}