Source: Models/CompositeCatalogItem.js

'use strict';

/*global require*/
var clone = require('terriajs-cesium/Source/Core/clone');
var defaultValue = require('terriajs-cesium/Source/Core/defaultValue');
var defined = require('terriajs-cesium/Source/Core/defined');
var defineProperties = require('terriajs-cesium/Source/Core/defineProperties');
var freezeObject = require('terriajs-cesium/Source/Core/freezeObject');
var knockout = require('terriajs-cesium/Source/ThirdParty/knockout');
var RuntimeError = require('terriajs-cesium/Source/Core/RuntimeError');
var when = require('terriajs-cesium/Source/ThirdParty/when');
var inherit = require('../Core/inherit');
var overrideProperty = require('../Core/overrideProperty');

var CatalogItem = require('./CatalogItem');
var createCatalogMemberFromType = require('./createCatalogMemberFromType');

/**
 * A {@link CatalogItem} composed of multiple other catalog items.  When this item is enabled or shown, the composed items are
 * enabled or shown as well.  Other properties, including {@link CatalogItem#rectangle},
 * {@link CatalogItem#clock}, and {@link CatalogItem#legendUrls}, are not composed in any way, so you should manually set those
 * properties on this object as appropriate.
 *
 * @alias CompositeCatalogItem
 * @constructor
 * @extends CatalogItem
 *
 * @param {Terria} terria The Terria instance.
 * @param {CatalogItem[]} [items] The items to compose.
 */
var CompositeCatalogItem = function(terria, items) {
	CatalogItem.call(this, terria);

	this.items = defined(items) ? items.slice() : [];

    knockout.track(this, ['items']);

    overrideProperty(this, 'legendUrls', {
        get: function() {
            if (!defined(this._legendUrls) || this._legendUrls.length === 0) {
                this._legendUrls = [];
                for (var i = 0; i < this.items.length; ++i) {
                    if (this.items[i].legendUrls) {
                        this._legendUrls = this._legendUrls.concat(this.items[i].legendUrls);
                    }
                }
            }
            return this._legendUrls;
        }
    });

	knockout.getObservable(this, 'items').subscribe(function() {
		for (var i = 0; i < this.items.length; ++i) {
			this.items[i].showInNowViewingWhenEnabled = false;
		}
	}, this);
};

inherit(CatalogItem, CompositeCatalogItem);

defineProperties(CompositeCatalogItem.prototype, {
    /**
     * Gets the type of data member represented by this instance.
     * @memberOf CompositeCatalogItem.prototype
     * @type {String}
     */
    type : {
        get : function() {
            return 'composite';
        }
    },

    /**
     * Gets a human-readable name for this type of data source, such as 'Web Map Service (WMS)'.
     * @memberOf CompositeCatalogItem.prototype
     * @type {String}
     */
    typeName : {
        get : function() {
            return 'Composite';
        }
    },

    /**
     * Gets the set of functions used to update individual properties in {@link CatalogMember#updateFromJson}.
     * When a property name in the returned object literal matches the name of a property on this instance, the value
     * will be called as a function and passed a reference to this instance, a reference to the source JSON object
     * literal, and the name of the property.
     * @memberOf CompositeCatalogItem.prototype
     * @type {Object}
     */
    updaters : {
        get : function() {
            return CompositeCatalogItem.defaultUpdaters;
        }
    },

    /**
     * Gets the set of functions used to serialize individual properties in {@link CatalogMember#serializeToJson}.
     * When a property name on the model matches the name of a property in the serializers object literal,
     * the value will be called as a function and passed a reference to the model, a reference to the destination
     * JSON object literal, and the name of the property.
     * @memberOf CompositeCatalogItem.prototype
     * @type {Object}
     */
    serializers : {
        get : function() {
            return CompositeCatalogItem.defaultSerializers;
        }
    },

    /**
     * Gets the set of names of the properties to be serialized for this object when {@link CatalogMember#serializeToJson} is called
     * for a share link.
     * @memberOf CompositeCatalogItem.prototype
     * @type {String[]}
     */
    propertiesForSharing : {
        get : function() {
            return CompositeCatalogItem.defaultPropertiesForSharing;
        }
    },

	/**
     * Gets a value indicating whether this data source, when enabled, can be reordered with respect to other data sources.
     * Data sources that cannot be reordered are typically displayed above reorderable data sources.
     * @memberOf CsvCatalogItem.prototype
     * @type {Boolean}
     */
    supportsReordering : {
        get : function() {
            // we will use the heuristic that the composite supports reordering only
            // if all its subitems do
            var result = true;
            for (var itemIndex = 0; itemIndex < this.items.length; ++itemIndex) {
                var item = this.items[itemIndex];
                result = result && item.supportsReordering;
            }
            return result;
        }
    }
});

/**
 * Gets or sets the set of default updater functions to use in {@link CatalogMember#updateFromJson}.  Types derived from this type
 * should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#updaters} property.
 * @type {Object}
 */
 // Adapted from CatalogGroup
CompositeCatalogItem.defaultUpdaters = clone(CatalogItem.defaultUpdaters);

CompositeCatalogItem.defaultUpdaters.items = function(compositeCatalogItem, json, propertyName, options) {
    // Let the item finish loading first.  Otherwise, these changes could get clobbered by the load.
    return when(compositeCatalogItem.load(), function() {
        var promises = [];
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        var items = json.items;
        for (var itemIndex = 0; itemIndex < items.length; ++itemIndex) {
            var item = items[itemIndex];
            if (!defined(item.type)) {
                throw new RuntimeError('Each item must have a type.');
            }
            if (item.type === 'composite') {
                throw new RuntimeError('Composites cannot include composite items.');
            }
            var existingItem = createCatalogMemberFromType(item.type, compositeCatalogItem.terria);
            compositeCatalogItem.add(existingItem);
            promises.push(existingItem.updateFromJson(item, options));
        }

        return when.all(promises);
    });
};

CompositeCatalogItem.defaultUpdaters.isLoading = function(compositeCatalogItem, json, propertyName) {};

freezeObject(CompositeCatalogItem.defaultUpdaters);

/**
 * Gets or sets the set of default serializer functions to use in {@link CatalogMember#serializeToJson}.  Types derived from this type
 * should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#serializers} property.
 * @type {Object}
 */
CompositeCatalogItem.defaultSerializers = clone(CatalogItem.defaultSerializers);

CompositeCatalogItem.defaultSerializers.items = function(compositeCatalogItem, json, propertyName, options) {
    var items = json.items = [];

    for (var i = 0; i < compositeCatalogItem.items.length; ++i) {
        var item = compositeCatalogItem.items[i].serializeToJson(options);
        if (defined(item)) {
            items.push(item);
        }
    }
};

CompositeCatalogItem.defaultSerializers.isLoading = function(compositeCatalogItem, json, propertyName, options) {};

freezeObject(CompositeCatalogItem.defaultSerializers);

/**
 * Gets or sets the default set of properties that are serialized when serializing a {@link CatalogItem}-derived object
 * for a share link.
 * @type {String[]}
 */
CompositeCatalogItem.defaultPropertiesForSharing = clone(CatalogItem.defaultPropertiesForSharing);
CompositeCatalogItem.defaultPropertiesForSharing.push('items');

freezeObject(CompositeCatalogItem.defaultPropertiesForSharing);

//

CompositeCatalogItem.prototype._load = function() {
	return when.all(
        this.items.map(function(item) {
            return item.load();
        })
    );
};

CompositeCatalogItem.prototype._getValuesThatInfluenceLoad = function() {
	var result = [];

	for (var i = 0; i < this.items.length; ++i) {
		result.push.apply(result, this.items[i]._getValuesThatInfluenceLoad());
	}

	return result;
};

CompositeCatalogItem.prototype._enable = function() {
    var i;
    try {
    	for (i = 0; i < this.items.length; ++i) {
    		this.items[i]._enable();
    	}
    } catch(e) {
        for (var j = 0; j < i; ++j) {
            this.items[j]._disable();
        }
        this.isEnabled = false;
        throw e;
    }
};

CompositeCatalogItem.prototype._disable = function() {
	for (var i = 0; i < this.items.length; ++i) {
		this.items[i]._disable();
	}
};

CompositeCatalogItem.prototype._show = function() {
    var i;
    try {
    	for (i = 0; i < this.items.length; ++i) {
    		this.items[i]._show();
    	}
    } catch(e) {
        for (var j = 0; j < i; ++j) {
            this.items[j]._hide();
        }
        this.isShown = false;
        throw e;
    }
};

CompositeCatalogItem.prototype._hide = function() {
	for (var i = 0; i < this.items.length; ++i) {
		this.items[i]._hide();
	}
};

/**
 * Adds an item or group to this composite.
 *
 * @param {CatalogMember} item The item to add.
 */
CompositeCatalogItem.prototype.add = function(item) {
    this.items.push(item);
};


module.exports = CompositeCatalogItem;