jspredict.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. // jspredict v1.0.3
  2. // Author: Roshan Jobanputra
  3. // https://github.com/nsat/jspredict
  4. // Changelog:
  5. // v1.0.3 (rosh93) - If we cant approximate our aos within max_iterations, return null and dont attempt to return a bad transit object. Fix a few jslint warnings
  6. // v1.0.2 (jotenko) - Added parameter 'maxTransits' to function 'transits' (allows the user to define a maximum number of transits to be calculated, for performance management)
  7. // v1.0.1 (nsat) - First release
  8. // Copyright (c) 2015, Spire Global Inc
  9. // All rights reserved.
  10. //
  11. // Redistribution and use in source and binary forms, with or without
  12. // modification, are permitted provided that the following conditions are met:
  13. // * Redistributions of source code must retain the above copyright
  14. // notice, this list of conditions and the following disclaimer.
  15. // * Redistributions in binary form must reproduce the above copyright
  16. // notice, this list of conditions and the following disclaimer in the
  17. // documentation and/or other materials provided with the distribution.
  18. // * Neither the name of the Spire Global Inc nor the
  19. // names of its contributors may be used to endorse or promote products
  20. // derived from this software without specific prior written permission.
  21. //
  22. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  23. // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  24. // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  25. // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
  26. // Spire Global Inc BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  27. // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  28. // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  29. // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  30. // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  31. // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  32. // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  33. // SUCH DAMAGE.
  34. // Based on:
  35. // PREDICT: http://www.qsl.net/kd2bd/predict.html
  36. // PyPredict: https://github.com/nsat/pypredict
  37. // Python-SGP4: https://github.com/brandon-rhodes/python-sgp4
  38. // Depends on:
  39. // Satellite.js: https://github.com/shashwatak/satellite-js
  40. // Moment.js: https://github.com/moment/moment
  41. // API
  42. // jspredict
  43. //
  44. // Inputs:
  45. // tle = 3 line string
  46. // qth = 3 element array [latitude (degrees), longitude (degrees), altitude (km)]
  47. // time/start/end = unix timestamp (ms) or date object (new Date())
  48. // observe(tle 'required', qth 'optional', time 'optional')
  49. //
  50. // observes(tle 'required', qth 'optional', start 'optional', end 'required', interval 'optional')
  51. //
  52. // transits(tle 'required', qth 'required', start 'optional', end 'required', minElevation 'optional')
  53. // transit
  54. (function (global, factory) {
  55. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  56. typeof define === 'function' && define.amd ? define(factory) :
  57. global.jspredict = factory()
  58. }(this, function () {
  59. // Meteor includes the moment dependency differently than Npm or Bower,
  60. // so we need this hack (using m_moment) else it gets upset about moment = require('moment');
  61. var m_moment;
  62. // Npm
  63. if (typeof require !== 'undefined') {
  64. var satellite = require('satellite.js').satellite;
  65. m_moment = require('moment');
  66. }
  67. // Meteor
  68. if (this.satellite) {
  69. var satellite = this.satellite;
  70. }
  71. m_moment = m_moment || moment;
  72. var xkmper = 6.378137E3; // earth radius (km) wgs84
  73. var astro_unit = 1.49597870691E8; // Astronomical unit - km (IAU 76)
  74. var solar_radius = 6.96000E5; // solar radius - km (IAU 76)
  75. var deg2rad = Math.PI / 180;
  76. var ms2day = 1000 * 60 * 60 * 24; // milliseconds to day
  77. var max_iterations = 250;
  78. var defaultMinElevation = 4; // degrees
  79. var _jspredict = {
  80. observe: function(tle, qth, start) {
  81. var tles = tle.split('\n');
  82. var satrec = satellite.twoline2satrec(tles[1], tles[2]);
  83. if (this._badSat(satrec, qth, start)) {
  84. return null;
  85. }
  86. return this._observe(satrec, qth, start)
  87. },
  88. observes: function(tle, qth, start, end, interval) {
  89. start = m_moment(start);
  90. end = m_moment(end);
  91. var tles = tle.split('\n');
  92. var satrec = satellite.twoline2satrec(tles[1], tles[2]);
  93. if (this._badSat(satrec, qth, start)) {
  94. return null;
  95. }
  96. var observes = [], observed;
  97. var iterations = 0;
  98. while (start < end && iterations < max_iterations) {
  99. observed = this._observe(satrec, qth, start);
  100. if (!observed) {
  101. break;
  102. }
  103. observes.push(observed);
  104. start.add(interval);
  105. iterations += 1;
  106. }
  107. return observes
  108. },
  109. transits: function(tle, qth, start, end, minElevation, maxTransits) {
  110. start = m_moment(start);
  111. end = m_moment(end);
  112. if (!minElevation) {
  113. minElevation = defaultMinElevation;
  114. }
  115. if (!maxTransits) {
  116. maxTransits = max_iterations;
  117. }
  118. var tles = tle.split('\n');
  119. var satrec = satellite.twoline2satrec(tles[1], tles[2]);
  120. if (this._badSat(satrec, qth, start)) {
  121. return [];
  122. }
  123. var time = start.valueOf();
  124. var transits = [];
  125. var nextTransit;
  126. var iterations = 0;
  127. while (iterations < max_iterations && transits.length < maxTransits) {
  128. transit = this._quickPredict(satrec, qth, time);
  129. if (!transit) {
  130. break;
  131. }
  132. if (transit.end > end.valueOf()) {
  133. break;
  134. }
  135. if (transit.end > start.valueOf() && transit.maxElevation > minElevation) {
  136. transits.push(transit);
  137. }
  138. time = transit.end + 60 * 1000;
  139. iterations += 1;
  140. }
  141. return transits
  142. },
  143. _observe: function(satrec, qth, start) {
  144. start = m_moment(start);
  145. var eci = this._eci(satrec, start);
  146. var gmst = this._gmst(start);
  147. if (!eci.position) {
  148. return null;
  149. }
  150. var geo = satellite.eciToGeodetic(eci.position, gmst);
  151. var solar_vector = this._calculateSolarPosition(start.valueOf());
  152. var eclipse = this._satEclipsed(eci.position, solar_vector);
  153. var track = {
  154. eci: eci,
  155. gmst: gmst,
  156. latitude: geo.latitude / deg2rad,
  157. longitude: this._boundLongitude(geo.longitude / deg2rad),
  158. altitude: geo.height,
  159. footprint: 12756.33 * Math.acos(xkmper / (xkmper + geo.height)),
  160. sunlit: !eclipse.eclipsed,
  161. eclipseDepth: eclipse.depth / deg2rad
  162. }
  163. // If we have a groundstation let's get those additional observe parameters
  164. if (qth && qth.length == 3) {
  165. var observerGd = {
  166. longitude: qth[1] * deg2rad,
  167. latitude: qth[0] * deg2rad,
  168. height: qth[2]
  169. }
  170. var positionEcf = satellite.eciToEcf(eci.position, gmst),
  171. velocityEcf = satellite.eciToEcf(eci.velocity, gmst),
  172. observerEcf = satellite.geodeticToEcf(observerGd),
  173. lookAngles = satellite.ecfToLookAngles(observerGd, positionEcf),
  174. doppler = satellite.dopplerFactor(observerEcf, positionEcf, velocityEcf);
  175. track.azimuth = lookAngles.azimuth / deg2rad;
  176. track.elevation = lookAngles.elevation / deg2rad;
  177. track.rangeSat = lookAngles.rangeSat;
  178. track.doppler = doppler;
  179. }
  180. return track
  181. },
  182. _quickPredict: function(satrec, qth, start) {
  183. var transit = {};
  184. var lastel = 0;
  185. var iterations = 0;
  186. if (this._badSat(satrec, qth, start)) {
  187. return null;
  188. }
  189. var daynum = this._findAOS(satrec, qth, start);
  190. if (!daynum) {
  191. return null;
  192. }
  193. transit.start = daynum;
  194. var observed = this._observe(satrec, qth, daynum);
  195. if (!observed) {
  196. return null;
  197. }
  198. var iel = Math.round(observed.elevation);
  199. var maxEl = 0, apexAz = 0, minAz = 360, maxAz = 0;
  200. while (iel >= 0 && iterations < max_iterations) {
  201. lastel = iel;
  202. daynum = daynum + ms2day * Math.cos((observed.elevation-1.0)*deg2rad)*Math.sqrt(observed.altitude)/25000.0;
  203. observed = this._observe(satrec, qth, daynum);
  204. iel = Math.round(observed.elevation);
  205. if (maxEl < observed.elevation) {
  206. maxEl = observed.elevation;
  207. apexAz = observed.azimuth;
  208. }
  209. maxAz = Math.max(maxAz, observed.azimuth);
  210. minAz = Math.min(minAz, observed.azimuth);
  211. iterations += 1;
  212. }
  213. if (lastel !== 0) {
  214. daynum = this._findLOS(satrec, qth, daynum);
  215. }
  216. transit.end = daynum;
  217. transit.maxElevation = maxEl;
  218. transit.apexAzimuth = apexAz;
  219. transit.maxAzimuth = maxAz;
  220. transit.minAzimuth = minAz;
  221. transit.duration = transit.end - transit.start;
  222. return transit
  223. },
  224. _badSat: function(satrec, qth, start) {
  225. if (qth && !this._aosHappens(satrec, qth)) {
  226. return true
  227. } else if (start && this._decayed(satrec, start)) {
  228. return true
  229. } else {
  230. return false
  231. }
  232. },
  233. _aosHappens: function(satrec, qth) {
  234. var lin, sma, apogee;
  235. var meanmo = satrec.no * 24 * 60 / (2 * Math.PI); // convert rad/min to rev/day
  236. if (meanmo === 0) {
  237. return false
  238. } else {
  239. lin = satrec.inclo / deg2rad;
  240. if (lin >= 90.0) {
  241. lin = 180.0 - lin;
  242. }
  243. sma = 331.25 * Math.exp(Math.log(1440.0/meanmo)*(2.0/3.0));
  244. apogee = sma * (1.0 + satrec.ecco) - xkmper;
  245. if ((Math.acos(xkmper/(apogee+xkmper))+(lin*deg2rad)) > Math.abs(qth[0]*deg2rad)) {
  246. return true
  247. } else {
  248. return false
  249. }
  250. }
  251. },
  252. _decayed: function(satrec, start) {
  253. start = m_moment(start);
  254. var satepoch = m_moment.utc(satrec.epochyr, "YY").add(satrec.epochdays, 'days').valueOf();
  255. var meanmo = satrec.no * 24 * 60 / (2 * Math.PI); // convert rad/min to rev/day
  256. var drag = satrec.ndot * 24 * 60 * 24 * 60 / (2 * Math.PI); // convert rev/day^2
  257. if (satepoch + ms2day * ((16.666666-meanmo)/(10.0*Math.abs(drag))) < start) {
  258. return true
  259. } else {
  260. return false
  261. }
  262. },
  263. _findAOS: function(satrec, qth, start) {
  264. var current = start;
  265. var observed = this._observe(satrec, qth, current);
  266. if (!observed) {
  267. return null;
  268. }
  269. var aostime = 0;
  270. var iterations = 0;
  271. if (observed.elevation > 0) {
  272. return current
  273. }
  274. while (observed.elevation < -1 && iterations < max_iterations) {
  275. current = current - ms2day * 0.00035*(observed.elevation*((observed.altitude/8400.0)+0.46)-2.0);
  276. observed = this._observe(satrec, qth, current);
  277. if (!observed) {
  278. break;
  279. }
  280. iterations += 1;
  281. }
  282. iterations = 0;
  283. while (aostime === 0 && iterations < max_iterations) {
  284. if (!observed) {
  285. break;
  286. }
  287. if (Math.abs(observed.elevation) < 0.50) { // this was 0.03 but switched to 0.50 for performance
  288. aostime = current;
  289. } else {
  290. current = current - ms2day * observed.elevation * Math.sqrt(observed.altitude)/530000.0;
  291. observed = this._observe(satrec, qth, current);
  292. }
  293. iterations += 1;
  294. }
  295. if (aostime === 0) {
  296. return null;
  297. }
  298. return aostime
  299. },
  300. _findLOS: function(satrec, qth, start) {
  301. var current = start;
  302. var observed = this._observe(satrec, qth, current);
  303. var lostime = 0;
  304. var iterations = 0;
  305. while (lostime === 0 && iterations < max_iterations) {
  306. if (Math.abs(observed.elevation) < 0.50) { // this was 0.03 but switched to 0.50 for performance
  307. lostime = current;
  308. } else {
  309. current = current + ms2day * observed.elevation * Math.sqrt(observed.altitude)/502500.0;
  310. observed = this._observe(satrec, qth, current);
  311. if (!observed) {
  312. break;
  313. }
  314. }
  315. iterations += 1;
  316. }
  317. return lostime
  318. },
  319. _eci: function(satrec, date) {
  320. date = new Date(date.valueOf());
  321. return satellite.propagate(
  322. satrec,
  323. date.getUTCFullYear(),
  324. date.getUTCMonth() + 1, // months range 1-12
  325. date.getUTCDate(),
  326. date.getUTCHours(),
  327. date.getUTCMinutes(),
  328. date.getUTCSeconds()
  329. );
  330. },
  331. _gmst: function(date) {
  332. date = new Date(date.valueOf());
  333. return satellite.gstimeFromDate(
  334. date.getUTCFullYear(),
  335. date.getUTCMonth() + 1, // months range 1-12
  336. date.getUTCDate(),
  337. date.getUTCHours(),
  338. date.getUTCMinutes(),
  339. date.getUTCSeconds()
  340. );
  341. },
  342. _boundLongitude: function(longitude) {
  343. while (longitude < -180) {
  344. longitude += 360;
  345. }
  346. while (longitude > 180) {
  347. longitude -= 360;
  348. }
  349. return longitude
  350. },
  351. _satEclipsed: function(pos, sol) {
  352. var sd_earth = Math.asin(xkmper / this._magnitude(pos));
  353. var rho = this._vecSub(sol, pos);
  354. var sd_sun = Math.asin(solar_radius / rho.w);
  355. var earth = this._scalarMultiply(-1, pos);
  356. var delta = this._angle(sol, earth);
  357. var eclipseDepth = sd_earth - sd_sun - delta;
  358. var eclipse;
  359. if (sd_earth < sd_sun) {
  360. eclipse = false;
  361. } else if (eclipseDepth >= 0) {
  362. eclipse = true;
  363. } else {
  364. eclipse = false;
  365. }
  366. return {
  367. depth: eclipseDepth,
  368. eclipsed: eclipse
  369. }
  370. },
  371. _calculateSolarPosition: function(start) {
  372. var time = start / ms2day + 2444238.5; // jul_utc
  373. var mjd = time - 2415020.0;
  374. var year = 1900 + mjd / 365.25;
  375. var T = (mjd + this._deltaET(year) / (ms2day / 1000)) / 36525.0;
  376. var M = deg2rad * ((358.47583 + ((35999.04975 * T) % 360) - (0.000150 + 0.0000033 * T) * Math.pow(T, 2)) % 360);
  377. var L = deg2rad * ((279.69668 + ((36000.76892 * T) % 360) + 0.0003025 * Math.pow(T, 2)) % 360);
  378. var e = 0.01675104 - (0.0000418 + 0.000000126 * T) * T;
  379. var C = deg2rad * ((1.919460 - (0.004789 + 0.000014 * T) * T) * Math.sin(M) + (0.020094 - 0.000100 * T) * Math.sin(2 * M) + 0.000293 * Math.sin(3 * M));
  380. var O = deg2rad * ((259.18 - 1934.142 * T) % 360.0);
  381. var Lsa = (L + C - deg2rad * (0.00569 - 0.00479 * Math.sin(O))) % (2 * Math.PI);
  382. var nu = (M + C) % (2 * Math.PI);
  383. var R = 1.0000002 * (1 - Math.pow(e, 2)) / (1 + e * Math.cos(nu));
  384. var eps = deg2rad * (23.452294 - (0.0130125 + (0.00000164 - 0.000000503 * T) * T) * T + 0.00256 * Math.cos(O));
  385. var R = astro_unit * R;
  386. return {
  387. x: R * Math.cos(Lsa),
  388. y: R * Math.sin(Lsa) * Math.cos(eps),
  389. z: R * Math.sin(Lsa) * Math.sin(eps),
  390. w: R
  391. }
  392. },
  393. _deltaET: function(year) {
  394. return 26.465 + 0.747622 * (year - 1950) + 1.886913 * Math.sin((2 * Math.PI) * (year - 1975) / 33)
  395. },
  396. _vecSub: function(v1, v2) {
  397. var vec = {
  398. x: v1.x - v2.x,
  399. y: v1.y - v2.y,
  400. z: v1.z - v2.z
  401. }
  402. vec.w = this._magnitude(vec);
  403. return vec
  404. },
  405. _scalarMultiply: function(k, v) {
  406. return {
  407. x: k * v.x,
  408. y: k * v.y,
  409. z: k * v.z,
  410. w: v.w ? Math.abs(k) * v.w : undefined
  411. }
  412. },
  413. _magnitude: function(v) {
  414. return Math.sqrt(Math.pow(v.x, 2) + Math.pow(v.y, 2) + Math.pow(v.z, 2))
  415. },
  416. _angle: function(v1, v2) {
  417. var dot = (v1.x * v2.x + v1.y * v2.y + v1.z * v2.z);
  418. return Math.acos(dot / (this._magnitude(v1) * this._magnitude(v2)))
  419. }
  420. }
  421. return _jspredict;
  422. }));