coordconver 0.1.3 → 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 +270 -67
- 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 +286 -104
- data/lib/coordconver/degree.rb +55 -58
- 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 +90 -0
- data/lib/coordconver/geo/cache.rb +66 -0
- data/lib/coordconver/geo/client.rb +25 -0
- data/lib/coordconver/geo/configuration.rb +112 -0
- data/lib/coordconver/geo/gaode.rb +105 -0
- 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 +97 -0
- data/lib/coordconver/utils/request.rb +43 -0
- data/lib/coordconver/version.rb +3 -1
- data/lib/coordconver.rb +27 -2
- metadata +43 -32
- data/.travis.yml +0 -8
- data/bin/coordconver +0 -4
data/exe/coordconver
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 坐标转换命令行工具
|
|
5
|
+
#
|
|
6
|
+
# coordconver wgs2gcj 121.468738 31.247138 # 单点坐标系转换
|
|
7
|
+
# coordconver dms 38.65777 104.08296 # 十进制度 -> 度分秒
|
|
8
|
+
# coordconver bearing 116.397 39.909 121.474 31.230 # 两点方位角(度)
|
|
9
|
+
# coordconver dest 116.397 39.909 153 1000 # 沿方位角偏移后的坐标
|
|
10
|
+
# coordconver gk 121.47 31.24 --zone3 # 高斯-克吕格正算
|
|
11
|
+
# cat points.txt | coordconver wgs2gcj # 批量: 每行 "lng,lat" 或 "lng lat"
|
|
12
|
+
|
|
13
|
+
require "optparse"
|
|
14
|
+
require "csv"
|
|
15
|
+
require "json"
|
|
16
|
+
|
|
17
|
+
lib = File.expand_path("../lib", __dir__)
|
|
18
|
+
$LOAD_PATH.unshift(lib) if File.exist?(File.join(lib, "coordconver.rb"))
|
|
19
|
+
require "coordconver"
|
|
20
|
+
|
|
21
|
+
CONVERSION_ALIASES = {
|
|
22
|
+
"wgs" => "wgs", "wgs84" => "wgs",
|
|
23
|
+
"gcj" => "gcj", "gcj02" => "gcj",
|
|
24
|
+
"bd" => "bd", "bd09" => "bd"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
METHOD_MAP = {
|
|
28
|
+
%w[wgs gcj] => "wgs_gcj", %w[gcj wgs] => "gcj_wgs",
|
|
29
|
+
%w[wgs bd] => "wgs_bd", %w[bd wgs] => "bd_wgs",
|
|
30
|
+
%w[gcj bd] => "gcj_bd", %w[bd gcj] => "bd_gcj"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def usage_and_exit(message = nil)
|
|
34
|
+
warn message if message
|
|
35
|
+
warn "用法: coordconver <命令> [参数] (--help 查看完整帮助)"
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_conversion(name)
|
|
40
|
+
match = name.to_s.match(%r{\A(wgs84|wgs|gcj02|gcj|bd09|bd)\s*(?:2|to|->|=>|_)\s*(wgs84|wgs|gcj02|gcj|bd09|bd)\z}i)
|
|
41
|
+
usage_and_exit("无法识别的转换名称: #{name}") unless match
|
|
42
|
+
|
|
43
|
+
from = CONVERSION_ALIASES[match[1].downcase]
|
|
44
|
+
to = CONVERSION_ALIASES[match[2].downcase]
|
|
45
|
+
usage_and_exit("源坐标系与目标坐标系相同") if from == to
|
|
46
|
+
|
|
47
|
+
METHOD_MAP[[from, to]]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_coords(args)
|
|
51
|
+
coords = args.flat_map { |a| a.to_s.split(/[,\s]+/).reject(&:empty?).map(&:to_f) }
|
|
52
|
+
usage_and_exit("坐标参数不足, 需要 <lng> <lat>") if coords.size < 2
|
|
53
|
+
coords
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def line_coords(line, csv_mode)
|
|
57
|
+
if csv_mode
|
|
58
|
+
row = CSV.parse_line(line)
|
|
59
|
+
return [] unless row
|
|
60
|
+
|
|
61
|
+
nums = row.select { |c| c && c.match?(/\A[+-]?\d+(\.\d+)?\z/) }
|
|
62
|
+
nums.map(&:to_f)
|
|
63
|
+
else
|
|
64
|
+
line.split(/[,\s]+/).reject(&:empty?).map(&:to_f)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
options = { round: 6, csv: false }
|
|
69
|
+
parser = OptionParser.new do |opts|
|
|
70
|
+
opts.banner = "coordconver #{Coordconver::VERSION} — 坐标转换命令行工具"
|
|
71
|
+
|
|
72
|
+
opts.separator ""
|
|
73
|
+
opts.separator "坐标系转换 (WGS-84 地球 / GCJ-02 火星 / BD-09 百度):"
|
|
74
|
+
opts.separator " coordconver <转换名> <lng> <lat> 转换名如 wgs2gcj, gcj2bd, wgs84togcj02"
|
|
75
|
+
|
|
76
|
+
opts.separator ""
|
|
77
|
+
opts.separator "其他命令:"
|
|
78
|
+
opts.separator " dms <lng> <lat> 十进制度转度分秒"
|
|
79
|
+
opts.separator " dec <度分秒> <度分秒> 度分秒转十进制度"
|
|
80
|
+
opts.separator " dist <lng1> <lat1> <lng2> <lat2> 两点球面距离(米)"
|
|
81
|
+
opts.separator " bearing <lng1> <lat1> <lng2> <lat2> 两点初始方位角(度, 正北为0)"
|
|
82
|
+
opts.separator " dest <lng> <lat> <方位角> <距离米> 沿方位角偏移后的坐标"
|
|
83
|
+
opts.separator " gk <lng> <lat> 高斯-克吕格正算 -> [东坐标, 北坐标]"
|
|
84
|
+
opts.separator " invgk <x> <y> --cm 中央经线 高斯-克吕格反算"
|
|
85
|
+
opts.separator " geo <地址> --baidu|--gaode|--tencent 地址转坐标(需 API key)"
|
|
86
|
+
opts.separator " regeo <lng> <lat> --baidu|--gaode|--tencent 坐标转地址(需 API key)"
|
|
87
|
+
opts.separator ""
|
|
88
|
+
opts.separator "批量模式: 未给坐标参数时从 stdin 读取, 每行一条 \"lng,lat\" 或 \"lng lat\""
|
|
89
|
+
|
|
90
|
+
opts.separator ""
|
|
91
|
+
opts.separator "选项:"
|
|
92
|
+
opts.on("--baidu", "geo/regeo 使用百度地图 API") { options[:provider] = "baidu" }
|
|
93
|
+
opts.on("--gaode", "geo/regeo 使用高德地图 API (默认)") { options[:provider] = "gaode" }
|
|
94
|
+
opts.on("--tencent", "geo/regeo 使用腾讯地图 API") { options[:provider] = "tencent" }
|
|
95
|
+
opts.on("--cache", "geo/regeo 启用进程内结果缓存") { options[:cache] = true }
|
|
96
|
+
opts.on("--csv", "批量输入按 CSV 行解析(自动跳过表头)") { options[:csv] = true }
|
|
97
|
+
opts.on("-j", "--json", "以 JSON 数组输出") { options[:json] = true }
|
|
98
|
+
opts.on("--geojson", "转换结果以 GeoJSON 输出") { options[:geojson] = true }
|
|
99
|
+
opts.on("-r", "--round N", Integer, "小数位数, 默认 6") { |v| options[:round] = v }
|
|
100
|
+
opts.separator ""
|
|
101
|
+
opts.separator "高斯-克吕格选项:"
|
|
102
|
+
opts.on("--cm 度", Float, "中央经线(gk 可选, invgk 必填)") { |v| options[:cm] = v }
|
|
103
|
+
opts.on("--zone3", "按 3° 带推算中央经线(默认 6° 带)") { options[:zone_width] = 3 }
|
|
104
|
+
opts.on("--zone6", "按 6° 带推算中央经线") { options[:zone_width] = 6 }
|
|
105
|
+
opts.on("--ellipsoid 名称", "cgcs2000(默认)/wgs84/krassovsky/iag75") { |v| options[:ellipsoid] = v }
|
|
106
|
+
opts.on("-h", "--help", "显示帮助") { puts opts; exit 0 }
|
|
107
|
+
opts.on("-v", "--version", "显示版本") { puts "coordconver #{Coordconver::VERSION}"; exit 0 }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
parser.parse!(ARGV)
|
|
111
|
+
|
|
112
|
+
def configure_from_env(provider)
|
|
113
|
+
Coordconver.configure do |c|
|
|
114
|
+
if provider == "baidu" && ENV["COORDCONVER_BAIDU_AK"]
|
|
115
|
+
c.baidu_map_aks = ENV["COORDCONVER_BAIDU_AK"].split(",").map(&:strip)
|
|
116
|
+
elsif provider == "gaode" && ENV["COORDCONVER_GAODE_KEY"]
|
|
117
|
+
c.gaode_map_keys = ENV["COORDCONVER_GAODE_KEY"].split(",").map(&:strip)
|
|
118
|
+
elsif provider == "tencent" && ENV["COORDCONVER_TENCENT_KEY"]
|
|
119
|
+
c.tencent_map_keys = ENV["COORDCONVER_TENCENT_KEY"].split(",").map(&:strip)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def emit(result, options)
|
|
125
|
+
rounded = result.map { |v| v.is_a?(Numeric) ? v.round(options[:round]) : v }
|
|
126
|
+
options[:json] ? puts(JSON.generate(rounded)) : puts(rounded.join(","))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def emit_features(coords_list, options)
|
|
130
|
+
features = coords_list.map do |lng, lat|
|
|
131
|
+
{ "type" => "Feature",
|
|
132
|
+
"geometry" => Coordconver.to_geojson(lng, lat),
|
|
133
|
+
"properties" => {} }
|
|
134
|
+
end
|
|
135
|
+
if features.size == 1
|
|
136
|
+
puts JSON.generate(features[0])
|
|
137
|
+
else
|
|
138
|
+
puts JSON.generate({ "type" => "FeatureCollection", "features" => features })
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
command = ARGV.shift
|
|
143
|
+
usage_and_exit if command.nil?
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
case command.downcase
|
|
147
|
+
when "dms"
|
|
148
|
+
usage_and_exit("dms 需要 <lng> <lat> 两个参数") if ARGV.size < 2
|
|
149
|
+
emit(Coordconver.degree(Float(ARGV[0]), Float(ARGV[1])), options)
|
|
150
|
+
when "dec"
|
|
151
|
+
usage_and_exit("dec 需要 <度分秒> <度分秒> 两个参数") if ARGV.size < 2
|
|
152
|
+
emit(Coordconver.undegree(ARGV[0], ARGV[1]), options)
|
|
153
|
+
when "dist"
|
|
154
|
+
usage_and_exit("dist 需要 <lng1> <lat1> <lng2> <lat2> 四个参数") if ARGV.size < 4
|
|
155
|
+
puts Coordconver.distance(Float(ARGV[0]), Float(ARGV[1]), Float(ARGV[2]), Float(ARGV[3]))
|
|
156
|
+
when "bearing"
|
|
157
|
+
usage_and_exit("bearing 需要 <lng1> <lat1> <lng2> <lat2> 四个参数") if ARGV.size < 4
|
|
158
|
+
puts Coordconver.bearing(Float(ARGV[0]), Float(ARGV[1]), Float(ARGV[2]), Float(ARGV[3])).round(2)
|
|
159
|
+
when "dest"
|
|
160
|
+
usage_and_exit("dest 需要 <lng> <lat> <方位角> <距离米> 四个参数") if ARGV.size < 4
|
|
161
|
+
result = Coordconver.destination_point(Float(ARGV[0]), Float(ARGV[1]), Float(ARGV[2]), Float(ARGV[3]))
|
|
162
|
+
emit(result, options)
|
|
163
|
+
when "gk", "invgk"
|
|
164
|
+
if command.downcase == "gk"
|
|
165
|
+
usage_and_exit("gk 需要 <lng> <lat> 两个参数") if ARGV.size < 2
|
|
166
|
+
result = Coordconver.gauss_kruger(
|
|
167
|
+
Float(ARGV[0]), Float(ARGV[1]),
|
|
168
|
+
central_meridian: options[:cm],
|
|
169
|
+
zone_width: options[:zone_width] || 6,
|
|
170
|
+
ellipsoid: options[:ellipsoid] || :cgcs2000
|
|
171
|
+
)
|
|
172
|
+
emit(result, { round: 2 })
|
|
173
|
+
else
|
|
174
|
+
usage_and_exit("invgk 需要 <x> <y> 并指定 --cm 中央经线") if ARGV.size < 2 || options[:cm].nil?
|
|
175
|
+
result = Coordconver.inverse_gauss_kruger(
|
|
176
|
+
Float(ARGV[0]), Float(ARGV[1]),
|
|
177
|
+
central_meridian: options[:cm],
|
|
178
|
+
ellipsoid: options[:ellipsoid] || :cgcs2000
|
|
179
|
+
)
|
|
180
|
+
emit(result, options)
|
|
181
|
+
end
|
|
182
|
+
when "geo"
|
|
183
|
+
provider = options[:provider] || "gaode"
|
|
184
|
+
usage_and_exit("geo 需要 <地址> 参数") if ARGV.empty?
|
|
185
|
+
configure_from_env(provider)
|
|
186
|
+
Coordconver.configure { |c| c.cache = true } if options[:cache]
|
|
187
|
+
location = {
|
|
188
|
+
"baidu" => -> { Coordconver::Baidu.geo_location(ARGV[0]) },
|
|
189
|
+
"gaode" => -> { Coordconver::Gaode.geo_location(ARGV[0]) },
|
|
190
|
+
"tencent" => -> { Coordconver::Tencent.geo_location(ARGV[0]) }
|
|
191
|
+
}[provider].call
|
|
192
|
+
if location
|
|
193
|
+
emit(location, options)
|
|
194
|
+
else
|
|
195
|
+
warn "解析失败: #{ARGV[0].inspect}"
|
|
196
|
+
exit 2
|
|
197
|
+
end
|
|
198
|
+
when "regeo"
|
|
199
|
+
provider = options[:provider] || "gaode"
|
|
200
|
+
usage_and_exit("regeo 需要 <lng> <lat> 参数") if ARGV.size < 2
|
|
201
|
+
configure_from_env(provider)
|
|
202
|
+
Coordconver.configure { |c| c.cache = true } if options[:cache]
|
|
203
|
+
address = {
|
|
204
|
+
"baidu" => -> { Coordconver::Baidu.regeo_address(Float(ARGV[0]), Float(ARGV[1])) },
|
|
205
|
+
"gaode" => -> { Coordconver::Gaode.regeo_address(Float(ARGV[0]), Float(ARGV[1])) },
|
|
206
|
+
"tencent" => -> { Coordconver::Tencent.regeo_address(Float(ARGV[0]), Float(ARGV[1])) }
|
|
207
|
+
}[provider].call
|
|
208
|
+
if address
|
|
209
|
+
puts address
|
|
210
|
+
else
|
|
211
|
+
warn "解析失败: #{ARGV[0]},#{ARGV[1]}"
|
|
212
|
+
exit 2
|
|
213
|
+
end
|
|
214
|
+
else
|
|
215
|
+
method = parse_conversion(command)
|
|
216
|
+
if ARGV.empty?
|
|
217
|
+
usage_and_exit("需要 <lng> <lat> 参数, 或通过 stdin 管道输入批量坐标") if $stdin.tty?
|
|
218
|
+
|
|
219
|
+
converted = []
|
|
220
|
+
$stdin.each_line do |line|
|
|
221
|
+
coords = line_coords(line, options[:csv])
|
|
222
|
+
next if coords.size < 2
|
|
223
|
+
|
|
224
|
+
pair = Coordconver.public_send(method, coords[0], coords[1])
|
|
225
|
+
if options[:geojson]
|
|
226
|
+
converted << pair
|
|
227
|
+
else
|
|
228
|
+
emit(pair, options)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
emit_features(converted, options) if options[:geojson]
|
|
232
|
+
else
|
|
233
|
+
coords = parse_coords(ARGV)
|
|
234
|
+
pair = Coordconver.public_send(method, coords[0], coords[1])
|
|
235
|
+
options[:geojson] ? emit_features([pair], options) : emit(pair, options)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
rescue Coordconver::Error, ArgumentError, TypeError, CSV::MalformedCSVError => e
|
|
239
|
+
warn "错误: #{e.message}"
|
|
240
|
+
exit 2
|
|
241
|
+
end
|
|
@@ -1,106 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# 坐标系互转核心算法(纯离线计算)。
|
|
4
|
+
#
|
|
5
|
+
# BD-09 百度坐标系, 百度地图使用
|
|
6
|
+
# GCJ-02 火星坐标系(国测局), 高德/腾讯/谷歌中国使用
|
|
7
|
+
# WGS-84 地球坐标系, GPS 设备原始坐标
|
|
1
8
|
module Coordconver
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
9
|
+
X_PI = 3.14159265358979324 * 3000.0 / 180.0
|
|
10
|
+
PI = 3.1415926535897932384626
|
|
11
|
+
# 克拉索夫斯基椭球参数(GCJ-02 偏移算法使用)
|
|
12
|
+
SEMI_MAJOR_AXIS = 6378245.0
|
|
13
|
+
ECCENTRICITY_SQUARED = 0.00669342162296594323
|
|
14
|
+
# 平均地球半径(米), 球面距离/方位角计算使用
|
|
15
|
+
EARTH_RADIUS = 6_371_008.8
|
|
16
|
+
# 精确逆变换迭代次数
|
|
17
|
+
PRECISE_ITERATIONS = 5
|
|
18
|
+
# 中国区域大致边界。GCJ-02 加密仅在中国境内生效, 境外 WGS-84 与 GCJ-02 一致
|
|
19
|
+
CHINA_LNG_RANGE = [72.004, 137.8347].freeze
|
|
20
|
+
CHINA_LAT_RANGE = [0.8293, 55.8271].freeze
|
|
21
|
+
|
|
22
|
+
# 轻量坐标点结构, 可直接传给转换方法:
|
|
23
|
+
# Coordconver.wgs_gcj(Coordconver.point(121.47, 31.25))
|
|
24
|
+
Point = Struct.new(:lng, :lat, keyword_init: true) do
|
|
25
|
+
def to_a
|
|
26
|
+
[lng, lat]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{ lng: lng, lat: lat }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# 构造 Point 结构
|
|
36
|
+
def point(lng, lat)
|
|
37
|
+
Point.new(lng: lng.to_f, lat: lat.to_f)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 判断坐标是否在中国境外
|
|
41
|
+
# Coordconver.out_of_china?(121.47, 31.25) # => false
|
|
42
|
+
def out_of_china?(lng, lat)
|
|
43
|
+
lng < CHINA_LNG_RANGE[0] || lng > CHINA_LNG_RANGE[1] ||
|
|
44
|
+
lat < CHINA_LAT_RANGE[0] || lat > CHINA_LAT_RANGE[1]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ---------- 坐标系转换 ----------
|
|
48
|
+
# 所有方法支持三种入参: (lng, lat) 双参 / Point / [lng, lat] / {lng:, lat:}
|
|
49
|
+
# 并支持 as: :hash 返回 {lng:, lat:}, as: :point 返回 Point, 默认 [lng, lat]
|
|
50
|
+
|
|
51
|
+
# 百度坐标系 (BD-09) -> 火星坐标系 (GCJ-02)
|
|
52
|
+
# 即 百度 -> 谷歌、高德、腾讯
|
|
53
|
+
# Coordconver.bd_gcj(121.47981626441, 31.251161076123) # => [121.47328, 31.24522]
|
|
54
|
+
def bd_gcj(lng, lat = nil, as: nil)
|
|
55
|
+
a, b = extract_pair(lng, lat)
|
|
56
|
+
x = a - 0.0065
|
|
57
|
+
y = b - 0.006
|
|
58
|
+
z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI)
|
|
59
|
+
theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI)
|
|
60
|
+
fmt([z * Math.cos(theta), z * Math.sin(theta)], as)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 百度坐标系 (BD-09) -> 地球坐标 (WGS-84)
|
|
64
|
+
# 即 百度 -> 地球坐标 (近似, 误差米级)
|
|
65
|
+
def bd_wgs(lng, lat = nil, as: nil)
|
|
66
|
+
a, b = extract_pair(lng, lat)
|
|
67
|
+
fmt(gcj_wgs_raw(*bd_gcj_raw(a, b)), as)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 百度坐标系 (BD-09) -> 地球坐标 (WGS-84), 迭代精确解, 厘米级
|
|
71
|
+
def bd_wgs_precise(lng, lat = nil, as: nil)
|
|
72
|
+
a, b = extract_pair(lng, lat)
|
|
73
|
+
fmt(gcj_wgs_iterate(*bd_gcj_raw(a, b)), as)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 火星坐标系 (GCJ-02) -> 地球坐标 (WGS-84)
|
|
77
|
+
# 即 谷歌、高德、腾讯 -> 地球坐标。境外坐标原样返回。(近似, 误差米级)
|
|
78
|
+
def gcj_wgs(lng, lat = nil, as: nil)
|
|
79
|
+
a, b = extract_pair(lng, lat)
|
|
80
|
+
fmt(gcj_wgs_raw(a, b), as)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 火星坐标系 (GCJ-02) -> 地球坐标 (WGS-84), 迭代精确解, 厘米级
|
|
84
|
+
# Coordconver.gcj_wgs_precise(*Coordconver.wgs_gcj(121.47, 31.24)) # => [121.47, 31.24]
|
|
85
|
+
def gcj_wgs_precise(lng, lat = nil, as: nil)
|
|
86
|
+
a, b = extract_pair(lng, lat)
|
|
87
|
+
fmt(gcj_wgs_iterate(a, b), as)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 火星坐标系 (GCJ-02) -> 百度坐标系 (BD-09)
|
|
91
|
+
# 即 谷歌、高德、腾讯 -> 百度
|
|
92
|
+
def gcj_bd(lng, lat = nil, as: nil)
|
|
93
|
+
a, b = extract_pair(lng, lat)
|
|
94
|
+
z = Math.sqrt(a * a + b * b) + 0.00002 * Math.sin(b * X_PI)
|
|
95
|
+
theta = Math.atan2(b, a) + 0.000003 * Math.cos(a * X_PI)
|
|
96
|
+
fmt([z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006], as)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 地球坐标 (WGS-84) -> 百度坐标系 (BD-09)
|
|
100
|
+
# 即 地球坐标 -> 百度
|
|
101
|
+
def wgs_bd(lng, lat = nil, as: nil)
|
|
102
|
+
a, b = extract_pair(lng, lat)
|
|
103
|
+
fmt(gcj_bd_raw(*wgs_gcj_raw(a, b)), as)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# 地球坐标 (WGS-84) -> 火星坐标系 (GCJ-02)
|
|
107
|
+
# 即 地球坐标 -> 谷歌、高德、腾讯。境外坐标原样返回
|
|
108
|
+
def wgs_gcj(lng, lat = nil, as: nil)
|
|
109
|
+
a, b = extract_pair(lng, lat)
|
|
110
|
+
fmt(wgs_gcj_raw(a, b), as)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ---------- 批量转换 ----------
|
|
114
|
+
# 支持数组/Point/Hash 混合输入:
|
|
115
|
+
# Coordconver.transform([[121.46, 31.24], {lng: 116.39, lat: 39.90}], from: :wgs84, to: :gcj02)
|
|
116
|
+
# from/to 支持 :wgs84(wgs) :gcj02(gcj) :bd09(bd)
|
|
117
|
+
def transform(points, from:, to:, as: nil)
|
|
118
|
+
method = transform_method(from, to)
|
|
119
|
+
points.map do |pt|
|
|
120
|
+
a, b = extract_pair(pt)
|
|
121
|
+
fmt(public_send(method, a, b), as)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ---------- 球面几何 ----------
|
|
126
|
+
|
|
127
|
+
# 两点球面距离(Haversine 公式), 返回米。两点需在同一坐标系下。
|
|
128
|
+
# Coordconver.distance(116.397428, 39.90923, 121.473701, 31.230416) # => 约 106.8 万米
|
|
129
|
+
def distance(lng1, lat1, lng2, lat2)
|
|
130
|
+
dlng = to_radian(lng2 - lng1)
|
|
131
|
+
dlat = to_radian(lat2 - lat1)
|
|
132
|
+
a = Math.sin(dlat / 2)**2 +
|
|
133
|
+
Math.cos(to_radian(lat1)) * Math.cos(to_radian(lat2)) * Math.sin(dlng / 2)**2
|
|
134
|
+
(2 * EARTH_RADIUS * Math.asin(Math.sqrt(a))).round(1)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# 起点到终点的初始方位角(以正北为 0°, 顺时针 0-360°)
|
|
138
|
+
# Coordconver.bearing(116.397428, 39.90923, 121.473701, 31.230416) # => 约 137.5
|
|
139
|
+
def bearing(lng1, lat1, lng2, lat2)
|
|
140
|
+
p1 = to_radian(lat1)
|
|
141
|
+
p2 = to_radian(lat2)
|
|
142
|
+
dl = to_radian(lng2 - lng1)
|
|
143
|
+
y = Math.sin(dl) * Math.cos(p2)
|
|
144
|
+
x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl)
|
|
145
|
+
((Math.atan2(y, x) * 180.0 / PI) + 360) % 360
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# 从起点沿方位角移动指定距离(米)后的坐标
|
|
149
|
+
# Coordconver.destination_point(116.397428, 39.90923, 137.5, 1_068_000)
|
|
150
|
+
def destination_point(lng, lat, bearing_deg, distance_m, as: nil)
|
|
151
|
+
d = distance_m.to_f / EARTH_RADIUS
|
|
152
|
+
p1 = to_radian(lat)
|
|
153
|
+
b = to_radian(bearing_deg)
|
|
154
|
+
p2 = Math.asin(Math.sin(p1) * Math.cos(d) + Math.cos(p1) * Math.sin(d) * Math.cos(b))
|
|
155
|
+
l2 = to_radian(lng) + Math.atan2(
|
|
156
|
+
Math.sin(b) * Math.sin(d) * Math.cos(p1),
|
|
157
|
+
Math.cos(d) - Math.sin(p1) * Math.sin(p2)
|
|
158
|
+
)
|
|
159
|
+
lon = ((l2 * 180.0 / PI) + 540) % 360 - 180
|
|
160
|
+
fmt([lon, p2 * 180.0 / PI], as)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# ---------- 兼容旧式/语义化别名 ----------
|
|
164
|
+
alias wgs84_gcj02 wgs_gcj
|
|
165
|
+
alias gcj02_wgs84 gcj_wgs
|
|
166
|
+
alias gcj02_bd09 gcj_bd
|
|
167
|
+
alias bd09_gcj02 bd_gcj
|
|
168
|
+
alias wgs84_bd09 wgs_bd
|
|
169
|
+
alias bd09_wgs84 bd_wgs
|
|
170
|
+
alias gcj02_wgs84_precise gcj_wgs_precise
|
|
171
|
+
alias bd09_wgs84_precise bd_wgs_precise
|
|
172
|
+
|
|
173
|
+
# 归一化坐标入参: 支持 (lng, lat) 双参 / Point / [lng, lat] / {lng:, lat:}
|
|
174
|
+
def extract_pair(lng, lat = nil)
|
|
175
|
+
return [lng.to_f, lat.to_f] unless lat.nil?
|
|
176
|
+
|
|
177
|
+
case lng
|
|
178
|
+
when Point then [lng.lng.to_f, lng.lat.to_f]
|
|
179
|
+
when Hash
|
|
180
|
+
[float_of(lng[:lng] || lng["lng"]), float_of(lng[:lat] || lng["lat"])]
|
|
181
|
+
when Array then [float_of(lng[0]), float_of(lng[1])]
|
|
182
|
+
else raise ArgumentError, "无法解析的坐标: #{lng.inspect} (支持 (lng, lat) / Point / [lng, lat] / {lng:, lat:})"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def float_of(v)
|
|
189
|
+
raise ArgumentError, "坐标值无效: #{v.inspect}" if v.nil?
|
|
190
|
+
|
|
191
|
+
Float(v)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def fmt(pair, as)
|
|
195
|
+
case as
|
|
196
|
+
when nil then pair
|
|
197
|
+
when :hash then { lng: pair[0], lat: pair[1] }
|
|
198
|
+
when :point then Point.new(lng: pair[0], lat: pair[1])
|
|
199
|
+
else raise ArgumentError, "未知的返回格式: #{as.inspect} (支持 :hash / :point)"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def transform_method(from, to)
|
|
204
|
+
norm = ->(s) { s.to_s.downcase.gsub(/(84|02|09)\z/, "") }
|
|
205
|
+
map = {
|
|
206
|
+
%w[wgs gcj] => :wgs_gcj, %w[gcj wgs] => :gcj_wgs,
|
|
207
|
+
%w[wgs bd] => :wgs_bd, %w[bd wgs] => :bd_wgs,
|
|
208
|
+
%w[gcj bd] => :gcj_bd, %w[bd gcj] => :bd_gcj
|
|
209
|
+
}
|
|
210
|
+
m = map[[norm.call(from), norm.call(to)]]
|
|
211
|
+
raise ArgumentError, "不支持的转换: #{from} -> #{to}" unless m
|
|
212
|
+
|
|
213
|
+
m
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def to_radian(deg)
|
|
217
|
+
deg * PI / 180.0
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# gcj_wgs 迭代精确解: 解方程 wgs_gcj(w) = g
|
|
221
|
+
def gcj_wgs_iterate(lng, lat)
|
|
222
|
+
w = [lng, lat]
|
|
223
|
+
PRECISE_ITERATIONS.times do
|
|
224
|
+
cur = wgs_gcj_raw(w[0], w[1])
|
|
225
|
+
w = [w[0] + (lng - cur[0]), w[1] + (lat - cur[1])]
|
|
226
|
+
end
|
|
227
|
+
w
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def gcj_wgs_raw(lng, lat)
|
|
231
|
+
return [lng, lat] if out_of_china?(lng, lat)
|
|
232
|
+
|
|
233
|
+
dlat = transformlat(lng - 105.0, lat - 35.0)
|
|
234
|
+
dlng = transformlng(lng - 105.0, lat - 35.0)
|
|
235
|
+
radlat = lat / 180.0 * PI
|
|
236
|
+
magic = 1 - ECCENTRICITY_SQUARED * Math.sin(radlat) * Math.sin(radlat)
|
|
237
|
+
sqrtmagic = Math.sqrt(magic)
|
|
238
|
+
dlat = (dlat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtmagic) * PI)
|
|
239
|
+
dlng = (dlng * 180.0) / (SEMI_MAJOR_AXIS / sqrtmagic * Math.cos(radlat) * PI)
|
|
240
|
+
mglat = lat + dlat
|
|
241
|
+
mglng = lng + dlng
|
|
242
|
+
[lng * 2 - mglng, lat * 2 - mglat]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def wgs_gcj_raw(lng, lat)
|
|
246
|
+
return [lng, lat] if out_of_china?(lng, lat)
|
|
247
|
+
|
|
248
|
+
dlat = transformlat(lng - 105.0, lat - 35.0)
|
|
249
|
+
dlng = transformlng(lng - 105.0, lat - 35.0)
|
|
250
|
+
radlat = lat / 180.0 * PI
|
|
251
|
+
magic = 1 - ECCENTRICITY_SQUARED * Math.sin(radlat) * Math.sin(radlat)
|
|
252
|
+
sqrtmagic = Math.sqrt(magic)
|
|
253
|
+
dlat = (dlat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtmagic) * PI)
|
|
254
|
+
dlng = (dlng * 180.0) / (SEMI_MAJOR_AXIS / sqrtmagic * Math.cos(radlat) * PI)
|
|
255
|
+
[lng + dlng, lat + dlat]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def gcj_bd_raw(lng, lat)
|
|
259
|
+
z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * X_PI)
|
|
260
|
+
theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * X_PI)
|
|
261
|
+
[z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def bd_gcj_raw(lng, lat)
|
|
265
|
+
x = lng - 0.0065
|
|
266
|
+
y = lat - 0.006
|
|
267
|
+
z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI)
|
|
268
|
+
theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI)
|
|
269
|
+
[z * Math.cos(theta), z * Math.sin(theta)]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def transformlat(lng, lat)
|
|
273
|
+
ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(lng.abs)
|
|
274
|
+
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0
|
|
275
|
+
ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0
|
|
276
|
+
ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0
|
|
277
|
+
ret
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def transformlng(lng, lat)
|
|
281
|
+
ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(lng.abs)
|
|
282
|
+
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0
|
|
283
|
+
ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0
|
|
284
|
+
ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0
|
|
285
|
+
ret
|
|
286
|
+
end
|
|
287
|
+
end
|
|
106
288
|
end
|