tg_geometry 0.1.0 → 0.3.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -79
  3. data/README.md +168 -184
  4. data/Rakefile +3 -3
  5. data/benchmark/ewkb_roundtrip.rb +29 -0
  6. data/benchmark/falcon_concurrency.rb +1 -1
  7. data/benchmark/feature_source.rb +92 -0
  8. data/benchmark/geom_query.rb +33 -0
  9. data/benchmark/nearest_segment.rb +20 -0
  10. data/docs/ARCHITECTURE.md +29 -107
  11. data/docs/BENCHMARKING.md +20 -1
  12. data/docs/CASUAL_EXAMPLE.md +71 -458
  13. data/docs/CONCURRENCY.md +13 -7
  14. data/docs/ERROR_HANDLING.md +30 -0
  15. data/docs/FEATURE_SOURCE.md +166 -0
  16. data/docs/GEOMETRY_QUERIES.md +23 -0
  17. data/docs/LIMITATIONS.md +17 -54
  18. data/docs/MEMORY_OWNERSHIP.md +20 -2
  19. data/docs/NEAREST_SEGMENT.md +17 -0
  20. data/docs/SRID_AND_EWKB.md +23 -0
  21. data/ext/tg_geometry/extconf.rb +46 -4
  22. data/ext/tg_geometry/tg_geometry_ext.c +3643 -168
  23. data/ext/tg_geometry/tg_geometry_vendor_json.c +17 -0
  24. data/ext/tg_geometry/tg_geometry_vendor_tg.c +3 -0
  25. data/ext/tg_geometry/vendor/.vendored +8 -2
  26. data/ext/tg_geometry/vendor/json/LICENSE +20 -0
  27. data/ext/tg_geometry/vendor/json/VERSION +3 -0
  28. data/ext/tg_geometry/vendor/json/json.c +1024 -0
  29. data/ext/tg_geometry/vendor/json/json.h +207 -0
  30. data/lib/tg/geometry/active_record_source.rb +16 -34
  31. data/lib/tg/geometry/active_record_type.rb +61 -0
  32. data/lib/tg/geometry/registry.rb +16 -67
  33. data/lib/tg/geometry/version.rb +1 -1
  34. data/lib/tg/geometry.rb +85 -0
  35. data/script/vendor_libs.rb +22 -6
  36. data/spec/active_record_type_spec.rb +45 -0
  37. data/spec/{expansion_a_auto_strategy_spec.rb → auto_strategy_spec.rb} +1 -1
  38. data/spec/{block_12_batch_packed_spec.rb → batch_packed_spec.rb} +1 -1
  39. data/spec/{block_20_concurrency_spec.rb → concurrency_spec.rb} +1 -1
  40. data/spec/constructors_spec.rb +104 -0
  41. data/spec/{block_13_error_hardening_spec.rb → error_hardening_spec.rb} +1 -1
  42. data/spec/feature_source_nogvl_spec.rb +51 -0
  43. data/spec/feature_source_spec.rb +268 -0
  44. data/spec/fixtures/feature_source/invalid_geometry_middle.geojson +8 -0
  45. data/spec/fixtures/feature_source/malformed_json.geojson +1 -0
  46. data/spec/fixtures/feature_source/mixed_geometry_types.geojson +8 -0
  47. data/spec/fixtures/feature_source/osm_like_feature_collection.geojson +10 -0
  48. data/spec/fixtures/feature_source/properties_null_missing.geojson +7 -0
  49. data/spec/fixtures/feature_source/simple_feature_collection.geojson +15 -0
  50. data/spec/fixtures/postgis/README.md +16 -0
  51. data/spec/fixtures/postgis/boundary_point_cases.geojson +83 -0
  52. data/spec/fixtures/postgis/multipolygon_large.ewkb +0 -0
  53. data/spec/fixtures/postgis/point_4326.ewkb +0 -0
  54. data/spec/fixtures/postgis/polygon_3857.ewkb +0 -0
  55. data/spec/fixtures/postgis/polygon_4326_simple.ewkb +0 -0
  56. data/spec/fixtures/postgis/polygon_4326_with_hole.ewkb +0 -0
  57. data/spec/{expansion_d_format_coverage_spec.rb → format_coverage_spec.rb} +1 -1
  58. data/spec/{block_20_fuzz_spec.rb → fuzz_spec.rb} +1 -1
  59. data/spec/{block_4_geom_api_spec.rb → geom_api_spec.rb} +1 -1
  60. data/spec/{block_3_geom_parse_spec.rb → geom_parse_spec.rb} +1 -1
  61. data/spec/{block_8_index_borrowed_geometry_spec.rb → index_borrowed_geometry_spec.rb} +1 -1
  62. data/spec/{block_6_index_build_spec.rb → index_build_spec.rb} +2 -2
  63. data/spec/{block_9_flat_query_spec.rb → index_flat_query_spec.rb} +1 -1
  64. data/spec/index_geom_query_spec.rb +68 -0
  65. data/spec/{block_7_index_owned_geometry_spec.rb → index_owned_geometry_spec.rb} +1 -1
  66. data/spec/{block_10_rtree_strategy_spec.rb → index_rtree_accounting_spec.rb} +1 -1
  67. data/spec/{block_11_rtree_order_spec.rb → index_rtree_order_spec.rb} +1 -1
  68. data/spec/keyword_validation_spec.rb +31 -0
  69. data/spec/{block_1_skeleton_spec.rb → load_and_errors_spec.rb} +1 -1
  70. data/spec/{expansion_e_low_level_geometry_spec.rb → low_level_geometry_spec.rb} +1 -1
  71. data/spec/{block_14_memory_gc_hardening_spec.rb → memory_gc_spec.rb} +1 -1
  72. data/spec/nearest_segment_spec.rb +62 -0
  73. data/spec/postgis_fixtures_spec.rb +68 -0
  74. data/spec/{expansion_i_ractor_spec.rb → ractor_spec.rb} +1 -1
  75. data/spec/{block_5_rect_api_spec.rb → rect_api_spec.rb} +1 -1
  76. data/spec/{expansion_b_registry_spec.rb → registry_spec.rb} +1 -1
  77. data/spec/srid_spec.rb +43 -0
  78. data/spec/{expansion_j_full_tg_api_coverage_spec.rb → tg_api_coverage_spec.rb} +1 -1
  79. data/spec/to_ewkb_spec.rb +37 -0
  80. data/spec/{block_2_vendor_spec.rb → vendor_sources_spec.rb} +4 -4
  81. metadata +87 -38
  82. data/docs/ACTIVE_RECORD.md +0 -26
  83. data/docs/AUTO_STRATEGY.md +0 -15
  84. data/docs/EXPANSION_E_TO_H_STATUS.md +0 -51
  85. data/docs/FORMAT_COVERAGE.md +0 -23
  86. data/docs/FULL_TG_API_COVERAGE.md +0 -109
  87. data/docs/LOW_LEVEL_GEOMETRY.md +0 -121
  88. data/docs/RACTOR.md +0 -40
  89. data/docs/REGISTRY.md +0 -37
  90. data/docs/RELEASE_CHECKLIST.md +0 -39
  91. /data/spec/{expansion_c_active_record_source_spec.rb → active_record_source_spec.rb} +0 -0
@@ -5,7 +5,7 @@ require "json"
5
5
  require "objspace"
6
6
  require "weakref"
7
7
 
8
- RSpec.describe "Release Core Block 8 Index borrowed geometry ingestion" do
8
+ RSpec.describe "Index borrowed geometry ingestion" do
9
9
  let(:polygon_a_wkt) { "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))" }
10
10
  let(:polygon_b_wkt) { "POLYGON ((10 10, 12 10, 12 12, 10 12, 10 10))" }
11
11
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Release Core Block 6 Index build skeleton" do
5
+ RSpec.describe "Index build" do
6
6
  let(:geom) { TG::Geometry.parse_wkt("POINT (1 2)") }
7
7
 
8
8
  it "builds an empty immutable index" do
@@ -54,7 +54,7 @@ RSpec.describe "Release Core Block 6 Index build skeleton" do
54
54
  .to raise_error(TG::Geometry::ArgumentError)
55
55
  end
56
56
 
57
- it "requires String values for owned geometry modes in the block 6 skeleton" do
57
+ it "requires String values for owned geometry modes in the owned geometry build path" do
58
58
  expect { TG::Geometry::Index.build([[1, Object.new]], via: :geojson, strategy: :flat) }
59
59
  .to raise_error(TypeError)
60
60
  expect { TG::Geometry::Index.build([[1, Object.new]], via: :wkb, strategy: :flat) }
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Release Core Block 9 flat query engine" do
5
+ RSpec.describe "Flat index query engine" do
6
6
  let(:zone_a) { TG::Geometry.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") }
7
7
  let(:zone_b) { TG::Geometry.parse_wkt("POLYGON ((5 5, 12 5, 12 12, 5 12, 5 5))") }
8
8
  let(:zone_c) { TG::Geometry.parse_wkt("POLYGON ((20 20, 22 20, 22 22, 20 22, 20 20))") }
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Index geometry queries" do
6
+ def polygon(min_x, min_y, max_x, max_y)
7
+ TG::Geometry.polygon([[min_x, min_y], [max_x, min_y], [max_x, max_y], [min_x, max_y], [min_x, min_y]])
8
+ end
9
+
10
+ let(:zones) do
11
+ [
12
+ [:a, polygon(0, 0, 10, 10)],
13
+ [:b, polygon(8, 8, 18, 18)],
14
+ [:c, polygon(30, 30, 40, 40)]
15
+ ]
16
+ end
17
+
18
+ def build(strategy)
19
+ TG::Geometry::Index.build(zones, via: :geom, strategy: strategy)
20
+ end
21
+
22
+ it "returns intersecting ids in insertion order" do
23
+ query = polygon(9, 9, 12, 12)
24
+
25
+ expect(build(:flat).intersecting_geom_ids(query)).to eq(%i[a b])
26
+ expect(build(:rtree).intersecting_geom_ids(query)).to eq(%i[a b])
27
+ end
28
+
29
+ it "supports line query intersections" do
30
+ query = TG::Geometry.line_string([[35, 35], [45, 45]])
31
+
32
+ expect(build(:flat).intersecting_geom_ids(query)).to eq([:c])
33
+ end
34
+
35
+ it "returns empty arrays for outside query geometries" do
36
+ expect(build(:rtree).intersecting_geom_ids(polygon(100, 100, 101, 101))).to eq([])
37
+ end
38
+
39
+ it "uses stored_geom covers query semantics" do
40
+ index = build(:flat)
41
+
42
+ expect(index.covering_geom_ids(TG::Geometry.point(5, 5))).to eq([:a])
43
+ expect(index.covering_geom_ids(TG::Geometry.point(0, 5))).to eq([:a])
44
+ expect(index.covering_geom_ids(polygon(9, 9, 12, 12))).to eq([:b])
45
+ end
46
+
47
+ it "uses stored_geom contains query semantics and excludes boundary" do
48
+ index = build(:flat)
49
+
50
+ expect(index.containing_geom_ids(TG::Geometry.point(5, 5))).to eq([:a])
51
+ expect(index.containing_geom_ids(TG::Geometry.point(0, 5))).to eq([])
52
+ expect(index.covering_geom_ids(TG::Geometry.point(0, 5))).to eq([:a])
53
+ end
54
+
55
+ it "does not read Index.build predicate for geometry methods" do
56
+ index = TG::Geometry::Index.build(zones, via: :geom, strategy: :flat, predicate: :contains)
57
+
58
+ expect(index.covering_geom_ids(TG::Geometry.point(0, 5))).to eq([:a])
59
+ end
60
+
61
+ it "raises TypeError for non-Geom query values" do
62
+ index = build(:flat)
63
+
64
+ expect { index.intersecting_geom_ids(nil) }.to raise_error(TypeError)
65
+ expect { index.covering_geom_ids("bad") }.to raise_error(TypeError)
66
+ expect { index.containing_geom_ids(1) }.to raise_error(TypeError)
67
+ end
68
+ end
@@ -3,7 +3,7 @@
3
3
  require "spec_helper"
4
4
  require "objspace"
5
5
 
6
- RSpec.describe "Release Core Block 7 Index owned geometry ingestion" do
6
+ RSpec.describe "Index owned geometry ingestion" do
7
7
  let(:geojson_a) do
8
8
  '{"type":"Polygon","coordinates":[[[0,0],[2,0],[2,2],[0,2],[0,0]]]}'
9
9
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Release Core Block 10 exact rtree accounting" do
5
+ RSpec.describe "Exact rtree accounting" do
6
6
  let(:zone_a) { TG::Geometry.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") }
7
7
  let(:zone_b) { TG::Geometry.parse_wkt("POLYGON ((20 20, 30 20, 30 30, 20 30, 20 20))") }
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Release Core Block 11 rtree ordered results" do
5
+ RSpec.describe "Rtree ordered results" do
6
6
  let(:entries) do
7
7
  [
8
8
  [:first, TG::Geometry.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")],
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "strict keyword validation" do
6
+ let(:geom_without_srid) { TG::Geometry.parse_wkt("POINT (1 2)") }
7
+ let(:wkb) { geom_without_srid.to_wkb }
8
+
9
+ it "rejects unsupported SRID and EWKB flags on parse_wkb" do
10
+ expect { TG::Geometry.parse_wkb(wkb, srid: 4326) }
11
+ .to raise_error(TG::Geometry::ArgumentError, /unknown keyword: :srid/)
12
+ expect { TG::Geometry.parse_wkb(wkb, ewkb: true) }
13
+ .to raise_error(TG::Geometry::ArgumentError, /unknown keyword: :ewkb/)
14
+ end
15
+
16
+ it "rejects public release_gvl knobs on new v0.3.0 APIs" do
17
+ expect { TG::Geometry.line_string([[0, 0], [1, 1]], release_gvl: true) }
18
+ .to raise_error(TG::Geometry::ArgumentError, /unknown keyword: :release_gvl/)
19
+ expect { geom_without_srid.to_ewkb(srid: 4326, release_gvl: true) }
20
+ .to raise_error(TG::Geometry::ArgumentError, /unknown keyword: :release_gvl/)
21
+ end
22
+
23
+ it "rejects unknown constructor keywords" do
24
+ square = [[0, 0], [1, 0], [1, 1], [0, 0]]
25
+
26
+ expect { TG::Geometry.polygon(square, autoclose: true) }
27
+ .to raise_error(TG::Geometry::ArgumentError, /unknown keyword: :autoclose/)
28
+ expect { TG::Geometry.multi_polygon([{ exterior: square, holes: [], extra: true }]) }
29
+ .to raise_error(TG::Geometry::ArgumentError, /unknown keyword: :extra/)
30
+ end
31
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Release Core Block 1 skeleton" do
5
+ RSpec.describe "Load and error hierarchy" do
6
6
  it "compiles the native extension" do
7
7
  expect(File.file?(EXT_SO)).to be(true)
8
8
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Expansion Block E low-level geometry wrappers" do
5
+ RSpec.describe "Low-level geometry wrappers" do
6
6
  it "exposes Point coordinates without constructing a mutable point wrapper" do
7
7
  geom = TG::Geometry.parse_wkt("POINT (1 2)")
8
8
 
@@ -3,7 +3,7 @@
3
3
  require "spec_helper"
4
4
  require "objspace"
5
5
 
6
- RSpec.describe "Release Core Block 14 memory, GC, and compaction hardening" do
6
+ RSpec.describe "Memory, GC, and compaction hardening" do
7
7
  let(:small_polygon_wkt) { "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))" }
8
8
  let(:small_polygon_geojson) { '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}' }
9
9
  let(:malformed_geojson) { '{"type":"Polygon","coordinates":[' }
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "nearest_segment" do
6
+ it "finds nearest line segments and projection points" do
7
+ line = TG::Geometry.line_string([[0, 0], [10, 0], [10, 10]]).line
8
+ nearest = line.nearest_segment(5, 3)
9
+
10
+ expect(nearest).to be_a(TG::Geometry::NearestSegment)
11
+ expect(nearest).to be_frozen
12
+ expect(nearest.segment).to be_a(TG::Geometry::Segment)
13
+ expect(nearest.index).to be >= 0
14
+ expect(nearest.distance).to be_within(1e-12).of(3.0)
15
+ expect(nearest.point).to eq([5.0, 0.0])
16
+ end
17
+
18
+ it "returns zero distance for a point on a segment" do
19
+ nearest = TG::Geometry.line_string([[0, 0], [10, 0]]).line.nearest_segment(5, 0)
20
+
21
+ expect(nearest.index).to eq(0)
22
+ expect(nearest.distance).to be_within(1e-12).of(0.0)
23
+ expect(nearest.point).to eq([5.0, 0.0])
24
+ end
25
+
26
+ it "handles outside-bbox and degenerate segments" do
27
+ outside = TG::Geometry.line_string([[0, 0], [10, 0]]).line.nearest_segment(20, 5)
28
+ degenerate = TG::Geometry.line_string([[1, 1], [1, 1]]).line.nearest_segment(4, 5)
29
+
30
+ expect(outside.index).to eq(0)
31
+ expect(outside.point).to eq([10.0, 0.0])
32
+ expect(degenerate.index).to eq(0)
33
+ expect(degenerate.point).to eq([1.0, 1.0])
34
+ end
35
+
36
+ it "finds nearest ring segments without depending on equal-distance tie break" do
37
+ ring = TG::Geometry.polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]).polygon.exterior_ring
38
+ center = ring.nearest_segment(5, 5)
39
+ corner = ring.nearest_segment(0, 0)
40
+
41
+ expect(center.index).to be_between(0, 3).inclusive
42
+ expect(center.distance).to be_within(1e-12).of(5.0)
43
+ expect(corner.distance).to be_within(1e-12).of(0.0)
44
+ end
45
+
46
+ it "keeps nearest result valid after parent GC" do
47
+ nearest = TG::Geometry.line_string([[0, 0], [10, 0]]).line.nearest_segment(5, 2)
48
+
49
+ GC.start
50
+ GC.compact if GC.respond_to?(:compact)
51
+
52
+ expect(nearest.segment.points).to eq([[0.0, 0.0], [10.0, 0.0]])
53
+ expect(nearest.point).to eq([5.0, 0.0])
54
+ end
55
+
56
+ it "rejects non-finite coordinates" do
57
+ line = TG::Geometry.line_string([[0, 0], [10, 0]]).line
58
+
59
+ expect { line.nearest_segment(Float::NAN, 0) }.to raise_error(TG::Geometry::ArgumentError)
60
+ expect { line.nearest_segment(0, Float::INFINITY) }.to raise_error(TG::Geometry::ArgumentError)
61
+ end
62
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "spec_helper"
5
+
6
+ RSpec.describe "PostGIS EWKB fixtures" do
7
+ FIXTURE_ROOT = File.expand_path("fixtures/postgis", __dir__)
8
+
9
+ CASES = {
10
+ "point_4326.ewkb" => { srid: 4326, bbox: [37.6, 55.7, 37.6, 55.7] },
11
+ "polygon_4326_simple.ewkb" => { srid: 4326, bbox: [0.0, 0.0, 10.0, 10.0] },
12
+ "polygon_4326_with_hole.ewkb" => { srid: 4326, bbox: [0.0, 0.0, 10.0, 10.0] },
13
+ "polygon_3857.ewkb" => { srid: 3857, bbox: [0.0, 0.0, 10.0, 10.0] },
14
+ "multipolygon_large.ewkb" => { srid: 4326, bbox: [99.0, 49.0, 101.0, 51.0] }
15
+ }.freeze
16
+
17
+ CASES.each do |filename, expected|
18
+ it "parses and roundtrips #{filename}" do
19
+ bytes = File.binread(File.join(FIXTURE_ROOT, filename))
20
+ geom = TG::Geometry.parse_wkb(bytes)
21
+ bbox = geom.bbox
22
+
23
+ expect(geom.srid).to eq(expected[:srid])
24
+ expect([bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y]).to all(be_a(Float))
25
+ [bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y].zip(expected[:bbox]).each do |actual, expected_coord|
26
+ expect(actual).to be_within(1e-9).of(expected_coord)
27
+ end
28
+ expect(geom.to_ewkb).to eq(bytes)
29
+ expect(TG::Geometry.parse_wkb(geom.to_wkb).srid).to be_nil
30
+ end
31
+ end
32
+
33
+ it "keeps hole coverage semantics stable" do
34
+ geom = TG::Geometry.parse_wkb(File.binread(File.join(FIXTURE_ROOT, "polygon_4326_with_hole.ewkb")))
35
+
36
+ expect(geom.covers_xy?(3, 3)).to be(false)
37
+ expect(geom.covers_xy?(1, 1)).to be(true)
38
+ end
39
+
40
+ it "checks boundary cases from GeoJSON" do
41
+ simple = TG::Geometry.parse_wkb(File.binread(File.join(FIXTURE_ROOT, "polygon_4326_simple.ewkb")))
42
+ data = JSON.parse(File.read(File.join(FIXTURE_ROOT, "boundary_point_cases.geojson")))
43
+ points = data.fetch("features").to_h do |feature|
44
+ [feature.fetch("properties").fetch("name"), feature.fetch("geometry").fetch("coordinates")]
45
+ end
46
+
47
+ expect(simple.covers_xy?(*points.fetch("inside_simple"))).to be(true)
48
+ expect(simple.covers_xy?(*points.fetch("boundary_simple"))).to be(true)
49
+ end
50
+
51
+ it "sanity-checks the large multipolygon quickly" do
52
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
53
+ geom = TG::Geometry.parse_wkb(File.binread(File.join(FIXTURE_ROOT, "multipolygon_large.ewkb")))
54
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
55
+
56
+ expect(elapsed).to be < 1.0
57
+ expect(geom.covers_xy?(100.0, 50.0)).to be(true)
58
+ expect(geom.covers_xy?(200.0, 50.0)).to be(false)
59
+ end
60
+
61
+ it "documents SRID mismatch as user responsibility" do
62
+ geom_4326 = TG::Geometry.parse_wkb(File.binread(File.join(FIXTURE_ROOT, "polygon_4326_simple.ewkb")))
63
+ geom_3857 = TG::Geometry.parse_wkb(File.binread(File.join(FIXTURE_ROOT, "polygon_3857.ewkb")))
64
+
65
+ expect(geom_4326.srid).not_to eq(geom_3857.srid)
66
+ expect(geom_4326.intersects?(geom_3857)).to be(true)
67
+ end
68
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Expansion Block I Ractor investigation" do
5
+ RSpec.describe "Ractor boundary" do
6
6
  it "keeps native wrappers outside Ractor shareability claims" do
7
7
  skip "Ractor is not available on this Ruby" unless defined?(Ractor)
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Release Core Block 5 Rect API" do
5
+ RSpec.describe "Rect API" do
6
6
  it "constructs an immutable rect with coordinate accessors" do
7
7
  rect = TG::Geometry::Rect.new(1, 2, 5, 8)
8
8
 
@@ -26,7 +26,7 @@ RSpec.describe TG::Geometry::Registry do
26
26
  expect(registry.find_covering(25, 25)).to eq(:b)
27
27
  end
28
28
 
29
- it "supports subclass source blocks" do
29
+ it "supports subclass source definitions" do
30
30
  entries = [[:a, zone_a]]
31
31
  klass = Class.new(described_class) do
32
32
  source { entries }
data/spec/srid_spec.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "SRID metadata" do
6
+ let(:plain_wkb) { TG::Geometry.parse_wkt("POINT (1 2)").to_wkb }
7
+ let(:ewkb_4326) { TG::Geometry.parse_wkt("POINT (1 2)").to_ewkb(srid: 4326) }
8
+ let(:ewkb_3857) { TG::Geometry.parse_wkt("POINT (1 2)").to_ewkb(srid: 3857) }
9
+ let(:ewkb_0) { TG::Geometry.parse_wkt("POINT (1 2)").to_ewkb(srid: 0) }
10
+
11
+ it "returns nil for plain WKB and non-SRID text formats" do
12
+ expect(TG::Geometry.parse_wkb(plain_wkb).srid).to be_nil
13
+ expect(TG::Geometry.parse_geojson('{"type":"Point","coordinates":[1,2]}').srid).to be_nil
14
+ expect(TG::Geometry.parse_wkt("POINT (1 2)").srid).to be_nil
15
+ end
16
+
17
+ it "extracts SRID from little-endian EWKB and hex EWKB" do
18
+ geom = TG::Geometry.parse_wkb(ewkb_4326)
19
+
20
+ expect(geom.srid).to eq(4326)
21
+ expect([geom.bbox.min_x, geom.bbox.min_y]).to eq([1.0, 2.0])
22
+ expect(TG::Geometry.parse_wkb(ewkb_3857).srid).to eq(3857)
23
+ expect(TG::Geometry.parse_wkb(ewkb_0).srid).to eq(0)
24
+ expect(TG::Geometry.parse_hex(ewkb_4326.unpack1("H*")).srid).to eq(4326)
25
+ end
26
+
27
+ it "extracts SRID from big-endian EWKB" do
28
+ # Big-endian EWKB: byte order 0, type POINT | SRID flag, SRID 4326, x=1.0, y=2.0.
29
+ ewkb = [0, 0x20000001, 4326].pack("C N N") + [1.0, 2.0].pack("G G")
30
+
31
+ expect(TG::Geometry.parse_wkb(ewkb).srid).to eq(4326)
32
+ expect(TG::Geometry.parse_wkb(ewkb).point).to eq([1.0, 2.0])
33
+ end
34
+
35
+ it "does not crash on malformed EWKB" do
36
+ expect { TG::Geometry.parse_wkb("\x01\x01\x00".b) }.to raise_error(TG::Geometry::ParseError)
37
+ end
38
+
39
+ it "uses constructor srid metadata" do
40
+ expect(TG::Geometry.polygon([[0, 0], [1, 0], [1, 1], [0, 0]], srid: 4326).srid).to eq(4326)
41
+ expect(TG::Geometry.polygon([[0, 0], [1, 0], [1, 1], [0, 0]]).srid).to be_nil
42
+ end
43
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Expansion Block J grouped TG API coverage" do
5
+ RSpec.describe "TG API coverage" do
6
6
  it "adds safe point and empty geometry constructors without public allocation" do
7
7
  point = TG::Geometry.point_zm(1, 2, 3, 4)
8
8
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Geom#to_ewkb" do
6
+ let(:geom_without_srid) { TG::Geometry.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") }
7
+ let(:geom_with_srid) { TG::Geometry.parse_wkb(geom_without_srid.to_ewkb(srid: 4326)) }
8
+
9
+ it "roundtrips semantic geometry and SRID" do
10
+ ewkb = geom_with_srid.to_ewkb
11
+ reparsed = TG::Geometry.parse_wkb(ewkb)
12
+
13
+ expect(reparsed.srid).to eq(4326)
14
+ expect(reparsed.type).to eq(:polygon)
15
+ expect(reparsed.bbox.max_x).to eq(10.0)
16
+ expect(reparsed.covers_xy?(5, 5)).to be(true)
17
+ end
18
+
19
+ it "can add or override SRID explicitly" do
20
+ expect(TG::Geometry.parse_wkb(geom_without_srid.to_ewkb(srid: 4326)).srid).to eq(4326)
21
+ expect(TG::Geometry.parse_wkb(geom_with_srid.to_ewkb(srid: 3857)).srid).to eq(3857)
22
+ end
23
+
24
+ it "returns frozen binary strings" do
25
+ ewkb = geom_with_srid.to_ewkb
26
+
27
+ expect(ewkb.encoding).to eq(Encoding::BINARY)
28
+ expect(ewkb).to be_frozen
29
+ end
30
+
31
+ it "rejects missing or invalid SRID" do
32
+ expect { geom_without_srid.to_ewkb }.to raise_error(TG::Geometry::ArgumentError)
33
+ expect { geom_with_srid.to_ewkb(srid: -1) }.to raise_error(TG::Geometry::ArgumentError)
34
+ expect { geom_with_srid.to_ewkb(srid: 2**31) }.to raise_error(TG::Geometry::ArgumentError)
35
+ expect { geom_with_srid.to_ewkb(srid: "4326") }.to raise_error(TG::Geometry::ArgumentError)
36
+ end
37
+ end
@@ -4,16 +4,16 @@ require "spec_helper"
4
4
  require "open3"
5
5
  require "tmpdir"
6
6
 
7
- RSpec.describe "Release Core Block 2 vendored build" do
7
+ RSpec.describe "Vendored source build" do
8
8
  let(:vendor_dir) { File.join(ROOT, "ext", "tg_geometry", "vendor") }
9
9
 
10
- it "keeps tidwall/tg vendored under the contract path" do
10
+ it "keeps tidwall/tg vendored under the expected path" do
11
11
  %w[tg.c tg.h VERSION].each do |name|
12
12
  expect(File.file?(File.join(vendor_dir, "tg", name))).to be(true), "missing vendor/tg/#{name}"
13
13
  end
14
14
  end
15
15
 
16
- it "keeps rtree.c vendored under the contract path" do
16
+ it "keeps rtree.c vendored under the expected path" do
17
17
  %w[rtree.c rtree.h VERSION].each do |name|
18
18
  expect(File.file?(File.join(vendor_dir, "rtree", name))).to be(true), "missing vendor/rtree/#{name}"
19
19
  end
@@ -51,7 +51,7 @@ RSpec.describe "Release Core Block 2 vendored build" do
51
51
  end
52
52
  end
53
53
 
54
- it "uses the release-core compiler warning flags" do
54
+ it "uses the expected compiler warning flags" do
55
55
  extconf = File.read(File.join(ROOT, "ext", "tg_geometry", "extconf.rb"))
56
56
 
57
57
  expect(extconf).to include('"-std=c11"')