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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 01f50a6c3b237bc983741eaf46a6440220c023dde0d91e8a1e64debb0dd8cf9e
|
|
4
|
+
data.tar.gz: e07214e6f201842029f415e1dc4c39f77cd9c003187cc9068013f72300f5398a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c80efd671c29339c6023a4b7b616eb0f4eba73513aecea5dea1f239797a8c8c3c4b9a9fd12a1bc69d359a599b3dfc3c4891a8a2073d29e5525083538379b3dea
|
|
7
|
+
data.tar.gz: '0508a2a6f736791048212c214b75533131c83d0e38029a9b2a13d7860991f882868faeb6237f6305645fc0b4a4819ac0fd21b2c3968006e2a85d2b736425a7ff'
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - unreleased
|
|
4
|
+
|
|
5
|
+
Initial release-core implementation for `tg_geometry`.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Canonical public require path: `require "tg/geometry"`.
|
|
10
|
+
- Public namespace: `TG::Geometry`.
|
|
11
|
+
- Native extension build through `ext/tg_geometry/extconf.rb`.
|
|
12
|
+
- Vendored `tidwall/tg` and `tidwall/rtree.c` sources with pinned `VERSION` files and upstream license files.
|
|
13
|
+
- Error classes:
|
|
14
|
+
- `TG::Geometry::Error`
|
|
15
|
+
- `TG::Geometry::ParseError`
|
|
16
|
+
- `TG::Geometry::ArgumentError < ::ArgumentError`
|
|
17
|
+
- `TG::Geometry::FrozenIndexError`
|
|
18
|
+
- 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
|
+
- Immutable `TG::Geometry::Rect` API.
|
|
29
|
+
- Immutable `TG::Geometry::Index.build` with strict `[[id, object], ...]` entry format.
|
|
30
|
+
- Index ingestion modes:
|
|
31
|
+
- `via: :geom` borrowed geometry with `geom_owner` lifetime protection;
|
|
32
|
+
- `via: :geojson` owned geometry;
|
|
33
|
+
- `via: :wkb` owned geometry.
|
|
34
|
+
- Index strategies:
|
|
35
|
+
- `:flat`
|
|
36
|
+
- `:rtree`
|
|
37
|
+
- Deterministic insertion-order results for flat and rtree queries.
|
|
38
|
+
- Exact rtree memory accounting through a custom malloc/free allocator with headers.
|
|
39
|
+
- Native-endian packed point batch API: `TG::Geometry::Index#covering_ids_batch_packed`.
|
|
40
|
+
- Debug-only test hooks under `TG_DEBUG_TEST=1` for allocation failure simulation and byte counter inspection.
|
|
41
|
+
- Block 14 memory/GC/compaction hardening specs.
|
|
42
|
+
- Benchmark harnesses:
|
|
43
|
+
- `benchmark/parse_throughput.rb`
|
|
44
|
+
- `benchmark/gvl_threshold.rb`
|
|
45
|
+
- `benchmark/flat_vs_rtree.rb`
|
|
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`
|
|
58
|
+
|
|
59
|
+
### Not included
|
|
60
|
+
|
|
61
|
+
- `strategy: :auto` is not part of the release-core contract; it is tracked as Expansion Block A below.
|
|
62
|
+
- No Ractor support claim.
|
|
63
|
+
- No no-GVL execution.
|
|
64
|
+
- No full GIS, routing, geocoding, projections, geodesic helpers, nearest POI index, or result-geometry overlay operations.
|
|
65
|
+
- No public performance claims until benchmark results are produced by this gem.
|
|
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
|
|
99
|
+
|
|
100
|
+
- Expansion Block F callback/search APIs remain blocked until a callback safety contract, exception semantics, GVL rules, and callback overhead benchmarks are approved.
|
|
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.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roman Haydarov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# tg_geometry
|
|
2
|
+
|
|
3
|
+
`tg_geometry` is a Ruby C extension around the vendored `tidwall/tg` geometry
|
|
4
|
+
library and `tidwall/rtree.c`.
|
|
5
|
+
|
|
6
|
+
It exposes the public Ruby namespace `TG::Geometry` and the canonical require
|
|
7
|
+
path:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "tg/geometry"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The gem targets fast in-process planar geometry parsing, predicates,
|
|
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.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Add this line to your application's Gemfile:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "tg_geometry"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bundle install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The extension is built from vendored C sources. There is no GEOS, PostGIS,
|
|
48
|
+
PROJ, GDAL, system TG, or system rtree dependency.
|
|
49
|
+
|
|
50
|
+
Supported first-release platforms are Linux and macOS on x86_64/aarch64.
|
|
51
|
+
Windows is not supported in this release.
|
|
52
|
+
|
|
53
|
+
## Basic parsing and predicates
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
require "tg/geometry"
|
|
57
|
+
|
|
58
|
+
zone = TG::Geometry.parse_geojson(<<~JSON)
|
|
59
|
+
{
|
|
60
|
+
"type": "Polygon",
|
|
61
|
+
"coordinates": [[[0,0], [10,0], [10,10], [0,10], [0,0]]]
|
|
62
|
+
}
|
|
63
|
+
JSON
|
|
64
|
+
|
|
65
|
+
zone.frozen? # => true
|
|
66
|
+
zone.type # => :polygon
|
|
67
|
+
zone.covers_xy?(5, 5) # => true
|
|
68
|
+
zone.covers_xy?(0, 0) # => true, boundary is covered
|
|
69
|
+
zone.bbox # => #<TG::Geometry::Rect ...>
|
|
70
|
+
|
|
71
|
+
wkt = zone.to_wkt
|
|
72
|
+
wkb = zone.to_wkb
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`TG::Geometry::Geom` objects are immutable. They cannot be manually allocated or
|
|
76
|
+
manually freed from Ruby. Native memory is released by Ruby GC through the typed
|
|
77
|
+
data wrapper.
|
|
78
|
+
|
|
79
|
+
## Parse API
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
TG::Geometry.parse(str, format: :auto, index: :ystripes)
|
|
83
|
+
TG::Geometry.parse_geojson(str, index: :ystripes)
|
|
84
|
+
TG::Geometry.parse_wkt(str, index: :ystripes)
|
|
85
|
+
TG::Geometry.parse_wkb(bytes, index: :ystripes)
|
|
86
|
+
TG::Geometry.parse_hex(str, index: :ystripes)
|
|
87
|
+
TG::Geometry.parse_geobin(bytes, index: :ystripes)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Accepted `format:` values for `parse` are:
|
|
91
|
+
|
|
92
|
+
- `:auto`
|
|
93
|
+
- `:geojson`
|
|
94
|
+
- `:wkt`
|
|
95
|
+
- `:wkb`
|
|
96
|
+
- `:hex`
|
|
97
|
+
- `:geobin`
|
|
98
|
+
|
|
99
|
+
Accepted TG internal polygon index values are:
|
|
100
|
+
|
|
101
|
+
- `:default`
|
|
102
|
+
- `:none`
|
|
103
|
+
- `:natural`
|
|
104
|
+
- `:ystripes`
|
|
105
|
+
|
|
106
|
+
Parse failures raise `TG::Geometry::ParseError`. Invalid options raise
|
|
107
|
+
`TG::Geometry::ArgumentError`, which inherits from Ruby's `::ArgumentError`.
|
|
108
|
+
|
|
109
|
+
## Geom API
|
|
110
|
+
|
|
111
|
+
Release-core methods:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
geom.type
|
|
115
|
+
geom.bbox
|
|
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
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Expanded methods include additional predicates, format writers, metadata
|
|
125
|
+
accessors, and read-only borrowed child wrappers. See:
|
|
126
|
+
|
|
127
|
+
- `docs/FORMAT_COVERAGE.md`
|
|
128
|
+
- `docs/LOW_LEVEL_GEOMETRY.md`
|
|
129
|
+
- `docs/FULL_TG_API_COVERAGE.md`
|
|
130
|
+
|
|
131
|
+
For point predicates, this release prioritizes exact `covers` / `contains`
|
|
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.
|
|
136
|
+
|
|
137
|
+
## Rect API
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
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
|
+
rect.center # => [5.0, 5.0]
|
|
147
|
+
rect.contains_point?(5, 5) # => true
|
|
148
|
+
rect.intersects?(other_rect)
|
|
149
|
+
rect.expand_to_include(other_rect)
|
|
150
|
+
rect.expand_to_include_point(x, y)
|
|
151
|
+
```
|
|
152
|
+
|
|
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?`.
|
|
158
|
+
|
|
159
|
+
## Immutable Index
|
|
160
|
+
|
|
161
|
+
`TG::Geometry::Index` is built once and then read-only forever.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
entries = [
|
|
165
|
+
[:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}'],
|
|
166
|
+
[:zone_b, '{"type":"Polygon","coordinates":[[[20,20],[30,20],[30,30],[20,30],[20,20]]]}']
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
index = TG::Geometry::Index.build(
|
|
170
|
+
entries,
|
|
171
|
+
via: :geojson,
|
|
172
|
+
strategy: :rtree,
|
|
173
|
+
predicate: :covers,
|
|
174
|
+
geometry_index: :ystripes
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
index.frozen? # => true
|
|
178
|
+
index.size # => 2
|
|
179
|
+
index.strategy # => :rtree
|
|
180
|
+
index.predicate # => :covers
|
|
181
|
+
index.find_covering(5, 5) # => :zone_a
|
|
182
|
+
index.covering_ids(5, 5) # => [:zone_a]
|
|
183
|
+
index.intersecting_rect(0, 0, 25, 25)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Accepted input format:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
[[id1, object1], [id2, object2], ...]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Rules:
|
|
193
|
+
|
|
194
|
+
- `entries` must be an Array.
|
|
195
|
+
- Every entry must be a two-element Array.
|
|
196
|
+
- `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
|
+
- Duplicate ids are allowed.
|
|
200
|
+
- Returned ids are the same Ruby objects stored in the index; they are not
|
|
201
|
+
copied, frozen, stringified, or duplicated.
|
|
202
|
+
- Result order is insertion order for both `:flat` and `:rtree`.
|
|
203
|
+
|
|
204
|
+
Accepted `via:` modes:
|
|
205
|
+
|
|
206
|
+
- `:geom` — borrow an existing `TG::Geometry::Geom`; the index marks the owner
|
|
207
|
+
wrapper so the borrowed native pointer remains valid.
|
|
208
|
+
- `:geojson` — parse and own native TG geometries inside the index.
|
|
209
|
+
- `:wkb` — parse and own native TG geometries inside the index.
|
|
210
|
+
|
|
211
|
+
Accepted strategies:
|
|
212
|
+
|
|
213
|
+
- `:flat`
|
|
214
|
+
- `:rtree`
|
|
215
|
+
|
|
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
|
+
Accepted predicates:
|
|
223
|
+
|
|
224
|
+
- `:covers` — default for geofencing; boundary points are included.
|
|
225
|
+
- `:contains` — stricter OGC-style containment semantics.
|
|
226
|
+
|
|
227
|
+
## Packed batch point queries
|
|
228
|
+
|
|
229
|
+
For high-throughput same-process point lookups, the index supports a packed
|
|
230
|
+
native-endian double input format:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
points = [5.0, 5.0, 25.0, 25.0].pack("d*")
|
|
234
|
+
index.covering_ids_batch_packed(points)
|
|
235
|
+
# => [:zone_a, :zone_b]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Input format:
|
|
239
|
+
|
|
240
|
+
- Ruby String treated as raw bytes.
|
|
241
|
+
- Native-endian doubles.
|
|
242
|
+
- Pairs of `lon, lat`.
|
|
243
|
+
- Length must be a multiple of 16 bytes.
|
|
244
|
+
- Empty string returns `[]`.
|
|
245
|
+
|
|
246
|
+
This format is intentionally native-endian for same-process speed and simplicity.
|
|
247
|
+
Do not use it as a cross-platform serialized file format.
|
|
248
|
+
|
|
249
|
+
## Registry reload pattern
|
|
250
|
+
|
|
251
|
+
`Registry` is Ruby-level sugar over immutable indexes:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
class DeliveryZones < TG::Geometry::Registry
|
|
255
|
+
source do
|
|
256
|
+
[
|
|
257
|
+
[:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}']
|
|
258
|
+
]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
index_options via: :geojson, strategy: :rtree, predicate: :covers
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
registry = DeliveryZones.new
|
|
265
|
+
registry.reload!
|
|
266
|
+
registry.find_covering(5, 5)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Reload builds a new full immutable index first and swaps the reference only after
|
|
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`.
|
|
280
|
+
|
|
281
|
+
See `docs/REGISTRY.md` and `docs/ACTIVE_RECORD.md` for the expanded helpers.
|
|
282
|
+
|
|
283
|
+
## Memory ownership model
|
|
284
|
+
|
|
285
|
+
The implementation uses explicit allocator pairs and GC accounting:
|
|
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` |
|
|
297
|
+
|
|
298
|
+
`ObjectSpace.memsize_of(index)` includes entries, owned TG geometries, and exact
|
|
299
|
+
rtree allocation bytes. Borrowed geometries are not double-counted by the index.
|
|
300
|
+
|
|
301
|
+
See `docs/MEMORY_OWNERSHIP.md` for the full table and cleanup rules.
|
|
302
|
+
|
|
303
|
+
## Concurrency model
|
|
304
|
+
|
|
305
|
+
`Index` and `Geom` are immutable after construction. Concurrent read-only use
|
|
306
|
+
from normal Ruby threads is supported by design and covered by tests.
|
|
307
|
+
|
|
308
|
+
The first release keeps GVL for parse, write, query, batch, and rtree build/free
|
|
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.
|
|
312
|
+
|
|
313
|
+
No Ractor support is claimed.
|
|
314
|
+
|
|
315
|
+
See `docs/CONCURRENCY.md` and `docs/RACTOR.md`.
|
|
316
|
+
|
|
317
|
+
## Benchmarks
|
|
318
|
+
|
|
319
|
+
Benchmark scripts live in `benchmark/`:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
bundle exec ruby benchmark/parse_throughput.rb
|
|
323
|
+
bundle exec ruby benchmark/flat_vs_rtree.rb
|
|
324
|
+
bundle exec ruby benchmark/batch_packed_vs_loop.rb
|
|
325
|
+
bundle exec ruby benchmark/objectspace_memsize.rb
|
|
326
|
+
bundle exec ruby benchmark/rss_stability.rb
|
|
327
|
+
bundle exec ruby benchmark/gvl_threshold.rb
|
|
328
|
+
bundle exec ruby benchmark/falcon_concurrency.rb
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
By default, benchmarks use a fast local matrix. Set `TGEOMETRY_BENCH_FULL=1` for
|
|
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.
|
|
336
|
+
|
|
337
|
+
## Limitations
|
|
338
|
+
|
|
339
|
+
`tg_geometry` is not a full GIS system.
|
|
340
|
+
|
|
341
|
+
Not included in this release:
|
|
342
|
+
|
|
343
|
+
- geocoding;
|
|
344
|
+
- routing;
|
|
345
|
+
- projections;
|
|
346
|
+
- geodesic distance/area;
|
|
347
|
+
- buffer / union / difference / overlay result geometry operations;
|
|
348
|
+
- nearest POI index;
|
|
349
|
+
- Rails dependency in the core extension;
|
|
350
|
+
- Redis or external service dependency;
|
|
351
|
+
- public callback/search APIs;
|
|
352
|
+
- Ractor support claim;
|
|
353
|
+
- no-GVL execution claim;
|
|
354
|
+
- universal `:auto` strategy.
|
|
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.
|
|
360
|
+
|
|
361
|
+
## Development
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
bundle install
|
|
365
|
+
bundle exec rake compile
|
|
366
|
+
bundle exec rake spec
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Useful targeted checks:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
bundle exec rspec spec/block_12_batch_packed_spec.rb
|
|
373
|
+
bundle exec rspec spec/block_14_memory_gc_hardening_spec.rb
|
|
374
|
+
bundle exec rspec spec/block_20_concurrency_spec.rb
|
|
375
|
+
bundle exec rspec spec/block_20_fuzz_spec.rb
|
|
376
|
+
```
|
|
377
|
+
|
|
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
|
+
## License
|
|
383
|
+
|
|
384
|
+
MIT. Vendored upstream license files for `tidwall/tg` and `tidwall/rtree.c` are
|
|
385
|
+
included under `ext/tg_geometry/vendor/`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "rake/clean"
|
|
7
|
+
|
|
8
|
+
EXT_NAME = "tg_geometry_ext_geometry_ext"
|
|
9
|
+
EXT_DIR = File.expand_path("ext/tg_geometry", __dir__)
|
|
10
|
+
LIB_DIR = File.expand_path("lib", __dir__)
|
|
11
|
+
DLEXT = RbConfig::CONFIG.fetch("DLEXT")
|
|
12
|
+
PLATFORM = RbConfig::CONFIG.fetch("arch")
|
|
13
|
+
BUILD_RUBY_VERSION = RUBY_VERSION
|
|
14
|
+
BUILD_DIR = File.expand_path(File.join("tmp", PLATFORM, EXT_NAME, BUILD_RUBY_VERSION), __dir__)
|
|
15
|
+
EXT_SO = File.join(EXT_DIR, "#{EXT_NAME}.#{DLEXT}")
|
|
16
|
+
LIB_SO = File.join(LIB_DIR, "#{EXT_NAME}.#{DLEXT}")
|
|
17
|
+
|
|
18
|
+
EXT_BUILD_GLOBS = [
|
|
19
|
+
File.join(EXT_DIR, "Makefile"),
|
|
20
|
+
File.join(EXT_DIR, "mkmf.log"),
|
|
21
|
+
File.join(EXT_DIR, "*.o"),
|
|
22
|
+
File.join(EXT_DIR, "*.so"),
|
|
23
|
+
File.join(EXT_DIR, "*.bundle"),
|
|
24
|
+
File.join(EXT_DIR, "*.dll"),
|
|
25
|
+
File.join(EXT_DIR, "*.dylib")
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
CLEAN.include(
|
|
29
|
+
"tmp",
|
|
30
|
+
"lib/*.so",
|
|
31
|
+
"lib/*.bundle",
|
|
32
|
+
"lib/*.dll",
|
|
33
|
+
"lib/*.dylib",
|
|
34
|
+
*EXT_BUILD_GLOBS
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Keep the source tree clean like pq_crypto: ext/tg_geometry contains the checked-in
|
|
38
|
+
# extension sources and ext/tg_geometry/vendor contains the checked-in vendored C
|
|
39
|
+
# sources, while Makefile/mkmf.log/object/shared-library build outputs live in tmp/.
|
|
40
|
+
def remove_in_place_extension_artifacts!
|
|
41
|
+
EXT_BUILD_GLOBS.each { |pattern| FileUtils.rm_f(Dir[pattern]) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_extension!(debug: false)
|
|
45
|
+
remove_in_place_extension_artifacts!
|
|
46
|
+
FileUtils.rm_rf(BUILD_DIR)
|
|
47
|
+
FileUtils.mkdir_p(BUILD_DIR)
|
|
48
|
+
|
|
49
|
+
env = debug ? { "TG_DEBUG_TEST" => "1" } : {}
|
|
50
|
+
Dir.chdir(BUILD_DIR) do
|
|
51
|
+
sh(env, RbConfig.ruby, File.join(EXT_DIR, "extconf.rb"))
|
|
52
|
+
sh "make"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
FileUtils.mkdir_p(LIB_DIR)
|
|
56
|
+
FileUtils.cp(File.join(BUILD_DIR, "#{EXT_NAME}.#{DLEXT}"), LIB_SO)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc "Compile the native extension into tmp/ and copy it to lib/"
|
|
60
|
+
task "compile" do
|
|
61
|
+
build_extension!(debug: false)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
desc "Compile the native extension with test-only debug hooks"
|
|
65
|
+
task "compile:test" do
|
|
66
|
+
build_extension!(debug: true)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
require "rspec/core/rake_task"
|
|
71
|
+
|
|
72
|
+
RSpec::Core::RakeTask.new(:spec => "compile:test")
|
|
73
|
+
|
|
74
|
+
task :gc_stress_env do
|
|
75
|
+
ENV["RUBY_GC_STRESS"] = "1"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
namespace :spec do
|
|
79
|
+
RSpec::Core::RakeTask.new(:gc_stress => ["compile:test", "gc_stress_env"]) do |task|
|
|
80
|
+
task.rspec_opts = ["--tag", "~skip"]
|
|
81
|
+
task.verbose = true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
RSpec::Core::RakeTask.new(:gc_compact => "compile:test") do |task|
|
|
85
|
+
task.pattern = "spec/block_3_geom_parse_spec.rb spec/block_6_index_build_spec.rb spec/block_8_index_borrowed_geometry_spec.rb spec/block_14_memory_gc_hardening_spec.rb"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
rescue LoadError
|
|
89
|
+
task :spec do
|
|
90
|
+
abort "RSpec is required to run the test suite. Install development dependencies with bundle install."
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
namespace :spec do
|
|
94
|
+
task :gc_stress => :spec
|
|
95
|
+
task :gc_compact => :spec
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
namespace :benchmark do
|
|
100
|
+
Dir[File.join(__dir__, "benchmark", "*.rb")].sort.each do |path|
|
|
101
|
+
next if File.basename(path).start_with?("_")
|
|
102
|
+
|
|
103
|
+
name = File.basename(path, ".rb")
|
|
104
|
+
desc "Run benchmark/#{name}.rb"
|
|
105
|
+
task name => :compile do
|
|
106
|
+
ruby path
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
namespace :vendor do
|
|
112
|
+
desc "Sync vendored tidwall/tg and rtree.c sources to pinned commits"
|
|
113
|
+
task :sync do
|
|
114
|
+
ruby "script/vendor_libs.rb", "--sync"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
desc "Verify vendored tidwall/tg and rtree.c sources against pinned tree SHA256"
|
|
118
|
+
task :verify do
|
|
119
|
+
ruby "script/vendor_libs.rb", "--verify"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
desc "Alias for vendor:sync"
|
|
124
|
+
task vendor: "vendor:sync"
|
|
125
|
+
|
|
126
|
+
desc "Vendor sources, compile, run specs"
|
|
127
|
+
task full_build: ["vendor:verify", "compile", "spec"]
|
|
128
|
+
|
|
129
|
+
task default: :spec
|