Models/NowViewing.js

"use strict";

/*global require*/
var CesiumEvent = require("terriajs-cesium/Source/Core/Event").default;
var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;

var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
  .default;
var EventHelper = require("terriajs-cesium/Source/Core/EventHelper").default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var raiseErrorToUser = require("./raiseErrorToUser");

/**
 * The model for the "Now Viewing" pane.
 */
var NowViewing = function(terria) {
  this._terria = terria;
  this._eventSubscriptions = new EventHelper();

  /**
   * Gets the list of items that we are "now viewing".  It is recommended that you use
   * the methods on this instance instead of manipulating the list of items directly.
   * This property is observable.
   * @type {CatalogItem[]}
   */
  this.items = [];

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

  this.showNowViewingRequested = new CesiumEvent();

  this._eventSubscriptions.add(
    this.terria.beforeViewerChanged,
    function() {
      beforeViewerChanged(this);
    },
    this
  );

  this._eventSubscriptions.add(
    this.terria.afterViewerChanged,
    function() {
      afterViewerChanged(this);
    },
    this
  );
};

Object.defineProperties(NowViewing.prototype, {
  /**
   * Gets the Terria instance.
   * @memberOf NowViewing.prototype
   * @type  {Terria}
   */
  terria: {
    get: function() {
      return this._terria;
    }
  },

  /**
   * Gets a value indicating whether the "Now Viewing" pane has one or more items.
   * @memberOf NowViewing.prototype
   * @type {Boolean}
   */
  hasItems: {
    get: function() {
      return this.items.length > 0;
    }
  },

  /**
   * Gets a value indicating whether the "Now Viewing" pane has at list own data
   * source that is currently shown.
   * @memberOf NowViewing.prototype
   * @type {Boolean}
   */
  hasShownItems: {
    get: function() {
      for (var i = 0; i < this.items.length; ++i) {
        if (this.items[i].isShown) {
          return true;
        }
      }
      return false;
    }
  },

  hasChildren: {
    get: function() {
      return this.items.length > 0;
    }
  }
});

/**
 * Destroys this instance, including unsubscribing it from any events.
 */
NowViewing.prototype.destroy = function() {
  this._eventSubscriptions.removeAll();
};

/**
 * Adds an item to the "Now Viewing" pane.
 *
 * @param {CatalogMember} item The item to add.
 */
NowViewing.prototype.add = function(item, index) {
  index = defaultValue(index, 0);

  // Keep reorderable data sources (e.g.: imagery layers) below non-orderable ones (e.g.: GeoJSON).
  if (item.supportsReordering) {
    while (index < this.items.length && !this.items[index].supportsReordering) {
      ++index;
    }
  } else {
    while (
      index > 0 &&
      this.items.length > 0 &&
      this.items[index - 1].supportsReordering
    ) {
      --index;
    }
  }

  if (!item.keepOnTop) {
    while (
      index < this.items.length &&
      this.items[index].keepOnTop &&
      this.items[index].supportsReordering === item.supportsReordering
    ) {
      ++index;
    }
  } else {
    while (
      index > 0 &&
      this.items.length > 0 &&
      this.items[index - 1].keepOnTop &&
      this.items[index - 1] === item.supportsReordering
    ) {
      --index;
    }
  }

  this.items.splice(index, 0, item);
};

/**
 * Removes an item from the "Now Viewing" pane and from the map.
 *
 * @param {CatalogMember} item The item to remove.
 */
NowViewing.prototype.remove = function(item) {
  item.isEnabled = false;
  this.items.remove(item);
};

/**
 * Removes all data sources from the "Now Viewing" pane and from the map.
 */
NowViewing.prototype.removeAll = function() {
  // Work backwards through the list of items because setting isEnabled=false
  // will usually remove the item from the list.
  for (var i = this.items.length - 1; i >= 0; --i) {
    this.items[i].isEnabled = false;
  }

  this.items.removeAll();
};

/**
 * Raises an item, making it displayed on top of the item that is currently above it.  If it
 * is nonsensical to move this item up (e.g. it is already at the top), this method does nothing.
 *
 * @param {CatalogMember} item The item to raise.
 * @param {Number} [index] The index of the item of the list, if it is already known.
 */
NowViewing.prototype.raise = function(item, index) {
  if (defined(index)) {
    if (this.items[index] !== item) {
      throw new DeveloperError("The provided index is not correct.");
    }
  } else {
    index = this.items.indexOf(item);
    if (index < 0) {
      return;
    }
  }

  if (index === 0) {
    return;
  }

  // Don't allow reorderable data sources to move above non-reorderable ones.
  if (item.supportsReordering && !this.items[index - 1].supportsReordering) {
    return;
  }

  var terria = this.terria;

  terria.currentViewer.raise(index);

  this.items.splice(index, 1);
  this.items.splice(index - 1, 0, item);

  terria.currentViewer.updateLayerOrderAfterReorder();

  terria.currentViewer.notifyRepaintRequired();
};

/**
 * Lowers an item, making it displayed below the item that is currently below it.  If it
 * is nonsensical to move this item down (e.g. it is already at the bottom), this method does nothing.
 *
 * @param {CatalogMember} item The item to lower.
 * @param {Number} [index] The index of the item of the list, if it is already known.
 */
NowViewing.prototype.lower = function(item, index) {
  if (defined(index)) {
    if (this.items[index] !== item) {
      throw new DeveloperError("The provided index is not correct.");
    }
  } else {
    index = this.items.indexOf(item);
    if (index < 0) {
      return;
    }
  }

  if (index === this.items.length - 1) {
    return;
  }

  var itemBelow = this.items[index + 1];

  // Don't allow non-reorderable data sources to move below reorderable ones.
  if (!item.supportsReordering && itemBelow.supportsReordering) {
    return;
  }

  var terria = this.terria;

  terria.currentViewer.lower(index);

  this.items.splice(index, 1);
  this.items.splice(index + 1, 0, item);

  terria.currentViewer.updateLayerOrderAfterReorder();

  terria.currentViewer.notifyRepaintRequired();
};

/**
 * Toggles the {@link NowViewing#isOpen} flag.  If it's open, it is closed.  If it's closed, it is opened.
 */
NowViewing.prototype.toggleOpen = function() {
  this.isOpen = !this.isOpen;
};

/**
 * Records the the index of each data source in the Now Viewing list in a {@link CatalogItem#nowViewingIndex} property
 * on the data source.  This is used to save the state of the Now Viewing list and is not intended for general
 * use.
 * @private
 */
NowViewing.prototype.recordNowViewingIndices = function() {
  for (var i = 0; i < this.items.length; ++i) {
    this.items[i].nowViewingIndex = i;
  }
};

/**
 * Sorts the data sources in the Now Viewing list by their {@link CatalogItem#nowViewingIndex} properties.  This is used
 * to restore the state of the Now Viewing list and is not intended for general use.
 * @private
 */
NowViewing.prototype.sortByNowViewingIndices = function() {
  var sortedItems = this.items.slice();
  sortedItems.sort(function(a, b) {
    return a.nowViewingIndex - b.nowViewingIndex;
  });

  for (var i = 0; i < sortedItems.length; ++i) {
    var item = sortedItems[i];
    var existingIndex = this.items.indexOf(item);
    var lastIndex;
    // Check if raise didn't succeed (ie. item's index didn't change), and quit the loop if so.
    while (existingIndex > i && existingIndex !== lastIndex) {
      lastIndex = existingIndex;
      this.raise(item, lastIndex);
      existingIndex = this.items.indexOf(item);
    }
  }

  if (defined(this.terria.currentViewer)) {
    this.terria.currentViewer.notifyRepaintRequired();
  }
};

function beforeViewerChanged(nowViewing) {
  // Hide and disable all data sources, without actually changing
  // their isEnabled and isShown flags.

  var dataSources = nowViewing.items;

  for (var i = 0; i < dataSources.length; ++i) {
    var dataSource = dataSources[i];
    if (defined(dataSource._loadForEnablePromise)) {
      continue;
    }

    if (dataSource.isShown) {
      dataSource._hide();
    }

    if (dataSource.isEnabled) {
      dataSource._disable();
    }
  }
}

function afterViewerChanged(nowViewing) {
  // Re-enable and re-show all data sources that were previously enabled or shown.
  // Work from the bottom data source up so that the correct order is created.

  var dataSources = nowViewing.items;

  for (var i = dataSources.length - 1; i >= 0; --i) {
    var dataSource = dataSources[i];
    if (defined(dataSource._loadForEnablePromise)) {
      continue;
    }

    if (dataSource.isEnabled) {
      try {
        dataSource._enable();
      } catch (e) {
        raiseErrorToUser(nowViewing.terria, e);
      }
      //Re-enable attribution
      nowViewing.terria.currentViewer.addAttribution(dataSource.attribution);
    }

    if (dataSource.isShown) {
      try {
        dataSource._show();
      } catch (e) {
        raiseErrorToUser(nowViewing.terria, e);
      }
    }
  }
}

module.exports = NowViewing;