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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +103 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +385 -0
- data/Rakefile +129 -0
- data/benchmark/_support.rb +115 -0
- data/benchmark/batch_packed_vs_loop.rb +27 -0
- data/benchmark/falcon_concurrency.rb +25 -0
- data/benchmark/flat_vs_rtree.rb +27 -0
- data/benchmark/gvl_threshold.rb +41 -0
- data/benchmark/objectspace_memsize.rb +17 -0
- data/benchmark/parse_throughput.rb +38 -0
- data/benchmark/rss_stability.rb +70 -0
- data/docs/ACTIVE_RECORD.md +26 -0
- data/docs/ARCHITECTURE.md +130 -0
- data/docs/AUTO_STRATEGY.md +15 -0
- data/docs/BENCHMARKING.md +75 -0
- data/docs/CASUAL_EXAMPLE.md +618 -0
- data/docs/CONCURRENCY.md +65 -0
- data/docs/ERROR_HANDLING.md +55 -0
- data/docs/EXPANSION_E_TO_H_STATUS.md +51 -0
- data/docs/FORMAT_COVERAGE.md +23 -0
- data/docs/FULL_TG_API_COVERAGE.md +109 -0
- data/docs/LIMITATIONS.md +61 -0
- data/docs/LOW_LEVEL_GEOMETRY.md +121 -0
- data/docs/MEMORY_OWNERSHIP.md +94 -0
- data/docs/RACTOR.md +40 -0
- data/docs/REGISTRY.md +37 -0
- data/docs/RELEASE_CHECKLIST.md +39 -0
- data/ext/tg_geometry/extconf.rb +91 -0
- data/ext/tg_geometry/tg_geometry_ext.c +3054 -0
- data/ext/tg_geometry/tg_geometry_vendor_rtree.c +1 -0
- data/ext/tg_geometry/tg_geometry_vendor_tg.c +24 -0
- data/ext/tg_geometry/vendor/.vendored +16 -0
- data/ext/tg_geometry/vendor/rtree/LICENSE +20 -0
- data/ext/tg_geometry/vendor/rtree/README.md +202 -0
- data/ext/tg_geometry/vendor/rtree/VERSION +3 -0
- data/ext/tg_geometry/vendor/rtree/rtree.c +840 -0
- data/ext/tg_geometry/vendor/rtree/rtree.h +105 -0
- data/ext/tg_geometry/vendor/tg/LICENSE +19 -0
- data/ext/tg_geometry/vendor/tg/README.md +197 -0
- data/ext/tg_geometry/vendor/tg/VERSION +3 -0
- data/ext/tg_geometry/vendor/tg/tg.c +16010 -0
- data/ext/tg_geometry/vendor/tg/tg.h +359 -0
- data/lib/tg/geometry/active_record_source.rb +57 -0
- data/lib/tg/geometry/registry.rb +119 -0
- data/lib/tg/geometry/version.rb +7 -0
- data/lib/tg/geometry.rb +6 -0
- data/lib/tg_geometry.rb +3 -0
- data/script/vendor_libs.rb +264 -0
- data/spec/block_10_rtree_strategy_spec.rb +82 -0
- data/spec/block_11_rtree_order_spec.rb +53 -0
- data/spec/block_12_batch_packed_spec.rb +55 -0
- data/spec/block_13_error_hardening_spec.rb +65 -0
- data/spec/block_14_memory_gc_hardening_spec.rb +116 -0
- data/spec/block_1_skeleton_spec.rb +45 -0
- data/spec/block_20_concurrency_spec.rb +157 -0
- data/spec/block_20_fuzz_spec.rb +145 -0
- data/spec/block_2_vendor_spec.rb +79 -0
- data/spec/block_3_geom_parse_spec.rb +89 -0
- data/spec/block_4_geom_api_spec.rb +90 -0
- data/spec/block_5_rect_api_spec.rb +96 -0
- data/spec/block_6_index_build_spec.rb +111 -0
- data/spec/block_7_index_owned_geometry_spec.rb +143 -0
- data/spec/block_8_index_borrowed_geometry_spec.rb +106 -0
- data/spec/block_9_flat_query_spec.rb +65 -0
- data/spec/expansion_a_auto_strategy_spec.rb +14 -0
- data/spec/expansion_b_registry_spec.rb +47 -0
- data/spec/expansion_c_active_record_source_spec.rb +42 -0
- data/spec/expansion_d_format_coverage_spec.rb +30 -0
- data/spec/expansion_e_low_level_geometry_spec.rb +82 -0
- data/spec/expansion_i_ractor_spec.rb +25 -0
- data/spec/expansion_j_full_tg_api_coverage_spec.rb +114 -0
- data/spec/spec_helper.rb +15 -0
- 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
|