Source: Map/LeafletDataSourceDisplay.js

const BoundingSphere = require('terriajs-cesium/Source/Core/BoundingSphere');
const BoundingSphereState = require('terriajs-cesium/Source/DataSources/BoundingSphereState');
const Check = require('terriajs-cesium/Source/Core/Check');
const createGuid = require('terriajs-cesium/Source/Core/createGuid');
const CustomDataSource = require('terriajs-cesium/Source/DataSources/CustomDataSource');
const defaultValue = require('terriajs-cesium/Source/Core/defaultValue');
const defined = require('terriajs-cesium/Source/Core/defined');
const defineProperties = require('terriajs-cesium/Source/Core/defineProperties');
const destroyObject = require('terriajs-cesium/Source/Core/destroyObject');
const EventHelper = require('terriajs-cesium/Source/Core/EventHelper');

/**
 * Visualizes a collection of {@link DataSource} instances in Leaflet.
 * @alias LeafletDataSourceDisplay
 * @constructor
 *
 * @param {Object} options Object with the following properties:
 * @param {Scene} options.scene The scene in which to display the data.
 * @param {DataSourceCollection} options.dataSourceCollection The data sources to display.
 * @param {LeafletDataSourceDisplay~VisualizersCallback} [options.visualizersCallback=LeafletDataSourceDisplay.defaultVisualizersCallback]
 *        A function which creates an array of visualizers used for visualization.
 *        If undefined, all standard visualizers are used.
 */
function LeafletDataSourceDisplay(options) {
    //>>includeStart('debug', pragmas.debug);
    Check.typeOf.object('options', options);
    Check.typeOf.object('options.scene', options.scene);
    Check.typeOf.object('options.dataSourceCollection', options.dataSourceCollection);
    //>>includeEnd('debug');

    this._displayID = createGuid();

    var scene = options.scene;
    var dataSourceCollection = options.dataSourceCollection;

    this._eventHelper = new EventHelper();
    this._eventHelper.add(dataSourceCollection.dataSourceAdded, this._onDataSourceAdded, this);
    this._eventHelper.add(dataSourceCollection.dataSourceRemoved, this._onDataSourceRemoved, this);
    this._eventHelper.add(dataSourceCollection.dataSourceMoved, this._onDataSourceMoved, this);

    this._dataSourceCollection = dataSourceCollection;
    this._scene = scene;
    this._visualizersCallback = defaultValue(options.visualizersCallback, LeafletDataSourceDisplay.defaultVisualizersCallback);

    for (var i = 0, len = dataSourceCollection.length; i < len; i++) {
        this._onDataSourceAdded(dataSourceCollection, dataSourceCollection.get(i));
    }

    var defaultDataSource = new CustomDataSource();
    this._onDataSourceAdded(undefined, defaultDataSource);
    this._defaultDataSource = defaultDataSource;

    this._ready = false;
}

/**
 * Gets or sets the default function which creates an array of visualizers used for visualization.
 * By default, this function uses all standard visualizers.
 *
 * @type {LeafletDataSourceDisplay~VisualizersCallback}
 */
LeafletDataSourceDisplay.defaultVisualizersCallback = function(scene, entityCluster, dataSource) {
    return [];
};

defineProperties(LeafletDataSourceDisplay.prototype, {
    /**
     * Gets the scene associated with this display.
     * @memberof LeafletDataSourceDisplay.prototype
     * @type {LeafletScene}
     */
    scene : {
        get : function() {
            return this._scene;
        }
    },
    /**
     * Gets the collection of data sources to display.
     * @memberof LeafletDataSourceDisplay.prototype
     * @type {DataSourceCollection}
     */
    dataSources : {
        get : function() {
            return this._dataSourceCollection;
        }
    },
    /**
     * Gets the default data source instance which can be used to
     * manually create and visualize entities not tied to
     * a specific data source. This instance is always available
     * and does not appear in the list dataSources collection.
     * @memberof LeafletDataSourceDisplay.prototype
     * @type {CustomDataSource}
     */
    defaultDataSource : {
        get : function() {
            return this._defaultDataSource;
        }
    },

    /**
     * Gets a value indicating whether or not all entities in the data source are ready
     * @memberof LeafletDataSourceDisplay.prototype
     * @type {Boolean}
     * @readonly
     */
    ready : {
        get : function() {
            return this._ready;
        }
    }
});

/**
 * Returns true if this object was destroyed; otherwise, false.
 * <br /><br />
 * If this object was destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
 *
 * @returns {Boolean} True if this object was destroyed; otherwise, false.
 *
 * @see LeafletDataSourceDisplay#destroy
 */
LeafletDataSourceDisplay.prototype.isDestroyed = function() {
    return false;
};

/**
 * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
 * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
 * <br /><br />
 * Once an object is destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
 * assign the return value (<code>undefined</code>) to the object as done in the example.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * LeafletDataSourceDisplay = dataSourceDisplay.destroy();
 *
 * @see DataSourceDisplay#isDestroyed
 */
LeafletDataSourceDisplay.prototype.destroy = function() {
    this._eventHelper.removeAll();

    var dataSourceCollection = this._dataSourceCollection;
    for (var i = 0, length = dataSourceCollection.length; i < length; ++i) {
        this._onDataSourceRemoved(this._dataSourceCollection, dataSourceCollection.get(i));
    }
    this._onDataSourceRemoved(undefined, this._defaultDataSource);

    return destroyObject(this);
};

/**
 * Updates the display to the provided time.
 *
 * @param {JulianDate} time The simulation time.
 * @returns {Boolean} True if all data sources are ready to be displayed, false otherwise.
 */
LeafletDataSourceDisplay.prototype.update = function(time) {
    //>>includeStart('debug', pragmas.debug);
    Check.defined('time', time);
    //>>includeEnd('debug');

    var result = true;

    var i;
    var x;
    var visualizers;
    var vLength;
    var dataSources = this._dataSourceCollection;
    var length = dataSources.length;
    for (i = 0; i < length; i++) {
        var dataSource = dataSources.get(i);
        if (defined(dataSource.update)) {
            result = dataSource.update(time) && result;
        }

        visualizers = dataSource._visualizersByDisplayID[this._displayID];
        vLength = visualizers.length;
        for (x = 0; x < vLength; x++) {
            result = visualizers[x].update(time) && result;
        }
    }

    visualizers = this._defaultDataSource._visualizersByDisplayID[this._displayID];
    vLength = visualizers.length;
    for (x = 0; x < vLength; x++) {
        result = visualizers[x].update(time) && result;
    }

    this._ready = result;

    return result;
};

var getBoundingSphereArrayScratch = [];
var getBoundingSphereBoundingSphereScratch = new BoundingSphere();

/**
 * Computes a bounding sphere which encloses the visualization produced for the specified entity.
 * The bounding sphere is in the fixed frame of the scene's globe.
 *
 * @param {Entity} entity The entity whose bounding sphere to compute.
 * @param {Boolean} allowPartial If true, pending bounding spheres are ignored and an answer will be returned from the currently available data.
 *                               If false, the the function will halt and return pending if any of the bounding spheres are pending.
 * @param {BoundingSphere} result The bounding sphere onto which to store the result.
 * @returns {BoundingSphereState} BoundingSphereState.DONE if the result contains the bounding sphere,
 *                       BoundingSphereState.PENDING if the result is still being computed, or
 *                       BoundingSphereState.FAILED if the entity has no visualization in the current scene.
 * @private
 */
LeafletDataSourceDisplay.prototype.getBoundingSphere = function(entity, allowPartial, result) {
    //>>includeStart('debug', pragmas.debug);
    Check.defined('entity', entity);
    Check.typeOf.bool('allowPartial', allowPartial);
    Check.defined('result', result);
    //>>includeEnd('debug');

    if (!this._ready) {
        return BoundingSphereState.PENDING;
    }

    var i;
    var length;
    var dataSource = this._defaultDataSource;
    if (!dataSource.entities.contains(entity)) {
        dataSource = undefined;

        var dataSources = this._dataSourceCollection;
        length = dataSources.length;
        for (i = 0; i < length; i++) {
            var d = dataSources.get(i);
            if (d.entities.contains(entity)) {
                dataSource = d;
                break;
            }
        }
    }

    if (!defined(dataSource)) {
        return BoundingSphereState.FAILED;
    }

    var boundingSpheres = getBoundingSphereArrayScratch;
    var tmp = getBoundingSphereBoundingSphereScratch;

    var count = 0;
    var state = BoundingSphereState.DONE;
    var visualizers = dataSource._visualizers;
    var visualizersLength = visualizers.length;

    for (i = 0; i < visualizersLength; i++) {
        var visualizer = visualizers[i];
        if (defined(visualizer.getBoundingSphere)) {
            state = visualizers[i].getBoundingSphere(entity, tmp);
            if (!allowPartial && state === BoundingSphereState.PENDING) {
                return BoundingSphereState.PENDING;
            } else if (state === BoundingSphereState.DONE) {
                boundingSpheres[count] = BoundingSphere.clone(tmp, boundingSpheres[count]);
                count++;
            }
        }
    }

    if (count === 0) {
        return BoundingSphereState.FAILED;
    }

    boundingSpheres.length = count;
    BoundingSphere.fromBoundingSpheres(boundingSpheres, result);
    return BoundingSphereState.DONE;
};

LeafletDataSourceDisplay.prototype._onDataSourceAdded = function(dataSourceCollection, dataSource) {
    var visualizers = this._visualizersCallback(this._scene, undefined, dataSource);

    dataSource._visualizersByDisplayID = dataSource._visualizersByDisplayID || {};
    dataSource._visualizersByDisplayID[this._displayID] = visualizers;

    dataSource._visualizers = dataSource._visualizers || [];
    dataSource._visualizers = dataSource._visualizers.concat(visualizers);
};

LeafletDataSourceDisplay.prototype._onDataSourceRemoved = function(dataSourceCollection, dataSource) {
    var visualizers = dataSource._visualizersByDisplayID[this._displayID];
    if (!defined(visualizers)) {
        return;
    }

    var length = visualizers.length;
    for (var i = 0; i < length; i++) {
        var visualizer = visualizers[i];
        visualizer.destroy();

        var index = dataSource._visualizers.indexOf(visualizer);
        dataSource._visualizers.splice(index, 1);
    }

    delete dataSource._visualizersByDisplayID[this._displayID];
};

LeafletDataSourceDisplay.prototype._onDataSourceMoved = function(dataSource, newIndex, oldIndex) {
    // Not currently implemented for Leaflet.
};

/**
 * A function which creates an array of visualizers used for visualization.
 * @callback LeafletDataSourceDisplay~VisualizersCallback
 *
 * @param {Scene} scene The scene to create visualizers for.
 * @param {DataSource} dataSource The data source to create visualizers for.
 * @returns {Visualizer[]} An array of visualizers used for visualization.
 *
 * @example
 * function createVisualizers(scene, dataSource) {
 *     return [new Cesium.BillboardVisualizer(scene, dataSource.entities)];
 * }
 */

module.exports = LeafletDataSourceDisplay;