Models/KmlCatalogItem.js

"use strict";

/*global require,Document*/

var defined = require("terriajs-cesium/Source/Core/defined").default;

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

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");
var i18next = require("i18next").default;

/**
 * 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);

Object.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 i18next.t("models.kml.name");
    }
  },

  /**
   * 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 = i18next.t(
        "models.kml.dataSourceErrorMessage"
      );
      result.serviceErrorMessage = i18next.t("models.kml.serviceErrorMessage");
      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")
        .default;

      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: i18next.t("models.kml.unexpectedTypeTitle"),
                message: i18next.t("models.kml.unexpectedTypeTitle", {
                  appName: that.terria.appName,
                  email:
                    '<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: i18next.t("models.kml.errorLoadingTitle"),
    message: i18next.t("models.kml.unexpectedTypeTitle", {
      appName: terria.appName,
      email:
        '<a href="mailto:' +
        terria.supportEmail +
        '">' +
        terria.supportEmail +
        "</a>."
    })
  });
}

module.exports = KmlCatalogItem;