contrek 1.0.3 → 1.0.4

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: e3520e328bbf96ac5cb581ebfeedd1a1e70da62937bb1544d83c869a73b468da
4
- data.tar.gz: 341b3852da6c311be99d0483cc4e99bb63e4669db6b68eda697081282d7d9c99
3
+ metadata.gz: d96b6c7ae60aefc5e7afed98c4619cf4e77cd0a226f2994d4dbc4f91952b315c
4
+ data.tar.gz: 336e0c2ca0f36f4608fd1547bdbcb27b5d78624a288d329a003e54412d6ce287
5
5
  SHA512:
6
- metadata.gz: 05ae4b575e7a207b1cea114c45cac1fee1082e403f9e051a8db8f0b64b7b603e7fc8e6a1f083b29cc36032d4d42191884452942faf0c91d4fea9f48b52eb5224
7
- data.tar.gz: 44b9b82008cb944ffd8aa598a72b08a9a09247f948f8d23eeec76f1fca6867a514dc32cfcc510359f90abf19ffd418a16d1f33aa1cd645a959444e8f00e8bdc7
6
+ metadata.gz: 615ae9e725c5fa149bb3f9f30be365863d348962f26fbda8610285a8e096b80dc6d564438480a8b783dc607c5bc4b3c66bb9bc5ca808d2783c494b74e3b3b7be
7
+ data.tar.gz: 1d2fc4088a58c1ffe2dbe46c08b67a728bf3eacce02ead907aa7b871565982e3161681205930de777bc7af3e702310dd3030fb961c0cbd71fb15280704e37da3
data/Gemfile.lock CHANGED
@@ -1,15 +1,19 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.0.2)
4
+ contrek (1.0.4)
5
5
  chunky_png (~> 1.4)
6
+ concurrent-ruby (~> 1.3.5)
6
7
  rice (~> 4.5)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
10
11
  specs:
11
12
  ast (2.4.3)
13
+ base64 (0.3.0)
12
14
  chunky_png (1.4.0)
15
+ concurrent-ruby (1.3.5)
16
+ curses (1.5.3)
13
17
  diff-lcs (1.6.1)
14
18
  json (2.11.3)
15
19
  language_server-protocol (3.17.0.4)
@@ -54,6 +58,8 @@ GEM
54
58
  lint_roller (~> 1.1)
55
59
  rubocop (>= 1.75.0, < 2.0)
56
60
  rubocop-ast (>= 1.38.0, < 2.0)
61
+ ruby-prof (1.7.2)
62
+ base64
57
63
  ruby-progressbar (1.13.0)
58
64
  standard (1.51.1)
59
65
  language_server-protocol (~> 3.17.0.2)
@@ -77,7 +83,9 @@ PLATFORMS
77
83
 
78
84
  DEPENDENCIES
79
85
  contrek!
86
+ curses (>= 1.5.3)
80
87
  rspec (~> 3.12)
88
+ ruby-prof (>= 1.7.2)
81
89
  standard (~> 1.51)
82
90
 
83
91
  BUNDLED WITH
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Contrek
2
- Contrek is a Ruby library (C++ powered) to trace png bitmap areas polygonal contours. Manages png images usign png++ and picoPNG (version 20101224) libraries.
2
+ Contrek is a Ruby library (C++ powered) to trace png bitmap areas polygonal contours. Manages png images usign png++ and picoPNG (version 20101224) libraries and may work multithreading.
3
3
 
4
4
  ## About Contrek library
5
- Contrek (contour trekking) simply scans your png bitmap and returns shape contour as close polygonal lines, both for the external and internal sides. It can compute the nesting level of the polygons found with a tree structure. It supports various levels and modes of compression and approximation of the found coordinates.
5
+ Contrek (**con**tour **trek**king) simply scans your png bitmap and returns shape contour as close polygonal lines, both for the external and internal sides. It can compute the nesting level of the polygons found with a tree structure. It supports various levels and modes of compression and approximation of the found coordinates. It is capable of multithreaded processing (currently only on the Ruby side), splitting the image into vertical strips and recombining the coordinates in pairs.
6
6
 
7
7
  In the following image all the non-white pixels have been examined and the result is the red polygon for the outer contour and the green one for the inner one
8
8
  ![alt text](contrek.png "Contour tracing")
@@ -43,7 +43,8 @@ result = Contrek.contour!(
43
43
  png_file_path: "labyrinth3.png",
44
44
  options: {
45
45
  class: "value_not_matcher",
46
- color: {r: 255, g: 0, b: 0, a: 255}
46
+ color: {r: 255, g: 0, b: 0, a: 255},
47
+ finder: {treemap: true}
47
48
  }
48
49
  )
49
50
  ```
@@ -85,6 +86,67 @@ You can also read base64 png images
85
86
  png_bitmap = CPPRemotePngBitMap.new("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==")
86
87
  ```
87
88
 
89
+ Multithreaded contour processing is supported. However, on Ruby MRI (the standard Ruby implementation, at least up to 3.x), the Global Interpreter Lock (GIL) prevents more than one thread from executing Ruby code simultaneously. As a consequence, execution remains effectively serialized even on multicore systems, unless the gem is used under JRuby or TruffleRuby (not tested).
90
+
91
+ The actual multithreaded implementation effort is therefore focused on the native C++ component, which is currently under development and is expected to provide substantially higher performance.
92
+
93
+ ```ruby
94
+ result = Contrek.contour!(
95
+ png_file_path: "./spec/files/images/rectangle_8x8.png",
96
+ options: {
97
+ number_of_threads: 2,
98
+ native: false,
99
+ class: "value_not_matcher",
100
+ color: {r: 255, g: 255, b: 255, a: 255},
101
+ finder: {number_of_tiles: 2, compress: {uniq: true, linear: true}}
102
+ }
103
+ )
104
+ ```
105
+ The example uses 2 threads, and the image has been divided into 2 vertical bands. It is also possible to rely on the system to determine the maximum number of threads supported by your CPU (cores) by passing the appropriate parameter.
106
+
107
+ ```ruby
108
+ number_of_threads: nil
109
+ ```
110
+
111
+ Regarding multithreading:
112
+
113
+ - The algorithm splits the contour-detection workflow into multiple phases that can be executed in parallel. The initial contour extraction on each band and the subsequent merging of coordinates between adjacent bands—performed pairwise, recursively, and in a non-deterministic order—results in a final output that is not idempotent. Idempotence is guaranteed only when the exact same merging sequence is repeated.
114
+
115
+ - The treemap option is currently ignored (multithreaded treemap support will be introduced in upcoming revisions).
116
+
117
+
118
+ ## Multithreaded approach
119
+
120
+ The multithreaded contour-tracing implementation operates as follows:
121
+
122
+ The input image is divided into multiple vertical bands—at least two—where each band shares a one-pixel-wide vertical strip with its adjacent bands.
123
+ For example, if the image is 20 pixels wide and split into two bands, the first band may span from x=0 to x=9 (10 pixels), and the second from x=9 to x=19 (11 pixels).
124
+ The vertical column at x=9 is therefore the shared region.
125
+
126
+ ```md
127
+ 01234567890123456789
128
+ ---------*----------
129
+ ```
130
+
131
+ Each band is processed independently as if it were a standalone image, with its contour-tracing assigned to a dedicated thread.
132
+ Whenever two adjacent bands have completed processing, they are assigned to an available thread that performs the merge of their coordinates.
133
+
134
+ For example, with three bands
135
+ B1 – B2 – B3
136
+ the merging sequence might be:
137
+
138
+ B1 + B2 → (B1+B2) + B3
139
+ or
140
+
141
+ B2 + B3 → B1 + (B2+B3)
142
+
143
+ The order is therefore nondeterministic, constrained only by the possible combinations derived from the initial number of bands.
144
+
145
+ The merging algorithm operates by splitting polygons that intersect the shared band into sequential subsets of coordinates—called parts—distinguishing between segments inside the shared region and those outside it.
146
+ The first stage merges the outer boundary; the second stage merges the disconnected inner parts, which are inserted where needed into the outer sequences produced in the first stage.
147
+
148
+ This process is applied recursively, merging bands until a single final band remains, upon which the coordinate-compression algorithms are executed.
149
+
88
150
  ## Performances native vs pure
89
151
  One of the most complex test you can find under the spec folder is named "scans poly 1200x800", scans this [image](spec/files/images/sample_1200x800.png) computing coordinates to draw polygons drawn in this [result](spec/files/stored_samples/sample_1200x800.png).
90
152
  On pure ruby implementation kept time
data/contrek.gemspec CHANGED
@@ -19,7 +19,11 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_development_dependency "rspec", "~> 3.12"
21
21
  s.add_development_dependency "standard", "~> 1.51"
22
+ s.add_development_dependency "curses", "~> 1.5", ">= 1.5.3"
23
+ s.add_development_dependency "ruby-prof", "~> 1.7", ">= 1.7.2"
24
+
22
25
  s.add_dependency "chunky_png", "~> 1.4"
26
+ s.add_dependency "concurrent-ruby", "~> 1.3.5"
23
27
  s.add_dependency "rice", "~> 4.5"
24
28
 
25
29
  s.required_ruby_version = ">= 3.0.0"
@@ -3,19 +3,40 @@ module Contrek
3
3
  class Bitmap
4
4
  include Painting
5
5
 
6
- def scan
7
- x = 0
6
+ def scan(start_x: 0, end_x: w)
7
+ x = start_x
8
8
  y = 0
9
9
  loop do
10
10
  yield x, y, value_at(x, y)
11
11
  x += 1
12
- if x == w
13
- x = 0
12
+ if x == end_x
13
+ x = start_x
14
14
  y += 1
15
15
  break if y == h
16
16
  end
17
17
  end
18
18
  end
19
+
20
+ def copy_rect(src_bitmap:, src_x:, src_y:, dest_x:, dest_y:, rect_w:, rect_h:)
21
+ rect_h.times do |hp|
22
+ rect_w.times do |wp|
23
+ src_pos_x = src_x + wp
24
+ src_pos_y = src_y + hp
25
+ src_color = src_bitmap.value_at(src_pos_x, src_pos_y)
26
+ dest_pos_x = dest_x + wp
27
+ dest_pos_y = dest_y + hp
28
+ value_set(dest_pos_x, dest_pos_y, src_color)
29
+ end
30
+ end
31
+ end
32
+
33
+ def clear!(color: " ")
34
+ w.times do |x|
35
+ h.times do |y|
36
+ value_set(x, y, color)
37
+ end
38
+ end
39
+ end
19
40
  end
20
41
  end
21
42
  end
@@ -28,6 +28,35 @@ module Contrek
28
28
 
29
29
  @raw[y * @module + x] = value
30
30
  end
31
+
32
+ def dup!
33
+ ChunkyBitmap.new(@raw, @module)
34
+ end
35
+
36
+ def draw_polygons(polygons)
37
+ polygons.each do |polygon|
38
+ [[:outer, "o"], [:inner, "i"]].each do |side, color|
39
+ sequences = polygon[side]
40
+ sequences = [sequences] if side == :outer
41
+ sequences.each do |sequence|
42
+ sequence.each do |position|
43
+ value_set(position[:x], position[:y], color)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def to_terminal
51
+ puts " " + (0...@module).map { |i| (i % 10).to_s }.join
52
+ n = 0
53
+ @raw.scan(/.{1,#{@module}}/).each do |line|
54
+ puts n.to_s + " " + line
55
+ n += 1
56
+ n = 0 if n >= 10
57
+ end
58
+ puts
59
+ end
31
60
  end
32
61
  end
33
62
  end
@@ -0,0 +1,9 @@
1
+ module Contrek
2
+ module Bitmaps
3
+ class CustomBitmap < PngBitmap
4
+ def initialize(w:, h:)
5
+ @image = ChunkyPNG::Image.new(w, h, ChunkyPNG::Color::TRANSPARENT)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -57,6 +57,10 @@ module Contrek
57
57
  def inspect
58
58
  "PngBitMap"
59
59
  end
60
+
61
+ def clear
62
+ @image = ChunkyPNG::Image.new(w, h, ChunkyPNG::Color.rgba(255, 255, 255, 255))
63
+ end
60
64
  end
61
65
  end
62
66
  end
@@ -0,0 +1,13 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class ClippedPolygonFinder < Finder::PolygonFinder
4
+ attr_reader :start_x, :end_x
5
+
6
+ def initialize(bitmap:, matcher:, start_x:, end_x:, options: {})
7
+ @start_x = start_x
8
+ @end_x = end_x
9
+ super(bitmap, matcher, nil, options)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,86 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class Cluster
4
+ attr_reader :tiles, :hub
5
+
6
+ def initialize(finder:, height:, width:)
7
+ @finder = finder
8
+ @tiles = []
9
+ @hub = Hub.new(height:, width:)
10
+ end
11
+
12
+ def add(tile)
13
+ last_tile = @tiles.last
14
+ last_tile.next = last_tile.circular_next = tile if last_tile
15
+ @tiles << tile
16
+ tile.prev = last_tile
17
+ tile.circular_next = last_tile
18
+ tile.cluster = self
19
+ end
20
+
21
+ # Here, two tiles are merged into a single one. The tile is assigned new shapes composed of the detected
22
+ # outer sequences and both the old and new inner ones. Shapes that are not in the contact area are directly
23
+ # reassigned unchanged to the new shape.
24
+ def merge_tiles!
25
+ tot_inner = 0
26
+ tot_outer = 0
27
+
28
+ new_shapes = []
29
+ tot_outer += Benchmark.measure do
30
+ @tiles.each do |tile|
31
+ tile.shapes.each do |shape|
32
+ next if shape.outer_polyline.on?(Polyline::TRACKED_OUTER) || shape.outer_polyline.width == 0
33
+ if shape.outer_polyline.boundary?
34
+ shape.outer_polyline.partition!
35
+ shape.outer_polyline.precalc!
36
+ end
37
+ end
38
+ end
39
+ end.real
40
+
41
+ @tiles.each do |tile|
42
+ tile.shapes.each do |shape|
43
+ next if shape.outer_polyline.on?(Polyline::TRACKED_OUTER) || shape.outer_polyline.width == 0
44
+
45
+ if shape.outer_polyline.boundary? && shape.outer_polyline.next_tile_eligible_shapes.any?
46
+ new_outer = nil
47
+ new_inners = shape.inner_polylines
48
+ cursor = Cursor.new(cluster: self, shape: shape)
49
+
50
+ tot_outer += Benchmark.measure do
51
+ new_outer = cursor.join_outers!
52
+ end.real
53
+ tot_inner += Benchmark.measure do
54
+ new_inner_seq = cursor.join_inners!(new_outer)
55
+ new_inners += new_inner_seq if new_inner_seq.any?
56
+ new_inners += cursor.orphan_inners
57
+ end.real
58
+
59
+ new_shapes << Shape.new.tap do |shape|
60
+ polyline = Polyline.new(tile: tile, polygon: new_outer.to_a)
61
+ shape.outer_polyline = polyline
62
+ polyline.shape = shape
63
+ shape.inner_polylines = new_inners
64
+ end
65
+ else
66
+ new_shapes << shape
67
+ end
68
+ end
69
+ end
70
+ past_tot_outer = @tiles.first.benchmarks[:outer] + @tiles.last.benchmarks[:outer]
71
+ past_tot_inner = @tiles.first.benchmarks[:inner] + @tiles.last.benchmarks[:inner]
72
+
73
+ tile = Tile.new(
74
+ finder: @finder,
75
+ start_x: @tiles.first.start_x,
76
+ end_x: @tiles.last.end_x,
77
+ benchmarks: {outer: tot_outer + past_tot_outer, inner: tot_inner + past_tot_inner},
78
+ name: @tiles.first.name + @tiles.last.name
79
+ )
80
+
81
+ tile.assign_shapes!(new_shapes)
82
+ tile
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,219 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class Cursor
4
+ attr_reader :orphan_inners
5
+ def initialize(cluster:, shape:)
6
+ @polylines_sequence = []
7
+ @cluster = cluster
8
+ @outer_polyline = shape.outer_polyline
9
+ @orphan_inners = []
10
+ @shapes = [shape]
11
+ end
12
+
13
+ def inspect
14
+ self.class
15
+ end
16
+
17
+ # Given the initial polyline, draw its outer boundary, possibly extending into
18
+ # adjacent polylines, and then connect them. At the end, @polylines_sequence
19
+ # contains the merged polylines. Returns a new resulting polyline.
20
+ def join_outers!
21
+ seq_log = []
22
+ @polylines_sequence << @outer_polyline
23
+
24
+ outer_joined_polyline = Sequence.new
25
+
26
+ traverse_outer(@outer_polyline.parts.first,
27
+ seq_log,
28
+ @polylines_sequence,
29
+ @shapes,
30
+ outer_joined_polyline)
31
+ outer_joined_polyline.pop! if outer_joined_polyline.head.payload == outer_joined_polyline.tail.payload &&
32
+ @cluster.tiles.first.left? && @cluster.tiles.last.right?
33
+
34
+ @polylines_sequence.uniq!
35
+
36
+ @polylines_sequence.each { |c| c.turn_on(Polyline::TRACKED_OUTER) }
37
+ (@shapes - [@outer_polyline.shape]).each do |shape|
38
+ @orphan_inners += shape.inner_polylines
39
+ shape.clear_inner!
40
+ end
41
+
42
+ outer_joined_polyline
43
+ end
44
+
45
+ def join_inners!(outer_seq)
46
+ # search for missing sequence to sew
47
+ missing_shapes = []
48
+ @cluster.tiles.each do |tile|
49
+ tile.shapes.each do |shape|
50
+ next if shape.outer_polyline.on?(Polyline::TRACKED_OUTER) ||
51
+ shape.outer_polyline.on?(Polyline::TRACKED_INNER) ||
52
+ !shape.outer_polyline.boundary? ||
53
+ @polylines_sequence.include?(shape.outer_polyline)
54
+ missing_shapes << shape
55
+ end
56
+ end
57
+ @polylines_sequence.each do |polyline|
58
+ missing_shapes.each do |missing_shape|
59
+ outer_polyline = missing_shape.outer_polyline
60
+ if (intersection = polyline.intersection(outer_polyline)).any?
61
+ inject_sequences_left, inject_sequences_right = polyline.sew!(intersection, outer_polyline)
62
+ combine!(inject_sequences_right, inject_sequences_left).each do |sewn_sequence|
63
+ sewn_sequence.uniq!
64
+ @orphan_inners << sewn_sequence if sewn_sequence.size > 1 && sewn_sequence.map { |c| c[:x] }.uniq.size > 1 # segmenti non sono ammessi, solo aree
65
+ end
66
+ outer_polyline.clear!
67
+ outer_polyline.turn_on(Polyline::TRACKED_OUTER)
68
+ outer_polyline.turn_on(Polyline::TRACKED_INNER)
69
+ @orphan_inners += missing_shape.inner_polylines
70
+ end
71
+ end
72
+ end
73
+
74
+ retme = collect_inner_sequences(outer_seq)
75
+
76
+ @polylines_sequence.each do |polyline|
77
+ polyline.turn_on(Polyline::TRACKED_INNER)
78
+ end
79
+ retme
80
+ end
81
+
82
+ private
83
+
84
+ # rubocop:disable Lint/NonLocalExitFromIterator
85
+ def traverse_outer(act_part, all_parts, polylines, shapes, outer_joined_polyline)
86
+ all_parts << act_part if all_parts.last != act_part
87
+ shapes << act_part.polyline.shape if !shapes.include?(act_part.polyline.shape)
88
+
89
+ if act_part.is?(Part::EXCLUSIVE)
90
+ return if act_part.size == 0
91
+ while (position = act_part.next_position)
92
+ return if outer_joined_polyline.size > 1 &&
93
+ outer_joined_polyline.head.payload == position.payload &&
94
+ act_part == all_parts.first
95
+ outer_joined_polyline.add(position)
96
+ end
97
+ else
98
+ while (new_position = act_part.iterator)
99
+ return if outer_joined_polyline.size > 1 &&
100
+ outer_joined_polyline.head.payload == new_position.payload &&
101
+ act_part == all_parts.first
102
+ outer_joined_polyline.add(Position.new(position: new_position.payload, hub: @cluster.hub))
103
+
104
+ act_part.polyline.next_tile_eligible_shapes.each do |shape|
105
+ if (part = shape.outer_polyline.find_first_part_by_position(new_position))
106
+ if all_parts[-2] != part
107
+ if all_parts.size >= 2
108
+ map = all_parts[-2..].map(&:type).uniq
109
+ break if map.size == 1 && map.first == Part::SEAM
110
+ end
111
+ polylines << part.polyline.shape.outer_polyline
112
+ part.next_position(new_position.payload)
113
+ traverse_outer(part, all_parts, polylines, shapes, outer_joined_polyline)
114
+ return
115
+ end
116
+ end
117
+ end
118
+ act_part.passes += 1
119
+ act_part.next_position
120
+ end
121
+ end
122
+ next_part = act_part.circular_next
123
+ next_part.rewind!
124
+ traverse_outer(act_part.circular_next, all_parts, polylines, shapes, outer_joined_polyline)
125
+ end
126
+
127
+ def collect_inner_sequences(outer_seq)
128
+ return_sequences = []
129
+ @polylines_sequence.each do |polyline|
130
+ polyline.parts.each do |part|
131
+ if part.innerable?
132
+ all_parts = []
133
+ bounds = {min: polyline.max_y, max: 0}
134
+ traverse_inner(part, all_parts, bounds)
135
+ range_of_bounds = (bounds[:min]..bounds[:max])
136
+
137
+ retme_sequence = Sequence.new
138
+ all_parts.map do |part|
139
+ part.touch!
140
+
141
+ retme_sequence.move_from(part) do |pos|
142
+ next false if part.is?(Part::ADDED) && !(range_of_bounds === pos.payload[:y])
143
+ !(polyline.tile.tg_border?(pos.payload) && pos.end_point.queues.include?(outer_seq))
144
+ end
145
+ end.flatten
146
+ raw_sequence = retme_sequence.to_a
147
+ return_sequences << raw_sequence if raw_sequence.map { _1[:x] }.uniq.take(2).size > 1
148
+ end
149
+ end
150
+ end
151
+ return_sequences
152
+ end
153
+
154
+ def traverse_inner(act_part, all_parts, bounds)
155
+ return if act_part == all_parts.first
156
+
157
+ if act_part.size > 0
158
+ min_y, max_y = act_part.to_a.minmax_by { |p| p[:y] }
159
+ bounds[:min] = [bounds[:min], min_y[:y]].min
160
+ bounds[:max] = [bounds[:max], max_y[:y]].max
161
+ end
162
+ if act_part.innerable?
163
+ all_parts << act_part
164
+ while (act_part = act_part.next)
165
+ if act_part.innerable?
166
+ all_parts << act_part
167
+ else
168
+ act_part.polyline.next_tile_eligible_shapes.each do |shape|
169
+ shape.outer_polyline.parts.each do |dest_part|
170
+ next if dest_part.trasmuted
171
+ if dest_part.intersect_part?(act_part)
172
+ link_seq = duplicates_intersection(dest_part, act_part)
173
+ if link_seq.any?
174
+ ins_part = Part.new(Part::ADDED, act_part.polyline)
175
+ link_seq.each { |pos| ins_part.add(Position.new(position: pos, hub: @cluster.hub)) }
176
+ all_parts << ins_part
177
+ end
178
+ traverse_inner(dest_part.circular_next, all_parts, bounds)
179
+ return
180
+ end
181
+ end
182
+ end
183
+ all_parts << act_part if act_part.is?(Part::SEAM)
184
+ end
185
+ end
186
+ elsif act_part.next
187
+ traverse_inner(act_part.next, all_parts, bounds)
188
+ end
189
+ end
190
+ # rubocop:enable Lint/NonLocalExitFromIterator
191
+
192
+ # The sequences should contain inverted parts, e.g., 4,5,6,6,5,4. These parts must be
193
+ # removed from the sequences, and the remaining elements are compared. Only the
194
+ # elements that appear once between the two sequences are kept. This represents
195
+ # a connection between parts inserted afterwards.
196
+ # TODO evaluate the adoption of remove_adjacent_pairs!
197
+ def duplicates_intersection(part_a, part_b)
198
+ a1 = part_a.inverts ? part_a.remove_adjacent_pairs : part_a.to_a
199
+ b1 = part_b.inverts ? part_b.remove_adjacent_pairs : part_b.to_a
200
+
201
+ (a1 - b1) + (b1 - a1)
202
+ end
203
+
204
+ # example
205
+ # a = [[A],[B,C]]
206
+ # b = [[D],[E,F]]
207
+ # res = [[D,B,C],[E,F,A]]
208
+ def combine!(seqa, seqb)
209
+ rets = []
210
+ [seqa.count, seqb.count].min.times do
211
+ last = seqa.pop
212
+ first = seqb.shift
213
+ rets << first + last
214
+ end
215
+ rets
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,14 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class EndPoint
4
+ attr_reader :queues
5
+ def initialize
6
+ @queues = []
7
+ end
8
+
9
+ def inspect
10
+ "EndPoint[#{@queues.size}]"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class FakeCluster < Finder::NodeCluster
4
+ def initialize(polygons, options)
5
+ @options = options
6
+ @polygons = polygons
7
+ end
8
+ end
9
+ end
10
+ end