Source: Models/KmlCatalogItem.js

'use strict';

/*global require,Document*/

var defined = require('terriajs-cesium/Source/Core/defined');
var defineProperties = require('terriajs-cesium/Source/Core/defineProperties');
var Ellipsoid = require('terriajs-cesium/Source/Core/Ellipsoid');
//var KmlDataSource = require('terriajs-cesium/Source/DataSources/KmlDataSource');
var knockout = require('terriajs-cesium/Source/ThirdParty/knockout');
var PolygonHierarchy = require('terriajs-cesium/Source/Core/PolygonHierarchy');
var sampleTerrain = require('terriajs-cesium/Source/Core/sampleTerrain');
var when = require('terriajs-cesium/Source/ThirdParty/when');

var DataSourceCatalogItem = require('./DataSourceCatalogItem');
var Metadata = require('./Metadata');
var TerriaError = require('../Core/TerriaError');
var inherit = require('../Core/inherit');
var promiseFunctionToExplicitDeferred = require('../Core/promiseFunctionToExplicitDeferred');
var proxyCatalogItemUrl = require('./proxyCatalogItemUrl');
var readXml = require('../Core/readXml');

/**
 * A {@link CatalogItem} representing KML or KMZ feature data.
 *
 * @alias KmlCatalogItem
 * @constructor
 * @extends CatalogItem
 *
 * @param {Terria} terria The Terria instance.
 * @param {String} [url] The URL from which to retrieve the KML or KMZ data.
 */
var KmlCatalogItem =  function(terria, url) {
    DataSourceCatalogItem.call(this, terria);

    this._dataSource = undefined;

    this.url = url;

    /**
     * Gets or sets the KML or KMZ data, represented as a binary Blob, DOM Document, or a Promise for one of those things.
     * If this property is set, {@link CatalogItem#url} is ignored.
     * This property is observable.
     * @type {Blob|Document|Promise}
     */
    this.data = undefined;

    /**
     * Gets or sets the URL from which the {@link KmlCatalogItem#data} was obtained.  This will be used
     * to resolve any resources linked in the KML file, if any.  This property is observable.
     * @type {String}
     */
    this.dataSourceUrl = undefined;

    knockout.track(this, ['data', 'dataSourceUrl']);
};

inherit(DataSourceCatalogItem, KmlCatalogItem);

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

    /**
     * Gets a human-readable name for this type of data source, 'KML'.
     * @memberOf KmlCatalogItem.prototype
     * @type {String}
     */
    typeName : {
        get : function() {
            return 'KML or KMZ';
        }
    },

    /**
     * Gets the metadata associated with this data source and the server that provided it, if applicable.
     * @memberOf KmlCatalogItem.prototype
     * @type {Metadata}
     */
    metadata : {
        get : function() {
            var result = new Metadata();
            result.isLoading = false;
            result.dataSourceErrorMessage = 'This data source does not have any details available.';
            result.serviceErrorMessage = 'This service does not have any details available.';
            return result;
        }
    },
    /**
     * Gets the data source associated with this catalog item.
     * @memberOf KmlCatalogItem.prototype
     * @type {DataSource}
     */
    dataSource : {
        get : function() {
            return this._dataSource;
        }
    }
});

KmlCatalogItem.prototype._getValuesThatInfluenceLoad = function() {
    return [this.url, this.data];
};

var kmzRegex = /\.kmz$/i;

KmlCatalogItem.prototype._load = function() {
    var codeSplittingDeferred = when.defer();

    var that = this;
    require.ensure('terriajs-cesium/Source/DataSources/KmlDataSource', function() {
        var KmlDataSource = require('terriajs-cesium/Source/DataSources/KmlDataSource');

        promiseFunctionToExplicitDeferred(codeSplittingDeferred, function() {
            // If there is an existing data source, remove it first.
            var reAdd = false;
            if (defined(that._dataSource)) {
                reAdd = that.terria.dataSources.remove(that._dataSource, true);
            }

            var dataSource = new KmlDataSource({
                // Currently we don't pass camera and canvas, which are technically required as of Cesium v1.23.
                // We get away with it because A) the code to check that they're supplied is removed
                // in release builds of Cesium, and B) the code that actually uses them (building network
                // link URLs) has guards so it won't totally fail if they're not supplied.  But for
                // proper network link support, we'll need to figure out how to get those things in here,
                // even though a single KmlCatalogItem can be shown on multiple maps.  Some refactoring of
                // Cesium will be required.
                proxy: {
                    // Don't cache resources referenced by the KML.
                    getURL: url => that.terria.corsProxy.getURLProxyIfNecessary(url, '0d')
                }
            });
            that._dataSource = dataSource;

            if (reAdd) {
                that.terria.dataSources.add(that._dataSource);
            }

            if (defined(that.data)) {
                return when(that.data, function(data) {
                    if (data instanceof Document) {
                        return dataSource.load(data, proxyCatalogItemUrl(that, that.dataSourceUrl, '1d')).then(function() {
                            doneLoading(that);
                        }).otherwise(function() {
                            errorLoading(that);
                        });
                    } else if (typeof Blob !== 'undefined' && data instanceof Blob) {
                        if (that.dataSourceUrl && that.dataSourceUrl.match(kmzRegex)) {
                            return dataSource.load(data, proxyCatalogItemUrl(that, that.dataSourceUrl, '1d')).then(function() {
                                doneLoading(that);
                            }).otherwise(function() {
                                errorLoading(that);
                            });
                        } else {
                            return readXml(data).then(function(xml) {
                                return dataSource.load(xml, proxyCatalogItemUrl(that, that.dataSourceUrl, '1d')).then(function() {
                                    doneLoading(that);
                                });
                            }).otherwise(function() {
                                errorLoading(that);
                            });
                        }
                    } else if (data instanceof String || typeof data === 'string') {
                        var parser = new DOMParser();
                        var xml;
                        try {
                            xml = parser.parseFromString(data, 'text/xml');
                        } catch (e) {
                        }

                        if (!xml || !xml.documentElement || xml.getElementsByTagName('parsererror').length > 0) {
                            errorLoading(that);
                        }
                        return dataSource.load(xml, proxyCatalogItemUrl(that, that.dataSourceUrl, '1d')).then(function() {
                            doneLoading(that);
                        }).otherwise(function() {
                            errorLoading(that);
                        });
                    } else {
                        throw new TerriaError({
                            sender: that,
                            title: 'Unexpected type of KML data',
                            message: '\
KmlCatalogItem.data is expected to be an XML Document, Blob, or File, but it was none of these. \
This may indicate a bug in '+that.terria.appName+' or incorrect use of the '+that.terria.appName+' API. \
If you believe it is a bug in '+that.terria.appName+', please report it by emailing \
<a href="mailto:'+that.terria.supportEmail+'">'+that.terria.supportEmail+'</a>.'
                        });
                    }
                });
            } else {
                return dataSource.load(proxyCatalogItemUrl(that, that.url, '1d')).then(function() {
                    doneLoading(that);
                }).otherwise(function() {
                    errorLoading(that);
                });
            }
        });
    }, 'Cesium-DataSources');

    return codeSplittingDeferred.promise;
};

function doneLoading(kmlItem) {
    var dataSource = kmlItem._dataSource;
    kmlItem.clock = dataSource.clock;

    // Clamp features to terrain.
    if (defined(kmlItem.terria.cesium)) {
        var positionsToSample = [];
        var correspondingCartesians = [];

        var entities = dataSource.entities.values;
        for (var i = 0; i < entities.length; ++i) {
            var entity = entities[i];

            var polygon = entity.polygon;
            if (defined(polygon)) {
                polygon.perPositionHeight = true;
                var polygonHierarchy = polygon.hierarchy.getValue(); // assuming hierarchy is not time-varying
                samplePolygonHierarchyPositions(polygonHierarchy, positionsToSample, correspondingCartesians);
            }
        }

        var terrainProvider = kmlItem.terria.cesium.scene.globe.terrainProvider;
        sampleTerrain(terrainProvider, 11, positionsToSample).then(function() {
            var i;
            for (i = 0; i < positionsToSample.length; ++i) {
                var position = positionsToSample[i];
                if (!defined(position.height)) {
                    continue;
                }

                Ellipsoid.WGS84.cartographicToCartesian(position, correspondingCartesians[i]);
            }

            // Force the polygons to be rebuilt.
            for (i = 0; i < entities.length; ++i) {
                var polygon = entities[i].polygon;
                if (!defined(polygon)) {
                    continue;
                }

                var existingHierarchy = polygon.hierarchy.getValue();
                polygon.hierarchy = new PolygonHierarchy(existingHierarchy.positions, existingHierarchy.holes);
            }
        });
    }
}

function samplePolygonHierarchyPositions(polygonHierarchy, positionsToSample, correspondingCartesians) {
    var positions = polygonHierarchy.positions;

    var i;
    for (i = 0; i < positions.length; ++i) {
        var position = positions[i];
        correspondingCartesians.push(position);
        positionsToSample.push(Ellipsoid.WGS84.cartesianToCartographic(position));
    }

    var holes = polygonHierarchy.holes;
    for (i = 0; i < holes.length; ++i) {
        samplePolygonHierarchyPositions(holes[i], positionsToSample, correspondingCartesians);
    }
}

function errorLoading(kmlItem) {
    var terria = kmlItem.terria;
    throw new TerriaError({
        sender: kmlItem,
        title: 'Error loading KML or KMZ',
        message: '\
An error occurred while loading a KML or KMZ file.  This may indicate that the file is invalid or that it \
is not supported by '+terria.appName+'.  If you would like assistance or further information, please email us \
at <a href="mailto:'+terria.supportEmail+'">'+terria.supportEmail+'</a>.'
    });
}

module.exports = KmlCatalogItem;