TimeIntervalCollection.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136
  1. import binarySearch from "./binarySearch.js";
  2. import defaultValue from "./defaultValue.js";
  3. import defined from "./defined.js";
  4. import DeveloperError from "./DeveloperError.js";
  5. import Event from "./Event.js";
  6. import GregorianDate from "./GregorianDate.js";
  7. import isLeapYear from "./isLeapYear.js";
  8. import Iso8601 from "./Iso8601.js";
  9. import JulianDate from "./JulianDate.js";
  10. import TimeInterval from "./TimeInterval.js";
  11. function compareIntervalStartTimes(left, right) {
  12. return JulianDate.compare(left.start, right.start);
  13. }
  14. /**
  15. * A non-overlapping collection of {@link TimeInterval} instances sorted by start time.
  16. * @alias TimeIntervalCollection
  17. * @constructor
  18. *
  19. * @param {TimeInterval[]} [intervals] An array of intervals to add to the collection.
  20. */
  21. function TimeIntervalCollection(intervals) {
  22. this._intervals = [];
  23. this._changedEvent = new Event();
  24. if (defined(intervals)) {
  25. const length = intervals.length;
  26. for (let i = 0; i < length; i++) {
  27. this.addInterval(intervals[i]);
  28. }
  29. }
  30. }
  31. Object.defineProperties(TimeIntervalCollection.prototype, {
  32. /**
  33. * Gets an event that is raised whenever the collection of intervals change.
  34. * @memberof TimeIntervalCollection.prototype
  35. * @type {Event}
  36. * @readonly
  37. */
  38. changedEvent: {
  39. get: function () {
  40. return this._changedEvent;
  41. },
  42. },
  43. /**
  44. * Gets the start time of the collection.
  45. * @memberof TimeIntervalCollection.prototype
  46. * @type {JulianDate}
  47. * @readonly
  48. */
  49. start: {
  50. get: function () {
  51. const intervals = this._intervals;
  52. return intervals.length === 0 ? undefined : intervals[0].start;
  53. },
  54. },
  55. /**
  56. * Gets whether or not the start time is included in the collection.
  57. * @memberof TimeIntervalCollection.prototype
  58. * @type {Boolean}
  59. * @readonly
  60. */
  61. isStartIncluded: {
  62. get: function () {
  63. const intervals = this._intervals;
  64. return intervals.length === 0 ? false : intervals[0].isStartIncluded;
  65. },
  66. },
  67. /**
  68. * Gets the stop time of the collection.
  69. * @memberof TimeIntervalCollection.prototype
  70. * @type {JulianDate}
  71. * @readonly
  72. */
  73. stop: {
  74. get: function () {
  75. const intervals = this._intervals;
  76. const length = intervals.length;
  77. return length === 0 ? undefined : intervals[length - 1].stop;
  78. },
  79. },
  80. /**
  81. * Gets whether or not the stop time is included in the collection.
  82. * @memberof TimeIntervalCollection.prototype
  83. * @type {Boolean}
  84. * @readonly
  85. */
  86. isStopIncluded: {
  87. get: function () {
  88. const intervals = this._intervals;
  89. const length = intervals.length;
  90. return length === 0 ? false : intervals[length - 1].isStopIncluded;
  91. },
  92. },
  93. /**
  94. * Gets the number of intervals in the collection.
  95. * @memberof TimeIntervalCollection.prototype
  96. * @type {Number}
  97. * @readonly
  98. */
  99. length: {
  100. get: function () {
  101. return this._intervals.length;
  102. },
  103. },
  104. /**
  105. * Gets whether or not the collection is empty.
  106. * @memberof TimeIntervalCollection.prototype
  107. * @type {Boolean}
  108. * @readonly
  109. */
  110. isEmpty: {
  111. get: function () {
  112. return this._intervals.length === 0;
  113. },
  114. },
  115. });
  116. /**
  117. * Compares this instance against the provided instance componentwise and returns
  118. * <code>true</code> if they are equal, <code>false</code> otherwise.
  119. *
  120. * @param {TimeIntervalCollection} [right] The right hand side collection.
  121. * @param {TimeInterval.DataComparer} [dataComparer] A function which compares the data of the two intervals. If omitted, reference equality is used.
  122. * @returns {Boolean} <code>true</code> if they are equal, <code>false</code> otherwise.
  123. */
  124. TimeIntervalCollection.prototype.equals = function (right, dataComparer) {
  125. if (this === right) {
  126. return true;
  127. }
  128. if (!(right instanceof TimeIntervalCollection)) {
  129. return false;
  130. }
  131. const intervals = this._intervals;
  132. const rightIntervals = right._intervals;
  133. const length = intervals.length;
  134. if (length !== rightIntervals.length) {
  135. return false;
  136. }
  137. for (let i = 0; i < length; i++) {
  138. if (!TimeInterval.equals(intervals[i], rightIntervals[i], dataComparer)) {
  139. return false;
  140. }
  141. }
  142. return true;
  143. };
  144. /**
  145. * Gets the interval at the specified index.
  146. *
  147. * @param {Number} index The index of the interval to retrieve.
  148. * @returns {TimeInterval|undefined} The interval at the specified index, or <code>undefined</code> if no interval exists as that index.
  149. */
  150. TimeIntervalCollection.prototype.get = function (index) {
  151. //>>includeStart('debug', pragmas.debug);
  152. if (!defined(index)) {
  153. throw new DeveloperError("index is required.");
  154. }
  155. //>>includeEnd('debug');
  156. return this._intervals[index];
  157. };
  158. /**
  159. * Removes all intervals from the collection.
  160. */
  161. TimeIntervalCollection.prototype.removeAll = function () {
  162. if (this._intervals.length > 0) {
  163. this._intervals.length = 0;
  164. this._changedEvent.raiseEvent(this);
  165. }
  166. };
  167. /**
  168. * Finds and returns the interval that contains the specified date.
  169. *
  170. * @param {JulianDate} date The date to search for.
  171. * @returns {TimeInterval|undefined} The interval containing the specified date, <code>undefined</code> if no such interval exists.
  172. */
  173. TimeIntervalCollection.prototype.findIntervalContainingDate = function (date) {
  174. const index = this.indexOf(date);
  175. return index >= 0 ? this._intervals[index] : undefined;
  176. };
  177. /**
  178. * Finds and returns the data for the interval that contains the specified date.
  179. *
  180. * @param {JulianDate} date The date to search for.
  181. * @returns {Object} The data for the interval containing the specified date, or <code>undefined</code> if no such interval exists.
  182. */
  183. TimeIntervalCollection.prototype.findDataForIntervalContainingDate = function (
  184. date
  185. ) {
  186. const index = this.indexOf(date);
  187. return index >= 0 ? this._intervals[index].data : undefined;
  188. };
  189. /**
  190. * Checks if the specified date is inside this collection.
  191. *
  192. * @param {JulianDate} julianDate The date to check.
  193. * @returns {Boolean} <code>true</code> if the collection contains the specified date, <code>false</code> otherwise.
  194. */
  195. TimeIntervalCollection.prototype.contains = function (julianDate) {
  196. return this.indexOf(julianDate) >= 0;
  197. };
  198. const indexOfScratch = new TimeInterval();
  199. /**
  200. * Finds and returns the index of the interval in the collection that contains the specified date.
  201. *
  202. * @param {JulianDate} date The date to search for.
  203. * @returns {Number} The index of the interval that contains the specified date, if no such interval exists,
  204. * it returns a negative number which is the bitwise complement of the index of the next interval that
  205. * starts after the date, or if no interval starts after the specified date, the bitwise complement of
  206. * the length of the collection.
  207. */
  208. TimeIntervalCollection.prototype.indexOf = function (date) {
  209. //>>includeStart('debug', pragmas.debug);
  210. if (!defined(date)) {
  211. throw new DeveloperError("date is required");
  212. }
  213. //>>includeEnd('debug');
  214. const intervals = this._intervals;
  215. indexOfScratch.start = date;
  216. indexOfScratch.stop = date;
  217. let index = binarySearch(
  218. intervals,
  219. indexOfScratch,
  220. compareIntervalStartTimes
  221. );
  222. if (index >= 0) {
  223. if (intervals[index].isStartIncluded) {
  224. return index;
  225. }
  226. if (
  227. index > 0 &&
  228. intervals[index - 1].stop.equals(date) &&
  229. intervals[index - 1].isStopIncluded
  230. ) {
  231. return index - 1;
  232. }
  233. return ~index;
  234. }
  235. index = ~index;
  236. if (
  237. index > 0 &&
  238. index - 1 < intervals.length &&
  239. TimeInterval.contains(intervals[index - 1], date)
  240. ) {
  241. return index - 1;
  242. }
  243. return ~index;
  244. };
  245. /**
  246. * Returns the first interval in the collection that matches the specified parameters.
  247. * All parameters are optional and <code>undefined</code> parameters are treated as a don't care condition.
  248. *
  249. * @param {Object} [options] Object with the following properties:
  250. * @param {JulianDate} [options.start] The start time of the interval.
  251. * @param {JulianDate} [options.stop] The stop time of the interval.
  252. * @param {Boolean} [options.isStartIncluded] <code>true</code> if <code>options.start</code> is included in the interval, <code>false</code> otherwise.
  253. * @param {Boolean} [options.isStopIncluded] <code>true</code> if <code>options.stop</code> is included in the interval, <code>false</code> otherwise.
  254. * @returns {TimeInterval|undefined} The first interval in the collection that matches the specified parameters.
  255. */
  256. TimeIntervalCollection.prototype.findInterval = function (options) {
  257. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  258. const start = options.start;
  259. const stop = options.stop;
  260. const isStartIncluded = options.isStartIncluded;
  261. const isStopIncluded = options.isStopIncluded;
  262. const intervals = this._intervals;
  263. for (let i = 0, len = intervals.length; i < len; i++) {
  264. const interval = intervals[i];
  265. if (
  266. (!defined(start) || interval.start.equals(start)) &&
  267. (!defined(stop) || interval.stop.equals(stop)) &&
  268. (!defined(isStartIncluded) ||
  269. interval.isStartIncluded === isStartIncluded) &&
  270. (!defined(isStopIncluded) || interval.isStopIncluded === isStopIncluded)
  271. ) {
  272. return intervals[i];
  273. }
  274. }
  275. return undefined;
  276. };
  277. /**
  278. * Adds an interval to the collection, merging intervals that contain the same data and
  279. * splitting intervals of different data as needed in order to maintain a non-overlapping collection.
  280. * The data in the new interval takes precedence over any existing intervals in the collection.
  281. *
  282. * @param {TimeInterval} interval The interval to add.
  283. * @param {TimeInterval.DataComparer} [dataComparer] A function which compares the data of the two intervals. If omitted, reference equality is used.
  284. */
  285. TimeIntervalCollection.prototype.addInterval = function (
  286. interval,
  287. dataComparer
  288. ) {
  289. //>>includeStart('debug', pragmas.debug);
  290. if (!defined(interval)) {
  291. throw new DeveloperError("interval is required");
  292. }
  293. //>>includeEnd('debug');
  294. if (interval.isEmpty) {
  295. return;
  296. }
  297. const intervals = this._intervals;
  298. // Handle the common case quickly: we're adding a new interval which is after all existing intervals.
  299. if (
  300. intervals.length === 0 ||
  301. JulianDate.greaterThan(interval.start, intervals[intervals.length - 1].stop)
  302. ) {
  303. intervals.push(interval);
  304. this._changedEvent.raiseEvent(this);
  305. return;
  306. }
  307. // Keep the list sorted by the start date
  308. let index = binarySearch(intervals, interval, compareIntervalStartTimes);
  309. if (index < 0) {
  310. index = ~index;
  311. } else {
  312. // interval's start date exactly equals the start date of at least one interval in the collection.
  313. // It could actually equal the start date of two intervals if one of them does not actually
  314. // include the date. In that case, the binary search could have found either. We need to
  315. // look at the surrounding intervals and their IsStartIncluded properties in order to make sure
  316. // we're working with the correct interval.
  317. // eslint-disable-next-line no-lonely-if
  318. if (
  319. index > 0 &&
  320. interval.isStartIncluded &&
  321. intervals[index - 1].isStartIncluded &&
  322. intervals[index - 1].start.equals(interval.start)
  323. ) {
  324. --index;
  325. } else if (
  326. index < intervals.length &&
  327. !interval.isStartIncluded &&
  328. intervals[index].isStartIncluded &&
  329. intervals[index].start.equals(interval.start)
  330. ) {
  331. ++index;
  332. }
  333. }
  334. let comparison;
  335. if (index > 0) {
  336. // Not the first thing in the list, so see if the interval before this one
  337. // overlaps this one.
  338. comparison = JulianDate.compare(intervals[index - 1].stop, interval.start);
  339. if (
  340. comparison > 0 ||
  341. (comparison === 0 &&
  342. (intervals[index - 1].isStopIncluded || interval.isStartIncluded))
  343. ) {
  344. // There is an overlap
  345. if (
  346. defined(dataComparer)
  347. ? dataComparer(intervals[index - 1].data, interval.data)
  348. : intervals[index - 1].data === interval.data
  349. ) {
  350. // Overlapping intervals have the same data, so combine them
  351. if (JulianDate.greaterThan(interval.stop, intervals[index - 1].stop)) {
  352. interval = new TimeInterval({
  353. start: intervals[index - 1].start,
  354. stop: interval.stop,
  355. isStartIncluded: intervals[index - 1].isStartIncluded,
  356. isStopIncluded: interval.isStopIncluded,
  357. data: interval.data,
  358. });
  359. } else {
  360. interval = new TimeInterval({
  361. start: intervals[index - 1].start,
  362. stop: intervals[index - 1].stop,
  363. isStartIncluded: intervals[index - 1].isStartIncluded,
  364. isStopIncluded:
  365. intervals[index - 1].isStopIncluded ||
  366. (interval.stop.equals(intervals[index - 1].stop) &&
  367. interval.isStopIncluded),
  368. data: interval.data,
  369. });
  370. }
  371. intervals.splice(index - 1, 1);
  372. --index;
  373. } else {
  374. // Overlapping intervals have different data. The new interval
  375. // being added 'wins' so truncate the previous interval.
  376. // If the existing interval extends past the end of the new one,
  377. // split the existing interval into two intervals.
  378. comparison = JulianDate.compare(
  379. intervals[index - 1].stop,
  380. interval.stop
  381. );
  382. if (
  383. comparison > 0 ||
  384. (comparison === 0 &&
  385. intervals[index - 1].isStopIncluded &&
  386. !interval.isStopIncluded)
  387. ) {
  388. intervals.splice(
  389. index,
  390. 0,
  391. new TimeInterval({
  392. start: interval.stop,
  393. stop: intervals[index - 1].stop,
  394. isStartIncluded: !interval.isStopIncluded,
  395. isStopIncluded: intervals[index - 1].isStopIncluded,
  396. data: intervals[index - 1].data,
  397. })
  398. );
  399. }
  400. intervals[index - 1] = new TimeInterval({
  401. start: intervals[index - 1].start,
  402. stop: interval.start,
  403. isStartIncluded: intervals[index - 1].isStartIncluded,
  404. isStopIncluded: !interval.isStartIncluded,
  405. data: intervals[index - 1].data,
  406. });
  407. }
  408. }
  409. }
  410. while (index < intervals.length) {
  411. // Not the last thing in the list, so see if the intervals after this one overlap this one.
  412. comparison = JulianDate.compare(interval.stop, intervals[index].start);
  413. if (
  414. comparison > 0 ||
  415. (comparison === 0 &&
  416. (interval.isStopIncluded || intervals[index].isStartIncluded))
  417. ) {
  418. // There is an overlap
  419. if (
  420. defined(dataComparer)
  421. ? dataComparer(intervals[index].data, interval.data)
  422. : intervals[index].data === interval.data
  423. ) {
  424. // Overlapping intervals have the same data, so combine them
  425. interval = new TimeInterval({
  426. start: interval.start,
  427. stop: JulianDate.greaterThan(intervals[index].stop, interval.stop)
  428. ? intervals[index].stop
  429. : interval.stop,
  430. isStartIncluded: interval.isStartIncluded,
  431. isStopIncluded: JulianDate.greaterThan(
  432. intervals[index].stop,
  433. interval.stop
  434. )
  435. ? intervals[index].isStopIncluded
  436. : interval.isStopIncluded,
  437. data: interval.data,
  438. });
  439. intervals.splice(index, 1);
  440. } else {
  441. // Overlapping intervals have different data. The new interval
  442. // being added 'wins' so truncate the next interval.
  443. intervals[index] = new TimeInterval({
  444. start: interval.stop,
  445. stop: intervals[index].stop,
  446. isStartIncluded: !interval.isStopIncluded,
  447. isStopIncluded: intervals[index].isStopIncluded,
  448. data: intervals[index].data,
  449. });
  450. if (intervals[index].isEmpty) {
  451. intervals.splice(index, 1);
  452. } else {
  453. // Found a partial span, so it is not possible for the next
  454. // interval to be spanned at all. Stop looking.
  455. break;
  456. }
  457. }
  458. } else {
  459. // Found the last one we're spanning, so stop looking.
  460. break;
  461. }
  462. }
  463. // Add the new interval
  464. intervals.splice(index, 0, interval);
  465. this._changedEvent.raiseEvent(this);
  466. };
  467. /**
  468. * Removes the specified interval from this interval collection, creating a hole over the specified interval.
  469. * The data property of the input interval is ignored.
  470. *
  471. * @param {TimeInterval} interval The interval to remove.
  472. * @returns {Boolean} <code>true</code> if the interval was removed, <code>false</code> if no part of the interval was in the collection.
  473. */
  474. TimeIntervalCollection.prototype.removeInterval = function (interval) {
  475. //>>includeStart('debug', pragmas.debug);
  476. if (!defined(interval)) {
  477. throw new DeveloperError("interval is required");
  478. }
  479. //>>includeEnd('debug');
  480. if (interval.isEmpty) {
  481. return false;
  482. }
  483. const intervals = this._intervals;
  484. let index = binarySearch(intervals, interval, compareIntervalStartTimes);
  485. if (index < 0) {
  486. index = ~index;
  487. }
  488. let result = false;
  489. // Check for truncation of the end of the previous interval.
  490. if (
  491. index > 0 &&
  492. (JulianDate.greaterThan(intervals[index - 1].stop, interval.start) ||
  493. (intervals[index - 1].stop.equals(interval.start) &&
  494. intervals[index - 1].isStopIncluded &&
  495. interval.isStartIncluded))
  496. ) {
  497. result = true;
  498. if (
  499. JulianDate.greaterThan(intervals[index - 1].stop, interval.stop) ||
  500. (intervals[index - 1].isStopIncluded &&
  501. !interval.isStopIncluded &&
  502. intervals[index - 1].stop.equals(interval.stop))
  503. ) {
  504. // Break the existing interval into two pieces
  505. intervals.splice(
  506. index,
  507. 0,
  508. new TimeInterval({
  509. start: interval.stop,
  510. stop: intervals[index - 1].stop,
  511. isStartIncluded: !interval.isStopIncluded,
  512. isStopIncluded: intervals[index - 1].isStopIncluded,
  513. data: intervals[index - 1].data,
  514. })
  515. );
  516. }
  517. intervals[index - 1] = new TimeInterval({
  518. start: intervals[index - 1].start,
  519. stop: interval.start,
  520. isStartIncluded: intervals[index - 1].isStartIncluded,
  521. isStopIncluded: !interval.isStartIncluded,
  522. data: intervals[index - 1].data,
  523. });
  524. }
  525. // Check if the Start of the current interval should remain because interval.start is the same but
  526. // it is not included.
  527. if (
  528. index < intervals.length &&
  529. !interval.isStartIncluded &&
  530. intervals[index].isStartIncluded &&
  531. interval.start.equals(intervals[index].start)
  532. ) {
  533. result = true;
  534. intervals.splice(
  535. index,
  536. 0,
  537. new TimeInterval({
  538. start: intervals[index].start,
  539. stop: intervals[index].start,
  540. isStartIncluded: true,
  541. isStopIncluded: true,
  542. data: intervals[index].data,
  543. })
  544. );
  545. ++index;
  546. }
  547. // Remove any intervals that are completely overlapped by the input interval.
  548. while (
  549. index < intervals.length &&
  550. JulianDate.greaterThan(interval.stop, intervals[index].stop)
  551. ) {
  552. result = true;
  553. intervals.splice(index, 1);
  554. }
  555. // Check for the case where the input interval ends on the same date
  556. // as an existing interval.
  557. if (index < intervals.length && interval.stop.equals(intervals[index].stop)) {
  558. result = true;
  559. if (!interval.isStopIncluded && intervals[index].isStopIncluded) {
  560. // Last point of interval should remain because the stop date is included in
  561. // the existing interval but is not included in the input interval.
  562. if (
  563. index + 1 < intervals.length &&
  564. intervals[index + 1].start.equals(interval.stop) &&
  565. intervals[index].data === intervals[index + 1].data
  566. ) {
  567. // Combine single point with the next interval
  568. intervals.splice(index, 1);
  569. intervals[index] = new TimeInterval({
  570. start: intervals[index].start,
  571. stop: intervals[index].stop,
  572. isStartIncluded: true,
  573. isStopIncluded: intervals[index].isStopIncluded,
  574. data: intervals[index].data,
  575. });
  576. } else {
  577. intervals[index] = new TimeInterval({
  578. start: interval.stop,
  579. stop: interval.stop,
  580. isStartIncluded: true,
  581. isStopIncluded: true,
  582. data: intervals[index].data,
  583. });
  584. }
  585. } else {
  586. // Interval is completely overlapped
  587. intervals.splice(index, 1);
  588. }
  589. }
  590. // Truncate any partially-overlapped intervals.
  591. if (
  592. index < intervals.length &&
  593. (JulianDate.greaterThan(interval.stop, intervals[index].start) ||
  594. (interval.stop.equals(intervals[index].start) &&
  595. interval.isStopIncluded &&
  596. intervals[index].isStartIncluded))
  597. ) {
  598. result = true;
  599. intervals[index] = new TimeInterval({
  600. start: interval.stop,
  601. stop: intervals[index].stop,
  602. isStartIncluded: !interval.isStopIncluded,
  603. isStopIncluded: intervals[index].isStopIncluded,
  604. data: intervals[index].data,
  605. });
  606. }
  607. if (result) {
  608. this._changedEvent.raiseEvent(this);
  609. }
  610. return result;
  611. };
  612. /**
  613. * Creates a new instance that is the intersection of this collection and the provided collection.
  614. *
  615. * @param {TimeIntervalCollection} other The collection to intersect with.
  616. * @param {TimeInterval.DataComparer} [dataComparer] A function which compares the data of the two intervals. If omitted, reference equality is used.
  617. * @param {TimeInterval.MergeCallback} [mergeCallback] A function which merges the data of the two intervals. If omitted, the data from the left interval will be used.
  618. * @returns {TimeIntervalCollection} A new TimeIntervalCollection which is the intersection of this collection and the provided collection.
  619. */
  620. TimeIntervalCollection.prototype.intersect = function (
  621. other,
  622. dataComparer,
  623. mergeCallback
  624. ) {
  625. //>>includeStart('debug', pragmas.debug);
  626. if (!defined(other)) {
  627. throw new DeveloperError("other is required.");
  628. }
  629. //>>includeEnd('debug');
  630. const result = new TimeIntervalCollection();
  631. let left = 0;
  632. let right = 0;
  633. const intervals = this._intervals;
  634. const otherIntervals = other._intervals;
  635. while (left < intervals.length && right < otherIntervals.length) {
  636. const leftInterval = intervals[left];
  637. const rightInterval = otherIntervals[right];
  638. if (JulianDate.lessThan(leftInterval.stop, rightInterval.start)) {
  639. ++left;
  640. } else if (JulianDate.lessThan(rightInterval.stop, leftInterval.start)) {
  641. ++right;
  642. } else {
  643. // The following will return an intersection whose data is 'merged' if the callback is defined
  644. if (
  645. defined(mergeCallback) ||
  646. (defined(dataComparer) &&
  647. dataComparer(leftInterval.data, rightInterval.data)) ||
  648. (!defined(dataComparer) && rightInterval.data === leftInterval.data)
  649. ) {
  650. const intersection = TimeInterval.intersect(
  651. leftInterval,
  652. rightInterval,
  653. new TimeInterval(),
  654. mergeCallback
  655. );
  656. if (!intersection.isEmpty) {
  657. // Since we start with an empty collection for 'result', and there are no overlapping intervals in 'this' (as a rule),
  658. // the 'intersection' will never overlap with a previous interval in 'result'. So, no need to do any additional 'merging'.
  659. result.addInterval(intersection, dataComparer);
  660. }
  661. }
  662. if (
  663. JulianDate.lessThan(leftInterval.stop, rightInterval.stop) ||
  664. (leftInterval.stop.equals(rightInterval.stop) &&
  665. !leftInterval.isStopIncluded &&
  666. rightInterval.isStopIncluded)
  667. ) {
  668. ++left;
  669. } else {
  670. ++right;
  671. }
  672. }
  673. }
  674. return result;
  675. };
  676. /**
  677. * Creates a new instance from a JulianDate array.
  678. *
  679. * @param {Object} options Object with the following properties:
  680. * @param {JulianDate[]} options.julianDates An array of ISO 8601 dates.
  681. * @param {Boolean} [options.isStartIncluded=true] <code>true</code> if start time is included in the interval, <code>false</code> otherwise.
  682. * @param {Boolean} [options.isStopIncluded=true] <code>true</code> if stop time is included in the interval, <code>false</code> otherwise.
  683. * @param {Boolean} [options.leadingInterval=false] <code>true</code> if you want to add a interval from Iso8601.MINIMUM_VALUE to start time, <code>false</code> otherwise.
  684. * @param {Boolean} [options.trailingInterval=false] <code>true</code> if you want to add a interval from stop time to Iso8601.MAXIMUM_VALUE, <code>false</code> otherwise.
  685. * @param {Function} [options.dataCallback] A function that will be return the data that is called with each interval before it is added to the collection. If unspecified, the data will be the index in the collection.
  686. * @param {TimeIntervalCollection} [result] An existing instance to use for the result.
  687. * @returns {TimeIntervalCollection} The modified result parameter or a new instance if none was provided.
  688. */
  689. TimeIntervalCollection.fromJulianDateArray = function (options, result) {
  690. //>>includeStart('debug', pragmas.debug);
  691. if (!defined(options)) {
  692. throw new DeveloperError("options is required.");
  693. }
  694. if (!defined(options.julianDates)) {
  695. throw new DeveloperError("options.iso8601Array is required.");
  696. }
  697. //>>includeEnd('debug');
  698. if (!defined(result)) {
  699. result = new TimeIntervalCollection();
  700. }
  701. const julianDates = options.julianDates;
  702. const length = julianDates.length;
  703. const dataCallback = options.dataCallback;
  704. const isStartIncluded = defaultValue(options.isStartIncluded, true);
  705. const isStopIncluded = defaultValue(options.isStopIncluded, true);
  706. const leadingInterval = defaultValue(options.leadingInterval, false);
  707. const trailingInterval = defaultValue(options.trailingInterval, false);
  708. let interval;
  709. // Add a default interval, which will only end up being used up to first interval
  710. let startIndex = 0;
  711. if (leadingInterval) {
  712. ++startIndex;
  713. interval = new TimeInterval({
  714. start: Iso8601.MINIMUM_VALUE,
  715. stop: julianDates[0],
  716. isStartIncluded: true,
  717. isStopIncluded: !isStartIncluded,
  718. });
  719. interval.data = defined(dataCallback)
  720. ? dataCallback(interval, result.length)
  721. : result.length;
  722. result.addInterval(interval);
  723. }
  724. for (let i = 0; i < length - 1; ++i) {
  725. let startDate = julianDates[i];
  726. const endDate = julianDates[i + 1];
  727. interval = new TimeInterval({
  728. start: startDate,
  729. stop: endDate,
  730. isStartIncluded: result.length === startIndex ? isStartIncluded : true,
  731. isStopIncluded: i === length - 2 ? isStopIncluded : false,
  732. });
  733. interval.data = defined(dataCallback)
  734. ? dataCallback(interval, result.length)
  735. : result.length;
  736. result.addInterval(interval);
  737. startDate = endDate;
  738. }
  739. if (trailingInterval) {
  740. interval = new TimeInterval({
  741. start: julianDates[length - 1],
  742. stop: Iso8601.MAXIMUM_VALUE,
  743. isStartIncluded: !isStopIncluded,
  744. isStopIncluded: true,
  745. });
  746. interval.data = defined(dataCallback)
  747. ? dataCallback(interval, result.length)
  748. : result.length;
  749. result.addInterval(interval);
  750. }
  751. return result;
  752. };
  753. const scratchGregorianDate = new GregorianDate();
  754. const monthLengths = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  755. /**
  756. * Adds duration represented as a GregorianDate to a JulianDate
  757. *
  758. * @param {JulianDate} julianDate The date.
  759. * @param {GregorianDate} duration An duration represented as a GregorianDate.
  760. * @param {JulianDate} result An existing instance to use for the result.
  761. * @returns {JulianDate} The modified result parameter.
  762. *
  763. * @private
  764. */
  765. function addToDate(julianDate, duration, result) {
  766. if (!defined(result)) {
  767. result = new JulianDate();
  768. }
  769. JulianDate.toGregorianDate(julianDate, scratchGregorianDate);
  770. let millisecond = scratchGregorianDate.millisecond + duration.millisecond;
  771. let second = scratchGregorianDate.second + duration.second;
  772. let minute = scratchGregorianDate.minute + duration.minute;
  773. let hour = scratchGregorianDate.hour + duration.hour;
  774. let day = scratchGregorianDate.day + duration.day;
  775. let month = scratchGregorianDate.month + duration.month;
  776. let year = scratchGregorianDate.year + duration.year;
  777. if (millisecond >= 1000) {
  778. second += Math.floor(millisecond / 1000);
  779. millisecond = millisecond % 1000;
  780. }
  781. if (second >= 60) {
  782. minute += Math.floor(second / 60);
  783. second = second % 60;
  784. }
  785. if (minute >= 60) {
  786. hour += Math.floor(minute / 60);
  787. minute = minute % 60;
  788. }
  789. if (hour >= 24) {
  790. day += Math.floor(hour / 24);
  791. hour = hour % 24;
  792. }
  793. // If days is greater than the month's length we need to remove those number of days,
  794. // readjust month and year and repeat until days is less than the month's length.
  795. monthLengths[2] = isLeapYear(year) ? 29 : 28;
  796. while (day > monthLengths[month] || month >= 13) {
  797. if (day > monthLengths[month]) {
  798. day -= monthLengths[month];
  799. ++month;
  800. }
  801. if (month >= 13) {
  802. --month;
  803. year += Math.floor(month / 12);
  804. month = month % 12;
  805. ++month;
  806. }
  807. monthLengths[2] = isLeapYear(year) ? 29 : 28;
  808. }
  809. scratchGregorianDate.millisecond = millisecond;
  810. scratchGregorianDate.second = second;
  811. scratchGregorianDate.minute = minute;
  812. scratchGregorianDate.hour = hour;
  813. scratchGregorianDate.day = day;
  814. scratchGregorianDate.month = month;
  815. scratchGregorianDate.year = year;
  816. return JulianDate.fromGregorianDate(scratchGregorianDate, result);
  817. }
  818. const scratchJulianDate = new JulianDate();
  819. const durationRegex = /P(?:([\d.,]+)Y)?(?:([\d.,]+)M)?(?:([\d.,]+)W)?(?:([\d.,]+)D)?(?:T(?:([\d.,]+)H)?(?:([\d.,]+)M)?(?:([\d.,]+)S)?)?/;
  820. /**
  821. * Parses ISO8601 duration string
  822. *
  823. * @param {String} iso8601 An ISO 8601 duration.
  824. * @param {GregorianDate} result An existing instance to use for the result.
  825. * @returns {Boolean} True is parsing succeeded, false otherwise
  826. *
  827. * @private
  828. */
  829. function parseDuration(iso8601, result) {
  830. if (!defined(iso8601) || iso8601.length === 0) {
  831. return false;
  832. }
  833. // Reset object
  834. result.year = 0;
  835. result.month = 0;
  836. result.day = 0;
  837. result.hour = 0;
  838. result.minute = 0;
  839. result.second = 0;
  840. result.millisecond = 0;
  841. if (iso8601[0] === "P") {
  842. const matches = iso8601.match(durationRegex);
  843. if (!defined(matches)) {
  844. return false;
  845. }
  846. if (defined(matches[1])) {
  847. // Years
  848. result.year = Number(matches[1].replace(",", "."));
  849. }
  850. if (defined(matches[2])) {
  851. // Months
  852. result.month = Number(matches[2].replace(",", "."));
  853. }
  854. if (defined(matches[3])) {
  855. // Weeks
  856. result.day = Number(matches[3].replace(",", ".")) * 7;
  857. }
  858. if (defined(matches[4])) {
  859. // Days
  860. result.day += Number(matches[4].replace(",", "."));
  861. }
  862. if (defined(matches[5])) {
  863. // Hours
  864. result.hour = Number(matches[5].replace(",", "."));
  865. }
  866. if (defined(matches[6])) {
  867. // Weeks
  868. result.minute = Number(matches[6].replace(",", "."));
  869. }
  870. if (defined(matches[7])) {
  871. // Seconds
  872. const seconds = Number(matches[7].replace(",", "."));
  873. result.second = Math.floor(seconds);
  874. result.millisecond = (seconds % 1) * 1000;
  875. }
  876. } else {
  877. // They can technically specify the duration as a normal date with some caveats. Try our best to load it.
  878. if (iso8601[iso8601.length - 1] !== "Z") {
  879. // It's not a date, its a duration, so it always has to be UTC
  880. iso8601 += "Z";
  881. }
  882. JulianDate.toGregorianDate(
  883. JulianDate.fromIso8601(iso8601, scratchJulianDate),
  884. result
  885. );
  886. }
  887. // A duration of 0 will cause an infinite loop, so just make sure something is non-zero
  888. return (
  889. result.year ||
  890. result.month ||
  891. result.day ||
  892. result.hour ||
  893. result.minute ||
  894. result.second ||
  895. result.millisecond
  896. );
  897. }
  898. const scratchDuration = new GregorianDate();
  899. /**
  900. * Creates a new instance from an {@link http://en.wikipedia.org/wiki/ISO_8601|ISO 8601} time interval (start/end/duration).
  901. *
  902. * @param {Object} options Object with the following properties:
  903. * @param {String} options.iso8601 An ISO 8601 interval.
  904. * @param {Boolean} [options.isStartIncluded=true] <code>true</code> if start time is included in the interval, <code>false</code> otherwise.
  905. * @param {Boolean} [options.isStopIncluded=true] <code>true</code> if stop time is included in the interval, <code>false</code> otherwise.
  906. * @param {Boolean} [options.leadingInterval=false] <code>true</code> if you want to add a interval from Iso8601.MINIMUM_VALUE to start time, <code>false</code> otherwise.
  907. * @param {Boolean} [options.trailingInterval=false] <code>true</code> if you want to add a interval from stop time to Iso8601.MAXIMUM_VALUE, <code>false</code> otherwise.
  908. * @param {Function} [options.dataCallback] A function that will be return the data that is called with each interval before it is added to the collection. If unspecified, the data will be the index in the collection.
  909. * @param {TimeIntervalCollection} [result] An existing instance to use for the result.
  910. * @returns {TimeIntervalCollection} The modified result parameter or a new instance if none was provided.
  911. */
  912. TimeIntervalCollection.fromIso8601 = function (options, result) {
  913. //>>includeStart('debug', pragmas.debug);
  914. if (!defined(options)) {
  915. throw new DeveloperError("options is required.");
  916. }
  917. if (!defined(options.iso8601)) {
  918. throw new DeveloperError("options.iso8601 is required.");
  919. }
  920. //>>includeEnd('debug');
  921. const dates = options.iso8601.split("/");
  922. const start = JulianDate.fromIso8601(dates[0]);
  923. const stop = JulianDate.fromIso8601(dates[1]);
  924. const julianDates = [];
  925. if (!parseDuration(dates[2], scratchDuration)) {
  926. julianDates.push(start, stop);
  927. } else {
  928. let date = JulianDate.clone(start);
  929. julianDates.push(date);
  930. while (JulianDate.compare(date, stop) < 0) {
  931. date = addToDate(date, scratchDuration);
  932. const afterStop = JulianDate.compare(stop, date) <= 0;
  933. if (afterStop) {
  934. JulianDate.clone(stop, date);
  935. }
  936. julianDates.push(date);
  937. }
  938. }
  939. return TimeIntervalCollection.fromJulianDateArray(
  940. {
  941. julianDates: julianDates,
  942. isStartIncluded: options.isStartIncluded,
  943. isStopIncluded: options.isStopIncluded,
  944. leadingInterval: options.leadingInterval,
  945. trailingInterval: options.trailingInterval,
  946. dataCallback: options.dataCallback,
  947. },
  948. result
  949. );
  950. };
  951. /**
  952. * Creates a new instance from a {@link http://en.wikipedia.org/wiki/ISO_8601|ISO 8601} date array.
  953. *
  954. * @param {Object} options Object with the following properties:
  955. * @param {String[]} options.iso8601Dates An array of ISO 8601 dates.
  956. * @param {Boolean} [options.isStartIncluded=true] <code>true</code> if start time is included in the interval, <code>false</code> otherwise.
  957. * @param {Boolean} [options.isStopIncluded=true] <code>true</code> if stop time is included in the interval, <code>false</code> otherwise.
  958. * @param {Boolean} [options.leadingInterval=false] <code>true</code> if you want to add a interval from Iso8601.MINIMUM_VALUE to start time, <code>false</code> otherwise.
  959. * @param {Boolean} [options.trailingInterval=false] <code>true</code> if you want to add a interval from stop time to Iso8601.MAXIMUM_VALUE, <code>false</code> otherwise.
  960. * @param {Function} [options.dataCallback] A function that will be return the data that is called with each interval before it is added to the collection. If unspecified, the data will be the index in the collection.
  961. * @param {TimeIntervalCollection} [result] An existing instance to use for the result.
  962. * @returns {TimeIntervalCollection} The modified result parameter or a new instance if none was provided.
  963. */
  964. TimeIntervalCollection.fromIso8601DateArray = function (options, result) {
  965. //>>includeStart('debug', pragmas.debug);
  966. if (!defined(options)) {
  967. throw new DeveloperError("options is required.");
  968. }
  969. if (!defined(options.iso8601Dates)) {
  970. throw new DeveloperError("options.iso8601Dates is required.");
  971. }
  972. //>>includeEnd('debug');
  973. return TimeIntervalCollection.fromJulianDateArray(
  974. {
  975. julianDates: options.iso8601Dates.map(function (date) {
  976. return JulianDate.fromIso8601(date);
  977. }),
  978. isStartIncluded: options.isStartIncluded,
  979. isStopIncluded: options.isStopIncluded,
  980. leadingInterval: options.leadingInterval,
  981. trailingInterval: options.trailingInterval,
  982. dataCallback: options.dataCallback,
  983. },
  984. result
  985. );
  986. };
  987. /**
  988. * Creates a new instance from a {@link http://en.wikipedia.org/wiki/ISO_8601|ISO 8601} duration array.
  989. *
  990. * @param {Object} options Object with the following properties:
  991. * @param {JulianDate} options.epoch An date that the durations are relative to.
  992. * @param {String} options.iso8601Durations An array of ISO 8601 durations.
  993. * @param {Boolean} [options.relativeToPrevious=false] <code>true</code> if durations are relative to previous date, <code>false</code> if always relative to the epoch.
  994. * @param {Boolean} [options.isStartIncluded=true] <code>true</code> if start time is included in the interval, <code>false</code> otherwise.
  995. * @param {Boolean} [options.isStopIncluded=true] <code>true</code> if stop time is included in the interval, <code>false</code> otherwise.
  996. * @param {Boolean} [options.leadingInterval=false] <code>true</code> if you want to add a interval from Iso8601.MINIMUM_VALUE to start time, <code>false</code> otherwise.
  997. * @param {Boolean} [options.trailingInterval=false] <code>true</code> if you want to add a interval from stop time to Iso8601.MAXIMUM_VALUE, <code>false</code> otherwise.
  998. * @param {Function} [options.dataCallback] A function that will be return the data that is called with each interval before it is added to the collection. If unspecified, the data will be the index in the collection.
  999. * @param {TimeIntervalCollection} [result] An existing instance to use for the result.
  1000. * @returns {TimeIntervalCollection} The modified result parameter or a new instance if none was provided.
  1001. */
  1002. TimeIntervalCollection.fromIso8601DurationArray = function (options, result) {
  1003. //>>includeStart('debug', pragmas.debug);
  1004. if (!defined(options)) {
  1005. throw new DeveloperError("options is required.");
  1006. }
  1007. if (!defined(options.epoch)) {
  1008. throw new DeveloperError("options.epoch is required.");
  1009. }
  1010. if (!defined(options.iso8601Durations)) {
  1011. throw new DeveloperError("options.iso8601Durations is required.");
  1012. }
  1013. //>>includeEnd('debug');
  1014. const epoch = options.epoch;
  1015. const iso8601Durations = options.iso8601Durations;
  1016. const relativeToPrevious = defaultValue(options.relativeToPrevious, false);
  1017. const julianDates = [];
  1018. let date, previousDate;
  1019. const length = iso8601Durations.length;
  1020. for (let i = 0; i < length; ++i) {
  1021. // Allow a duration of 0 on the first iteration, because then it is just the epoch
  1022. if (parseDuration(iso8601Durations[i], scratchDuration) || i === 0) {
  1023. if (relativeToPrevious && defined(previousDate)) {
  1024. date = addToDate(previousDate, scratchDuration);
  1025. } else {
  1026. date = addToDate(epoch, scratchDuration);
  1027. }
  1028. julianDates.push(date);
  1029. previousDate = date;
  1030. }
  1031. }
  1032. return TimeIntervalCollection.fromJulianDateArray(
  1033. {
  1034. julianDates: julianDates,
  1035. isStartIncluded: options.isStartIncluded,
  1036. isStopIncluded: options.isStopIncluded,
  1037. leadingInterval: options.leadingInterval,
  1038. trailingInterval: options.trailingInterval,
  1039. dataCallback: options.dataCallback,
  1040. },
  1041. result
  1042. );
  1043. };
  1044. export default TimeIntervalCollection;