Skip to content

Commit fff912e

Browse files
authored
repair corrupt (multi)polygons as far as possible. Fixes many clipping (#908)
1 parent 99c5f8b commit fff912e

5 files changed

Lines changed: 169 additions & 11 deletions

File tree

include/geom.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ void make_valid(GeometryT &geom) { }
7777

7878
void make_valid(MultiPolygon &mp);
7979

80+
// Attempt to repair an invalid areal geometry in place: dissolve-based
81+
// make_valid first (preserves area), then a zero-width buffer as a last
82+
// resort. Returns true if mp is valid afterwards; on failure mp is left as the
83+
// best-effort input so callers never regress.
84+
bool repair_multi_polygon(MultiPolygon &mp);
85+
8086
void union_many(std::vector<MultiPolygon> &mps);
8187

8288
Point intersect_edge(Point const &a, Point const &b, char edge, Box const &bbox);

src/geom.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#include <boost/geometry/geometries/segment.hpp>
55
#include <boost/geometry/index/rtree.hpp>
6+
#include <boost/geometry/algorithms/buffer.hpp>
7+
#include <boost/geometry/strategies/buffer.hpp>
68

79
#include "geometry/correct.hpp"
810

@@ -144,6 +146,83 @@ void make_valid(MultiPolygon &mp)
144146
mp = result;
145147
}
146148

149+
// Repair a single (possibly invalid) polygon in an area-preserving way and
150+
// append the resulting valid polygon(s) to `out`. Returns true on success.
151+
// `minArea` is the lower bound on the repaired area we are willing to accept.
152+
static bool repair_one_polygon(const Polygon &p, double minArea, MultiPolygon &out)
153+
{
154+
// 1) Dissolve (resolves self-intersections of this single polygon).
155+
try {
156+
MultiPolygon fixed;
157+
geometry::correct(p, fixed, 1E-12);
158+
if (geom::is_valid(fixed) && std::abs(geom::area(fixed)) >= minArea) {
159+
for (auto &fp : fixed) out.push_back(std::move(fp));
160+
return true;
161+
}
162+
} catch (const std::exception &) {
163+
// fall through to the buffer attempt
164+
}
165+
166+
// 2) Zero-width buffer as a last resort.
167+
try {
168+
MultiPolygon buffered;
169+
geom::strategy::buffer::distance_symmetric<double> distanceStrategy(0.0);
170+
geom::strategy::buffer::side_straight sideStrategy;
171+
geom::strategy::buffer::join_miter joinStrategy;
172+
geom::strategy::buffer::end_flat endStrategy;
173+
geom::strategy::buffer::point_square pointStrategy;
174+
175+
geom::buffer(p, buffered, distanceStrategy, sideStrategy, joinStrategy, endStrategy, pointStrategy);
176+
geom::correct(buffered);
177+
if (geom::is_valid(buffered) && std::abs(geom::area(buffered)) >= minArea) {
178+
for (auto &bp : buffered) out.push_back(std::move(bp));
179+
return true;
180+
}
181+
} catch (const std::exception &) {
182+
// keep best-effort polygon
183+
}
184+
185+
return false;
186+
}
187+
188+
bool repair_multi_polygon(MultiPolygon &mp)
189+
{
190+
if (geom::is_valid(mp)) return true;
191+
192+
// Repair PER POLYGON, area-preserving. Running make_valid/buffer on the whole
193+
// multipolygon can catastrophically COLLAPSE large/complex inputs: a clipped
194+
// reservoir with >1000 rings dropped ~99% of its area, leaving missing lake
195+
// tiles at low zoom. Conversely, simply keeping the whole invalid geometry
196+
// lets a single self-intersecting ring render as a spurious "spike".
197+
//
198+
// Fixing each polygon independently gets the best of both: a self-touching
199+
// ring is cleaned (no spike) while the rest stays intact, and we avoid the
200+
// O(n^2) cross-polygon union that caused the collapse. A polygon whose repair
201+
// would not preserve its area is kept as-is (invalid but complete renders;
202+
// only a tiny local artefact, never a dropped area).
203+
MultiPolygon out;
204+
bool allValid = true;
205+
for (const auto &p : mp) {
206+
if (geom::is_valid(p)) {
207+
out.push_back(p);
208+
continue;
209+
}
210+
// Lenient threshold: resolving a self-intersection legitimately changes a
211+
// single polygon's (shoelace) area, so anything down to half the original
212+
// is accepted. Per-polygon repair cannot trigger the cross-polygon union
213+
// that previously caused the catastrophic ~99% collapse, so this only
214+
// rejects a genuine local collapse.
215+
const double minArea = 0.5 * std::abs(geom::area(p));
216+
if (!repair_one_polygon(p, minArea, out)) {
217+
out.push_back(p);
218+
allValid = false;
219+
}
220+
}
221+
222+
mp = std::move(out);
223+
return allValid;
224+
}
225+
147226
// ---------------
148227
// Union multipolygons
149228
// from https://github.com/boostorg/geometry/discussions/947

src/tile_data.cpp

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <algorithm>
22
#include <iostream>
3+
#include <sstream>
34
#include "tile_data.h"
45
#include "coordinates_geom.h"
56
#include "leased_store.h"
@@ -8,6 +9,36 @@
89
using namespace std;
910
extern bool verbose;
1011

12+
// Human-readable name for a Boost.Geometry validity failure, used for diagnostics.
13+
static const char* validityFailureName(geom::validity_failure_type failure) {
14+
switch (failure) {
15+
case geom::no_failure: return "no_failure";
16+
case geom::failure_few_points: return "few_points";
17+
case geom::failure_wrong_topological_dimension: return "wrong_topological_dimension";
18+
case geom::failure_spikes: return "spikes";
19+
case geom::failure_duplicate_points: return "duplicate_points";
20+
case geom::failure_not_closed: return "not_closed";
21+
case geom::failure_self_intersections: return "self_intersections";
22+
case geom::failure_wrong_orientation: return "wrong_orientation";
23+
case geom::failure_interior_rings_outside: return "interior_rings_outside";
24+
case geom::failure_nested_interior_rings: return "nested_interior_rings";
25+
case geom::failure_disconnected_interior: return "disconnected_interior";
26+
case geom::failure_intersecting_interiors: return "intersecting_interiors";
27+
case geom::failure_wrong_corner_order: return "wrong_corner_order";
28+
case geom::failure_invalid_coordinate: return "invalid_coordinate";
29+
default: return "unknown";
30+
}
31+
}
32+
33+
// Thin wrapper around the shared repair_multi_polygon() that adds per-object
34+
// verbose diagnostics. See geom.cpp for the dissolve + zero-width buffer logic.
35+
static bool repairMultiPolygon(MultiPolygon &mp, NodeID objectID) {
36+
bool ok = repair_multi_polygon(mp);
37+
if (!ok && verbose)
38+
std::cerr << ("multipolygon repair failed for object " + std::to_string(objectID) + "\n");
39+
return ok;
40+
}
41+
1142
thread_local LeasedStore<TileDataSource::point_store_t> pointStore;
1243
thread_local LeasedStore<TileDataSource::linestring_store_t> linestringStore;
1344
thread_local LeasedStore<TileDataSource::multi_linestring_store_t> multilinestringStore;
@@ -332,21 +363,39 @@ Geometry TileDataSource::buildWayGeometry(OutputGeometryType const geomType,
332363
geom::validity_failure_type failure = geom::validity_failure_type::no_failure;
333364
bool valid = geom::is_valid(mp,failure);
334365
if (!valid) {
366+
if (verbose) {
367+
// Build the whole line first and emit it with a single stream write:
368+
// tilemaker runs multi-threaded and chained operator<< calls are not
369+
// atomic, so per-token writes interleave into unreadable output.
370+
std::ostringstream msg;
371+
msg << "invalid multipolygon for object " << objectID
372+
<< " at z" << bbox.zoom << " " << bbox.index.x << "/" << bbox.index.y
373+
<< ": " << validityFailureName(failure) << "\n";
374+
std::cerr << msg.str();
375+
}
335376
if (failure==geom::failure_spikes) {
336377
geom::remove_spikes(mp);
337378
failure = geom::validity_failure_type::no_failure;
338379
valid = geom::is_valid(mp,failure);
339380
}
340381
if (!valid && (failure==geom::failure_self_intersections || failure==geom::failure_intersecting_interiors)) {
382+
// fast_clip can introduce self-intersections; redo the clip with the
383+
// slower but robust Boost intersection against the original geometry.
341384
MultiPolygon output;
342385
geom::intersection(input, box, output);
343386
geom::correct(output);
344387

345-
// retry with Boost intersection if fast_clip has caused self-intersections
388+
// The intersection result can itself still be invalid for very complex
389+
// multipolygons (e.g. large reservoirs), which previously produced
390+
// dropped or holey tiles. Repair it before returning.
391+
repairMultiPolygon(output, objectID);
346392
multiPolygonClipCache.add(bbox, objectID, output);
347393
return output;
348394
} else if (!valid) {
349-
// occasionally also wrong_topological_dimension, disconnected_interior
395+
// occasionally also wrong_topological_dimension, disconnected_interior:
396+
// defects geom::correct cannot mend. Repair mp in place; on failure it
397+
// is left unchanged so behaviour never regresses.
398+
repairMultiPolygon(mp, objectID);
350399
}
351400
}
352401

src/tile_worker.cpp

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,31 @@ void writeMultiPolygon(
235235
geom::correct(current);
236236

237237
geom::validity_failure_type failure;
238-
if (verbose && !geom::is_valid(current, failure)) {
239-
cout << "output multipolygon has " << boost_validity_error(failure) << endl;
240-
241-
if (!geom::is_valid(mp, failure))
242-
cout << "input multipolygon has " << boost_validity_error(failure) << endl;
243-
else
244-
cout << "input multipolygon valid" << endl;
238+
if (!geom::is_valid(current, failure)) {
239+
if (verbose) {
240+
cout << "output multipolygon has " << boost_validity_error(failure) << endl;
241+
242+
if (!geom::is_valid(mp, failure))
243+
cout << "input multipolygon has " << boost_validity_error(failure) << endl;
244+
else
245+
cout << "input multipolygon valid" << endl;
246+
}
247+
248+
if (simplifyLevel > 0) {
249+
// Simplification can turn a valid input into a self-intersecting/spiky
250+
// one; such polygons are silently dropped by many renderers (missing
251+
// features). Repair (dissolve, then zero-width buffer) before writing.
252+
bool repaired = repair_multi_polygon(current);
253+
254+
if (geom::is_empty(current))
255+
return;
256+
257+
if (verbose && !repaired) {
258+
geom::validity_failure_type postFailure;
259+
if (!geom::is_valid(current, postFailure))
260+
cout << "output multipolygon STILL invalid after repair: " << boost_validity_error(postFailure) << endl;
261+
}
262+
}
245263
}
246264

247265
vtzero::polygon_feature_builder fbuilder{vtLayer};

src/visvalingam.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,17 @@ Polygon simplifyVis(const Polygon &p, double max_distance) {
255255
}
256256
return output;
257257
}
258-
MultiPolygon simplifyVis(const MultiPolygon &mp, double max_distance) {
258+
MultiPolygon simplifyVis(const MultiPolygon &mp, double max_distance) {
259259
MultiPolygon output;
260260
for (const auto &p : mp) {
261261
output.emplace_back(simplifyVis(p, max_distance));
262262
}
263-
make_valid(output);
263+
// Per-ring simplification can leave the multipolygon invalid. Use the
264+
// area-preserving repair instead of a bare make_valid: on large/complex
265+
// geometries an unconditional dissolve can collapse the polygon and drop
266+
// almost all of its area (which showed up as missing lake tiles at low
267+
// zoom). repair_multi_polygon keeps the simplified geometry untouched if a
268+
// repair would not preserve the covered area.
269+
repair_multi_polygon(output);
264270
return output;
265271
}

0 commit comments

Comments
 (0)