geoprojection 0.1.1 → 0.1.2
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/Gemfile.lock +1 -1
- data/README.md +20 -2
- data/lib/geoprojection/distance.rb +129 -0
- data/lib/geoprojection/ellipse.rb +0 -4
- data/lib/geoprojection/version.rb +1 -1
- data/lib/geoprojection.rb +10 -0
- data/test.js +50 -0
- data/test2.js +17 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5ed2077814688af2c2731b2c9a6d9320d112b7a08d7f77e983038f43492fdb8
|
4
|
+
data.tar.gz: 9fa634a0b969585b9d18a302b586912e6fe679fdf6e1bb1d1cced4d705d3aa6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aea7ad9252cfbfc652f9b8c9d2b7295e6e1b7f6ecb72748326a06dac21896b633e94e2f7aee49eeee1117d7819c852770ba881805ec19894ee684eb34a68c7b6
|
7
|
+
data.tar.gz: e2cf4b75a12eea14e1d40b83a279fb67294fb25449c5fc137b3c582e3e1668a4dd7c4962805b381a5d2ad19dda64d0136a18dea6cf852efc36d015e8352840ff
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Geoprojection
|
2
2
|
|
3
|
-
Toolkit for translating distances (kilometers) from co-ordinates (latitude, longitude) onto geographically-precise (within reason) projections.
|
3
|
+
Toolkit for translating distances (kilometers) from co-ordinates (latitude, longitude) onto geographically-precise (within reason) projections and calculating distances between pairs of co-ordinates.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -21,12 +21,30 @@ Provide a center point (latitude, longitude) and a distance (kilometers) to calc
|
|
21
21
|
The default number of points is `36`. This can be overridden with the `points` parameter to `Geoprojection::Ellipse.new`.
|
22
22
|
|
23
23
|
```ruby
|
24
|
-
ellipse = Geoprojection::Ellipse.new(latitude: 50.8169, longitude: 0.1367, distance: 10, points:
|
24
|
+
ellipse = Geoprojection::Ellipse.new(latitude: 50.8169, longitude: 0.1367, distance: 10, points: 6)
|
25
25
|
ellipse.points
|
26
|
+
|
27
|
+
# => [[0.2788814046153052, 50.8169], [0.20779070230765262, 50.968049947019516], [0.0656092976923474, 50.968049947019516], [-0.0054814046153052465, 50.8169], [0.0656092976923473, 50.66575005298048], [0.20779070230765262, 50.66575005298048], [0.2788814046153052, 50.8169]]
|
26
28
|
```
|
27
29
|
|
28
30
|
The actual returned points will be N+1, as the start and end point are identical to provide a complete polygon.
|
29
31
|
|
32
|
+
### Distance
|
33
|
+
|
34
|
+
Calculate the [Vincenty distance](https://en.wikipedia.org/wiki/Vincenty%27s_formulae) between two pairs of co-ordinates (latitude, longitude) in miles or kilometers:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
distance = Geoprojection::Distance.new(
|
38
|
+
{ latitude: 50.8169, longitude: 0.1367 },
|
39
|
+
{ latitude: 51.2341, longitude: 1.4231 }
|
40
|
+
)
|
41
|
+
|
42
|
+
distance.kilometers
|
43
|
+
# => 148.2691075520691
|
44
|
+
distance.miles
|
45
|
+
# => 92.13012362873673
|
46
|
+
```
|
47
|
+
|
30
48
|
## Contributing
|
31
49
|
|
32
50
|
Make changes, run `make test`, create a pull request.
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Geoprojection
|
4
|
+
# Vincenty distance calculation for a pair of latitude/longitude co-ordinates.
|
5
|
+
class Distance
|
6
|
+
# rubocop:disable Metrics/AbcSize
|
7
|
+
def initialize(point1, point2)
|
8
|
+
@latitude1 = degrees_to_radians(point1.fetch(:latitude))
|
9
|
+
@longitude1 = degrees_to_radians(point1.fetch(:longitude))
|
10
|
+
@latitude2 = degrees_to_radians(point2.fetch(:latitude))
|
11
|
+
@longitude2 = degrees_to_radians(point2.fetch(:longitude))
|
12
|
+
@tan_geo_latitude1 = (1 - FLATTENING) * Math.tan(@longitude1)
|
13
|
+
@tan_geo_latitude2 = (1 - FLATTENING) * Math.tan(@longitude2)
|
14
|
+
@cos_geo_latitude1 = 1 / Math.sqrt(1 + (@tan_geo_latitude1**2))
|
15
|
+
@cos_geo_latitude2 = 1 / Math.sqrt(1 + (@tan_geo_latitude2**2))
|
16
|
+
@sin_geo_latitude1 = @tan_geo_latitude1 * @cos_geo_latitude1
|
17
|
+
@sin_geo_latitude2 = @tan_geo_latitude2 * @cos_geo_latitude2
|
18
|
+
end
|
19
|
+
# rubocop:enable Metrics/AbcSize
|
20
|
+
|
21
|
+
def miles
|
22
|
+
meters * METERS_PER_MILE
|
23
|
+
end
|
24
|
+
|
25
|
+
def kilometers
|
26
|
+
meters / 1000.0
|
27
|
+
end
|
28
|
+
|
29
|
+
def meters
|
30
|
+
distance(*computed_variables)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def degrees_to_radians(val)
|
36
|
+
val * DEGREES_PER_RADIAN
|
37
|
+
end
|
38
|
+
|
39
|
+
def distance(theta, sin_theta, cos_theta, cos_sq_alpha, cos2_theta_m)
|
40
|
+
r_square = reduced_square(cos_sq_alpha)
|
41
|
+
vertex_b = r_square / 1024 * (256 + (r_square * (-128 + (r_square * (74 - (47 * r_square))))))
|
42
|
+
WGS84_MINOR_AXIS * vertex_a(r_square) * (theta - delta_theta(vertex_b, sin_theta, cos2_theta_m, cos_theta))
|
43
|
+
end
|
44
|
+
|
45
|
+
def reduced_square(cos_sq_alpha)
|
46
|
+
cos_sq_alpha * ((WGS84_MAJOR_AXIS**2) - (WGS84_MINOR_AXIS**2)) / (WGS84_MINOR_AXIS**2)
|
47
|
+
end
|
48
|
+
|
49
|
+
def vertex_a(r_square)
|
50
|
+
1 + (r_square / 16_384 * (4096 + (r_square * (-768 + (r_square * (320 - (175 * r_square)))))))
|
51
|
+
end
|
52
|
+
|
53
|
+
def sine_theta(lambda1)
|
54
|
+
Math.sqrt(
|
55
|
+
((@cos_geo_latitude2 * Math.sin(lambda1))**2) +
|
56
|
+
(((@cos_geo_latitude1 * @sin_geo_latitude2) -
|
57
|
+
(@sin_geo_latitude1 * @cos_geo_latitude2 * Math.cos(lambda1)))**2)
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def delta_theta(vertex_b, sin_theta, cos2_theta_m, cos_theta)
|
62
|
+
vertex_b * sin_theta * (
|
63
|
+
delta_theta1(cos2_theta_m, vertex_b, cos_theta) +
|
64
|
+
delta_theta2(cos2_theta_m, vertex_b) +
|
65
|
+
delta_theta3(sin_theta, cos2_theta_m)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def delta_theta1(cos2_theta_m, vertex_b, cos_theta)
|
70
|
+
cos2_theta_m + ((vertex_b / 4 * cos_theta * -1))
|
71
|
+
end
|
72
|
+
|
73
|
+
def delta_theta2(cos2_theta_m, vertex_b)
|
74
|
+
(2 * (cos2_theta_m**2)) - ((vertex_b / 6 * cos2_theta_m * -3))
|
75
|
+
end
|
76
|
+
|
77
|
+
def delta_theta3(sin_theta, cos2_theta_m)
|
78
|
+
(4 * (sin_theta**2) * -3) + (4 * (cos2_theta_m**2))
|
79
|
+
end
|
80
|
+
|
81
|
+
def computed_variables
|
82
|
+
lambda1 = latitude_distance
|
83
|
+
|
84
|
+
precision = VINCENTY_PRECISION * 2
|
85
|
+
|
86
|
+
while precision > VINCENTY_PRECISION
|
87
|
+
theta, sin_theta, cos_theta, cos_sq_alpha, cos2_theta_m, lambda1, precision = iteration_variables(lambda1)
|
88
|
+
end
|
89
|
+
[theta, sin_theta, cos_theta, cos_sq_alpha, cos2_theta_m]
|
90
|
+
end
|
91
|
+
|
92
|
+
def iteration_variables(lambda1)
|
93
|
+
sin_theta = sine_theta(lambda1)
|
94
|
+
cos_theta = calculated_cos_theta(lambda1)
|
95
|
+
theta = Math.atan2(sin_theta, cos_theta)
|
96
|
+
sin_alpha = @cos_geo_latitude1 * @cos_geo_latitude2 * Math.sin(lambda1) / sin_theta
|
97
|
+
cos_sq_alpha = 1 - (sin_alpha**2)
|
98
|
+
cos2_theta_m = calculated_cos2_theta_m(cos_theta, cos_sq_alpha)
|
99
|
+
lambda2 = iteration_lambda(sin_alpha, theta, sin_theta, cos_theta, cos_sq_alpha, cos2_theta_m)
|
100
|
+
[theta, sin_theta, cos_theta, cos_sq_alpha, cos2_theta_m, lambda2, (lambda2 - lambda1).abs]
|
101
|
+
end
|
102
|
+
|
103
|
+
# rubocop:disable Metrics/ParameterLists
|
104
|
+
def iteration_lambda(sin_alpha, theta, sin_theta, cos_theta, cos_sq_alpha, cos2_theta_m)
|
105
|
+
c = calculated_c(cos_sq_alpha)
|
106
|
+
latitude_distance + (
|
107
|
+
(1 - c) * FLATTENING * sin_alpha *
|
108
|
+
(theta + (c * sin_theta * (cos2_theta_m + c + ((cos_theta * -1) + (2 * (cos2_theta_m**2))))))
|
109
|
+
)
|
110
|
+
end
|
111
|
+
# rubocop:enable Metrics/ParameterLists
|
112
|
+
|
113
|
+
def calculated_cos_theta(lambda_)
|
114
|
+
(@sin_geo_latitude1 * @sin_geo_latitude2) + (@cos_geo_latitude1 * @cos_geo_latitude2 * Math.cos(lambda_))
|
115
|
+
end
|
116
|
+
|
117
|
+
def calculated_c(cos_sq_alpha)
|
118
|
+
(FLATTENING / 16) * cos_sq_alpha * (4 + (FLATTENING * (4 - (3 * cos_sq_alpha))))
|
119
|
+
end
|
120
|
+
|
121
|
+
def calculated_cos2_theta_m(cos_theta, cos_sq_alpha)
|
122
|
+
cos_theta - (2 * @sin_geo_latitude1 * @sin_geo_latitude2 / cos_sq_alpha)
|
123
|
+
end
|
124
|
+
|
125
|
+
def latitude_distance
|
126
|
+
@latitude_distance ||= @latitude2 - @latitude1
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -3,10 +3,6 @@
|
|
3
3
|
module Geoprojection
|
4
4
|
# Elliptical projection of a center point (lat, long) with a provided distance from the center.
|
5
5
|
class Ellipse
|
6
|
-
DEGREES_PER_RADIAN = Math::PI / 180
|
7
|
-
EQUATOR_KM_PER_LATITUDE_RADIAN = 110.567 * DEGREES_PER_RADIAN
|
8
|
-
EQUATOR_KM_PER_LONGITUDE_RADIAN = 111.321 * DEGREES_PER_RADIAN
|
9
|
-
|
10
6
|
def initialize(latitude:, longitude:, distance:, points: 36)
|
11
7
|
@latitude = latitude
|
12
8
|
@longitude = longitude
|
data/lib/geoprojection.rb
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'geoprojection/version'
|
4
|
+
require_relative 'geoprojection/distance'
|
4
5
|
require_relative 'geoprojection/ellipse'
|
5
6
|
|
6
7
|
module Geoprojection
|
7
8
|
class Error < StandardError; end
|
9
|
+
|
10
|
+
DEGREES_PER_RADIAN = Math::PI / 180
|
11
|
+
EQUATOR_KM_PER_LATITUDE_RADIAN = 110.567 * DEGREES_PER_RADIAN
|
12
|
+
EQUATOR_KM_PER_LONGITUDE_RADIAN = 111.321 * DEGREES_PER_RADIAN
|
13
|
+
METERS_PER_MILE = 0.000621371
|
14
|
+
WGS84_MAJOR_AXIS = 6_378_137.0
|
15
|
+
WGS84_MINOR_AXIS = 6_356_752.314245
|
16
|
+
FLATTENING = (WGS84_MAJOR_AXIS - WGS84_MINOR_AXIS) / WGS84_MAJOR_AXIS
|
17
|
+
VINCENTY_PRECISION = 1e-12
|
8
18
|
end
|
data/test.js
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
const λ1 = 51.8169 * (Math.PI / 180);
|
2
|
+
const λ2 = 50.8169 * (Math.PI / 180);
|
3
|
+
const φ1 = -0.1367 * (Math.PI / 180);
|
4
|
+
const φ2 = -0.4367 * (Math.PI / 180);
|
5
|
+
const a = 6378137;
|
6
|
+
const b = 6356752.314245;
|
7
|
+
// const f = 1 / 298.257223563;
|
8
|
+
const f = (a - b) / a;
|
9
|
+
const L = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ.
|
10
|
+
const tanU1 = (1-f) * Math.tan(φ1), cosU1 = 1 / Math.sqrt((1 + tanU1*tanU1)), sinU1 = tanU1 * cosU1;
|
11
|
+
const tanU2 = (1-f) * Math.tan(φ2), cosU2 = 1 / Math.sqrt((1 + tanU2*tanU2)), sinU2 = tanU2 * cosU2;
|
12
|
+
|
13
|
+
let λ = L, sinλ = null, cosλ = null; // λ = difference in longitude on an auxiliary sphere
|
14
|
+
let σ = null, sinσ = null, cosσ = null; // σ = angular distance P₁ P₂ on the sphere
|
15
|
+
let cos2σₘ = null; // σₘ = angular distance on the sphere from the equator to the midpoint of the line
|
16
|
+
let cosSqα = null; // α = azimuth of the geodesic at the equator
|
17
|
+
|
18
|
+
let λʹ = null;
|
19
|
+
do {
|
20
|
+
console.log(λ);
|
21
|
+
sinλ = Math.sin(λ);
|
22
|
+
cosλ = Math.cos(λ);
|
23
|
+
console.log(sinλ);
|
24
|
+
console.log(cosλ);
|
25
|
+
console.log((cosU2*sinλ) * (cosU2*sinλ))
|
26
|
+
console.log((cosU1*sinU2-sinU1*cosU2*cosλ)**2);
|
27
|
+
const sinSqσ = (cosU2*sinλ) * (cosU2*sinλ) + (cosU1*sinU2-sinU1*cosU2*cosλ)**2;
|
28
|
+
sinσ = Math.sqrt(sinSqσ);
|
29
|
+
cosσ = sinU1*sinU2 + cosU1*cosU2*cosλ;
|
30
|
+
σ = Math.atan2(sinσ, cosσ);
|
31
|
+
const sinα = cosU1 * cosU2 * sinλ / sinσ;
|
32
|
+
cosSqα = 1 - sinα*sinα;
|
33
|
+
cos2σₘ = cosσ - 2*sinU1*sinU2/cosSqα;
|
34
|
+
const C = f/16*cosSqα*(4+f*(4-3*cosSqα));
|
35
|
+
λʹ = λ;
|
36
|
+
λ = L + (1-C) * f * sinα * (σ + C*sinσ*(cos2σₘ+C*cosσ*(-1+2*cos2σₘ*cos2σₘ)));
|
37
|
+
} while (Math.abs(λ-λʹ) > 1e-12);
|
38
|
+
|
39
|
+
const uSq = cosSqα * (a*a - b*b) / (b*b);
|
40
|
+
const A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
|
41
|
+
const B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
|
42
|
+
const Δσ = B*sinσ*(cos2σₘ+B/4*(cosσ*(-1+2*cos2σₘ*cos2σₘ)-B/6*cos2σₘ*(-3+4*sinσ*sinσ)*(-3+4*cos2σₘ*cos2σₘ)));
|
43
|
+
|
44
|
+
const s = b*A*(σ-Δσ); // s = length of the geodesic
|
45
|
+
|
46
|
+
console.log(s);
|
47
|
+
const α1 = Math.atan2(cosU2*sinλ, cosU1*sinU2-sinU1*cosU2*cosλ); // initial bearing
|
48
|
+
const α2 = Math.atan2(cosU1*sinλ, -sinU1*cosU2+cosU1*sinU2*cosλ); // final bearin
|
49
|
+
console.log(α1)
|
50
|
+
console.log(α2 * (Math.PI / 180))
|
data/test2.js
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
const lat1 = 50.8169;
|
2
|
+
const lat2 = 51.8169;
|
3
|
+
const lon1 = -0.1367;
|
4
|
+
const lon2 = -0.4367;
|
5
|
+
const R = 6371e3; // metres
|
6
|
+
const φ1 = lat1 * Math.PI/180; // φ, λ in radians
|
7
|
+
const φ2 = lat2 * Math.PI/180;
|
8
|
+
const Δφ = (lat2-lat1) * Math.PI/180;
|
9
|
+
const Δλ = (lon2-lon1) * Math.PI/180;
|
10
|
+
|
11
|
+
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
|
12
|
+
Math.cos(φ1) * Math.cos(φ2) *
|
13
|
+
Math.sin(Δλ/2) * Math.sin(Δλ/2);
|
14
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
15
|
+
|
16
|
+
const d = R * c;
|
17
|
+
console.log(d);
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geoprojection
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Farrell
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Write a longer description or delete this line.
|
14
14
|
email:
|
@@ -27,10 +27,13 @@ files:
|
|
27
27
|
- Rakefile
|
28
28
|
- doc/example.rb
|
29
29
|
- lib/geoprojection.rb
|
30
|
+
- lib/geoprojection/distance.rb
|
30
31
|
- lib/geoprojection/ellipse.rb
|
31
32
|
- lib/geoprojection/version.rb
|
32
33
|
- sig/geoprojection.rbs
|
34
|
+
- test.js
|
33
35
|
- test.rb
|
36
|
+
- test2.js
|
34
37
|
homepage: https://github.com/bobf/geoprojection
|
35
38
|
licenses: []
|
36
39
|
metadata:
|