text: import AssociativeArray from "../Core/AssociativeArray.js";
  import Cartesian3 from "../Core/Cartesian3.js";
  import defined from "../Core/defined.js";
  import destroyObject from "../Core/destroyObject.js";
  import DeveloperError from "../Core/DeveloperError.js";
  import JulianDate from "../Core/JulianDate.js";
  import Matrix3 from "../Core/Matrix3.js";
  import Matrix4 from "../Core/Matrix4.js";
  import ReferenceFrame from "../Core/ReferenceFrame.js";
  import TimeInterval from "../Core/TimeInterval.js";
  import Transforms from "../Core/Transforms.js";
  import PolylineCollection from "../Scene/PolylineCollection.js";
  import SceneMode from "../Scene/SceneMode.js";
  import CompositePositionProperty from "./CompositePositionProperty.js";
  import ConstantPositionProperty from "./ConstantPositionProperty.js";
  import MaterialProperty from "./MaterialProperty.js";
  import Property from "./Property.js";
  import ReferenceProperty from "./ReferenceProperty.js";
  import SampledPositionProperty from "./SampledPositionProperty.js";
  import ScaledPositionProperty from "./ScaledPositionProperty.js";
  import TimeIntervalCollectionPositionProperty from "./TimeIntervalCollectionPositionProperty.js";
  
  const defaultResolution = 60.0;
  const defaultWidth = 1.0;
  
  const scratchTimeInterval = new TimeInterval();
  const subSampleCompositePropertyScratch = new TimeInterval();
  const subSampleIntervalPropertyScratch = new TimeInterval();
  
  function EntityData(entity) {
    this.entity = entity;
    this.polyline = undefined;
    this.index = undefined;
    this.updater = undefined;
  }
  
  function subSampleSampledProperty(
    property,
    start,
    stop,
    times,
    updateTime,
    referenceFrame,
    maximumStep,
    startingIndex,
    result
  ) {
    let r = startingIndex;
    //Always step exactly on start (but only use it if it exists.)
    let tmp;
    tmp = property.getValueInReferenceFrame(start, referenceFrame, result[r]);
    if (defined(tmp)) {
      result[r++] = tmp;
    }
  
    let steppedOnNow =
      !defined(updateTime) ||
      JulianDate.lessThanOrEquals(updateTime, start) ||
      JulianDate.greaterThanOrEquals(updateTime, stop);
  
    //Iterate over all interval times and add the ones that fall in our
    //time range.  Note that times can contain data outside of
    //the intervals range.  This is by design for use with interpolation.
    let t = 0;
    const len = times.length;
    let current = times[t];
    const loopStop = stop;
    let sampling = false;
    let sampleStepsToTake;
    let sampleStepsTaken;
    let sampleStepSize;
  
    while (t < len) {
      if (!steppedOnNow && JulianDate.greaterThanOrEquals(current, updateTime)) {
        tmp = property.getValueInReferenceFrame(
          updateTime,
          referenceFrame,
          result[r]
        );
        if (defined(tmp)) {
          result[r++] = tmp;
        }
        steppedOnNow = true;
      }
      if (
        JulianDate.greaterThan(current, start) &&
        JulianDate.lessThan(current, loopStop) &&
        !current.equals(updateTime)
      ) {
        tmp = property.getValueInReferenceFrame(
          current,
          referenceFrame,
          result[r]
        );
        if (defined(tmp)) {
          result[r++] = tmp;
        }
      }
  
      if (t < len - 1) {
        if (maximumStep > 0 && !sampling) {
          const next = times[t + 1];
          const secondsUntilNext = JulianDate.secondsDifference(next, current);
          sampling = secondsUntilNext > maximumStep;
  
          if (sampling) {
            sampleStepsToTake = Math.ceil(secondsUntilNext / maximumStep);
            sampleStepsTaken = 0;
            sampleStepSize = secondsUntilNext / Math.max(sampleStepsToTake, 2);
            sampleStepsToTake = Math.max(sampleStepsToTake - 1, 1);
          }
        }
  
        if (sampling && sampleStepsTaken < sampleStepsToTake) {
          current = JulianDate.addSeconds(
            current,
            sampleStepSize,
            new JulianDate()
          );
          sampleStepsTaken++;
          continue;
        }
      }
      sampling = false;
      t++;
      current = times[t];
    }
  
    //Always step exactly on stop (but only use it if it exists.)
    tmp = property.getValueInReferenceFrame(stop, referenceFrame, result[r]);
    if (defined(tmp)) {
      result[r++] = tmp;
    }
  
    return r;
  }
  
  function subSampleGenericProperty(
    property,
    start,
    stop,
    updateTime,
    referenceFrame,
    maximumStep,
    startingIndex,
    result
  ) {
    let tmp;
    let i = 0;
    let index = startingIndex;
    let time = start;
    const stepSize = Math.max(maximumStep, 60);
    let steppedOnNow =
      !defined(updateTime) ||
      JulianDate.lessThanOrEquals(updateTime, start) ||
      JulianDate.greaterThanOrEquals(updateTime, stop);
    while (JulianDate.lessThan(time, stop)) {
      if (!steppedOnNow && JulianDate.greaterThanOrEquals(time, updateTime)) {
        steppedOnNow = true;
        tmp = property.getValueInReferenceFrame(
          updateTime,
          referenceFrame,
          result[index]
        );
        if (defined(tmp)) {
          result[index] = tmp;
          index++;
        }
      }
      tmp = property.getValueInReferenceFrame(
        time,
        referenceFrame,
        result[index]
      );
      if (defined(tmp)) {
        result[index] = tmp;
        index++;
      }
      i++;
      time = JulianDate.addSeconds(start, stepSize * i, new JulianDate());
    }
    //Always sample stop.
    tmp = property.getValueInReferenceFrame(stop, referenceFrame, result[index]);
    if (defined(tmp)) {
      result[index] = tmp;
      index++;
    }
    return index;
  }
  
  function subSampleIntervalProperty(
    property,
    start,
    stop,
    updateTime,
    referenceFrame,
    maximumStep,
    startingIndex,
    result
  ) {
    subSampleIntervalPropertyScratch.start = start;
    subSampleIntervalPropertyScratch.stop = stop;
  
    let index = startingIndex;
    const intervals = property.intervals;
    for (let i = 0; i < intervals.length; i++) {
      const interval = intervals.get(i);
      if (
        !TimeInterval.intersect(
          interval,
          subSampleIntervalPropertyScratch,
          scratchTimeInterval
        ).isEmpty
      ) {
        let time = interval.start;
        if (!interval.isStartIncluded) {
          if (interval.isStopIncluded) {
            time = interval.stop;
          } else {
            time = JulianDate.addSeconds(
              interval.start,
              JulianDate.secondsDifference(interval.stop, interval.start) / 2,
              new JulianDate()
            );
          }
        }
        const tmp = property.getValueInReferenceFrame(
          time,
          referenceFrame,
          result[index]
        );
        if (defined(tmp)) {
          result[index] = tmp;
          index++;
        }
      }
    }
    return index;
  }
  
  function subSampleConstantProperty(
    property,
    start,
    stop,
    updateTime,
    referenceFrame,
    maximumStep,
    startingIndex,
    result
  ) {
    const tmp = property.getValueInReferenceFrame(
      start,
      referenceFrame,
      result[startingIndex]
    );
    if (defined(tmp)) {
      result[startingIndex++] = tmp;
    }
    return startingIndex;
  }
  
  function subSampleCompositeProperty(
    property,
    start,
    stop,
    updateTime,
    referenceFrame,
    maximumStep,
    startingIndex,
    result
  ) {
    subSampleCompositePropertyScratch.start = start;
    subSampleCompositePropertyScratch.stop = stop;
  
    let index = startingIndex;
    const intervals = property.intervals;
    for (let i = 0; i < intervals.length; i++) {
      const interval = intervals.get(i);
      if (
        !TimeInterval.intersect(
          interval,
          subSampleCompositePropertyScratch,
          scratchTimeInterval
        ).isEmpty
      ) {
        const intervalStart = interval.start;
        const intervalStop = interval.stop;
  
        let sampleStart = start;
        if (JulianDate.greaterThan(intervalStart, sampleStart)) {
          sampleStart = intervalStart;
        }
  
        let sampleStop = stop;
        if (JulianDate.lessThan(intervalStop, sampleStop)) {
          sampleStop = intervalStop;
        }
  
        index = reallySubSample(
          interval.data,
          sampleStart,
          sampleStop,
          updateTime,
          referenceFrame,
          maximumStep,
          index,
          result
        );
      }
    }
    return index;
  }
  
  function reallySubSample(
    property,
    start,
    stop,
    updateTime,
    referenceFrame,
    maximumStep,
    index,
    result
  ) {
    //Unwrap any references until we have the actual property.
    while (property instanceof ReferenceProperty) {
      property = property.resolvedProperty;
    }
  
    if (property instanceof SampledPositionProperty) {
      const times = property._property._times;
      index = subSampleSampledProperty(
        property,
        start,
        stop,
        times,
        updateTime,
        referenceFrame,
        maximumStep,
        index,
        result
      );
    } else if (property instanceof CompositePositionProperty) {
      index = subSampleCompositeProperty(
        property,
        start,
        stop,
        updateTime,
        referenceFrame,
        maximumStep,
        index,
        result
      );
    } else if (property instanceof TimeIntervalCollectionPositionProperty) {
      index = subSampleIntervalProperty(
        property,
        start,
        stop,
        updateTime,
        referenceFrame,
        maximumStep,
        index,
        result
      );
    } else if (
      property instanceof ConstantPositionProperty ||
      (property instanceof ScaledPositionProperty &&
        Property.isConstant(property))
    ) {
      index = subSampleConstantProperty(
        property,
        start,
        stop,
        updateTime,
        referenceFrame,
        maximumStep,
        index,
        result
      );
    } else {
      //Fallback to generic sampling.
      index = subSampleGenericProperty(
        property,
        start,
        stop,
        updateTime,
        referenceFrame,
        maximumStep,
        index,
        result
      );
    }
    return index;
  }
  
  function subSample(
    property,
    start,
    stop,
    updateTime,
    referenceFrame,
    maximumStep,
    result
  ) {
    if (!defined(result)) {
      result = [];
    }
  
    const length = reallySubSample(
      property,
      start,
      stop,
      updateTime,
      referenceFrame,
      maximumStep,
      0,
      result
    );
    result.length = length;
    return result;
  }
  
  const toFixedScratch = new Matrix3();
  function PolylineUpdater(scene, referenceFrame) {
    this._unusedIndexes = [];
    this._polylineCollection = new PolylineCollection();
    this._scene = scene;
    this._referenceFrame = referenceFrame;
    scene.primitives.add(this._polylineCollection);
  }
  
  PolylineUpdater.prototype.update = function (time) {
    if (this._referenceFrame === ReferenceFrame.INERTIAL) {
      let toFixed = Transforms.computeIcrfToFixedMatrix(time, toFixedScratch);
      if (!defined(toFixed)) {
        toFixed = Transforms.computeTemeToPseudoFixedMatrix(time, toFixedScratch);
      }
      Matrix4.fromRotationTranslation(
        toFixed,
        Cartesian3.ZERO,
        this._polylineCollection.modelMatrix
      );
    }
  };
  
  PolylineUpdater.prototype.updateObject = function (time, item) {
    const entity = item.entity;
    const pathGraphics = entity._path;
    const positionProperty = entity._position;
  
    let sampleStart;
    let sampleStop;
    const showProperty = pathGraphics._show;
    let polyline = item.polyline;
    let show =
      entity.isShowing &&
      entity.isAvailable(time) &&
      (!defined(showProperty) || showProperty.getValue(time));
  
    //While we want to show the path, there may not actually be anything to show
    //depending on lead/trail settings.  Compute the interval of the path to
    //show and check against actual availability.
    if (show) {
      const leadTime = Property.getValueOrUndefined(pathGraphics._leadTime, time);
      const trailTime = Property.getValueOrUndefined(
        pathGraphics._trailTime,
        time
      );
      const availability = entity._availability;
      const hasAvailability = defined(availability);
      const hasLeadTime = defined(leadTime);
      const hasTrailTime = defined(trailTime);
  
      //Objects need to have either defined availability or both a lead and trail time in order to
      //draw a path (since we can't draw "infinite" paths.
      show = hasAvailability || (hasLeadTime && hasTrailTime);
  
      //The final step is to compute the actual start/stop times of the path to show.
      //If current time is outside of the availability interval, there's a chance that
      //we won't have to draw anything anyway.
      if (show) {
        if (hasTrailTime) {
          sampleStart = JulianDate.addSeconds(time, -trailTime, new JulianDate());
        }
        if (hasLeadTime) {
          sampleStop = JulianDate.addSeconds(time, leadTime, new JulianDate());
        }
  
        if (hasAvailability) {
          const start = availability.start;
          const stop = availability.stop;
  
          if (!hasTrailTime || JulianDate.greaterThan(start, sampleStart)) {
            sampleStart = start;
          }
  
          if (!hasLeadTime || JulianDate.lessThan(stop, sampleStop)) {
            sampleStop = stop;
          }
        }
        show = JulianDate.lessThan(sampleStart, sampleStop);
      }
    }
  
    if (!show) {
      //don't bother creating or updating anything else
      if (defined(polyline)) {
        this._unusedIndexes.push(item.index);
        item.polyline = undefined;
        polyline.show = false;
        item.index = undefined;
      }
      return;
    }
  
    if (!defined(polyline)) {
      const unusedIndexes = this._unusedIndexes;
      const length = unusedIndexes.length;
      if (length > 0) {
        const index = unusedIndexes.pop();
        polyline = this._polylineCollection.get(index);
        item.index = index;
      } else {
        item.index = this._polylineCollection.length;
        polyline = this._polylineCollection.add();
      }
      polyline.id = entity;
      item.polyline = polyline;
    }
  
    const resolution = Property.getValueOrDefault(
      pathGraphics._resolution,
      time,
      defaultResolution
    );
  
    polyline.show = true;
    polyline.positions = subSample(
      positionProperty,
      sampleStart,
      sampleStop,
      time,
      this._referenceFrame,
      resolution,
      polyline.positions.slice()
    );
    polyline.material = MaterialProperty.getValue(
      time,
      pathGraphics._material,
      polyline.material
    );
    polyline.width = Property.getValueOrDefault(
      pathGraphics._width,
      time,
      defaultWidth
    );
    polyline.distanceDisplayCondition = Property.getValueOrUndefined(
      pathGraphics._distanceDisplayCondition,
      time,
      polyline.distanceDisplayCondition
    );
  };
  
  PolylineUpdater.prototype.removeObject = function (item) {
    const polyline = item.polyline;
    if (defined(polyline)) {
      this._unusedIndexes.push(item.index);
      item.polyline = undefined;
      polyline.show = false;
      polyline.id = undefined;
      item.index = undefined;
    }
  };
  
  PolylineUpdater.prototype.destroy = function () {
    this._scene.primitives.remove(this._polylineCollection);
    return destroyObject(this);
  };
  
  /**
   * A {@link Visualizer} which maps {@link Entity#path} to a {@link Polyline}.
   * @alias PathVisualizer
   * @constructor
   *
   * @param {Scene} scene The scene the primitives will be rendered in.
   * @param {EntityCollection} entityCollection The entityCollection to visualize.
   */
  function PathVisualizer(scene, entityCollection) {
    //>>includeStart('debug', pragmas.debug);
    if (!defined(scene)) {
      throw new DeveloperError("scene is required.");
    }
    if (!defined(entityCollection)) {
      throw new DeveloperError("entityCollection is required.");
    }
    //>>includeEnd('debug');
  
    entityCollection.collectionChanged.addEventListener(
      PathVisualizer.prototype._onCollectionChanged,
      this
    );
  
    this._scene = scene;
    this._updaters = {};
    this._entityCollection = entityCollection;
    this._items = new AssociativeArray();
  
    this._onCollectionChanged(entityCollection, entityCollection.values, [], []);
  }
  
  /**
   * Updates all of the primitives created by this visualizer to match their
   * Entity counterpart at the given time.
   *
   * @param {JulianDate} time The time to update to.
   * @returns {Boolean} This function always returns true.
   */
  PathVisualizer.prototype.update = function (time) {
    //>>includeStart('debug', pragmas.debug);
    if (!defined(time)) {
      throw new DeveloperError("time is required.");
    }
    //>>includeEnd('debug');
  
    const updaters = this._updaters;
    for (const key in updaters) {
      if (updaters.hasOwnProperty(key)) {
        updaters[key].update(time);
      }
    }
  
    const items = this._items.values;
    if (
      items.length === 0 &&
      defined(this._updaters) &&
      Object.keys(this._updaters).length > 0
    ) {
      for (const u in updaters) {
        if (updaters.hasOwnProperty(u)) {
          updaters[u].destroy();
        }
      }
      this._updaters = {};
    }
  
    for (let i = 0, len = items.length; i < len; i++) {
      const item = items[i];
      const entity = item.entity;
      const positionProperty = entity._position;
  
      const lastUpdater = item.updater;
  
      let frameToVisualize = ReferenceFrame.FIXED;
      if (this._scene.mode === SceneMode.SCENE3D) {
        frameToVisualize = positionProperty.referenceFrame;
      }
  
      let currentUpdater = this._updaters[frameToVisualize];
  
      if (lastUpdater === currentUpdater && defined(currentUpdater)) {
        currentUpdater.updateObject(time, item);
        continue;
      }
  
      if (defined(lastUpdater)) {
        lastUpdater.removeObject(item);
      }
  
      if (!defined(currentUpdater)) {
        currentUpdater = new PolylineUpdater(this._scene, frameToVisualize);
        currentUpdater.update(time);
        this._updaters[frameToVisualize] = currentUpdater;
      }
  
      item.updater = currentUpdater;
      if (defined(currentUpdater)) {
        currentUpdater.updateObject(time, item);
      }
    }
    return true;
  };
  
  /**
   * Returns true if this object was destroyed; otherwise, false.
   *
   * @returns {Boolean} True if this object was destroyed; otherwise, false.
   */
  PathVisualizer.prototype.isDestroyed = function () {
    return false;
  };
  
  /**
   * Removes and destroys all primitives created by this instance.
   */
  PathVisualizer.prototype.destroy = function () {
    this._entityCollection.collectionChanged.removeEventListener(
      PathVisualizer.prototype._onCollectionChanged,
      this
    );
  
    const updaters = this._updaters;
    for (const key in updaters) {
      if (updaters.hasOwnProperty(key)) {
        updaters[key].destroy();
      }
    }
  
    return destroyObject(this);
  };
  
  PathVisualizer.prototype._onCollectionChanged = function (
    entityCollection,
    added,
    removed,
    changed
  ) {
    let i;
    let entity;
    let item;
    const items = this._items;
  
    for (i = added.length - 1; i > -1; i--) {
      entity = added[i];
      if (defined(entity._path) && defined(entity._position)) {
        items.set(entity.id, new EntityData(entity));
      }
    }
  
    for (i = changed.length - 1; i > -1; i--) {
      entity = changed[i];
      if (defined(entity._path) && defined(entity._position)) {
        if (!items.contains(entity.id)) {
          items.set(entity.id, new EntityData(entity));
        }
      } else {
        item = items.get(entity.id);
        if (defined(item)) {
          if (defined(item.updater)) {
            item.updater.removeObject(item);
          }
          items.remove(entity.id);
        }
      }
    }
  
    for (i = removed.length - 1; i > -1; i--) {
      entity = removed[i];
      item = items.get(entity.id);
      if (defined(item)) {
        if (defined(item.updater)) {
          item.updater.removeObject(item);
        }
        items.remove(entity.id);
      }
    }
  };
  
  //for testing
  PathVisualizer._subSample = subSample;
  export default PathVisualizer;
  