tg_geometry 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -79
  3. data/README.md +168 -184
  4. data/Rakefile +3 -3
  5. data/benchmark/ewkb_roundtrip.rb +29 -0
  6. data/benchmark/falcon_concurrency.rb +1 -1
  7. data/benchmark/feature_source.rb +92 -0
  8. data/benchmark/geom_query.rb +33 -0
  9. data/benchmark/nearest_segment.rb +20 -0
  10. data/docs/ARCHITECTURE.md +29 -107
  11. data/docs/BENCHMARKING.md +20 -1
  12. data/docs/CASUAL_EXAMPLE.md +71 -458
  13. data/docs/CONCURRENCY.md +13 -7
  14. data/docs/ERROR_HANDLING.md +30 -0
  15. data/docs/FEATURE_SOURCE.md +166 -0
  16. data/docs/GEOMETRY_QUERIES.md +23 -0
  17. data/docs/LIMITATIONS.md +17 -54
  18. data/docs/MEMORY_OWNERSHIP.md +20 -2
  19. data/docs/NEAREST_SEGMENT.md +17 -0
  20. data/docs/SRID_AND_EWKB.md +23 -0
  21. data/ext/tg_geometry/extconf.rb +46 -4
  22. data/ext/tg_geometry/tg_geometry_ext.c +3643 -168
  23. data/ext/tg_geometry/tg_geometry_vendor_json.c +17 -0
  24. data/ext/tg_geometry/tg_geometry_vendor_tg.c +3 -0
  25. data/ext/tg_geometry/vendor/.vendored +8 -2
  26. data/ext/tg_geometry/vendor/json/LICENSE +20 -0
  27. data/ext/tg_geometry/vendor/json/VERSION +3 -0
  28. data/ext/tg_geometry/vendor/json/json.c +1024 -0
  29. data/ext/tg_geometry/vendor/json/json.h +207 -0
  30. data/lib/tg/geometry/active_record_source.rb +16 -34
  31. data/lib/tg/geometry/active_record_type.rb +61 -0
  32. data/lib/tg/geometry/registry.rb +16 -67
  33. data/lib/tg/geometry/version.rb +1 -1
  34. data/lib/tg/geometry.rb +85 -0
  35. data/script/vendor_libs.rb +22 -6
  36. data/spec/active_record_type_spec.rb +45 -0
  37. data/spec/{expansion_a_auto_strategy_spec.rb → auto_strategy_spec.rb} +1 -1
  38. data/spec/{block_12_batch_packed_spec.rb → batch_packed_spec.rb} +1 -1
  39. data/spec/{block_20_concurrency_spec.rb → concurrency_spec.rb} +1 -1
  40. data/spec/constructors_spec.rb +104 -0
  41. data/spec/{block_13_error_hardening_spec.rb → error_hardening_spec.rb} +1 -1
  42. data/spec/feature_source_nogvl_spec.rb +51 -0
  43. data/spec/feature_source_spec.rb +268 -0
  44. data/spec/fixtures/feature_source/invalid_geometry_middle.geojson +8 -0
  45. data/spec/fixtures/feature_source/malformed_json.geojson +1 -0
  46. data/spec/fixtures/feature_source/mixed_geometry_types.geojson +8 -0
  47. data/spec/fixtures/feature_source/osm_like_feature_collection.geojson +10 -0
  48. data/spec/fixtures/feature_source/properties_null_missing.geojson +7 -0
  49. data/spec/fixtures/feature_source/simple_feature_collection.geojson +15 -0
  50. data/spec/fixtures/postgis/README.md +16 -0
  51. data/spec/fixtures/postgis/boundary_point_cases.geojson +83 -0
  52. data/spec/fixtures/postgis/multipolygon_large.ewkb +0 -0
  53. data/spec/fixtures/postgis/point_4326.ewkb +0 -0
  54. data/spec/fixtures/postgis/polygon_3857.ewkb +0 -0
  55. data/spec/fixtures/postgis/polygon_4326_simple.ewkb +0 -0
  56. data/spec/fixtures/postgis/polygon_4326_with_hole.ewkb +0 -0
  57. data/spec/{expansion_d_format_coverage_spec.rb → format_coverage_spec.rb} +1 -1
  58. data/spec/{block_20_fuzz_spec.rb → fuzz_spec.rb} +1 -1
  59. data/spec/{block_4_geom_api_spec.rb → geom_api_spec.rb} +1 -1
  60. data/spec/{block_3_geom_parse_spec.rb → geom_parse_spec.rb} +1 -1
  61. data/spec/{block_8_index_borrowed_geometry_spec.rb → index_borrowed_geometry_spec.rb} +1 -1
  62. data/spec/{block_6_index_build_spec.rb → index_build_spec.rb} +2 -2
  63. data/spec/{block_9_flat_query_spec.rb → index_flat_query_spec.rb} +1 -1
  64. data/spec/index_geom_query_spec.rb +68 -0
  65. data/spec/{block_7_index_owned_geometry_spec.rb → index_owned_geometry_spec.rb} +1 -1
  66. data/spec/{block_10_rtree_strategy_spec.rb → index_rtree_accounting_spec.rb} +1 -1
  67. data/spec/{block_11_rtree_order_spec.rb → index_rtree_order_spec.rb} +1 -1
  68. data/spec/keyword_validation_spec.rb +31 -0
  69. data/spec/{block_1_skeleton_spec.rb → load_and_errors_spec.rb} +1 -1
  70. data/spec/{expansion_e_low_level_geometry_spec.rb → low_level_geometry_spec.rb} +1 -1
  71. data/spec/{block_14_memory_gc_hardening_spec.rb → memory_gc_spec.rb} +1 -1
  72. data/spec/nearest_segment_spec.rb +62 -0
  73. data/spec/postgis_fixtures_spec.rb +68 -0
  74. data/spec/{expansion_i_ractor_spec.rb → ractor_spec.rb} +1 -1
  75. data/spec/{block_5_rect_api_spec.rb → rect_api_spec.rb} +1 -1
  76. data/spec/{expansion_b_registry_spec.rb → registry_spec.rb} +1 -1
  77. data/spec/srid_spec.rb +43 -0
  78. data/spec/{expansion_j_full_tg_api_coverage_spec.rb → tg_api_coverage_spec.rb} +1 -1
  79. data/spec/to_ewkb_spec.rb +37 -0
  80. data/spec/{block_2_vendor_spec.rb → vendor_sources_spec.rb} +4 -4
  81. metadata +87 -38
  82. data/docs/ACTIVE_RECORD.md +0 -26
  83. data/docs/AUTO_STRATEGY.md +0 -15
  84. data/docs/EXPANSION_E_TO_H_STATUS.md +0 -51
  85. data/docs/FORMAT_COVERAGE.md +0 -23
  86. data/docs/FULL_TG_API_COVERAGE.md +0 -109
  87. data/docs/LOW_LEVEL_GEOMETRY.md +0 -121
  88. data/docs/RACTOR.md +0 -40
  89. data/docs/REGISTRY.md +0 -37
  90. data/docs/RELEASE_CHECKLIST.md +0 -39
  91. /data/spec/{expansion_c_active_record_source_spec.rb → active_record_source_spec.rb} +0 -0
@@ -0,0 +1,207 @@
1
+ // https://github.com/tidwall/json.c
2
+ //
3
+ // Copyright 2023 Joshua J Baker. All rights reserved.
4
+ // Use of this source code is governed by an MIT-style
5
+ // license that can be found in the LICENSE file.
6
+ #ifndef JSON_H
7
+ #define JSON_H
8
+
9
+ #include <stddef.h>
10
+ #include <stdint.h>
11
+ #include <stdbool.h>
12
+
13
+ enum json_type {
14
+ JSON_NULL,
15
+ JSON_FALSE,
16
+ JSON_NUMBER,
17
+ JSON_STRING,
18
+ JSON_TRUE,
19
+ JSON_ARRAY,
20
+ JSON_OBJECT,
21
+ };
22
+
23
+ struct json { void *priv[4]; };
24
+
25
+ struct json_valid {
26
+ bool valid;
27
+ size_t pos;
28
+ };
29
+
30
+ // json_valid returns true if the input is valid json data.
31
+ bool json_valid(const char *json_str);
32
+ bool json_validn(const char *json_str, size_t len);
33
+ struct json_valid json_valid_ex(const char *json_str, int opts);
34
+ struct json_valid json_validn_ex(const char *json_str, size_t len, int opts);
35
+
36
+ // json_parse parses the input data and returns a json value.
37
+ //
38
+ // This function expects that the json is well-formed, and does not validate.
39
+ // Invalid json is safe and will not cause program errors, but it may return
40
+ // unexpected results. If you are consuming JSON from an unpredictable source
41
+ // then you may want to use the 'json_valid' function first.
42
+ //
43
+ // It's important to note that the resulting 'struct json' is backed by the
44
+ // same memory as json_str, thus a 'struct json' cannot outlive the original
45
+ // json_str. Attempting to use any of the json_*(struct json...) functions on
46
+ // the result, beyond the lifetime of the originating 'json_str', then it will
47
+ // likely result in a memory fault or some other type of undefined corruption.
48
+ struct json json_parse(const char *json_str);
49
+ struct json json_parsen(const char *json_str, size_t len);
50
+
51
+ // json_first returns the json's first child.
52
+ //
53
+ // For objects, the key of the first value is returned. For arrays, the first
54
+ // child value is returned. If the input json has no children, or is not an
55
+ // object or array, then a non-existent json value is returned.
56
+ struct json json_first(struct json json);
57
+
58
+ // json_next returns the json's next child.
59
+ //
60
+ // This typically follows a 'json_first' call, and is used to iterate over the
61
+ // children in an object or array. For arrays, each 'json_next' call will
62
+ // return a the next value. For objects, each 'json_next' call alternates over
63
+ // value, key, value key, value, etc. If there are no more children, then a
64
+ // non-existent json value is returned.
65
+ struct json json_next(struct json json);
66
+
67
+ // json_exists checks for the existence of json.
68
+ bool json_exists(struct json json);
69
+
70
+ // json_type returns the json's type.
71
+ //
72
+ // Returns one of the following: JSON_NULL, JSON_STRING, JSON_NUMBER,
73
+ // JSON_TRUE, JSON_FALSE, JSON_OBJECT, JSON_ARRAY.
74
+ // For non-existent json, JSON_NULL is returned.
75
+ enum json_type json_type(struct json json);
76
+
77
+ // json_raw returns the start of the raw json data.
78
+ //
79
+ // This function may return NULL if the input is non-existent. Also there is
80
+ // no guarentee that this data will be null-terminated C string. If you want to
81
+ // retain this data for longer that the data from the original 'json_parse'
82
+ // call then you should copy it first, such as:
83
+ //
84
+ // size_t len = json_raw_length(json);
85
+ // char *raw = malloc(len+1);
86
+ // memcpy(raw, json_raw(json), len);
87
+ // raw[len] = '\0';
88
+ //
89
+ const char *json_raw(struct json json);
90
+
91
+ // json_raw_length returns the length of the raw json data.
92
+ size_t json_raw_length(struct json json);
93
+
94
+ // Compare a json's raw data with a C string.
95
+ //
96
+ // This function performs a binary comparison of the characters. It compares
97
+ // each character, one-by-one, of both strings. If they are equal to each
98
+ // other, it continues until the characters differ or until the end of the JSON
99
+ // or a terminating null-character in the C string is reached.
100
+ // Returns -1, 0, +1 for less-than, equal-to, greater-than.
101
+ int json_raw_compare(struct json json, const char *str);
102
+ int json_raw_comparen(struct json json, const char *str, size_t len);
103
+
104
+ // Compares a json string to the provided C string.
105
+ //
106
+ // This function performs a binary comparison of the characters. It compares
107
+ // each character, one-by-one, of both strings. If they are equal to each
108
+ // other, it continues until the characters differ or until the end of the JSON
109
+ // or a terminating null-character in the C string is reached.
110
+ //
111
+ // If the JSON type is JSON_STRING, then each character that needs to be
112
+ // unescaped is done so in a streaming manner. ie. without the additional
113
+ // buffering or memory allocations. Of those characters, the ones that are
114
+ // non-ascii codepoints are encoded into their UTF-8 representation, of which
115
+ // those bytes are then compared to the C string.
116
+ //
117
+ // If the JSON type is not a JSON_STRING, then it performs a binary comparison
118
+ // of the raw JSON to the C string.
119
+ int json_string_compare(struct json json, const char *str);
120
+ int json_string_comparen(struct json json, const char *str, size_t len);
121
+
122
+ // json_string_length returns the number of characters needed to represent the
123
+ // JSON as a UTF-8 string.
124
+ size_t json_string_length(struct json json);
125
+
126
+ // json_string_is_escaped returns true if the json is a string that contains
127
+ // an escape sequence.
128
+ bool json_string_is_escaped(struct json json);
129
+
130
+ // json_string_copy copies a json string into the provided C string buffer.
131
+ //
132
+ // Returns the number of characters, not including the null-terminator, needed
133
+ // to store the JSON into the C string buffer.
134
+ // If the returned length is greater than nbytes-1, then only a parital copy
135
+ // occurred, for example:
136
+ //
137
+ // char buf[64];
138
+ // size_t len = json_string_copy(json, str, sizeof(str));
139
+ // if (len > sizeof(str)-1) {
140
+ // // ... copy did not complete ...
141
+ // }
142
+ //
143
+ size_t json_string_copy(struct json json, char *str, size_t nbytes);
144
+
145
+ // json_array_get returns the child json element at index. This is to be used
146
+ // on json with the type JSON_ARRAY.
147
+ struct json json_array_get(struct json json, size_t index);
148
+
149
+ // json_array_count returns the number of elements in a json array.
150
+ size_t json_array_count(struct json json);
151
+
152
+ // json_object_get returns the json value for its key. This is to be used on
153
+ // json with the type JSON_OBJECT.
154
+ struct json json_object_get(struct json json, const char *key);
155
+ struct json json_object_getn(struct json json, const char *key, size_t len);
156
+
157
+ // json_get finds json at the provide path.
158
+ //
159
+ // A path is a series of keys separated by a dot.
160
+ //
161
+ // json_get(json_str, "name");
162
+ // json_get(json_str, "user.id");
163
+ //
164
+ // The syntax is limited to very basic key names containing only alphanumeric
165
+ // characters, underscores, and dashes. If you need to access keys that have a
166
+ // broader range of characters, you can use the json_object_get and
167
+ // json_array_get functions.
168
+ //
169
+ // json_str = "{\"notes\": {\"special.info\": \"it's nice outside\"}";
170
+ // json = json_parse(json_str);
171
+ // json = json_object_get(json, "notes");
172
+ // json = json_object_get(json, "special.info");
173
+ //
174
+ struct json json_get(const char *json_str, const char *path);
175
+ struct json json_getn(const char *json_str, size_t len, const char *path);
176
+
177
+ // json_double returns a json's double value.
178
+ double json_double(struct json json);
179
+
180
+ // json_int returns a json's int value.
181
+ int json_int(struct json json);
182
+
183
+ // json_int64 returns a json's int64 value.
184
+ int64_t json_int64(struct json json);
185
+
186
+ // json_uint64 returns a json's uint64 value.
187
+ uint64_t json_uint64(struct json json);
188
+
189
+ // json_bool returns a json's boolean value.
190
+ bool json_bool(struct json json);
191
+
192
+ // json_escape is a utility function for converting a C string into a valid
193
+ // json string.
194
+ //
195
+ // Returns the number of characters, not including the null-terminator, needed
196
+ // to store the escaped JSON string in the C string buffer.
197
+ // If the returned length is greater than n-1, then only a parital copy
198
+ // occurred. In other words the
199
+ size_t json_escape(const char *str, char *escaped, size_t n);
200
+ size_t json_escapen(const char *str, size_t len, char *escaped, size_t n);
201
+
202
+ // json_ensure ensures that the json value has been entirely scanned.
203
+ // Only useful for when you need to call json_raw_length on large objects and
204
+ // arrays more than once.
205
+ struct json json_ensure(struct json json);
206
+
207
+ #endif // JSON_H
@@ -6,52 +6,34 @@ module TG
6
6
  module_function
7
7
 
8
8
  def call(scope, id:, geometry:, batch_size: 1_000)
9
- entries = []
10
-
11
- each_record(scope, batch_size: batch_size) do |record|
12
- entries << [read_field(record, id), read_field(record, geometry)]
9
+ enumerator(scope, batch_size: batch_size).map do |record|
10
+ [read_field(record, id), read_field(record, geometry)]
13
11
  end
14
-
15
- entries
16
12
  end
17
13
 
18
14
  def registry_source(scope, id:, geometry:, batch_size: 1_000)
19
- proc do
20
- TG::Geometry::ActiveRecordSource.call(
21
- scope,
22
- id: id,
23
- geometry: geometry,
24
- batch_size: batch_size
25
- )
26
- end
15
+ -> { TG::Geometry::ActiveRecordSource.call(scope, id: id, geometry: geometry, batch_size: batch_size) }
27
16
  end
28
17
 
29
- def each_record(scope, batch_size:)
30
- if scope.respond_to?(:find_each)
31
- scope.find_each(batch_size: batch_size) { |record| yield record }
32
- else
33
- scope.each { |record| yield record }
18
+ class << self
19
+ private
20
+
21
+ def enumerator(scope, batch_size:)
22
+ scope.respond_to?(:find_each) ? scope.find_each(batch_size: batch_size) : scope.each
34
23
  end
35
- end
36
- private_class_method :each_record
37
24
 
38
- def read_field(record, reader)
39
- case reader
40
- when Proc
41
- reader.call(record)
42
- when Symbol, String
43
- if record.respond_to?(reader)
44
- record.public_send(reader)
45
- elsif record.respond_to?(:[])
46
- record[reader]
47
- else
25
+ def read_field(record, reader)
26
+ case reader
27
+ in Proc then reader.call(record)
28
+ in Symbol | String if record.respond_to?(reader) then record.public_send(reader)
29
+ in Symbol | String if record.respond_to?(:[]) then record[reader]
30
+ in Symbol | String
48
31
  raise TG::Geometry::ArgumentError, "record does not expose #{reader.inspect}"
32
+ else
33
+ raise TG::Geometry::ArgumentError, "field reader must be Symbol, String, or Proc"
49
34
  end
50
- else
51
- raise TG::Geometry::ArgumentError, "field reader must be Symbol, String, or Proc"
52
35
  end
53
36
  end
54
- private_class_method :read_field
55
37
  end
56
38
  end
57
39
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "tg/geometry"
5
+
6
+ module TG
7
+ module Geometry
8
+ class ActiveRecordType < ActiveModel::Type::Value
9
+ HEX_PATTERN = /\A[0-9A-Fa-f]+\z/
10
+
11
+ def initialize(strict: true)
12
+ super()
13
+ @strict = strict
14
+ end
15
+
16
+ def type = :tg_geometry
17
+
18
+ def deserialize(value)
19
+ case value
20
+ when nil then nil
21
+ when TG::Geometry::Geom then value
22
+ when String then parse_string(value)
23
+ else
24
+ @strict ? raise(TG::Geometry::ArgumentError, "cannot deserialize #{value.class} as TG::Geometry::Geom") : nil
25
+ end
26
+ end
27
+
28
+ alias cast deserialize
29
+
30
+ def serialize(value)
31
+ case value
32
+ when nil, String then value
33
+ else raise TG::Geometry::ArgumentError, "TG::Geometry::ActiveRecordType is read-only in v0.3.0"
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def parse_string(str)
40
+ stripped = str.strip
41
+
42
+ if stripped.start_with?("\\x") && stripped[2..].match?(HEX_PATTERN)
43
+ TG::Geometry.parse_hex(stripped[2..])
44
+ elsif stripped.match?(HEX_PATTERN)
45
+ TG::Geometry.parse_hex(stripped)
46
+ elsif str.encoding == Encoding::ASCII_8BIT
47
+ TG::Geometry.parse_wkb(str)
48
+ elsif stripped.start_with?("{")
49
+ TG::Geometry.parse_geojson(str)
50
+ else
51
+ TG::Geometry.parse_wkt(str)
52
+ end
53
+ rescue TG::Geometry::ParseError => e
54
+ raise if @strict
55
+
56
+ warn "TG::Geometry::ActiveRecordType: parse failed: #{e.message}"
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module TG
4
6
  module Geometry
5
7
  class Registry
8
+ extend Forwardable
9
+
6
10
  DEFAULT_INDEX_OPTIONS = {
7
11
  via: :geojson,
8
12
  strategy: :rtree,
@@ -12,40 +16,23 @@ module TG
12
16
 
13
17
  class << self
14
18
  def source(&block)
15
- if block
16
- @source = block
17
- elsif instance_variable_defined?(:@source)
18
- @source
19
- elsif superclass.respond_to?(:source)
20
- superclass.source
21
- end
19
+ @source = block if block
20
+ @source || (superclass.source if superclass.respond_to?(:source))
22
21
  end
23
22
 
24
23
  def index_options(**options)
25
- if options.empty?
26
- inherited = superclass.respond_to?(:index_options) ? superclass.index_options : DEFAULT_INDEX_OPTIONS
27
- inherited.merge(@index_options || {}).freeze
28
- else
29
- @index_options = index_options.merge(options).freeze
30
- end
31
- end
24
+ inherited = superclass.respond_to?(:index_options) ? superclass.index_options : DEFAULT_INDEX_OPTIONS
25
+ return inherited.merge(@index_options || {}).freeze if options.empty?
32
26
 
33
- def active_record_source(scope, id:, geometry:, batch_size: 1_000)
34
- require_relative "active_record_source"
35
-
36
- source do
37
- TG::Geometry::ActiveRecordSource.call(
38
- scope,
39
- id: id,
40
- geometry: geometry,
41
- batch_size: batch_size
42
- )
43
- end
27
+ @index_options = inherited.merge(@index_options || {}, options).freeze
44
28
  end
45
29
  end
46
30
 
47
31
  attr_reader :index_options
48
32
 
33
+ def_delegators :index, :size, :bbox, :find_covering, :covering_ids,
34
+ :intersecting_rect, :covering_ids_batch_packed
35
+
49
36
  def initialize(entries: nil, source: nil, **index_options)
50
37
  @entries = entries
51
38
  @source = source
@@ -56,11 +43,7 @@ module TG
56
43
 
57
44
  def reload!
58
45
  new_index = TG::Geometry::Index.build(resolve_entries, **@index_options)
59
-
60
- @reload_mutex.synchronize do
61
- @index = new_index
62
- end
63
-
46
+ @reload_mutex.synchronize { @index = new_index }
64
47
  new_index
65
48
  end
66
49
 
@@ -68,51 +51,17 @@ module TG
68
51
  @index || raise(TG::Geometry::Error, "registry index is not loaded; call reload! first")
69
52
  end
70
53
 
71
- def loaded?
72
- !@index.nil?
73
- end
74
-
75
- def size
76
- current_index.size
77
- end
78
-
79
- def bbox
80
- current_index.bbox
81
- end
82
-
83
- def find_covering(lon, lat)
84
- current_index.find_covering(lon, lat)
85
- end
86
-
87
- def covering_ids(lon, lat)
88
- current_index.covering_ids(lon, lat)
89
- end
90
-
91
- def intersecting_rect(min_x, min_y, max_x, max_y)
92
- current_index.intersecting_rect(min_x, min_y, max_x, max_y)
93
- end
94
-
95
- def covering_ids_batch_packed(binary_string)
96
- current_index.covering_ids_batch_packed(binary_string)
97
- end
54
+ def loaded? = !@index.nil?
98
55
 
99
56
  private
100
57
 
101
- def current_index
102
- @index || raise(TG::Geometry::Error, "registry index is not loaded; call reload! first")
103
- end
104
-
105
58
  def resolve_entries
106
- return @entries unless @entries.nil?
59
+ return @entries if @entries
107
60
 
108
61
  callable = @source || self.class.source
109
62
  raise TG::Geometry::Error, "registry source is not configured" unless callable
110
63
 
111
- if callable.respond_to?(:call)
112
- instance_exec(&callable)
113
- else
114
- raise TG::Geometry::Error, "registry source must be callable"
115
- end
64
+ instance_exec(&callable)
116
65
  end
117
66
  end
118
67
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TG
4
4
  module Geometry
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/tg/geometry.rb CHANGED
@@ -4,3 +4,88 @@ require_relative "geometry/version"
4
4
  require "tg_geometry_ext_geometry_ext"
5
5
  require_relative "geometry/registry"
6
6
  require_relative "geometry/active_record_source"
7
+
8
+ module TG
9
+ # Fast immutable planar geometry engine with proper indexes and clean
10
+ # EWKB/PostGIS boundaries. Not a GIS. Not an rgeo replacement.
11
+ #
12
+ # All distances and coordinates are in input units. SRID is metadata only;
13
+ # no reprojection is performed.
14
+ module Geometry
15
+ # @!method self.line_string(points, index: :natural, srid: nil)
16
+ # @param points [Array<Array<Float>>]
17
+ # @param index [:default, :none, :natural, :ystripes]
18
+ # @param srid [Integer, nil]
19
+ # @return [TG::Geometry::Geom]
20
+ #
21
+ # @!method self.polygon(exterior, holes: [], index: :ystripes, srid: nil)
22
+ # @param exterior [Array<Array<Float>>]
23
+ # @param holes [Array<Array<Array<Float>>>]
24
+ # @param index [:default, :none, :natural, :ystripes]
25
+ # @param srid [Integer, nil]
26
+ # @return [TG::Geometry::Geom]
27
+ #
28
+ # @!method self.multi_polygon(polygons, index: :ystripes, srid: nil)
29
+ # Each polygon is a Hash with :exterior and optional :holes, or an Array
30
+ # shorthand for an exterior with no holes.
31
+ # @param polygons [Array<Hash, Array>]
32
+ # @return [TG::Geometry::Geom]
33
+ #
34
+ # @!method self.parse_wkb(bytes, index: :ystripes)
35
+ # Parses WKB or EWKB. SRID is preserved when the EWKB SRID flag is set.
36
+ # @return [TG::Geometry::Geom]
37
+ #
38
+ # @!method self.parse_hex(hex, index: :ystripes)
39
+ # Parses HEXWKB or HEXEWKB. SRID is preserved when the EWKB SRID flag is set.
40
+ # @return [TG::Geometry::Geom]
41
+
42
+ class Geom
43
+ # @!method srid
44
+ # @return [Integer, nil] SRID metadata; not used for reprojection
45
+ #
46
+ # @!method to_ewkb(srid: nil)
47
+ # Writes EWKB with the SRID flag set. Uses explicit srid: when provided,
48
+ # otherwise Geom#srid. Raises if no SRID is available. to_wkb remains plain.
49
+ # @return [String] frozen ASCII-8BIT EWKB string
50
+ end
51
+
52
+ class Index
53
+ # @!method self.build(entries, via:, strategy:, predicate: :covers, geometry_index: :ystripes)
54
+ # predicate: affects only legacy point query methods: find_covering,
55
+ # covering_ids(x, y), covering_ids_batch_packed. The *_geom_ids methods
56
+ # use their own predicates derived from the method name.
57
+ #
58
+ # @!method intersecting_geom_ids(geom)
59
+ # Stored geometries for which tg_geom_intersects(stored, query) is true.
60
+ # @return [Array<Object>] ids in insertion order
61
+ #
62
+ # @!method covering_geom_ids(geom)
63
+ # Stored geometries for which tg_geom_covers(stored, query) is true.
64
+ # Direction: stored covers query. Boundary points are covered.
65
+ # @return [Array<Object>] ids in insertion order
66
+ #
67
+ # @!method containing_geom_ids(geom)
68
+ # Stored geometries for which tg_geom_contains(stored, query) is true.
69
+ # Direction: stored contains query. Boundary points are not contained.
70
+ # @return [Array<Object>] ids in insertion order
71
+ end
72
+
73
+ class Line
74
+ # @!method nearest_segment(x, y)
75
+ # @return [TG::Geometry::NearestSegment, nil] planar Euclidean
76
+ end
77
+
78
+ class Ring
79
+ # @!method nearest_segment(x, y)
80
+ # @return [TG::Geometry::NearestSegment, nil] planar Euclidean
81
+ end
82
+
83
+ # Result of Line#nearest_segment / Ring#nearest_segment.
84
+ class NearestSegment
85
+ # @!attribute [r] segment @return [TG::Geometry::Segment]
86
+ # @!attribute [r] index @return [Integer]
87
+ # @!attribute [r] distance @return [Float] planar Euclidean in input units
88
+ # @!attribute [r] point @return [Array(Float, Float)] projection
89
+ end
90
+ end
91
+ end
@@ -26,6 +26,14 @@ PINS = {
26
26
  sha256: "4afc86cbd3abe03730206031a5aff5b8b29d37b055fc356052f6f06e1d1f9a61",
27
27
  target: "rtree",
28
28
  files: %w[rtree.c rtree.h LICENSE README.md]
29
+ },
30
+ json: {
31
+ repo: "https://github.com/tidwall/json.c.git",
32
+ ref: "main",
33
+ commit: "4aaf99b5c08e4e1364f97500fe5c5d5a2617b4a6",
34
+ sha256: "059393302de9325ab6ccf79c298234f2b688a59d138103305da317d2d0176fe2",
35
+ target: "json",
36
+ files: %w[json.c json.h LICENSE]
29
37
  }
30
38
  }.freeze
31
39
 
@@ -160,9 +168,13 @@ def write_manifest(results)
160
168
  File.utime(NORMALIZED_MTIME, NORMALIZED_MTIME, MANIFEST_PATH)
161
169
  end
162
170
 
163
- def expected_vendor_results
171
+ def expected_vendor_results(manifest = nil)
172
+ manifest ||= parse_kv_file(MANIFEST_PATH)
173
+
164
174
  PINS.each_with_object({}) do |(name, pin), results|
165
- results[name] = pin.merge(tree_sha256: pin.fetch(:sha256))
175
+ manifest_sha = manifest["#{name}_tree_sha256"]
176
+ tree_sha = pin.fetch(:sha256) || manifest_sha
177
+ results[name] = pin.merge(tree_sha256: tree_sha)
166
178
  end
167
179
  end
168
180
 
@@ -186,6 +198,7 @@ end
186
198
 
187
199
  def verify_vendor_tree
188
200
  failures = []
201
+ manifest = parse_kv_file(MANIFEST_PATH)
189
202
 
190
203
  PINS.each do |name, pin|
191
204
  target = File.join(VENDOR_DIR, pin.fetch(:target))
@@ -207,12 +220,15 @@ def verify_vendor_tree
207
220
  next unless failures.none? { |failure| failure.start_with?("#{name}:") }
208
221
 
209
222
  actual_sha256 = tree_sha256_for(target)
210
- expected_sha256 = pin.fetch(:sha256)
211
- failures << "#{name}: tree_sha256 mismatch: expected #{expected_sha256}, got #{actual_sha256}" unless actual_sha256 == expected_sha256
223
+ expected_sha256 = pin.fetch(:sha256) || manifest["#{name}_tree_sha256"]
224
+ if expected_sha256.nil? || expected_sha256.empty?
225
+ failures << "#{name}: tree_sha256 missing from manifest"
226
+ elsif actual_sha256 != expected_sha256
227
+ failures << "#{name}: tree_sha256 mismatch: expected #{expected_sha256}, got #{actual_sha256}"
228
+ end
212
229
  end
213
230
 
214
- expected_results = expected_vendor_results
215
- manifest = parse_kv_file(MANIFEST_PATH)
231
+ expected_results = expected_vendor_results(manifest)
216
232
  if manifest.empty?
217
233
  failures << "manifest: missing #{MANIFEST_PATH}"
218
234
  else
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tg/geometry/active_record_type"
5
+
6
+ RSpec.describe TG::Geometry::ActiveRecordType do
7
+ let(:type) { described_class.new }
8
+ let(:ewkb) { TG::Geometry.parse_wkt("POINT (1 2)").to_ewkb(srid: 4326) }
9
+ let(:geom) { TG::Geometry.parse_wkb(ewkb) }
10
+
11
+ it "deserializes supported read shapes" do
12
+ expect(type.type).to eq(:tg_geometry)
13
+ expect(type.deserialize(nil)).to be_nil
14
+ expect(type.deserialize(geom)).to equal(geom)
15
+ expect(type.deserialize(ewkb).srid).to eq(4326)
16
+ expect(type.deserialize(ewkb.unpack1("H*")).srid).to eq(4326)
17
+ expect(type.deserialize("\\x#{ewkb.unpack1('H*')}").srid).to eq(4326)
18
+ expect(type.deserialize('{"type":"Point","coordinates":[1,2]}').srid).to be_nil
19
+ expect(type.deserialize("POINT (1 2)").srid).to be_nil
20
+ end
21
+
22
+ it "detects ASCII-8BIT hex before WKB fallback" do
23
+ hex = ewkb.unpack1("H*").b
24
+
25
+ expect(type.deserialize(hex).srid).to eq(4326)
26
+ end
27
+
28
+ it "is strict by default and can warn+nil in non-strict mode" do
29
+ expect { type.deserialize("not geometry") }.to raise_error(TG::Geometry::ParseError)
30
+
31
+ non_strict = described_class.new(strict: false)
32
+ expect { expect(non_strict.deserialize("not geometry")).to be_nil }
33
+ .to output(/parse failed/).to_stderr
34
+ end
35
+
36
+ it "is read-only on serialize" do
37
+ expect(type.serialize(nil)).to be_nil
38
+ expect(type.serialize("raw")).to eq("raw")
39
+ expect { type.serialize(geom) }.to raise_error(TG::Geometry::ArgumentError)
40
+ end
41
+
42
+ it "uses cast as deserialize" do
43
+ expect(type.cast(ewkb).srid).to eq(4326)
44
+ end
45
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "Expansion Block A auto strategy status" do
5
+ RSpec.describe "Auto strategy status" do
6
6
  it "does not expose :auto in the first public release" do
7
7
  entries = [[1, '{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]}']]
8
8