tg_geometry 0.1.0

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +103 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +385 -0
  6. data/Rakefile +129 -0
  7. data/benchmark/_support.rb +115 -0
  8. data/benchmark/batch_packed_vs_loop.rb +27 -0
  9. data/benchmark/falcon_concurrency.rb +25 -0
  10. data/benchmark/flat_vs_rtree.rb +27 -0
  11. data/benchmark/gvl_threshold.rb +41 -0
  12. data/benchmark/objectspace_memsize.rb +17 -0
  13. data/benchmark/parse_throughput.rb +38 -0
  14. data/benchmark/rss_stability.rb +70 -0
  15. data/docs/ACTIVE_RECORD.md +26 -0
  16. data/docs/ARCHITECTURE.md +130 -0
  17. data/docs/AUTO_STRATEGY.md +15 -0
  18. data/docs/BENCHMARKING.md +75 -0
  19. data/docs/CASUAL_EXAMPLE.md +618 -0
  20. data/docs/CONCURRENCY.md +65 -0
  21. data/docs/ERROR_HANDLING.md +55 -0
  22. data/docs/EXPANSION_E_TO_H_STATUS.md +51 -0
  23. data/docs/FORMAT_COVERAGE.md +23 -0
  24. data/docs/FULL_TG_API_COVERAGE.md +109 -0
  25. data/docs/LIMITATIONS.md +61 -0
  26. data/docs/LOW_LEVEL_GEOMETRY.md +121 -0
  27. data/docs/MEMORY_OWNERSHIP.md +94 -0
  28. data/docs/RACTOR.md +40 -0
  29. data/docs/REGISTRY.md +37 -0
  30. data/docs/RELEASE_CHECKLIST.md +39 -0
  31. data/ext/tg_geometry/extconf.rb +91 -0
  32. data/ext/tg_geometry/tg_geometry_ext.c +3054 -0
  33. data/ext/tg_geometry/tg_geometry_vendor_rtree.c +1 -0
  34. data/ext/tg_geometry/tg_geometry_vendor_tg.c +24 -0
  35. data/ext/tg_geometry/vendor/.vendored +16 -0
  36. data/ext/tg_geometry/vendor/rtree/LICENSE +20 -0
  37. data/ext/tg_geometry/vendor/rtree/README.md +202 -0
  38. data/ext/tg_geometry/vendor/rtree/VERSION +3 -0
  39. data/ext/tg_geometry/vendor/rtree/rtree.c +840 -0
  40. data/ext/tg_geometry/vendor/rtree/rtree.h +105 -0
  41. data/ext/tg_geometry/vendor/tg/LICENSE +19 -0
  42. data/ext/tg_geometry/vendor/tg/README.md +197 -0
  43. data/ext/tg_geometry/vendor/tg/VERSION +3 -0
  44. data/ext/tg_geometry/vendor/tg/tg.c +16010 -0
  45. data/ext/tg_geometry/vendor/tg/tg.h +359 -0
  46. data/lib/tg/geometry/active_record_source.rb +57 -0
  47. data/lib/tg/geometry/registry.rb +119 -0
  48. data/lib/tg/geometry/version.rb +7 -0
  49. data/lib/tg/geometry.rb +6 -0
  50. data/lib/tg_geometry.rb +3 -0
  51. data/script/vendor_libs.rb +264 -0
  52. data/spec/block_10_rtree_strategy_spec.rb +82 -0
  53. data/spec/block_11_rtree_order_spec.rb +53 -0
  54. data/spec/block_12_batch_packed_spec.rb +55 -0
  55. data/spec/block_13_error_hardening_spec.rb +65 -0
  56. data/spec/block_14_memory_gc_hardening_spec.rb +116 -0
  57. data/spec/block_1_skeleton_spec.rb +45 -0
  58. data/spec/block_20_concurrency_spec.rb +157 -0
  59. data/spec/block_20_fuzz_spec.rb +145 -0
  60. data/spec/block_2_vendor_spec.rb +79 -0
  61. data/spec/block_3_geom_parse_spec.rb +89 -0
  62. data/spec/block_4_geom_api_spec.rb +90 -0
  63. data/spec/block_5_rect_api_spec.rb +96 -0
  64. data/spec/block_6_index_build_spec.rb +111 -0
  65. data/spec/block_7_index_owned_geometry_spec.rb +143 -0
  66. data/spec/block_8_index_borrowed_geometry_spec.rb +106 -0
  67. data/spec/block_9_flat_query_spec.rb +65 -0
  68. data/spec/expansion_a_auto_strategy_spec.rb +14 -0
  69. data/spec/expansion_b_registry_spec.rb +47 -0
  70. data/spec/expansion_c_active_record_source_spec.rb +42 -0
  71. data/spec/expansion_d_format_coverage_spec.rb +30 -0
  72. data/spec/expansion_e_low_level_geometry_spec.rb +82 -0
  73. data/spec/expansion_i_ractor_spec.rb +25 -0
  74. data/spec/expansion_j_full_tg_api_coverage_spec.rb +114 -0
  75. data/spec/spec_helper.rb +15 -0
  76. metadata +157 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Release Core Block 6 Index build skeleton" do
6
+ let(:geom) { TG::Geometry.parse_wkt("POINT (1 2)") }
7
+
8
+ it "builds an empty immutable index" do
9
+ index = TG::Geometry::Index.build([], via: :geojson, strategy: :flat)
10
+
11
+ expect(index).to be_a(TG::Geometry::Index)
12
+ expect(index).to be_frozen
13
+ expect(index.size).to eq(0)
14
+ expect(index.strategy).to eq(:flat)
15
+ expect(index.predicate).to eq(:covers)
16
+ expect(index.bbox).to be_nil
17
+ end
18
+
19
+ it "requires entries to be an Array" do
20
+ expect { TG::Geometry::Index.build("bad", via: :geojson, strategy: :flat) }
21
+ .to raise_error(TypeError)
22
+ end
23
+
24
+ it "requires each entry to be a two-element Array" do
25
+ expect { TG::Geometry::Index.build([:bad], via: :geojson, strategy: :flat) }
26
+ .to raise_error(TypeError)
27
+
28
+ expect { TG::Geometry::Index.build([[1, "{}", :extra]], via: :geojson, strategy: :flat) }
29
+ .to raise_error(TG::Geometry::ArgumentError)
30
+ end
31
+
32
+ it "rejects nil ids" do
33
+ expect { TG::Geometry::Index.build([[nil, "{}"]], via: :geojson, strategy: :flat) }
34
+ .to raise_error(TG::Geometry::ArgumentError, /id cannot be nil/)
35
+ end
36
+
37
+ it "allows false ids and duplicate ids" do
38
+ index = TG::Geometry::Index.build([[false, geom], [:same, geom], [:same, geom]],
39
+ via: :geom,
40
+ strategy: :flat)
41
+
42
+ expect(index.size).to eq(3)
43
+ expect(index.bbox).to be_a(TG::Geometry::Rect)
44
+ end
45
+
46
+ it "validates via, strategy, predicate, and geometry_index symbols" do
47
+ expect { TG::Geometry::Index.build([], via: :shape, strategy: :flat) }
48
+ .to raise_error(TG::Geometry::ArgumentError)
49
+ expect { TG::Geometry::Index.build([], via: :geojson, strategy: :bogus) }
50
+ .to raise_error(TG::Geometry::ArgumentError)
51
+ expect { TG::Geometry::Index.build([], via: :geojson, strategy: :flat, predicate: :touches) }
52
+ .to raise_error(TG::Geometry::ArgumentError)
53
+ expect { TG::Geometry::Index.build([], via: :geojson, strategy: :flat, geometry_index: :packed) }
54
+ .to raise_error(TG::Geometry::ArgumentError)
55
+ end
56
+
57
+ it "requires String values for owned geometry modes in the block 6 skeleton" do
58
+ expect { TG::Geometry::Index.build([[1, Object.new]], via: :geojson, strategy: :flat) }
59
+ .to raise_error(TypeError)
60
+ expect { TG::Geometry::Index.build([[1, Object.new]], via: :wkb, strategy: :flat) }
61
+ .to raise_error(TypeError)
62
+ end
63
+
64
+ it "requires Geom values for via: :geom" do
65
+ expect { TG::Geometry::Index.build([[1, "not geom"]], via: :geom, strategy: :flat) }
66
+ .to raise_error(TypeError)
67
+ end
68
+
69
+ it "cleans up immediately after failed build validation" do
70
+ entries = [[1, geom], [nil, geom], [2, geom]]
71
+
72
+ expect { TG::Geometry::Index.build(entries, via: :geom, strategy: :flat) }
73
+ .to raise_error(TG::Geometry::ArgumentError)
74
+
75
+ GC.start
76
+ GC.compact if GC.respond_to?(:compact)
77
+ end
78
+
79
+ it "survives GC.stress build/free lifecycle" do
80
+ old_stress = GC.stress
81
+ GC.stress = true
82
+
83
+ 10.times do
84
+ index = TG::Geometry::Index.build([[false, geom], [:dup, geom], [:dup, geom]],
85
+ via: :geom,
86
+ strategy: :rtree,
87
+ predicate: :contains,
88
+ geometry_index: :ystripes)
89
+ expect(index).to be_frozen
90
+ expect(index.size).to eq(3)
91
+ expect(index.strategy).to eq(:rtree)
92
+ expect(index.predicate).to eq(:contains)
93
+ end
94
+ ensure
95
+ GC.stress = old_stress
96
+ end
97
+
98
+ it "survives GC.compact build/free lifecycle" do
99
+ index = TG::Geometry::Index.build([["id", geom]], via: :geom, strategy: :flat)
100
+
101
+ GC.start
102
+ GC.compact if GC.respond_to?(:compact)
103
+
104
+ expect(index.size).to eq(1)
105
+ expect(index.bbox).to be_a(TG::Geometry::Rect)
106
+ end
107
+
108
+ it "keeps public allocate disabled" do
109
+ expect { TG::Geometry::Index.allocate }.to raise_error(TypeError)
110
+ end
111
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "objspace"
5
+
6
+ RSpec.describe "Release Core Block 7 Index owned geometry ingestion" do
7
+ let(:geojson_a) do
8
+ '{"type":"Polygon","coordinates":[[[0,0],[2,0],[2,2],[0,2],[0,0]]]}'
9
+ end
10
+
11
+ let(:geojson_b) do
12
+ '{"type":"Polygon","coordinates":[[[10,10],[12,10],[12,12],[10,12],[10,10]]]}'
13
+ end
14
+
15
+ let(:geojson_c) do
16
+ '{"type":"Polygon","coordinates":[[[-5,-4],[-2,-4],[-2,-1],[-5,-1],[-5,-4]]]}'
17
+ end
18
+
19
+ let(:wkb_a) { TG::Geometry.parse_geojson(geojson_a).to_wkb }
20
+ let(:wkb_b) { TG::Geometry.parse_geojson(geojson_b).to_wkb }
21
+ let(:wkb_c) { TG::Geometry.parse_geojson(geojson_c).to_wkb }
22
+
23
+ it "builds an immutable index via: :geojson and computes the union bbox" do
24
+ index = TG::Geometry::Index.build([[:a, geojson_a], [:b, geojson_b], [:c, geojson_c]],
25
+ via: :geojson,
26
+ strategy: :flat)
27
+
28
+ expect(index).to be_frozen
29
+ expect(index.size).to eq(3)
30
+ expect(index.strategy).to eq(:flat)
31
+ expect(index.predicate).to eq(:covers)
32
+
33
+ bbox = index.bbox
34
+ expect(bbox.min_x).to eq(-5.0)
35
+ expect(bbox.min_y).to eq(-4.0)
36
+ expect(bbox.max_x).to eq(12.0)
37
+ expect(bbox.max_y).to eq(12.0)
38
+ end
39
+
40
+ it "builds an immutable index via: :wkb and treats input strings as bytes" do
41
+ index = TG::Geometry::Index.build([[:a, wkb_a], [:b, wkb_b], [:c, wkb_c]],
42
+ via: :wkb,
43
+ strategy: :flat,
44
+ geometry_index: :natural)
45
+
46
+ expect(index).to be_frozen
47
+ expect(index.size).to eq(3)
48
+
49
+ bbox = index.bbox
50
+ expect(bbox.min_x).to eq(-5.0)
51
+ expect(bbox.min_y).to eq(-4.0)
52
+ expect(bbox.max_x).to eq(12.0)
53
+ expect(bbox.max_y).to eq(12.0)
54
+ end
55
+
56
+ it "raises ParseError for malformed geojson at first, middle, and last positions" do
57
+ malformed = "not geojson"
58
+
59
+ expect do
60
+ TG::Geometry::Index.build([[1, malformed], [2, geojson_a], [3, geojson_b]],
61
+ via: :geojson,
62
+ strategy: :flat)
63
+ end.to raise_error(TG::Geometry::ParseError)
64
+
65
+ expect do
66
+ TG::Geometry::Index.build([[1, geojson_a], [2, malformed], [3, geojson_b]],
67
+ via: :geojson,
68
+ strategy: :flat)
69
+ end.to raise_error(TG::Geometry::ParseError)
70
+
71
+ expect do
72
+ TG::Geometry::Index.build([[1, geojson_a], [2, geojson_b], [3, malformed]],
73
+ via: :geojson,
74
+ strategy: :flat)
75
+ end.to raise_error(TG::Geometry::ParseError)
76
+
77
+ GC.start
78
+ GC.compact if GC.respond_to?(:compact)
79
+ end
80
+
81
+ it "raises ParseError for malformed wkb at first, middle, and last positions" do
82
+ malformed = "not wkb".b
83
+
84
+ expect do
85
+ TG::Geometry::Index.build([[1, malformed], [2, wkb_a], [3, wkb_b]],
86
+ via: :wkb,
87
+ strategy: :flat)
88
+ end.to raise_error(TG::Geometry::ParseError)
89
+
90
+ expect do
91
+ TG::Geometry::Index.build([[1, wkb_a], [2, malformed], [3, wkb_b]],
92
+ via: :wkb,
93
+ strategy: :flat)
94
+ end.to raise_error(TG::Geometry::ParseError)
95
+
96
+ expect do
97
+ TG::Geometry::Index.build([[1, wkb_a], [2, wkb_b], [3, malformed]],
98
+ via: :wkb,
99
+ strategy: :flat)
100
+ end.to raise_error(TG::Geometry::ParseError)
101
+
102
+ GC.start
103
+ GC.compact if GC.respond_to?(:compact)
104
+ end
105
+
106
+ it "disposes already parsed owned geometries after partial build failure" do
107
+ 20.times do
108
+ expect do
109
+ TG::Geometry::Index.build([[1, geojson_a], [2, geojson_b], [3, "bad geojson"]],
110
+ via: :geojson,
111
+ strategy: :flat)
112
+ end.to raise_error(TG::Geometry::ParseError)
113
+ end
114
+
115
+ GC.start
116
+ GC.compact if GC.respond_to?(:compact)
117
+ end
118
+
119
+ it "reports owned geometry native memory through ObjectSpace.memsize_of" do
120
+ borrowed_geom = TG::Geometry.parse_geojson(geojson_a)
121
+ borrowed = TG::Geometry::Index.build([[:a, borrowed_geom]], via: :geom, strategy: :flat)
122
+ owned = TG::Geometry::Index.build([[:a, geojson_a]], via: :geojson, strategy: :flat)
123
+
124
+ expect(ObjectSpace.memsize_of(owned)).to be > ObjectSpace.memsize_of(borrowed)
125
+ end
126
+
127
+ it "survives GC.stress and GC.compact for owned geometry build/free" do
128
+ old_stress = GC.stress
129
+ GC.stress = true
130
+
131
+ 5.times do
132
+ index = TG::Geometry::Index.build([[:a, geojson_a], [:b, geojson_b]],
133
+ via: :geojson,
134
+ strategy: :flat)
135
+ expect(index.size).to eq(2)
136
+ expect(index.bbox).to be_a(TG::Geometry::Rect)
137
+ end
138
+ ensure
139
+ GC.stress = old_stress
140
+ GC.start
141
+ GC.compact if GC.respond_to?(:compact)
142
+ end
143
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "json"
5
+ require "objspace"
6
+ require "weakref"
7
+
8
+ RSpec.describe "Release Core Block 8 Index borrowed geometry ingestion" do
9
+ let(:polygon_a_wkt) { "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))" }
10
+ let(:polygon_b_wkt) { "POLYGON ((10 10, 12 10, 12 12, 10 12, 10 10))" }
11
+
12
+ it "builds an immutable index via: :geom using borrowed TG::Geometry::Geom objects" do
13
+ geom_a = TG::Geometry.parse_wkt(polygon_a_wkt)
14
+ geom_b = TG::Geometry.parse_wkt(polygon_b_wkt)
15
+
16
+ index = TG::Geometry::Index.build([[:a, geom_a], [:b, geom_b]],
17
+ via: :geom,
18
+ strategy: :flat)
19
+
20
+ expect(index).to be_a(TG::Geometry::Index)
21
+ expect(index).to be_frozen
22
+ expect(index.size).to eq(2)
23
+ expect(index.strategy).to eq(:flat)
24
+ expect(index.predicate).to eq(:covers)
25
+
26
+ bbox = index.bbox
27
+ expect(bbox.min_x).to eq(0.0)
28
+ expect(bbox.min_y).to eq(0.0)
29
+ expect(bbox.max_x).to eq(12.0)
30
+ expect(bbox.max_y).to eq(12.0)
31
+ end
32
+
33
+ it "keeps borrowed geometry owners alive after caller drops local references" do
34
+ geom = TG::Geometry.parse_wkt(polygon_a_wkt)
35
+ weak_geom = WeakRef.new(geom)
36
+
37
+ index = TG::Geometry::Index.build([[:zone, geom]], via: :geom, strategy: :flat)
38
+ geom = nil
39
+
40
+ GC.start(full_mark: true, immediate_sweep: true)
41
+ GC.compact if GC.respond_to?(:compact)
42
+ GC.start(full_mark: true, immediate_sweep: true)
43
+
44
+ expect(weak_geom.weakref_alive?).to be(true)
45
+ expect(index.bbox.max_x).to eq(2.0)
46
+ end
47
+
48
+ it "allows the borrowed geometry wrapper to be collected only after the index is released" do
49
+ geom = TG::Geometry.parse_wkt(polygon_a_wkt)
50
+ weak_geom = WeakRef.new(geom)
51
+
52
+ index = TG::Geometry::Index.build([[:zone, geom]], via: :geom, strategy: :flat)
53
+ geom = nil
54
+
55
+ GC.start(full_mark: true, immediate_sweep: true)
56
+ GC.compact if GC.respond_to?(:compact)
57
+ expect(weak_geom.weakref_alive?).to be(true)
58
+
59
+ index = nil
60
+ 5.times do
61
+ GC.start(full_mark: true, immediate_sweep: true)
62
+ GC.compact if GC.respond_to?(:compact)
63
+ end
64
+
65
+ expect(weak_geom.weakref_alive?).to be_falsey
66
+ end
67
+
68
+ it "does not free borrowed native geometry when the index is disposed" do
69
+ geom = TG::Geometry.parse_wkt(polygon_a_wkt)
70
+
71
+ index = TG::Geometry::Index.build([[:zone, geom]], via: :geom, strategy: :flat)
72
+ expect(index.size).to eq(1)
73
+
74
+ index = nil
75
+ GC.start(full_mark: true, immediate_sweep: true)
76
+ GC.compact if GC.respond_to?(:compact)
77
+
78
+ expect(geom.bbox.max_x).to eq(2.0)
79
+ expect(geom.covers_xy?(1.0, 1.0)).to be(true)
80
+ end
81
+
82
+ it "does not count borrowed native geometry bytes in Index ObjectSpace.memsize_of" do
83
+ point_count = 1_000
84
+ coordinates = Array.new(point_count) do |i|
85
+ angle = (2.0 * Math::PI * i) / point_count
86
+ [Math.cos(angle), Math.sin(angle)]
87
+ end
88
+ coordinates << coordinates.first
89
+
90
+ geojson = JSON.generate(type: "Polygon", coordinates: [coordinates])
91
+ geom = TG::Geometry.parse_geojson(geojson)
92
+
93
+ borrowed_index = TG::Geometry::Index.build([[:large, geom]], via: :geom, strategy: :flat)
94
+ owned_index = TG::Geometry::Index.build([[:large, geojson]], via: :geojson, strategy: :flat)
95
+
96
+ expect(ObjectSpace.memsize_of(geom)).to be > 10_000
97
+ expect(ObjectSpace.memsize_of(borrowed_index)).to be < ObjectSpace.memsize_of(geom)
98
+ expect(ObjectSpace.memsize_of(owned_index)).to be > ObjectSpace.memsize_of(borrowed_index)
99
+ end
100
+
101
+ it "requires TG::Geometry::Geom values for via: :geom" do
102
+ expect do
103
+ TG::Geometry::Index.build([[:bad, polygon_a_wkt]], via: :geom, strategy: :flat)
104
+ end.to raise_error(TypeError)
105
+ end
106
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Release Core Block 9 flat query engine" do
6
+ let(:zone_a) { TG::Geometry.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") }
7
+ let(:zone_b) { TG::Geometry.parse_wkt("POLYGON ((5 5, 12 5, 12 12, 5 12, 5 5))") }
8
+ let(:zone_c) { TG::Geometry.parse_wkt("POLYGON ((20 20, 22 20, 22 22, 20 22, 20 20))") }
9
+
10
+ it "finds the first covering id by insertion order" do
11
+ index = TG::Geometry::Index.build([[:a, zone_a], [:b, zone_b]], via: :geom, strategy: :flat)
12
+
13
+ expect(index.find_covering(6, 6)).to eq(:a)
14
+ expect(index.find_covering(11, 11)).to eq(:b)
15
+ expect(index.find_covering(100, 100)).to be_nil
16
+ end
17
+
18
+ it "returns all covering ids in insertion order, including duplicates and false ids" do
19
+ index = TG::Geometry::Index.build([[false, zone_a], [:dup, zone_a], [:dup, zone_b]],
20
+ via: :geom,
21
+ strategy: :flat)
22
+
23
+ expect(index.covering_ids(6, 6)).to eq([false, :dup, :dup])
24
+ expect(index.covering_ids(1, 1)).to eq([false, :dup])
25
+ end
26
+
27
+ it "distinguishes :covers from strict :contains on boundaries" do
28
+ covers_index = TG::Geometry::Index.build([[:zone, zone_a]], via: :geom, strategy: :flat)
29
+ contains_index = TG::Geometry::Index.build([[:zone, zone_a]],
30
+ via: :geom,
31
+ strategy: :flat,
32
+ predicate: :contains)
33
+
34
+ expect(covers_index.find_covering(0, 5)).to eq(:zone)
35
+ expect(contains_index.find_covering(0, 5)).to be_nil
36
+ expect(contains_index.find_covering(5, 5)).to eq(:zone)
37
+ end
38
+
39
+ it "returns exact-filtered intersecting rect ids in insertion order" do
40
+ index = TG::Geometry::Index.build([[:a, zone_a], [:b, zone_b], [:c, zone_c]],
41
+ via: :geom,
42
+ strategy: :flat)
43
+
44
+ expect(index.intersecting_rect(9, 9, 11, 11)).to eq(%i[a b])
45
+ expect(index.intersecting_rect(30, 30, 31, 31)).to eq([])
46
+ end
47
+
48
+ it "handles empty indexes" do
49
+ index = TG::Geometry::Index.build([], via: :geojson, strategy: :flat)
50
+
51
+ expect(index.find_covering(1, 1)).to be_nil
52
+ expect(index.covering_ids(1, 1)).to eq([])
53
+ expect(index.intersecting_rect(0, 0, 1, 1)).to eq([])
54
+ end
55
+
56
+ it "rejects non-finite point and rect query coordinates" do
57
+ index = TG::Geometry::Index.build([[:a, zone_a]], via: :geom, strategy: :flat)
58
+
59
+ expect { index.find_covering(Float::NAN, 1) }.to raise_error(TG::Geometry::ArgumentError)
60
+ expect { index.covering_ids(1, Float::INFINITY) }.to raise_error(TG::Geometry::ArgumentError)
61
+ expect { index.intersecting_rect(0, 0, Float::INFINITY, 1) }
62
+ .to raise_error(TG::Geometry::ArgumentError)
63
+ expect { index.intersecting_rect(2, 0, 1, 1) }.to raise_error(TG::Geometry::ArgumentError)
64
+ end
65
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Expansion Block A auto strategy status" do
6
+ it "does not expose :auto in the first public release" do
7
+ entries = [[1, '{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]}']]
8
+
9
+ expect(TG::Geometry::Index).not_to respond_to(:auto_strategy_threshold)
10
+ expect do
11
+ TG::Geometry::Index.build(entries, via: :geojson, strategy: :auto)
12
+ end.to raise_error(TG::Geometry::ArgumentError)
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe TG::Geometry::Registry do
6
+ let(:zone_a) do
7
+ '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}'
8
+ end
9
+
10
+ let(:zone_b) do
11
+ '{"type":"Polygon","coordinates":[[[20,20],[30,20],[30,30],[20,30],[20,20]]]}'
12
+ end
13
+
14
+ it "builds and swaps immutable indexes on reload" do
15
+ current_entries = [[:a, zone_a]]
16
+ registry = described_class.new(source: -> { current_entries }, via: :geojson, strategy: :flat)
17
+ old_index = registry.reload!
18
+
19
+ expect(registry.find_covering(5, 5)).to eq(:a)
20
+
21
+ current_entries = [[:b, zone_b]]
22
+ new_index = registry.reload!
23
+
24
+ expect(old_index.find_covering(5, 5)).to eq(:a)
25
+ expect(new_index.find_covering(25, 25)).to eq(:b)
26
+ expect(registry.find_covering(25, 25)).to eq(:b)
27
+ end
28
+
29
+ it "supports subclass source blocks" do
30
+ entries = [[:a, zone_a]]
31
+ klass = Class.new(described_class) do
32
+ source { entries }
33
+ end
34
+
35
+ registry = klass.new(via: :geojson, strategy: :flat)
36
+ registry.reload!
37
+
38
+ expect(registry.loaded?).to eq(true)
39
+ expect(registry.covering_ids(5, 5)).to eq([:a])
40
+ end
41
+
42
+ it "raises before reload" do
43
+ registry = described_class.new(entries: [[:a, zone_a]], via: :geojson, strategy: :flat)
44
+
45
+ expect { registry.find_covering(5, 5) }.to raise_error(TG::Geometry::Error)
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe TG::Geometry::ActiveRecordSource do
6
+ Record = Struct.new(:id, :geojson, keyword_init: true)
7
+
8
+ let(:zone) do
9
+ '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}'
10
+ end
11
+
12
+ it "builds entries from enumerable records without requiring Rails" do
13
+ records = [Record.new(id: :a, geojson: zone)]
14
+
15
+ expect(described_class.call(records, id: :id, geometry: :geojson)).to eq([[:a, zone]])
16
+ end
17
+
18
+ it "supports proc readers" do
19
+ records = [Record.new(id: :a, geojson: zone)]
20
+
21
+ entries = described_class.call(
22
+ records,
23
+ id: ->(record) { record.id.to_s },
24
+ geometry: ->(record) { record.geojson }
25
+ )
26
+
27
+ expect(entries).to eq([["a", zone]])
28
+ end
29
+
30
+ it "can feed a Registry source" do
31
+ records = [Record.new(id: :a, geojson: zone)]
32
+ registry = TG::Geometry::Registry.new(
33
+ source: described_class.registry_source(records, id: :id, geometry: :geojson),
34
+ via: :geojson,
35
+ strategy: :flat
36
+ )
37
+
38
+ registry.reload!
39
+
40
+ expect(registry.find_covering(5, 5)).to eq(:a)
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Expansion Block D format coverage" do
6
+ it "roundtrips Hex" do
7
+ geom = TG::Geometry.parse_wkt("POINT (1 2)")
8
+ hex = geom.to_hex
9
+
10
+ expect(hex.encoding).to eq(Encoding::UTF_8)
11
+ expect(TG::Geometry.parse_hex(hex).to_wkt).to eq("POINT(1 2)")
12
+ end
13
+
14
+ it "roundtrips GeoBIN" do
15
+ geom = TG::Geometry.parse_wkt("POINT (1 2)")
16
+ geobin = geom.to_geobin
17
+
18
+ expect(geobin.encoding).to eq(Encoding::ASCII_8BIT)
19
+ expect(TG::Geometry.parse_geobin(geobin).to_wkt).to eq("POINT(1 2)")
20
+ end
21
+
22
+ it "returns raw extra_json without JSON parsing" do
23
+ geom = TG::Geometry.parse_geojson(
24
+ '{"type":"Feature","properties":{"name":"a"},"geometry":{"type":"Point","coordinates":[1,2]}}'
25
+ )
26
+
27
+ expect(geom.extra_json).to eq('{"properties":{"name":"a"}}')
28
+ expect(geom.extra_json).to be_a(String)
29
+ end
30
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Expansion Block E low-level geometry wrappers" do
6
+ it "exposes Point coordinates without constructing a mutable point wrapper" do
7
+ geom = TG::Geometry.parse_wkt("POINT (1 2)")
8
+
9
+ expect(geom.point).to eq([1.0, 2.0])
10
+ expect(geom.line).to be_nil
11
+ expect(geom.polygon).to be_nil
12
+ end
13
+
14
+ it "exposes borrowed LineString accessors while keeping the parent Geom alive" do
15
+ geom = TG::Geometry.parse_wkt("LINESTRING (0 0, 3 4)")
16
+ line = geom.line
17
+
18
+ expect(line).to be_a(TG::Geometry::Line)
19
+ expect(line).to be_frozen
20
+ expect(line.num_points).to eq(2)
21
+ expect(line.num_segments).to eq(1)
22
+ expect(line.point_at(1)).to eq([3.0, 4.0])
23
+ expect(line.points).to eq([[0.0, 0.0], [3.0, 4.0]])
24
+ expect(line.length).to eq(5.0)
25
+ expect(line.bbox.center).to eq([1.5, 2.0])
26
+
27
+ geom = nil
28
+ GC.start
29
+ GC.compact if GC.respond_to?(:compact)
30
+
31
+ expect(line.length).to eq(5.0)
32
+ expect(line.points).to eq([[0.0, 0.0], [3.0, 4.0]])
33
+ end
34
+
35
+ it "exposes borrowed Polygon and Ring accessors without freeing child pointers" do
36
+ geom = TG::Geometry.parse_wkt(
37
+ "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 4 2, 4 4, 2 4, 2 2))"
38
+ )
39
+ polygon = geom.polygon
40
+ exterior = polygon.exterior_ring
41
+ hole = polygon.hole_at(0)
42
+
43
+ expect(polygon).to be_a(TG::Geometry::Polygon)
44
+ expect(polygon).to be_frozen
45
+ expect(polygon.num_holes).to eq(1)
46
+ expect(polygon.holes.map(&:class)).to eq([TG::Geometry::Ring])
47
+ expect(polygon.bbox.center).to eq([5.0, 5.0])
48
+
49
+ expect(exterior).to be_a(TG::Geometry::Ring)
50
+ expect(exterior).to be_frozen
51
+ expect(exterior.num_points).to eq(5)
52
+ expect(exterior.num_segments).to eq(4)
53
+ expect(exterior.point_at(2)).to eq([10.0, 10.0])
54
+ expect(exterior.points.first).to eq([0.0, 0.0])
55
+ expect(exterior.area).to eq(100.0)
56
+ expect(exterior.perimeter).to eq(40.0)
57
+ expect(exterior.convex?).to be(true)
58
+ expect(hole.area).to eq(4.0)
59
+
60
+ geom = nil
61
+ polygon = nil
62
+ GC.start
63
+ GC.compact if GC.respond_to?(:compact)
64
+
65
+ expect(exterior.area).to eq(100.0)
66
+ expect(hole.perimeter).to eq(8.0)
67
+ end
68
+
69
+ it "keeps low-level wrapper allocation private" do
70
+ expect { TG::Geometry::Line.allocate }.to raise_error(TypeError)
71
+ expect { TG::Geometry::Ring.allocate }.to raise_error(TypeError)
72
+ expect { TG::Geometry::Polygon.allocate }.to raise_error(TypeError)
73
+ end
74
+
75
+ it "rejects out-of-range child indexes" do
76
+ line = TG::Geometry.parse_wkt("LINESTRING (0 0, 1 1)").line
77
+ polygon = TG::Geometry.parse_wkt("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))").polygon
78
+
79
+ expect { line.point_at(2) }.to raise_error(TG::Geometry::ArgumentError)
80
+ expect { polygon.hole_at(0) }.to raise_error(TG::Geometry::ArgumentError)
81
+ end
82
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Expansion Block I Ractor investigation" do
6
+ it "keeps native wrappers outside Ractor shareability claims" do
7
+ skip "Ractor is not available on this Ruby" unless defined?(Ractor)
8
+
9
+ geom = TG::Geometry.parse_wkt("POINT (1 2)")
10
+ rect = TG::Geometry::Rect.new(0, 0, 1, 1)
11
+ index = TG::Geometry::Index.build(
12
+ [[:zone, '{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]}']],
13
+ via: :geojson,
14
+ strategy: :flat
15
+ )
16
+
17
+ expect(Ractor.shareable?(geom)).to be(false)
18
+ expect(Ractor.shareable?(rect)).to be(false)
19
+ expect(Ractor.shareable?(index)).to be(false)
20
+
21
+ expect { Ractor.make_shareable(geom) }.to raise_error(Ractor::Error)
22
+ expect { Ractor.make_shareable(rect) }.to raise_error(Ractor::Error)
23
+ expect { Ractor.make_shareable(index) }.to raise_error(Ractor::Error)
24
+ end
25
+ end