Skip to content

Commit 698051a

Browse files
committed
feat: [extensions] add dissolve alternative using buffer
1 parent 5cdfeb3 commit 698051a

File tree

1 file changed

+299
-0
lines changed

1 file changed

+299
-0
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Boost.Geometry
2+
//
3+
// Copyright (c) 2025 Barend Gehrels, Amsterdam, the Netherlands.
4+
5+
// Distributed under the Boost Software License, Version 1.0.
6+
// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7+
8+
// Official repository: https://github.com/boostorg/geometry
9+
// Documentation: http://www.boost.org/libs/geometry
10+
11+
#if defined(TEST_WITH_GEOJSON)
12+
#define BOOST_GEOMETRY_DEBUG_SEGMENT_IDENTIFIER
13+
#define BOOST_GEOMETRY_DEBUG_IDENTIFIER
14+
#endif
15+
16+
#include <geometry_test_common.hpp>
17+
18+
#include <boost/geometry/algorithms/buffer.hpp>
19+
#include <boost/geometry/algorithms/convert.hpp>
20+
21+
// To check results
22+
#include <boost/geometry/algorithms/correct_closure.hpp>
23+
#include <boost/geometry/algorithms/area.hpp>
24+
#include <boost/geometry/algorithms/is_valid.hpp>
25+
26+
#include <boost/geometry/geometries/geometries.hpp>
27+
#include <boost/geometry/geometries/point_xy.hpp>
28+
29+
#include <boost/geometry/strategies/strategies.hpp>
30+
31+
#include <boost/geometry/io/wkt/wkt.hpp>
32+
33+
#include "dissolve_overlay_cases.hpp"
34+
35+
#if defined(TEST_WITH_GEOJSON)
36+
#include <boost/geometry/extensions/gis/io/geojson/geojson_writer.hpp>
37+
#include "dissolve_geojson_visitor.hpp"
38+
#endif
39+
40+
// Equivalent with BOOST_CHECK_CLOSE
41+
// See also "expectation_limits.hpp" in the test directory
42+
template <typename Settings>
43+
bool is_equal_within_tolerance(Settings const& settings, double const value, double const expected)
44+
{
45+
double const fraction = settings.percentage / 100.0;
46+
double const lower_limit = expected * (1.0 - fraction);
47+
double const upper_limit = expected * (1.0 + fraction);
48+
return value >= lower_limit && value <= upper_limit;
49+
}
50+
51+
//! Unittest settings
52+
struct ut_settings
53+
{
54+
double buffer_distance{1.0e-5};
55+
double percentage{0.001};
56+
bool test_validity{true};
57+
};
58+
59+
template <typename Geometry>
60+
std::string as_wkt(Geometry const& geometry)
61+
{
62+
std::ostringstream out;
63+
out << bg::wkt(geometry);
64+
return out.str();
65+
}
66+
67+
template <typename GeometryOut, typename Geometry>
68+
void dissolve_alternative(GeometryOut& geometry_out, Geometry const& geometry,
69+
double buffer_distance)
70+
{
71+
using point_type = typename bg::point_type<Geometry>::type;
72+
const bg::strategy::buffer::distance_symmetric<double> distance_strategy(buffer_distance);
73+
const bg::strategy::buffer::side_straight side_strategy;
74+
const bg::strategy::buffer::join_miter join_strategy;
75+
const bg::strategy::buffer::end_flat end_strategy;
76+
const bg::strategy::buffer::point_circle point_strategy;
77+
78+
// Convert the input geometry to a multi linestring.
79+
bg::model::multi_linestring<bg::model::linestring<point_type>> geometry_lines;
80+
81+
bg::convert(geometry, geometry_lines);
82+
83+
bg::model::multi_polygon<bg::model::polygon<point_type>> buffered;
84+
bg::buffer(geometry_lines, buffered, distance_strategy,
85+
side_strategy, join_strategy, end_strategy, point_strategy);
86+
87+
// Clean all interior rings.
88+
for (auto& polygon : buffered)
89+
{
90+
bg::interior_rings(polygon).clear();
91+
}
92+
93+
// Buffer again but now with a negative distance to remove the buffer
94+
const bg::strategy::buffer::distance_symmetric<double> deflate(-buffer_distance);
95+
bg::buffer(buffered, geometry_out, deflate,
96+
side_strategy, join_strategy, end_strategy, point_strategy);
97+
}
98+
99+
template <typename GeometryOut, typename Geometry>
100+
void test_dissolve(std::string const& caseid, Geometry const& geometry,
101+
double expected_area, ut_settings const& settings)
102+
{
103+
#if defined(TEST_WITH_GEOJSON)
104+
std::ostringstream filename;
105+
// For QGis, it is usually convenient to always write to the same geojson file.
106+
filename << "/tmp/"
107+
// << caseid << "_"
108+
<< "dissolve.geojson";
109+
std::ofstream geojson_file(filename.str().c_str());
110+
111+
boost::geometry::geojson_writer writer(geojson_file);
112+
#endif
113+
114+
using coordinate_type = typename bg::coordinate_type<Geometry>::type;
115+
using multi_polygon = bg::model::multi_polygon<GeometryOut>;
116+
multi_polygon dissolved;
117+
118+
dissolve_alternative(dissolved, geometry, settings.buffer_distance);
119+
120+
#if defined(TEST_WITH_GEOJSON)
121+
geojson_visitor visitor(writer);
122+
#else
123+
bg::detail::overlay::overlay_null_visitor visitor;
124+
#endif
125+
126+
if (settings.test_validity)
127+
{
128+
std::string message;
129+
bool const valid = bg::is_valid(dissolved, message);
130+
BOOST_CHECK_MESSAGE(valid,
131+
"dissolve: " << caseid
132+
<< " geometry is not valid: " << message);
133+
}
134+
135+
auto const detected_area = bg::area(dissolved);
136+
137+
BOOST_CHECK_MESSAGE(is_equal_within_tolerance(settings, detected_area, expected_area),
138+
"dissolve: " << caseid
139+
<< " #area expected: " << expected_area
140+
<< " detected: " << detected_area
141+
);
142+
143+
#if defined(TEST_WITH_GEOJSON)
144+
writer.feature(geometry);
145+
writer.add_property("type", "input");
146+
147+
for (const auto& polygon : dissolved)
148+
{
149+
writer.feature(polygon);
150+
writer.add_property("type", "dissolved");
151+
}
152+
#endif
153+
154+
}
155+
156+
template <typename Geometry, typename GeometryOut>
157+
void test_one(std::string caseid, std::string const& wkt,
158+
double expected_area, ut_settings const& settings)
159+
{
160+
Geometry geometry;
161+
bg::read_wkt(wkt, geometry);
162+
163+
// If defined as closed, it should be closed. The algorithm itself
164+
// cannot close it without making a copy.
165+
bg::correct_closure(geometry);
166+
167+
test_dissolve<GeometryOut>(caseid, geometry,
168+
expected_area,
169+
settings);
170+
171+
// Verify if reversed version is identical
172+
bg::reverse(geometry);
173+
174+
caseid += "_rev";
175+
test_dissolve<GeometryOut>(caseid, geometry,
176+
expected_area,
177+
settings);
178+
}
179+
180+
#define TEST_DISSOLVE(caseid, area, clips_ignored, holes_ignored, points_ignored) { \
181+
ut_settings settings; \
182+
(test_one<polygon, polygon>) ( #caseid, caseid, area, settings); }
183+
184+
#define TEST_DISSOLVE_WITH(caseid, area, clips_ignored, holes_ignored, points_ignored, settings) { \
185+
(test_one<polygon, polygon>) ( #caseid, caseid, area, settings); }
186+
187+
#define TEST_DISSOLVE_IGNORE(caseid, area, clips_ignored, holes_ignored, points_ignored) { \
188+
ut_settings settings; settings.test_validity = false; \
189+
(test_one<polygon, polygon>) ( #caseid, caseid, area, settings); }
190+
191+
#define TEST_MULTI(caseid, area, clips_ignored, holes_ignored, points_ignored) { \
192+
ut_settings settings; \
193+
(test_one<multi_polygon, polygon>) ( #caseid, caseid, area, settings); }
194+
195+
template <typename P, bool Clockwise>
196+
void test_all()
197+
{
198+
typedef bg::model::polygon<P, Clockwise> polygon;
199+
typedef bg::model::multi_polygon<polygon> multi_polygon;
200+
201+
TEST_DISSOLVE(dissolve_1, 8.0, 1, 0, 4);
202+
203+
// Two (potential) holes are filtered out
204+
TEST_DISSOLVE(dissolve_2, 8.9296875, 1, 1, 12);
205+
TEST_DISSOLVE(dissolve_3, 4.0, 2, 0, 8);
206+
TEST_DISSOLVE(dissolve_4, 8.0, 2, 0, 8);
207+
TEST_DISSOLVE(dissolve_5, 12.0, 2, 0, 8);
208+
TEST_DISSOLVE(dissolve_6, 16.0, 1, 0, 5);
209+
210+
TEST_DISSOLVE(dissolve_7, 50.48056402439, 1, 0, 7);
211+
TEST_DISSOLVE(dissolve_8, 25.6158412, 1, 0, 11);
212+
213+
// CCW polygons should turn CW after dissolve
214+
TEST_DISSOLVE(dissolve_9, 25.6158412, 1, 0, 11);
215+
TEST_DISSOLVE(dissolve_10, 60.0, 1, 0, 7);
216+
TEST_DISSOLVE(dissolve_11, 60.0, 1, 0, 7);
217+
218+
// More pentagrams
219+
TEST_DISSOLVE(dissolve_12, 186556.84077318, 1, 0, 15);
220+
TEST_DISSOLVE(dissolve_13, 361733.91651, 1, 0, 15);
221+
222+
TEST_DISSOLVE(dissolve_14, 4.0, 3, 0, 13);
223+
TEST_DISSOLVE(dissolve_15, 4.0, 3, 0, 13);
224+
// Fixed by using buffer
225+
TEST_DISSOLVE(dissolve_16, 12.1333, 8, 0, 38);
226+
227+
TEST_DISSOLVE(dissolve_17, 14.5, 2, 0, 11);
228+
TEST_DISSOLVE(dissolve_18, 15.0, 3, 0, 15);
229+
230+
TEST_DISSOLVE(dissolve_d1, 8.0, 1, 0, 4);
231+
TEST_DISSOLVE(dissolve_d2, 16.0, 1, 0, 5);
232+
233+
TEST_DISSOLVE(dissolve_h1_a, 16.0, 1, 1, 9);
234+
TEST_DISSOLVE(dissolve_h1_b, 16.0, 1, 1, 9);
235+
TEST_DISSOLVE(dissolve_h2, 16.25, 2, 0, 13);
236+
TEST_DISSOLVE(dissolve_h3, 16.0, 1, 1, 14);
237+
TEST_DISSOLVE(dissolve_h4, 16.0, 1, 3, 17);
238+
239+
// The default distance results in a small artefact by buffer deflate.
240+
// It can be considered as invalid.
241+
// That should be solved within buffer itself, or in is_valid, or both.
242+
TEST_DISSOLVE_WITH(dissolve_star_a, 7.38821, 2, 0, 15,
243+
ut_settings{1.0e-6});
244+
TEST_DISSOLVE(dissolve_star_b, 7.28259, 2, 0, 15);
245+
TEST_DISSOLVE(dissolve_star_c, 7.399696, 1, 0, 11);
246+
247+
TEST_DISSOLVE(dissolve_mail_2017_09_24_a, 0.5, 2, 0, 8);
248+
249+
TEST_DISSOLVE(dissolve_mail_2017_09_24_b, 16.0, 1, 0, 5);
250+
TEST_DISSOLVE(dissolve_mail_2017_09_24_c, 0.5, 2, 0, 8);
251+
TEST_DISSOLVE(dissolve_mail_2017_09_24_d, 0.5, 1, 0, 4);
252+
TEST_DISSOLVE(dissolve_mail_2017_09_24_e, 0.001801138128, 5, 0, 69);
253+
TEST_DISSOLVE(dissolve_mail_2017_09_24_f, 0.000361308800, 5, 0, 69);
254+
TEST_DISSOLVE(dissolve_mail_2017_09_24_g, 0.5, 1, 0, 4);
255+
TEST_DISSOLVE(dissolve_mail_2017_09_24_h, 0.5, 1, 0, 4);
256+
257+
// dissolve created an interior ring which is now removed
258+
TEST_DISSOLVE(dissolve_mail_2017_10_26_a, 8.0, 1, 1, 12);
259+
TEST_DISSOLVE(dissolve_mail_2017_10_26_b, 16.0, 1, 0, 5);
260+
TEST_DISSOLVE(dissolve_mail_2017_10_26_c, 6.0, 1, 0, 6);
261+
262+
TEST_DISSOLVE(dissolve_mail_2017_10_30_a, 0.0001241171, 2, 0, 9);
263+
264+
TEST_DISSOLVE(dissolve_ticket10713, 0.157052766, 2, 0, 8);
265+
266+
// One interior ring removed
267+
TEST_MULTI(multi_three_triangles, 42.7807, 1, 1, 13);
268+
TEST_MULTI(multi_simplex_two, 14.7, 1, 0, 8);
269+
TEST_MULTI(multi_simplex_three, 16.7945, 1, 0, 14);
270+
TEST_MULTI(multi_simplex_four, 20.7581, 1, 0, 18);
271+
TEST_MULTI(multi_disjoint, 24.0, 4, 0, 16);
272+
TEST_MULTI(multi_new_interior, 19.9706, 1, 1, 18);
273+
TEST_MULTI(ggl_list_20110307_javier_01_a, 6400.0, 2, 0, 11);
274+
275+
// Four interior rings removed
276+
TEST_DISSOLVE(ggl_list_20110307_javier_01_b, 4000000.0, 1, 2, 16);
277+
278+
// One interior ring removed
279+
TEST_DISSOLVE(dissolve_ticket17, 0.00925269995, 1, 1, 228);
280+
281+
// Cases using large coordinate values need other buffer.
282+
TEST_DISSOLVE_WITH(dissolve_reallife, 91756.916526794434, 1, 0, 25,
283+
ut_settings{1.0});
284+
285+
TEST_DISSOLVE(gitter_2013_04_a, 3224.83441, 3, 0, 21);
286+
TEST_DISSOLVE(gitter_2013_04_b, 31210.429356259738, 1, 0, 11);
287+
288+
TEST_DISSOLVE(ggl_list_denis, 22544.24890, 2, 0, 22);
289+
290+
// Will now be one ring.
291+
TEST_DISSOLVE(dissolve_mail_2018_08_19, 30.711696, 2, 1, 15);
292+
}
293+
294+
295+
int test_main(int, char* [])
296+
{
297+
test_all<bg::model::d2::point_xy<double>, true >();
298+
return 0;
299+
}

0 commit comments

Comments
 (0)