Source: DataSources/PathVisualizer.js

/*global define*/
define([
        '../Core/AssociativeArray',
        '../Core/Cartesian3',
        '../Core/defined',
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/JulianDate',
        '../Core/Matrix3',
        '../Core/Matrix4',
        '../Core/ReferenceFrame',
        '../Core/TimeInterval',
        '../Core/Transforms',
        '../Scene/PolylineCollection',
        '../Scene/SceneMode',
        './CompositePositionProperty',
        './ConstantPositionProperty',
        './MaterialProperty',
        './Property',
        './ReferenceProperty',
        './SampledPositionProperty',
        './ScaledPositionProperty',
        './TimeIntervalCollectionPositionProperty'
    ], function(
        AssociativeArray,
        Cartesian3,
        defined,
        destroyObject,
        DeveloperError,
        JulianDate,
        Matrix3,
        Matrix4,
        ReferenceFrame,
        TimeInterval,
        Transforms,
        PolylineCollection,
        SceneMode,
        CompositePositionProperty,
        ConstantPositionProperty,
        MaterialProperty,
        Property,
        ReferenceProperty,
        SampledPositionProperty,
        ScaledPositionProperty,
        TimeIntervalCollectionPositionProperty) {
    'use strict';

    var defaultResolution = 60.0;
    var defaultWidth = 1.0;

    var scratchTimeInterval = new TimeInterval();
    var subSampleCompositePropertyScratch = new TimeInterval();
    var 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) {
        var r = startingIndex;
        //Always step exactly on start (but only use it if it exists.)
        var tmp;
        tmp = property.getValueInReferenceFrame(start, referenceFrame, result[r]);
        if (defined(tmp)) {
            result[r++] = tmp;
        }

        var 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.
        var t = 0;
        var len = times.length;
        var current = times[t];
        var loopStop = stop;
        var sampling = false;
        var sampleStepsToTake;
        var sampleStepsTaken;
        var 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) {
                    var next = times[t + 1];
                    var 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) {
        var tmp;
        var i = 0;
        var index = startingIndex;
        var time = start;
        var stepSize = Math.max(maximumStep, 60);
        var 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;

        var index = startingIndex;
        var intervals = property.intervals;
        for (var i = 0; i < intervals.length; i++) {
            var interval = intervals.get(i);
            if (!TimeInterval.intersect(interval, subSampleIntervalPropertyScratch, scratchTimeInterval).isEmpty) {
                var 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());
                    }
                }
                var 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) {
        var 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;

        var index = startingIndex;
        var intervals = property.intervals;
        for (var i = 0; i < intervals.length; i++) {
            var interval = intervals.get(i);
            if (!TimeInterval.intersect(interval, subSampleCompositePropertyScratch, scratchTimeInterval).isEmpty) {
                var intervalStart = interval.start;
                var intervalStop = interval.stop;

                var sampleStart = start;
                if (JulianDate.greaterThan(intervalStart, sampleStart)) {
                    sampleStart = intervalStart;
                }

                var 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) {
        var innerProperty = property;

        while (innerProperty instanceof ReferenceProperty || innerProperty instanceof ScaledPositionProperty) {
            if (innerProperty instanceof ReferenceProperty) {
                innerProperty = innerProperty.resolvedProperty;
            }
            if (innerProperty instanceof ScaledPositionProperty) {
                innerProperty = innerProperty._value;
            }
        }

        if (innerProperty instanceof SampledPositionProperty) {
            var times = innerProperty._property._times;
            index = subSampleSampledProperty(property, start, stop, times, updateTime, referenceFrame, maximumStep, index, result);
        } else if (innerProperty instanceof CompositePositionProperty) {
            index = subSampleCompositeProperty(property, start, stop, updateTime, referenceFrame, maximumStep, index, result);
        } else if (innerProperty instanceof TimeIntervalCollectionPositionProperty) {
            index = subSampleIntervalProperty(property, start, stop, updateTime, referenceFrame, maximumStep, index, result);
        } else if (innerProperty instanceof ConstantPositionProperty) {
            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 = [];
        }

        var length = reallySubSample(property, start, stop, updateTime, referenceFrame, maximumStep, 0, result);
        result.length = length;
        return result;
    }

    var 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) {
            var 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) {
        var entity = item.entity;
        var pathGraphics = entity._path;
        var positionProperty = entity._position;

        var sampleStart;
        var sampleStop;
        var showProperty = pathGraphics._show;
        var polyline = item.polyline;
        var show = entity.isShowing && (!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) {
            var leadTime = Property.getValueOrUndefined(pathGraphics._leadTime, time);
            var trailTime = Property.getValueOrUndefined(pathGraphics._trailTime, time);
            var availability = entity._availability;
            var hasAvailability = defined(availability);
            var hasLeadTime = defined(leadTime);
            var 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) {
                    var start = availability.start;
                    var 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)) {
            var unusedIndexes = this._unusedIndexes;
            var length = unusedIndexes.length;
            if (length > 0) {
                var 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;
        }

        var 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) {
        var 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');

        var updaters = this._updaters;
        for ( var key in updaters) {
            if (updaters.hasOwnProperty(key)) {
                updaters[key].update(time);
            }
        }

        var items = this._items.values;
        for (var i = 0, len = items.length; i < len; i++) {
            var item = items[i];
            var entity = item.entity;
            var positionProperty = entity._position;

            var lastUpdater = item.updater;

            var frameToVisualize = ReferenceFrame.FIXED;
            if (this._scene.mode === SceneMode.SCENE3D) {
                frameToVisualize = positionProperty.referenceFrame;
            }

            var 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);

        var updaters = this._updaters;
        for ( var key in updaters) {
            if (updaters.hasOwnProperty(key)) {
                updaters[key].destroy();
            }
        }

        return destroyObject(this);
    };

    PathVisualizer.prototype._onCollectionChanged = function(entityCollection, added, removed, changed) {
        var i;
        var entity;
        var item;
        var 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)) {
                    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;

    return PathVisualizer;
});