geo_calc 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +20 -0
- data/README.textile +181 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/geo_calc.gemspec +74 -0
- data/lib/geo_calc/calculations.rb +333 -0
- data/lib/geo_calc/core_ext.rb +228 -0
- data/lib/geo_calc/geo.rb +170 -0
- data/lib/geo_calc/geo_point.rb +103 -0
- data/lib/geo_calc/js/geo_calc.js +551 -0
- data/lib/geo_calc.rb +1 -0
- data/spec/geo_calc/calculations_spec.rb +174 -0
- data/spec/geo_calc/core_ext_spec.rb +272 -0
- data/spec/geo_calc/geo_point_spec.rb +228 -0
- data/spec/geo_calc/geo_spec.rb +99 -0
- data/spec/spec_helper.rb +12 -0
- metadata +118 -0
@@ -0,0 +1,551 @@
|
|
1
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
2
|
+
/* Latitude/longitude spherical geodesy formulae & scripts (c) Chris Veness 2002-2010 */
|
3
|
+
/* - www.movable-type.co.uk/scripts/latlong.html */
|
4
|
+
/* */
|
5
|
+
/* Sample usage: */
|
6
|
+
/* var p1 = new LatLon(51.5136, -0.0983); */
|
7
|
+
/* var p2 = new LatLon(51.4778, -0.0015); */
|
8
|
+
/* var dist = p1.distanceTo(p2); // in km */
|
9
|
+
/* var brng = p1.bearingTo(p2); // in degrees clockwise from north */
|
10
|
+
/* ... etc */
|
11
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
12
|
+
|
13
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
14
|
+
/* Note that minimal error checking is performed in this example code! */
|
15
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
16
|
+
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Creates a point on the earth's surface at the supplied latitude / longitude
|
20
|
+
*
|
21
|
+
* @constructor
|
22
|
+
* @param {Number} lat: latitude in numeric degrees
|
23
|
+
* @param {Number} lon: longitude in numeric degrees
|
24
|
+
* @param {Number} [rad=6371]: radius of earth if different value is required from standard 6,371km
|
25
|
+
*/
|
26
|
+
function LatLon(lat, lon, rad) {
|
27
|
+
if (typeof(rad) == 'undefined') rad = 6371; // earth's mean radius in km
|
28
|
+
// only accept numbers or valid numeric strings
|
29
|
+
this._lat = typeof(lat)=='number' ? lat : typeof(lat)=='string' && lat.trim()!='' ? +lat : NaN;
|
30
|
+
this._lon = typeof(lat)=='number' ? lon : typeof(lon)=='string' && lon.trim()!='' ? +lon : NaN;
|
31
|
+
this._radius = typeof(rad)=='number' ? rad : typeof(rad)=='string' && trim(lon)!='' ? +rad : NaN;
|
32
|
+
}
|
33
|
+
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Returns the distance from this point to the supplied point, in km
|
37
|
+
* (using Haversine formula)
|
38
|
+
*
|
39
|
+
* from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
|
40
|
+
* Sky and Telescope, vol 68, no 2, 1984
|
41
|
+
*
|
42
|
+
* @param {LatLon} point: Latitude/longitude of destination point
|
43
|
+
* @param {Number} [precision=4]: no of significant digits to use for returned value
|
44
|
+
* @returns {Number} Distance in km between this point and destination point
|
45
|
+
*/
|
46
|
+
LatLon.prototype.distanceTo = function(point, precision) {
|
47
|
+
// default 4 sig figs reflects typical 0.3% accuracy of spherical model
|
48
|
+
if (typeof precision == 'undefined') precision = 4;
|
49
|
+
|
50
|
+
var R = this._radius;
|
51
|
+
var lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
|
52
|
+
var lat2 = point._lat.toRad(), lon2 = point._lon.toRad();
|
53
|
+
var dLat = lat2 - lat1;
|
54
|
+
var dLon = lon2 - lon1;
|
55
|
+
|
56
|
+
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
57
|
+
Math.cos(lat1) * Math.cos(lat2) *
|
58
|
+
Math.sin(dLon/2) * Math.sin(dLon/2);
|
59
|
+
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
60
|
+
var d = R * c;
|
61
|
+
return d.toPrecisionFixed(precision);
|
62
|
+
}
|
63
|
+
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Returns the (initial) bearing from this point to the supplied point, in degrees
|
67
|
+
* see http://williams.best.vwh.net/avform.htm#Crs
|
68
|
+
*
|
69
|
+
* @param {LatLon} point: Latitude/longitude of destination point
|
70
|
+
* @returns {Number} Initial bearing in degrees from North
|
71
|
+
*/
|
72
|
+
LatLon.prototype.bearingTo = function(point) {
|
73
|
+
var lat1 = this._lat.toRad(), lat2 = point._lat.toRad();
|
74
|
+
var dLon = (point._lon-this._lon).toRad();
|
75
|
+
|
76
|
+
var y = Math.sin(dLon) * Math.cos(lat2);
|
77
|
+
var x = Math.cos(lat1)*Math.sin(lat2) -
|
78
|
+
Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
|
79
|
+
var brng = Math.atan2(y, x);
|
80
|
+
|
81
|
+
return (brng.toDeg()+360) % 360;
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
/**
|
86
|
+
* Returns final bearing arriving at supplied destination point from this point; the final bearing
|
87
|
+
* will differ from the initial bearing by varying degrees according to distance and latitude
|
88
|
+
*
|
89
|
+
* @param {LatLon} point: Latitude/longitude of destination point
|
90
|
+
* @returns {Number} Final bearing in degrees from North
|
91
|
+
*/
|
92
|
+
LatLon.prototype.finalBearingTo = function(point) {
|
93
|
+
// get initial bearing from supplied point back to this point...
|
94
|
+
var lat1 = point._lat.toRad(), lat2 = this._lat.toRad();
|
95
|
+
var dLon = (this._lon-point._lon).toRad();
|
96
|
+
|
97
|
+
var y = Math.sin(dLon) * Math.cos(lat2);
|
98
|
+
var x = Math.cos(lat1)*Math.sin(lat2) -
|
99
|
+
Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
|
100
|
+
var brng = Math.atan2(y, x);
|
101
|
+
|
102
|
+
// ... & reverse it by adding 180°
|
103
|
+
return (brng.toDeg()+180) % 360;
|
104
|
+
}
|
105
|
+
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Returns the midpoint between this point and the supplied point.
|
109
|
+
* see http://mathforum.org/library/drmath/view/51822.html for derivation
|
110
|
+
*
|
111
|
+
* @param {LatLon} point: Latitude/longitude of destination point
|
112
|
+
* @returns {LatLon} Midpoint between this point and the supplied point
|
113
|
+
*/
|
114
|
+
LatLon.prototype.midpointTo = function(point) {
|
115
|
+
lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
|
116
|
+
lat2 = point._lat.toRad();
|
117
|
+
var dLon = (point._lon-this._lon).toRad();
|
118
|
+
|
119
|
+
var Bx = Math.cos(lat2) * Math.cos(dLon);
|
120
|
+
var By = Math.cos(lat2) * Math.sin(dLon);
|
121
|
+
|
122
|
+
lat3 = Math.atan2(Math.sin(lat1)+Math.sin(lat2),
|
123
|
+
Math.sqrt( (Math.cos(lat1)+Bx)*(Math.cos(lat1)+Bx) + By*By) );
|
124
|
+
lon3 = lon1 + Math.atan2(By, Math.cos(lat1) + Bx);
|
125
|
+
|
126
|
+
return new LatLon(lat3.toDeg(), lon3.toDeg());
|
127
|
+
}
|
128
|
+
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Returns the destination point from this point having travelled the given distance (in km) on the
|
132
|
+
* given initial bearing (bearing may vary before destination is reached)
|
133
|
+
*
|
134
|
+
* see http://williams.best.vwh.net/avform.htm#LL
|
135
|
+
*
|
136
|
+
* @param {Number} brng: Initial bearing in degrees
|
137
|
+
* @param {Number} dist: Distance in km
|
138
|
+
* @returns {LatLon} Destination point
|
139
|
+
*/
|
140
|
+
LatLon.prototype.destinationPoint = function(brng, dist) {
|
141
|
+
dist = typeof(dist)=='number' ? dist : typeof(dist)=='string' && dist.trim()!='' ? +dist : NaN;
|
142
|
+
dist = dist/this._radius; // convert dist to angular distance in radians
|
143
|
+
brng = brng.toRad(); //
|
144
|
+
var lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
|
145
|
+
|
146
|
+
var lat2 = Math.asin( Math.sin(lat1)*Math.cos(dist) +
|
147
|
+
Math.cos(lat1)*Math.sin(dist)*Math.cos(brng) );
|
148
|
+
var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1),
|
149
|
+
Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2));
|
150
|
+
lon2 = (lon2+3*Math.PI)%(2*Math.PI) - Math.PI; // normalise to -180...+180
|
151
|
+
|
152
|
+
return new LatLon(lat2.toDeg(), lon2.toDeg());
|
153
|
+
}
|
154
|
+
|
155
|
+
|
156
|
+
/**
|
157
|
+
* Returns the point of intersection of two paths defined by point and bearing
|
158
|
+
*
|
159
|
+
* see http://williams.best.vwh.net/avform.htm#Intersection
|
160
|
+
*
|
161
|
+
* @param {LatLon} p1: First point
|
162
|
+
* @param {Number} brng1: Initial bearing from first point
|
163
|
+
* @param {LatLon} p2: Second point
|
164
|
+
* @param {Number} brng2: Initial bearing from second point
|
165
|
+
* @returns {LatLon} Destination point (null if no unique intersection defined)
|
166
|
+
*/
|
167
|
+
LatLon.intersection = function(p1, brng1, p2, brng2) {
|
168
|
+
brng1 = typeof brng1 == 'number' ? brng1 : typeof brng1 == 'string' && trim(brng1)!='' ? +brng1 : NaN;
|
169
|
+
brng2 = typeof brng2 == 'number' ? brng2 : typeof brng2 == 'string' && trim(brng2)!='' ? +brng2 : NaN;
|
170
|
+
lat1 = p1._lat.toRad(), lon1 = p1._lon.toRad();
|
171
|
+
lat2 = p2._lat.toRad(), lon2 = p2._lon.toRad();
|
172
|
+
brng13 = brng1.toRad(), brng23 = brng2.toRad();
|
173
|
+
dLat = lat2-lat1, dLon = lon2-lon1;
|
174
|
+
|
175
|
+
dist12 = 2*Math.asin( Math.sqrt( Math.sin(dLat/2)*Math.sin(dLat/2) +
|
176
|
+
Math.cos(lat1)*Math.cos(lat2)*Math.sin(dLon/2)*Math.sin(dLon/2) ) );
|
177
|
+
if (dist12 == 0) return null;
|
178
|
+
|
179
|
+
// initial/final bearings between points
|
180
|
+
brngA = Math.acos( ( Math.sin(lat2) - Math.sin(lat1)*Math.cos(dist12) ) /
|
181
|
+
( Math.sin(dist12)*Math.cos(lat1) ) );
|
182
|
+
if (isNaN(brngA)) brngA = 0; // protect against rounding
|
183
|
+
brngB = Math.acos( ( Math.sin(lat1) - Math.sin(lat2)*Math.cos(dist12) ) /
|
184
|
+
( Math.sin(dist12)*Math.cos(lat2) ) );
|
185
|
+
|
186
|
+
if (Math.sin(lon2-lon1) > 0) {
|
187
|
+
brng12 = brngA;
|
188
|
+
brng21 = 2*Math.PI - brngB;
|
189
|
+
} else {
|
190
|
+
brng12 = 2*Math.PI - brngA;
|
191
|
+
brng21 = brngB;
|
192
|
+
}
|
193
|
+
|
194
|
+
alpha1 = (brng13 - brng12 + Math.PI) % (2*Math.PI) - Math.PI; // angle 2-1-3
|
195
|
+
alpha2 = (brng21 - brng23 + Math.PI) % (2*Math.PI) - Math.PI; // angle 1-2-3
|
196
|
+
|
197
|
+
if (Math.sin(alpha1)==0 && Math.sin(alpha2)==0) return null; // infinite intersections
|
198
|
+
if (Math.sin(alpha1)*Math.sin(alpha2) < 0) return null; // ambiguous intersection
|
199
|
+
|
200
|
+
//alpha1 = Math.abs(alpha1);
|
201
|
+
//alpha2 = Math.abs(alpha2);
|
202
|
+
// ... Ed Williams takes abs of alpha1/alpha2, but seems to break calculation?
|
203
|
+
|
204
|
+
alpha3 = Math.acos( -Math.cos(alpha1)*Math.cos(alpha2) +
|
205
|
+
Math.sin(alpha1)*Math.sin(alpha2)*Math.cos(dist12) );
|
206
|
+
dist13 = Math.atan2( Math.sin(dist12)*Math.sin(alpha1)*Math.sin(alpha2),
|
207
|
+
Math.cos(alpha2)+Math.cos(alpha1)*Math.cos(alpha3) )
|
208
|
+
lat3 = Math.asin( Math.sin(lat1)*Math.cos(dist13) +
|
209
|
+
Math.cos(lat1)*Math.sin(dist13)*Math.cos(brng13) );
|
210
|
+
dLon13 = Math.atan2( Math.sin(brng13)*Math.sin(dist13)*Math.cos(lat1),
|
211
|
+
Math.cos(dist13)-Math.sin(lat1)*Math.sin(lat3) );
|
212
|
+
lon3 = lon1+dLon13;
|
213
|
+
lon3 = (lon3+Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..180º
|
214
|
+
|
215
|
+
return new LatLon(lat3.toDeg(), lon3.toDeg());
|
216
|
+
}
|
217
|
+
|
218
|
+
|
219
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
220
|
+
|
221
|
+
/**
|
222
|
+
* Returns the distance from this point to the supplied point, in km, travelling along a rhumb line
|
223
|
+
*
|
224
|
+
* see http://williams.best.vwh.net/avform.htm#Rhumb
|
225
|
+
*
|
226
|
+
* @param {LatLon} point: Latitude/longitude of destination point
|
227
|
+
* @returns {Number} Distance in km between this point and destination point
|
228
|
+
*/
|
229
|
+
LatLon.prototype.rhumbDistanceTo = function(point) {
|
230
|
+
var R = this._radius;
|
231
|
+
var lat1 = this._lat.toRad(), lat2 = point._lat.toRad();
|
232
|
+
var dLat = (point._lat-this._lat).toRad();
|
233
|
+
var dLon = Math.abs(point._lon-this._lon).toRad();
|
234
|
+
|
235
|
+
var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
|
236
|
+
var q = (!isNaN(dLat/dPhi)) ? dLat/dPhi : Math.cos(lat1); // E-W line gives dPhi=0
|
237
|
+
// if dLon over 180° take shorter rhumb across 180° meridian:
|
238
|
+
if (dLon > Math.PI) dLon = 2*Math.PI - dLon;
|
239
|
+
var dist = Math.sqrt(dLat*dLat + q*q*dLon*dLon) * R;
|
240
|
+
|
241
|
+
return dist.toPrecisionFixed(4); // 4 sig figs reflects typical 0.3% accuracy of spherical model
|
242
|
+
}
|
243
|
+
|
244
|
+
/**
|
245
|
+
* Returns the bearing from this point to the supplied point along a rhumb line, in degrees
|
246
|
+
*
|
247
|
+
* @param {LatLon} point: Latitude/longitude of destination point
|
248
|
+
* @returns {Number} Bearing in degrees from North
|
249
|
+
*/
|
250
|
+
LatLon.prototype.rhumbBearingTo = function(point) {
|
251
|
+
var lat1 = this._lat.toRad(), lat2 = point._lat.toRad();
|
252
|
+
var dLon = (point._lon-this._lon).toRad();
|
253
|
+
|
254
|
+
var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
|
255
|
+
if (Math.abs(dLon) > Math.PI) dLon = dLon>0 ? -(2*Math.PI-dLon) : (2*Math.PI+dLon);
|
256
|
+
var brng = Math.atan2(dLon, dPhi);
|
257
|
+
|
258
|
+
return (brng.toDeg()+360) % 360;
|
259
|
+
}
|
260
|
+
|
261
|
+
/**
|
262
|
+
* Returns the destination point from this point having travelled the given distance (in km) on the
|
263
|
+
* given bearing along a rhumb line
|
264
|
+
*
|
265
|
+
* @param {Number} brng: Bearing in degrees from North
|
266
|
+
* @param {Number} dist: Distance in km
|
267
|
+
* @returns {LatLon} Destination point
|
268
|
+
*/
|
269
|
+
LatLon.prototype.rhumbDestinationPoint = function(brng, dist) {
|
270
|
+
var R = this._radius;
|
271
|
+
var d = parseFloat(dist)/R; // d = angular distance covered on earth's surface
|
272
|
+
var lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
|
273
|
+
brng = brng.toRad();
|
274
|
+
|
275
|
+
var lat2 = lat1 + d*Math.cos(brng);
|
276
|
+
var dLat = lat2-lat1;
|
277
|
+
var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
|
278
|
+
var q = (!isNaN(dLat/dPhi)) ? dLat/dPhi : Math.cos(lat1); // E-W line gives dPhi=0
|
279
|
+
var dLon = d*Math.sin(brng)/q;
|
280
|
+
// check for some daft bugger going past the pole
|
281
|
+
if (Math.abs(lat2) > Math.PI/2) lat2 = lat2>0 ? Math.PI-lat2 : -(Math.PI-lat2);
|
282
|
+
lon2 = (lon1+dLon+3*Math.PI)%(2*Math.PI) - Math.PI;
|
283
|
+
|
284
|
+
return new LatLon(lat2.toDeg(), lon2.toDeg());
|
285
|
+
}
|
286
|
+
|
287
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
288
|
+
|
289
|
+
|
290
|
+
/**
|
291
|
+
* Returns the latitude of this point; signed numeric degrees if no format, otherwise format & dp
|
292
|
+
* as per Geo.toLat()
|
293
|
+
*
|
294
|
+
* @param {String} [format]: Return value as 'd', 'dm', 'dms'
|
295
|
+
* @param {Number} [dp=0|2|4]: No of decimal places to display
|
296
|
+
* @returns {Number|String} Numeric degrees if no format specified, otherwise deg/min/sec
|
297
|
+
*
|
298
|
+
* @requires Geo
|
299
|
+
*/
|
300
|
+
LatLon.prototype.lat = function(format, dp) {
|
301
|
+
if (typeof format == 'undefined') return this._lat;
|
302
|
+
|
303
|
+
return Geo.toLat(this._lat, format, dp);
|
304
|
+
}
|
305
|
+
|
306
|
+
/**
|
307
|
+
* Returns the longitude of this point; signed numeric degrees if no format, otherwise format & dp
|
308
|
+
* as per Geo.toLon()
|
309
|
+
*
|
310
|
+
* @param {String} [format]: Return value as 'd', 'dm', 'dms'
|
311
|
+
* @param {Number} [dp=0|2|4]: No of decimal places to display
|
312
|
+
* @returns {Number|String} Numeric degrees if no format specified, otherwise deg/min/sec
|
313
|
+
*
|
314
|
+
* @requires Geo
|
315
|
+
*/
|
316
|
+
LatLon.prototype.lon = function(format, dp) {
|
317
|
+
if (typeof format == 'undefined') return this._lon;
|
318
|
+
|
319
|
+
return Geo.toLon(this._lon, format, dp);
|
320
|
+
}
|
321
|
+
|
322
|
+
/**
|
323
|
+
* Returns a string representation of this point; format and dp as per lat()/lon()
|
324
|
+
*
|
325
|
+
* @param {String} [format]: Return value as 'd', 'dm', 'dms'
|
326
|
+
* @param {Number} [dp=0|2|4]: No of decimal places to display
|
327
|
+
* @returns {String} Comma-separated latitude/longitude
|
328
|
+
*
|
329
|
+
* @requires Geo
|
330
|
+
*/
|
331
|
+
LatLon.prototype.toString = function(format, dp) {
|
332
|
+
if (typeof format == 'undefined') format = 'dms';
|
333
|
+
|
334
|
+
if (isNaN(this._lat) || isNaN(this._lon)) return '-,-';
|
335
|
+
|
336
|
+
return Geo.toLat(this._lat, format, dp) + ', ' + Geo.toLon(this._lon, format, dp);
|
337
|
+
}
|
338
|
+
|
339
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
340
|
+
|
341
|
+
// ---- extend Number object with methods for converting degrees/radians
|
342
|
+
|
343
|
+
/** Converts numeric degrees to radians */
|
344
|
+
if (typeof(Number.prototype.toRad) === "undefined") {
|
345
|
+
Number.prototype.toRad = function() {
|
346
|
+
return this * Math.PI / 180;
|
347
|
+
}
|
348
|
+
}
|
349
|
+
|
350
|
+
/** Converts radians to numeric (signed) degrees */
|
351
|
+
if (typeof(Number.prototype.toDeg) === "undefined") {
|
352
|
+
Number.prototype.toDeg = function() {
|
353
|
+
return this * 180 / Math.PI;
|
354
|
+
}
|
355
|
+
}
|
356
|
+
|
357
|
+
/**
|
358
|
+
* Formats the significant digits of a number, using only fixed-point notation (no exponential)
|
359
|
+
*
|
360
|
+
* @param {Number} precision: Number of significant digits to appear in the returned string
|
361
|
+
* @returns {String} A string representation of number which contains precision significant digits
|
362
|
+
*/
|
363
|
+
if (typeof(Number.prototype.toPrecisionFixed) === "undefined") {
|
364
|
+
Number.prototype.toPrecisionFixed = function(precision) {
|
365
|
+
if (isNaN(this)) return 'NaN';
|
366
|
+
var numb = this < 0 ? -this : this; // can't take log of -ve number...
|
367
|
+
var sign = this < 0 ? '-' : '';
|
368
|
+
|
369
|
+
if (numb == 0) { n = '0.'; while (precision--) n += '0'; return n }; // can't take log of zero
|
370
|
+
|
371
|
+
var scale = Math.ceil(Math.log(numb)*Math.LOG10E); // no of digits before decimal
|
372
|
+
var n = String(Math.round(numb * Math.pow(10, precision-scale)));
|
373
|
+
if (scale > 0) { // add trailing zeros & insert decimal as required
|
374
|
+
l = scale - n.length;
|
375
|
+
while (l-- > 0) n = n + '0';
|
376
|
+
if (scale < n.length) n = n.slice(0,scale) + '.' + n.slice(scale);
|
377
|
+
} else { // prefix decimal and leading zeros if required
|
378
|
+
while (scale++ < 0) n = '0' + n;
|
379
|
+
n = '0.' + n;
|
380
|
+
}
|
381
|
+
return sign + n;
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
/** Trims whitespace from string (q.v. blog.stevenlevithan.com/archives/faster-trim-javascript) */
|
386
|
+
if (typeof(String.prototype.trim) === "undefined") {
|
387
|
+
String.prototype.trim = function() {
|
388
|
+
return String(this).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
|
389
|
+
}
|
390
|
+
}
|
391
|
+
|
392
|
+
|
393
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
394
|
+
/* Geodesy representation conversion functions (c) Chris Veness 2002-2010 */
|
395
|
+
/* - www.movable-type.co.uk/scripts/latlong.html */
|
396
|
+
/* */
|
397
|
+
/* Sample usage: */
|
398
|
+
/* var lat = Geo.parseDMS('51° 28′ 40.12″ N'); */
|
399
|
+
/* var lon = Geo.parseDMS('000° 00′ 05.31″ W'); */
|
400
|
+
/* var p1 = new LatLon(lat, lon); */
|
401
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
402
|
+
|
403
|
+
var Geo = {}; // Geo namespace, representing static class
|
404
|
+
|
405
|
+
/**
|
406
|
+
* Parses string representing degrees/minutes/seconds into numeric degrees
|
407
|
+
*
|
408
|
+
* This is very flexible on formats, allowing signed decimal degrees, or deg-min-sec optionally
|
409
|
+
* suffixed by compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W)
|
410
|
+
* or fixed-width format without separators (eg 0033709W). Seconds and minutes may be omitted.
|
411
|
+
* (Note minimal validation is done).
|
412
|
+
*
|
413
|
+
* @param {String|Number} dmsStr: Degrees or deg/min/sec in variety of formats
|
414
|
+
* @returns {Number} Degrees as decimal number
|
415
|
+
* @throws {TypeError} dmsStr is an object, perhaps DOM object without .value?
|
416
|
+
*/
|
417
|
+
Geo.parseDMS = function(dmsStr) {
|
418
|
+
if (typeof deg == 'object') throw new TypeError('Geo.parseDMS - dmsStr is [DOM?] object');
|
419
|
+
|
420
|
+
// check for signed decimal degrees without NSEW, if so return it directly
|
421
|
+
if (typeof dmsStr === 'number' && isFinite(dmsStr)) return Number(dmsStr);
|
422
|
+
|
423
|
+
// strip off any sign or compass dir'n & split out separate d/m/s
|
424
|
+
var dms = String(dmsStr).trim().replace(/^-/,'').replace(/[NSEW]$/i,'').split(/[^0-9.,]+/);
|
425
|
+
if (dms[dms.length-1]=='') dms.splice(dms.length-1); // from trailing symbol
|
426
|
+
|
427
|
+
if (dms == '') return NaN;
|
428
|
+
|
429
|
+
// and convert to decimal degrees...
|
430
|
+
switch (dms.length) {
|
431
|
+
case 3: // interpret 3-part result as d/m/s
|
432
|
+
var deg = dms[0]/1 + dms[1]/60 + dms[2]/3600;
|
433
|
+
break;
|
434
|
+
case 2: // interpret 2-part result as d/m
|
435
|
+
var deg = dms[0]/1 + dms[1]/60;
|
436
|
+
break;
|
437
|
+
case 1: // just d (possibly decimal) or non-separated dddmmss
|
438
|
+
var deg = dms[0];
|
439
|
+
// check for fixed-width unseparated format eg 0033709W
|
440
|
+
if (/[NS]/i.test(dmsStr)) deg = '0' + deg; // - normalise N/S to 3-digit degrees
|
441
|
+
if (/[0-9]{7}/.test(deg)) deg = deg.slice(0,3)/1 + deg.slice(3,5)/60 + deg.slice(5)/3600;
|
442
|
+
break;
|
443
|
+
default:
|
444
|
+
return NaN;
|
445
|
+
}
|
446
|
+
if (/^-|[WS]$/i.test(dmsStr.trim())) deg = -deg; // take '-', west and south as -ve
|
447
|
+
return Number(deg);
|
448
|
+
}
|
449
|
+
|
450
|
+
/**
|
451
|
+
* Convert decimal degrees to deg/min/sec format
|
452
|
+
* - degree, prime, double-prime symbols are added, but sign is discarded, though no compass
|
453
|
+
* direction is added
|
454
|
+
*
|
455
|
+
* @private
|
456
|
+
* @param {Number} deg: Degrees
|
457
|
+
* @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
458
|
+
* @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
459
|
+
* @returns {String} deg formatted as deg/min/secs according to specified format
|
460
|
+
* @throws {TypeError} deg is an object, perhaps DOM object without .value?
|
461
|
+
*/
|
462
|
+
Geo.toDMS = function(deg, format, dp) {
|
463
|
+
if (typeof deg == 'object') throw new TypeError('Geo.toDMS - deg is [DOM?] object');
|
464
|
+
if (isNaN(deg)) return 'NaN'; // give up here if we can't make a number from deg
|
465
|
+
|
466
|
+
// default values
|
467
|
+
if (typeof format == 'undefined') format = 'dms';
|
468
|
+
if (typeof dp == 'undefined') {
|
469
|
+
switch (format) {
|
470
|
+
case 'd': dp = 4; break;
|
471
|
+
case 'dm': dp = 2; break;
|
472
|
+
case 'dms': dp = 0; break;
|
473
|
+
default: format = 'dms'; dp = 0; // be forgiving on invalid format
|
474
|
+
}
|
475
|
+
}
|
476
|
+
|
477
|
+
deg = Math.abs(deg); // (unsigned result ready for appending compass dir'n)
|
478
|
+
|
479
|
+
switch (format) {
|
480
|
+
case 'd':
|
481
|
+
d = deg.toFixed(dp); // round degrees
|
482
|
+
if (d<100) d = '0' + d; // pad with leading zeros
|
483
|
+
if (d<10) d = '0' + d;
|
484
|
+
dms = d + '\u00B0'; // add º symbol
|
485
|
+
break;
|
486
|
+
case 'dm':
|
487
|
+
var min = (deg*60).toFixed(dp); // convert degrees to minutes & round
|
488
|
+
var d = Math.floor(min / 60); // get component deg/min
|
489
|
+
var m = (min % 60).toFixed(dp); // pad with trailing zeros
|
490
|
+
if (d<100) d = '0' + d; // pad with leading zeros
|
491
|
+
if (d<10) d = '0' + d;
|
492
|
+
if (m<10) m = '0' + m;
|
493
|
+
dms = d + '\u00B0' + m + '\u2032'; // add º, ' symbols
|
494
|
+
break;
|
495
|
+
case 'dms':
|
496
|
+
var sec = (deg*3600).toFixed(dp); // convert degrees to seconds & round
|
497
|
+
var d = Math.floor(sec / 3600); // get component deg/min/sec
|
498
|
+
var m = Math.floor(sec/60) % 60;
|
499
|
+
var s = (sec % 60).toFixed(dp); // pad with trailing zeros
|
500
|
+
if (d<100) d = '0' + d; // pad with leading zeros
|
501
|
+
if (d<10) d = '0' + d;
|
502
|
+
if (m<10) m = '0' + m;
|
503
|
+
if (s<10) s = '0' + s;
|
504
|
+
dms = d + '\u00B0' + m + '\u2032' + s + '\u2033'; // add º, ', " symbols
|
505
|
+
break;
|
506
|
+
}
|
507
|
+
|
508
|
+
return dms;
|
509
|
+
}
|
510
|
+
|
511
|
+
/**
|
512
|
+
* Convert numeric degrees to deg/min/sec latitude (suffixed with N/S)
|
513
|
+
*
|
514
|
+
* @param {Number} deg: Degrees
|
515
|
+
* @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
516
|
+
* @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
517
|
+
* @returns {String} Deg/min/seconds
|
518
|
+
*/
|
519
|
+
Geo.toLat = function(deg, format, dp) {
|
520
|
+
var lat = Geo.toDMS(deg, format, dp);
|
521
|
+
return lat=='' ? '' : lat.slice(1) + (deg<0 ? 'S' : 'N'); // knock off initial '0' for lat!
|
522
|
+
}
|
523
|
+
|
524
|
+
/**
|
525
|
+
* Convert numeric degrees to deg/min/sec longitude (suffixed with E/W)
|
526
|
+
*
|
527
|
+
* @param {Number} deg: Degrees
|
528
|
+
* @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
529
|
+
* @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
530
|
+
* @returns {String} Deg/min/seconds
|
531
|
+
*/
|
532
|
+
Geo.toLon = function(deg, format, dp) {
|
533
|
+
var lon = Geo.toDMS(deg, format, dp);
|
534
|
+
return lon=='' ? '' : lon + (deg<0 ? 'W' : 'E');
|
535
|
+
}
|
536
|
+
|
537
|
+
/**
|
538
|
+
* Convert numeric degrees to deg/min/sec as a bearing (0º..360º)
|
539
|
+
*
|
540
|
+
* @param {Number} deg: Degrees
|
541
|
+
* @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
|
542
|
+
* @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
|
543
|
+
* @returns {String} Deg/min/seconds
|
544
|
+
*/
|
545
|
+
Geo.toBrng = function(deg, format, dp) {
|
546
|
+
deg = (Number(deg)+360) % 360; // normalise -ve values to 180º..360º
|
547
|
+
var brng = Geo.toDMS(deg, format, dp);
|
548
|
+
return brng.replace('360', '0'); // just in case rounding took us up to 360º!
|
549
|
+
}
|
550
|
+
|
551
|
+
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
data/lib/geo_calc.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'geo_calc/geo_point'
|