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
data/lib/coordconver/degree.rb
CHANGED
|
@@ -1,59 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# 经纬度(十进制度)与度分秒(DMS)互转。
|
|
4
|
+
#
|
|
5
|
+
# 经 Coordconver.extend(Degree) 挂载, 以下两种写法等价:
|
|
6
|
+
# Coordconver.degree(38.65777, 104.08296)
|
|
7
|
+
# Coordconver::Degree.degree(38.65777, 104.08296)
|
|
1
8
|
module Coordconver
|
|
2
9
|
module Degree
|
|
3
|
-
|
|
4
|
-
base.extend(ClassMethods)
|
|
5
|
-
end
|
|
6
|
-
|
|
7
|
-
module ClassMethods
|
|
8
|
-
|
|
9
|
-
# 取整数位
|
|
10
|
-
def num(n)
|
|
11
|
-
n.to_i
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# 取小数数位 Decimal digit
|
|
15
|
-
def dig(n)
|
|
16
|
-
n.to_f-num(n)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# 计算分
|
|
20
|
-
def min(n)
|
|
21
|
-
dig(n)*60
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# 计算秒
|
|
25
|
-
def sec(n)
|
|
26
|
-
(min(n)-num(min(n)))*60
|
|
27
|
-
end
|
|
10
|
+
extend self
|
|
28
11
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"#{num(n)}°#{num(min(n))}′#{num(sec(n))}″"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# 获取分
|
|
40
|
-
def get_min(n)
|
|
41
|
-
n.split('°').last.to_f
|
|
42
|
-
end
|
|
12
|
+
# 坐标转度分秒, 返回 [经度, 纬度] 的 DMS 字符串。
|
|
13
|
+
# 负值按西经/南纬处理, 取绝对值后再格式化。
|
|
14
|
+
# Coordconver.degree(38.65777, 104.08296) # => ["38°39′27.97″", "104°4′58.66″"]
|
|
15
|
+
def degree(lng, lat)
|
|
16
|
+
[cal_degree(lng), cal_degree(lat)]
|
|
17
|
+
end
|
|
43
18
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
19
|
+
# 单个十进制度数转度分秒字符串, 默认保留两位小数秒。
|
|
20
|
+
# Coordconver.cal_degree(38.65777) # => "38°39′27.97″"
|
|
21
|
+
# Coordconver.cal_degree(38.65777, 0) # => "38°39′27″"
|
|
22
|
+
def cal_degree(n, precision = 2)
|
|
23
|
+
sign = n.negative? ? "-" : ""
|
|
24
|
+
n = n.abs
|
|
25
|
+
d = n.to_i
|
|
26
|
+
m = (n - d) * 60
|
|
27
|
+
s = (m - m.to_i) * 60
|
|
28
|
+
if precision.zero?
|
|
29
|
+
"#{sign}#{d}°#{m.to_i}′#{s.round}″"
|
|
30
|
+
else
|
|
31
|
+
"#{sign}#{d}°#{m.to_i}′#{s.round(precision)}″"
|
|
47
32
|
end
|
|
33
|
+
end
|
|
48
34
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
# 度分秒转坐标, 返回 [经度, 纬度] 浮点值。
|
|
36
|
+
# 兼容 "38°39′27.97″"、"38°39′27.97""、"38d39m27.97s"、"38:39:27.97" 等写法。
|
|
37
|
+
# Coordconver.undegree("38°39′27.97″", "104°4′58.66″") # => [38.65777, 104.08296]
|
|
38
|
+
def undegree(lng, lat)
|
|
39
|
+
[cal_undegree(lng), cal_undegree(lat)]
|
|
40
|
+
end
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
# 单个度分秒字符串转十进制度数。
|
|
43
|
+
# Coordconver.cal_undegree("104°4′58.66″") # => 104.08296
|
|
44
|
+
# Coordconver.cal_undegree("-38°39′27.97″") # => -38.65777
|
|
45
|
+
def cal_undegree(str)
|
|
46
|
+
parts = str.to_s.scan(/[-+]?\d+(?:\.\d+)?/)
|
|
47
|
+
return 0.0 if parts.empty?
|
|
48
|
+
|
|
49
|
+
deg, min, sec = parts.map(&:to_f)
|
|
50
|
+
min ||= 0.0
|
|
51
|
+
sec ||= 0.0
|
|
52
|
+
value = deg.abs + min / 60.0 + sec / 3600.0
|
|
53
|
+
value = -value if str.to_s =~ /^\s*[-–]/ || deg.negative?
|
|
54
|
+
value.round(6)
|
|
57
55
|
end
|
|
58
56
|
end
|
|
59
57
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coordconver
|
|
4
|
+
# 统一错误基类, 便于业务侧 rescue Coordconver::Error
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# 缺少 API key 等配置问题时抛出
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# 网络超时、连接失败、响应非 JSON 时抛出
|
|
11
|
+
class NetworkError < Error; end
|
|
12
|
+
|
|
13
|
+
# 服务端返回业务错误(如 key 无效、配额超限)时抛出
|
|
14
|
+
class ApiError < Error
|
|
15
|
+
attr_reader :code, :raw
|
|
16
|
+
|
|
17
|
+
def initialize(message = nil, code: nil, raw: nil)
|
|
18
|
+
super(message)
|
|
19
|
+
@code = code
|
|
20
|
+
@raw = raw
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# WKT / GeoJSON 与坐标互转。
|
|
6
|
+
# 经 Coordconver.extend(Formats) 挂载:
|
|
7
|
+
# Coordconver.parse_wkt("POINT (121.47 31.25)") # => [121.47, 31.25]
|
|
8
|
+
# Coordconver.to_wkt(121.47, 31.25) # => "POINT (121.47 31.25)"
|
|
9
|
+
# Coordconver.to_geojson(121.47, 31.25) # => {"type"=>"Point", "coordinates"=>[121.47, 31.25]}
|
|
10
|
+
module Coordconver
|
|
11
|
+
module Formats
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# 解析 WKT 点: "POINT (121.47 31.25)" / "POINT(121.47 31.25)" -> [lng, lat]
|
|
15
|
+
def parse_wkt(wkt)
|
|
16
|
+
match = wkt.to_s.match(/point\s*z?m?\s*\(\s*([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)/i)
|
|
17
|
+
raise ArgumentError, "无法解析 WKT: #{wkt.inspect} (仅支持 POINT)" unless match
|
|
18
|
+
|
|
19
|
+
[match[1].to_f, match[2].to_f]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 生成 WKT 点
|
|
23
|
+
def to_wkt(lng, lat)
|
|
24
|
+
"POINT (#{lng} #{lat})"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 解析 GeoJSON: 接受 Hash 或 JSON 字符串。
|
|
28
|
+
# 支持 Point 与包着 Point 的 Feature。
|
|
29
|
+
# 返回 [lng, lat]
|
|
30
|
+
def parse_geojson(input)
|
|
31
|
+
obj = input.is_a?(Hash) ? input : JSON.parse(input.to_s)
|
|
32
|
+
obj = obj["geometry"] if obj["type"] == "Feature"
|
|
33
|
+
raise ArgumentError, "仅支持 GeoJSON Point, 实际: #{obj['type'].inspect}" unless obj["type"] == "Point"
|
|
34
|
+
|
|
35
|
+
coords = obj["coordinates"]
|
|
36
|
+
raise ArgumentError, "GeoJSON Point 缺少 coordinates" unless coords.is_a?(Array) && coords.size >= 2
|
|
37
|
+
|
|
38
|
+
[coords[0].to_f, coords[1].to_f]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 生成 GeoJSON Point Hash (GeoJSON 规范中坐标为 [lng, lat] 顺序)
|
|
42
|
+
def to_geojson(lng, lat)
|
|
43
|
+
{ "type" => "Point", "coordinates" => [lng, lat] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 生成 GeoJSON Point JSON 字符串
|
|
47
|
+
def to_geojson_string(lng, lat)
|
|
48
|
+
JSON.generate(to_geojson(lng, lat))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coordconver
|
|
4
|
+
module Geo
|
|
5
|
+
# 百度/高德/腾讯 API 的统一调度层:
|
|
6
|
+
# 组装 key -> LRU 缓存 -> 请求 -> 识别 key 类失败 -> 轮换下一个 key 重试。
|
|
7
|
+
module Api
|
|
8
|
+
# provider: :baidu / :gaode / :tencent
|
|
9
|
+
# params: 业务参数(不含 key)
|
|
10
|
+
# key_param: key 参数名 (:ak / :key)
|
|
11
|
+
# sign: 可选 Proc, 对含 key 的完整参数做签名(百度 SN 校验)
|
|
12
|
+
# bad_result: Proc, 判断响应是否属于"key 无效/配额超限"类失败(应轮换 key)
|
|
13
|
+
def self.call(provider:, url:, params:, key_param:, sign: nil, bad_result:)
|
|
14
|
+
keys = Configuration.keys_for(provider)
|
|
15
|
+
attempts = 0
|
|
16
|
+
loop do
|
|
17
|
+
key = KeyPool.next_key(provider, keys)
|
|
18
|
+
full_params = params.merge(key_param => key)
|
|
19
|
+
full_params = sign.call(full_params) if sign
|
|
20
|
+
cache_key = [provider, url, full_params]
|
|
21
|
+
|
|
22
|
+
hit = Cache.get(cache_key)
|
|
23
|
+
if hit
|
|
24
|
+
return hit unless bad_result.call(hit)
|
|
25
|
+
|
|
26
|
+
Cache.delete(cache_key) # 防御: 不应缓存过失败响应
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
response = Utils::Request.get(url, params: full_params)
|
|
31
|
+
return Cache.set(cache_key, response) unless bad_result.call(response)
|
|
32
|
+
|
|
33
|
+
KeyPool.mark_bad(provider, key)
|
|
34
|
+
attempts += 1
|
|
35
|
+
if attempts >= keys.size
|
|
36
|
+
raise ApiError.new(
|
|
37
|
+
"#{provider} 请求失败: 所有 #{keys.size} 个 key 均无效或配额超限",
|
|
38
|
+
code: provider, raw: response
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -1,45 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
1
5
|
module Coordconver
|
|
2
6
|
module Geo
|
|
7
|
+
# 百度地图 Web 服务 API (Geocoding API v3)
|
|
8
|
+
# 申请 key: https://lbsyun.baidu.com/apiconsole/key
|
|
9
|
+
# 建议创建"服务端"类型 AK 并配置 IP 白名单; 若 AK 类型为"SN 校验", 需同时配置 baidu_sn
|
|
3
10
|
module Baidu
|
|
4
11
|
extend self
|
|
5
12
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Utils::Request.get(url, params: nil)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def geo(address)
|
|
14
|
-
url = base_url
|
|
13
|
+
# 视为"key 无效/配额超限"的状态码, 触发自动轮换下一个 key
|
|
14
|
+
# 200 AK不存在 201 AK被禁用 210 AK参数错误 401 配额超限 402 每秒请求量超限
|
|
15
|
+
KEY_ERROR_STATUSES = [200, 201, 210, 401, 402].freeze
|
|
15
16
|
|
|
17
|
+
# 地理编码: 结构化地址 -> 坐标
|
|
18
|
+
# Coordconver::Baidu.geo("百度大厦")["result"]["location"] # => {"lng"=>116.30..., "lat"=>40.05...}
|
|
19
|
+
# 支持可选参数: city(城市名), ret_coordtype(返回坐标类型, 默认 bd09ll)
|
|
20
|
+
def geo(address, options = {})
|
|
16
21
|
params = {
|
|
17
|
-
|
|
18
|
-
output:
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
address: address,
|
|
23
|
+
output: "json"
|
|
24
|
+
}.merge(options)
|
|
25
|
+
request("/geocoding/v3/", params)
|
|
26
|
+
end
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
# 逆地理编码: 坐标 -> 结构化地址
|
|
29
|
+
# Coordconver::Baidu.regeo(116.322987, 39.983424)["result"]["formatted_address"]
|
|
30
|
+
# options: coordtype(输入坐标类型, 默认 bd09ll, 可选 wgs84ll / gcj02ll 等)
|
|
31
|
+
def regeo(lng, lat, options = {})
|
|
32
|
+
params = {
|
|
33
|
+
location: "#{lat},#{lng}",
|
|
34
|
+
output: "json",
|
|
35
|
+
coordtype: "bd09ll"
|
|
36
|
+
}.merge(options)
|
|
37
|
+
request("/reverse_geocoding/v3/", params)
|
|
23
38
|
end
|
|
24
39
|
|
|
25
|
-
|
|
40
|
+
# IP 定位: IP -> 城市 + 坐标 (BD-09)
|
|
41
|
+
def ip(ip = nil)
|
|
42
|
+
params = { coor: "bd09ll", output: "json" }
|
|
43
|
+
params[:ip] = ip unless ip.nil? || ip.empty?
|
|
44
|
+
request("/location/ip", params)
|
|
45
|
+
end
|
|
26
46
|
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
# 便捷方法: 地址 -> [lng, lat] (BD-09), 解析失败返回 nil
|
|
48
|
+
def geo_location(address, options = {})
|
|
49
|
+
loc = geo(address, options).dig("result", "location")
|
|
50
|
+
loc && [loc["lng"], loc["lat"]]
|
|
29
51
|
end
|
|
30
52
|
|
|
31
|
-
#
|
|
53
|
+
# 便捷方法: 坐标 -> 地址字符串 (BD-09), 解析失败返回 nil
|
|
54
|
+
def regeo_address(lng, lat, options = {})
|
|
55
|
+
regeo(lng, lat, options).dig("result", "formatted_address")
|
|
56
|
+
end
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
private
|
|
34
59
|
|
|
35
|
-
|
|
60
|
+
def request(path, params)
|
|
61
|
+
sn_key = Configuration.settings[:baidu_sn]
|
|
62
|
+
Api.call(
|
|
63
|
+
provider: :baidu,
|
|
64
|
+
url: base_url + path,
|
|
65
|
+
params: params,
|
|
66
|
+
key_param: :ak,
|
|
67
|
+
sign: sn_key ? ->(p) { p.merge(sn: sn(path, p, sn_key)) } : nil,
|
|
68
|
+
bad_result: ->(r) { KEY_ERROR_STATUSES.include?(r["status"].to_i) }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
36
71
|
|
|
37
72
|
def base_url
|
|
38
|
-
Configuration.settings[:baidu_base_url]
|
|
73
|
+
Configuration.settings[:baidu_base_url].sub(%r{/\z}, "")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# SN 校验: sn = md5(urlencode(path + "?" + 按参数名字典序拼接的 query + sk))
|
|
77
|
+
def sn(path, params, sk)
|
|
78
|
+
query = params.sort.map { |k, v| "#{k}=#{v}" }.join("&")
|
|
79
|
+
Digest::MD5.hexdigest(custom_uri_encode("#{path}?#{query}") + sk)
|
|
39
80
|
end
|
|
40
81
|
|
|
41
|
-
|
|
42
|
-
|
|
82
|
+
# 百度 SN 校验要求的 URI 编码: 仅 A-Za-z0-9-_.~ 不编码, 其余按字节转 %XX 大写
|
|
83
|
+
def custom_uri_encode(str)
|
|
84
|
+
str.gsub(/[^A-Za-z0-9\-_.~]/) do |char|
|
|
85
|
+
char.bytes.map { |b| format("%%%02X", b) }.join
|
|
86
|
+
end
|
|
43
87
|
end
|
|
44
88
|
end
|
|
45
89
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coordconver
|
|
4
|
+
module Geo
|
|
5
|
+
# 进程内 LRU + TTL 缓存, 用于地理编码结果去重以节省配额。
|
|
6
|
+
# 默认关闭, 通过 config.cache = true 开启; 可配置 cache_ttl / cache_size。
|
|
7
|
+
module Cache
|
|
8
|
+
DEFAULT_TTL = 3600
|
|
9
|
+
DEFAULT_SIZE = 1024
|
|
10
|
+
|
|
11
|
+
@store = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def enabled?
|
|
16
|
+
Configuration.configured? && Configuration.settings[:cache] ? true : false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 命中且未过期返回值, 否则返回 nil
|
|
20
|
+
def get(key)
|
|
21
|
+
return nil unless enabled?
|
|
22
|
+
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
entry = @store[key]
|
|
25
|
+
return nil unless entry
|
|
26
|
+
return nil if Time.now - entry[:at] > ttl
|
|
27
|
+
|
|
28
|
+
@store.delete(key)
|
|
29
|
+
@store[key] = entry # LRU: 重新插入到队尾
|
|
30
|
+
entry[:value]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 存入并返回 value; 未启用时原样返回 value
|
|
35
|
+
def set(key, value)
|
|
36
|
+
return value unless enabled?
|
|
37
|
+
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
@store.delete(key)
|
|
40
|
+
@store[key] = { at: Time.now, value: value }
|
|
41
|
+
@store.shift while @store.size > size
|
|
42
|
+
value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def delete(key)
|
|
47
|
+
@mutex.synchronize { @store.delete(key) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear!
|
|
51
|
+
@mutex.synchronize { @store.clear }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def ttl
|
|
57
|
+
Configuration.settings[:cache_ttl] || DEFAULT_TTL
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def size
|
|
61
|
+
Configuration.settings[:cache_size] || DEFAULT_SIZE
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Coordconver
|
|
2
4
|
module Geo
|
|
3
|
-
|
|
5
|
+
# 初始化配置。保留旧写法, 也可用 Coordconver.configure 块:
|
|
6
|
+
# Coordconver::Client.new(["百度ak1", "百度ak2"], ["高德key1"])
|
|
4
7
|
class Client
|
|
5
|
-
def initialize(baidu_map_aks,
|
|
6
|
-
Configuration.init(baidu_map_aks,
|
|
8
|
+
def initialize(baidu_map_aks = [], gaode_map_keys = [], options = {})
|
|
9
|
+
Configuration.init(baidu_map_aks, gaode_map_keys, options)
|
|
7
10
|
end
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
|
-
end
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# 全局配置块:
|
|
17
|
+
# Coordconver.configure do |config|
|
|
18
|
+
# config.baidu_map_aks = ["ak1"]
|
|
19
|
+
# config.gaode_map_keys = ["key1"]
|
|
20
|
+
# end
|
|
21
|
+
def configure
|
|
22
|
+
Geo::Configuration.init unless Geo::Configuration.configured?
|
|
23
|
+
yield Geo::Configuration
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -1,22 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Coordconver
|
|
2
4
|
module Geo
|
|
5
|
+
# 全局配置。支持两种写法:
|
|
6
|
+
#
|
|
7
|
+
# # 推荐: 全局配置块, key 支持数组以实现多 key 轮询
|
|
8
|
+
# Coordconver.configure do |config|
|
|
9
|
+
# config.baidu_map_aks = ["你的百度ak1", "你的百度ak2"]
|
|
10
|
+
# config.gaode_map_keys = ["你的高德key1", "你的高德key2"]
|
|
11
|
+
# config.tencent_map_keys = ["你的腾讯key1"]
|
|
12
|
+
# # 可选: 百度 SN 校验(仅当 AK 类型为"SN 校验"时需要), 服务端 AK 用 IP 白名单则无需配置
|
|
13
|
+
# config.baidu_sn = "你的百度SK"
|
|
14
|
+
# # 可选: 进程内缓存地理编码结果, 节省配额 (默认 false)
|
|
15
|
+
# config.cache = true
|
|
16
|
+
# config.cache_ttl = 3600 # 缓存秒数, 默认 3600
|
|
17
|
+
# config.cache_size = 1024 # 缓存条目上限, 默认 1024 (LRU 淘汰)
|
|
18
|
+
# # 可选: 超时(秒)
|
|
19
|
+
# config.open_timeout = 5
|
|
20
|
+
# config.read_timeout = 10
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # 兼容旧写法
|
|
24
|
+
# Coordconver::Client.new(["百度ak"], ["高德key"], tencent_map_keys: ["腾讯key"])
|
|
3
25
|
module Configuration
|
|
4
26
|
extend self
|
|
5
27
|
|
|
6
28
|
DEFAULT_MAP_OPTIONS = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
29
|
+
baidu_base_url: "https://api.map.baidu.com",
|
|
30
|
+
gaode_base_url: "https://restapi.amap.com/v3",
|
|
31
|
+
tencent_base_url: "https://apis.map.qq.com/ws",
|
|
32
|
+
open_timeout: 5,
|
|
33
|
+
read_timeout: 10,
|
|
34
|
+
cache: false,
|
|
35
|
+
cache_ttl: 3600,
|
|
36
|
+
cache_size: 1024
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
PROVIDER_KEY_FIELD = {
|
|
40
|
+
baidu: :baidu_map_aks,
|
|
41
|
+
gaode: :gaode_map_keys,
|
|
42
|
+
tencent: :tencent_map_keys
|
|
43
|
+
}.freeze
|
|
11
44
|
|
|
12
45
|
attr_reader :settings
|
|
13
46
|
|
|
14
|
-
def
|
|
15
|
-
|
|
47
|
+
def configured?
|
|
48
|
+
!@settings.nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def init(baidu_map_aks = nil, gaode_map_keys = nil, options = {})
|
|
52
|
+
tencent_keys = Array(options.delete(:tencent_map_keys))
|
|
53
|
+
@settings = DEFAULT_MAP_OPTIONS.merge(
|
|
54
|
+
baidu_map_aks: Array(baidu_map_aks).compact.reject(&:empty?),
|
|
55
|
+
gaode_map_keys: Array(gaode_map_keys).compact.reject(&:empty?),
|
|
56
|
+
tencent_map_keys: tencent_keys.compact.reject(&:empty?)
|
|
57
|
+
).merge(options)
|
|
58
|
+
self
|
|
16
59
|
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
60
|
|
|
61
|
+
def reset!
|
|
62
|
+
@settings = nil
|
|
63
|
+
KeyPool.reset!
|
|
64
|
+
Cache.clear!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def [](key)
|
|
68
|
+
settings[key]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# 按 provider (:baidu/:gaode/:tencent) 取可用 key 数组, 未配置时抛出清晰错误
|
|
72
|
+
def keys_for(provider)
|
|
73
|
+
field = PROVIDER_KEY_FIELD[provider]
|
|
74
|
+
raise ArgumentError, "未知的服务商: #{provider.inspect}" unless field
|
|
21
75
|
|
|
76
|
+
keys = settings[field]
|
|
77
|
+
raise ConfigurationError, "未配置 #{provider} 的 key (#{field}), 请通过 Coordconver.configure 或 Coordconver::Client.new 设置" if keys.empty?
|
|
22
78
|
|
|
79
|
+
keys
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def method_missing(name, *args)
|
|
83
|
+
if name.to_s.end_with?("=") && settings.key?(name.to_s.chomp("=").to_sym)
|
|
84
|
+
settings[name.to_s.chomp("=").to_sym] = args.first
|
|
85
|
+
else
|
|
86
|
+
super
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def respond_to_missing?(name, include_private = false)
|
|
91
|
+
(name.to_s.end_with?("=") && settings.key?(name.to_s.chomp("=").to_sym)) || super
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def settings
|
|
95
|
+
@settings or raise ConfigurationError, "尚未配置地图 API key。请先执行 Coordconver.configure { |c| c.baidu_map_aks = [...]; c.gaode_map_keys = [...] } 或 Coordconver::Client.new([百度ak数组], [高德key数组])"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 兼容旧接口: 取一个 key (现为实现为轮询)
|
|
99
|
+
def sample_baidu_ak
|
|
100
|
+
KeyPool.next_key(:baidu, keys_for(:baidu))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def sample_gaode_key
|
|
104
|
+
KeyPool.next_key(:gaode, keys_for(:gaode))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def sample_tencent_key
|
|
108
|
+
KeyPool.next_key(:tencent, keys_for(:tencent))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|