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.
@@ -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
- # Coordconver::Gaode.regeo(116.480881,39.989410)
7
- # 116.480881,39.989410-->北京市朝阳区阜通东大街6号
8
- def regeo(lng, lat)
9
- url = regeo_url(lng, lat)
10
- # params = {
11
- # key: gaode_map_key,
12
- # location: lng + "," + lat
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
- Utils::Request.get(url, params: nil)
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
- # Coordconver::Gaode.geo("北京市朝阳区阜通东大街6号")
19
- # 116.480881,39.989410-->北京市朝阳区阜通东大街6号
20
- def geo(address)
21
- url = geo_url
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
- key: gaode_map_key,
25
- address: address
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
- Utils::Request.get(url, params: params)
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
- private
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
- def regeo_url(lng, lat)
34
- base_url + '/regeo' + "?key=#{gaode_map_key}&location=#{lng},#{lat}"
72
+ loc.split(",").map(&:to_f)
35
73
  end
36
74
 
37
- def geo_url
38
- base_url + '/geo'
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
- def base_url
42
- Configuration.settings[:gaode_base_url]
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 gaode_map_key
46
- Configuration.settings[:gaode_map_keys].sample
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
- require 'net/http'
2
- require 'json'
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
- DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36"
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
- RETRY_SLEEP_TIME = 3
14
+ RETRY_BASE_SLEEP = 0.5
13
15
 
14
16
  HTTP_VERB_MAP = {
15
- get: Net::HTTP::Get,
16
- post: Net::HTTP::Post,
17
- put: Net::HTTP::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
- 'user-agent' => DEFAULT_USER_AGENT,
23
- 'accept' => 'application/json',
24
- 'content-type' => 'application/json',
25
- 'connection' => 'close'
26
- }
27
-
28
- def initialize(method, url, params: nil, body: nil, headers: {}, opts: {})
29
- method = method.downcase.to_sym
30
- err_msg = "http method #{method} is not supported"
31
- raise err_msg unless HTTP_VERB_MAP.keys.include?(method)
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 = URI.encode_www_form(params) unless params.nil?
34
- @request = prepare_request(method, body, headers)
35
- @opts = opts
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
- tries ||= DEFAULT_RETRY_TIMES
40
- opts ||= default_opts.merge @opts
41
- Net::HTTP.start(@uri.host, @uri.port, opts) do |http|
42
- http.request(@request)
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
- def prepare_request(method, body, headers)
52
- headers = DEFAULT_HEADERS.merge(headers)
53
- request = HTTP_VERB_MAP[method].new @uri
54
- request.initialize_http_header(headers)
55
- request.body = body.to_json unless body.nil?
56
- request
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
- def default_opts
61
- {
62
- use_ssl: 'https' == @uri.scheme,
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: {}, opts: {})
23
- raw_response = Utils::Http.new(
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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Coordconver
2
- VERSION = "0.1.4"
4
+ VERSION = "1.1.0"
3
5
  end
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
- include Coordconver
14
- include Degree
15
- include Geo::Configuration
16
- include Geo
17
- include Utils
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