EarthOrientationParameters.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import binarySearch from "./binarySearch.js";
  2. import Check from "./Check.js";
  3. import defaultValue from "./defaultValue.js";
  4. import defined from "./defined.js";
  5. import EarthOrientationParametersSample from "./EarthOrientationParametersSample.js";
  6. import JulianDate from "./JulianDate.js";
  7. import LeapSecond from "./LeapSecond.js";
  8. import Resource from "./Resource.js";
  9. import RuntimeError from "./RuntimeError.js";
  10. import TimeConstants from "./TimeConstants.js";
  11. import TimeStandard from "./TimeStandard.js";
  12. /**
  13. * Specifies Earth polar motion coordinates and the difference between UT1 and UTC.
  14. * These Earth Orientation Parameters (EOP) are primarily used in the transformation from
  15. * the International Celestial Reference Frame (ICRF) to the International Terrestrial
  16. * Reference Frame (ITRF).
  17. * This object is normally not instantiated directly, use {@link EarthOrientationParameters.fromUrl}.
  18. *
  19. * @alias EarthOrientationParameters
  20. * @constructor
  21. *
  22. * @param {object} [options] Object with the following properties:
  23. * @param {object} [options.data] The actual EOP data. If neither this
  24. * parameter nor options.data is specified, all EOP values are assumed
  25. * to be 0.0.
  26. * @param {boolean} [options.addNewLeapSeconds=true] True if leap seconds that
  27. * are specified in the EOP data but not in {@link JulianDate.leapSeconds}
  28. * should be added to {@link JulianDate.leapSeconds}. False if
  29. * new leap seconds should be handled correctly in the context
  30. * of the EOP data but otherwise ignored.
  31. *
  32. * @private
  33. */
  34. function EarthOrientationParameters(options) {
  35. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  36. this._dates = undefined;
  37. this._samples = undefined;
  38. this._dateColumn = -1;
  39. this._xPoleWanderRadiansColumn = -1;
  40. this._yPoleWanderRadiansColumn = -1;
  41. this._ut1MinusUtcSecondsColumn = -1;
  42. this._xCelestialPoleOffsetRadiansColumn = -1;
  43. this._yCelestialPoleOffsetRadiansColumn = -1;
  44. this._taiMinusUtcSecondsColumn = -1;
  45. this._columnCount = 0;
  46. this._lastIndex = -1;
  47. this._addNewLeapSeconds = defaultValue(options.addNewLeapSeconds, true);
  48. if (defined(options.data)) {
  49. // Use supplied EOP data.
  50. onDataReady(this, options.data);
  51. } else {
  52. // Use all zeros for EOP data.
  53. onDataReady(this, {
  54. columnNames: [
  55. "dateIso8601",
  56. "modifiedJulianDateUtc",
  57. "xPoleWanderRadians",
  58. "yPoleWanderRadians",
  59. "ut1MinusUtcSeconds",
  60. "lengthOfDayCorrectionSeconds",
  61. "xCelestialPoleOffsetRadians",
  62. "yCelestialPoleOffsetRadians",
  63. "taiMinusUtcSeconds",
  64. ],
  65. samples: [],
  66. });
  67. }
  68. }
  69. /**
  70. *
  71. * @param {Resource|string} [url] The URL from which to obtain EOP data. If neither this
  72. * parameter nor options.data is specified, all EOP values are assumed
  73. * to be 0.0. If options.data is specified, this parameter is
  74. * ignored.
  75. * @param {object} [options] Object with the following properties:
  76. * @param {boolean} [options.addNewLeapSeconds=true] True if leap seconds that
  77. * are specified in the EOP data but not in {@link JulianDate.leapSeconds}
  78. * should be added to {@link JulianDate.leapSeconds}. False if
  79. * new leap seconds should be handled correctly in the context
  80. * of the EOP data but otherwise ignored.
  81. *
  82. * @example
  83. * // An example EOP data file, EOP.json:
  84. * {
  85. * "columnNames" : ["dateIso8601","modifiedJulianDateUtc","xPoleWanderRadians","yPoleWanderRadians","ut1MinusUtcSeconds","lengthOfDayCorrectionSeconds","xCelestialPoleOffsetRadians","yCelestialPoleOffsetRadians","taiMinusUtcSeconds"],
  86. * "samples" : [
  87. * "2011-07-01T00:00:00Z",55743.0,2.117957047295119e-7,2.111518721609984e-6,-0.2908948,-2.956e-4,3.393695767766752e-11,3.3452143996557983e-10,34.0,
  88. * "2011-07-02T00:00:00Z",55744.0,2.193297093339541e-7,2.115460256837405e-6,-0.29065,-1.824e-4,-8.241832578862112e-11,5.623838700870617e-10,34.0,
  89. * "2011-07-03T00:00:00Z",55745.0,2.262286080161428e-7,2.1191157519929706e-6,-0.2905572,1.9e-6,-3.490658503988659e-10,6.981317007977318e-10,34.0
  90. * ]
  91. * }
  92. *
  93. * @example
  94. * // Loading the EOP data
  95. * const eop = await Cesium.EarthOrientationParameters.fromUrl('Data/EOP.json');
  96. * Cesium.Transforms.earthOrientationParameters = eop;
  97. */
  98. EarthOrientationParameters.fromUrl = async function (url, options) {
  99. //>>includeStart('debug', pragmas.debug);
  100. Check.defined("url", url);
  101. //>>includeEnd('debug');
  102. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  103. const resource = Resource.createIfNeeded(url);
  104. // Download EOP data.
  105. let eopData;
  106. try {
  107. eopData = await resource.fetchJson();
  108. } catch (e) {
  109. throw new RuntimeError(
  110. `An error occurred while retrieving the EOP data from the URL ${resource.url}.`
  111. );
  112. }
  113. return new EarthOrientationParameters({
  114. addNewLeapSeconds: options.addNewLeapSeconds,
  115. data: eopData,
  116. });
  117. };
  118. /**
  119. * A default {@link EarthOrientationParameters} instance that returns zero for all EOP values.
  120. */
  121. EarthOrientationParameters.NONE = Object.freeze({
  122. compute: function (date, result) {
  123. if (!defined(result)) {
  124. result = new EarthOrientationParametersSample(0.0, 0.0, 0.0, 0.0, 0.0);
  125. } else {
  126. result.xPoleWander = 0.0;
  127. result.yPoleWander = 0.0;
  128. result.xPoleOffset = 0.0;
  129. result.yPoleOffset = 0.0;
  130. result.ut1MinusUtc = 0.0;
  131. }
  132. return result;
  133. },
  134. });
  135. /**
  136. * Computes the Earth Orientation Parameters (EOP) for a given date by interpolating.
  137. * If the EOP data has not yet been download, this method returns undefined.
  138. *
  139. * @param {JulianDate} date The date for each to evaluate the EOP.
  140. * @param {EarthOrientationParametersSample} [result] The instance to which to copy the result.
  141. * If this parameter is undefined, a new instance is created and returned.
  142. * @returns {EarthOrientationParametersSample} The EOP evaluated at the given date, or
  143. * undefined if the data necessary to evaluate EOP at the date has not yet been
  144. * downloaded.
  145. *
  146. * @exception {RuntimeError} The loaded EOP data has an error and cannot be used.
  147. *
  148. * @see EarthOrientationParameters#fromUrl
  149. */
  150. EarthOrientationParameters.prototype.compute = function (date, result) {
  151. // We cannot compute until the samples are available.
  152. if (!defined(this._samples)) {
  153. return undefined;
  154. }
  155. if (!defined(result)) {
  156. result = new EarthOrientationParametersSample(0.0, 0.0, 0.0, 0.0, 0.0);
  157. }
  158. if (this._samples.length === 0) {
  159. result.xPoleWander = 0.0;
  160. result.yPoleWander = 0.0;
  161. result.xPoleOffset = 0.0;
  162. result.yPoleOffset = 0.0;
  163. result.ut1MinusUtc = 0.0;
  164. return result;
  165. }
  166. const dates = this._dates;
  167. const lastIndex = this._lastIndex;
  168. let before = 0;
  169. let after = 0;
  170. if (defined(lastIndex)) {
  171. const previousIndexDate = dates[lastIndex];
  172. const nextIndexDate = dates[lastIndex + 1];
  173. const isAfterPrevious = JulianDate.lessThanOrEquals(
  174. previousIndexDate,
  175. date
  176. );
  177. const isAfterLastSample = !defined(nextIndexDate);
  178. const isBeforeNext =
  179. isAfterLastSample || JulianDate.greaterThanOrEquals(nextIndexDate, date);
  180. if (isAfterPrevious && isBeforeNext) {
  181. before = lastIndex;
  182. if (!isAfterLastSample && nextIndexDate.equals(date)) {
  183. ++before;
  184. }
  185. after = before + 1;
  186. interpolate(this, dates, this._samples, date, before, after, result);
  187. return result;
  188. }
  189. }
  190. let index = binarySearch(dates, date, JulianDate.compare, this._dateColumn);
  191. if (index >= 0) {
  192. // If the next entry is the same date, use the later entry. This way, if two entries
  193. // describe the same moment, one before a leap second and the other after, then we will use
  194. // the post-leap second data.
  195. if (index < dates.length - 1 && dates[index + 1].equals(date)) {
  196. ++index;
  197. }
  198. before = index;
  199. after = index;
  200. } else {
  201. after = ~index;
  202. before = after - 1;
  203. // Use the first entry if the date requested is before the beginning of the data.
  204. if (before < 0) {
  205. before = 0;
  206. }
  207. }
  208. this._lastIndex = before;
  209. interpolate(this, dates, this._samples, date, before, after, result);
  210. return result;
  211. };
  212. function compareLeapSecondDates(leapSecond, dateToFind) {
  213. return JulianDate.compare(leapSecond.julianDate, dateToFind);
  214. }
  215. function onDataReady(eop, eopData) {
  216. if (!defined(eopData.columnNames)) {
  217. throw new RuntimeError(
  218. "Error in loaded EOP data: The columnNames property is required."
  219. );
  220. }
  221. if (!defined(eopData.samples)) {
  222. throw new RuntimeError(
  223. "Error in loaded EOP data: The samples property is required."
  224. );
  225. }
  226. const dateColumn = eopData.columnNames.indexOf("modifiedJulianDateUtc");
  227. const xPoleWanderRadiansColumn = eopData.columnNames.indexOf(
  228. "xPoleWanderRadians"
  229. );
  230. const yPoleWanderRadiansColumn = eopData.columnNames.indexOf(
  231. "yPoleWanderRadians"
  232. );
  233. const ut1MinusUtcSecondsColumn = eopData.columnNames.indexOf(
  234. "ut1MinusUtcSeconds"
  235. );
  236. const xCelestialPoleOffsetRadiansColumn = eopData.columnNames.indexOf(
  237. "xCelestialPoleOffsetRadians"
  238. );
  239. const yCelestialPoleOffsetRadiansColumn = eopData.columnNames.indexOf(
  240. "yCelestialPoleOffsetRadians"
  241. );
  242. const taiMinusUtcSecondsColumn = eopData.columnNames.indexOf(
  243. "taiMinusUtcSeconds"
  244. );
  245. if (
  246. dateColumn < 0 ||
  247. xPoleWanderRadiansColumn < 0 ||
  248. yPoleWanderRadiansColumn < 0 ||
  249. ut1MinusUtcSecondsColumn < 0 ||
  250. xCelestialPoleOffsetRadiansColumn < 0 ||
  251. yCelestialPoleOffsetRadiansColumn < 0 ||
  252. taiMinusUtcSecondsColumn < 0
  253. ) {
  254. throw new RuntimeError(
  255. "Error in loaded EOP data: The columnNames property must include modifiedJulianDateUtc, xPoleWanderRadians, yPoleWanderRadians, ut1MinusUtcSeconds, xCelestialPoleOffsetRadians, yCelestialPoleOffsetRadians, and taiMinusUtcSeconds columns"
  256. );
  257. }
  258. const samples = (eop._samples = eopData.samples);
  259. const dates = (eop._dates = []);
  260. eop._dateColumn = dateColumn;
  261. eop._xPoleWanderRadiansColumn = xPoleWanderRadiansColumn;
  262. eop._yPoleWanderRadiansColumn = yPoleWanderRadiansColumn;
  263. eop._ut1MinusUtcSecondsColumn = ut1MinusUtcSecondsColumn;
  264. eop._xCelestialPoleOffsetRadiansColumn = xCelestialPoleOffsetRadiansColumn;
  265. eop._yCelestialPoleOffsetRadiansColumn = yCelestialPoleOffsetRadiansColumn;
  266. eop._taiMinusUtcSecondsColumn = taiMinusUtcSecondsColumn;
  267. eop._columnCount = eopData.columnNames.length;
  268. eop._lastIndex = undefined;
  269. let lastTaiMinusUtc;
  270. const addNewLeapSeconds = eop._addNewLeapSeconds;
  271. // Convert the ISO8601 dates to JulianDates.
  272. for (let i = 0, len = samples.length; i < len; i += eop._columnCount) {
  273. const mjd = samples[i + dateColumn];
  274. const taiMinusUtc = samples[i + taiMinusUtcSecondsColumn];
  275. const day = mjd + TimeConstants.MODIFIED_JULIAN_DATE_DIFFERENCE;
  276. const date = new JulianDate(day, taiMinusUtc, TimeStandard.TAI);
  277. dates.push(date);
  278. if (addNewLeapSeconds) {
  279. if (taiMinusUtc !== lastTaiMinusUtc && defined(lastTaiMinusUtc)) {
  280. // We crossed a leap second boundary, so add the leap second
  281. // if it does not already exist.
  282. const leapSeconds = JulianDate.leapSeconds;
  283. const leapSecondIndex = binarySearch(
  284. leapSeconds,
  285. date,
  286. compareLeapSecondDates
  287. );
  288. if (leapSecondIndex < 0) {
  289. const leapSecond = new LeapSecond(date, taiMinusUtc);
  290. leapSeconds.splice(~leapSecondIndex, 0, leapSecond);
  291. }
  292. }
  293. lastTaiMinusUtc = taiMinusUtc;
  294. }
  295. }
  296. }
  297. function fillResultFromIndex(eop, samples, index, columnCount, result) {
  298. const start = index * columnCount;
  299. result.xPoleWander = samples[start + eop._xPoleWanderRadiansColumn];
  300. result.yPoleWander = samples[start + eop._yPoleWanderRadiansColumn];
  301. result.xPoleOffset = samples[start + eop._xCelestialPoleOffsetRadiansColumn];
  302. result.yPoleOffset = samples[start + eop._yCelestialPoleOffsetRadiansColumn];
  303. result.ut1MinusUtc = samples[start + eop._ut1MinusUtcSecondsColumn];
  304. }
  305. function linearInterp(dx, y1, y2) {
  306. return y1 + dx * (y2 - y1);
  307. }
  308. function interpolate(eop, dates, samples, date, before, after, result) {
  309. const columnCount = eop._columnCount;
  310. // First check the bounds on the EOP data
  311. // If we are after the bounds of the data, return zeros.
  312. // The 'before' index should never be less than zero.
  313. if (after > dates.length - 1) {
  314. result.xPoleWander = 0;
  315. result.yPoleWander = 0;
  316. result.xPoleOffset = 0;
  317. result.yPoleOffset = 0;
  318. result.ut1MinusUtc = 0;
  319. return result;
  320. }
  321. const beforeDate = dates[before];
  322. const afterDate = dates[after];
  323. if (beforeDate.equals(afterDate) || date.equals(beforeDate)) {
  324. fillResultFromIndex(eop, samples, before, columnCount, result);
  325. return result;
  326. } else if (date.equals(afterDate)) {
  327. fillResultFromIndex(eop, samples, after, columnCount, result);
  328. return result;
  329. }
  330. const factor =
  331. JulianDate.secondsDifference(date, beforeDate) /
  332. JulianDate.secondsDifference(afterDate, beforeDate);
  333. const startBefore = before * columnCount;
  334. const startAfter = after * columnCount;
  335. // Handle UT1 leap second edge case
  336. let beforeUt1MinusUtc = samples[startBefore + eop._ut1MinusUtcSecondsColumn];
  337. let afterUt1MinusUtc = samples[startAfter + eop._ut1MinusUtcSecondsColumn];
  338. const offsetDifference = afterUt1MinusUtc - beforeUt1MinusUtc;
  339. if (offsetDifference > 0.5 || offsetDifference < -0.5) {
  340. // The absolute difference between the values is more than 0.5, so we may have
  341. // crossed a leap second. Check if this is the case and, if so, adjust the
  342. // afterValue to account for the leap second. This way, our interpolation will
  343. // produce reasonable results.
  344. const beforeTaiMinusUtc =
  345. samples[startBefore + eop._taiMinusUtcSecondsColumn];
  346. const afterTaiMinusUtc =
  347. samples[startAfter + eop._taiMinusUtcSecondsColumn];
  348. if (beforeTaiMinusUtc !== afterTaiMinusUtc) {
  349. if (afterDate.equals(date)) {
  350. // If we are at the end of the leap second interval, take the second value
  351. // Otherwise, the interpolation below will yield the wrong side of the
  352. // discontinuity
  353. // At the end of the leap second, we need to start accounting for the jump
  354. beforeUt1MinusUtc = afterUt1MinusUtc;
  355. } else {
  356. // Otherwise, remove the leap second so that the interpolation is correct
  357. afterUt1MinusUtc -= afterTaiMinusUtc - beforeTaiMinusUtc;
  358. }
  359. }
  360. }
  361. result.xPoleWander = linearInterp(
  362. factor,
  363. samples[startBefore + eop._xPoleWanderRadiansColumn],
  364. samples[startAfter + eop._xPoleWanderRadiansColumn]
  365. );
  366. result.yPoleWander = linearInterp(
  367. factor,
  368. samples[startBefore + eop._yPoleWanderRadiansColumn],
  369. samples[startAfter + eop._yPoleWanderRadiansColumn]
  370. );
  371. result.xPoleOffset = linearInterp(
  372. factor,
  373. samples[startBefore + eop._xCelestialPoleOffsetRadiansColumn],
  374. samples[startAfter + eop._xCelestialPoleOffsetRadiansColumn]
  375. );
  376. result.yPoleOffset = linearInterp(
  377. factor,
  378. samples[startBefore + eop._yCelestialPoleOffsetRadiansColumn],
  379. samples[startAfter + eop._yCelestialPoleOffsetRadiansColumn]
  380. );
  381. result.ut1MinusUtc = linearInterp(
  382. factor,
  383. beforeUt1MinusUtc,
  384. afterUt1MinusUtc
  385. );
  386. return result;
  387. }
  388. export default EarthOrientationParameters;