tg_geometry 0.3.1 → 0.3.3
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 +33 -0
- data/README.md +81 -251
- data/benchmark/_support.rb +11 -4
- data/benchmark/distance_memory_accounting.rb +46 -0
- data/benchmark/distance_point_geom.rb +49 -0
- data/benchmark/distance_within_radius.rb +75 -0
- data/docs/BENCHMARKING.md +28 -1
- data/docs/CONCURRENCY.md +8 -0
- data/docs/GEOMETRY_QUERIES.md +42 -0
- data/docs/LIMITATIONS.md +12 -0
- data/ext/tg_geometry/tg_geometry_ext.c +843 -4
- data/ext/tg_geometry/vendor/.vendored +8 -8
- data/ext/tg_geometry/vendor/json/VERSION +1 -1
- data/ext/tg_geometry/vendor/json/json.c +20 -20
- data/ext/tg_geometry/vendor/json/json.h +20 -20
- data/ext/tg_geometry/vendor/rtree/VERSION +2 -2
- data/ext/tg_geometry/vendor/rtree/rtree.c +121 -148
- data/ext/tg_geometry/vendor/rtree/rtree.h +14 -11
- data/ext/tg_geometry/vendor/tg/VERSION +1 -1
- data/ext/tg_geometry/vendor/tg/tg.c +1854 -1339
- data/ext/tg_geometry/vendor/tg/tg.h +18 -17
- data/lib/tg/geometry/version.rb +1 -1
- data/lib/tg/geometry.rb +40 -0
- data/script/vendor_libs.rb +11 -7
- data/spec/distance_spec.rb +258 -0
- metadata +12 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// https://github.com/tidwall/tg
|
|
2
2
|
//
|
|
3
3
|
// Copyright 2023 Joshua J Baker. All rights reserved.
|
|
4
|
-
// Use of this source code is governed by a license
|
|
4
|
+
// Use of this source code is governed by a license
|
|
5
5
|
// that can be found in the LICENSE file.
|
|
6
6
|
|
|
7
7
|
#ifndef TG_H
|
|
@@ -26,7 +26,7 @@ struct tg_segment {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
/// A rectangle defined by a minimum and maximum coordinates.
|
|
29
|
-
/// Returned by the tg_geom_rect(), tg_ring_rect(), and other \*_rect()
|
|
29
|
+
/// Returned by the tg_geom_rect(), tg_ring_rect(), and other \*_rect()
|
|
30
30
|
/// functions for getting a geometry's minimum bounding rectangle.
|
|
31
31
|
/// Also used internally for geometry indexing.
|
|
32
32
|
/// @see RectFuncs
|
|
@@ -62,10 +62,10 @@ enum tg_geom_type {
|
|
|
62
62
|
/// Used for polygons, rings, and lines to make the point-in-polygon and
|
|
63
63
|
/// geometry intersection operations fast.
|
|
64
64
|
///
|
|
65
|
-
/// An index can also be used for efficiently traversing, searching, and
|
|
66
|
-
/// performing nearest-neighbor (kNN) queries on the segment using
|
|
65
|
+
/// An index can also be used for efficiently traversing, searching, and
|
|
66
|
+
/// performing nearest-neighbor (kNN) queries on the segment using
|
|
67
67
|
/// tg_ring_index_*() and tg_ring_nearest() functions.
|
|
68
|
-
enum tg_index {
|
|
68
|
+
enum tg_index {
|
|
69
69
|
TG_DEFAULT, ///< default is TG_NATURAL or tg_env_set_default_index().
|
|
70
70
|
TG_NONE, ///< no indexing available, or disabled.
|
|
71
71
|
TG_NATURAL, ///< indexing with natural ring order, for rings/lines
|
|
@@ -73,7 +73,7 @@ enum tg_index {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
/// @defgroup GeometryConstructors Geometry constructors
|
|
76
|
-
/// Functions for creating and freeing geometries.
|
|
76
|
+
/// Functions for creating and freeing geometries.
|
|
77
77
|
/// @{
|
|
78
78
|
struct tg_geom *tg_geom_new_point(struct tg_point point);
|
|
79
79
|
struct tg_geom *tg_geom_new_linestring(const struct tg_line *line);
|
|
@@ -110,6 +110,7 @@ const struct tg_poly *tg_geom_poly_at(const struct tg_geom *geom, int index);
|
|
|
110
110
|
int tg_geom_num_geometries(const struct tg_geom *geom);
|
|
111
111
|
const struct tg_geom *tg_geom_geometry_at(const struct tg_geom *geom, int index);
|
|
112
112
|
const char *tg_geom_extra_json(const struct tg_geom *geom);
|
|
113
|
+
const char *tg_geom_extra_json2(const struct tg_geom *geom, size_t *jsonlen);
|
|
113
114
|
bool tg_geom_is_empty(const struct tg_geom *geom);
|
|
114
115
|
int tg_geom_dims(const struct tg_geom *geom);
|
|
115
116
|
bool tg_geom_has_z(const struct tg_geom *geom);
|
|
@@ -240,7 +241,7 @@ bool tg_rect_intersects_point(struct tg_rect a, struct tg_point b);
|
|
|
240
241
|
/// Functions for working directly with the tg_ring type.
|
|
241
242
|
///
|
|
242
243
|
/// There are no direct spatial predicates for tg_ring.
|
|
243
|
-
/// If you want to perform operations like "intersects" or "covers" then you
|
|
244
|
+
/// If you want to perform operations like "intersects" or "covers" then you
|
|
244
245
|
/// must upcast the ring to a tg_geom, like such:
|
|
245
246
|
///
|
|
246
247
|
/// ```
|
|
@@ -265,17 +266,17 @@ int tg_ring_index_spread(const struct tg_ring *ring);
|
|
|
265
266
|
int tg_ring_index_num_levels(const struct tg_ring *ring);
|
|
266
267
|
int tg_ring_index_level_num_rects(const struct tg_ring *ring, int levelidx);
|
|
267
268
|
struct tg_rect tg_ring_index_level_rect(const struct tg_ring *ring, int levelidx, int rectidx);
|
|
268
|
-
bool tg_ring_nearest_segment(const struct tg_ring *ring,
|
|
269
|
+
bool tg_ring_nearest_segment(const struct tg_ring *ring,
|
|
269
270
|
double (*rect_dist)(struct tg_rect rect, int *more, void *udata),
|
|
270
271
|
double (*seg_dist)(struct tg_segment seg, int *more, void *udata),
|
|
271
272
|
bool (*iter)(struct tg_segment seg, double dist, int index, void *udata),
|
|
272
273
|
void *udata);
|
|
273
|
-
void tg_ring_line_search(const struct tg_ring *a, const struct tg_line *b,
|
|
274
|
-
bool (*iter)(struct tg_segment aseg, int aidx, struct tg_segment bseg,
|
|
274
|
+
void tg_ring_line_search(const struct tg_ring *a, const struct tg_line *b,
|
|
275
|
+
bool (*iter)(struct tg_segment aseg, int aidx, struct tg_segment bseg,
|
|
275
276
|
int bidx, void *udata),
|
|
276
277
|
void *udata);
|
|
277
|
-
void tg_ring_ring_search(const struct tg_ring *a, const struct tg_ring *b,
|
|
278
|
-
bool (*iter)(struct tg_segment aseg, int aidx, struct tg_segment bseg,
|
|
278
|
+
void tg_ring_ring_search(const struct tg_ring *a, const struct tg_ring *b,
|
|
279
|
+
bool (*iter)(struct tg_segment aseg, int aidx, struct tg_segment bseg,
|
|
279
280
|
int bidx, void *udata),
|
|
280
281
|
void *udata);
|
|
281
282
|
double tg_ring_area(const struct tg_ring *ring);
|
|
@@ -286,7 +287,7 @@ double tg_ring_perimeter(const struct tg_ring *ring);
|
|
|
286
287
|
/// Functions for working directly with the tg_line type.
|
|
287
288
|
///
|
|
288
289
|
/// There are no direct spatial predicates for tg_line.
|
|
289
|
-
/// If you want to perform operations like "intersects" or "covers" then you
|
|
290
|
+
/// If you want to perform operations like "intersects" or "covers" then you
|
|
290
291
|
/// must upcast the line to a tg_geom, like such:
|
|
291
292
|
///
|
|
292
293
|
/// ```
|
|
@@ -310,13 +311,13 @@ int tg_line_index_spread(const struct tg_line *line);
|
|
|
310
311
|
int tg_line_index_num_levels(const struct tg_line *line);
|
|
311
312
|
int tg_line_index_level_num_rects(const struct tg_line *line, int levelidx);
|
|
312
313
|
struct tg_rect tg_line_index_level_rect(const struct tg_line *line, int levelidx, int rectidx);
|
|
313
|
-
bool tg_line_nearest_segment(const struct tg_line *line,
|
|
314
|
+
bool tg_line_nearest_segment(const struct tg_line *line,
|
|
314
315
|
double (*rect_dist)(struct tg_rect rect, int *more, void *udata),
|
|
315
316
|
double (*seg_dist)(struct tg_segment seg, int *more, void *udata),
|
|
316
317
|
bool (*iter)(struct tg_segment seg, double dist, int index, void *udata),
|
|
317
318
|
void *udata);
|
|
318
|
-
void tg_line_line_search(const struct tg_line *a, const struct tg_line *b,
|
|
319
|
-
bool (*iter)(struct tg_segment aseg, int aidx, struct tg_segment bseg,
|
|
319
|
+
void tg_line_line_search(const struct tg_line *a, const struct tg_line *b,
|
|
320
|
+
bool (*iter)(struct tg_segment aseg, int aidx, struct tg_segment bseg,
|
|
320
321
|
int bidx, void *udata),
|
|
321
322
|
void *udata);
|
|
322
323
|
double tg_line_length(const struct tg_line *line);
|
|
@@ -326,7 +327,7 @@ double tg_line_length(const struct tg_line *line);
|
|
|
326
327
|
/// Functions for working directly with the tg_poly type.
|
|
327
328
|
///
|
|
328
329
|
/// There are no direct spatial predicates for tg_poly.
|
|
329
|
-
/// If you want to perform operations like "intersects" or "covers" then you
|
|
330
|
+
/// If you want to perform operations like "intersects" or "covers" then you
|
|
330
331
|
/// must upcast the poly to a tg_geom, like such:
|
|
331
332
|
///
|
|
332
333
|
/// ```
|
data/lib/tg/geometry/version.rb
CHANGED
data/lib/tg/geometry.rb
CHANGED
|
@@ -43,6 +43,30 @@ module TG
|
|
|
43
43
|
# @!method srid
|
|
44
44
|
# @return [Integer, nil] SRID metadata; not used for reprojection
|
|
45
45
|
#
|
|
46
|
+
# @!method distance_to_lnglat_meters(lng, lat)
|
|
47
|
+
# Approximate meters in a query-local equirectangular frame. Not geodesy.
|
|
48
|
+
# @return [Float]
|
|
49
|
+
#
|
|
50
|
+
# @!method boundary_distance_to_lnglat_meters(lng, lat)
|
|
51
|
+
# Approximate meters to nearest boundary/segment/point.
|
|
52
|
+
# @return [Float]
|
|
53
|
+
#
|
|
54
|
+
# @!method nearest_point_lnglat(lng, lat)
|
|
55
|
+
# Raw planar nearest boundary/geometry point. Longitude is not wrapped.
|
|
56
|
+
# @return [Array(Float, Float)]
|
|
57
|
+
#
|
|
58
|
+
# @!method distance_to_xy(x, y)
|
|
59
|
+
# Planar distance in input coordinate units.
|
|
60
|
+
# @return [Float]
|
|
61
|
+
#
|
|
62
|
+
# @!method boundary_distance_to_xy(x, y)
|
|
63
|
+
# Planar boundary distance in input coordinate units.
|
|
64
|
+
# @return [Float]
|
|
65
|
+
#
|
|
66
|
+
# @!method nearest_point_xy(x, y)
|
|
67
|
+
# Planar nearest boundary/geometry point in input coordinate units.
|
|
68
|
+
# @return [Array(Float, Float)]
|
|
69
|
+
#
|
|
46
70
|
# @!method to_ewkb(srid: nil)
|
|
47
71
|
# Writes EWKB with the SRID flag set. Uses explicit srid: when provided,
|
|
48
72
|
# otherwise Geom#srid. Raises if no SRID is available. to_wkb remains plain.
|
|
@@ -68,6 +92,22 @@ module TG
|
|
|
68
92
|
# Stored geometries for which tg_geom_contains(stored, query) is true.
|
|
69
93
|
# Direction: stored contains query. Boundary points are not contained.
|
|
70
94
|
# @return [Array<Object>] ids in insertion order
|
|
95
|
+
#
|
|
96
|
+
# @!method within_distance_lnglat_meters(lng, lat, radius_m, sort: false)
|
|
97
|
+
# Rtree bbox prefilter plus exact approximate-meter distance filter.
|
|
98
|
+
# @return [Array<Array(Object, Float)>]
|
|
99
|
+
#
|
|
100
|
+
# @!method within_distance_ids_lnglat_meters(lng, lat, radius_m)
|
|
101
|
+
# Same membership as within_distance_lnglat_meters, ids only.
|
|
102
|
+
# @return [Array<Object>]
|
|
103
|
+
#
|
|
104
|
+
# @!method within_distance_xy(x, y, radius, sort: false)
|
|
105
|
+
# Rtree bbox prefilter plus exact planar distance filter.
|
|
106
|
+
# @return [Array<Array(Object, Float)>]
|
|
107
|
+
#
|
|
108
|
+
# @!method within_distance_ids_xy(x, y, radius)
|
|
109
|
+
# Same membership as within_distance_xy, ids only.
|
|
110
|
+
# @return [Array<Object>]
|
|
71
111
|
end
|
|
72
112
|
|
|
73
113
|
class Line
|
data/script/vendor_libs.rb
CHANGED
|
@@ -14,24 +14,24 @@ PINS = {
|
|
|
14
14
|
tg: {
|
|
15
15
|
repo: "https://github.com/tidwall/tg.git",
|
|
16
16
|
ref: "main",
|
|
17
|
-
commit: "
|
|
18
|
-
sha256: "
|
|
17
|
+
commit: "532fc38e1a7bd440339cf83259a9d7983f04ad69",
|
|
18
|
+
sha256: "dbf9a49d5ff326974312e94fb39c5687260a2c2b27b10971b24635ed6f79184c",
|
|
19
19
|
target: "tg",
|
|
20
20
|
files: %w[tg.c tg.h LICENSE README.md]
|
|
21
21
|
},
|
|
22
22
|
rtree: {
|
|
23
23
|
repo: "https://github.com/tidwall/rtree.c.git",
|
|
24
|
-
ref: "v0.
|
|
25
|
-
commit: "
|
|
26
|
-
sha256: "
|
|
24
|
+
ref: "v0.6.1",
|
|
25
|
+
commit: "eebc774929f89fd5f4095f7c8835a87f2d2fb73f",
|
|
26
|
+
sha256: "6779f4b2c9df8f199907d26c3fba11bc07c6f923d80881577c19f50119e15177",
|
|
27
27
|
target: "rtree",
|
|
28
28
|
files: %w[rtree.c rtree.h LICENSE README.md]
|
|
29
29
|
},
|
|
30
30
|
json: {
|
|
31
31
|
repo: "https://github.com/tidwall/json.c.git",
|
|
32
32
|
ref: "main",
|
|
33
|
-
commit: "
|
|
34
|
-
sha256: "
|
|
33
|
+
commit: "104d8cbf5d09ceba37d469574cbf5696349bcaf1",
|
|
34
|
+
sha256: "764155b839b910f45aaaf0d0881a47570d4ef0919465c5117c7658ab3dac0594",
|
|
35
35
|
target: "json",
|
|
36
36
|
files: %w[json.c json.h LICENSE]
|
|
37
37
|
}
|
|
@@ -74,6 +74,10 @@ def normalize_tree!(directory)
|
|
|
74
74
|
next if File.symlink?(path)
|
|
75
75
|
|
|
76
76
|
if File.file?(path)
|
|
77
|
+
if %w[.c .h].include?(File.extname(path))
|
|
78
|
+
source = File.binread(path)
|
|
79
|
+
File.binwrite(path, source.gsub(/[ \t]+(?=\r?$)/, ""))
|
|
80
|
+
end
|
|
77
81
|
File.chmod(0o644, path)
|
|
78
82
|
File.utime(NORMALIZED_MTIME, NORMALIZED_MTIME, path)
|
|
79
83
|
elsif File.directory?(path)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "point to geometry distance" do
|
|
6
|
+
R = 6_371_008.8
|
|
7
|
+
DEG = Math::PI / 180.0
|
|
8
|
+
|
|
9
|
+
def meters_per_lng_at(lat)
|
|
10
|
+
R * DEG * Math.cos(lat * DEG)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def meters_per_lat
|
|
14
|
+
R * DEG
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def point_to_segment_distance(px, py, ax, ay, bx, by)
|
|
18
|
+
dx = bx - ax
|
|
19
|
+
dy = by - ay
|
|
20
|
+
len_sq = dx * dx + dy * dy
|
|
21
|
+
return Math.hypot(px - ax, py - ay) if len_sq.zero?
|
|
22
|
+
|
|
23
|
+
t = (((px - ax) * dx) + ((py - ay) * dy)) / len_sq
|
|
24
|
+
t = 0.0 if t < 0.0
|
|
25
|
+
t = 1.0 if t > 1.0
|
|
26
|
+
Math.hypot(px - (ax + t * dx), py - (ay + t * dy))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def lnglat_segment_distance_m(query_lng, query_lat, a, b)
|
|
30
|
+
sx = meters_per_lng_at(query_lat)
|
|
31
|
+
sy = meters_per_lat
|
|
32
|
+
ax = sx * (a[0] - query_lng)
|
|
33
|
+
ay = sy * (a[1] - query_lat)
|
|
34
|
+
bx = sx * (b[0] - query_lng)
|
|
35
|
+
by = sy * (b[1] - query_lat)
|
|
36
|
+
point_to_segment_distance(0.0, 0.0, ax, ay, bx, by)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "implements polygon inside, boundary, outside, and hole semantics" do
|
|
40
|
+
polygon = TG::Geometry.polygon(
|
|
41
|
+
[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]],
|
|
42
|
+
holes: [[[2, 2], [4, 2], [4, 4], [2, 4], [2, 2]]]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(polygon.distance_to_xy(5, 5)).to eq(0.0)
|
|
46
|
+
expect(polygon.boundary_distance_to_xy(5, 5)).to be_within(1e-12).of(Math.sqrt(2.0))
|
|
47
|
+
expect(polygon.distance_to_xy(0, 5)).to eq(0.0)
|
|
48
|
+
expect(polygon.boundary_distance_to_xy(0, 5)).to eq(0.0)
|
|
49
|
+
expect(polygon.distance_to_xy(12, 5)).to be_within(1e-12).of(2.0)
|
|
50
|
+
expect(polygon.distance_to_xy(3, 3)).to be_within(1e-12).of(1.0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "uses approximate local meters for lng/lat outside distances" do
|
|
54
|
+
polygon = TG::Geometry.polygon([[0, 0], [0.01, 0], [0.01, 0.01], [0, 0.01], [0, 0]])
|
|
55
|
+
distance = polygon.distance_to_lnglat_meters(0.02, 0.005)
|
|
56
|
+
expected = meters_per_lng_at(0.005) * 0.01
|
|
57
|
+
|
|
58
|
+
expect(distance).to be_within(0.01).of(expected)
|
|
59
|
+
expect(polygon.distance_to_lnglat_meters(0.005, 0.005)).to eq(0.0)
|
|
60
|
+
expect(polygon.boundary_distance_to_lnglat_meters(0.005, 0.005)).to be > 0.0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "uses minimum distance over multipolygon members" do
|
|
64
|
+
multipolygon = TG::Geometry.parse_wkt(
|
|
65
|
+
"MULTIPOLYGON (((0 0, 2 0, 2 2, 0 2, 0 0)), ((10 0, 12 0, 12 2, 10 2, 10 0)))"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
expect(multipolygon.distance_to_xy(1, 1)).to eq(0.0)
|
|
69
|
+
expect(multipolygon.distance_to_xy(6, 1)).to be_within(1e-12).of(4.0)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "handles lines, points, multipoints, and mixed geometry collections" do
|
|
73
|
+
line = TG::Geometry.line_string([[0, 0], [10, 0]])
|
|
74
|
+
point = TG::Geometry.point(3, 4)
|
|
75
|
+
multipoint = TG::Geometry.parse_wkt("MULTIPOINT ((0 0), (10 0))")
|
|
76
|
+
collection = TG::Geometry.parse_wkt(
|
|
77
|
+
"GEOMETRYCOLLECTION (POLYGON ((20 20, 30 20, 30 30, 20 30, 20 20)), LINESTRING (0 0, 10 0))"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expect(line.distance_to_xy(5, 3)).to be_within(1e-12).of(line.boundary_distance_to_xy(5, 3))
|
|
81
|
+
expect(line.distance_to_xy(5, 3)).to be_within(1e-12).of(3.0)
|
|
82
|
+
expect(point.distance_to_xy(0, 0)).to be_within(1e-12).of(5.0)
|
|
83
|
+
expect(multipoint.distance_to_xy(7, 0)).to be_within(1e-12).of(3.0)
|
|
84
|
+
expect(collection.distance_to_xy(5, 2)).to be_within(1e-12).of(2.0)
|
|
85
|
+
expect(collection.distance_to_xy(25, 25)).to eq(0.0)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "skips empty members but raises when nothing is measurable" do
|
|
89
|
+
mixed = TG::Geometry.parse_wkt("GEOMETRYCOLLECTION (POINT EMPTY, POINT (1 1))")
|
|
90
|
+
all_empty = TG::Geometry.parse_wkt("GEOMETRYCOLLECTION (POINT EMPTY, LINESTRING EMPTY)")
|
|
91
|
+
|
|
92
|
+
expect(mixed.distance_to_xy(4, 5)).to be_within(1e-12).of(5.0)
|
|
93
|
+
expect { all_empty.distance_to_xy(0, 0) }.to raise_error(TG::Geometry::ArgumentError)
|
|
94
|
+
expect { TG::Geometry.empty_polygon.boundary_distance_to_xy(0, 0) }.to raise_error(TG::Geometry::ArgumentError)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "returns nearest boundary points for interior polygon queries" do
|
|
98
|
+
polygon = TG::Geometry.polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])
|
|
99
|
+
nearest = polygon.nearest_point_xy(5, 5)
|
|
100
|
+
boundary_distance = polygon.boundary_distance_to_xy(5, 5)
|
|
101
|
+
|
|
102
|
+
expect(polygon.distance_to_xy(5, 5)).to eq(0.0)
|
|
103
|
+
expect(boundary_distance).to be_within(1e-12).of(5.0)
|
|
104
|
+
expect(point_to_segment_distance(5, 5, nearest[0], nearest[1], nearest[0], nearest[1])).to be_within(1e-12).of(boundary_distance)
|
|
105
|
+
expect(nearest[0] == 0.0 || nearest[0] == 10.0 || nearest[1] == 0.0 || nearest[1] == 10.0).to be(true)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "keeps nearest lng/lat raw and does not force-wrap returned longitude" do
|
|
109
|
+
line = TG::Geometry.line_string([[181, 0], [181, 1]])
|
|
110
|
+
nearest = line.nearest_point_lnglat(180, 0.5)
|
|
111
|
+
|
|
112
|
+
expect(nearest[0]).to be_within(1e-12).of(181.0)
|
|
113
|
+
expect(nearest[1]).to be_within(1e-12).of(0.5)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "does not wrap antimeridian proximity" do
|
|
117
|
+
point = TG::Geometry.point(179.9, 0)
|
|
118
|
+
|
|
119
|
+
expect(point.distance_to_lnglat_meters(-179.9, 0)).to be > 30_000_000
|
|
120
|
+
expect(point.covers_xy?(-179.9, 0)).to be(false)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "matches independent local-meter segment math and stays finite near the pole" do
|
|
124
|
+
line = TG::Geometry.line_string([[0, 0], [0.01, 0]])
|
|
125
|
+
expected = lnglat_segment_distance_m(0.005, 0.01, [0, 0], [0.01, 0])
|
|
126
|
+
|
|
127
|
+
expect(line.distance_to_lnglat_meters(0.005, 0.01)).to be_within(0.001).of(expected)
|
|
128
|
+
expect(TG::Geometry.point(10, 90).distance_to_lnglat_meters(0, 90)).to be_finite
|
|
129
|
+
expect(TG::Geometry.point(10, 90).nearest_point_lnglat(0, 90)).to eq([10.0, 90.0])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "keeps Geom and Index memsize unchanged after distance calls" do
|
|
133
|
+
require "objspace"
|
|
134
|
+
|
|
135
|
+
geom = TG::Geometry.polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])
|
|
136
|
+
index = TG::Geometry::Index.build([[:zone, geom]], via: :geom, strategy: :rtree)
|
|
137
|
+
geom_size = ObjectSpace.memsize_of(geom)
|
|
138
|
+
index_size = ObjectSpace.memsize_of(index)
|
|
139
|
+
|
|
140
|
+
5.times do
|
|
141
|
+
geom.distance_to_xy(5, 5)
|
|
142
|
+
geom.nearest_point_lnglat(0.01, 0.01)
|
|
143
|
+
index.within_distance_xy(5, 5, 10)
|
|
144
|
+
index.within_distance_lnglat_meters(0.01, 0.01, 2_000)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
expect(ObjectSpace.memsize_of(geom)).to eq(geom_size)
|
|
148
|
+
expect(ObjectSpace.memsize_of(index)).to eq(index_size)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "survives GC stress and compaction" do
|
|
152
|
+
geom = TG::Geometry.line_string([[0, 0], [10, 0]])
|
|
153
|
+
|
|
154
|
+
GC.stress = true
|
|
155
|
+
expect(geom.distance_to_xy(5, 3)).to be_within(1e-12).of(3.0)
|
|
156
|
+
expect(geom.nearest_point_xy(5, 3)).to eq([5.0, 0.0])
|
|
157
|
+
ensure
|
|
158
|
+
GC.stress = false
|
|
159
|
+
GC.compact if GC.respond_to?(:compact)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
RSpec.describe "Index distance radius queries" do
|
|
164
|
+
def build_distance_entries
|
|
165
|
+
[
|
|
166
|
+
[:zone, TG::Geometry.polygon([[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]])],
|
|
167
|
+
[:line, TG::Geometry.line_string([[4, 0], [4, 4]])],
|
|
168
|
+
[:point, TG::Geometry.point(8, 0)]
|
|
169
|
+
]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "matches brute force membership for xy and returns filter distances" do
|
|
173
|
+
entries = build_distance_entries
|
|
174
|
+
index = TG::Geometry::Index.build(entries, via: :geom, strategy: :rtree)
|
|
175
|
+
|
|
176
|
+
srand(1234)
|
|
177
|
+
30.times do
|
|
178
|
+
x = rand * 10.0 - 1.0
|
|
179
|
+
y = rand * 5.0 - 1.0
|
|
180
|
+
radius = rand * 3.0
|
|
181
|
+
expected = entries.filter_map do |id, geom|
|
|
182
|
+
distance = geom.distance_to_xy(x, y)
|
|
183
|
+
[id, distance] if distance <= radius
|
|
184
|
+
end
|
|
185
|
+
actual = index.within_distance_xy(x, y, radius)
|
|
186
|
+
|
|
187
|
+
expect(actual.map(&:first)).to eq(expected.map(&:first))
|
|
188
|
+
actual.each do |id, distance|
|
|
189
|
+
geom = entries.assoc(id)[1]
|
|
190
|
+
expect(distance).to be_within(1e-12).of(geom.distance_to_xy(x, y))
|
|
191
|
+
end
|
|
192
|
+
expect(index.within_distance_ids_xy(x, y, radius)).to eq(expected.map(&:first))
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "matches brute force membership for lng/lat meters" do
|
|
197
|
+
entries = [
|
|
198
|
+
[:zone, TG::Geometry.polygon([[0, 0], [0.02, 0], [0.02, 0.02], [0, 0.02], [0, 0]])],
|
|
199
|
+
[:line, TG::Geometry.line_string([[0.04, 0], [0.04, 0.04]])],
|
|
200
|
+
[:point, TG::Geometry.point(0.08, 0)]
|
|
201
|
+
]
|
|
202
|
+
index = TG::Geometry::Index.build(entries, via: :geom, strategy: :rtree)
|
|
203
|
+
|
|
204
|
+
srand(5678)
|
|
205
|
+
30.times do
|
|
206
|
+
lng = rand * 0.1 - 0.01
|
|
207
|
+
lat = rand * 0.05 - 0.01
|
|
208
|
+
radius = rand * 3_000.0
|
|
209
|
+
expected = entries.filter_map do |id, geom|
|
|
210
|
+
distance = geom.distance_to_lnglat_meters(lng, lat)
|
|
211
|
+
[id, distance] if distance <= radius
|
|
212
|
+
end
|
|
213
|
+
actual = index.within_distance_lnglat_meters(lng, lat, radius)
|
|
214
|
+
|
|
215
|
+
expect(actual.map(&:first)).to eq(expected.map(&:first))
|
|
216
|
+
actual.each do |id, distance|
|
|
217
|
+
geom = entries.assoc(id)[1]
|
|
218
|
+
expect(distance).to be_within(1e-9).of(geom.distance_to_lnglat_meters(lng, lat))
|
|
219
|
+
end
|
|
220
|
+
expect(index.within_distance_ids_lnglat_meters(lng, lat, radius)).to eq(expected.map(&:first))
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "sorts filtered pairs by distance only when requested" do
|
|
225
|
+
entries = build_distance_entries
|
|
226
|
+
index = TG::Geometry::Index.build(entries, via: :geom, strategy: :rtree)
|
|
227
|
+
sorted = index.within_distance_xy(3, 1, 10, sort: true)
|
|
228
|
+
|
|
229
|
+
expect(sorted.map(&:last)).to eq(sorted.map(&:last).sort)
|
|
230
|
+
expect(index.within_distance_xy(3, 1, 10, sort: false).map(&:first)).to eq([:zone, :line, :point])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "handles zero radius and near-pole/full-longitude prefilter cases" do
|
|
234
|
+
zone = TG::Geometry.polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
|
|
235
|
+
pole_point = TG::Geometry.point(90, 90)
|
|
236
|
+
index = TG::Geometry::Index.build([[:zone, zone], [:pole, pole_point]], via: :geom, strategy: :rtree)
|
|
237
|
+
|
|
238
|
+
expect(index.within_distance_ids_xy(0.5, 0.5, 0)).to eq([:zone])
|
|
239
|
+
expect(index.within_distance_ids_lnglat_meters(0, 90, 1)).to eq([:pole])
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "rejects invalid arguments and keywords" do
|
|
243
|
+
index = TG::Geometry::Index.build(build_distance_entries, via: :geom, strategy: :rtree)
|
|
244
|
+
geom = TG::Geometry.point(0, 0)
|
|
245
|
+
|
|
246
|
+
expect { geom.distance_to_lnglat_meters(Float::NAN, 0) }.to raise_error(TG::Geometry::ArgumentError)
|
|
247
|
+
expect { geom.distance_to_lnglat_meters(0, Float::INFINITY) }.to raise_error(TG::Geometry::ArgumentError)
|
|
248
|
+
expect { geom.distance_to_lnglat_meters(181, 0) }.to raise_error(TG::Geometry::ArgumentError)
|
|
249
|
+
expect { geom.distance_to_lnglat_meters(0, -91) }.to raise_error(TG::Geometry::ArgumentError)
|
|
250
|
+
expect { geom.distance_to_xy(0, Float::NAN) }.to raise_error(TG::Geometry::ArgumentError)
|
|
251
|
+
expect { geom.distance_to_xy(0, 0, metric: :meters) }.to raise_error(TG::Geometry::ArgumentError)
|
|
252
|
+
expect { index.within_distance_xy(0, 0, -1) }.to raise_error(TG::Geometry::ArgumentError)
|
|
253
|
+
expect { index.within_distance_lnglat_meters(0, 0, Float::INFINITY) }.to raise_error(TG::Geometry::ArgumentError)
|
|
254
|
+
expect { index.within_distance_xy(0, 0, 1, bogus: true) }.to raise_error(TG::Geometry::ArgumentError)
|
|
255
|
+
expect { index.within_distance_ids_xy(0, 0, 1, sort: true) }.to raise_error(TG::Geometry::ArgumentError)
|
|
256
|
+
expect { index.within_distance_ids_lnglat_meters(0, 0, 1, sort: true) }.to raise_error(TG::Geometry::ArgumentError)
|
|
257
|
+
end
|
|
258
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tg_geometry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Haydarov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rake
|
|
@@ -64,9 +63,9 @@ description: Defines TG::Geometry with immutable Geom parsing and constructor wr
|
|
|
64
63
|
wrappers, value Segment wrappers, Registry reload sugar, optional ActiveRecord source
|
|
65
64
|
helpers, and an immutable geofencing-oriented Index with owned and borrowed geometry
|
|
66
65
|
ingestion, flat/rtree strategies, deterministic ordered id results, exact rtree
|
|
67
|
-
allocation accounting, and native-endian packed point batch queries,
|
|
68
|
-
|
|
69
|
-
support is not claimed.
|
|
66
|
+
allocation accounting, and native-endian packed point batch queries, explicit local
|
|
67
|
+
point-to-geometry distance/radius queries, and FeatureSource GeoJSON FeatureCollection
|
|
68
|
+
extraction/build paths over vendored C sources. Ractor support is not claimed.
|
|
70
69
|
email:
|
|
71
70
|
- romnhajdarov@gmail.com
|
|
72
71
|
executables: []
|
|
@@ -81,6 +80,9 @@ files:
|
|
|
81
80
|
- Rakefile
|
|
82
81
|
- benchmark/_support.rb
|
|
83
82
|
- benchmark/batch_packed_vs_loop.rb
|
|
83
|
+
- benchmark/distance_memory_accounting.rb
|
|
84
|
+
- benchmark/distance_point_geom.rb
|
|
85
|
+
- benchmark/distance_within_radius.rb
|
|
84
86
|
- benchmark/ewkb_roundtrip.rb
|
|
85
87
|
- benchmark/falcon_concurrency.rb
|
|
86
88
|
- benchmark/feature_source.rb
|
|
@@ -135,6 +137,7 @@ files:
|
|
|
135
137
|
- spec/batch_packed_spec.rb
|
|
136
138
|
- spec/concurrency_spec.rb
|
|
137
139
|
- spec/constructors_spec.rb
|
|
140
|
+
- spec/distance_spec.rb
|
|
138
141
|
- spec/error_hardening_spec.rb
|
|
139
142
|
- spec/feature_source_nogvl_spec.rb
|
|
140
143
|
- spec/feature_source_spec.rb
|
|
@@ -183,7 +186,6 @@ metadata:
|
|
|
183
186
|
homepage_uri: https://github.com/roman-haidarov/tg_geometry
|
|
184
187
|
source_code_uri: https://github.com/roman-haidarov/tg_geometry/tree/main
|
|
185
188
|
changelog_uri: https://github.com/roman-haidarov/tg_geometry/blob/main/CHANGELOG.md
|
|
186
|
-
post_install_message:
|
|
187
189
|
rdoc_options: []
|
|
188
190
|
require_paths:
|
|
189
191
|
- lib
|
|
@@ -198,9 +200,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
198
200
|
- !ruby/object:Gem::Version
|
|
199
201
|
version: '0'
|
|
200
202
|
requirements: []
|
|
201
|
-
rubygems_version: 3.
|
|
202
|
-
signing_key:
|
|
203
|
+
rubygems_version: 3.6.7
|
|
203
204
|
specification_version: 4
|
|
204
205
|
summary: Native extension for TG::Geometry parsing, predicates, immutable indexes,
|
|
205
|
-
FeatureSource imports, registries, low-level wrappers,
|
|
206
|
+
FeatureSource imports, registries, low-level wrappers, packed point batches, and
|
|
207
|
+
local distance queries
|
|
206
208
|
test_files: []
|