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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdc6ab78992bee6ba1708b13adfdb0e4eec26fef04c4fc8eae87df21fbfed5fa
|
|
4
|
+
data.tar.gz: ada3eff77de10cb101cbedbd35e2d40e119fe37c5a09003afe9df1ab266827fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87200faec4b6d0442eab7df12f768807193aed725bb75b25502a64cc5b2e24e97f2f9d98cf9254c54e3f62d9b0481f39dc2287818f69a740e9728a33e97d9f0b
|
|
7
|
+
data.tar.gz: 3e14f4563facef36fc109e1c8006b707502bb384f67c1c27da05faa7dcec7b7a7970d82a22d39ac99c00595c74944c7a6467fa81a5cdd1d14129df70f4d3675a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,103 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 27.05.2026
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `TG::Geometry.line_string`, `TG::Geometry.polygon`, and `TG::Geometry.multi_polygon` constructors from Ruby arrays.
|
|
8
|
+
- `TG::Geometry::Geom#srid` with automatic EWKB SRID extraction in `parse_wkb` and `parse_hex`.
|
|
9
|
+
- `TG::Geometry::Geom#to_ewkb` writer with explicit `srid:` override.
|
|
10
|
+
- `TG::Geometry::Index#intersecting_geom_ids`, `#covering_geom_ids`, and `#containing_geom_ids`.
|
|
11
|
+
- `TG::Geometry::Line#nearest_segment` and `TG::Geometry::Ring#nearest_segment` with `TG::Geometry::NearestSegment` result objects.
|
|
12
|
+
- `TG::Geometry::ActiveRecordType` read-only optional require.
|
|
13
|
+
- PostGIS/EWKB fixture suite in `spec/fixtures/postgis/`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Reject unsupported/unknown keywords on v0.3.0 APIs instead of silently ignoring them, including non-goals such as `release_gvl:`, `parse_wkb(srid:)`, and `parse_wkb(ewkb:)`.
|
|
18
|
+
- Harden constructor cleanup paths so intermediate C `tg_ring` / `tg_poly` objects are released when Ruby exceptions occur during nested polygon construction.
|
|
19
|
+
- Keep geometry-index query collection in a C-only status-returning phase before Ruby result materialization, preserving the intended no-GVL-safe internal shape.
|
|
20
|
+
|
|
21
|
+
### Clarified
|
|
22
|
+
|
|
23
|
+
- `Index.build(predicate:)` affects only legacy point query methods: `find_covering`, `covering_ids(x, y)`, and `covering_ids_batch_packed`.
|
|
24
|
+
|
|
25
|
+
### Not included
|
|
26
|
+
|
|
27
|
+
- Geodesic / Haversine distance.
|
|
28
|
+
- Projection / reprojection.
|
|
29
|
+
- Buffer / union / difference / convex hull.
|
|
30
|
+
- Index nearest_ids (KNN).
|
|
31
|
+
- GeoBIN bbox helpers.
|
|
32
|
+
- Streaming FeatureSource.
|
|
33
|
+
- Index serialization / mmap.
|
|
34
|
+
- Ractor support.
|
|
35
|
+
- Windows / JRuby.
|
|
36
|
+
- Write-side ActiveRecordType / AR scopes / migrations.
|
|
37
|
+
- Z/M variants of constructors.
|
|
38
|
+
- Public `release_gvl:` option.
|
|
39
|
+
|
|
3
40
|
## 0.1.0 - unreleased
|
|
4
41
|
|
|
5
|
-
Initial release
|
|
42
|
+
Initial public release candidate for `tg_geometry`.
|
|
6
43
|
|
|
7
44
|
### Added
|
|
8
45
|
|
|
9
46
|
- Canonical public require path: `require "tg/geometry"`.
|
|
10
47
|
- Public namespace: `TG::Geometry`.
|
|
11
48
|
- Native extension build through `ext/tg_geometry/extconf.rb`.
|
|
12
|
-
- Vendored `tidwall/tg` and `tidwall/
|
|
49
|
+
- Vendored `tidwall/tg`, `tidwall/rtree.c`, and `tidwall/json.c` sources with pinned `VERSION` files and upstream license files.
|
|
13
50
|
- Error classes:
|
|
14
51
|
- `TG::Geometry::Error`
|
|
15
52
|
- `TG::Geometry::ParseError`
|
|
16
53
|
- `TG::Geometry::ArgumentError < ::ArgumentError`
|
|
17
54
|
- `TG::Geometry::FrozenIndexError`
|
|
18
55
|
- Immutable `TG::Geometry::Geom` parsing for GeoJSON, WKT, WKB, Hex, GeoBIN, and auto format detection.
|
|
19
|
-
- `TG::Geometry::Geom` methods:
|
|
20
|
-
- `#type`
|
|
21
|
-
- `#bbox`
|
|
22
|
-
- `#covers_xy?`
|
|
23
|
-
- `#contains?`
|
|
24
|
-
- `#intersects?`
|
|
25
|
-
- `#to_geojson`
|
|
26
|
-
- `#to_wkt`
|
|
27
|
-
- `#to_wkb`
|
|
28
56
|
- Immutable `TG::Geometry::Rect` API.
|
|
29
57
|
- Immutable `TG::Geometry::Index.build` with strict `[[id, object], ...]` entry format.
|
|
30
|
-
- Index ingestion modes:
|
|
31
|
-
|
|
32
|
-
- `via: :geojson` owned geometry;
|
|
33
|
-
- `via: :wkb` owned geometry.
|
|
34
|
-
- Index strategies:
|
|
35
|
-
- `:flat`
|
|
36
|
-
- `:rtree`
|
|
58
|
+
- Index ingestion modes: `via: :geom`, `via: :geojson`, and `via: :wkb`.
|
|
59
|
+
- Index strategies: `:flat` and `:rtree`.
|
|
37
60
|
- Deterministic insertion-order results for flat and rtree queries.
|
|
38
|
-
- Exact rtree memory accounting through a custom malloc/free allocator with headers.
|
|
61
|
+
- Exact rtree memory accounting through a custom malloc/free allocator with allocation headers.
|
|
39
62
|
- Native-endian packed point batch API: `TG::Geometry::Index#covering_ids_batch_packed`.
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- `benchmark/batch_packed_vs_loop.rb`
|
|
47
|
-
- `benchmark/falcon_concurrency.rb`
|
|
48
|
-
- `benchmark/objectspace_memsize.rb`
|
|
49
|
-
- `benchmark/rss_stability.rb`
|
|
50
|
-
- Documentation:
|
|
51
|
-
- `docs/ARCHITECTURE.md`
|
|
52
|
-
- `docs/MEMORY_OWNERSHIP.md`
|
|
53
|
-
- `docs/CONCURRENCY.md`
|
|
54
|
-
- `docs/ERROR_HANDLING.md`
|
|
55
|
-
- `docs/BENCHMARKING.md`
|
|
56
|
-
- `docs/LIMITATIONS.md`
|
|
57
|
-
- `docs/RELEASE_CHECKLIST.md`
|
|
63
|
+
- GeoJSON FeatureSource APIs for reading FeatureCollection entries/features and building an Index directly from file, JSON string, or IO.
|
|
64
|
+
- FeatureSource bulk execution now runs file read, JSON traversal, and TG geometry parsing in a C-only no-GVL phase before Ruby materialization / Index ownership transfer. The implementation uses Ruby VM no-GVL APIs only: `RB_NOGVL_OFFLOAD_SAFE` when available, otherwise `rb_thread_call_without_gvl`. It does not use a manual Fiber scheduler block/unblock worker path.
|
|
65
|
+
- Ruby-level `TG::Geometry::Registry` reload helper.
|
|
66
|
+
- Optional `TG::Geometry::ActiveRecordSource` helper without a Rails runtime dependency.
|
|
67
|
+
- Benchmark scripts under `benchmark/`.
|
|
68
|
+
- Minimal documentation under `docs/`.
|
|
58
69
|
|
|
59
70
|
### Not included
|
|
60
71
|
|
|
61
|
-
- `strategy: :auto
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
### OPEN QUESTION
|
|
68
|
-
|
|
69
|
-
- Final ASAN setup requires Roman approval before replacing the placeholder CI job.
|
|
70
|
-
- Final Valgrind setup requires Roman approval before replacing the placeholder CI job.
|
|
71
|
-
|
|
72
|
-
## Unreleased
|
|
73
|
-
|
|
74
|
-
### Added
|
|
75
|
-
|
|
76
|
-
- Expansion Block A status: `strategy: :auto` remains postponed for the first public release; explicit `:flat` / `:rtree` strategies are required.
|
|
77
|
-
- Expansion Block B: `TG::Geometry::Registry` Ruby helper for immutable Index reload/swap workflows.
|
|
78
|
-
- Expansion Block C: optional `TG::Geometry::ActiveRecordSource` helper that converts relation-like records into strict `[[id, object], ...]` entries without adding a Rails dependency.
|
|
79
|
-
- Expansion Block D: `TG::Geometry.parse_hex`, `TG::Geometry.parse_geobin`, `TG::Geometry::Geom#to_hex`, `#to_geobin`, and `#extra_json`.
|
|
80
|
-
- Expansion Block E: read-only borrowed low-level wrappers: `TG::Geometry::Line`, `TG::Geometry::Ring`, `TG::Geometry::Polygon`, plus `TG::Geometry::Geom#point`, `#line`, and `#polygon`.
|
|
81
|
-
- Expansion Block I: Ractor unsupported-boundary investigation documented in `docs/RACTOR.md` with specs asserting native wrappers are not treated as shareable Ractor objects.
|
|
82
|
-
- Expansion Block J grouped API coverage:
|
|
83
|
-
- safe point and empty geometry constructors;
|
|
84
|
-
- additional `TG::Geometry::Geom` predicates;
|
|
85
|
-
- geometry metadata and Z/M read accessors;
|
|
86
|
-
- MultiPoint/MultiLineString/MultiPolygon and GeometryCollection accessors;
|
|
87
|
-
- borrowed child `TG::Geometry::Geom` wrappers with `geom_owner`;
|
|
88
|
-
- value `TG::Geometry::Segment` wrappers from Line/Ring segment accessors.
|
|
89
|
-
|
|
90
|
-
### Fixed
|
|
91
|
-
|
|
92
|
-
- Corrected `benchmark/gvl_threshold.rb` so each target size uses a valid WKT payload near that size instead of repeatedly benchmarking the same tiny polygon.
|
|
93
|
-
|
|
94
|
-
### Documentation
|
|
95
|
-
|
|
96
|
-
- Added docs for Registry, ActiveRecord source helper, additional format coverage, low-level borrowed geometry wrappers, Ractor unsupported-boundary status, grouped full TG API coverage, Auto Strategy postponed status, and Expansion Blocks E–H status.
|
|
97
|
-
|
|
98
|
-
### OPEN QUESTION
|
|
72
|
+
- `strategy: :auto`.
|
|
73
|
+
- Ractor support claim.
|
|
74
|
+
- no general no-GVL claim for short query/parse/write paths.
|
|
75
|
+
- Full GIS functionality such as routing, projections, geodesics, overlay result geometries, or nearest POI search.
|
|
76
|
+
- Public callback/search APIs.
|
|
77
|
+
- Universal performance claims.
|
|
99
78
|
|
|
100
|
-
-
|
|
101
|
-
- Expansion Block G no-allocation point query optimization remains blocked until boundary/hole-boundary equivalence tests and benchmarks prove it preserves `:covers` / `:contains` semantics.
|
|
102
|
-
- Expansion Block H geodesic/projection helpers remain blocked until an explicit optional dependency/API decision is approved.
|
|
103
|
-
- Remaining Expansion Block J scope such as Line/Ring/Polygon constructors, callback/search APIs, nearest segment APIs, global environment configuration, and allocator override APIs remains blocked until separate ownership/thread-safety contracts are approved.
|
|
79
|
+
- Suppressed known GCC diagnostics from vendored tidwall/tg wrapper on Linux CI without muting warnings from tg_geometry's own C code.
|
data/README.md
CHANGED
|
@@ -1,39 +1,17 @@
|
|
|
1
1
|
# tg_geometry
|
|
2
2
|
|
|
3
|
-
`tg_geometry` is a Ruby C extension around
|
|
4
|
-
library and `tidwall/rtree.c`.
|
|
3
|
+
`tg_geometry` is a Ruby C extension around vendored `tidwall/tg`, `tidwall/rtree.c`, and pinned `tidwall/json.c`.
|
|
5
4
|
|
|
6
|
-
It exposes the public Ruby namespace `TG::Geometry
|
|
7
|
-
path:
|
|
5
|
+
It exposes the public Ruby namespace `TG::Geometry`:
|
|
8
6
|
|
|
9
7
|
```ruby
|
|
10
8
|
require "tg/geometry"
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
The gem
|
|
14
|
-
format conversion, and geofencing-oriented immutable indexes. It does not try
|
|
15
|
-
to be a full GIS system.
|
|
16
|
-
|
|
17
|
-
## Status
|
|
18
|
-
|
|
19
|
-
This repository is prepared as a first public release candidate with an
|
|
20
|
-
expanded API surface:
|
|
21
|
-
|
|
22
|
-
- release-core `Geom`, `Rect`, and immutable `Index` APIs;
|
|
23
|
-
- expanded format coverage for Hex and GeoBIN;
|
|
24
|
-
- read-only borrowed wrappers for lower-level TG geometry components;
|
|
25
|
-
- `Registry` reload/swap sugar;
|
|
26
|
-
- optional ActiveRecord-style source helpers that do not add a Rails runtime
|
|
27
|
-
dependency.
|
|
28
|
-
|
|
29
|
-
`strategy: :auto`, Ractor support, callback/search APIs, no-allocation point
|
|
30
|
-
query optimization, geodesic helpers, projections, and no-GVL execution are not
|
|
31
|
-
claimed in this release.
|
|
11
|
+
The gem is focused on fast in-process planar geometry parsing, predicates, format conversion, GeoJSON FeatureCollection imports, and immutable geofencing indexes. It is not a full GIS system.
|
|
32
12
|
|
|
33
13
|
## Installation
|
|
34
14
|
|
|
35
|
-
Add this line to your application's Gemfile:
|
|
36
|
-
|
|
37
15
|
```ruby
|
|
38
16
|
gem "tg_geometry"
|
|
39
17
|
```
|
|
@@ -44,17 +22,13 @@ Then run:
|
|
|
44
22
|
bundle install
|
|
45
23
|
```
|
|
46
24
|
|
|
47
|
-
The extension
|
|
48
|
-
PROJ, GDAL, system TG, or system rtree dependency.
|
|
25
|
+
The extension builds from vendored C sources. It does not require GEOS, PostGIS, PROJ, GDAL, system TG, or system rtree.
|
|
49
26
|
|
|
50
|
-
Supported
|
|
51
|
-
Windows is not supported in this release.
|
|
27
|
+
Supported platforms: Linux and macOS on x86_64/aarch64. Windows is not supported for the first release.
|
|
52
28
|
|
|
53
|
-
##
|
|
29
|
+
## Parsing and predicates
|
|
54
30
|
|
|
55
31
|
```ruby
|
|
56
|
-
require "tg/geometry"
|
|
57
|
-
|
|
58
32
|
zone = TG::Geometry.parse_geojson(<<~JSON)
|
|
59
33
|
{
|
|
60
34
|
"type": "Polygon",
|
|
@@ -67,16 +41,11 @@ zone.type # => :polygon
|
|
|
67
41
|
zone.covers_xy?(5, 5) # => true
|
|
68
42
|
zone.covers_xy?(0, 0) # => true, boundary is covered
|
|
69
43
|
zone.bbox # => #<TG::Geometry::Rect ...>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
wkb = zone.to_wkb
|
|
44
|
+
zone.to_wkt
|
|
45
|
+
zone.to_wkb
|
|
73
46
|
```
|
|
74
47
|
|
|
75
|
-
|
|
76
|
-
manually freed from Ruby. Native memory is released by Ruby GC through the typed
|
|
77
|
-
data wrapper.
|
|
78
|
-
|
|
79
|
-
## Parse API
|
|
48
|
+
Parse shortcuts:
|
|
80
49
|
|
|
81
50
|
```ruby
|
|
82
51
|
TG::Geometry.parse(str, format: :auto, index: :ystripes)
|
|
@@ -87,62 +56,50 @@ TG::Geometry.parse_hex(str, index: :ystripes)
|
|
|
87
56
|
TG::Geometry.parse_geobin(bytes, index: :ystripes)
|
|
88
57
|
```
|
|
89
58
|
|
|
90
|
-
|
|
59
|
+
`TG::Geometry::Geom` objects are immutable and cannot be manually allocated or manually freed from Ruby.
|
|
91
60
|
|
|
92
|
-
|
|
93
|
-
- `:geojson`
|
|
94
|
-
- `:wkt`
|
|
95
|
-
- `:wkb`
|
|
96
|
-
- `:hex`
|
|
97
|
-
- `:geobin`
|
|
61
|
+
## Constructors
|
|
98
62
|
|
|
99
|
-
|
|
63
|
+
Construct simple planar geometries directly from Ruby arrays without parsing strings or bytes:
|
|
100
64
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
65
|
+
```ruby
|
|
66
|
+
line = TG::Geometry.line_string([[0.0, 0.0], [10.0, 0.0]], index: :natural, srid: 4326)
|
|
67
|
+
poly = TG::Geometry.polygon(
|
|
68
|
+
[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]],
|
|
69
|
+
holes: [[[2, 2], [4, 2], [4, 4], [2, 4], [2, 2]]],
|
|
70
|
+
index: :ystripes,
|
|
71
|
+
srid: 4326
|
|
72
|
+
)
|
|
73
|
+
mp = TG::Geometry.multi_polygon([
|
|
74
|
+
{ exterior: [[0, 0], [1, 0], [1, 1], [0, 0]], holes: [] },
|
|
75
|
+
[[10, 10], [11, 10], [11, 11], [10, 10]]
|
|
76
|
+
])
|
|
77
|
+
```
|
|
105
78
|
|
|
106
|
-
|
|
107
|
-
`TG::Geometry::ArgumentError`, which inherits from Ruby's `::ArgumentError`.
|
|
79
|
+
Constructors are strict: rings must already be closed, invalid coordinates raise `TG::Geometry::ArgumentError`, and no winding/self-intersection fixes are performed. `srid:` is metadata on the wrapper; it does not alter coordinates or perform reprojection.
|
|
108
80
|
|
|
109
|
-
##
|
|
81
|
+
## SRID and EWKB
|
|
110
82
|
|
|
111
|
-
|
|
83
|
+
`parse_wkb` and `parse_hex` preserve EWKB SRID metadata when the EWKB SRID flag is present:
|
|
112
84
|
|
|
113
85
|
```ruby
|
|
114
|
-
geom.
|
|
115
|
-
geom.
|
|
116
|
-
geom.covers_xy?(x, y)
|
|
117
|
-
geom.contains?(other_geom)
|
|
118
|
-
geom.intersects?(other_geom)
|
|
119
|
-
geom.to_geojson
|
|
120
|
-
geom.to_wkt
|
|
121
|
-
geom.to_wkb
|
|
86
|
+
geom = TG::Geometry.parse_wkb(postgis_bytea)
|
|
87
|
+
geom.srid # => 4326, 3857, 0, or nil for plain WKB
|
|
122
88
|
```
|
|
123
89
|
|
|
124
|
-
|
|
125
|
-
accessors, and read-only borrowed child wrappers. See:
|
|
90
|
+
`to_wkb` always writes plain WKB. Use `to_ewkb` when a PostGIS-compatible SRID-bearing payload is required:
|
|
126
91
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
92
|
+
```ruby
|
|
93
|
+
ewkb = geom.to_ewkb # uses geom.srid
|
|
94
|
+
ewkb = geom.to_ewkb(srid: 4326) # explicit override
|
|
95
|
+
```
|
|
130
96
|
|
|
131
|
-
|
|
132
|
-
semantics over the fastest possible no-allocation path. Query methods construct
|
|
133
|
-
a temporary TG point geometry and free it before returning. A future optimized
|
|
134
|
-
point path requires boundary and hole-boundary equivalence tests plus benchmark
|
|
135
|
-
proof.
|
|
97
|
+
SRID is metadata only. `tg_geometry` does not check SRID compatibility, transform coordinates, or calculate geodesic distances.
|
|
136
98
|
|
|
137
|
-
## Rect
|
|
99
|
+
## Rect
|
|
138
100
|
|
|
139
101
|
```ruby
|
|
140
102
|
rect = TG::Geometry::Rect.new(0, 0, 10, 10)
|
|
141
|
-
|
|
142
|
-
rect.min_x
|
|
143
|
-
rect.min_y
|
|
144
|
-
rect.max_x
|
|
145
|
-
rect.max_y
|
|
146
103
|
rect.center # => [5.0, 5.0]
|
|
147
104
|
rect.contains_point?(5, 5) # => true
|
|
148
105
|
rect.intersects?(other_rect)
|
|
@@ -150,16 +107,10 @@ rect.expand_to_include(other_rect)
|
|
|
150
107
|
rect.expand_to_include_point(x, y)
|
|
151
108
|
```
|
|
152
109
|
|
|
153
|
-
`Rect` rejects non-finite coordinates and invalid coordinate order. It is frozen
|
|
154
|
-
after construction.
|
|
155
|
-
|
|
156
|
-
There is intentionally no first-release `Rect#contains?` method because the name
|
|
157
|
-
is ambiguous. Use `contains_point?`.
|
|
110
|
+
`Rect` rejects non-finite coordinates and invalid coordinate order. It is frozen after construction.
|
|
158
111
|
|
|
159
112
|
## Immutable Index
|
|
160
113
|
|
|
161
|
-
`TG::Geometry::Index` is built once and then read-only forever.
|
|
162
|
-
|
|
163
114
|
```ruby
|
|
164
115
|
entries = [
|
|
165
116
|
[:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}'],
|
|
@@ -183,7 +134,7 @@ index.covering_ids(5, 5) # => [:zone_a]
|
|
|
183
134
|
index.intersecting_rect(0, 0, 25, 25)
|
|
184
135
|
```
|
|
185
136
|
|
|
186
|
-
Accepted input
|
|
137
|
+
Accepted input shape:
|
|
187
138
|
|
|
188
139
|
```ruby
|
|
189
140
|
[[id1, object1], [id2, object2], ...]
|
|
@@ -194,17 +145,13 @@ Rules:
|
|
|
194
145
|
- `entries` must be an Array.
|
|
195
146
|
- Every entry must be a two-element Array.
|
|
196
147
|
- `id` may be any Ruby object except `nil`.
|
|
197
|
-
- `false` ids are accepted, but discouraged because `find_covering` uses `nil`
|
|
198
|
-
for no match.
|
|
199
148
|
- Duplicate ids are allowed.
|
|
200
|
-
- Returned ids are the same Ruby objects stored in the index
|
|
201
|
-
copied, frozen, stringified, or duplicated.
|
|
149
|
+
- Returned ids are the same Ruby objects stored in the index.
|
|
202
150
|
- Result order is insertion order for both `:flat` and `:rtree`.
|
|
203
151
|
|
|
204
152
|
Accepted `via:` modes:
|
|
205
153
|
|
|
206
|
-
- `:geom` — borrow an existing `TG::Geometry::Geom
|
|
207
|
-
wrapper so the borrowed native pointer remains valid.
|
|
154
|
+
- `:geom` — borrow an existing `TG::Geometry::Geom` and keep its owner alive.
|
|
208
155
|
- `:geojson` — parse and own native TG geometries inside the index.
|
|
209
156
|
- `:wkb` — parse and own native TG geometries inside the index.
|
|
210
157
|
|
|
@@ -213,21 +160,82 @@ Accepted strategies:
|
|
|
213
160
|
- `:flat`
|
|
214
161
|
- `:rtree`
|
|
215
162
|
|
|
216
|
-
`strategy: :auto` is not exposed in this release. The benchmark output does not
|
|
217
|
-
support a single universal threshold: flat scan may win for early insertion-order
|
|
218
|
-
hits or heavily overlapping datasets, while rtree may win for misses, later hits,
|
|
219
|
-
or selective rectangle queries. Choose the strategy explicitly and benchmark on
|
|
220
|
-
your own data.
|
|
221
|
-
|
|
222
163
|
Accepted predicates:
|
|
223
164
|
|
|
224
165
|
- `:covers` — default for geofencing; boundary points are included.
|
|
225
|
-
- `:contains` — stricter
|
|
166
|
+
- `:contains` — stricter containment semantics.
|
|
226
167
|
|
|
227
|
-
|
|
168
|
+
The `predicate:` option affects only the legacy point-based query methods (`find_covering`, `covering_ids(x, y)`, `covering_ids_batch_packed`). The geometry-based query methods (`intersecting_geom_ids`, `covering_geom_ids`, `containing_geom_ids`) use their own predicates based on the method name.
|
|
169
|
+
|
|
170
|
+
`strategy: :auto` is intentionally not exposed. Choose the strategy explicitly and benchmark on your own data.
|
|
171
|
+
|
|
172
|
+
## Geometry-based index queries
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
query = TG::Geometry.polygon([[1, 1], [2, 1], [2, 2], [1, 1]])
|
|
176
|
+
index.intersecting_geom_ids(query)
|
|
177
|
+
index.covering_geom_ids(query)
|
|
178
|
+
index.containing_geom_ids(query)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Predicate direction is explicit:
|
|
182
|
+
|
|
183
|
+
| Method | Predicate direction | Boundary semantics |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `intersecting_geom_ids(query)` | stored geom intersects query | any intersection |
|
|
186
|
+
| `covering_geom_ids(query)` | stored geom covers query | boundary included |
|
|
187
|
+
| `containing_geom_ids(query)` | stored geom contains query | strict interior; boundary excluded |
|
|
188
|
+
|
|
189
|
+
Results are ids only and preserve insertion order. Duplicate ids remain possible if duplicate ids were inserted.
|
|
190
|
+
|
|
191
|
+
## GeoJSON FeatureSource
|
|
192
|
+
|
|
193
|
+
`TG::Geometry::FeatureSource` reads GeoJSON `FeatureCollection` sources without `JSON.parse` of the whole document into Ruby Hash/Array objects.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
entries = TG::Geometry::FeatureSource.read_entries_file(
|
|
197
|
+
"zones.geojson",
|
|
198
|
+
id: ["properties", "@id"],
|
|
199
|
+
only: [:polygon, :multipolygon]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
index = TG::Geometry::Index.build(
|
|
203
|
+
entries,
|
|
204
|
+
via: :geojson,
|
|
205
|
+
strategy: :rtree,
|
|
206
|
+
predicate: :covers
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
For imports that also need raw properties JSON:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
features = TG::Geometry::FeatureSource.read_features_file(
|
|
214
|
+
"zones.geojson",
|
|
215
|
+
id: ["properties", "@id"],
|
|
216
|
+
report: true,
|
|
217
|
+
on_invalid: :skip
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
features[:features].each do |id, geometry_json, properties_json|
|
|
221
|
+
# Store geometry_json and parse properties_json in application code if needed.
|
|
222
|
+
end
|
|
223
|
+
```
|
|
228
224
|
|
|
229
|
-
For
|
|
230
|
-
|
|
225
|
+
For direct file-to-index loading:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
index = TG::Geometry::FeatureSource.build_index_file(
|
|
229
|
+
"zones.geojson",
|
|
230
|
+
id: ["properties", "@id"],
|
|
231
|
+
strategy: :rtree,
|
|
232
|
+
predicate: :covers
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
FeatureSource methods are explicit: use `_file` for paths, `_json` for raw content strings, and `_io` for IO objects. There is no path/content auto-detection.
|
|
237
|
+
|
|
238
|
+
## Packed batch point queries
|
|
231
239
|
|
|
232
240
|
```ruby
|
|
233
241
|
points = [5.0, 5.0, 25.0, 25.0].pack("d*")
|
|
@@ -235,18 +243,23 @@ index.covering_ids_batch_packed(points)
|
|
|
235
243
|
# => [:zone_a, :zone_b]
|
|
236
244
|
```
|
|
237
245
|
|
|
238
|
-
Input
|
|
246
|
+
Input is a Ruby String containing native-endian doubles in `lon, lat` pairs. Length must be a multiple of 16 bytes. Empty string returns `[]`.
|
|
247
|
+
|
|
248
|
+
## Nearest segment
|
|
249
|
+
|
|
250
|
+
`Line#nearest_segment(x, y)` and `Ring#nearest_segment(x, y)` return a frozen `TG::Geometry::NearestSegment`:
|
|
239
251
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
252
|
+
```ruby
|
|
253
|
+
nearest = polygon.polygon.exterior_ring.nearest_segment(5, 5)
|
|
254
|
+
nearest.segment # => TG::Geometry::Segment
|
|
255
|
+
nearest.index # => segment index
|
|
256
|
+
nearest.distance # => Float
|
|
257
|
+
nearest.point # => [x, y] projection on the segment
|
|
258
|
+
```
|
|
245
259
|
|
|
246
|
-
|
|
247
|
-
Do not use it as a cross-platform serialized file format.
|
|
260
|
+
Distance is planar Euclidean distance in input coordinate units. It is not meters unless your input coordinates are already meters. Equal-distance tie-breaks follow tg iteration order and are not API-stable.
|
|
248
261
|
|
|
249
|
-
## Registry
|
|
262
|
+
## Registry helper
|
|
250
263
|
|
|
251
264
|
`Registry` is Ruby-level sugar over immutable indexes:
|
|
252
265
|
|
|
@@ -266,53 +279,32 @@ registry.reload!
|
|
|
266
279
|
registry.find_covering(5, 5)
|
|
267
280
|
```
|
|
268
281
|
|
|
269
|
-
Reload builds a new
|
|
270
|
-
successful build:
|
|
271
|
-
|
|
272
|
-
```ruby
|
|
273
|
-
new_index = TG::Geometry::Index.build(entries, via: :geojson, strategy: :rtree)
|
|
274
|
-
@index = new_index
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Old indexes remain alive while existing readers hold references to them. There
|
|
278
|
-
is no in-place mutation, no public `add`, `delete`, `clear`, or `rebuild!` API on
|
|
279
|
-
`Index`.
|
|
282
|
+
Reload builds a new immutable index first and swaps the reference only after a successful build. Existing readers keep using the previous index safely.
|
|
280
283
|
|
|
281
|
-
|
|
284
|
+
## ActiveRecord integration — read-only
|
|
282
285
|
|
|
283
|
-
|
|
286
|
+
`TG::Geometry::ActiveRecordType` is an optional read-only convenience type for PostGIS columns. It is not required by `tg/geometry`; load it explicitly:
|
|
284
287
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
| Resource | Allocator | Deallocator | Owner |
|
|
288
|
-
|---|---|---|---|
|
|
289
|
-
| `tg_geom_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby `Geom` object |
|
|
290
|
-
| TG geometry in `Geom` | TG parser/constructor | `tg_geom_free` | `Geom` wrapper |
|
|
291
|
-
| `tg_index_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby `Index` object |
|
|
292
|
-
| Index entries array | `calloc` | `free` | `Index` |
|
|
293
|
-
| TG geometry via `:geojson` / `:wkb` | TG parser | `tg_geom_free` | `Index` |
|
|
294
|
-
| TG geometry via `:geom` | Existing `Geom` wrapper | Existing `Geom` wrapper | Borrowed by `Index` through `geom_owner` |
|
|
295
|
-
| rtree internals | custom `tg_rtree_malloc` with header | custom `tg_rtree_free` | rtree / `Index` accounting |
|
|
296
|
-
| Ruby ids | Ruby VM | Ruby GC | Marked and compacted by `Index` |
|
|
288
|
+
```ruby
|
|
289
|
+
require "tg/geometry/active_record_type"
|
|
297
290
|
|
|
298
|
-
|
|
299
|
-
|
|
291
|
+
class Zone < ApplicationRecord
|
|
292
|
+
attribute :geom, TG::Geometry::ActiveRecordType.new
|
|
293
|
+
end
|
|
300
294
|
|
|
301
|
-
|
|
295
|
+
zone.geom.srid
|
|
296
|
+
zone.geom.covers_xy?(lon, lat)
|
|
297
|
+
```
|
|
302
298
|
|
|
303
|
-
|
|
299
|
+
It can deserialize EWKB bytes, hex EWKB, `\x`-prefixed hex EWKB, GeoJSON, and WKT. Writing `Geom` values is intentionally unsupported in v0.3.0. User applications need `activemodel >= 6.0`; `activerecord` is not a gem dependency.
|
|
304
300
|
|
|
305
|
-
|
|
306
|
-
from normal Ruby threads is supported by design and covered by tests.
|
|
301
|
+
## Memory and concurrency
|
|
307
302
|
|
|
308
|
-
The
|
|
309
|
-
paths. This is intentional: the rtree allocator calls Ruby GC accounting APIs,
|
|
310
|
-
and no-GVL execution would require separate input-copying and allocator-accounting
|
|
311
|
-
design.
|
|
303
|
+
The implementation uses explicit allocator pairs and Ruby GC accounting for native memory. `ObjectSpace.memsize_of(index)` includes entries, owned TG geometries, and exact rtree allocation bytes. Borrowed geometries are not double-counted by the index.
|
|
312
304
|
|
|
313
|
-
|
|
305
|
+
`Index` and `Geom` are immutable after construction. Concurrent read-only use from normal Ruby threads is supported. Short query/parse/write paths keep the GVL. FeatureSource bulk loading uses a C-only no-GVL heavy phase for file read, JSON traversal, and TG geometry parsing, then reacquires the GVL to create Ruby objects or transfer ownership into the final Index. On Ruby versions that expose `RB_NOGVL_OFFLOAD_SAFE`, that no-GVL phase is marked offload-safe for the Ruby VM. On older Rubies it still releases the GVL for other Ruby threads, but no explicit Fiber scheduler friendliness is claimed.
|
|
314
306
|
|
|
315
|
-
|
|
307
|
+
No Ractor support and no universal performance claim are advertised.
|
|
316
308
|
|
|
317
309
|
## Benchmarks
|
|
318
310
|
|
|
@@ -326,37 +318,34 @@ bundle exec ruby benchmark/objectspace_memsize.rb
|
|
|
326
318
|
bundle exec ruby benchmark/rss_stability.rb
|
|
327
319
|
bundle exec ruby benchmark/gvl_threshold.rb
|
|
328
320
|
bundle exec ruby benchmark/falcon_concurrency.rb
|
|
321
|
+
bundle exec ruby benchmark/feature_source.rb
|
|
322
|
+
bundle exec ruby benchmark/geom_query.rb
|
|
323
|
+
bundle exec ruby benchmark/nearest_segment.rb
|
|
324
|
+
bundle exec ruby benchmark/ewkb_roundtrip.rb
|
|
329
325
|
```
|
|
330
326
|
|
|
331
|
-
|
|
332
|
-
the larger matrix where supported.
|
|
333
|
-
|
|
334
|
-
The repository benchmarks are engineering tools, not universal marketing claims.
|
|
335
|
-
Do not copy upstream TG C benchmark numbers as Ruby gem performance claims.
|
|
327
|
+
The benchmarks are engineering tools, not marketing claims.
|
|
336
328
|
|
|
337
329
|
## Limitations
|
|
338
330
|
|
|
339
331
|
`tg_geometry` is not a full GIS system.
|
|
340
332
|
|
|
341
|
-
Not included
|
|
342
|
-
|
|
343
|
-
-
|
|
344
|
-
-
|
|
345
|
-
-
|
|
346
|
-
-
|
|
347
|
-
-
|
|
348
|
-
-
|
|
349
|
-
-
|
|
350
|
-
-
|
|
351
|
-
-
|
|
352
|
-
-
|
|
353
|
-
-
|
|
354
|
-
-
|
|
355
|
-
|
|
356
|
-
TG works in planar XY coordinates. If lon/lat coordinates are passed in, length,
|
|
357
|
-
area, and perimeter-style values are in input coordinate units, not meters.
|
|
358
|
-
Use PostGIS, GEOS, PROJ, or other GIS tooling when full GIS functionality is
|
|
359
|
-
needed.
|
|
333
|
+
Not included:
|
|
334
|
+
|
|
335
|
+
- Geodesic / Haversine distance;
|
|
336
|
+
- Projection / reprojection;
|
|
337
|
+
- Buffer / union / difference / convex hull;
|
|
338
|
+
- Index nearest_ids (KNN);
|
|
339
|
+
- GeoBIN bbox helpers;
|
|
340
|
+
- Streaming FeatureSource;
|
|
341
|
+
- Index serialization / mmap;
|
|
342
|
+
- Ractor support;
|
|
343
|
+
- Windows / JRuby;
|
|
344
|
+
- Write-side ActiveRecordType / AR scopes / migrations;
|
|
345
|
+
- Z/M variants of array constructors;
|
|
346
|
+
- Public `release_gvl:` option.
|
|
347
|
+
|
|
348
|
+
TG works in planar XY coordinates. If lon/lat coordinates are passed in, length, area, perimeter, and nearest-segment distances are in input coordinate units, not meters.
|
|
360
349
|
|
|
361
350
|
## Development
|
|
362
351
|
|
|
@@ -369,17 +358,12 @@ bundle exec rake spec
|
|
|
369
358
|
Useful targeted checks:
|
|
370
359
|
|
|
371
360
|
```bash
|
|
372
|
-
bundle exec rspec spec/
|
|
373
|
-
bundle exec rspec spec/
|
|
374
|
-
bundle exec rspec spec/
|
|
375
|
-
bundle exec rspec spec/
|
|
361
|
+
bundle exec rspec spec/batch_packed_spec.rb
|
|
362
|
+
bundle exec rspec spec/memory_gc_spec.rb
|
|
363
|
+
bundle exec rspec spec/concurrency_spec.rb
|
|
364
|
+
bundle exec rspec spec/fuzz_spec.rb
|
|
376
365
|
```
|
|
377
366
|
|
|
378
|
-
Memory-tool CI jobs for ASAN and Valgrind are intentionally left as OPEN QUESTION
|
|
379
|
-
placeholders until the exact setup is approved. Do not replace them with guessed
|
|
380
|
-
configuration.
|
|
381
|
-
|
|
382
367
|
## License
|
|
383
368
|
|
|
384
|
-
MIT. Vendored upstream license files for `tidwall/tg` and `tidwall/
|
|
385
|
-
included under `ext/tg_geometry/vendor/`.
|
|
369
|
+
MIT. Vendored upstream license files for `tidwall/tg`, `tidwall/rtree.c`, and `tidwall/json.c` are included under `ext/tg_geometry/vendor/`.
|