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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coordconver 算法层 (JS 移植版)
|
|
3
|
+
* 与 Ruby gem (lib/coordconver) 中的算法保持数值一致, 供在线试用页使用。
|
|
4
|
+
* 纯函数, 无 DOM 依赖, 可在 Node 中独立测试。
|
|
5
|
+
*/
|
|
6
|
+
(function (root) {
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
var X_PI = (3.14159265358979324 * 3000.0) / 180.0;
|
|
10
|
+
var PI = Math.PI;
|
|
11
|
+
var SEMI_MAJOR_AXIS = 6378245.0; // GCJ-02 偏移算法 (克拉索夫斯基椭球)
|
|
12
|
+
var EE = 0.00669342162296594323;
|
|
13
|
+
var EARTH_RADIUS = 6371008.8;
|
|
14
|
+
|
|
15
|
+
var ELLIPSOIDS = {
|
|
16
|
+
cgcs2000: { a: 6378137.0, rf: 298.257222101 },
|
|
17
|
+
wgs84: { a: 6378137.0, rf: 298.257223563 },
|
|
18
|
+
krassovsky: { a: 6378245.0, rf: 298.3 }, // 北京54
|
|
19
|
+
iag75: { a: 6378140.0, rf: 298.257 } // 西安80
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function outOfChina(lng, lat) {
|
|
23
|
+
return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function transformLat(lng, lat) {
|
|
27
|
+
var ret =
|
|
28
|
+
-100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
|
|
29
|
+
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0;
|
|
30
|
+
ret += ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) / 3.0;
|
|
31
|
+
ret += ((160.0 * Math.sin((lat / 12.0) * PI) + 320 * Math.sin((lat * PI) / 30.0)) * 2.0) / 3.0;
|
|
32
|
+
return ret;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function transformLng(lng, lat) {
|
|
36
|
+
var ret =
|
|
37
|
+
300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
|
|
38
|
+
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0;
|
|
39
|
+
ret += ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) / 3.0;
|
|
40
|
+
ret += ((150.0 * Math.sin((lng / 12.0) * PI) + 300.0 * Math.sin((lng / 30.0) * PI)) * 2.0) / 3.0;
|
|
41
|
+
return ret;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function wgs2gcj(lng, lat) {
|
|
45
|
+
if (outOfChina(lng, lat)) return [lng, lat];
|
|
46
|
+
var dLat = transformLat(lng - 105.0, lat - 35.0);
|
|
47
|
+
var dLng = transformLng(lng - 105.0, lat - 35.0);
|
|
48
|
+
var radlat = (lat / 180.0) * PI;
|
|
49
|
+
var magic = 1 - EE * Math.sin(radlat) * Math.sin(radlat);
|
|
50
|
+
var sqrtmagic = Math.sqrt(magic);
|
|
51
|
+
dLat = (dLat * 180.0) / (((SEMI_MAJOR_AXIS * (1 - EE)) / (magic * sqrtmagic)) * PI);
|
|
52
|
+
dLng = (dLng * 180.0) / ((SEMI_MAJOR_AXIS / sqrtmagic) * Math.cos(radlat) * PI);
|
|
53
|
+
return [lng + dLng, lat + dLat];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function gcj2wgs(lng, lat) {
|
|
57
|
+
if (outOfChina(lng, lat)) return [lng, lat];
|
|
58
|
+
var dLat = transformLat(lng - 105.0, lat - 35.0);
|
|
59
|
+
var dLng = transformLng(lng - 105.0, lat - 35.0);
|
|
60
|
+
var radlat = (lat / 180.0) * PI;
|
|
61
|
+
var magic = 1 - EE * Math.sin(radlat) * Math.sin(radlat);
|
|
62
|
+
var sqrtmagic = Math.sqrt(magic);
|
|
63
|
+
dLat = (dLat * 180.0) / (((SEMI_MAJOR_AXIS * (1 - EE)) / (magic * sqrtmagic)) * PI);
|
|
64
|
+
dLng = (dLng * 180.0) / ((SEMI_MAJOR_AXIS / sqrtmagic) * Math.cos(radlat) * PI);
|
|
65
|
+
var mglat = lat + dLat;
|
|
66
|
+
var mglng = lng + dLng;
|
|
67
|
+
return [lng * 2 - mglng, lat * 2 - mglat];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function gcj2wgsPrecise(lng, lat) {
|
|
71
|
+
var w = [lng, lat];
|
|
72
|
+
for (var i = 0; i < 5; i++) {
|
|
73
|
+
var cur = wgs2gcj(w[0], w[1]);
|
|
74
|
+
w = [w[0] + (lng - cur[0]), w[1] + (lat - cur[1])];
|
|
75
|
+
}
|
|
76
|
+
return w;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function gcj2bd(lng, lat) {
|
|
80
|
+
var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * X_PI);
|
|
81
|
+
var theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * X_PI);
|
|
82
|
+
return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function bd2gcj(lng, lat) {
|
|
86
|
+
var x = lng - 0.0065;
|
|
87
|
+
var y = lat - 0.006;
|
|
88
|
+
var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
|
|
89
|
+
var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
|
|
90
|
+
return [z * Math.cos(theta), z * Math.sin(theta)];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function wgs2bd(lng, lat) {
|
|
94
|
+
return gcj2bd.apply(null, wgs2gcj(lng, lat));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function bd2wgs(lng, lat) {
|
|
98
|
+
return gcj2wgs.apply(null, bd2gcj(lng, lat));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function bd2wgsPrecise(lng, lat) {
|
|
102
|
+
return gcj2wgsPrecise.apply(null, bd2gcj(lng, lat));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function distance(lng1, lat1, lng2, lat2) {
|
|
106
|
+
var dlng = ((lng2 - lng1) * PI) / 180.0;
|
|
107
|
+
var dlat = ((lat2 - lat1) * PI) / 180.0;
|
|
108
|
+
var a =
|
|
109
|
+
Math.sin(dlat / 2) * Math.sin(dlat / 2) +
|
|
110
|
+
Math.cos((lat1 * PI) / 180.0) *
|
|
111
|
+
Math.cos((lat2 * PI) / 180.0) *
|
|
112
|
+
Math.sin(dlng / 2) *
|
|
113
|
+
Math.sin(dlng / 2);
|
|
114
|
+
return Math.round(2 * EARTH_RADIUS * Math.asin(Math.sqrt(a)) * 10) / 10;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function bearing(lng1, lat1, lng2, lat2) {
|
|
118
|
+
var p1 = (lat1 * PI) / 180.0;
|
|
119
|
+
var p2 = (lat2 * PI) / 180.0;
|
|
120
|
+
var dl = ((lng2 - lng1) * PI) / 180.0;
|
|
121
|
+
var y = Math.sin(dl) * Math.cos(p2);
|
|
122
|
+
var x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl);
|
|
123
|
+
return (((Math.atan2(y, x) * 180.0) / PI) + 360) % 360;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function destinationPoint(lng, lat, bearingDeg, distanceM) {
|
|
127
|
+
var d = distanceM / EARTH_RADIUS;
|
|
128
|
+
var p1 = (lat * PI) / 180.0;
|
|
129
|
+
var b = (bearingDeg * PI) / 180.0;
|
|
130
|
+
var p2 = Math.asin(Math.sin(p1) * Math.cos(d) + Math.cos(p1) * Math.sin(d) * Math.cos(b));
|
|
131
|
+
var l2 =
|
|
132
|
+
(lng * PI) / 180.0 +
|
|
133
|
+
Math.atan2(Math.sin(b) * Math.sin(d) * Math.cos(p1), Math.cos(d) - Math.sin(p1) * Math.sin(p2));
|
|
134
|
+
var lon = (((l2 * 180.0) / PI) + 540) % 360 - 180;
|
|
135
|
+
return [lon, (p2 * 180.0) / PI];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toDMS(n, precision) {
|
|
139
|
+
precision = precision === undefined ? 2 : precision;
|
|
140
|
+
var sign = n < 0 ? "-" : "";
|
|
141
|
+
n = Math.abs(n);
|
|
142
|
+
var d = Math.floor(n);
|
|
143
|
+
var m = (n - d) * 60;
|
|
144
|
+
var s = (m - Math.floor(m)) * 60;
|
|
145
|
+
if (precision === 0) {
|
|
146
|
+
return sign + d + "°" + Math.floor(m) + "′" + Math.round(s) + "″";
|
|
147
|
+
}
|
|
148
|
+
return sign + d + "°" + Math.floor(m) + "′" + s.toFixed(precision) + "″";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function fromDMS(str) {
|
|
152
|
+
var parts = String(str).match(/[-+]?\d+(?:\.\d+)?/g);
|
|
153
|
+
if (!parts || parts.length === 0) return 0;
|
|
154
|
+
var neg = /^\s*[-–]/.test(String(str)) || parseFloat(parts[0]) < 0;
|
|
155
|
+
var deg = Math.abs(parseFloat(parts[0]));
|
|
156
|
+
var min = parts.length > 1 ? parseFloat(parts[1]) : 0;
|
|
157
|
+
var sec = parts.length > 2 ? parseFloat(parts[2]) : 0;
|
|
158
|
+
var value = deg + min / 60.0 + sec / 3600.0;
|
|
159
|
+
return Math.round((neg ? -value : value) * 1e6) / 1e6;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function zoneMeridian(lng, width) {
|
|
163
|
+
if (width !== 3 && width !== 6) throw new Error("带宽仅支持 3 或 6 度");
|
|
164
|
+
var zone = width === 6 ? Math.floor(lng / 6) + 1 : Math.round(lng / 3);
|
|
165
|
+
return [zone, width === 6 ? zone * 6 - 3 : zone * 3];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function gaussKruger(lng, lat, opts) {
|
|
169
|
+
opts = opts || {};
|
|
170
|
+
var ellipsoid = ELLIPSOIDS[opts.ellipsoid || "cgcs2000"];
|
|
171
|
+
if (!ellipsoid) throw new Error("未知椭球: " + opts.ellipsoid);
|
|
172
|
+
var a = ellipsoid.a;
|
|
173
|
+
var f = 1.0 / ellipsoid.rf;
|
|
174
|
+
var cm =
|
|
175
|
+
opts.centralMeridian !== undefined && opts.centralMeridian !== null
|
|
176
|
+
? opts.centralMeridian
|
|
177
|
+
: zoneMeridian(lng, opts.zoneWidth || 6)[1];
|
|
178
|
+
var fe = opts.falseEasting === undefined ? 500000 : opts.falseEasting;
|
|
179
|
+
var fn = opts.falseNorthing || 0;
|
|
180
|
+
var e2 = f * (2 - f);
|
|
181
|
+
var ep2 = e2 / (1 - e2);
|
|
182
|
+
|
|
183
|
+
var p = (lat * PI) / 180.0;
|
|
184
|
+
var dl = ((lng - cm) * PI) / 180.0;
|
|
185
|
+
var sinP = Math.sin(p);
|
|
186
|
+
var cosP = Math.cos(p);
|
|
187
|
+
var tanP = Math.tan(p);
|
|
188
|
+
|
|
189
|
+
var n = a / Math.sqrt(1 - e2 * sinP * sinP);
|
|
190
|
+
var t = tanP * tanP;
|
|
191
|
+
var c = ep2 * cosP * cosP;
|
|
192
|
+
var aa = dl * cosP;
|
|
193
|
+
var m =
|
|
194
|
+
a *
|
|
195
|
+
((1 - e2 / 4 - (3 * e2 * e2) / 64 - (5 * Math.pow(e2, 3)) / 256) * p -
|
|
196
|
+
((3 * e2) / 8 + (3 * e2 * e2) / 32 + (45 * Math.pow(e2, 3)) / 1024) * Math.sin(2 * p) +
|
|
197
|
+
((15 * e2 * e2) / 256 + (45 * Math.pow(e2, 3)) / 1024) * Math.sin(4 * p) -
|
|
198
|
+
((35 * Math.pow(e2, 3)) / 3072) * Math.sin(6 * p));
|
|
199
|
+
|
|
200
|
+
var x =
|
|
201
|
+
fe + n * (aa + ((1 - t + c) * Math.pow(aa, 3)) / 6 + ((5 - 18 * t + t * t + 72 * c - 58 * ep2) * Math.pow(aa, 5)) / 120);
|
|
202
|
+
var y =
|
|
203
|
+
fn +
|
|
204
|
+
m +
|
|
205
|
+
n * tanP * ((aa * aa) / 2 + ((5 - t + 9 * c + 4 * c * c) * Math.pow(aa, 4)) / 24 + ((61 - 58 * t + t * t + 600 * c - 330 * ep2) * Math.pow(aa, 6)) / 720);
|
|
206
|
+
return [round4(x), round4(y)];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function inverseGaussKruger(x, y, opts) {
|
|
210
|
+
opts = opts || {};
|
|
211
|
+
var ellipsoid = ELLIPSOIDS[opts.ellipsoid || "cgcs2000"];
|
|
212
|
+
if (!ellipsoid) throw new Error("未知椭球: " + opts.ellipsoid);
|
|
213
|
+
if (opts.centralMeridian === undefined || opts.centralMeridian === null) {
|
|
214
|
+
throw new Error("反算必须指定中央经线");
|
|
215
|
+
}
|
|
216
|
+
var a = ellipsoid.a;
|
|
217
|
+
var f = 1.0 / ellipsoid.rf;
|
|
218
|
+
var fe = opts.falseEasting === undefined ? 500000 : opts.falseEasting;
|
|
219
|
+
var fn = opts.falseNorthing || 0;
|
|
220
|
+
var e2 = f * (2 - f);
|
|
221
|
+
var ep2 = e2 / (1 - e2);
|
|
222
|
+
|
|
223
|
+
var m = y - fn;
|
|
224
|
+
var mu = m / (a * (1 - e2 / 4 - (3 * e2 * e2) / 64 - (5 * Math.pow(e2, 3)) / 256));
|
|
225
|
+
var e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2));
|
|
226
|
+
|
|
227
|
+
var p1 =
|
|
228
|
+
mu +
|
|
229
|
+
((3 * e1) / 2 - (27 * Math.pow(e1, 3)) / 32) * Math.sin(2 * mu) +
|
|
230
|
+
((21 * e1 * e1) / 16 - (55 * Math.pow(e1, 4)) / 32) * Math.sin(4 * mu) +
|
|
231
|
+
((151 * Math.pow(e1, 3)) / 96) * Math.sin(6 * mu) +
|
|
232
|
+
((1097 * Math.pow(e1, 4)) / 512) * Math.sin(8 * mu);
|
|
233
|
+
|
|
234
|
+
var sinP1 = Math.sin(p1);
|
|
235
|
+
var cosP1 = Math.cos(p1);
|
|
236
|
+
var tanP1 = Math.tan(p1);
|
|
237
|
+
var c1 = ep2 * cosP1 * cosP1;
|
|
238
|
+
var t1 = tanP1 * tanP1;
|
|
239
|
+
var n1 = a / Math.sqrt(1 - e2 * sinP1 * sinP1);
|
|
240
|
+
var r1 = (a * (1 - e2)) / Math.pow(1 - e2 * sinP1 * sinP1, 1.5);
|
|
241
|
+
var d = (x - fe) / n1;
|
|
242
|
+
|
|
243
|
+
var p =
|
|
244
|
+
p1 -
|
|
245
|
+
((n1 * tanP1) / r1) *
|
|
246
|
+
((d * d) / 2 -
|
|
247
|
+
((5 + 3 * t1 + 10 * c1 - 4 * c1 * c1 - 9 * ep2) * Math.pow(d, 4)) / 24 +
|
|
248
|
+
((61 + 90 * t1 + 298 * c1 + 45 * t1 * t1 - 252 * ep2 - 3 * c1 * c1) * Math.pow(d, 6)) / 720);
|
|
249
|
+
var l =
|
|
250
|
+
(d -
|
|
251
|
+
((1 + 2 * t1 + c1) * Math.pow(d, 3)) / 6 +
|
|
252
|
+
((5 - 2 * c1 + 28 * t1 - 3 * c1 * c1 + 8 * ep2 + 24 * t1 * t1) * Math.pow(d, 5)) / 120) /
|
|
253
|
+
cosP1;
|
|
254
|
+
|
|
255
|
+
var lng = ((l + (opts.centralMeridian * PI) / 180.0) * 180.0) / PI;
|
|
256
|
+
var lat = (p * 180.0) / PI;
|
|
257
|
+
return [round9(lng), round9(lat)];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function round4(v) {
|
|
261
|
+
return Math.round(v * 10000) / 10000;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function round9(v) {
|
|
265
|
+
return Math.round(v * 1e9) / 1e9;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
var api = {
|
|
269
|
+
outOfChina: outOfChina,
|
|
270
|
+
wgs2gcj: wgs2gcj,
|
|
271
|
+
gcj2wgs: gcj2wgs,
|
|
272
|
+
gcj2wgsPrecise: gcj2wgsPrecise,
|
|
273
|
+
gcj2bd: gcj2bd,
|
|
274
|
+
bd2gcj: bd2gcj,
|
|
275
|
+
wgs2bd: wgs2bd,
|
|
276
|
+
bd2wgs: bd2wgs,
|
|
277
|
+
bd2wgsPrecise: bd2wgsPrecise,
|
|
278
|
+
distance: distance,
|
|
279
|
+
bearing: bearing,
|
|
280
|
+
destinationPoint: destinationPoint,
|
|
281
|
+
toDMS: toDMS,
|
|
282
|
+
fromDMS: fromDMS,
|
|
283
|
+
zoneMeridian: zoneMeridian,
|
|
284
|
+
gaussKruger: gaussKruger,
|
|
285
|
+
inverseGaussKruger: inverseGaussKruger
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
289
|
+
module.exports = api;
|
|
290
|
+
} else {
|
|
291
|
+
root.CC = api;
|
|
292
|
+
}
|
|
293
|
+
})(typeof self !== "undefined" ? self : this);
|
data/docs/index.html
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>坐标转换在线试用 · coordconver</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f6f8fa; --card: #ffffff; --ink: #1f2328; --muted: #656d76;
|
|
10
|
+
--line: #d0d7de; --accent: #0969da; --accent-ink: #ffffff;
|
|
11
|
+
--ok-bg: #dafbe1; --err: #cf222e; --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--ink); line-height: 1.6; }
|
|
15
|
+
header { background: #24292f; color: #fff; padding: 28px 16px 24px; text-align: center; }
|
|
16
|
+
header h1 { margin: 0 0 6px; font-size: 26px; }
|
|
17
|
+
header h1 code { font-family: var(--mono); background: none; color: #79c0ff; }
|
|
18
|
+
header p { margin: 0; color: #c9d1d9; font-size: 14px; }
|
|
19
|
+
header a { color: #79c0ff; }
|
|
20
|
+
main { max-width: 860px; margin: 0 auto; padding: 20px 16px 48px; }
|
|
21
|
+
.card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 20px; margin-top: 20px; }
|
|
22
|
+
.card h2 { margin: 0 0 4px; font-size: 17px; }
|
|
23
|
+
.card .desc { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
|
|
24
|
+
label { font-size: 13px; color: var(--muted); display: block; margin: 10px 0 4px; }
|
|
25
|
+
textarea, input[type=text], input[type=number], select {
|
|
26
|
+
width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px;
|
|
27
|
+
font-size: 14px; font-family: var(--mono); background: #fff; color: var(--ink);
|
|
28
|
+
}
|
|
29
|
+
textarea { min-height: 96px; resize: vertical; }
|
|
30
|
+
textarea[readonly] { background: var(--bg); }
|
|
31
|
+
.row { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
32
|
+
.row > * { flex: 1; min-width: 160px; }
|
|
33
|
+
button {
|
|
34
|
+
margin-top: 12px; padding: 8px 18px; border: 1px solid var(--line); border-radius: 6px;
|
|
35
|
+
background: var(--accent); color: var(--accent-ink); font-size: 14px; cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
button.secondary { background: #fff; color: var(--ink); }
|
|
38
|
+
button:hover { filter: brightness(1.05); }
|
|
39
|
+
.hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
|
|
40
|
+
.result { margin-top: 12px; padding: 10px 12px; border-radius: 6px; font-family: var(--mono); font-size: 13px; white-space: pre-wrap; word-break: break-all; display: none; }
|
|
41
|
+
.result.ok { display: block; background: var(--ok-bg); }
|
|
42
|
+
.result.err { display: block; background: #ffebe9; color: var(--err); }
|
|
43
|
+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
44
|
+
@media (max-width: 640px) { .grid2 { grid-template-columns: 1fr; } }
|
|
45
|
+
.check { display: flex; align-items: center; gap: 6px; margin-top: 10px; font-size: 13px; color: var(--muted); }
|
|
46
|
+
.check input { width: auto; }
|
|
47
|
+
table.ref { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
|
|
48
|
+
table.ref td { border-top: 1px solid var(--line); padding: 6px 8px; }
|
|
49
|
+
table.ref td:first-child { font-family: var(--mono); color: var(--accent); white-space: nowrap; }
|
|
50
|
+
footer { text-align: center; color: var(--muted); font-size: 13px; padding: 0 16px 40px; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<header>
|
|
55
|
+
<h1><code>coordconver</code> 坐标转换在线试用</h1>
|
|
56
|
+
<p>纯前端实现, 与 <a href="https://github.com/cheenwe/coordconver">Ruby gem</a> 算法数值一致 · 无需安装即可体验</p>
|
|
57
|
+
</header>
|
|
58
|
+
<main>
|
|
59
|
+
|
|
60
|
+
<div class="card">
|
|
61
|
+
<h2>坐标系转换</h2>
|
|
62
|
+
<p class="desc">WGS-84(地球/GPS) / GCJ-02(火星/高德·腾讯) / BD-09(百度) 互转。支持批量: 每行一条 "经度,纬度"。</p>
|
|
63
|
+
<div class="row">
|
|
64
|
+
<div>
|
|
65
|
+
<label for="from">源坐标系</label>
|
|
66
|
+
<select id="from">
|
|
67
|
+
<option value="wgs">WGS-84 地球坐标 (GPS)</option>
|
|
68
|
+
<option value="gcj">GCJ-02 火星坐标 (高德/腾讯)</option>
|
|
69
|
+
<option value="bd">BD-09 百度坐标</option>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<label for="to">目标坐标系</label>
|
|
74
|
+
<select id="to">
|
|
75
|
+
<option value="gcj">GCJ-02 火星坐标 (高德/腾讯)</option>
|
|
76
|
+
<option value="wgs">WGS-84 地球坐标 (GPS)</option>
|
|
77
|
+
<option value="bd">BD-09 百度坐标</option>
|
|
78
|
+
</select>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<label for="conv-in">输入坐标</label>
|
|
82
|
+
<textarea id="conv-in" placeholder="121.468738,31.247138 116.481499,39.990475"></textarea>
|
|
83
|
+
<div class="check"><input type="checkbox" id="precise"><label for="precise" style="margin:0">使用精确逆变换 (目标为 WGS-84 时, 厘米级, 稍慢)</label></div>
|
|
84
|
+
<button id="conv-run">转换</button>
|
|
85
|
+
<button class="secondary" id="conv-example">填入示例</button>
|
|
86
|
+
<label for="conv-out">结果</label>
|
|
87
|
+
<textarea id="conv-out" readonly></textarea>
|
|
88
|
+
<div class="result" id="conv-msg"></div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="card">
|
|
92
|
+
<h2>经纬度 ⇄ 度分秒</h2>
|
|
93
|
+
<div class="grid2">
|
|
94
|
+
<div>
|
|
95
|
+
<label for="dms-in">十进制度 (如 38.65777,104.08296)</label>
|
|
96
|
+
<input type="text" id="dms-in" value="38.65777,104.08296">
|
|
97
|
+
<button id="dms-run">转度分秒</button>
|
|
98
|
+
<div class="result" id="dms-out"></div>
|
|
99
|
+
</div>
|
|
100
|
+
<div>
|
|
101
|
+
<label for="undms-in">度分秒 (如 38°39′27.97″,104°4′58.66″)</label>
|
|
102
|
+
<input type="text" id="undms-in" value='38°39′27.97″,104°4′58.66″'>
|
|
103
|
+
<button id="undms-run">转十进制度</button>
|
|
104
|
+
<div class="result" id="undms-out"></div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="card">
|
|
110
|
+
<h2>距离 / 方位角 / 偏移点</h2>
|
|
111
|
+
<p class="desc">两点需在同一坐标系下; 方位角以正北为 0°, 顺时针 0-360°。</p>
|
|
112
|
+
<div class="row">
|
|
113
|
+
<div><label for="p1">起点 (经度,纬度)</label><input type="text" id="p1" value="116.397428,39.90923"></div>
|
|
114
|
+
<div><label for="p2">终点 (经度,纬度)</label><input type="text" id="p2" value="121.473701,31.230416"></div>
|
|
115
|
+
</div>
|
|
116
|
+
<button id="geo-run">计算距离与方位角</button>
|
|
117
|
+
<div class="result" id="geo-out"></div>
|
|
118
|
+
<div class="row" style="margin-top:12px">
|
|
119
|
+
<div><label for="dp-bearing">方位角 (度)</label><input type="number" id="dp-bearing" value="153"></div>
|
|
120
|
+
<div><label for="dp-dist">距离 (米)</label><input type="number" id="dp-dist" value="100000"></div>
|
|
121
|
+
</div>
|
|
122
|
+
<button id="dp-run">从起点偏移</button>
|
|
123
|
+
<div class="result" id="dp-out"></div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="card">
|
|
127
|
+
<h2>高斯-克吕格投影</h2>
|
|
128
|
+
<p class="desc">经纬度 ⇄ 高斯平面坐标 (米), 支持测绘常用椭球与 3°/6° 带号。</p>
|
|
129
|
+
<div class="row">
|
|
130
|
+
<div>
|
|
131
|
+
<label for="gk-ellipsoid">椭球</label>
|
|
132
|
+
<select id="gk-ellipsoid">
|
|
133
|
+
<option value="cgcs2000">CGCS2000</option>
|
|
134
|
+
<option value="wgs84">WGS-84</option>
|
|
135
|
+
<option value="krassovsky">北京54 (克拉索夫斯基)</option>
|
|
136
|
+
<option value="iag75">西安80 (IAG-75)</option>
|
|
137
|
+
</select>
|
|
138
|
+
</div>
|
|
139
|
+
<div>
|
|
140
|
+
<label for="gk-zone">投影带</label>
|
|
141
|
+
<select id="gk-zone"><option value="6">6° 带</option><option value="3">3° 带</option></select>
|
|
142
|
+
</div>
|
|
143
|
+
<div><label for="gk-cm">中央经线 (度, 反算必填)</label><input type="number" id="gk-cm" value="120"></div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="row">
|
|
146
|
+
<div><label for="gk-lnglat">经纬度 (经度,纬度)</label><input type="text" id="gk-lnglat" value="121.47,31.24"></div>
|
|
147
|
+
<div><label for="gk-xy">平面坐标 (东坐标,北坐标)</label><input type="text" id="gk-xy" placeholder="640045.58,3458515.34"></div>
|
|
148
|
+
</div>
|
|
149
|
+
<button id="gk-run">正算 (左 → 右)</button>
|
|
150
|
+
<button class="secondary" id="invgk-run">反算 (右 → 左)</button>
|
|
151
|
+
<div class="result" id="gk-out"></div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="card">
|
|
155
|
+
<h2>坐标系速查</h2>
|
|
156
|
+
<table class="ref">
|
|
157
|
+
<tr><td>WGS-84</td><td>国际标准, GPS/北斗芯片原始坐标 — iOS 原生定位, OSM, 海外谷歌地图</td></tr>
|
|
158
|
+
<tr><td>GCJ-02</td><td>国测局火星坐标, WGS-84 加密所得 — 高德, 腾讯, 谷歌中国</td></tr>
|
|
159
|
+
<tr><td>BD-09</td><td>百度标准, GCJ-02 再次加密所得 — 百度地图, 百度 SDK</td></tr>
|
|
160
|
+
</table>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
</main>
|
|
164
|
+
<footer>
|
|
165
|
+
本页为 <a href="https://github.com/cheenwe/coordconver">coordconver</a> 的前端试用版 ·
|
|
166
|
+
地址⇄坐标的地理编码 (百度/高德/腾讯 API) 请在 Ruby/CLI 中使用 ·
|
|
167
|
+
<a href="https://rubygems.org/gems/coordconver">gem install coordconver</a>
|
|
168
|
+
</footer>
|
|
169
|
+
|
|
170
|
+
<script src="cc-algorithms.js"></script>
|
|
171
|
+
<script>
|
|
172
|
+
(function () {
|
|
173
|
+
"use strict";
|
|
174
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
175
|
+
var cc = window.CC;
|
|
176
|
+
|
|
177
|
+
function parsePair(text) {
|
|
178
|
+
var nums = String(text).split(/[,\s]+/).filter(Boolean).map(Number);
|
|
179
|
+
if (nums.length < 2 || nums.some(isNaN)) throw new Error("坐标格式应为: 经度,纬度");
|
|
180
|
+
return nums;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function show(id, text, isError) {
|
|
184
|
+
var el = $(id);
|
|
185
|
+
el.textContent = text;
|
|
186
|
+
el.className = "result " + (isError ? "err" : "ok");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function fmt(p) { return p[0].toFixed(6) + "," + p[1].toFixed(6); }
|
|
190
|
+
|
|
191
|
+
// ---------- 坐标系转换 ----------
|
|
192
|
+
var CONV = {
|
|
193
|
+
wgs: { gcj: cc.wgs2gcj, bd: cc.wgs2bd },
|
|
194
|
+
gcj: { wgs: function (l, a, precise) { return precise ? cc.gcj2wgsPrecise(l, a) : cc.gcj2wgs(l, a); }, bd: cc.gcj2bd },
|
|
195
|
+
bd: { wgs: function (l, a, precise) { return precise ? cc.bd2wgsPrecise(l, a) : cc.bd2wgs(l, a); }, gcj: cc.bd2gcj }
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
$("conv-run").onclick = function () {
|
|
199
|
+
try {
|
|
200
|
+
var from = $("from").value, to = $("to").value;
|
|
201
|
+
if (from === to) throw new Error("源坐标系与目标坐标系相同");
|
|
202
|
+
var fn = CONV[from][to];
|
|
203
|
+
var precise = $("precise").checked;
|
|
204
|
+
var lines = $("conv-in").value.split("\n").filter(function (l) { return l.trim(); });
|
|
205
|
+
if (!lines.length) throw new Error("请输入坐标");
|
|
206
|
+
var out = lines.map(function (line) {
|
|
207
|
+
var p = parsePair(line);
|
|
208
|
+
return fmt(fn(p[0], p[1], precise));
|
|
209
|
+
}).join("\n");
|
|
210
|
+
$("conv-out").value = out;
|
|
211
|
+
show("conv-msg", "已转换 " + lines.length + " 个点");
|
|
212
|
+
} catch (e) {
|
|
213
|
+
show("conv-msg", "错误: " + e.message, true);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
$("conv-example").onclick = function () {
|
|
217
|
+
$("conv-in").value = "121.468738,31.247138\n116.481499,39.990475\n104.08296,30.659462";
|
|
218
|
+
show("conv-msg", "示例已填入 (WGS-84 坐标), 点击\"转换\"试试");
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// ---------- 度分秒 ----------
|
|
222
|
+
$("dms-run").onclick = function () {
|
|
223
|
+
try {
|
|
224
|
+
var p = parsePair($("dms-in").value);
|
|
225
|
+
show("dms-out", cc.toDMS(p[0]) + "," + cc.toDMS(p[1]));
|
|
226
|
+
} catch (e) { show("dms-out", "错误: " + e.message, true); }
|
|
227
|
+
};
|
|
228
|
+
$("undms-run").onclick = function () {
|
|
229
|
+
try {
|
|
230
|
+
var parts = $("undms-in").value.split(/[,\s]+/).filter(Boolean);
|
|
231
|
+
if (parts.length < 2) throw new Error("需要两个度分秒值");
|
|
232
|
+
show("undms-out", cc.fromDMS(parts[0]) + "," + cc.fromDMS(parts[1]));
|
|
233
|
+
} catch (e) { show("undms-out", "错误: " + e.message, true); }
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// ---------- 距离/方位角/偏移 ----------
|
|
237
|
+
$("geo-run").onclick = function () {
|
|
238
|
+
try {
|
|
239
|
+
var a = parsePair($("p1").value), b = parsePair($("p2").value);
|
|
240
|
+
show("geo-out",
|
|
241
|
+
"距离: " + cc.distance(a[0], a[1], b[0], b[1]).toLocaleString() + " 米\n" +
|
|
242
|
+
"方位角: " + cc.bearing(a[0], a[1], b[0], b[1]).toFixed(2) + "°");
|
|
243
|
+
} catch (e) { show("geo-out", "错误: " + e.message, true); }
|
|
244
|
+
};
|
|
245
|
+
$("dp-run").onclick = function () {
|
|
246
|
+
try {
|
|
247
|
+
var a = parsePair($("p1").value);
|
|
248
|
+
var b = cc.destinationPoint(a[0], a[1], Number($("dp-bearing").value), Number($("dp-dist").value));
|
|
249
|
+
show("dp-out", "偏移点: " + fmt(b));
|
|
250
|
+
} catch (e) { show("dp-out", "错误: " + e.message, true); }
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// ---------- 高斯-克吕格 ----------
|
|
254
|
+
function gkOpts() {
|
|
255
|
+
return {
|
|
256
|
+
ellipsoid: $("gk-ellipsoid").value,
|
|
257
|
+
zoneWidth: Number($("gk-zone").value),
|
|
258
|
+
centralMeridian: $("gk-cm").value === "" ? null : Number($("gk-cm").value)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
$("gk-run").onclick = function () {
|
|
262
|
+
try {
|
|
263
|
+
var p = parsePair($("gk-lnglat").value);
|
|
264
|
+
var xy = cc.gaussKruger(p[0], p[1], gkOpts());
|
|
265
|
+
$("gk-xy").value = xy[0] + "," + xy[1];
|
|
266
|
+
var zone = cc.zoneMeridian(p[0], gkOpts().zoneWidth);
|
|
267
|
+
show("gk-out", "带号 " + zone[0] + " · 中央经线 " + zone[1] + "° · 东坐标 " + xy[0] + " · 北坐标 " + xy[1]);
|
|
268
|
+
} catch (e) { show("gk-out", "错误: " + e.message, true); }
|
|
269
|
+
};
|
|
270
|
+
$("invgk-run").onclick = function () {
|
|
271
|
+
try {
|
|
272
|
+
var xy = parsePair($("gk-xy").value);
|
|
273
|
+
var opts = gkOpts();
|
|
274
|
+
if (opts.centralMeridian === null) throw new Error("反算必须填写中央经线");
|
|
275
|
+
var ll = cc.inverseGaussKruger(xy[0], xy[1], opts);
|
|
276
|
+
$("gk-lnglat").value = ll[0] + "," + ll[1];
|
|
277
|
+
show("gk-out", "经度 " + ll[0] + " · 纬度 " + ll[1]);
|
|
278
|
+
} catch (e) { show("gk-out", "错误: " + e.message, true); }
|
|
279
|
+
};
|
|
280
|
+
})();
|
|
281
|
+
</script>
|
|
282
|
+
</body>
|
|
283
|
+
</html>
|