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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
scope.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
reader.
|
|
42
|
-
|
|
43
|
-
|
|
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
|
data/lib/tg/geometry/registry.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/tg/geometry/version.rb
CHANGED
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
|
data/script/vendor_libs.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|