coordconver 0.1.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +36 -0
- data/CHANGELOG.md +59 -0
- data/README.en.md +177 -0
- data/README.md +242 -175
- data/coordconver.gemspec +33 -17
- data/docs/cc-algorithms.js +293 -0
- data/docs/index.html +283 -0
- data/exe/coordconver +241 -0
- data/lib/coordconver/coordconver.rb +268 -86
- data/lib/coordconver/degree.rb +47 -49
- data/lib/coordconver/errors.rb +23 -0
- data/lib/coordconver/formats.rb +51 -0
- data/lib/coordconver/geo/api.rb +45 -0
- data/lib/coordconver/geo/baidu.rb +67 -23
- data/lib/coordconver/geo/cache.rb +66 -0
- data/lib/coordconver/geo/client.rb +18 -5
- data/lib/coordconver/geo/configuration.rb +99 -9
- data/lib/coordconver/geo/gaode.rb +80 -25
- data/lib/coordconver/geo/key_pool.rb +78 -0
- data/lib/coordconver/geo/tencent.rb +69 -0
- data/lib/coordconver/projection.rb +129 -0
- data/lib/coordconver/utils/http.rb +71 -44
- data/lib/coordconver/utils/request.rb +9 -6
- data/lib/coordconver/version.rb +3 -1
- data/lib/coordconver.rb +20 -6
- metadata +37 -33
- data/.travis.yml +0 -8
- data/bin/coordconver +0 -4
|
@@ -1,49 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Coordconver
|
|
2
4
|
module Geo
|
|
5
|
+
# 高德地图 Web 服务 API
|
|
6
|
+
# 申请 key: https://console.amap.com/dev/key/app (类型选"Web服务")
|
|
3
7
|
module Gaode
|
|
4
8
|
extend self
|
|
5
9
|
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
# 视为"key 无效/配额超限"的 infocode, 触发自动轮换下一个 key
|
|
11
|
+
# 10001 KEY不合法 10003 日调用量超限 10009 KEY类型错误 10044 超出支持范围 20003 日调用量超限
|
|
12
|
+
KEY_ERROR_INFOCODES = %w[10001 10003 10009 10044 20003].freeze
|
|
13
|
+
|
|
14
|
+
# 高德官方坐标转换接口支持的源坐标系
|
|
15
|
+
COORDSYS_MAP = {
|
|
16
|
+
gps: "gps", wgs84: "gps", wgs: "gps",
|
|
17
|
+
baidu: "baidu", bd09: "baidu", bd: "baidu",
|
|
18
|
+
gcj02: "autonavi", gcj: "autonavi", autonavi: "autonavi"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# 地理编码: 结构化地址 -> 坐标 (GCJ-02)
|
|
22
|
+
# Coordconver::Gaode.geo("北京方恒国际中心A座")["geocodes"][0]["location"] # => "116.480724,39.989584"
|
|
23
|
+
# 支持可选参数: city(城市名), citylimit, batch, ...
|
|
24
|
+
def geo(address, options = {})
|
|
25
|
+
params = { address: address }.merge(options)
|
|
26
|
+
request("/geocode/geo", params)
|
|
27
|
+
end
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
# 逆地理编码: 坐标 (GCJ-02) -> 结构化地址
|
|
30
|
+
# Coordconver::Gaode.regeo(116.480881, 39.989410)["regeocode"]["formatted_address"]
|
|
31
|
+
# options: extensions(base/all), radius, poitype, ...
|
|
32
|
+
def regeo(lng, lat, options = {})
|
|
33
|
+
params = { location: "#{lng},#{lat}" }.merge(options)
|
|
34
|
+
request("/geocode/regeo", params)
|
|
16
35
|
end
|
|
17
36
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
# 官方坐标转换 API: 将 GPS(WGS-84)/百度(BD-09) 坐标转换为高德坐标 (GCJ-02)。
|
|
38
|
+
# 与离线算法互为补充, 适合需要官方权威转换结果的场景。
|
|
39
|
+
# 单点或数组均可(最多 40 个点):
|
|
40
|
+
# Coordconver::Gaode.convert([121.468738, 31.247138], from: :gps)
|
|
41
|
+
# Coordconver::Gaode.convert([[121.46, 31.24], [116.39, 39.90]], from: :gps)
|
|
42
|
+
# 返回完整 JSON; from 支持 :gps(wgs84) / :baidu(bd09) / :gcj02
|
|
43
|
+
def convert(points, from: :gps)
|
|
44
|
+
coordsys = COORDSYS_MAP[from.to_s.downcase.to_sym]
|
|
45
|
+
raise ArgumentError, "不支持的源坐标系: #{from.inspect} (支持 :gps/:wgs84, :baidu/:bd09, :gcj02)" unless coordsys
|
|
46
|
+
|
|
47
|
+
list = normalize_point_list(points)
|
|
48
|
+
raise ArgumentError, "单次最多转换 40 个坐标点" if list.size > 40
|
|
22
49
|
|
|
23
50
|
params = {
|
|
24
|
-
|
|
25
|
-
|
|
51
|
+
locations: list.map { |pt| Coordconver.extract_pair(pt).join(",") }.join(";"),
|
|
52
|
+
coordsys: coordsys
|
|
26
53
|
}
|
|
54
|
+
request("/assistant/coordinate/convert", params)
|
|
55
|
+
end
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
# convert 的便捷版本, 返回 [[lng, lat], ...] (GCJ-02), 失败返回 nil
|
|
58
|
+
def convert_location(points, from: :gps)
|
|
59
|
+
resp = convert(points, from: from)
|
|
60
|
+
return nil unless resp["status"] == "1" && resp["locations"]
|
|
61
|
+
|
|
62
|
+
resp["locations"].split(";").map { |s| s.split(",").map(&:to_f) }
|
|
29
63
|
end
|
|
30
64
|
|
|
31
|
-
|
|
65
|
+
# 便捷方法: 地址 -> [lng, lat] (GCJ-02), 解析失败返回 nil
|
|
66
|
+
def geo_location(address, options = {})
|
|
67
|
+
resp = geo(address, options)
|
|
68
|
+
geocode = resp["geocodes"] && resp["geocodes"][0]
|
|
69
|
+
loc = geocode && geocode["location"]
|
|
70
|
+
return nil if loc.nil? || loc.empty?
|
|
32
71
|
|
|
33
|
-
|
|
34
|
-
base_url + '/regeo' + "?key=#{gaode_map_key}&location=#{lng},#{lat}"
|
|
72
|
+
loc.split(",").map(&:to_f)
|
|
35
73
|
end
|
|
36
74
|
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
# 便捷方法: 坐标 -> 地址字符串 (GCJ-02), 解析失败返回 nil
|
|
76
|
+
def regeo_address(lng, lat, options = {})
|
|
77
|
+
regeo(lng, lat, options).dig("regeocode", "formatted_address")
|
|
39
78
|
end
|
|
40
79
|
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def normalize_point_list(points)
|
|
83
|
+
list = points.is_a?(Array) && points.first.is_a?(Numeric) ? [points] : Array(points)
|
|
84
|
+
raise ArgumentError, "未提供坐标点" if list.empty?
|
|
85
|
+
|
|
86
|
+
list
|
|
43
87
|
end
|
|
44
88
|
|
|
45
|
-
def
|
|
46
|
-
|
|
89
|
+
def request(path, params)
|
|
90
|
+
Api.call(
|
|
91
|
+
provider: :gaode,
|
|
92
|
+
url: base_url + path,
|
|
93
|
+
params: params,
|
|
94
|
+
key_param: :key,
|
|
95
|
+
bad_result: ->(r) { r["status"].to_s == "0" && KEY_ERROR_INFOCODES.include?(r["infocode"].to_s) }
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def base_url
|
|
100
|
+
# 兼容旧版本以 /geocode 结尾的 base_url 配置
|
|
101
|
+
Configuration.settings[:gaode_base_url].sub(%r{/geocode/?\z}, "").sub(%r{/\z}, "")
|
|
47
102
|
end
|
|
48
103
|
end
|
|
49
104
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coordconver
|
|
4
|
+
module Geo
|
|
5
|
+
# 多 key 智能轮询: round-robin 取 key, 请求失败(配额超限/无效)时摘除该 key,
|
|
6
|
+
# 冷却时间过后自动恢复。线程安全。
|
|
7
|
+
class KeyPool
|
|
8
|
+
# 被摘除 key 的冷却时长(秒), 覆盖"每秒/每日配额超限"两类场景
|
|
9
|
+
COOLDOWN = 300
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@index = {}
|
|
14
|
+
@disabled_at = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 轮询取下一个可用 key; 全部处于冷却期时抛出 ApiError
|
|
18
|
+
def next_key(provider, keys)
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
purge_expired(Time.now)
|
|
21
|
+
enabled = keys.reject { |k| @disabled_at[[provider, k]] }
|
|
22
|
+
if enabled.empty?
|
|
23
|
+
raise ApiError, "所有 #{provider} key 均不可用(配额超限或无效), 请更换或补充 key"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@index[provider] = (@index[provider] ||= -1) + 1
|
|
27
|
+
enabled[@index[provider] % enabled.size]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mark_bad(provider, key)
|
|
32
|
+
@mutex.synchronize { @disabled_at[[provider, key]] = Time.now }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def disabled?(provider, key)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
purge_expired(Time.now)
|
|
38
|
+
!@disabled_at[[provider, key]].nil?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset!
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
@index.clear
|
|
45
|
+
@disabled_at.clear
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def purge_expired(now)
|
|
52
|
+
@disabled_at.reject! { |_, t| now - t > COOLDOWN }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
def instance
|
|
57
|
+
@instance ||= new
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def next_key(provider, keys)
|
|
61
|
+
instance.next_key(provider, keys)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def mark_bad(provider, key)
|
|
65
|
+
instance.mark_bad(provider, key)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def disabled?(provider, key)
|
|
69
|
+
instance.disabled?(provider, key)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def reset!
|
|
73
|
+
instance.reset!
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coordconver
|
|
4
|
+
module Geo
|
|
5
|
+
# 腾讯位置服务 WebService API
|
|
6
|
+
# 申请 key: https://lbs.qq.com/dev/console/application/mine (启用 WebServiceAPI)
|
|
7
|
+
# 坐标为 GCJ-02; 逆地理编码 location 参数为 "lat,lng" 顺序
|
|
8
|
+
module Tencent
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# 视为"key 无效/配额超限"的状态码, 触发自动轮换下一个 key
|
|
12
|
+
# 110 请求来源未被授权 112 IP未被授权 120 每秒调用量超限 121 日调用量超限 199 key结构错误
|
|
13
|
+
KEY_ERROR_STATUSES = [110, 112, 120, 121, 199].freeze
|
|
14
|
+
|
|
15
|
+
# 地理编码: 结构化地址 -> 坐标 (GCJ-02)
|
|
16
|
+
# Coordconver::Tencent.geo("北京市海淀区彩和坊路海淀西大街74号")["result"]["location"]
|
|
17
|
+
# 支持可选参数: region(城市), get_poi, ...
|
|
18
|
+
def geo(address, options = {})
|
|
19
|
+
params = { address: address }.merge(options)
|
|
20
|
+
request("/geocoder/v1/", params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 逆地理编码: 坐标 (GCJ-02) -> 结构化地址
|
|
24
|
+
# Coordconver::Tencent.regeo(116.322987, 39.983424)["result"]["address"]
|
|
25
|
+
def regeo(lng, lat, options = {})
|
|
26
|
+
params = {
|
|
27
|
+
location: "#{lat},#{lng}", # 腾讯接口为 lat,lng 顺序
|
|
28
|
+
get_poi: 0
|
|
29
|
+
}.merge(options)
|
|
30
|
+
request("/geocoder/v1/", params)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# IP 定位: IP -> 城市 + 坐标 (GCJ-02)
|
|
34
|
+
def ip(ip = nil)
|
|
35
|
+
params = {}
|
|
36
|
+
params[:ip] = ip unless ip.nil? || ip.empty?
|
|
37
|
+
request("/location/v1/ip", params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 便捷方法: 地址 -> [lng, lat] (GCJ-02), 解析失败返回 nil
|
|
41
|
+
def geo_location(address, options = {})
|
|
42
|
+
loc = geo(address, options).dig("result", "location")
|
|
43
|
+
loc && [loc["lng"], loc["lat"]]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 便捷方法: 坐标 -> 地址字符串 (GCJ-02), 解析失败返回 nil
|
|
47
|
+
def regeo_address(lng, lat, options = {})
|
|
48
|
+
result = regeo(lng, lat, options).dig("result") || {}
|
|
49
|
+
result.dig("formatted_addresses", "recommend") || result["address"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def request(path, params)
|
|
55
|
+
Api.call(
|
|
56
|
+
provider: :tencent,
|
|
57
|
+
url: base_url + path,
|
|
58
|
+
params: params,
|
|
59
|
+
key_param: :key,
|
|
60
|
+
bad_result: ->(r) { KEY_ERROR_STATUSES.include?(r["status"].to_i) }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def base_url
|
|
65
|
+
Configuration.settings[:tencent_base_url].sub(%r{/\z}, "")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# 高斯-克吕格投影(横轴墨卡托)正反算, 以及 3°/6° 带号与中央经线换算。
|
|
4
|
+
# 适用于 CGCS2000 / 北京54 / 西安80 / WGS-84 椭球, 精度毫米级。
|
|
5
|
+
#
|
|
6
|
+
# 经 Coordconver.extend(Projection) 挂载:
|
|
7
|
+
# Coordconver.gauss_kruger(121.47, 31.24) # => [x东坐标, y北坐标]
|
|
8
|
+
# Coordconver.inverse_gauss_kruger(543_000, 3_456_000, central_meridian: 120)
|
|
9
|
+
# Coordconver.zone_meridian(121.47, width: 3) # => [40, 120]
|
|
10
|
+
module Coordconver
|
|
11
|
+
module Projection
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# 椭球参数: a 长半轴(米), rf 扁率倒数
|
|
15
|
+
ELLIPSOIDS = {
|
|
16
|
+
cgcs2000: [6_378_137.0, 298.257_222_101],
|
|
17
|
+
wgs84: [6_378_137.0, 298.257_223_563],
|
|
18
|
+
krassovsky: [6_378_245.0, 298.3], # 北京54
|
|
19
|
+
iag75: [6_378_140.0, 298.257] # 西安80
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# 高斯-克吕格正算: 经纬度 -> 平面坐标 [东坐标 x, 北坐标 y] (米)
|
|
23
|
+
# central_meridian: 中央经线(度), 缺省按 zone_width 自动推算
|
|
24
|
+
# zone_width: 3 或 6 (度), 仅在未指定 central_meridian 时使用
|
|
25
|
+
# false_easting: 东坐标偏移(米), 默认 500_000 (测绘惯例, 保证 x 为正)
|
|
26
|
+
# ellipsoid: :cgcs2000 / :wgs84 / :krassovsky(北京54) / :iag75(西安80)
|
|
27
|
+
def gauss_kruger(lng, lat, central_meridian: nil, zone_width: 6,
|
|
28
|
+
ellipsoid: :cgcs2000, false_easting: 500_000, false_northing: 0)
|
|
29
|
+
a, f = ellipsoid_params(ellipsoid)
|
|
30
|
+
cm = central_meridian || zone_meridian(lng, width: zone_width)[1]
|
|
31
|
+
e2 = f * (2 - f)
|
|
32
|
+
ep2 = e2 / (1 - e2)
|
|
33
|
+
|
|
34
|
+
p = deg2rad(lat)
|
|
35
|
+
dl = deg2rad(lng - cm)
|
|
36
|
+
sin_p = Math.sin(p)
|
|
37
|
+
cos_p = Math.cos(p)
|
|
38
|
+
tan_p = Math.tan(p)
|
|
39
|
+
|
|
40
|
+
n = a / Math.sqrt(1 - e2 * sin_p * sin_p)
|
|
41
|
+
t = tan_p * tan_p
|
|
42
|
+
c = ep2 * cos_p * cos_p
|
|
43
|
+
aa = dl * cos_p
|
|
44
|
+
m = meridional_arc(p, a, e2)
|
|
45
|
+
|
|
46
|
+
x = false_easting + n * (aa +
|
|
47
|
+
(1 - t + c) * aa**3 / 6 +
|
|
48
|
+
(5 - 18 * t + t * t + 72 * c - 58 * ep2) * aa**5 / 120)
|
|
49
|
+
y = false_northing + m + n * tan_p * (aa**2 / 2 +
|
|
50
|
+
(5 - t + 9 * c + 4 * c * c) * aa**4 / 24 +
|
|
51
|
+
(61 - 58 * t + t * t + 600 * c - 330 * ep2) * aa**6 / 720)
|
|
52
|
+
[x.round(4), y.round(4)]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 高斯-克吕格反算: 平面坐标 -> 经纬度 [lng, lat]
|
|
56
|
+
# central_meridian 必填(反算无法从 x, y 推出投影带)
|
|
57
|
+
def inverse_gauss_kruger(x, y, central_meridian:, ellipsoid: :cgcs2000,
|
|
58
|
+
false_easting: 500_000, false_northing: 0)
|
|
59
|
+
a, f = ellipsoid_params(ellipsoid)
|
|
60
|
+
e2 = f * (2 - f)
|
|
61
|
+
ep2 = e2 / (1 - e2)
|
|
62
|
+
|
|
63
|
+
m = y - false_northing
|
|
64
|
+
mu = m / (a * (1 - e2 / 4 - 3 * e2**2 / 64 - 5 * e2**3 / 256))
|
|
65
|
+
e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2))
|
|
66
|
+
|
|
67
|
+
p1 = footpoint_latitude(mu, e1)
|
|
68
|
+
sin_p1 = Math.sin(p1)
|
|
69
|
+
cos_p1 = Math.cos(p1)
|
|
70
|
+
tan_p1 = Math.tan(p1)
|
|
71
|
+
|
|
72
|
+
c1 = ep2 * cos_p1 * cos_p1
|
|
73
|
+
t1 = tan_p1 * tan_p1
|
|
74
|
+
n1 = a / Math.sqrt(1 - e2 * sin_p1 * sin_p1)
|
|
75
|
+
r1 = a * (1 - e2) / (1 - e2 * sin_p1 * sin_p1)**1.5
|
|
76
|
+
d = (x - false_easting) / n1
|
|
77
|
+
|
|
78
|
+
p = p1 - (n1 * tan_p1 / r1) * (d**2 / 2 -
|
|
79
|
+
(5 + 3 * t1 + 10 * c1 - 4 * c1 * c1 - 9 * ep2) * d**4 / 24 +
|
|
80
|
+
(61 + 90 * t1 + 298 * c1 + 45 * t1 * t1 - 252 * ep2 - 3 * c1 * c1) * d**6 / 720)
|
|
81
|
+
l = (d -
|
|
82
|
+
(1 + 2 * t1 + c1) * d**3 / 6 +
|
|
83
|
+
(5 - 2 * c1 + 28 * t1 - 3 * c1 * c1 + 8 * ep2 + 24 * t1 * t1) * d**5 / 120) / cos_p1
|
|
84
|
+
|
|
85
|
+
[rad2deg(l + deg2rad(central_meridian)), rad2deg(p)].map { |v| v.round(9) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 带号与中央经线: zone_meridian(121.47) => [21, 123], zone_meridian(121.47, width: 3) => [40, 120]
|
|
89
|
+
def zone_meridian(lng, width: 6)
|
|
90
|
+
raise ArgumentError, "带宽仅支持 3 或 6 度" unless [3, 6].include?(width)
|
|
91
|
+
|
|
92
|
+
zone = width == 6 ? (lng / 6).floor + 1 : (lng / 3).round
|
|
93
|
+
[zone, width == 6 ? zone * 6 - 3 : zone * 3]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def ellipsoid_params(name)
|
|
99
|
+
params = ELLIPSOIDS[name.to_s.downcase.to_sym]
|
|
100
|
+
raise ArgumentError, "未知椭球: #{name.inspect} (支持 #{ELLIPSOIDS.keys.join(', ')})" unless params
|
|
101
|
+
|
|
102
|
+
[params[0], 1.0 / params[1]]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# 子午线弧长
|
|
106
|
+
def meridional_arc(p, a, e2)
|
|
107
|
+
a * ((1 - e2 / 4 - 3 * e2**2 / 64 - 5 * e2**3 / 256) * p -
|
|
108
|
+
(3 * e2 / 8 + 3 * e2**2 / 32 + 45 * e2**3 / 1024) * Math.sin(2 * p) +
|
|
109
|
+
(15 * e2**2 / 256 + 45 * e2**3 / 1024) * Math.sin(4 * p) -
|
|
110
|
+
(35 * e2**3 / 3072) * Math.sin(6 * p))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 底点纬度迭代
|
|
114
|
+
def footpoint_latitude(mu, e1)
|
|
115
|
+
mu + (3 * e1 / 2 - 27 * e1**3 / 32) * Math.sin(2 * mu) +
|
|
116
|
+
(21 * e1**2 / 16 - 55 * e1**4 / 32) * Math.sin(4 * mu) +
|
|
117
|
+
(151 * e1**3 / 96) * Math.sin(6 * mu) +
|
|
118
|
+
(1097 * e1**4 / 512) * Math.sin(8 * mu)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def deg2rad(deg)
|
|
122
|
+
deg * Math::PI / 180.0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def rad2deg(rad)
|
|
126
|
+
rad * 180.0 / Math::PI
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -1,70 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
3
5
|
|
|
4
6
|
module Coordconver
|
|
5
7
|
module Utils
|
|
8
|
+
# 轻量 HTTP 客户端, 仅依赖标准库。
|
|
9
|
+
# 网络层失败(超时/连接错误/非 2xx)会自动重试, 重试耗尽后抛出 Coordconver::NetworkError。
|
|
6
10
|
class Http
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
DEFAULT_OPEN_TIMEOUT = 20
|
|
10
|
-
DEFAULT_READ_TIMEOUT = 120
|
|
11
|
+
DEFAULT_OPEN_TIMEOUT = 5
|
|
12
|
+
DEFAULT_READ_TIMEOUT = 10
|
|
11
13
|
DEFAULT_RETRY_TIMES = 3
|
|
12
|
-
|
|
14
|
+
RETRY_BASE_SLEEP = 0.5
|
|
13
15
|
|
|
14
16
|
HTTP_VERB_MAP = {
|
|
15
|
-
get:
|
|
16
|
-
post:
|
|
17
|
-
put:
|
|
17
|
+
get: Net::HTTP::Get,
|
|
18
|
+
post: Net::HTTP::Post,
|
|
19
|
+
put: Net::HTTP::Put,
|
|
18
20
|
delete: Net::HTTP::Delete
|
|
19
|
-
}
|
|
21
|
+
}.freeze
|
|
20
22
|
|
|
21
23
|
DEFAULT_HEADERS = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
"user-agent" => "coordconver/#{Coordconver::VERSION} (+https://github.com/cheenwe/coordconver)",
|
|
25
|
+
"accept" => "application/json"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
RETRYABLE_ERRORS = [
|
|
29
|
+
Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET,
|
|
30
|
+
Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError, EOFError
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
def initialize(method, url, params: nil, body: nil, headers: {})
|
|
34
|
+
method = method.to_s.downcase.to_sym
|
|
35
|
+
raise ArgumentError, "不支持的 HTTP method: #{method}" unless HTTP_VERB_MAP.key?(method)
|
|
36
|
+
|
|
32
37
|
@uri = URI(url)
|
|
33
|
-
@uri.query =
|
|
34
|
-
@
|
|
35
|
-
@
|
|
38
|
+
@uri.query = build_query(params) if params && !params.empty?
|
|
39
|
+
@method = method
|
|
40
|
+
@body = body
|
|
41
|
+
@headers = headers
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def send_request
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
attempts = 0
|
|
46
|
+
begin
|
|
47
|
+
attempts += 1
|
|
48
|
+
perform
|
|
49
|
+
rescue *RETRYABLE_ERRORS => e
|
|
50
|
+
raise NetworkError, "#{@uri.host} 请求失败: #{e.class}: #{e.message}" if attempts >= DEFAULT_RETRY_TIMES
|
|
51
|
+
|
|
52
|
+
sleep(RETRY_BASE_SLEEP * (2**(attempts - 1)))
|
|
53
|
+
retry
|
|
43
54
|
end
|
|
44
|
-
# if raise Timeout::Error retry it for 3 times
|
|
45
|
-
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
46
|
-
(tries -= 1).zero? ? (raise "Time out") : retry
|
|
47
55
|
end
|
|
48
56
|
|
|
49
57
|
private
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
def perform
|
|
60
|
+
opts = {
|
|
61
|
+
use_ssl: @uri.scheme == "https",
|
|
62
|
+
open_timeout: open_timeout,
|
|
63
|
+
read_timeout: read_timeout
|
|
64
|
+
}
|
|
65
|
+
Net::HTTP.start(@uri.host, @uri.port, **opts) do |http|
|
|
66
|
+
response = http.request(build_request)
|
|
67
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
68
|
+
raise NetworkError, "#{@uri.host} 返回 HTTP #{response.code}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
response
|
|
57
72
|
end
|
|
73
|
+
end
|
|
58
74
|
|
|
75
|
+
def build_request
|
|
76
|
+
request = HTTP_VERB_MAP[@method].new(@uri)
|
|
77
|
+
request.initialize_http_header(DEFAULT_HEADERS.merge(@headers))
|
|
78
|
+
request.body = @body.to_json if @body
|
|
79
|
+
request
|
|
80
|
+
end
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
open_timeout: DEFAULT_OPEN_TIMEOUT,
|
|
64
|
-
read_timeout: DEFAULT_READ_TIMEOUT
|
|
65
|
-
}
|
|
66
|
-
end
|
|
82
|
+
def build_query(params)
|
|
83
|
+
URI.encode_www_form(params)
|
|
84
|
+
end
|
|
67
85
|
|
|
86
|
+
def open_timeout
|
|
87
|
+
configured = Coordconver::Geo::Configuration.configured? && Coordconver::Geo::Configuration.settings[:open_timeout]
|
|
88
|
+
configured || DEFAULT_OPEN_TIMEOUT
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read_timeout
|
|
92
|
+
configured = Coordconver::Geo::Configuration.configured? && Coordconver::Geo::Configuration.settings[:read_timeout]
|
|
93
|
+
configured || DEFAULT_READ_TIMEOUT
|
|
94
|
+
end
|
|
68
95
|
end
|
|
69
96
|
end
|
|
70
97
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Coordconver
|
|
2
4
|
module Utils
|
|
3
5
|
module Request
|
|
@@ -7,11 +9,11 @@ module Coordconver
|
|
|
7
9
|
send_request(:get, url, params: params, headers: headers)
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
def post(url, body: , headers: {})
|
|
12
|
+
def post(url, body: nil, headers: {})
|
|
11
13
|
send_request(:post, url, body: body, headers: headers)
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
def put(url, body: , headers: {})
|
|
16
|
+
def put(url, body: nil, headers: {})
|
|
15
17
|
send_request(:put, url, body: body, headers: headers)
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -19,17 +21,18 @@ module Coordconver
|
|
|
19
21
|
send_request(:delete, url, params: params, headers: headers)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def send_request(method, url, params: nil, body: nil, headers: {}
|
|
23
|
-
raw_response =
|
|
24
|
+
def send_request(method, url, params: nil, body: nil, headers: {})
|
|
25
|
+
raw_response = Http.new(
|
|
24
26
|
method.to_sym,
|
|
25
27
|
url,
|
|
26
28
|
params: params,
|
|
27
29
|
body: body,
|
|
28
|
-
headers: headers
|
|
29
|
-
opts: opts
|
|
30
|
+
headers: headers
|
|
30
31
|
).send_request
|
|
31
32
|
|
|
32
33
|
parse_body(raw_response.body)
|
|
34
|
+
rescue JSON::ParserError => e
|
|
35
|
+
raise NetworkError, "响应不是合法 JSON: #{e.message}"
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def parse_body(body)
|
data/lib/coordconver/version.rb
CHANGED
data/lib/coordconver.rb
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "coordconver/version"
|
|
4
|
+
require "coordconver/errors"
|
|
2
5
|
require "coordconver/coordconver"
|
|
3
6
|
require "coordconver/degree"
|
|
7
|
+
require "coordconver/formats"
|
|
8
|
+
require "coordconver/projection"
|
|
4
9
|
require "coordconver/geo/configuration"
|
|
10
|
+
require "coordconver/geo/key_pool"
|
|
11
|
+
require "coordconver/geo/cache"
|
|
12
|
+
require "coordconver/geo/api"
|
|
5
13
|
require "coordconver/geo/client"
|
|
6
14
|
require "coordconver/geo/baidu"
|
|
7
15
|
require "coordconver/geo/gaode"
|
|
16
|
+
require "coordconver/geo/tencent"
|
|
8
17
|
require "coordconver/utils/http"
|
|
9
18
|
require "coordconver/utils/request"
|
|
10
19
|
|
|
11
20
|
module Coordconver
|
|
21
|
+
# 坐标系转换/距离/方位角等方法已直接定义在 Coordconver 单例上,
|
|
22
|
+
# 如 Coordconver.wgs_gcj(lng, lat) / Coordconver.distance(...) / Coordconver.bearing(...)
|
|
23
|
+
extend Degree
|
|
24
|
+
extend Formats
|
|
25
|
+
extend Projection
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
# 便捷常量别名, 兼容 Coordconver::Baidu / Coordconver::Gaode 等用法
|
|
28
|
+
Baidu = Geo::Baidu
|
|
29
|
+
Gaode = Geo::Gaode
|
|
30
|
+
Tencent = Geo::Tencent
|
|
31
|
+
Client = Geo::Client
|
|
32
|
+
Configuration = Geo::Configuration
|
|
19
33
|
end
|