Skip to content

Commit 42d8b8a

Browse files
authored
Simplify buildings algorithm (#918)
1 parent fe8fe57 commit 42d8b8a

10 files changed

Lines changed: 419 additions & 18 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ file(GLOB tilemaker_src_files
132132
src/shp_mem_tiles.cpp
133133
src/shp_processor.cpp
134134
src/significant_tags.cpp
135+
src/simplify_buildings.cpp
135136
src/sorted_node_store.cpp
136137
src/sorted_way_store.cpp
137138
src/tag_map.cpp

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ tilemaker: \
125125
src/shp_mem_tiles.o \
126126
src/shp_processor.o \
127127
src/significant_tags.o \
128+
src/simplify_buildings.o \
128129
src/sorted_node_store.o \
129130
src/sorted_way_store.o \
130131
src/tag_map.o \

docs/CONFIGURATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ You can add optional parameters to layers:
7777
* `simplify_level` - how much to simplify features (in degrees of longitude) on the zoom level `simplify_below-1`
7878
* `simplify_length` - how much to simplify features (in kilometers) on the zoom level `simplify_below-1`, preceding `simplify_level`
7979
* `simplify_ratio` - (optional: the default value is 2.0) the actual simplify level will be `simplify_level * pow(simplify_ratio, (simplify_below-1) - <current zoom>)`
80-
* `simplify_algorithm` - which simplification algorithm to use (defaults to Douglas-Peucker; you can also specify `"visvalingam"`, which can be better for landuse and similar polygons)
80+
* `simplify_algorithm` - which simplification algorithm to use (defaults to Douglas-Peucker; you can also specify `"visvalingam"`, which can be better for landuse and similar polygons, or `"buildings"`, which preserves rectilinear shapes)
8181
* `filter_below` - filter areas by minimum size below this zoom level
8282
* `filter_area` - minimum size (in square degrees of longitude) for the zoom level `filter_below-1`
8383
* `feature_limit` - restrict the number of features written to each tile

include/geom.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ typedef boost::geometry::model::polygon<Point> Polygon;
3434
typedef boost::geometry::model::multi_polygon<Polygon> MultiPolygon;
3535
typedef boost::geometry::model::multi_linestring<Linestring> MultiLinestring;
3636
typedef boost::geometry::model::box<Point> Box;
37+
typedef boost::geometry::model::segment<Point> Segment;
3738
typedef boost::geometry::ring_type<Polygon>::type Ring;
3839
typedef boost::geometry::interior_type<Polygon>::type InteriorRing;
3940
typedef boost::variant<Point,Linestring,MultiLinestring,MultiPolygon> Geometry;

include/shared_data.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ struct LayerDef {
4646

4747
static const uint DOUGLAS_PEUCKER = 0;
4848
static const uint VISVALINGAM = 1;
49+
static const uint BUILDINGS = 2;
4950
};
5051

5152
///\brief Defines layers used in map rendering

include/simplify_buildings.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*! \file */
2+
#ifndef _SIMPLIFY_BUILDINGS_H
3+
#define _SIMPLIFY_BUILDINGS_H
4+
5+
struct BuildingSimplifyConfig {
6+
// "Short" segment: the short side of the stub being removed (in coordinate units).
7+
// For lat/lon coordinates use e.g. 5.0/100000.0; scale accordingly for projected coords.
8+
double distance_filter = 5.0 / 100000.0;
9+
10+
// Area guard: don't remove stubs where d1*d2 (product of the two stub sides) exceeds this.
11+
// Prevents collapsing significant right-angle jogs. Set very large to disable.
12+
double area_filter = 50.0;
13+
14+
// Narrow-edge exception: bypass the area guard when either stub side is shorter than this.
15+
double area_narrow = 2.0;
16+
17+
// Minimum sine of the angle between the two outer lines (ring[km1]→ring[k] and
18+
// ring[kp2]→ring[kp3]). When these lines are nearly parallel their intersection is at
19+
// infinity; the fallback midpoint passes the length check via the triangle inequality
20+
// and destroys a significant corner. 0.1 ≈ sin(5.7°) — reject anything more parallel.
21+
static constexpr double PARALLEL_TOL = 0.1;
22+
23+
// Degrees tolerance for removing collinear points (straight-line removal).
24+
static constexpr double COLLINEAR_TOL = 8.0;
25+
26+
// Degrees tolerance for snapping the intersection to a right angle.
27+
static constexpr double SNAP_TOL = 3.0;
28+
29+
// Parametric tolerance for the proper-intersection test: values in [tol, 1-tol] count.
30+
// Keeps endpoint-touching segments from being treated as intersecting.
31+
static constexpr double INTERSECT_TOL = 0.001;
32+
};
33+
34+
void simplify_building(Polygon &poly, const BuildingSimplifyConfig &cfg);
35+
void simplify_building(MultiPolygon &mp, const BuildingSimplifyConfig &cfg);
36+
void simplifyBuildings(MultiPolygon &mp, double max_distance);
37+
38+
#endif //_SIMPLIFY_BUILDINGS_H

src/geom.cpp

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -225,26 +225,68 @@ bool repair_multi_polygon(MultiPolygon &mp)
225225

226226
// ---------------
227227
// Union multipolygons
228-
// from https://github.com/boostorg/geometry/discussions/947
228+
// Groups polygons into connected components by bbox intersection (via R-tree +
229+
// union-find), then runs the binary reduction only within each component.
230+
// Disjoint polygons are concatenated directly, skipping the expensive union_()
231+
// call that Boost still runs even for non-overlapping geometry.
229232
void union_many(std::vector<MultiPolygon> &to_unify) {
230-
if (to_unify.size()<2) return;
231-
size_t step = 1;
232-
size_t half_step;
233+
if (to_unify.size() < 2) return;
234+
235+
namespace bgi = boost::geometry::index;
236+
typedef std::pair<Box, size_t> BoxIdx;
237+
238+
std::vector<Box> boxes(to_unify.size());
239+
for (size_t i = 0; i < to_unify.size(); i++)
240+
boost::geometry::envelope(to_unify[i], boxes[i]);
241+
242+
// Union-find with path compression
243+
std::vector<size_t> parent(to_unify.size());
244+
for (size_t i = 0; i < to_unify.size(); i++) parent[i] = i;
245+
std::function<size_t(size_t)> find = [&](size_t x) -> size_t {
246+
return parent[x] == x ? x : (parent[x] = find(parent[x]));
247+
};
248+
249+
// Incrementally build an R-tree; unite each polygon with all prior ones
250+
// whose bboxes intersect (transitivity is handled by union-find)
251+
bgi::rtree<BoxIdx, bgi::quadratic<16>> rtree;
252+
for (size_t i = 0; i < to_unify.size(); i++) {
253+
for (auto const &v : rtree | bgi::adaptors::queried(bgi::intersects(boxes[i]))) {
254+
size_t ri = find(i), rj = find(v.second);
255+
if (ri != rj) parent[ri] = rj;
256+
}
257+
rtree.insert({boxes[i], i});
258+
}
233259

234-
// the outer loop doubles the distance between two polygons to be merged at every iteration
235-
do {
236-
half_step = step;
237-
step *= 2;
238-
size_t i = 0;
260+
// Bucket indices by component root (roots are in 0..n-1)
261+
std::vector<std::vector<size_t>> components(to_unify.size());
262+
for (size_t i = 0; i < to_unify.size(); i++)
263+
components[find(i)].push_back(i);
239264

240-
// the inner loop merges polygons at i and i+half_step storing the result at i
265+
// Union within each component; concatenate all results into to_unify[0]
266+
MultiPolygon result;
267+
for (auto &comp : components) {
268+
if (comp.empty()) continue;
269+
if (comp.size() == 1) {
270+
for (auto &p : to_unify[comp[0]]) result.push_back(std::move(p));
271+
continue;
272+
}
273+
std::vector<MultiPolygon> sub;
274+
sub.reserve(comp.size());
275+
for (size_t idx : comp) sub.push_back(std::move(to_unify[idx]));
276+
277+
size_t step = 1, half_step;
241278
do {
242-
MultiPolygon unified;
243-
boost::geometry::union_(to_unify.at(i), to_unify.at(i + half_step), unified);
244-
to_unify.at(i) = std::move(unified);
245-
i += step;
246-
} while (i + half_step < to_unify.size());
247-
} while (step < to_unify.size());
279+
half_step = step; step *= 2;
280+
for (size_t i = 0; i + half_step < sub.size(); i += step) {
281+
MultiPolygon unified;
282+
boost::geometry::union_(sub[i], sub[i + half_step], unified);
283+
sub[i] = std::move(unified);
284+
}
285+
} while (step < sub.size());
286+
287+
for (auto &p : sub[0]) result.push_back(std::move(p));
288+
}
289+
to_unify[0] = std::move(result);
248290
}
249291

250292
// ---------------

src/shared_data.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ void Config::readConfig(rapidjson::Document &jsonConfig, bool &hasClippingBox, B
324324
int combinePolyBelow=it->value.HasMember("combine_polygons_below") ? it->value["combine_polygons_below"].GetInt() : 0;
325325
bool sortZOrderAscending = it->value.HasMember("z_order_ascending") ? it->value["z_order_ascending"].GetBool() : (featureLimit==0);
326326
string algo = it->value.HasMember("simplify_algorithm") ? it->value["simplify_algorithm"].GetString() : "";
327-
uint simplifyAlgo = algo=="visvalingam" ? LayerDef::VISVALINGAM : LayerDef::DOUGLAS_PEUCKER;
327+
uint simplifyAlgo = algo=="visvalingam" ? LayerDef::VISVALINGAM :
328+
algo=="buildings" ? LayerDef::BUILDINGS : LayerDef::DOUGLAS_PEUCKER;
328329
string source = it->value.HasMember("source") ? it->value["source"].GetString() : "";
329330
vector<string> sourceColumns;
330331
bool allSourceColumns = false;

0 commit comments

Comments
 (0)