contrek 1.3.1 → 1.3.2

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +3 -3
  5. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +23 -3
  6. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/FastPngBitmap.cpp +2 -2
  7. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/RawBitmap.cpp +2 -2
  8. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/RemoteFastPngBitmap.cpp +1 -1
  9. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.h +2 -2
  10. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.h +4 -2
  11. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.cpp +6 -6
  12. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.cpp +16 -10
  13. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.h +1 -1
  14. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.cpp +16 -15
  15. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.h +3 -3
  16. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Finder.cpp +1 -9
  17. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/GeoJsonStreamingMerger.h +81 -0
  18. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/InnerPolyline.cpp +9 -28
  19. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/InnerPolyline.h +10 -11
  20. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Part.h +5 -3
  21. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.cpp +4 -3
  22. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.h +7 -5
  23. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Position.cpp +1 -1
  24. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Queueable.h +2 -2
  25. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Sequence.cpp +0 -14
  26. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Sequence.h +0 -2
  27. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Shape.cpp +5 -5
  28. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.cpp +31 -26
  29. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.h +7 -7
  30. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.cpp +12 -31
  31. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.h +10 -8
  32. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/SubPool.h +47 -0
  33. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/SvgStreamingMerger.h +43 -0
  34. data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +2 -1
  35. data/lib/contrek/finder/concurrent/cluster.rb +6 -5
  36. data/lib/contrek/finder/concurrent/cursor.rb +2 -1
  37. data/lib/contrek/finder/concurrent/finder.rb +1 -12
  38. data/lib/contrek/finder/concurrent/inner_polyline.rb +8 -25
  39. data/lib/contrek/finder/concurrent/sequence.rb +1 -17
  40. data/lib/contrek/version.rb +1 -1
  41. metadata +8 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fff9996602ac84d2a5d96e840150e59cb457bc8050c53758efac88f170bef79
4
- data.tar.gz: 40958fef1d22e2458beae7291cfdca85102eda4983bf01ef468c334ddce99ec8
3
+ metadata.gz: 8fcfc790a324e42855e8b8e816f946da8ea764943a44f4d9999b4cc7a360209d
4
+ data.tar.gz: 8f0d0f42350fc8236a9c2e5ca974ab11d91a5bcfbdd4a7fdef9cf310bf764306
5
5
  SHA512:
6
- metadata.gz: 7f8f1f1368c472fb9770a892874f54817a67745929a0d772c5a9cc3e387fcd868e5662ad67e341d6a53232b6be4eb028438545535adfb05be3d69c497c3918da
7
- data.tar.gz: cc7991d57ce63d43c98165c1f3684d1f31d05993a6e4a20e40437d70d5b251a1de0032ca354f44d2bb8efbf30286423f6b2925e01da0f039c71661427f4d2716
6
+ metadata.gz: b7a5c008b6811b36ce143642d6300e89d7975dd0861cfc429880bbd76bf8a4b2eadf6c24351ca6d3de746ddbecaec849b4aed2052e973e34a386a9ae20e4a4a6
7
+ data.tar.gz: 8ff2d2465df0afcd376255bb575a46581d27888ec5e7293d9971e27f624550dc83e1fb47ca11814cbeb220c0b38c51b7179087361fbab9140ca181a728de706e
data/CHANGELOG.md CHANGED
@@ -129,3 +129,6 @@ All notable changes to this project will be documented in this file.
129
129
 
130
130
  ## [1.3.1] - 2026-06-20
131
131
  - **Streaming merger:** The progressive streaming extraction mode has now reached new heights of efficiency on the C++ side. This mode allows the data source to be processed in contiguous blocks. All isolated polygons, as well as those extending into the bottom strips, are removed from the list and streamed directly to the SVG file. This drastically reduces RAM requirements. An extreme test on an 81920×81920 pixel image containing a massive number of polygons (20,000,000) was processed using roughly 40 strips of 2000 pixels each in less than 300 seconds, peaking at a RAM usage of just 13GB.
132
+
133
+ ## [1.3.2] - 2026-06-27
134
+ - **Streaming merger:** A series of optimizations have been implemented at the C++ memory pool level; using a different strategy, these are now freed as soon as possible depending on the source data content and the morphology of the containing polygons. This has allowed for a further reduction in the peak memory indicated in the previous changelog, specifically from 13GB to 4.3GB, on the same image with 20,000,000 polygons.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.3.1)
4
+ contrek (1.3.2)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Contrek
2
- Contrek is a Ruby gem powered by a <u>[standalone C++17 core library](#-c-standalone-library-usage)</u> for fast contour tracing and edge detection in PNG images and raw memory streams. It employs a **topological approach** to extract polygonal contours, representing shapes as a connected graph of shared endpoints. This ensures perfect adjacency and structural integrity for shape analysis and raster-to-vector workflows, such as PNG to SVG conversion, managed via libspng (0.7.4) with multithreading support.
3
-
2
+ **Contrek** is a standalone <u>[C++17 topology-preserving streaming polygonization engine](#-c-standalone-library-usage)</u> for raster-to-vector conversion. It extracts polygonal contours from PNG images and raw memory streams using programmable pixel matchers, representing shapes as connected graphs of shared vertices to guarantee topological consistency. Its streaming architecture supports gigapixel-scale datasets, multithreaded execution, and incremental vector serialization to multiple formats such as SVG while keeping memory consumption bounded. A fully featured Ruby wrapper is also available, exposing the complete C++ API through an idiomatic Ruby interface.
4
3
 
5
4
  ## About Contrek library
6
5
 
7
6
  Contrek (**con**tour **trek**king) simply scans your png/raw 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, splitting the image into vertical strips and recombining the coordinates in pairs.
8
7
 
9
8
  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
9
+
10
10
  ![alt text](contrek.png "Contour tracing")
11
11
 
12
12
  ## 🚀 Why Contrek?
@@ -58,7 +58,7 @@ The **Stripe-Merging** algorithm has been validated through a dedicated testing
58
58
  * **Extraction Fidelity:** Verifying polygon precision across both engines.
59
59
 
60
60
  The complete testing suite, source code, and raw benchmarks are available here:
61
- 👉 **[test_opencv_contrek](https://github.com/runout77/test_opencv_contrek)**
61
+ 👉 **[test_contrek](https://github.com/runout77/test_contrek)**
62
62
 
63
63
  ## Prerequisites
64
64
 
@@ -18,6 +18,8 @@
18
18
  #include <cstring>
19
19
  #include <algorithm>
20
20
  #include <cstdio>
21
+ #include <fstream>
22
+ #include <unistd.h>
21
23
 
22
24
  #include "polygon/finder/PolygonFinder.h"
23
25
  #include "polygon/finder/concurrent/ClippedPolygonFinder.h"
@@ -35,6 +37,8 @@
35
37
  #include "polygon/finder/concurrent/HorizontalMerger.h"
36
38
  #include "polygon/finder/concurrent/VerticalMerger.h"
37
39
  #include "polygon/finder/concurrent/StreamingMerger.h"
40
+ #include "polygon/finder/concurrent/SvgStreamingMerger.h"
41
+ #include "polygon/finder/concurrent/GeoJsonStreamingMerger.h"
38
42
  #include "polygon/finder/concurrent/Sequence.h"
39
43
  #include "polygon/finder/concurrent/Position.h"
40
44
  #include "polygon/finder/Polygon.h"
@@ -316,7 +320,7 @@ void stream_png_image(const std::string& filepath, uint32_t stripe_height, bool
316
320
  int stripe_count = 0;
317
321
  // main stripes loop
318
322
  for (uint32_t current_y_offset = 0; current_y_offset < total_height; current_y_offset += stripe_height) {
319
- int uncovered_height = total_height - current_y_offset;
323
+ uint32_t uncovered_height = total_height - current_y_offset;
320
324
 
321
325
  // copy previous last line to the next new one (each contigue stripe must share one pixel scanline)
322
326
  if (current_y_offset > 0) {
@@ -384,6 +388,17 @@ void Tests::test_i() {
384
388
  std::cout << "Memory usage peak: " << get_peak_rss() << " MB" << std::endl;
385
389
  }
386
390
 
391
+ double get_current_rss_mb() {
392
+ std::ifstream statm("/proc/self/statm");
393
+ uint32_t size = 0;
394
+ uint32_t resident = 0;
395
+
396
+ statm >> size >> resident;
397
+
398
+ uint32_t page_size = sysconf(_SC_PAGESIZE);
399
+ return (resident * page_size) / (1024.0 * 1024.0);
400
+ }
401
+
387
402
  void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_height) {
388
403
  std::vector<ProcessResult*> result_clones;
389
404
  std::vector<std::string> varguments = {"--bounds"};
@@ -422,14 +437,17 @@ void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_h
422
437
  if (!shared_stream) {
423
438
  std::cerr << "Error: Unable creating output streaming file!" << std::endl;
424
439
  }
440
+ std::vector<char> buffer(4 * 1024 * 1024); // Buffer (4MB)
441
+ shared_stream.rdbuf()->pubsetbuf(buffer.data(), buffer.size());
425
442
 
426
- StreamingMerger vmerger(0, &varguments, &shared_stream, total_width, total_height);
443
+ SvgStreamingMerger vmerger(0, &varguments, &shared_stream, total_width, total_height);
444
+ // GeoJsonStreamingMerger vmerger(0, &varguments, &shared_stream, total_width, total_height, 4294901760);
427
445
  try {
428
446
  size_t row_size = static_cast<size_t>(total_width) * 4;
429
447
  int stripe_count = 0;
430
448
  // main stripes loop
431
449
  for (uint32_t current_y_offset = 0; current_y_offset < total_height; current_y_offset += stripe_height) {
432
- int uncovered_height = total_height - current_y_offset;
450
+ uint32_t uncovered_height = total_height - current_y_offset;
433
451
 
434
452
  // copy previous last line to the next new one (each contigue stripe must share one pixel scanline)
435
453
  if (current_y_offset > 0) {
@@ -463,6 +481,7 @@ void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_h
463
481
  std::cout << "stripe " << stripe_count << ": found polygons " << result->groups << std::endl;
464
482
  vmerger.add_tile(*result, !(current_y_offset + stripe_height < total_height));
465
483
  delete result;
484
+ std::cout << "-> RSS before merge: " << get_current_rss_mb() << " MB" << std::endl;
466
485
  }
467
486
  stripe_count++;
468
487
  }
@@ -479,5 +498,6 @@ void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_h
479
498
 
480
499
  void Tests::test_l() {
481
500
  stream_progressive_png_image("../images/mixed_shapes_1024x1024.png", 300);
501
+ // stream_progressive_png_image("../images/test_20480x20480.png", 2000);
482
502
  std::cout << "Memory usage peak: " << get_peak_rss() << " MB" << std::endl;
483
503
  }
@@ -32,9 +32,9 @@ FastPngBitmap::FastPngBitmap(std::string filename) : RawBitmap() {
32
32
  } else {
33
33
  spng_ctx *ctx = spng_ctx_new(0);
34
34
  spng_set_png_buffer(ctx, file_buffer.data(), file_buffer.size());
35
- struct spng_ihdr ihdr;
35
+ struct spng_ihdr ihdr = {};
36
36
  spng_get_ihdr(ctx, &ihdr);
37
- size_t out_size;
37
+ size_t out_size = 0;
38
38
  spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &out_size);
39
39
  this->define(ihdr.width, ihdr.height, 4, false);
40
40
  unsigned char* raw_ptr = this->image.get();
@@ -29,7 +29,7 @@ int RawBitmap::h() {
29
29
  return this->height;
30
30
  }
31
31
 
32
- char RawBitmap::value_at(int x, int y) {
32
+ char RawBitmap::value_at(int, int) {
33
33
  return(0);
34
34
  }
35
35
 
@@ -48,7 +48,7 @@ unsigned int RawBitmap::rgb_value_at(int x, int y) {
48
48
  }
49
49
 
50
50
  void RawBitmap::draw_pixel(int x, int y, unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
51
- if (x < 0 || x >= width || y < 0 || y >= height) return;
51
+ if (x < 0 || x >= static_cast<int>(width) || y < 0 || y >= static_cast<int>(height)) return;
52
52
  unsigned char* p = &image[(static_cast<size_t>(y) * width + x) * bpp];
53
53
  p[0] = r;
54
54
  if (bpp > 1) p[1] = g;
@@ -30,7 +30,7 @@ RemoteFastPngBitmap::RemoteFastPngBitmap(std::string *dataurl) : FastPngBitmap("
30
30
  struct spng_ihdr ihdr;
31
31
  int error = spng_get_ihdr(ctx, &ihdr);
32
32
  if (!error) {
33
- size_t out_size; // RGBA8 dimension
33
+ size_t out_size = 0; // RGBA8 dimension
34
34
  spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &out_size);
35
35
  this->define(ihdr.width, ihdr.height, 4, false);
36
36
  error = spng_decode_image(ctx, image.get(), out_size, SPNG_FMT_RGBA8, SPNG_DECODE_TRNS);
@@ -43,7 +43,7 @@ struct SmallVec {
43
43
  ptr = np; cap = n;
44
44
  }
45
45
  void clear() { sz = 0; ptr = buf; cap = INLINE_CAP; }
46
- int size() const { return sz; }
46
+ unsigned int size() const { return static_cast<unsigned int>(sz); }
47
47
  int& operator[](int i) { return ptr[i]; }
48
48
  int operator[](int i) const { return ptr[i]; }
49
49
  int* begin() { return ptr; }
@@ -88,7 +88,6 @@ class Node : public Listable {
88
88
  int abs_x_index;
89
89
  int up_indexer, down_indexer;
90
90
  int tangs_count;
91
- char name;
92
91
  int track;
93
92
  int outer_index, inner_index;
94
93
  int upper_start = std::numeric_limits<int>::max();
@@ -112,6 +111,7 @@ class Node : public Listable {
112
111
  Node(int min_x, int max_x, int y, NodeCluster* cluster, char name);
113
112
  void precalc_tangs_sequences(NodeCluster& cluster);
114
113
  bool processed = false;
114
+ char name;
115
115
  int inner_left_index = -1;
116
116
  int inner_right_index = -1;
117
117
  };
@@ -14,7 +14,6 @@
14
14
  #include <map>
15
15
  #include <string>
16
16
  #include <utility>
17
- #include "PolygonFinder.h"
18
17
  #include "Lists.h"
19
18
  #include "RectBounds.h"
20
19
  #include "Polygon.h"
@@ -34,8 +33,10 @@ class NodeCluster {
34
33
  int count = 0;
35
34
  int nodes;
36
35
  int width;
36
+ int height;
37
37
 
38
38
  public:
39
+ int get_height() const { return height; }
39
40
  pf_Options *options;
40
41
  std::vector<std::pair<int, int>> treemap; // [a,b] a = index of parent outer, b = index of inner of parent outer
41
42
  std::pair<int, int> test_in_hole_a(Node* node);
@@ -43,7 +44,6 @@ class NodeCluster {
43
44
  std::vector<std::deque<Node>> vert_nodes;
44
45
  void compress_coords(std::list<Polygon>& polygons, pf_Options options);
45
46
  List *root_nodes;
46
- int height;
47
47
  std::list<Polygon> polygons;
48
48
  NodeCluster(int h, int w, pf_Options *options);
49
49
  virtual ~NodeCluster();
@@ -55,3 +55,5 @@ class NodeCluster {
55
55
  std::list<Node*>::iterator exam(std::list<Node*>::iterator inode, Node *node, Node *father, Node *root_node);
56
56
  std::vector<std::vector<Node*>> sequences;
57
57
  };
58
+
59
+ #include "PolygonFinder.h"
@@ -29,14 +29,14 @@
29
29
 
30
30
  PolygonFinder::PolygonFinder(Bitmap *bitmap,
31
31
  Matcher *matcher,
32
- Bitmap *test_bitmap,
32
+ Bitmap *,
33
33
  std::vector<std::string> *options,
34
34
  int start_x,
35
35
  int end_x)
36
- : source_bitmap(bitmap),
37
- matcher(matcher),
38
- start_x(start_x),
39
- end_x(end_x == -1 ? bitmap->w() : end_x)
36
+ : start_x(start_x),
37
+ end_x(end_x == -1 ? bitmap->w() : end_x),
38
+ source_bitmap(bitmap),
39
+ matcher(matcher)
40
40
  { this->rgb_matcher = dynamic_cast<RGBMatcher*>(matcher);
41
41
  if (options != nullptr) FinderUtils::sanitize_options(this->options, options);
42
42
 
@@ -70,7 +70,7 @@ PolygonFinder::PolygonFinder(Bitmap *bitmap,
70
70
 
71
71
  std::list<ShapeLine*> *PolygonFinder::get_shapelines() {
72
72
  std::list<ShapeLine*> *sll = new std::list<ShapeLine*>();
73
- for (int line = 0; line < this->node_cluster->height; line++) {
73
+ for (int line = 0; line < this->node_cluster->get_height(); line++) {
74
74
  for (Node& node : this->node_cluster->vert_nodes[line]) {
75
75
  ShapeLine *sl = new ShapeLine({node.min_x, node.max_x, node.y});
76
76
  sll->push_back(sl);
@@ -17,7 +17,7 @@
17
17
  #include "Cursor.h"
18
18
  #include "../../CpuTimer.h"
19
19
 
20
- Cluster::Cluster(Finder *finder, int height, int start_x, int end_x)
20
+ Cluster::Cluster(Finder *finder, int height)
21
21
  : finder_(finder)
22
22
  { tiles_.reserve(2); // only two (left|right)
23
23
  this->hub_ = new Hub(height);
@@ -55,6 +55,7 @@ Tile* Cluster::merge_tiles() {
55
55
  CpuTimer timer;
56
56
 
57
57
  std::vector<Shape*> new_shapes;
58
+ std::vector<Shape*> detach_shapes;
58
59
  std::vector<InnerPolyline*> all_new_inner_polylines;
59
60
 
60
61
  timer.start();
@@ -78,34 +79,34 @@ Tile* Cluster::merge_tiles() {
78
79
 
79
80
  if (shape->outer_polyline->any_ancients) {
80
81
  Cursor cursor(*this, shape);
81
- Sequence* new_outer = nullptr;
82
-
83
82
  timer.start();
84
- new_outer = cursor.join_outers();
83
+ Sequence new_outer = cursor.join_outers();
85
84
  tot_outer += timer.stop();
86
85
 
87
86
  timer.start();
88
87
  std::vector<InnerPolyline*> new_inners = shape->inner_polylines;
89
- std::vector<InnerPolyline*> new_inner_polylines = cursor.join_inners(new_outer, treemap);
88
+ std::vector<InnerPolyline*> new_inner_polylines = cursor.join_inners(treemap);
90
89
  tot_inner += timer.stop();
91
90
 
92
91
  for (InnerPolyline* inner_polyline : new_inner_polylines) {
93
92
  new_inners.push_back(inner_polyline);
94
93
  if (treemap) {
95
- inner_polyline->sequence()->compute_vertical_bounds();
94
+ inner_polyline->compute_vertical_bounds();
96
95
  all_new_inner_polylines.push_back(inner_polyline);
97
96
  }
98
97
  }
99
98
  for (auto s : cursor.orphan_inners()) {
100
99
  new_inners.push_back(s);
101
100
  }
102
- Polyline* polyline = tile->shapes_pool->acquire_polyline(tile, new_outer->to_vector(), std::nullopt);
101
+ shape->clear_inner();
102
+
103
+ Polyline* polyline = tile->shapes_pool->acquire_polyline(tile, new_outer.to_vector(), std::nullopt);
103
104
  Shape* inserting_new_shape = tile->shapes_pool->acquire_shape(polyline, new_inners);
104
105
  new_shapes.push_back(inserting_new_shape);
105
106
  polyline->shape = inserting_new_shape;
106
107
 
107
108
  for (InnerPolyline* inner_polyline : new_inner_polylines) {
108
- inner_polyline->sequence()->shape = inserting_new_shape;
109
+ inner_polyline->shape = inserting_new_shape;
109
110
  }
110
111
  if (treemap) {
111
112
  for (const auto merged_shape : cursor.shapes_sequence()) {
@@ -116,6 +117,9 @@ Tile* Cluster::merge_tiles() {
116
117
  assign_ancestry(inserting_new_shape, inside_inner_polyline);
117
118
  }
118
119
  }
120
+ for (const auto merged_shape : cursor.shapes_sequence()) {
121
+ detach_shapes.push_back(merged_shape);
122
+ }
119
123
  } else {
120
124
  if (treemap) {
121
125
  if (shape->fixed) {
@@ -134,7 +138,6 @@ Tile* Cluster::merge_tiles() {
134
138
 
135
139
  double past_tot_outer = tiles_.front()->benchmarks.outer + tiles_.back()->benchmarks.outer;
136
140
  double past_tot_inner = tiles_.front()->benchmarks.inner + tiles_.back()->benchmarks.inner;
137
-
138
141
  Benchmarks b{
139
142
  tot_outer + past_tot_outer,
140
143
  tot_inner + past_tot_inner
@@ -144,6 +147,9 @@ Tile* Cluster::merge_tiles() {
144
147
  this->finder_, tiles_.front()->start_x(), tiles_.back()->end_x(), tiles_.front()->name() + tiles_.back()->name(), b);
145
148
 
146
149
  tile->assign_shapes(new_shapes);
150
+ for (const auto shape : detach_shapes) {
151
+ shape->detach_from_pool();
152
+ }
147
153
  for (Tile* old_tile : tiles_) {
148
154
  tile->adopt(old_tile);
149
155
  }
@@ -151,7 +157,7 @@ Tile* Cluster::merge_tiles() {
151
157
  }
152
158
 
153
159
  void Cluster::assign_ancestry(Shape *shape, InnerPolyline* inner_polyline)
154
- { shape->set_parent_shape(inner_polyline->sequence()->shape);
160
+ { shape->set_parent_shape(inner_polyline->shape);
155
161
  shape->parent_inner_polyline = inner_polyline;
156
162
  shape->fixed = true;
157
163
  }
@@ -25,7 +25,7 @@ class Cluster {
25
25
  void is_children(Shape* shape, std::vector<InnerPolyline*> inner_polylines);
26
26
 
27
27
  public:
28
- Cluster(Finder *finder, int height, int start_x, int end_x);
28
+ Cluster(Finder *finder, int height);
29
29
  virtual ~Cluster();
30
30
  void add(Tile* tile);
31
31
  Tile* merge_tiles();
@@ -18,20 +18,20 @@ Cursor::Cursor(Cluster& cluster, Shape* shape)
18
18
  : cluster(cluster), shape(shape) {
19
19
  }
20
20
 
21
- Sequence* Cursor::join_outers()
21
+ Sequence Cursor::join_outers()
22
22
  { Polyline* outer_polyline = shape->outer_polyline;
23
23
  this->shapes_sequence_.push_back(this->shape);
24
24
  this->shapes_sequence_lookup.insert(this->shape);
25
- Sequence* outer_joined_polyline = outer_polyline->tile->shapes_pool->acquire_sequence();
25
+ Sequence outer_joined_polyline;
26
26
  std::vector<Part*> all_parts;
27
27
  this->traverse_outer(outer_polyline->parts().front(),
28
28
  all_parts,
29
29
  this->shapes_sequence_,
30
30
  outer_joined_polyline);
31
31
 
32
- if (outer_joined_polyline->head->payload == outer_joined_polyline->tail->payload &&
32
+ if (outer_joined_polyline.head->payload == outer_joined_polyline.tail->payload &&
33
33
  this->cluster.tiles().front()->left() &&
34
- this->cluster.tiles().back()->right()) outer_joined_polyline->pop();
34
+ this->cluster.tiles().back()->right()) outer_joined_polyline.pop();
35
35
 
36
36
  for (Shape* shape : shapes_sequence_) {
37
37
  shape->outer_polyline->turn_on(Polyline::TRACKED_OUTER);
@@ -50,7 +50,7 @@ Sequence* Cursor::join_outers()
50
50
  void Cursor::traverse_outer(Part* act_part,
51
51
  std::vector<Part*>& all_parts,
52
52
  std::vector<Shape*>& shapes_sequence,
53
- Sequence* outer_joined_polyline) {
53
+ Sequence& outer_joined_polyline) {
54
54
  while (act_part != nullptr) {
55
55
  Part* last_part = (!all_parts.empty()) ? all_parts.back() : nullptr;
56
56
  if (all_parts.empty() || last_part != act_part) {
@@ -61,7 +61,7 @@ void Cursor::traverse_outer(Part* act_part,
61
61
  if (act_part->size == 0) return;
62
62
 
63
63
  while (Position *position = act_part->next_position(nullptr)) {
64
- outer_joined_polyline->add(position);
64
+ outer_joined_polyline.add(position);
65
65
  }
66
66
  } else {
67
67
  if (act_part->dead_end &&
@@ -69,13 +69,13 @@ void Cursor::traverse_outer(Part* act_part,
69
69
  last_part->is(Part::SEAM) &&
70
70
  last_part->polyline() == act_part->polyline()) return;
71
71
  while (Position *new_position = static_cast<Position*>(act_part->iterator())) {
72
- if (outer_joined_polyline->size > 1 &&
73
- outer_joined_polyline->head->payload == new_position->payload &&
72
+ if (outer_joined_polyline.size > 1 &&
73
+ outer_joined_polyline.head->payload == new_position->payload &&
74
74
  act_part == all_parts.front()) {
75
75
  return;
76
76
  }
77
77
  this->cluster.positions_pool.emplace_back(this->cluster.hub(), new_position->payload);
78
- outer_joined_polyline->add(&this->cluster.positions_pool.back());
78
+ outer_joined_polyline.add(&this->cluster.positions_pool.back());
79
79
  new_position->end_point()->tracked_outer = true;
80
80
  int versus = act_part->versus();
81
81
  auto& q_set = new_position->end_point()->queues();
@@ -125,7 +125,7 @@ void Cursor::traverse_outer(Part* act_part,
125
125
  }
126
126
  }
127
127
 
128
- std::vector<InnerPolyline*> Cursor::join_inners(Sequence* outer_seq, bool treemap) {
128
+ std::vector<InnerPolyline*> Cursor::join_inners(bool treemap) {
129
129
  std::vector<InnerPolyline*> return_inner_polylines;
130
130
  std::vector<Shape*> processing_queue = shapes_sequence_;
131
131
  for (size_t i = 0; i < shapes_sequence_.size(); ++i)
@@ -140,10 +140,10 @@ std::vector<InnerPolyline*> Cursor::join_inners(Sequence* outer_seq, bool treema
140
140
  .max = 0
141
141
  };
142
142
  traverse_inner(part, all_parts, bounds, tracked_end_points);
143
- Sequence* retme_sequence = shape->outer_polyline->tile->shapes_pool->acquire_sequence();
143
+ Sequence retme_sequence;
144
144
  for (Part* part : all_parts)
145
145
  { part->touch();
146
- retme_sequence->move_from(*part, [&](QNode<Point>* pos) -> bool {
146
+ retme_sequence.move_from(*part, [&](QNode<Point>* pos) -> bool {
147
147
  Position *position = static_cast<Position*>(pos);
148
148
  if (part->is(Part::ADDED) &&
149
149
  !(position->payload.y >= bounds.min &&
@@ -153,8 +153,8 @@ std::vector<InnerPolyline*> Cursor::join_inners(Sequence* outer_seq, bool treema
153
153
  return(!(polyline->tile->tg_border(position->payload) && position->end_point()->tracked_outer));
154
154
  });
155
155
  }
156
- if (retme_sequence->is_not_vertical()) {
157
- InnerPolyline* inner_polyline = polyline->tile->shapes_pool->acquire_inner_polyline(retme_sequence);
156
+ if (retme_sequence.is_not_vertical()) {
157
+ InnerPolyline* inner_polyline = polyline->tile->shapes_pool->acquire_inner_polyline(&retme_sequence);
158
158
  return_inner_polylines.push_back(inner_polyline);
159
159
  if (treemap) {
160
160
  mark_children(tracked_end_points, polyline, inner_polyline);
@@ -201,7 +201,7 @@ void Cursor::traverse_inner(Part* act_part, std::vector<Part*>& all_parts, Bound
201
201
  if (act_part->innerable()) {
202
202
  all_parts.push_back(act_part);
203
203
  bool jumped = false;
204
- while (act_part = act_part->circular_next) {
204
+ while ((act_part = act_part->circular_next)) {
205
205
  if (act_part->innerable()) {
206
206
  all_parts.push_back(act_part);
207
207
  } else {
@@ -233,6 +233,7 @@ void Cursor::traverse_inner(Part* act_part, std::vector<Part*>& all_parts, Bound
233
233
  orphan_inners_.end(),
234
234
  shape->inner_polylines.begin(),
235
235
  shape->inner_polylines.end());
236
+ shape->clear_inner();
236
237
  }
237
238
  dest_part->polyline()->turn_on(Polyline::TRACKED_OUTER);
238
239
  if (!dest_part->touched()) {
@@ -20,8 +20,8 @@
20
20
  class Cursor {
21
21
  public:
22
22
  Cursor(Cluster& cluster, Shape* shape);
23
- Sequence* join_outers();
24
- std::vector<InnerPolyline*> join_inners(Sequence* outer_seq, bool treemap);
23
+ Sequence join_outers();
24
+ std::vector<InnerPolyline*> join_inners(bool treemap);
25
25
  std::list<InnerPolyline*> orphan_inners() { return orphan_inners_; }
26
26
  const std::vector<Shape*>& shapes_sequence() const { return shapes_sequence_; }
27
27
 
@@ -34,7 +34,7 @@ class Cursor {
34
34
  void traverse_outer(Part* act_part,
35
35
  std::vector<Part*>& all_parts,
36
36
  std::vector<Shape*>& shapes_sequence,
37
- Sequence* outer_joined_polyline);
37
+ Sequence& outer_joined_polyline);
38
38
  void traverse_inner(Part* act_part, std::vector<Part*> &all_parts, Bounds& bounds, std::vector<EndPoint*>& tracked_end_points);
39
39
  std::vector<Shape*> connect_missings(std::vector<Shape*> shapes_sequence, std::vector<Shape*> missing_shapes);
40
40
  void mark_children(std::vector<EndPoint*>& end_points, const Polyline* outer_polyline, InnerPolyline* inner_polyline);
@@ -95,16 +95,8 @@ void Finder::process_tiles() {
95
95
 
96
96
  if (it != arriving_tiles.end()) {
97
97
  Tile* twin_tile = *it;
98
- int start_x, end_x;
99
- if (twin_tile->start_x() == (tile->end_x() - 1)) {
100
- start_x = tile->start_x();
101
- end_x = twin_tile->end_x();
102
- } else {
103
- start_x = twin_tile->start_x();
104
- end_x = tile->end_x();
105
- }
106
98
 
107
- Cluster *cluster = new Cluster(this, this->height, start_x, end_x);
99
+ Cluster *cluster = new Cluster(this, this->height);
108
100
 
109
101
  if (twin_tile->start_x() == (tile->end_x() - 1)) {
110
102
  cluster->add(tile);
@@ -0,0 +1,81 @@
1
+ /*
2
+ * GeoJsonStreamingMerger.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
+ #include <vector>
12
+ #include <string>
13
+ #include "StreamingMerger.h"
14
+
15
+ class GeoJsonStreamingMerger : public StreamingMerger {
16
+ private:
17
+ unsigned int target_value;
18
+ bool is_first_feature = true;
19
+
20
+ protected:
21
+ void write_header() override {
22
+ if (stream) {
23
+ *stream << "{\"type\":\"FeatureCollection\",\"features\":[\n";
24
+ }
25
+ }
26
+
27
+ void write_footer() override {
28
+ if (stream) {
29
+ *stream << "\n]}";
30
+ }
31
+ }
32
+ void write_outer_polygon_start() override {}
33
+ void write_outer_polygon_end() override {}
34
+ void write_inner_polygon_start() override {}
35
+ void write_inner_polygon_end() override {}
36
+
37
+ public:
38
+ GeoJsonStreamingMerger(int number_of_threads,
39
+ std::vector<std::string>* options,
40
+ std::ofstream* stream_to,
41
+ int total_width, int total_height,
42
+ unsigned int pixel_value)
43
+ : StreamingMerger(number_of_threads, options, stream_to, total_width, total_height),
44
+ target_value(pixel_value) {}
45
+
46
+ void stream_raw_polygon(const Shape* shape) override {
47
+ if (!stream) return;
48
+ if (!is_first_feature) {
49
+ *stream << ",\n";
50
+ }
51
+ is_first_feature = false;
52
+
53
+ *stream << "{\"type\":\"Feature\",\"properties\":{\"value\":" << target_value
54
+ << "},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[ [";
55
+ const std::vector<Point>& points = shape->outer_polyline->raw();
56
+ const size_t points_size = points.size();
57
+ if (points_size > 0) {
58
+ for (size_t i = 0; i < points_size; ++i) {
59
+ *stream << "[" << points[i].y << "," << points[i].x << "]";
60
+ if (i < points_size - 1) *stream << ",";
61
+ }
62
+ *stream << ",[" << points[0].y << "," << points[0].x << "]]";
63
+ } else {
64
+ *stream << "]]";
65
+ }
66
+
67
+ for (const auto& inner_polyline : shape->inner_polylines) {
68
+ const std::vector<Point>& inner_points = inner_polyline->raw();
69
+ const size_t inner_size = inner_points.size();
70
+ if (inner_size > 0) {
71
+ *stream << ",[";
72
+ for (size_t i = 0; i < inner_size; ++i) {
73
+ *stream << "[" << inner_points[i].y << "," << inner_points[i].x << "]";
74
+ if (i < inner_size - 1) *stream << ",";
75
+ }
76
+ *stream << ",[" << inner_points[0].y << "," << inner_points[0].x << "]]";
77
+ }
78
+ }
79
+ *stream << "]}}";
80
+ }
81
+ };
@@ -11,36 +11,18 @@
11
11
  #include <vector>
12
12
  #include "InnerPolyline.h"
13
13
 
14
- InnerPolyline::InnerPolyline(std::vector<Point> raw_coordinates, Shape* shape)
15
- : raw_coordinates_(std::move(raw_coordinates)),
16
- shape_(shape) {}
17
- InnerPolyline::InnerPolyline(Sequence* sequence)
18
- : sequence_(sequence) {
14
+ InnerPolyline::InnerPolyline(ShapePool* shape_pool, std::vector<Point> raw_coordinates, Shape* shape)
15
+ : shape(shape),
16
+ shape_pool(shape_pool),
17
+ raw_coordinates_(std::move(raw_coordinates)) {}
18
+ InnerPolyline::InnerPolyline(ShapePool* shape_pool, Sequence* sequence)
19
+ : shape_pool(shape_pool) {
19
20
  this->raw_coordinates_ = sequence->to_vector();
21
+ this->shape = sequence->shape;
20
22
  }
21
23
 
22
- std::vector<Point>& InnerPolyline::raw() {
23
- return this->raw_coordinates_;
24
- }
25
-
26
- Bounds& InnerPolyline::vertical_bounds() {
27
- if (sequence_ != nullptr) {
28
- return(sequence_->vertical_bounds);
29
- } else {
30
- return(this->raw_vertical_bounds());
31
- }
32
- }
33
-
34
- Shape* InnerPolyline::shape() {
35
- if (sequence_ != nullptr) {
36
- return(sequence_->shape);
37
- } else {
38
- return(this->shape_ ? this->shape_ : this->assigned_shape);
39
- }
40
- }
41
-
42
- Bounds& InnerPolyline::raw_vertical_bounds() {
43
- if (!raw_coordinates_.empty()) {
24
+ void InnerPolyline::compute_vertical_bounds()
25
+ { if (!raw_coordinates_.empty()) {
44
26
  int min_y = raw_coordinates_[0].y;
45
27
  int max_y = raw_coordinates_[0].y;
46
28
  for (const auto& pos : raw_coordinates_) {
@@ -50,5 +32,4 @@ Bounds& InnerPolyline::raw_vertical_bounds() {
50
32
  vertical_bounds_.min = min_y;
51
33
  vertical_bounds_.max = max_y;
52
34
  }
53
- return(vertical_bounds_);
54
35
  }