contrek 1.4.4 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2ca1b703822f8170679cfef42bdc5665fca4dcbeb49e0994efb78c1d325ff94
4
- data.tar.gz: b64cf8c4f9110bcbac96e01c86fb4b923a19d342eefd11e56933fdfdde1889fd
3
+ metadata.gz: a6ed7af7c2661396d55b6ddacd89dfc6c08c4c54744d1443d03a6ba533605ed4
4
+ data.tar.gz: 3308bedea2f5e89fe8031a65c404c250490542142101dba32fd09f8510cb8cfb
5
5
  SHA512:
6
- metadata.gz: 0e3e13330789b1d83025882e2179e54b2389092205ce9d0c5af8b7ba770118a1c585283f1367c15799c4ad3bb06ea1e97c6e15262c33ea229ef169fd343b03c1
7
- data.tar.gz: fd08b11d3a2604724960d0bbd2e24516e7136f39cacb304df970c5c96670800f8863b561a85f4c64ff14d0a0c24a5b3d45f7a93070161f7f94ac97148e414d6b
6
+ metadata.gz: 250b0ed1bce589beaae764e03226206853f9e9c3bb356fe3bf827cd25c69d72f33ad820f3afa5d9d31aa83e628e67dd8c4500782dcdb668f44b0e098932f6c4f
7
+ data.tar.gz: 7cf17a172afd9ec9b1788f2abe1779421d204886b99fd54e4729fbf9b616cc3968bb7586cafa5b17da6b5cbcf9641e3f210ee6d3d320e4be67e945022964af13
data/CHANGELOG.md CHANGED
@@ -167,3 +167,9 @@ All notable changes to this project will be documented in this file.
167
167
  - Fixed `VerticalMerger` handling of polygon orientation and starting points.
168
168
  - Added `detach()` to `RawBitmap` on the C++ side.
169
169
  - Added an OpenCV coordinate converter helper for converting contours to Contrek-compatible polygons.
170
+
171
+ ## [1.4.5] - 2026-10-11
172
+ - Added tile boundary validation for external geometries, with support for bypassing checks via `unsafe_mode: true`.
173
+
174
+ ## [1.4.6] - 2026-10-18
175
+ - Added tile metadata, polygon bounds, ordering, and shared boundary validation for Ruby and C++ mergers, with `unsafe_mode: true` to bypass validation.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.4.3)
4
+ contrek (1.4.6)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  ffi (~> 1.17)
data/README.md CHANGED
@@ -676,6 +676,64 @@ result = Contrek.contour!(
676
676
 
677
677
  The choice mainly affects how diagonally touching regions are interpreted.
678
678
 
679
+ ## Merging External Geometry
680
+
681
+ Contrek's merger can process polygon data produced by external contour
682
+ extractors, provided that the input geometry follows the requirements below.
683
+
684
+ ### The Laws of the Tiles
685
+
686
+ 1. **Adjacent tiles must share an overlap and have identical exposed boundary sequences.**
687
+ Adjacent tiles must share a single scanline of overlap. The exposed contour
688
+ segments on their shared boundary must have identical coordinate sequences,
689
+ including all intermediate vertices, expressed in the same coordinate system.
690
+ Any mismatch between the two sequences makes the tiles incompatible for merging.
691
+
692
+ 2. **Coordinates must describe cell boundaries.**
693
+ A cell is the square area occupied by a pixel in the raster. Coordinates
694
+ must describe the boundaries of these cells, rather than representing a
695
+ pixel by its center or by a single vertex.
696
+
697
+ 3. **Contours must follow cell boundaries.**
698
+ Contour segments supplied to the merger must follow the horizontal and
699
+ vertical edges of the raster cells. Diagonal segments are not allowed in
700
+ the geometry passed to the merger.
701
+
702
+ 4. **Polygon orientation (`versus`) must be consistent across all tiles.**
703
+ All polygons must follow the orientation declared by the result, either
704
+ clockwise or counterclockwise. The same orientation must be used by every
705
+ tile submitted to the merger.
706
+
707
+ 5. **Every contour must start at the canonical point of its first scanline.**
708
+ A *scanline* is a horizontal raster row scanned from left to right, like
709
+ the electron beam of a CRT display. Consider the topmost scanline
710
+ intersecting the contour. On that scanline, a **counterclockwise** contour
711
+ must start from the **leftmost** point, while a **clockwise** contour must
712
+ start from the **rightmost** point.
713
+
714
+ 6. **The bounding box must match the contour geometry.**
715
+ `min_x`, `max_x`, `min_y`, and `max_y` must correctly describe the spatial
716
+ extent of the contour.
717
+
718
+ 7. **Tile metadata must match the supplied geometry.**
719
+ `width`, `height`, and `versus` must correctly describe the tile. In
720
+ particular, `height` must represent the actual height of the result.
721
+
722
+ 8. **Polygons must be sorted in raster scan order.**
723
+ Polygons must be ordered by increasing **Y**, and then by increasing **X**
724
+ when they have the same Y: top to bottom, and left to right at the same
725
+ vertical position.
726
+
727
+ ### Boundary Validation
728
+
729
+ By default, Contrek validates the shared boundaries of adjacent tiles when merging externally supplied geometries. Boundary coordinates must match exactly, including intermediate vertices. Otherwise, the merger raises a `Tile boundary mismatch` error.
730
+
731
+ This validation can be disabled by setting `unsafe_mode: true`, avoiding validation overhead when the input geometries are known to be valid.
732
+
733
+ **Important:** Boundary validation does not verify all the Laws of the Tiles. Users supplying external geometries remain responsible for satisfying every requirement listed above.
734
+
735
+ **Important:** Boundary validation prioritizes correctness over performance and introduce additional processing overhead. Experienced users who can guarantee that their input geometries satisfy the Laws of the Tiles can enable `unsafe_mode: true` to bypass validation entirely and achieve maximum merging performance. This option can also be useful during debugging to isolate merging issues from validation errors.
736
+
679
737
  # Results
680
738
 
681
739
  The tracing result contains two kinds of information:
@@ -294,7 +294,9 @@ void Tests::test_h()
294
294
  PolygonFinder pl_right(&b_right, &matcher, nullptr, options);
295
295
  ProcessResult *right_result = pl_right.process_info();
296
296
 
297
- Options merger_options = {};
297
+ Options merger_options = {
298
+ {"unsafe_mode", true}
299
+ };
298
300
  HorizontalMerger hmerger(1, merger_options);
299
301
  hmerger.add_tile(*left_result);
300
302
  hmerger.add_tile(*right_result);
@@ -326,7 +328,9 @@ double get_peak_rss() {
326
328
 
327
329
  void stream_png_image(const std::string& filepath, uint32_t stripe_height, bool generate_svg = false, bool generate_png = false) {
328
330
  std::vector<ProcessResult*> result_clones;
329
- Options options = {};
331
+ Options options = {
332
+ {"unsafe_mode", true}
333
+ };
330
334
  VerticalMerger vmerger(0, options);
331
335
 
332
336
  PngSource source(filepath);
@@ -409,7 +413,7 @@ void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_h
409
413
 
410
414
  SvgStreamingMerger merger(
411
415
  0,
412
- {{"bounds", true}},
416
+ {{"bounds", true}, {"unsafe_mode", true}},
413
417
  &stream,
414
418
  source.width(),
415
419
  source.height());
@@ -68,7 +68,8 @@ struct pf_Options {
68
68
 
69
69
  struct ProcessResult {
70
70
  int groups;
71
- int width, height;
71
+ int width = 0;
72
+ int height = 0;
72
73
  int versus;
73
74
  int number_of_threads;
74
75
  bool has_bounds = false;
@@ -34,7 +34,7 @@ class GeoJsonStreamingMerger : public StreamingMerger {
34
34
  if (!transform || !crs) throw std::invalid_argument("Invalid geo_localization.");
35
35
 
36
36
  const std::string authority = crs->get<std::string>("authority", "");
37
- const auto code = crs->get("code", 0L);
37
+ const auto code = crs->get("code", std::int64_t{0});
38
38
  if (authority != "EPSG" || code != 4326) throw std::invalid_argument("Unsupported CRS: " + authority + ":" + std::to_string(code));
39
39
  const double x_origin = transform->get<double>("x_origin", 0.0);
40
40
  const double y_origin = transform->get<double>("y_origin", 0.0);
@@ -10,6 +10,9 @@
10
10
  #include <iostream>
11
11
  #include <string>
12
12
  #include <vector>
13
+ #include <algorithm>
14
+ #include <list>
15
+ #include <memory>
13
16
  #include "Merger.h"
14
17
 
15
18
  Merger::Merger(int number_of_threads, const Options& options)
@@ -17,7 +20,9 @@ Merger::Merger(int number_of_threads, const Options& options)
17
20
  if (options.contains("versus")) {
18
21
  std::cerr << "[Contrek WARNING] The given options 'versus' will be ignored." << std::endl;
19
22
  }
20
- if (!this->safe()) {
23
+ if (safe()) {
24
+ validator_ = std::make_unique<MergerValidator>(*this);
25
+ } else {
21
26
  std::cerr << "[Contrek WARNING] Processing tile with 'unsafe_mode: true'. "
22
27
  << "Incompatible result options might lead to unexpected vector geometry.\n";
23
28
  }
@@ -28,7 +33,10 @@ bool Merger::safe() {
28
33
  }
29
34
 
30
35
  void Merger::add_tile(ProcessResult& result)
31
- { if (this->height == 0) {
36
+ { if (validator_) {
37
+ validator_->validate_metadata(result);
38
+ }
39
+ if (this->height == 0) {
32
40
  this->height = result.height;
33
41
  } else {
34
42
  if (result.height != this->height && safe()) {
@@ -51,6 +59,9 @@ void Merger::add_tile(ProcessResult& result)
51
59
  }
52
60
 
53
61
  int end_x = this->current_x + result.width;
62
+ if (validator_) {
63
+ validator_->validate(result, this->current_x, end_x);
64
+ }
54
65
  Tile* tile = new Tile(this, this->current_x, end_x, std::to_string(tiles_.size()), tiles_.size(), Benchmarks {0, 0});
55
66
  tile->assign_raw_polygons(result.polygons, result.treemap);
56
67
  tiles_.queue_push(tile);
@@ -76,3 +87,4 @@ void Merger::translate(ProcessResult& result, int offset) {
76
87
  polygon.bounds.max_x += offset;
77
88
  }
78
89
  }
90
+
@@ -10,7 +10,10 @@
10
10
  #pragma once
11
11
  #include <string>
12
12
  #include <vector>
13
+ #include <list>
14
+ #include <memory>
13
15
  #include "Finder.h"
16
+ #include "MergerValidator.h"
14
17
  #include "../Options.h"
15
18
 
16
19
  class Merger : public Finder {
@@ -23,4 +26,5 @@ class Merger : public Finder {
23
26
  protected:
24
27
  void translate(ProcessResult& result, int offset);
25
28
  int current_x = 0;
29
+ std::unique_ptr<MergerValidator> validator_;
26
30
  };
@@ -0,0 +1,104 @@
1
+ /*
2
+ * MergerValidator.cpp
3
+ *
4
+ * Copyright (c) 2025-2026 Emanuele Cesaroni
5
+ *
6
+ * Licensed under the GNU Affero General Public License v3 (AGPLv3).
7
+ * See the LICENSE file in this directory for the full license text.
8
+ */
9
+
10
+ #include "MergerValidator.h"
11
+ #include "Merger.h"
12
+ #include <algorithm>
13
+ #include <stdexcept>
14
+ #include <vector>
15
+ #include <list>
16
+ #include <utility>
17
+
18
+ MergerValidator::MergerValidator(const Merger& merger)
19
+ : merger_(merger) {}
20
+
21
+ void MergerValidator::validate_metadata(const ProcessResult& result) const {
22
+ if (result.width <= 0) {
23
+ throw std::invalid_argument("Invalid result metadata: width must be a positive integer");
24
+ }
25
+ if (result.height <= 0) {
26
+ throw std::invalid_argument("Invalid result metadata: height must be a positive integer");
27
+ }
28
+ if (result.versus != Node::A && result.versus != Node::O) {
29
+ throw std::invalid_argument("Invalid result metadata: unsupported versus option");
30
+ }
31
+ }
32
+
33
+ void MergerValidator::validate(const ProcessResult& result, int start_x, int end_x) {
34
+ validate_bounds(result.polygons);
35
+ validate_polygon_order(result.polygons);
36
+ validate_boundary(result, start_x);
37
+
38
+ previous_boundary_ = boundary_coordinates(result.polygons, end_x);
39
+ has_previous_boundary_ = true;
40
+ }
41
+
42
+ void MergerValidator::validate_bounds(const std::list<Polygon>& polygons) const {
43
+ for (const auto& polygon : polygons) {
44
+ if (polygon.outer.empty()) {
45
+ throw std::invalid_argument("Invalid polygon: empty outer contour");
46
+ }
47
+ RectBounds actual = RectBounds::empty();
48
+
49
+ for (const auto& point : polygon.outer) {
50
+ actual.expand(point.x, point.y);
51
+ }
52
+
53
+ const auto& bounds = polygon.bounds;
54
+ if (bounds.min_x != actual.min_x ||
55
+ bounds.max_x != actual.max_x ||
56
+ bounds.min_y != actual.min_y ||
57
+ bounds.max_y != actual.max_y) {
58
+ throw std::invalid_argument("Invalid polygon bounds: bounding box does not match outer contour");
59
+ }
60
+ }
61
+ }
62
+
63
+ void MergerValidator::validate_polygon_order(const std::list<Polygon>& polygons) const {
64
+ bool has_previous = false;
65
+ std::pair<int, int> previous;
66
+
67
+ for (const auto& polygon : polygons) {
68
+ const auto& bounds = polygon.bounds;
69
+
70
+ const std::pair<int, int> current = merger_.transpose()
71
+ ? std::make_pair(bounds.min_x, bounds.min_y)
72
+ : std::make_pair(bounds.min_y, bounds.min_x);
73
+
74
+ if (has_previous && current < previous) {
75
+ throw std::invalid_argument("Invalid polygon order: polygons are not in raster scan order");
76
+ }
77
+ previous = current;
78
+ has_previous = true;
79
+ }
80
+ }
81
+
82
+ void MergerValidator::validate_boundary(const ProcessResult& result, int start_x) const {
83
+ if (!has_previous_boundary_) return;
84
+
85
+ const auto current = boundary_coordinates(result.polygons, start_x);
86
+ if (previous_boundary_ != current) {
87
+ throw std::invalid_argument("Tile boundary mismatch: adjacent tiles have different edge coordinates");
88
+ }
89
+ }
90
+
91
+ std::vector<int> MergerValidator::boundary_coordinates(const std::list<Polygon>& polygons, int boundary_x) {
92
+ std::vector<int> coordinates;
93
+
94
+ for (const auto& polygon : polygons) {
95
+ for (const auto& point : polygon.outer) {
96
+ if (point.x == boundary_x) {
97
+ coordinates.push_back(point.y);
98
+ }
99
+ }
100
+ }
101
+
102
+ std::sort(coordinates.begin(), coordinates.end());
103
+ return coordinates;
104
+ }
@@ -0,0 +1,35 @@
1
+ /*
2
+ * MergerValidator.h
3
+ *
4
+ * Copyright (c) 2025-2026 Emanuele Cesaroni
5
+ *
6
+ * Licensed under the GNU Affero General Public License v3 (AGPLv3).
7
+ * See the LICENSE file in this directory for the full license text.
8
+ */
9
+
10
+ #pragma once
11
+
12
+ #include <vector>
13
+ #include <list>
14
+ #include "../PolygonFinder.h"
15
+
16
+ class Merger;
17
+
18
+ class MergerValidator {
19
+ public:
20
+ explicit MergerValidator(const Merger& merger);
21
+
22
+ void validate_metadata(const ProcessResult& result) const;
23
+ void validate(const ProcessResult& result, int start_x, int end_x);
24
+
25
+ private:
26
+ const Merger& merger_;
27
+ std::vector<int> previous_boundary_;
28
+ bool has_previous_boundary_ = false;
29
+
30
+ void validate_bounds(const std::list<Polygon>& polygons) const;
31
+ void validate_polygon_order(const std::list<Polygon>& polygons) const;
32
+ void validate_boundary(const ProcessResult& result, int start_x) const;
33
+
34
+ static std::vector<int> boundary_coordinates(const std::list<Polygon>& polygons, int boundary_x);
35
+ };
@@ -60,27 +60,25 @@ void Tile::assign_raw_polygons(const std::list<Polygon>& raw_polylines, const st
60
60
  std::unordered_map<int, Shape*> shapes_map;
61
61
  int polyline_index = 0;
62
62
  for (const auto& raw_polyline : raw_polylines)
63
- { if (raw_polyline.bounds.width() > 0)
64
- { Polyline* polyline = this->shapes_pool->acquire_polyline(this, raw_polyline.outer, raw_polyline.bounds);
65
- std::vector<InnerPolyline*> inner_polylines_list;
66
- for (auto& raw_points : raw_polyline.inner) {
67
- inner_polylines_list.push_back(this->shapes_pool->acquire_inner_polyline(raw_points, nullptr));
68
- }
69
- Shape* shape = this->shapes_pool->acquire_shape(polyline, inner_polylines_list);
70
- polyline->shape = shape;
71
- this->shapes_.push_back(shape);
72
-
73
- if (treemap.size() > 0)
74
- { auto treemap_entry = treemap[polyline_index];
75
- shapes_map[polyline_index] = shape;
76
- if (treemap_entry.first != -1 && treemap_entry.second != -1) {
77
- auto it = shapes_map.find(treemap_entry.first);
78
- if (it != shapes_map.end()) {
79
- Shape* parent = it->second;
80
- shape->set_parent_shape(parent);
81
- shape->fixed = true;
82
- shape->parent_inner_polyline = parent->inner_polylines[treemap_entry.second];
83
- }
63
+ { Polyline* polyline = this->shapes_pool->acquire_polyline(this, raw_polyline.outer, raw_polyline.bounds);
64
+ std::vector<InnerPolyline*> inner_polylines_list;
65
+ for (auto& raw_points : raw_polyline.inner) {
66
+ inner_polylines_list.push_back(this->shapes_pool->acquire_inner_polyline(raw_points, nullptr));
67
+ }
68
+ Shape* shape = this->shapes_pool->acquire_shape(polyline, inner_polylines_list);
69
+ polyline->shape = shape;
70
+ this->shapes_.push_back(shape);
71
+
72
+ if (treemap.size() > 0)
73
+ { auto treemap_entry = treemap[polyline_index];
74
+ shapes_map[polyline_index] = shape;
75
+ if (treemap_entry.first != -1 && treemap_entry.second != -1) {
76
+ auto it = shapes_map.find(treemap_entry.first);
77
+ if (it != shapes_map.end()) {
78
+ Shape* parent = it->second;
79
+ shape->set_parent_shape(parent);
80
+ shape->fixed = true;
81
+ shape->parent_inner_polyline = parent->inner_polylines[treemap_entry.second];
84
82
  }
85
83
  }
86
84
  }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * OpencvConverter.cpp
3
+ *
4
+ * Copyright (c) 2025-2026 Emanuele Cesaroni
5
+ *
6
+ * Licensed under the GNU Affero General Public License v3 (AGPLv3).
7
+ * See the LICENSE file in this directory for the full license text.
8
+ */
9
+
10
+ #include "OpencvConverter.h"
11
+
12
+ #include <algorithm>
13
+ #include <functional>
14
+ #include <limits>
15
+ #include <utility>
16
+ #include <vector>
17
+
18
+ std::size_t OpencvConverter::EdgeKeyHash::operator()(const EdgeKey& key) const {
19
+ return std::hash<uint64_t>{}(key.a) ^
20
+ (std::hash<uint64_t>{}(key.b) << 1);
21
+ }
22
+
23
+ uint64_t OpencvConverter::point_key(const Point& p) {
24
+ return (static_cast<uint64_t>(static_cast<uint32_t>(p.x)) << 32) |
25
+ static_cast<uint32_t>(p.y);
26
+ }
27
+
28
+ OpencvConverter::EdgeKey OpencvConverter::edge_key(const Point& a, const Point& b) {
29
+ uint64_t ka = point_key(a);
30
+ uint64_t kb = point_key(b);
31
+ if (ka > kb) {
32
+ std::swap(ka, kb);
33
+ }
34
+ return {ka, kb};
35
+ }
36
+
37
+ void OpencvConverter::rotate_to_scanline_start(std::vector<Point>& points, const RectBounds& bounds) {
38
+ if (points.empty()) return;
39
+
40
+ std::size_t index = points.size();
41
+ int min_x = std::numeric_limits<int>::max();
42
+ for (std::size_t i = 0; i < points.size(); ++i) {
43
+ if (points[i].y == bounds.min_y && points[i].x < min_x) {
44
+ min_x = points[i].x;
45
+ index = i;
46
+ }
47
+ }
48
+ if (index != points.size() && index > 0) {
49
+ std::rotate(points.begin(), points.begin() + index, points.end());
50
+ }
51
+ }
@@ -0,0 +1,205 @@
1
+ /*
2
+ * OpencvConverter.h
3
+ *
4
+ * Copyright (c) 2025-2026 Emanuele Cesaroni
5
+ *
6
+ * Licensed under the GNU Affero General Public License v3 (AGPLv3).
7
+ * See the LICENSE file in this directory for the full license text.
8
+ */
9
+
10
+ #pragma once
11
+
12
+ #include <vector>
13
+ #include <unordered_map>
14
+ #include <unordered_set>
15
+ #include <cstdint>
16
+ #include <stdexcept>
17
+ #include <cstdlib>
18
+
19
+ #include "polygon/finder/Node.h"
20
+ #include "RectBounds.h"
21
+
22
+ class OpencvConverter {
23
+ private:
24
+ static constexpr int chain_dir[3][3] = {
25
+ {3, 4, 5},
26
+ {2, -1, 6},
27
+ {1, 0, 7}
28
+ };
29
+
30
+ static constexpr int corner_x[4] = {0, 1, 1, 0};
31
+ static constexpr int corner_y[4] = {1, 1, 0, 0};
32
+
33
+ struct BoundaryEdge {
34
+ Point a;
35
+ Point b;
36
+ };
37
+
38
+ struct EdgeKey {
39
+ uint64_t a;
40
+ uint64_t b;
41
+ bool operator==(const EdgeKey& other) const {
42
+ return a == other.a && b == other.b;
43
+ }
44
+ };
45
+
46
+ struct EdgeKeyHash {
47
+ std::size_t operator()(const EdgeKey& key) const;
48
+ };
49
+
50
+ static uint64_t point_key(const Point& p);
51
+ static EdgeKey edge_key(const Point& a, const Point& b);
52
+ static void rotate_to_scanline_start(std::vector<Point>& points, const RectBounds& bounds);
53
+
54
+ template<typename P>
55
+ static int chain_direction(const P& from, const P& to) {
56
+ const int dx = to.x - from.x;
57
+ const int dy = to.y - from.y;
58
+
59
+ if (dx == 0 && dy == 0)
60
+ throw std::invalid_argument("Zero-length contour step");
61
+
62
+ if (std::abs(dx) > 1 || std::abs(dy) > 1)
63
+ throw std::invalid_argument("Non-adjacent contour step");
64
+
65
+ return chain_dir[dx + 1][dy + 1];
66
+ }
67
+
68
+ public:
69
+ template<typename P>
70
+ static std::vector<Point> contour_to_cell_boundary(const std::vector<P>& contour, const RectBounds& bounds) {
71
+ std::vector<Point> raw;
72
+ const std::size_t n = contour.size();
73
+
74
+ if (n == 0) {
75
+ return raw;
76
+ }
77
+
78
+ if (n == 1) {
79
+ const int x = contour[0].x;
80
+ const int y = contour[0].y;
81
+ return {
82
+ Point(x, y + 1),
83
+ Point(x + 1, y + 1),
84
+ Point(x + 1, y),
85
+ Point(x, y)
86
+ };
87
+ }
88
+
89
+ raw.reserve(n * 2);
90
+
91
+ for (std::size_t i = 0; i < n; ++i) {
92
+ const P& previous = contour[(i + n - 1) % n];
93
+ const P& current = contour[i];
94
+ const P& next = contour[(i + 1) % n];
95
+ const int incoming = chain_direction(previous, current);
96
+ const int outgoing = chain_direction(current, next);
97
+ int corner = incoming >> 1;
98
+ const int last_corner = ((outgoing + 1) >> 1) & 3;
99
+ for (;;) {
100
+ raw.emplace_back(current.x + corner_x[corner], current.y + corner_y[corner]);
101
+ if (corner == last_corner) {
102
+ break;
103
+ }
104
+ corner = (corner + 1) & 3;
105
+ }
106
+ }
107
+
108
+ std::vector<BoundaryEdge> edges;
109
+ edges.reserve(raw.size());
110
+
111
+ for (std::size_t i = 0; i < raw.size(); ++i) {
112
+ const Point& a = raw[i];
113
+ const Point& b = raw[(i + 1) % raw.size()];
114
+ if (a.x == b.x && a.y == b.y) {
115
+ continue;
116
+ }
117
+ edges.push_back({a, b});
118
+ }
119
+
120
+ std::unordered_map<EdgeKey, int, EdgeKeyHash> counts;
121
+
122
+ for (const auto& edge : edges) {
123
+ ++counts[edge_key(edge.a, edge.b)];
124
+ }
125
+
126
+ std::unordered_set<EdgeKey, EdgeKeyHash> kept;
127
+ std::vector<BoundaryEdge> filtered;
128
+ filtered.reserve(edges.size());
129
+
130
+ for (const auto& edge : edges) {
131
+ const EdgeKey key = edge_key(edge.a, edge.b);
132
+ if (((counts[key] & 1) == 0) ||
133
+ (kept.find(key) != kept.end())) {
134
+ continue;
135
+ }
136
+ kept.insert(key);
137
+ filtered.push_back(edge);
138
+ }
139
+
140
+ if (filtered.empty()) {
141
+ return {};
142
+ }
143
+
144
+ std::unordered_map<uint64_t, std::vector<std::size_t>> outgoing;
145
+ for (std::size_t i = 0; i < filtered.size(); ++i) {
146
+ outgoing[point_key(filtered[i].a)].push_back(i);
147
+ }
148
+
149
+ std::vector<bool> used(filtered.size(), false);
150
+ std::size_t used_count = 0;
151
+ std::vector<Point> result;
152
+ result.reserve(filtered.size());
153
+
154
+ auto first_unused = [&]() {
155
+ for (std::size_t i = 0; i < used.size(); ++i) {
156
+ if (!used[i])
157
+ return i;
158
+ }
159
+ return used.size();
160
+ };
161
+
162
+ std::size_t current = 0;
163
+
164
+ while (used_count < filtered.size()) {
165
+ if (used[current]) {
166
+ current = first_unused();
167
+ if (current == used.size()) {
168
+ break;
169
+ }
170
+ }
171
+
172
+ const BoundaryEdge& edge = filtered[current];
173
+
174
+ used[current] = true;
175
+ ++used_count;
176
+
177
+ result.push_back(edge.a);
178
+
179
+ const uint64_t finish = point_key(edge.b);
180
+
181
+ auto it = outgoing.find(finish);
182
+ std::size_t next_edge = filtered.size();
183
+
184
+ if (it != outgoing.end()) {
185
+ for (std::size_t candidate : it->second) {
186
+ if (!used[candidate]) {
187
+ next_edge = candidate;
188
+ break;
189
+ }
190
+ }
191
+ }
192
+
193
+ if (next_edge != filtered.size()) {
194
+ current = next_edge;
195
+ } else {
196
+ current = first_unused();
197
+ if (current == used.size())
198
+ break;
199
+ }
200
+ }
201
+
202
+ rotate_to_scanline_start(result, bounds);
203
+ return result;
204
+ }
205
+ };
@@ -106,6 +106,8 @@
106
106
  #include "PolygonFinder/src/polygon/finder/concurrent/PartPool.cpp"
107
107
  #include "PolygonFinder/src/polygon/finder/concurrent/Merger.h"
108
108
  #include "PolygonFinder/src/polygon/finder/concurrent/Merger.cpp"
109
+ #include "PolygonFinder/src/polygon/finder/concurrent/MergerValidator.h"
110
+ #include "PolygonFinder/src/polygon/finder/concurrent/MergerValidator.cpp"
109
111
  #include "PolygonFinder/src/polygon/finder/concurrent/HorizontalMerger.h"
110
112
  #include "PolygonFinder/src/polygon/finder/concurrent/HorizontalMerger.cpp"
111
113
  #include "PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.h"
@@ -571,6 +573,7 @@ void Init_cpp_polygon_finder() {
571
573
  rb_cstd_ofstream.define_method("closed?", [](OfstreamWrapper& self) { return self.closed(); });
572
574
  rb_cstd_ofstream.define_method("rewind", [](OfstreamWrapper& self) { self.rewind(); });
573
575
  rb_cstd_ofstream.define_method("read", [](OfstreamWrapper& self) { return self.read(); });
576
+ rb_cstd_ofstream.define_method("path", [](OfstreamWrapper& self) { return self.path; });
574
577
 
575
578
  Data_Type<StreamingMerger> rb_cstreaming_merger = define_class<StreamingMerger, VerticalMerger>("CPPStreamingMerger");
576
579
  rb_cstreaming_merger.define_method("add_tile", [](StreamingMerger& self, Object rb_result, bool flush) {
@@ -18,23 +18,28 @@ module Contrek
18
18
  warn "[Contrek WARNING] The given options '#{warn_options.join(",")}' will be ignored."
19
19
  end
20
20
  @options = {unsafe_mode: false}.merge(@user_options).except(*IGNORE_OPTIONS)
21
- unless safe?
21
+ if safe?
22
+ @validator = MergerValidator.new(self)
23
+ else
22
24
  warn "[Contrek WARNING] Processing tile with 'unsafe_mode: true'. Incompatible result options might lead to unexpected vector geometry."
23
25
  end
24
26
  end
25
27
 
26
28
  def add_tile(result)
29
+ @validator&.validate_metadata(result)
30
+
27
31
  @height ||= result.metadata[:height]
28
32
  raise ArgumentError, "All results must have the same height" if @height != result.metadata[:height] && safe?
29
33
  @options[:versus] ||= result.metadata[:versus]
30
34
  raise ArgumentError, "All results must have the same versus option" if @options[:versus] != result.metadata[:versus] && safe?
31
- if safe? && result.metadata[:options].key?(:compress)
35
+ if safe? && result.metadata[:options]&.key?(:compress)
32
36
  if (result.metadata[:options][:compress].keys - [:uniq, :linear]).any?
33
37
  raise ArgumentError, "Result with not supported postprocessing compression mode"
34
38
  end
35
39
  end
36
40
 
37
41
  end_x = @current_x + result.metadata[:width]
42
+ @validator&.validate(result, @current_x, end_x)
38
43
 
39
44
  tile = Tile.new(
40
45
  finder: self,
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Contrek
4
+ module Concurrent
5
+ class MergerValidator
6
+ def initialize(merger)
7
+ @merger = merger
8
+ end
9
+
10
+ def validate(result, start_x, end_x)
11
+ validate_bounds(result.polygons)
12
+ validate_polygon_order(result.polygons)
13
+ validate_boundary(result, start_x)
14
+
15
+ @previous_boundary = boundary_coordinates(result.polygons, end_x)
16
+ end
17
+
18
+ def validate_metadata(result)
19
+ metadata = result.metadata
20
+ [:width, :height, :versus].each do |key|
21
+ raise ArgumentError, "Missing result metadata: #{key}" unless metadata.key?(key)
22
+ end
23
+ [:width, :height].each do |key|
24
+ value = metadata[key]
25
+ unless value.is_a?(Integer) && value.positive?
26
+ raise ArgumentError, "Invalid result metadata: #{key} must be a positive integer"
27
+ end
28
+ end
29
+ unless [:a, :o].include?(metadata[:versus])
30
+ raise ArgumentError, "Invalid result metadata: unsupported versus option"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def validate_polygon_order(polygons)
37
+ previous = nil
38
+ polygons.each do |polygon|
39
+ bounds = polygon[:bounds]
40
+ current = if @merger.transpose?
41
+ [bounds[:min_x], bounds[:min_y]]
42
+ else
43
+ [bounds[:min_y], bounds[:min_x]]
44
+ end
45
+ if previous && (current <=> previous) < 0
46
+ raise ArgumentError, "Invalid polygon order: polygons are not in raster scan order"
47
+ end
48
+ previous = current
49
+ end
50
+ end
51
+
52
+ def validate_boundary(result, start_x)
53
+ return unless @previous_boundary
54
+
55
+ current = boundary_coordinates(result.polygons, start_x)
56
+ unless @previous_boundary == current
57
+ raise ArgumentError, "Tile boundary mismatch: adjacent tiles have different edge coordinates"
58
+ end
59
+ end
60
+
61
+ def boundary_coordinates(polygons, boundary_x)
62
+ coordinates = []
63
+ polygons.each do |polygon|
64
+ polygon[:outer].each do |point|
65
+ coordinates << point[:y] if point[:x] == boundary_x
66
+ end
67
+ end
68
+ coordinates.sort!
69
+ end
70
+
71
+ def validate_bounds(polygons)
72
+ polygons.each do |polygon|
73
+ outer = polygon[:outer]
74
+ raise ArgumentError, "Invalid polygon: empty outer contour" if outer.empty?
75
+ bounds = polygon[:bounds]
76
+ min_x = max_x = outer[0][:x]
77
+ min_y = max_y = outer[0][:y]
78
+ outer.each do |point|
79
+ x = point[:x]
80
+ y = point[:y]
81
+ min_x = x if x < min_x
82
+ max_x = x if x > max_x
83
+ min_y = y if y < min_y
84
+ max_y = y if y > max_y
85
+ end
86
+ unless bounds[:min_x] == min_x &&
87
+ bounds[:max_x] == max_x &&
88
+ bounds[:min_y] == min_y &&
89
+ bounds[:max_y] == max_y
90
+ raise ArgumentError, "Invalid polygon bounds: bounding box does not match outer contour"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -92,7 +92,6 @@ module Contrek
92
92
  @shapes = []
93
93
  shapes_map = {}
94
94
  raw_polylines.each_with_index do |raw_polyline, polyline_index|
95
- next if raw_polyline[:bounds][:max_x] - raw_polyline[:bounds][:min_x] == 0
96
95
  shape = Shape.new.tap do |shape|
97
96
  shape.outer_polyline = Polyline.new(tile: self, polygon: raw_polyline[:outer], shape: shape, bounds: raw_polyline[:bounds])
98
97
  shape.inner_polylines = raw_polyline[:inner].map { |raw| InnerPolyline.new(shape: shape, raw_coordinates: raw) }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Contrek
2
4
  module Shared
3
5
  class OpencvConverter
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Contrek
4
- VERSION = "1.4.4"
4
+ VERSION = "1.4.6"
5
5
  end
data/lib/contrek.rb CHANGED
@@ -45,13 +45,13 @@ require "contrek/finder/concurrent/polyline"
45
45
  require "contrek/finder/concurrent/cluster"
46
46
  require "contrek/finder/concurrent/finder"
47
47
  require "contrek/finder/concurrent/merger"
48
+ require "contrek/finder/concurrent/merger_validator"
48
49
  require "contrek/finder/concurrent/horizontal_merger"
49
50
  require "contrek/finder/concurrent/vertical_merger"
50
51
  require "contrek/finder/concurrent/streaming_merger"
51
52
  require "contrek/finder/concurrent/svg_streaming_merger"
52
53
  require "contrek/finder/concurrent/geo_json_streaming_merger"
53
54
  require "contrek/finder/concurrent/cursor"
54
- require "contrek/map/mercator_projection"
55
55
  require "contrek/matchers/matcher"
56
56
  require "contrek/matchers/matcher_hsb"
57
57
  require "contrek/matchers/value_not_matcher"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrek
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.4
4
+ version: 1.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni
@@ -215,6 +215,8 @@ files:
215
215
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/InnerPolyline.h
216
216
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.cpp
217
217
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.h
218
+ - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/MergerValidator.cpp
219
+ - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/MergerValidator.h
218
220
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Part.cpp
219
221
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Part.h
220
222
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/PartPool.cpp
@@ -263,6 +265,8 @@ files:
263
265
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/reducers/UniqReducer.h
264
266
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/reducers/VisvalingamReducer.cpp
265
267
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/reducers/VisvalingamReducer.h
268
+ - ext/cpp_polygon_finder/PolygonFinder/src/polygon/shared/OpencvConverter.cpp
269
+ - ext/cpp_polygon_finder/PolygonFinder/src/polygon/shared/OpencvConverter.h
266
270
  - ext/cpp_polygon_finder/cpp_polygon_finder.cpp
267
271
  - ext/cpp_polygon_finder/extconf.rb
268
272
  - lib/LICENSE-MIT.md
@@ -303,6 +307,7 @@ files:
303
307
  - lib/contrek/finder/concurrent/inner_polyline.rb
304
308
  - lib/contrek/finder/concurrent/listable.rb
305
309
  - lib/contrek/finder/concurrent/merger.rb
310
+ - lib/contrek/finder/concurrent/merger_validator.rb
306
311
  - lib/contrek/finder/concurrent/part.rb
307
312
  - lib/contrek/finder/concurrent/partitionable.rb
308
313
  - lib/contrek/finder/concurrent/polyline.rb
@@ -323,7 +328,6 @@ files:
323
328
  - lib/contrek/finder/node_cluster.rb
324
329
  - lib/contrek/finder/polygon_finder.rb
325
330
  - lib/contrek/finder/result.rb
326
- - lib/contrek/map/mercator_projection.rb
327
331
  - lib/contrek/matchers/matcher.rb
328
332
  - lib/contrek/matchers/matcher_hsb.rb
329
333
  - lib/contrek/matchers/value_not_matcher.rb
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Map
4
- class MercatorProjection
5
- MERCATOR_RANGE = 256
6
-
7
- class LatLng
8
- attr_reader :lat, :lng
9
- def initialize(lat:, lng:)
10
- @lat = lat
11
- @lng = lng
12
- end
13
- end
14
-
15
- class Point
16
- attr_accessor :x, :y
17
- def initialize(x: 0, y: 0)
18
- @x = x
19
- @y = y
20
- end
21
- end
22
-
23
- def initialize
24
- @pixel_origin = Point.new(x: MERCATOR_RANGE / 2, y: MERCATOR_RANGE / 2)
25
- @pixels_per_lon_degree = MERCATOR_RANGE.to_f / 360
26
- @pixels_per_lon_radian = MERCATOR_RANGE.to_f / (2 * Math::PI)
27
- end
28
-
29
- def self.get_corners(center, zoom, map_width, map_height)
30
- scale = 2**zoom
31
- proj = Map::MercatorProjection.new
32
- center_px = proj.from_lat_lng_to_point(lat_lng: center)
33
- sw_point = Point.new(x: center_px.x - ((map_width / 2).to_f / scale), y: center_px.y + ((map_height / 2).to_f / scale))
34
- sw_lat_lon = proj.from_point_to_lat_lng(sw_point)
35
- ne_point = Point.new(x: center_px.x + ((map_width / 2).to_f / scale), y: center_px.y - ((map_height / 2).to_f / scale))
36
- ne_lat_lon = proj.from_point_to_lat_lng(ne_point)
37
- {
38
- N: ne_lat_lon.lat,
39
- E: ne_lat_lon.lng,
40
- S: sw_lat_lon.lat,
41
- W: sw_lat_lon.lng
42
- }
43
- end
44
-
45
- def from_lat_lng_to_point(lat_lng:, opt_point: nil)
46
- point = opt_point.nil? ? Point.new(x: 0, y: 0) : opt_point
47
- origin = @pixel_origin
48
- point.x = origin.x + (lat_lng.lng * @pixels_per_lon_degree)
49
- siny = bound(Math.sin(degreesToRadians(lat_lng.lat)), -0.9999, 0.9999)
50
- point.y = origin.y + (0.5 * Math.log((1 + siny) / (1 - siny)) * -@pixels_per_lon_radian)
51
- point
52
- end
53
-
54
- def from_point_to_lat_lng(point)
55
- origin = @pixel_origin
56
- lng = (point.x - origin.x) / @pixels_per_lon_degree
57
- lat_radians = (point.y - origin.y) / -@pixels_per_lon_radian
58
- lat = radiansToDegrees(2 * Math.atan(Math.exp(lat_radians)) - (Math::PI / 2))
59
- LatLng.new(lat: lat, lng: lng)
60
- end
61
-
62
- private
63
-
64
- def degreesToRadians(deg)
65
- deg * (Math::PI / 180)
66
- end
67
-
68
- def radiansToDegrees(rad)
69
- rad / (Math::PI / 180)
70
- end
71
-
72
- def bound(value, opt_min, opt_max)
73
- value = [value, opt_min].max if !opt_min.nil?
74
- value = [value, opt_max].min if !opt_max.nil?
75
- value
76
- end
77
- end
78
- end