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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -79
- data/README.md +168 -184
- data/Rakefile +3 -3
- data/benchmark/ewkb_roundtrip.rb +29 -0
- data/benchmark/falcon_concurrency.rb +1 -1
- data/benchmark/feature_source.rb +92 -0
- data/benchmark/geom_query.rb +33 -0
- data/benchmark/nearest_segment.rb +20 -0
- data/docs/ARCHITECTURE.md +29 -107
- data/docs/BENCHMARKING.md +20 -1
- data/docs/CASUAL_EXAMPLE.md +71 -458
- data/docs/CONCURRENCY.md +13 -7
- data/docs/ERROR_HANDLING.md +30 -0
- data/docs/FEATURE_SOURCE.md +166 -0
- data/docs/GEOMETRY_QUERIES.md +23 -0
- data/docs/LIMITATIONS.md +17 -54
- data/docs/MEMORY_OWNERSHIP.md +20 -2
- data/docs/NEAREST_SEGMENT.md +17 -0
- data/docs/SRID_AND_EWKB.md +23 -0
- data/ext/tg_geometry/extconf.rb +46 -4
- data/ext/tg_geometry/tg_geometry_ext.c +3643 -168
- data/ext/tg_geometry/tg_geometry_vendor_json.c +17 -0
- data/ext/tg_geometry/tg_geometry_vendor_tg.c +3 -0
- data/ext/tg_geometry/vendor/.vendored +8 -2
- data/ext/tg_geometry/vendor/json/LICENSE +20 -0
- data/ext/tg_geometry/vendor/json/VERSION +3 -0
- data/ext/tg_geometry/vendor/json/json.c +1024 -0
- data/ext/tg_geometry/vendor/json/json.h +207 -0
- data/lib/tg/geometry/active_record_source.rb +16 -34
- data/lib/tg/geometry/active_record_type.rb +61 -0
- data/lib/tg/geometry/registry.rb +16 -67
- data/lib/tg/geometry/version.rb +1 -1
- data/lib/tg/geometry.rb +85 -0
- data/script/vendor_libs.rb +22 -6
- data/spec/active_record_type_spec.rb +45 -0
- data/spec/{expansion_a_auto_strategy_spec.rb → auto_strategy_spec.rb} +1 -1
- data/spec/{block_12_batch_packed_spec.rb → batch_packed_spec.rb} +1 -1
- data/spec/{block_20_concurrency_spec.rb → concurrency_spec.rb} +1 -1
- data/spec/constructors_spec.rb +104 -0
- data/spec/{block_13_error_hardening_spec.rb → error_hardening_spec.rb} +1 -1
- data/spec/feature_source_nogvl_spec.rb +51 -0
- data/spec/feature_source_spec.rb +268 -0
- data/spec/fixtures/feature_source/invalid_geometry_middle.geojson +8 -0
- data/spec/fixtures/feature_source/malformed_json.geojson +1 -0
- data/spec/fixtures/feature_source/mixed_geometry_types.geojson +8 -0
- data/spec/fixtures/feature_source/osm_like_feature_collection.geojson +10 -0
- data/spec/fixtures/feature_source/properties_null_missing.geojson +7 -0
- data/spec/fixtures/feature_source/simple_feature_collection.geojson +15 -0
- data/spec/fixtures/postgis/README.md +16 -0
- data/spec/fixtures/postgis/boundary_point_cases.geojson +83 -0
- data/spec/fixtures/postgis/multipolygon_large.ewkb +0 -0
- data/spec/fixtures/postgis/point_4326.ewkb +0 -0
- data/spec/fixtures/postgis/polygon_3857.ewkb +0 -0
- data/spec/fixtures/postgis/polygon_4326_simple.ewkb +0 -0
- data/spec/fixtures/postgis/polygon_4326_with_hole.ewkb +0 -0
- data/spec/{expansion_d_format_coverage_spec.rb → format_coverage_spec.rb} +1 -1
- data/spec/{block_20_fuzz_spec.rb → fuzz_spec.rb} +1 -1
- data/spec/{block_4_geom_api_spec.rb → geom_api_spec.rb} +1 -1
- data/spec/{block_3_geom_parse_spec.rb → geom_parse_spec.rb} +1 -1
- data/spec/{block_8_index_borrowed_geometry_spec.rb → index_borrowed_geometry_spec.rb} +1 -1
- data/spec/{block_6_index_build_spec.rb → index_build_spec.rb} +2 -2
- data/spec/{block_9_flat_query_spec.rb → index_flat_query_spec.rb} +1 -1
- data/spec/index_geom_query_spec.rb +68 -0
- data/spec/{block_7_index_owned_geometry_spec.rb → index_owned_geometry_spec.rb} +1 -1
- data/spec/{block_10_rtree_strategy_spec.rb → index_rtree_accounting_spec.rb} +1 -1
- data/spec/{block_11_rtree_order_spec.rb → index_rtree_order_spec.rb} +1 -1
- data/spec/keyword_validation_spec.rb +31 -0
- data/spec/{block_1_skeleton_spec.rb → load_and_errors_spec.rb} +1 -1
- data/spec/{expansion_e_low_level_geometry_spec.rb → low_level_geometry_spec.rb} +1 -1
- data/spec/{block_14_memory_gc_hardening_spec.rb → memory_gc_spec.rb} +1 -1
- data/spec/nearest_segment_spec.rb +62 -0
- data/spec/postgis_fixtures_spec.rb +68 -0
- data/spec/{expansion_i_ractor_spec.rb → ractor_spec.rb} +1 -1
- data/spec/{block_5_rect_api_spec.rb → rect_api_spec.rb} +1 -1
- data/spec/{expansion_b_registry_spec.rb → registry_spec.rb} +1 -1
- data/spec/srid_spec.rb +43 -0
- data/spec/{expansion_j_full_tg_api_coverage_spec.rb → tg_api_coverage_spec.rb} +1 -1
- data/spec/to_ewkb_spec.rb +37 -0
- data/spec/{block_2_vendor_spec.rb → vendor_sources_spec.rb} +4 -4
- metadata +87 -38
- data/docs/ACTIVE_RECORD.md +0 -26
- data/docs/AUTO_STRATEGY.md +0 -15
- data/docs/EXPANSION_E_TO_H_STATUS.md +0 -51
- data/docs/FORMAT_COVERAGE.md +0 -23
- data/docs/FULL_TG_API_COVERAGE.md +0 -109
- data/docs/LOW_LEVEL_GEOMETRY.md +0 -121
- data/docs/RACTOR.md +0 -40
- data/docs/REGISTRY.md +0 -37
- data/docs/RELEASE_CHECKLIST.md +0 -39
- /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 "
|
|
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 "
|
|
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
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
|
|
@@ -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 "
|
|
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 "
|
|
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
|
|
@@ -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
|
|
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
|
|
@@ -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 "
|
|
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
|
|
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
|
|
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
|
|
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"')
|