contrek 1.4.5 → 1.4.7

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +28 -9
  5. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +1 -1
  6. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/FinderUtils.cpp +1 -0
  7. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.cpp +23 -26
  8. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.h +2 -2
  9. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.cpp +7 -4
  10. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.h +1 -1
  11. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Polygon.h +1 -0
  12. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.cpp +24 -11
  13. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h +68 -1
  14. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.cpp +1 -1
  15. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.cpp +8 -2
  16. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Finder.cpp +3 -0
  17. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/GeoJsonStreamingMerger.h +3 -5
  18. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.cpp +11 -30
  19. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.h +3 -3
  20. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/MergerValidator.cpp +104 -0
  21. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/MergerValidator.h +35 -0
  22. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.cpp +3 -2
  23. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.h +3 -1
  24. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Shape.cpp +1 -0
  25. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.cpp +2 -2
  26. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.h +1 -1
  27. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Tile.cpp +19 -21
  28. data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +11 -3
  29. data/lib/contrek/cpp/cpp_geo_json_concurrent_streaming_merger.rb +2 -2
  30. data/lib/contrek/cpp/cpp_result.rb +2 -1
  31. data/lib/contrek/finder/concurrent/cluster.rb +1 -1
  32. data/lib/contrek/finder/concurrent/cursor.rb +3 -1
  33. data/lib/contrek/finder/concurrent/finder.rb +2 -1
  34. data/lib/contrek/finder/concurrent/geo_json_streaming_merger.rb +3 -4
  35. data/lib/contrek/finder/concurrent/merger.rb +6 -25
  36. data/lib/contrek/finder/concurrent/merger_validator.rb +96 -0
  37. data/lib/contrek/finder/concurrent/polyline.rb +3 -2
  38. data/lib/contrek/finder/concurrent/shape.rb +2 -1
  39. data/lib/contrek/finder/concurrent/streaming_merger.rb +1 -1
  40. data/lib/contrek/finder/concurrent/tile.rb +9 -3
  41. data/lib/contrek/finder/node.rb +13 -23
  42. data/lib/contrek/finder/node_cluster.rb +17 -5
  43. data/lib/contrek/finder/polygon_finder.rb +29 -1
  44. data/lib/contrek/version.rb +1 -1
  45. data/lib/contrek.rb +1 -0
  46. metadata +4 -1
@@ -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
+ };
@@ -17,10 +17,11 @@
17
17
  #include "Tile.h"
18
18
  #include "Shape.h"
19
19
 
20
- Polyline::Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds)
20
+ Polyline::Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds, unsigned int class_id)
21
21
  : tile(tile),
22
22
  shape_pool(shape_pool),
23
- raw_(std::move(polygon))
23
+ raw_(std::move(polygon)),
24
+ class_id_(class_id)
24
25
  { if (bounds.has_value()) {
25
26
  min_x = bounds->min_x;
26
27
  max_x_ = bounds->max_x;
@@ -29,7 +29,7 @@ class InnerPolyline;
29
29
  class Polyline : public Partitionable {
30
30
  public:
31
31
  using Partitionable::Partitionable;
32
- Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds = std::nullopt);
32
+ Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds = std::nullopt, unsigned int class_id = 0);
33
33
  enum Flags : uint32_t {
34
34
  TRACKED_OUTER = 1 << 0
35
35
  };
@@ -47,6 +47,7 @@ class Polyline : public Partitionable {
47
47
  int max_y() const { return max_y_; }
48
48
  int min_y() const { return min_y_; }
49
49
  int max_x() const { return max_x_; }
50
+ unsigned int class_id() const { return class_id_; }
50
51
  void clear();
51
52
  bool is_empty();
52
53
  bool any_ancients = false;
@@ -63,5 +64,6 @@ class Polyline : public Partitionable {
63
64
  void find_boundary();
64
65
  uint32_t flags_ = 0;
65
66
  int name;
67
+ unsigned int class_id_;
66
68
  std::string named_;
67
69
  };
@@ -49,6 +49,7 @@ std::string Shape::name() {
49
49
  Polygon Shape::to_raw_polygon(bool bounds) {
50
50
  Polygon poly;
51
51
  poly.outer = this->outer_polyline->raw();
52
+ poly.class_id = this->outer_polyline->class_id();
52
53
  if (bounds) {
53
54
  this->outer_polyline->fill_bounds(poly.bounds);
54
55
  }
@@ -19,8 +19,8 @@ Shape* ShapePool::acquire_shape(Polyline* outer_polyline, const std::vector<Inne
19
19
  return shape;
20
20
  }
21
21
 
22
- Polyline* ShapePool::acquire_polyline(Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds = std::nullopt) {
23
- return this->polylines.acquire(this, tile, std::move(polygon), bounds);
22
+ Polyline* ShapePool::acquire_polyline(Tile* tile, std::vector<Point> polygon, unsigned int class_id, const std::optional<RectBounds>& bounds = std::nullopt) {
23
+ return this->polylines.acquire(this, tile, std::move(polygon), bounds, class_id);
24
24
  }
25
25
 
26
26
  InnerPolyline* ShapePool::acquire_inner_polyline(std::vector<Point> coords, Shape* shape) {
@@ -29,7 +29,7 @@ class ShapePool {
29
29
  Shape* acquire_shape(Polyline* outer_polyline, const std::vector<InnerPolyline*>& inner_polylines);
30
30
  InnerPolyline* acquire_inner_polyline(std::vector<Point> coords, Shape* s);
31
31
  InnerPolyline* acquire_inner_polyline(Sequence* seq);
32
- Polyline* acquire_polyline(Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds);
32
+ Polyline* acquire_polyline(Tile* tile, std::vector<Point> polygon, unsigned int class_id, const std::optional<RectBounds>& bounds);
33
33
  void set_owner(Tile* tile);
34
34
  void detach_shape();
35
35
  void detach_polyline();
@@ -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.class_id, 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
  }
@@ -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"
@@ -275,6 +277,9 @@ class To_Ruby<ProcessResult*>
275
277
  poly_hash[Symbol("bounds")] = bounds_hash;
276
278
  }
277
279
 
280
+ if(pr->options.contains("multiclass")) {
281
+ poly_hash[Symbol("class")] = x.class_id;
282
+ }
278
283
  out.push(poly_hash);
279
284
  }
280
285
  rr->polygons = out;
@@ -315,6 +320,10 @@ ProcessResult ruby_result_to_process_result(Rice::Object rb_result) {
315
320
  for (size_t i = 0; i < rb_polygons.size(); ++i) {
316
321
  Rice::Hash rb_poly = (Rice::Hash)rb_polygons[i];
317
322
  Polygon poly;
323
+ Rice::Object rb_class = rb_poly[Symbol("class")];
324
+ if (!rb_class.is_nil()) {
325
+ poly.class_id = detail::From_Ruby<unsigned int>().convert(rb_class.value());
326
+ }
318
327
  // BOUNDS
319
328
  Rice::Object rb_bounds_obj = rb_poly[Symbol("bounds")];
320
329
  if (!rb_bounds_obj.is_nil()) {
@@ -380,10 +389,9 @@ SvgStreamingMerger* create_svg_streaming_merger(Object self,
380
389
  GeoJsonStreamingMerger* create_geo_json_streaming_merger(Object self,
381
390
  int number_of_threads,
382
391
  Options options,
383
- Object stream_obj,
384
- int pixel_val) {
392
+ Object stream_obj) {
385
393
  OfstreamWrapper* wrapper = Rice::detail::From_Ruby<OfstreamWrapper*>().convert(stream_obj.value());
386
- return new GeoJsonStreamingMerger(number_of_threads, options, &wrapper->get_stream(), pixel_val);
394
+ return new GeoJsonStreamingMerger(number_of_threads, options, &wrapper->get_stream());
387
395
  }
388
396
 
389
397
  OfstreamWrapper* create_ofstream(Object self, std::string path) {
@@ -3,8 +3,8 @@
3
3
  module Contrek
4
4
  module Cpp
5
5
  class CPPGeoJsonConcurrentStreamingMerger < CPPGeoJsonStreamingMerger
6
- def self.new(stream_to:, number_of_threads: 0, options: nil, pixel_val: 0)
7
- super(number_of_threads, options, stream_to, pixel_val)
6
+ def self.new(stream_to:, number_of_threads: 0, options: nil)
7
+ super(number_of_threads, options, stream_to)
8
8
  end
9
9
  end
10
10
  end
@@ -23,7 +23,8 @@ module Contrek
23
23
  @to_points ||= raw_list.map do |polygon|
24
24
  {bounds: (polygon[:bounds] if polygon.key?(:bounds)),
25
25
  outer: self.class.to_points(polygon[:outer]),
26
- inner: polygon[:inner].map { |s| self.class.to_points(s) }}.compact
26
+ inner: polygon[:inner].map { |s| self.class.to_points(s) },
27
+ class: (polygon[:class] if polygon.key?(:class))}.compact
27
28
  end
28
29
  end
29
30
 
@@ -69,7 +69,7 @@ module Contrek
69
69
  end.real
70
70
  shape.clear_inner!
71
71
 
72
- polyline = Polyline.new(tile: tile, polygon: new_outer.to_a)
72
+ polyline = Polyline.new(tile: tile, polygon: new_outer.to_a, class_id: shape.outer_polyline.class_id)
73
73
  inserting_new_shape = Shape.init_by(polyline, new_inners)
74
74
  new_shapes << inserting_new_shape
75
75
  polyline.shape = inserting_new_shape
@@ -91,7 +91,9 @@ module Contrek
91
91
  new_position.end_point.tracked_outer = true
92
92
  versus = act_part.versus
93
93
  part = new_position.end_point.queues.find do |p|
94
- (p.versus == -versus) && p.polyline.tile != act_part.polyline.tile
94
+ (p.versus == -versus) &&
95
+ p.polyline.tile != act_part.polyline.tile &&
96
+ p.polyline.class_id == act_part.polyline.class_id
95
97
  end
96
98
  if part
97
99
  if all_parts[-2] != part
@@ -55,7 +55,8 @@ module Contrek
55
55
  versus: current_versus,
56
56
  bounds: true,
57
57
  treemap: @options[:treemap],
58
- connectivity: @options[:connectivity]
58
+ connectivity: @options[:connectivity],
59
+ multiclass: @options[:multiclass]
59
60
  }.compact,
60
61
  start_x: payload[:tile_start_x],
61
62
  end_x: payload[:tile_end_x]
@@ -3,11 +3,10 @@
3
3
  module Contrek
4
4
  module Concurrent
5
5
  class GeoJsonStreamingMerger < StreamingMerger
6
- def initialize(stream_to:, pixel_val:, options: {})
7
- @pixel_val = pixel_val
6
+ def initialize(stream_to:, options: {})
8
7
  @point_converter = self.class.coordinates_converter(options[:geo_localization])
9
8
 
10
- super(stream_to:, options:)
9
+ super
11
10
  end
12
11
 
13
12
  def write_header
@@ -35,7 +34,7 @@ module Contrek
35
34
  end
36
35
  feature_hash = {
37
36
  type: "Feature",
38
- properties: {PixelVal: @pixel_val},
37
+ properties: {PixelVal: polygon[:class]},
39
38
  geometry: {
40
39
  type: "Polygon",
41
40
  coordinates: polygon_coordinates
@@ -13,18 +13,21 @@ module Contrek
13
13
  @current_x = 0
14
14
  @tiles = Queue.new
15
15
  @whole_tile = nil
16
- @previous_boundary = nil
17
16
  @user_options = options
18
17
  if (warn_options = @user_options.keys & IGNORE_OPTIONS).any?
19
18
  warn "[Contrek WARNING] The given options '#{warn_options.join(",")}' will be ignored."
20
19
  end
21
20
  @options = {unsafe_mode: false}.merge(@user_options).except(*IGNORE_OPTIONS)
22
- unless safe?
21
+ if safe?
22
+ @validator = MergerValidator.new(self)
23
+ else
23
24
  warn "[Contrek WARNING] Processing tile with 'unsafe_mode: true'. Incompatible result options might lead to unexpected vector geometry."
24
25
  end
25
26
  end
26
27
 
27
28
  def add_tile(result)
29
+ @validator&.validate_metadata(result)
30
+
28
31
  @height ||= result.metadata[:height]
29
32
  raise ArgumentError, "All results must have the same height" if @height != result.metadata[:height] && safe?
30
33
  @options[:versus] ||= result.metadata[:versus]
@@ -35,8 +38,8 @@ module Contrek
35
38
  end
36
39
  end
37
40
 
38
- validate(result) if safe?
39
41
  end_x = @current_x + result.metadata[:width]
42
+ @validator&.validate(result, @current_x, end_x)
40
43
 
41
44
  tile = Tile.new(
42
45
  finder: self,
@@ -47,9 +50,6 @@ module Contrek
47
50
  tile.assign_raw_polygons!(result[:polygons])
48
51
 
49
52
  @tiles << tile
50
-
51
- @previous_boundary = boundary_coordinates(result.polygons, end_x) if safe?
52
-
53
53
  @maximum_width = end_x
54
54
  @current_x = end_x - 1
55
55
  end
@@ -65,25 +65,6 @@ module Contrek
65
65
 
66
66
  private
67
67
 
68
- def validate(result)
69
- return unless @previous_boundary
70
-
71
- current = boundary_coordinates(result.polygons, @current_x)
72
- unless @previous_boundary == current
73
- raise ArgumentError, "Tile boundary mismatch: adjacent tiles have different edge coordinates"
74
- end
75
- end
76
-
77
- def boundary_coordinates(polygons, boundary_x)
78
- coordinates = []
79
- polygons.each do |polygon|
80
- polygon[:outer].each do |point|
81
- coordinates << point[:y] if point[:x] == boundary_x
82
- end
83
- end
84
- coordinates.sort!
85
- end
86
-
87
68
  def translate(result, offset)
88
69
  result.polygons.each do |polygon|
89
70
  polygon[:outer].each { |p| p[:x] += offset }
@@ -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
@@ -7,10 +7,10 @@ module Contrek
7
7
 
8
8
  TRACKED_OUTER = 1 << 0
9
9
 
10
- attr_reader :raw, :name, :min_y, :max_y
10
+ attr_reader :raw, :name, :min_y, :max_y, :class_id
11
11
  attr_accessor :shape, :tile, :any_ancients, :inside_inner_polyline
12
12
 
13
- def initialize(tile:, polygon:, shape: nil, bounds: nil, force_named: nil)
13
+ def initialize(tile:, polygon:, shape: nil, bounds: nil, force_named: nil, class_id: 0)
14
14
  @tile = tile
15
15
  @name = tile.shapes.count
16
16
  @named = force_named
@@ -19,6 +19,7 @@ module Contrek
19
19
  @flags = 0
20
20
  @any_ancients = false
21
21
  @inside_inner_polyline = nil
22
+ @class_id = class_id
22
23
 
23
24
  if bounds.nil?
24
25
  find_boundary
@@ -40,8 +40,9 @@ module Contrek
40
40
  "Shape (outer_polyline = #{outer_polyline.named}, children count = #{@children_shapes.size}"
41
41
  end
42
42
 
43
- def to_raw_polygon(bounds: false)
43
+ def to_raw_polygon(bounds: false, multiclass: false)
44
44
  {bounds: (outer_polyline.get_bounds if bounds),
45
+ class: (outer_polyline.class_id if multiclass),
45
46
  outer: outer_polyline.raw,
46
47
  inner: inner_polylines.map(&:raw)}.compact
47
48
  end
@@ -37,7 +37,7 @@ module Contrek
37
37
  bounds = shape.outer_polyline.get_bounds
38
38
  if flush || bounds[:max_x] < safe_boundary_x
39
39
  @moved += 1
40
- polygon = shape.to_raw_polygon
40
+ polygon = shape.to_raw_polygon(multiclass: true)
41
41
  polygons_to_stream << polygon
42
42
  false
43
43
  else
@@ -57,7 +57,8 @@ module Contrek
57
57
 
58
58
  def to_raw_polygons
59
59
  bounds = @finder.options[:bounds]
60
- @shapes.map { |s| s.to_raw_polygon(bounds:) }
60
+ multiclass = @finder.options[:multiclass]
61
+ @shapes.map { |s| s.to_raw_polygon(bounds:, multiclass:) }
61
62
  end
62
63
 
63
64
  def compute_treemap
@@ -92,9 +93,14 @@ module Contrek
92
93
  @shapes = []
93
94
  shapes_map = {}
94
95
  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
96
  shape = Shape.new.tap do |shape|
97
- shape.outer_polyline = Polyline.new(tile: self, polygon: raw_polyline[:outer], shape: shape, bounds: raw_polyline[:bounds])
97
+ shape.outer_polyline = Polyline.new(
98
+ tile: self,
99
+ polygon: raw_polyline[:outer],
100
+ shape: shape,
101
+ bounds: raw_polyline[:bounds],
102
+ class_id: raw_polyline[:class] || 0
103
+ )
98
104
  shape.inner_polylines = raw_polyline[:inner].map { |raw| InnerPolyline.new(shape: shape, raw_coordinates: raw) }
99
105
  end
100
106
  @shapes << shape