[Git][debian-gis-team/mapbox-polylabel][master] 7 commits: New upstream version 2.1.0
Bas Couwenberg (@sebastic)
gitlab at salsa.debian.org
Fri Jul 3 12:03:25 BST 2026
Bas Couwenberg pushed to branch master at Debian GIS Project / mapbox-polylabel
Commits:
6b5982af by Bas Couwenberg at 2026-06-25T13:58:58+02:00
New upstream version 2.1.0
- - - - -
665cf175 by Bas Couwenberg at 2026-07-03T11:51:18+02:00
Update upstream to Mapnik fork, Mapbox dropped C++ implementation.
- - - - -
e5eac8d1 by Bas Couwenberg at 2026-07-03T12:55:01+02:00
New upstream version 2.1.0
- - - - -
acf8e6d3 by Bas Couwenberg at 2026-07-03T12:55:02+02:00
Update upstream source from tag 'upstream/2.1.0'
Update to upstream version '2.1.0'
with Debian dir 037484943ab5e23eee276d03178bc7d0be7acf95
- - - - -
e01886d7 by Bas Couwenberg at 2026-07-03T12:55:19+02:00
New upstream release.
- - - - -
b4d011e8 by Bas Couwenberg at 2026-07-03T12:56:48+02:00
Update copyright file.
- - - - -
ea7d8ac5 by Bas Couwenberg at 2026-07-03T12:58:27+02:00
Set distribution to unstable.
- - - - -
22 changed files:
- + .github/workflows/npm-release.yml
- .github/workflows/test.yml
- .gitignore
- − .gitmodules
- + CODEOWNERS
- LICENSE
- − Makefile
- README.md
- + bench.js
- + bench/bench.cpp
- debian/changelog
- debian/control
- debian/copyright
- debian/upstream/metadata
- debian/watch
- − debug.js
- include/mapbox/polylabel.hpp
- package-lock.json
- package.json
- polylabel.js
- test/test.cpp
- test/test.js
Changes:
=====================================
.github/workflows/npm-release.yml
=====================================
@@ -0,0 +1,17 @@
+name: Publish
+on:
+ push:
+ tags: ['v*']
+permissions:
+ id-token: write
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ environment: npm-release
+ steps:
+ - uses: actions/checkout at v6
+ - uses: actions/setup-node at v6
+ with:
+ node-version: 24
+ - run: npm ci
+ - run: npm publish --access public
=====================================
.github/workflows/test.yml
=====================================
@@ -4,24 +4,10 @@ jobs:
node:
runs-on: ubuntu-latest
steps:
- - name: Checkout
- uses: actions/checkout at v2
-
- - name: Setup Node
- uses: actions/setup-node at v1
+ - uses: actions/checkout at v6
+ - uses: actions/setup-node at v6
with:
- node-version: 20
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run tests
- run: npm test
- cpp:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout at v2
-
- - name: Test C++
- run: make test
+ node-version: 24
+ cache: npm
+ - run: npm ci
+ - run: npm test
=====================================
.gitignore
=====================================
@@ -1,3 +1 @@
-mason_packages
node_modules
-build
=====================================
.gitmodules deleted
=====================================
@@ -1,3 +0,0 @@
-[submodule ".mason"]
- path = .mason
- url = https://github.com/mapbox/mason
=====================================
CODEOWNERS
=====================================
@@ -0,0 +1 @@
+* @mapbox/gl-js
=====================================
LICENSE
=====================================
@@ -1,5 +1,5 @@
ISC License
-Copyright (c) 2016 Mapbox
+Copyright (c) 2026 Mapbox
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
=====================================
Makefile deleted
=====================================
@@ -1,27 +0,0 @@
-CXXFLAGS += -Iinclude -std=c++14 -Wall -Wextra -Wshadow -Werror -Wno-class-memaccess -g -fPIC
-
-MASON ?= .mason/mason
-VARIANT = variant 1.1.4
-GEOMETRY = geometry 0.9.2
-RAPIDJSON = rapidjson 1.1.0
-
-$(MASON):
- git submodule update --init
-
-DEPS = `$(MASON) cflags $(VARIANT)` \
- `$(MASON) cflags $(GEOMETRY)` \
- `$(MASON) cflags $(RAPIDJSON)`
-
-mason_packages/headers/geometry: $(MASON)
- $(MASON) install $(VARIANT)
- $(MASON) install $(GEOMETRY)
- $(MASON) install $(RAPIDJSON)
-
-build:
- mkdir -p build
-
-build/test: test/test.cpp include/mapbox/polylabel.hpp build mason_packages/headers/geometry
- $(CXX) $(CFLAGS) $(CXXFLAGS) $(DEPS) $< -o $@
-
-test: build/test
- ./build/test
=====================================
README.md
=====================================
@@ -48,20 +48,6 @@ Be careful to pick precision appropriate for the input units. E.g. in case of ge
[TypeScript type definitions](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/polylabel)
are available via `npm install --save @types/polylabel`.
-### C++ Usage
-
-It is recommended to install polylabel via [mason](https://github.com/mapbox/mason). You will also need to install its dependencies: [geometry.hpp](https://github.com/mapbox/geometry.hpp) and [variant](https://github.com/mapbox/variant).
-
-```C++
-#include <mapbox/polylabel.hpp>
-
-int main() {
- mapbox::geometry::polygon<double> polygon = readPolygon(); // Get polygon data from somewhere.
- mapbox::geometry::point<double> p = mapbox::polylabel(polygon, 1.0);
- return 0;
-}
-```
-
#### Ports to other languages
- [andrewharvey/geojson-polygon-labels](https://github.com/andrewharvey/geojson-polygon-labels) (CLI)
@@ -69,8 +55,10 @@ int main() {
- [Shapely](https://github.com/Toblerity/Shapely/blob/master/shapely/algorithms/polylabel.py) (Python)
- [polylabelr](https://CRAN.R-project.org/package=polylabelr) (R)
- [polylabel-rs](https://github.com/urschrei/polylabel-rs) (Rust)
-- [polylabel-java](https://github.com/FreshLlamanade/polylabel-java) (Java)
- [php-polylabel](https://github.com/dliebner/php-polylabel) (PHP)
- [dart_polylabel](https://github.com/beroso/dart_polylabel) (Dart)
- [ruby-polylabel](https://github.com/fredplante/ruby-polylabel) (Ruby)
- [Polylabel.jl](https://github.com/asinghvi17/Polylabel.jl) (Julia)
+- [geobase](https://github.com/navibyte/geospatial/blob/main/dart/geobase/lib/src/geometric/cartesian/areal/polylabel.dart) (Dart)
+- [PolylabelNet](https://github.com/oberbichler/PolylabelNet) (C#)
+- [polylabel-geometrynodes](https://github.com/farukahmet/polylabel-geometrynodes) (Blender)
=====================================
bench.js
=====================================
@@ -0,0 +1,24 @@
+import polylabel from './polylabel.js';
+import water1 from './test/fixtures/water1.json' with {type: 'json'};
+import water2 from './test/fixtures/water2.json' with {type: 'json'};
+
+const cases = [
+ ['water1 precision 1', water1, 1],
+ ['water1 precision 50', water1, 50],
+ ['water2 precision 1', water2, 1],
+];
+
+const RUNS = 100;
+
+for (const [name, polygon, precision] of cases) {
+ const times = [];
+ for (let r = 0; r < RUNS; r++) {
+ const t = performance.now();
+ polylabel(polygon, precision);
+ times.push(performance.now() - t);
+ }
+ times.sort((a, b) => a - b);
+ const min = times[0];
+ const median = times[times.length >> 1];
+ console.log(`${name}: min ${min.toFixed(2)}ms, median ${median.toFixed(2)}ms`);
+}
=====================================
bench/bench.cpp
=====================================
@@ -0,0 +1,172 @@
+#include <mapbox/polylabel.hpp>
+
+#include <chrono>
+#include <cctype>
+#include <cmath>
+#include <cstdlib>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace mapbox;
+
+volatile double sink = 0;
+
+class JsonFixtureParser {
+public:
+ explicit JsonFixtureParser(std::string input_) : input(std::move(input_)) {}
+
+ geometry::polygon<double> parsePolygon() {
+ geometry::polygon<double> polygon;
+ consume('[');
+ skipWhitespace();
+ while (!tryConsume(']')) {
+ polygon.push_back(parseRing());
+ skipWhitespace();
+ tryConsume(',');
+ }
+ return polygon;
+ }
+
+private:
+ geometry::linear_ring<double> parseRing() {
+ geometry::linear_ring<double> ring;
+ consume('[');
+ skipWhitespace();
+ while (!tryConsume(']')) {
+ consume('[');
+ const double x = parseNumber();
+ consume(',');
+ const double y = parseNumber();
+ consume(']');
+ ring.push_back({x, y});
+ skipWhitespace();
+ tryConsume(',');
+ }
+ return ring;
+ }
+
+ double parseNumber() {
+ skipWhitespace();
+ const char* begin = input.c_str() + pos;
+ char* end = nullptr;
+ const double value = std::strtod(begin, &end);
+ if (end == begin) throw std::runtime_error("expected number");
+ pos += static_cast<std::size_t>(end - begin);
+ skipWhitespace();
+ return value;
+ }
+
+ void consume(char expected) {
+ skipWhitespace();
+ if (pos >= input.size() || input[pos] != expected) {
+ throw std::runtime_error(std::string("expected '") + expected + "'");
+ }
+ ++pos;
+ skipWhitespace();
+ }
+
+ bool tryConsume(char expected) {
+ skipWhitespace();
+ if (pos < input.size() && input[pos] == expected) {
+ ++pos;
+ skipWhitespace();
+ return true;
+ }
+ return false;
+ }
+
+ void skipWhitespace() {
+ while (pos < input.size() && std::isspace(static_cast<unsigned char>(input[pos]))) {
+ ++pos;
+ }
+ }
+
+ std::string input;
+ std::size_t pos = 0;
+};
+
+geometry::polygon<double> fixture(const std::string& path) {
+ std::ifstream file(path.c_str());
+ std::stringstream buffer;
+ buffer << file.rdbuf();
+ return JsonFixtureParser(buffer.str()).parsePolygon();
+}
+
+geometry::linear_ring<double> regularRing(std::size_t n, double cx, double cy, double radius, bool clockwise) {
+ constexpr double pi = 3.141592653589793238462643383279502884;
+ geometry::linear_ring<double> ring;
+ ring.reserve(n + 1);
+
+ for (std::size_t i = 0; i < n; ++i) {
+ const std::size_t index = clockwise ? n - i : i;
+ const double angle = 2.0 * pi * static_cast<double>(index) / static_cast<double>(n);
+ ring.push_back({cx + std::cos(angle) * radius, cy + std::sin(angle) * radius});
+ }
+ ring.push_back(ring.front());
+ return ring;
+}
+
+geometry::polygon<double> donut(std::size_t n) {
+ geometry::polygon<double> polygon;
+ polygon.push_back(regularRing(n, 0, 0, 5000, false));
+ polygon.push_back(regularRing(n, 0, 0, 2200, true));
+ return polygon;
+}
+
+geometry::polygon<double> sawtoothSquare(std::size_t teethPerSide) {
+ geometry::linear_ring<double> ring;
+ ring.reserve(teethPerSide * 4 + 1);
+ const double side = 10000;
+ const double step = side / static_cast<double>(teethPerSide);
+
+ for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({-5000 + i * step, -5000 - (i % 2 ? 12.0 : 0.0)});
+ for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({5000 + (i % 2 ? 12.0 : 0.0), -5000 + i * step});
+ for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({5000 - i * step, 5000 + (i % 2 ? 12.0 : 0.0)});
+ for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({-5000 - (i % 2 ? 12.0 : 0.0), 5000 - i * step});
+ ring.push_back(ring.front());
+
+ return geometry::polygon<double>({ring});
+}
+
+void runCase(const std::string& name, const geometry::polygon<double>& polygon, double precision, std::size_t iterations) {
+ auto p = polylabel(polygon, precision);
+ sink += p.x + p.y;
+
+ const auto start = std::chrono::steady_clock::now();
+ double checksum = 0;
+ for (std::size_t i = 0; i < iterations; ++i) {
+ p = polylabel(polygon, precision);
+ checksum += p.x * 0.0000001 + p.y * 0.0000003;
+ }
+ const auto stop = std::chrono::steady_clock::now();
+ const std::chrono::duration<double, std::milli> elapsed = stop - start;
+ sink += checksum;
+
+ std::cout << std::left << std::setw(26) << name
+ << " iterations=" << std::setw(5) << iterations
+ << " total_ms=" << std::setw(12) << std::fixed << std::setprecision(3) << elapsed.count()
+ << " ms_per_call=" << std::setw(10) << std::setprecision(6) << elapsed.count() / iterations
+ << " result=(" << std::setprecision(6) << p.x << ", " << p.y << ")"
+ << '\n';
+}
+
+int main() {
+ const auto water1 = fixture("./test/fixtures/water1.json");
+ const auto water2 = fixture("./test/fixtures/water2.json");
+ const auto donut4096 = donut(4096);
+ const auto square8192 = sawtoothSquare(2048);
+
+ runCase("water1 precision 1", water1, 1.0, 400);
+ runCase("water1 precision 50", water1, 50.0, 2000);
+ runCase("water2 precision 1", water2, 1.0, 1000);
+ runCase("donut 8194 vertices", donut4096, 5.0, 80);
+ runCase("square 8193 vertices", square8192, 5.0, 80);
+
+ std::cerr << "sink=" << sink << '\n';
+}
=====================================
debian/changelog
=====================================
@@ -1,13 +1,15 @@
-mapbox-polylabel (2.0.1-2) UNRELEASED; urgency=medium
+mapbox-polylabel (2.1.0-1) unstable; urgency=medium
+ * New upstream release.
* Bump Standards-Version to 4.7.4, no changes.
* Update lintian overrides.
* Drop Rules-Requires-Root: no, default since dpkg 1.22.13.
* Use test-build-validate-cleanup instead of test-build-twice.
* Drop Priority: optional, default since dpkg 1.22.13.
- * Limit watch file to 2.0.x releases, 2.1 dropped the C++ implementation.
+ * Update upstream to Mapnik fork, Mapbox dropped C++ implementation.
+ * Update copyright file.
- -- Bas Couwenberg <sebastic at debian.org> Sun, 28 Jul 2024 19:43:49 +0200
+ -- Bas Couwenberg <sebastic at debian.org> Fri, 03 Jul 2026 12:58:12 +0200
mapbox-polylabel (2.0.1-1) unstable; urgency=medium
=====================================
debian/control
=====================================
@@ -8,7 +8,7 @@ Build-Depends: debhelper-compat (= 13),
Standards-Version: 4.7.4
Vcs-Browser: https://salsa.debian.org/debian-gis-team/mapbox-polylabel/
Vcs-Git: https://salsa.debian.org/debian-gis-team/mapbox-polylabel.git
-Homepage: https://github.com/mapbox/polylabel
+Homepage: https://github.com/mapnik/polylabel
Package: libmapbox-polylabel-dev
Architecture: all
=====================================
debian/copyright
=====================================
@@ -1,10 +1,10 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: polylabel
-Upstream-Contact: Mapbox (https://github.com/mapbox/polylabel/issues)
-Source: https://github.com/mapbox/polylabel
+Upstream-Contact: Mapnik developers (https://github.com/mapnik/polylabel/issues)
+Source: https://github.com/mapnik/polylabel
Files: *
-Copyright: 2016, Mapbox
+Copyright: 2026, Mapbox
License: ISC
Files: debian/*
=====================================
debian/upstream/metadata
=====================================
@@ -1,5 +1,5 @@
---
-Bug-Database: https://github.com/mapbox/polylabel/issues
-Bug-Submit: https://github.com/mapbox/polylabel/issues/new
-Repository: https://github.com/mapbox/polylabel.git
-Repository-Browse: https://github.com/mapbox/polylabel
+Bug-Database: https://github.com/mapnik/polylabel/issues
+Bug-Submit: https://github.com/mapnik/polylabel/issues/new
+Repository: https://github.com/mapnik/polylabel.git
+Repository-Browse: https://github.com/mapnik/polylabel
=====================================
debian/watch
=====================================
@@ -3,5 +3,5 @@ opts=\
dversionmangle=s/\+(debian|dfsg|ds|deb)\d*$//,\
uversionmangle=s/(\d)[_\.\-\+]?((RC|rc|pre|dev|beta|alpha)\d*)$/$1~$2/,\
filenamemangle=s/(?:.*\/)?(?:rel|v|polylabel)[\-\_]?(\d\S+)\.(tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))/polylabel-$1.$2/ \
-https://github.com/mapbox/polylabel/tags \
-(?:.*?/archive/(?:.*?/)?)?(?:rel|v|polylabel)[\-\_]?(2\.0\.\S+)\.(?:tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+https://github.com/mapnik/polylabel/tags \
+(?:.*?/archive/(?:.*?/)?)?(?:rel|v|polylabel)[\-\_]?(\d\S+)\.(?:tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
=====================================
debug.js deleted
=====================================
@@ -1,13 +0,0 @@
-
-import polylabel from './polylabel.js';
-import {readFileSync} from 'fs';
-
-const polygon = JSON.parse(readFileSync(new URL('./test/fixtures/water1.json', import.meta.url)));
-
-console.log(`num points: ${[].concat(...polygon).length}`);
-
-console.time('find point');
-const result = polylabel(polygon, 1, true);
-console.timeEnd('find point');
-
-console.log(result);
=====================================
include/mapbox/polylabel.hpp
=====================================
@@ -1,5 +1,37 @@
#pragma once
+// polylabel — find the pole of inaccessibility (most distant internal point)
+// of a polygon, useful for optimal label placement.
+//
+// This is the C++ port of polylabel v2.1.0 (https://github.com/mapbox/polylabel),
+// preserving the public API and mapbox/geometry.hpp dependencies of the prior
+// v2.0.1 C++ implementation while bringing in all v2.1.0 algorithmic
+// improvements:
+//
+// * Clamped initial cell size: cellSize = max(precision, min(width, height)),
+// with an immediate early-out when min(width, height) <= precision (covers
+// degenerate / sub-precision polygons).
+// * Flattened, cache-friendly coordinate buffer + per-ring end offsets,
+// eliminating pointer-chasing in the hot distance loop.
+// * Block-based bounding-box skipping: K consecutive edges share one bbox so
+// whole blocks are skipped in O(1) when they can neither improve the
+// distance nor flip the ray-cast parity.
+// * Early-out distance threshold: a child cell whose distance is provably
+// unable to beat the current best (or to be worth subdividing) bails out
+// of the outline scan as soon as that fact is established.
+// * Seed-based segment warm-start: each cell records its nearest segment and
+// hands it to its children, which almost always share the same nearest
+// segment and thus reach the early-out threshold without a full scan.
+// * Aggressive queue pruning: a cell is enqueued only if it can still beat
+// the best by more than `precision`; the main loop can `break` (not just
+// `continue`) on the first cell that cannot, because the priority queue is
+// ordered by decreasing potential.
+// * Centroid fallback: if the outer ring has zero area or the centroid lands
+// outside the polygon, fall back to the first vertex.
+//
+// The result is bit-identical to v2.0.1 for all existing test fixtures while
+// being substantially faster on large polygons.
+
#include <mapbox/geometry/polygon.hpp>
#include <mapbox/geometry/envelope.hpp>
#include <mapbox/geometry/point.hpp>
@@ -7,164 +39,351 @@
#include <algorithm>
#include <cmath>
+#include <cstddef>
#include <iostream>
+#include <limits>
#include <queue>
+#include <vector>
namespace mapbox {
namespace detail {
-// get squared distance from a point to a segment
+// number of consecutive edges grouped under a single bounding box for block-skip
+constexpr std::size_t K = 32;
+
+// get squared distance from a point (px, py) to a segment (x, y)-(bx, by)
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;
+T getSegDistSq(T px, T py, T x, T y, T bx, T by) {
+ auto dx = bx - x;
+ auto dy = by - y;
if (dx != 0 || dy != 0) {
-
- auto t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
+ auto t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
- x = b.x;
- y = b.y;
-
+ x = bx;
+ y = by;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
- dx = p.x - x;
- dy = p.y - y;
+ dx = px - x;
+ dy = py - y;
return dx * dx + dy * dy;
}
-// signed distance from point to polygon outline (negative if point is outside)
+// flattened polygon: a contiguous coordinate buffer + ring end offsets +
+// precomputed per-block bounding boxes for O(1) block-skip in the hot distance
+// loop. Built once per polylabel() call and shared (by const reference) across
+// all cells.
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();
+struct FlatPolygon {
+ std::vector<T> coords; // x0,y0,x1,y1,... (rings concatenated)
+ std::vector<std::size_t> ringEnds; // end offset (exclusive) into coords, per ring
+ std::vector<T> blocks; // minX,minY,maxX,maxY per block of K edges
+};
+
+template <class T>
+FlatPolygon<T> flatten(const geometry::polygon<T>& polygon) {
+ FlatPolygon<T> fp;
+
+ std::size_t numPoints = 0;
+ for (const auto& ring : polygon) numPoints += ring.size();
+ fp.coords.reserve(numPoints * 2);
+ fp.ringEnds.reserve(polygon.size());
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];
+ for (const auto& p : ring) {
+ fp.coords.push_back(p.x);
+ fp.coords.push_back(p.y);
+ }
+ fp.ringEnds.push_back(fp.coords.size());
+ }
- 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;
+ // precompute one bounding box per block of K consecutive edges (over both
+ // endpoints of every edge in it) so the distance scan can skip whole blocks
+ // in O(1). The block layout mirrors the flattened coords/ringEnds and is
+ // re-derived in the scan, so only the bboxes need storing: a flat
+ // [minX,minY,maxX,maxY] run per block.
+ const std::size_t stride = K * 2;
+ std::size_t numBlocks = 0;
+ std::size_t ringStart = 0;
+ for (std::size_t r = 0; r < fp.ringEnds.size(); r++) {
+ numBlocks += (fp.ringEnds[r] - ringStart + stride - 1) / stride; // ceil
+ ringStart = fp.ringEnds[r];
+ }
- minDistSq = std::min(minDistSq, getSegDistSq(point, a, b));
+ fp.blocks.resize(numBlocks * 4);
+ std::size_t g = 0;
+ ringStart = 0;
+ for (std::size_t r = 0; r < fp.ringEnds.size(); r++) {
+ const std::size_t ringEnd = fp.ringEnds[r];
+ for (std::size_t s = ringStart; s < ringEnd; s += stride, g += 4) {
+ const std::size_t end = (s + stride < ringEnd) ? s + stride : ringEnd;
+ // include the vertex preceding the block so the block's first edge
+ // (which connects to it) is accounted for in the bbox
+ const std::size_t prev = (s == ringStart) ? ringEnd - 2 : s - 2;
+
+ T minX = fp.coords[prev], minY = fp.coords[prev + 1];
+ T maxX = minX, maxY = minY;
+ for (std::size_t i = s; i < end; i += 2) {
+ const T px = fp.coords[i], py = fp.coords[i + 1];
+ if (px < minX) minX = px; else if (px > maxX) maxX = px;
+ if (py < minY) minY = py; else if (py > maxY) maxY = py;
+ }
+ fp.blocks[g] = minX;
+ fp.blocks[g + 1] = minY;
+ fp.blocks[g + 2] = maxX;
+ fp.blocks[g + 3] = maxY;
}
+ ringStart = ringEnd;
}
- return (inside ? 1 : -1) * std::sqrt(minDistSq);
+ return fp;
}
+template <class T>
+struct Cell;
+
+// signed distance from cell center to polygon outline (negative if outside),
+// also recording the nearest segment on the cell. maxD is a distance threshold:
+// if a partial result proves the center is no farther than maxD from the
+// outline, the scan bails out early and returns maxD, since the caller has
+// already determined such a cell can't beat the best. seed is the parent cell
+// (or nullptr); its nearest segment is checked first so boundary cells reach
+// the early-out threshold without scanning the whole outline.
+template <class T>
+T pointToPolygonDist(Cell<T>& cell, const FlatPolygon<T>& fp, T maxD, const Cell<T>* seed);
+
template <class T>
struct Cell {
- Cell(const geometry::point<T>& c_, T h_, const geometry::polygon<T>& polygon)
- : c(c_),
+ Cell(T x_, T y_, T h_, const FlatPolygon<T>& fp, T maxD, const Cell<T>* seed)
+ : x(x_),
+ y(y_),
h(h_),
- d(pointToPolygonDist(c, polygon)),
- max(d + h * std::sqrt(2))
+ nsx1(0), nsy1(0), nsx2(0), nsy2(0),
+ d(pointToPolygonDist(*this, fp, maxD, seed)),
+ max(d + h * std::sqrt(static_cast<T>(2)))
{}
- geometry::point<T> c; // cell center
- T h; // half the cell size
- T d; // distance from cell center to polygon
+ T x; // cell center x
+ T y; // cell center y
+ T h; // half the cell size
+ // nsx1..nsy2 hold the nearest segment found below, so child cells can seed
+ // their scan with it (a child is almost always nearest to the same segment)
+ T nsx1, nsy1, nsx2, nsy2;
+ 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 pointToPolygonDist(Cell<T>& cell, const FlatPolygon<T>& fp, T maxD, const Cell<T>* seed) {
+ const T x = cell.x;
+ const T y = cell.y;
+ bool inside = false;
+ T minDistSq = std::numeric_limits<T>::infinity();
+ const T thresholdSq = maxD > 0 ? maxD * maxD : T(-1);
+
+ if (seed != nullptr) {
+ cell.nsx1 = seed->nsx1;
+ cell.nsy1 = seed->nsy1;
+ cell.nsx2 = seed->nsx2;
+ cell.nsy2 = seed->nsy2;
+ minDistSq = getSegDistSq(x, y, seed->nsx1, seed->nsy1, seed->nsx2, seed->nsy2);
+ if (minDistSq <= thresholdSq) return maxD;
+ }
+
+ const std::size_t stride = K * 2;
+ const std::size_t numRings = fp.ringEnds.size();
+ std::size_t g = 0; // running block index into bboxes
+ std::size_t ringStart = 0;
+
+ for (std::size_t r = 0; r < numRings; r++) {
+ const std::size_t ringEnd = fp.ringEnds[r];
+
+ // previous vertex (b), starting from the last point in the ring; carried
+ // across blocks so each block's first edge connects to the prior vertex
+ T bx = fp.coords[ringEnd - 2];
+ T by = fp.coords[ringEnd - 1];
+
+ for (std::size_t s = ringStart; s < ringEnd; s += stride, g += 4) {
+ std::size_t end = s + stride;
+ if (end > ringEnd) end = ringEnd;
+ const T bminX = fp.blocks[g], bminY = fp.blocks[g + 1],
+ bmaxX = fp.blocks[g + 2], bmaxY = fp.blocks[g + 3];
+
+ // lower bound on the distance from (x, y) to any edge in this block
+ const T dx = x < bminX ? bminX - x : x > bmaxX ? x - bmaxX : T(0);
+ const T dy = y < bminY ? bminY - y : y > bmaxY ? y - bmaxY : T(0);
+ const bool skipDist = dx * dx + dy * dy >= minDistSq;
+
+ // this block's edges can only flip ray-cast parity if its bbox
+ // straddles y and extends right of x; else no edge crosses the
+ // rightward ray
+ const bool skipCross = y < bminY || y >= bmaxY || x > bmaxX;
+
+ if (skipDist && skipCross) {
+ bx = fp.coords[end - 2];
+ by = fp.coords[end - 1];
+ continue;
+ }
+
+ for (std::size_t i = s; i < end; i += 2) {
+ const T ax = fp.coords[i];
+ const T ay = fp.coords[i + 1];
+
+ if (!skipCross && (ay > y) != (by > y) &&
+ (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside;
+
+ if (!skipDist) {
+ const T distSq = getSegDistSq(x, y, ax, ay, bx, by);
+ if (distSq < minDistSq) {
+ minDistSq = distSq;
+ cell.nsx1 = ax; cell.nsy1 = ay;
+ cell.nsx2 = bx; cell.nsy2 = by;
+
+ // the point is already close enough to the outline that
+ // this cell can't possibly contain a better label
+ // position — stop scanning
+ if (minDistSq <= thresholdSq) return maxD;
+ }
+ }
+
+ bx = ax;
+ by = ay;
+ }
+ }
+ ringStart = ringEnd;
+ }
+
+ return minDistSq == 0 ? T(0) : (inside ? T(1) : T(-1)) * std::sqrt(minDistSq);
+}
+
+// get polygon centroid (over the outer ring, coords[0..ringEnds[0]))
+template <class T>
+Cell<T> getCentroidCell(const FlatPolygon<T>& fp) {
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;
+ T x = 0;
+ T y = 0;
+ const std::size_t end = fp.ringEnds[0];
+
+ for (std::size_t i = 0, j = end - 2; i < end; j = i, i += 2) {
+ const T ax = fp.coords[i];
+ const T ay = fp.coords[i + 1];
+ const T bx = fp.coords[j];
+ const T by = fp.coords[j + 1];
+ const T f = ax * by - bx * ay;
+ x += (ax + bx) * f;
+ y += (ay + by) * f;
+ area += f * T(3);
}
- return Cell<T>(area == 0 ? ring.at(0) : c / area, 0, polygon);
+ const T negInf = -std::numeric_limits<T>::infinity();
+ if (area == 0) {
+ return Cell<T>(fp.coords[0], fp.coords[1], T(0), fp, negInf, nullptr);
+ }
+ Cell<T> centroid(x / area, y / area, T(0), fp, negInf, nullptr);
+ if (centroid.d < 0) {
+ return Cell<T>(fp.coords[0], fp.coords[1], T(0), fp, negInf, nullptr);
+ }
+ return centroid;
}
} // namespace detail
+// Find the pole of inaccessibility of `polygon` (the most distant internal
+// point from the polygon outline) within `precision`.
+//
+// If `distance` is non-null, it receives the signed distance from the returned
+// point to the polygon outline (positive inside, negative outside, 0 on the
+// boundary), matching the `.distance` property exposed by the v2.1.0 JS API.
template <class T>
-geometry::point<T> polylabel(const geometry::polygon<T>& polygon, T precision = 1, bool debug = false) {
+geometry::point<T> polylabel(const geometry::polygon<T>& polygon, T precision = 1, bool debug = false, T* distance = nullptr) {
using namespace detail;
// find the bounding box of the outer ring
const geometry::box<T> envelope = geometry::envelope(polygon.at(0));
+ const T minX = envelope.min.x;
+ const T minY = envelope.min.y;
+ const T maxX = envelope.max.x;
+ const T maxY = envelope.max.y;
+
+ const T width = maxX - minX;
+ const T height = maxY - minY;
+ const T cellSize = std::max(precision, std::min(width, height));
+
+ if (cellSize == precision) {
+ // degenerate / sub-precision polygon: no interior to search
+ if (distance) *distance = T(0);
+ return envelope.min;
+ }
- 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;
+ // flatten the polygon rings into a single contiguous coordinate buffer for
+ // cache-friendly, pointer-chase-free access in the hot distance loop
+ FlatPolygon<T> fp = flatten(polygon);
// a priority queue of cells in order of their "potential" (max distance to polygon)
- auto compareMax = [] (const Cell<T>& a, const Cell<T>& b) {
+ 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));
- }
- }
+ const T negInf = -std::numeric_limits<T>::infinity();
// take centroid as the first best guess
- auto bestCell = getCentroidCell(polygon);
+ Cell<T> bestCell = getCentroidCell(fp);
// second guess: bounding box centroid
- Cell<T> bboxCell(envelope.min + size / 2.0, 0, polygon);
- if (bboxCell.d > bestCell.d) {
- bestCell = bboxCell;
- }
+ Cell<T> bboxCell(minX + width / T(2), minY + height / T(2), T(0), fp, negInf, nullptr);
+ 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();
+ std::size_t numProbes = 2;
+
+ auto potentiallyQueue = [&](T cx, T cy, T h, const Cell<T>* seed) {
+ // a cell is only useful if it can beat the best (d > bestCell.d) or is
+ // worth subdividing (max = d + h·√2 > bestCell.d + precision). Both
+ // fail once d ≤ threshold, so the distance scan can bail there early.
+ const T threshold = bestCell.d - std::max(T(0), h * std::sqrt(static_cast<T>(2)) - precision);
+ Cell<T> cell(cx, cy, h, fp, threshold, seed);
+ numProbes++;
+ if (cell.max > bestCell.d + precision) cellQueue.push(cell);
// update the best cell if we found a better one
if (cell.d > bestCell.d) {
bestCell = cell;
- if (debug) std::cout << "found best " << ::round(1e4 * cell.d) / 1e4 << " after " << numProbes << " probes" << std::endl;
+ if (debug) std::cout << "found best " << std::round(1e4 * cell.d) / 1e4 << " after " << numProbes << " probes" << std::endl;
+ }
+ };
+
+ // cover polygon with initial cells
+ T h = cellSize / T(2);
+ for (T x = minX; x < maxX; x += cellSize) {
+ for (T y = minY; y < maxY; y += cellSize) {
+ potentiallyQueue(x + h, y + h, h, nullptr);
}
+ }
- // do not drill down further if there's no chance of a better solution
- if (cell.max - bestCell.d <= precision) continue;
+ while (!cellQueue.empty()) {
+ // pick the most promising cell from the queue
+ Cell<T> cell = cellQueue.top();
+ cellQueue.pop();
- // 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;
+ // do not drill down further if there's no chance of a better solution.
+ // because the queue is ordered by decreasing `max`, every subsequent
+ // cell is no better either, so we can stop entirely.
+ if (cell.max - bestCell.d <= precision) break;
+
+ // split the cell into four cells, seeding each with the parent's nearest segment
+ h = cell.h / T(2);
+ potentiallyQueue(cell.x - h, cell.y - h, h, &cell);
+ potentiallyQueue(cell.x + h, cell.y - h, h, &cell);
+ potentiallyQueue(cell.x - h, cell.y + h, h, &cell);
+ potentiallyQueue(cell.x + h, cell.y + h, h, &cell);
}
if (debug) {
@@ -172,7 +391,8 @@ geometry::point<T> polylabel(const geometry::polygon<T>& polygon, T precision =
std::cout << "best distance: " << bestCell.d << std::endl;
}
- return bestCell.c;
+ if (distance) *distance = bestCell.d;
+ return geometry::point<T>{bestCell.x, bestCell.y};
}
} // namespace mapbox
=====================================
package-lock.json
=====================================
@@ -1,33 +1,36 @@
{
"name": "polylabel",
- "version": "2.0.1",
+ "version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "polylabel",
- "version": "2.0.1",
+ "version": "2.1.0",
"license": "ISC",
"dependencies": {
"tinyqueue": "^3.0.0"
},
"devDependencies": {
- "eslint": "^9.6.0",
- "eslint-config-mourner": "^4.0.1"
+ "eslint": "^10.5.0",
+ "eslint-config-mourner": "^5.0.1"
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "eslint-visitor-keys": "^3.3.0"
+ "eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
@@ -46,9 +49,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz",
- "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -56,62 +59,127 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.17.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz",
- "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==",
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/object-schema": "^2.1.4",
+ "@eslint/object-schema": "^3.0.5",
"debug": "^4.3.1",
- "minimatch": "^3.1.2"
+ "minimatch": "^10.2.4"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
}
},
- "node_modules/@eslint/eslintrc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
- "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
"dev": true,
- "license": "MIT",
+ "license": "Apache-2.0",
"dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
+ "@eslint/core": "^1.2.1"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
},
- "funding": {
- "url": "https://opencollective.com/eslint"
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/js": {
- "version": "9.6.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz",
- "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
}
},
"node_modules/@eslint/object-schema": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
- "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
@@ -129,9 +197,9 @@
}
},
"node_modules/@humanwhocodes/retry": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
- "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -142,78 +210,69 @@
"url": "https://github.com/sponsors/nzakas"
}
},
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "node_modules/@stylistic/eslint-plugin": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
+ "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/types": "^8.56.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "estraverse": "^5.3.0",
+ "picomatch": "^4.0.3"
},
"engines": {
- "node": ">= 8"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^9.0.0 || ^10.0.0"
}
},
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
- "license": "MIT",
+ "license": "Apache-2.0",
"engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
- "engines": {
- "node": ">= 8"
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/@stylistic/eslint-plugin-js": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.3.0.tgz",
- "integrity": "sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==",
+ "node_modules/@stylistic/eslint-plugin/node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"dependencies": {
- "@types/eslint": "^8.56.10",
- "acorn": "^8.11.3",
- "eslint-visitor-keys": "^4.0.0",
- "espree": "^10.0.1"
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
- "peerDependencies": {
- "eslint": ">=8.40.0"
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/@types/eslint": {
- "version": "8.56.10",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",
- "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==",
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "*",
- "@types/json-schema": "*"
- }
+ "license": "MIT"
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
@@ -224,10 +283,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
+ "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/acorn": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
- "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -248,9 +321,9 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -264,115 +337,33 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "license": "Python-2.0"
- },
"node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "node": "18 || 20 || >=22"
}
},
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "color-name": "~1.1.4"
+ "balanced-match": "^4.0.2"
},
"engines": {
- "node": ">=7.0.0"
+ "node": "18 || 20 || >=22"
}
},
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -385,13 +376,13 @@
}
},
"node_modules/debug": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
- "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -423,29 +414,33 @@
}
},
"node_modules/eslint": {
- "version": "9.6.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz",
- "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
+ "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
"dev": true,
"license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
"dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
- "@eslint/config-array": "^0.17.0",
- "@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "9.6.0",
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.2",
+ "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.3.0",
- "@nodelib/fs.walk": "^1.2.8",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.2",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.0.1",
- "eslint-visitor-keys": "^4.0.0",
- "espree": "^10.1.0",
- "esquery": "^1.5.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^8.0.0",
@@ -454,89 +449,95 @@
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
- "is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.4.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
+ "minimatch": "^10.2.4",
"natural-compare": "^1.4.0",
- "optionator": "^0.9.3",
- "strip-ansi": "^6.0.1",
- "text-table": "^0.2.0"
+ "optionator": "^0.9.3"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
}
},
"node_modules/eslint-config-mourner": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/eslint-config-mourner/-/eslint-config-mourner-4.0.1.tgz",
- "integrity": "sha512-seylu4qdUc7kabx42zEedJiCyRDYqwsZLFPPnMRntmOFOQv6cKoM+FqhYpLKZ5dF7A7wW6mCympQSkg6RVxGlg==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-config-mourner/-/eslint-config-mourner-5.0.1.tgz",
+ "integrity": "sha512-+bJjgJanAP1Nk0HIO1aV7CJZLa7zl/ymaFSff+HeSLHr9ifuHc8nX9dNdrjO7GnGdrgRMrmrAq3TCqfwj6NHiQ==",
"dev": true,
"license": "ISC",
"dependencies": {
- "@eslint/js": "^9.5.0",
- "@stylistic/eslint-plugin-js": "^2.2.2"
+ "@eslint/js": "^10.0.1",
+ "@stylistic/eslint-plugin": "^5.10.0",
+ "globals": "^17.6.0"
}
},
"node_modules/eslint-scope": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz",
- "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==",
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
- "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
- "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "acorn": "^8.12.0",
+ "acorn": "^8.16.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.0.0"
+ "eslint-visitor-keys": "^5.0.1"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esquery": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
- "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -600,16 +601,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fastq": {
- "version": "1.17.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
- "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -655,9 +646,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
- "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -675,9 +666,9 @@
}
},
"node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "version": "17.7.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
+ "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -687,43 +678,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/ignore": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
- "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
- "node_modules/import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -757,16 +721,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-path-inside": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
- "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -774,19 +728,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -848,30 +789,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "brace-expansion": "^5.0.5"
},
"engines": {
- "node": "*"
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
@@ -932,19 +869,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -965,6 +889,19 @@
"node": ">=8"
}
},
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -985,72 +922,6 @@
"node": ">=6"
}
},
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1074,52 +945,6 @@
"node": ">=8"
}
},
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/tinyqueue": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
=====================================
package.json
=====================================
@@ -1,6 +1,6 @@
{
"name": "polylabel",
- "version": "2.0.1",
+ "version": "2.1.0",
"description": "A JS library for finding optimal label position inside a polygon",
"keywords": [
"polygon",
@@ -14,12 +14,13 @@
"tinyqueue": "^3.0.0"
},
"devDependencies": {
- "eslint": "^9.6.0",
- "eslint-config-mourner": "^4.0.1"
+ "eslint": "^10.5.0",
+ "eslint-config-mourner": "^5.0.1"
},
"scripts": {
"pretest": "eslint *.js test/*.js",
- "test": "node test/test.js"
+ "test": "node test/test.js",
+ "prepublishOnly": "npm test"
},
"files": [
"include",
@@ -29,6 +30,6 @@
"type": "git",
"url": "https://github.com/mapbox/polylabel.git"
},
- "author": "Vladimir Agafonkin",
+ "author": "Volodymyr Agafonkin",
"license": "ISC"
}
=====================================
polylabel.js
=====================================
@@ -1,6 +1,9 @@
import Queue from 'tinyqueue';
+// number of consecutive edges grouped under a single bounding box for block-skip
+const K = 32;
+
export default function polylabel(polygon, precision = 1.0, debug = false) {
// find the bounding box of the outer ring
let minX = Infinity;
@@ -25,20 +28,41 @@ export default function polylabel(polygon, precision = 1.0, debug = false) {
return result;
}
+ // flatten the polygon rings into a single contiguous coordinate buffer for
+ // cache-friendly, pointer-chase-free access in the hot distance loop
+ let numPoints = 0;
+ for (const ring of polygon) numPoints += ring.length;
+ const coords = new Float64Array(numPoints * 2);
+ const ringEnds = []; // end offset into coords for each ring (start = previous end, or 0)
+ let c = 0;
+ for (const ring of polygon) {
+ for (let i = 0; i < ring.length; i++) {
+ coords[c++] = ring[i][0];
+ coords[c++] = ring[i][1];
+ }
+ ringEnds.push(c);
+ }
+
+ const blocks = buildBlocks(coords, ringEnds);
+
// a priority queue of cells in order of their "potential" (max distance to polygon)
const cellQueue = new Queue([], (a, b) => b.max - a.max);
// take centroid as the first best guess
- let bestCell = getCentroidCell(polygon);
+ let bestCell = getCentroidCell(coords, ringEnds, blocks);
// second guess: bounding box centroid
- const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon);
+ const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, coords, ringEnds, blocks, -Infinity, null);
if (bboxCell.d > bestCell.d) bestCell = bboxCell;
let numProbes = 2;
- function potentiallyQueue(x, y, h) {
- const cell = new Cell(x, y, h, polygon);
+ function potentiallyQueue(x, y, h, seed) {
+ // a cell is only useful if it can beat the best (d > bestCell.d) or is
+ // worth subdividing (max = d + h·√2 > bestCell.d + precision). Both fail
+ // once d ≤ threshold, so the distance scan can bail there early.
+ const threshold = bestCell.d - Math.max(0, h * Math.SQRT2 - precision);
+ const cell = new Cell(x, y, h, coords, ringEnds, blocks, threshold, seed);
numProbes++;
if (cell.max > bestCell.d + precision) cellQueue.push(cell);
@@ -53,23 +77,23 @@ export default function polylabel(polygon, precision = 1.0, debug = false) {
let h = cellSize / 2;
for (let x = minX; x < maxX; x += cellSize) {
for (let y = minY; y < maxY; y += cellSize) {
- potentiallyQueue(x + h, y + h, h);
+ potentiallyQueue(x + h, y + h, h, null);
}
}
while (cellQueue.length) {
// pick the most promising cell from the queue
- const {max, x, y, h: ch} = cellQueue.pop();
+ const cell = cellQueue.pop();
// do not drill down further if there's no chance of a better solution
- if (max - bestCell.d <= precision) break;
-
- // split the cell into four cells
- h = ch / 2;
- potentiallyQueue(x - h, y - h, h);
- potentiallyQueue(x + h, y - h, h);
- potentiallyQueue(x - h, y + h, h);
- potentiallyQueue(x + h, y + h, h);
+ if (cell.max - bestCell.d <= precision) break;
+
+ // split the cell into four cells, seeding each with the parent's nearest segment
+ h = cell.h / 2;
+ potentiallyQueue(cell.x - h, cell.y - h, h, cell);
+ potentiallyQueue(cell.x + h, cell.y - h, h, cell);
+ potentiallyQueue(cell.x - h, cell.y + h, h, cell);
+ potentiallyQueue(cell.x + h, cell.y + h, h, cell);
}
if (debug) {
@@ -81,67 +105,170 @@ export default function polylabel(polygon, precision = 1.0, debug = false) {
return result;
}
-function Cell(x, y, h, polygon) {
+function Cell(x, y, h, coords, ringEnds, blocks, maxD, seed) {
this.x = x; // cell center x
this.y = y; // cell center y
this.h = h; // half the cell size
- this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon
- this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
+ // nsx1..nsy2 hold the nearest segment found below, so child cells can seed
+ // their scan with it (a child is almost always nearest to the same segment)
+ this.nsx1 = 0; this.nsy1 = 0; this.nsx2 = 0; this.nsy2 = 0;
+ this.d = pointToPolygonDist(this, coords, ringEnds, blocks, maxD, seed); // distance from cell center to polygon
+ this.max = this.d + h * Math.SQRT2; // max distance to polygon within a cell
}
-// signed distance from point to polygon outline (negative if point is outside)
-function pointToPolygonDist(x, y, polygon) {
+// signed distance from cell center to polygon outline (negative if outside),
+// also recording the nearest segment on the cell. maxD is a distance threshold:
+// if a partial result proves the center is no farther than maxD from the outline,
+// the scan bails out early and returns maxD, since the caller has already
+// determined such a cell can't beat the best. seed is the parent cell (or null);
+// its nearest segment is checked first so boundary cells reach the early-out
+// threshold without scanning the whole outline.
+function pointToPolygonDist(cell, coords, ringEnds, blocks, maxD, seed) {
+ const x = cell.x;
+ const y = cell.y;
let inside = false;
let minDistSq = Infinity;
+ const thresholdSq = maxD > 0 ? maxD * maxD : -1;
- for (const ring of polygon) {
- for (let i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
- const a = ring[i];
- const b = ring[j];
+ if (seed !== null) {
+ cell.nsx1 = seed.nsx1; cell.nsy1 = seed.nsy1; cell.nsx2 = seed.nsx2; cell.nsy2 = seed.nsy2;
+ minDistSq = getSegDistSq(x, y, seed.nsx1, seed.nsy1, seed.nsx2, seed.nsy2);
+ if (minDistSq <= thresholdSq) return maxD;
+ }
+
+ const stride = K * 2;
+ const numRings = ringEnds.length;
+ let g = 0; // running block index into bboxes
+ let ringStart = 0;
+
+ for (let r = 0; r < numRings; r++) {
+ const ringEnd = ringEnds[r];
+
+ // previous vertex (b), starting from the last point in the ring; carried
+ // across blocks so each block's first edge connects to the prior vertex
+ let bx = coords[ringEnd - 2];
+ let by = coords[ringEnd - 1];
+
+ for (let s = ringStart; s < ringEnd; s += stride, g += 4) {
+ let end = s + stride;
+ if (end > ringEnd) end = ringEnd;
+ const bminX = blocks[g], bminY = blocks[g + 1], bmaxX = blocks[g + 2], bmaxY = blocks[g + 3];
+
+ // lower bound on the distance from (x, y) to any edge in this block
+ const dx = x < bminX ? bminX - x : x > bmaxX ? x - bmaxX : 0;
+ const dy = y < bminY ? bminY - y : y > bmaxY ? y - bmaxY : 0;
+ const skipDist = dx * dx + dy * dy >= minDistSq;
- if ((a[1] > y !== b[1] > y) &&
- (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside;
+ // this block's edges can only flip ray-cast parity if its bbox straddles
+ // y and extends right of x; else no edge crosses the rightward ray
+ const skipCross = y < bminY || y >= bmaxY || x > bmaxX;
- minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b));
+ if (skipDist && skipCross) {
+ bx = coords[end - 2];
+ by = coords[end - 1];
+ continue;
+ }
+
+ for (let i = s; i < end; i += 2) {
+ const ax = coords[i];
+ const ay = coords[i + 1];
+
+ if (!skipCross && (ay > y !== by > y) &&
+ (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside;
+
+ if (!skipDist) {
+ const distSq = getSegDistSq(x, y, ax, ay, bx, by);
+ if (distSq < minDistSq) {
+ minDistSq = distSq;
+ cell.nsx1 = ax; cell.nsy1 = ay; cell.nsx2 = bx; cell.nsy2 = by;
+
+ // the point is already close enough to the outline that this cell
+ // can't possibly contain a better label position — stop scanning
+ if (minDistSq <= thresholdSq) return maxD;
+ }
+ }
+
+ bx = ax;
+ by = ay;
+ }
}
+ ringStart = ringEnd;
}
return minDistSq === 0 ? 0 : (inside ? 1 : -1) * Math.sqrt(minDistSq);
}
-// get polygon centroid
-function getCentroidCell(polygon) {
+// precompute one bounding box per block of K consecutive edges (over both
+// endpoints of every edge in it) so the distance scan can skip whole blocks in
+// O(1). The block layout mirrors the flattened coords/ringEnds and is re-derived
+// in the scan, so only the bboxes need storing: a flat [minX,minY,maxX,maxY] run
+// per block, sized upfront from the ring lengths.
+function buildBlocks(coords, ringEnds) {
+ const stride = K * 2;
+ let numBlocks = 0;
+ let ringStart = 0;
+ for (let r = 0; r < ringEnds.length; r++) {
+ numBlocks += Math.ceil((ringEnds[r] - ringStart) / stride);
+ ringStart = ringEnds[r];
+ }
+
+ const blocks = new Float64Array(numBlocks * 4);
+ let g = 0;
+ ringStart = 0;
+ for (let r = 0; r < ringEnds.length; r++) {
+ const ringEnd = ringEnds[r];
+ for (let s = ringStart; s < ringEnd; s += stride, g += 4) {
+ const end = s + stride < ringEnd ? s + stride : ringEnd;
+ const prev = s === ringStart ? ringEnd - 2 : s - 2;
+
+ let minX = coords[prev], minY = coords[prev + 1];
+ let maxX = minX, maxY = minY;
+ for (let i = s; i < end; i += 2) {
+ const px = coords[i], py = coords[i + 1];
+ if (px < minX) minX = px; else if (px > maxX) maxX = px;
+ if (py < minY) minY = py; else if (py > maxY) maxY = py;
+ }
+ blocks[g] = minX; blocks[g + 1] = minY; blocks[g + 2] = maxX; blocks[g + 3] = maxY;
+ }
+ ringStart = ringEnd;
+ }
+
+ return blocks;
+}
+
+// get polygon centroid (over the outer ring, coords[0..ringEnds[0]))
+function getCentroidCell(coords, ringEnds, blocks) {
let area = 0;
let x = 0;
let y = 0;
- const points = polygon[0];
-
- for (let i = 0, len = points.length, j = len - 1; i < len; j = i++) {
- const a = points[i];
- const b = points[j];
- const f = a[0] * b[1] - b[0] * a[1];
- x += (a[0] + b[0]) * f;
- y += (a[1] + b[1]) * f;
+ const end = ringEnds[0];
+
+ for (let i = 0, j = end - 2; i < end; j = i, i += 2) {
+ const ax = coords[i];
+ const ay = coords[i + 1];
+ const bx = coords[j];
+ const by = coords[j + 1];
+ const f = ax * by - bx * ay;
+ x += (ax + bx) * f;
+ y += (ay + by) * f;
area += f * 3;
}
- const centroid = new Cell(x / area, y / area, 0, polygon);
- if (area === 0 || centroid.d < 0) return new Cell(points[0][0], points[0][1], 0, polygon);
+ const centroid = new Cell(x / area, y / area, 0, coords, ringEnds, blocks, -Infinity, null);
+ if (area === 0 || centroid.d < 0) return new Cell(coords[0], coords[1], 0, coords, ringEnds, blocks, -Infinity, null);
return centroid;
}
// get squared distance from a point to a segment
-function getSegDistSq(px, py, a, b) {
- let x = a[0];
- let y = a[1];
- let dx = b[0] - x;
- let dy = b[1] - y;
+function getSegDistSq(px, py, x, y, bx, by) {
+ let dx = bx - x;
+ let dy = by - y;
if (dx !== 0 || dy !== 0) {
const t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
- x = b[0];
- y = b[1];
+ x = bx;
+ y = by;
} else if (t > 0) {
x += dx * t;
=====================================
test/test.cpp
=====================================
@@ -1,64 +1,106 @@
#include <mapbox/polylabel.hpp>
-#include <rapidjson/document.h>
-#include <rapidjson/writer.h>
-#include <rapidjson/stringbuffer.h>
-
#include <cassert>
+#include <cctype>
+#include <cstdlib>
#include <fstream>
#include <sstream>
-#include <iostream>
+#include <stdexcept>
+#include <string>
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>;
+class JsonFixtureParser {
+public:
+ explicit JsonFixtureParser(std::string input_) : input(std::move(input_)) {}
-geometry::polygon<double> fixture(const std::string& path) {
- std::ifstream t(path.c_str());
- std::stringstream buffer;
- buffer << t.rdbuf();
+ geometry::polygon<double> parsePolygon() {
+ geometry::polygon<double> polygon;
+ consume('[');
+ skipWhitespace();
+ while (!tryConsume(']')) {
+ polygon.push_back(parseRing());
+ skipWhitespace();
+ tryConsume(',');
+ }
+ return polygon;
+ }
- rapidjson_document d;
- d.Parse(buffer.str().c_str());
+private:
+ geometry::linear_ring<double> parseRing() {
+ geometry::linear_ring<double> ring;
+ consume('[');
+ skipWhitespace();
+ while (!tryConsume(']')) {
+ consume('[');
+ const double x = parseNumber();
+ consume(',');
+ const double y = parseNumber();
+ consume(']');
+ ring.push_back({x, y});
+ skipWhitespace();
+ tryConsume(',');
+ }
+ return ring;
+ }
- geometry::polygon<double> result;
+ double parseNumber() {
+ skipWhitespace();
+ const char* begin = input.c_str() + pos;
+ char* end = nullptr;
+ const double value = std::strtod(begin, &end);
+ if (end == begin) throw std::runtime_error("expected number");
+ pos += static_cast<std::size_t>(end - begin);
+ skipWhitespace();
+ return value;
+ }
- 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()
- });
+ void consume(char expected) {
+ skipWhitespace();
+ if (pos >= input.size() || input[pos] != expected) {
+ throw std::runtime_error(std::string("expected '") + expected + "'");
+ }
+ ++pos;
+ skipWhitespace();
+ }
+
+ bool tryConsume(char expected) {
+ skipWhitespace();
+ if (pos < input.size() && input[pos] == expected) {
+ ++pos;
+ skipWhitespace();
+ return true;
+ }
+ return false;
+ }
+
+ void skipWhitespace() {
+ while (pos < input.size() && std::isspace(static_cast<unsigned char>(input[pos]))) {
+ ++pos;
}
- result.push_back(std::move(ring));
}
- return result;
+ std::string input;
+ std::size_t pos = 0;
+};
+
+geometry::polygon<double> fixture(const std::string& path) {
+ std::ifstream file(path.c_str());
+ std::stringstream buffer;
+ buffer << file.rdbuf();
+ return JsonFixtureParser(buffer.str()).parsePolygon();
}
int main() {
- geometry::polygon<double> water1 = fixture("./test/fixtures/water1.json");
- geometry::polygon<double> water2 = fixture("./test/fixtures/water2.json");
+ const auto water1 = fixture("./test/fixtures/water1.json");
+ const auto 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;
+ 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));
}
=====================================
test/test.js
=====================================
@@ -1,10 +1,8 @@
import polylabel from '../polylabel.js';
import test from 'node:test';
import assert from 'node:assert/strict';
-import {readFileSync} from 'fs';
-
-const water1 = JSON.parse(readFileSync(new URL('fixtures/water1.json', import.meta.url)));
-const water2 = JSON.parse(readFileSync(new URL('fixtures/water2.json', import.meta.url)));
+import water1 from './fixtures/water1.json' with {type: 'json'};
+import water2 from './fixtures/water2.json' with {type: 'json'};
test('finds pole of inaccessibility for water1 and precision 1', () => {
const p = polylabel(water1, 1);
View it on GitLab: https://salsa.debian.org/debian-gis-team/mapbox-polylabel/-/compare/2373c8e59aff4e3f56bde7f763a692f958d48469...ea7d8ac5ef41853bdae0200e2da9b9d956cac378
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/mapbox-polylabel/-/compare/2373c8e59aff4e3f56bde7f763a692f958d48469...ea7d8ac5ef41853bdae0200e2da9b9d956cac378
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260703/18550e06/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list