Models/ArcGisMapServerCatalogGroup.js

"use strict";

/*global require*/
var URI = require("urijs");

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

var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var loadJson = require("../Core/loadJson");
var when = require("terriajs-cesium/Source/ThirdParty/when").default;

var TerriaError = require("../Core/TerriaError");
var CatalogGroup = require("./CatalogGroup");
var inherit = require("../Core/inherit");
var proxyCatalogItemUrl = require("./proxyCatalogItemUrl");
var replaceUnderscores = require("../Core/replaceUnderscores");
var ArcGisMapServerCatalogItem = require("./ArcGisMapServerCatalogItem");
var i18next = require("i18next").default;

/**
 * A {@link CatalogGroup} representing a collection of layers from an ArcGIS Map Service.
 * Eg. http://www.ga.gov.au/gis/rest/services/earth_observation/Landcover_WM/MapServer
 *
 * @alias ArcGisMapServerCatalogGroup
 * @constructor
 * @extends CatalogGroup
 *
 * @param {Terria} terria The Terria instance.
 */
var ArcGisMapServerCatalogGroup = function(terria) {
  CatalogGroup.call(this, terria, "esri-mapServer-group");

  /**
   * Gets or sets the URL of the Map Server.  This property is observable.
   * @type {String}
   */
  this.url = "";

  /**
   * Gets or sets a description of the custodian of the data sources in this group.
   * This property is an HTML string that must be sanitized before display to the user.
   * This property is observable.
   * @type {String}
   */
  this.dataCustodian = undefined;

  /**
   * Gets or sets a hash of names of blacklisted data layers.  A layer that appears in this hash
   * will not be shown to the user.  In this hash, the keys should be the Title of the layers to blacklist,
   * and the values should be "true".  This property is observable.
   * @type {Object}
   */
  this.blacklist = undefined;

  /**
   * Gets or sets a hash of properties that will be set on each child item.
   * For example, { 'treat404AsError': false }
   */
  this.itemProperties = undefined;

  knockout.track(this, ["url", "dataCustodian", "blacklist", "itemProperties"]);
};

inherit(CatalogGroup, ArcGisMapServerCatalogGroup);

Object.defineProperties(ArcGisMapServerCatalogGroup.prototype, {
  /**
   * Gets the type of data member represented by this instance.
   * @memberOf ArcGisMapServerCatalogGroup.prototype
   * @type {String}
   */
  type: {
    get: function() {
      return "esri-mapServer-group";
    }
  },

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

  /**
   * 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 lieral,
   * 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 ArcGisMapServerCatalogGroup.prototype
   * @type {Object}
   */
  serializers: {
    get: function() {
      return ArcGisMapServerCatalogGroup.defaultSerializers;
    }
  }
});

/**
 * 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}
 */
ArcGisMapServerCatalogGroup.defaultSerializers = clone(
  CatalogGroup.defaultSerializers
);

ArcGisMapServerCatalogGroup.defaultSerializers.items =
  CatalogGroup.enabledShareableItemsSerializer;

ArcGisMapServerCatalogGroup.defaultSerializers.isLoading = function(
  wmsGroup,
  json,
  propertyName,
  options
) {};

Object.freeze(ArcGisMapServerCatalogGroup.defaultSerializers);

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

ArcGisMapServerCatalogGroup.prototype._load = function() {
  return loadMapServer(this);
};

// loadMapServer is exposed so that ArcGisCatalogGroup can call it,
// to load a MapServer as if it were an ArcGisMapServerCatalogGroup.
ArcGisMapServerCatalogGroup.loadMapServer = function(catalogGroup) {
  return loadMapServer(catalogGroup);
};

function loadMapServer(catalogGroup) {
  function getJson(segment) {
    var uri = new URI(catalogGroup.url).segment(segment).addQuery("f", "json");
    return loadJson(proxyCatalogItemUrl(catalogGroup, uri.toString(), "1d"));
  }
  var terria = catalogGroup.terria;
  return when
    .all([getJson(""), getJson("layers"), getJson("legend")])
    .then(function(result) {
      var serviceJson = result[0];
      var layersJson = result[1];
      var legendJson = result[2];

      // Is this really a MapServer REST response?
      if (
        !serviceJson ||
        !serviceJson.layers ||
        !layersJson ||
        !layersJson.layers
      ) {
        throw new TerriaError({
          title: i18next.t(
            "models.arcGisMapServerCatalogGroup.invalidServiceTitle"
          ),
          message: i18next.t(
            "models.arcGisMapServerCatalogGroup.invalidServiceMessage",
            {
              email:
                '<a href="mailto:' +
                terria.supportEmail +
                '">' +
                terria.supportEmail +
                "</a>"
            }
          )
        });
      }

      var dataCustodian = catalogGroup.dataCustodian;
      if (
        !defined(dataCustodian) &&
        defined(serviceJson.documentInfo) &&
        defined(serviceJson.documentInfo.Author)
      ) {
        dataCustodian = serviceJson.documentInfo.Author;
      }
      if (
        (catalogGroup.name === "Unnamed Item" ||
          catalogGroup.name === catalogGroup.url) &&
        defined(serviceJson.mapName) &&
        serviceJson.mapName.length > 0
      ) {
        catalogGroup.name = serviceJson.mapName;
      }

      addLayersRecursively(
        catalogGroup,
        serviceJson,
        layersJson,
        legendJson,
        -1,
        layersJson.layers,
        catalogGroup,
        dataCustodian
      );
    })
    .otherwise(function(e) {
      throw new TerriaError({
        sender: catalogGroup,
        title: i18next.t(
          "models.arcGisMapServerCatalogGroup.groupNotAvailableTitle"
        ),
        message: i18next.t(
          "models.arcGisMapServerCatalogGroup.groupNotAvailableMessage",
          {
            cors:
              '<a href="http://enable-cors.org/" target="_blank">' +
              i18next.t("models.arcGisMapServerCatalogGroup.cors") +
              "</a>",
            appName: terria.appName,
            email:
              '<a href="mailto:' +
              terria.supportEmail +
              '">' +
              terria.supportEmail +
              "</a>"
          }
        )
      });
    });
}

function addLayersRecursively(
  mapServiceGroup,
  topLevelJson,
  topLevelLayersJson,
  topLevelLegendJson,
  parentID,
  layers,
  thisGroup,
  dataCustodian
) {
  if (!(layers instanceof Array)) {
    layers = [layers];
  }

  for (var i = 0; i < layers.length; ++i) {
    var layer = layers[i];

    if (
      parentID === -1 &&
      layer.parentLayer !== null &&
      defined(layer.parentLayer)
    ) {
      continue;
    } else if (
      parentID !== -1 &&
      (!layer.parentLayer || layer.parentLayer.id !== parentID)
    ) {
      continue;
    }

    if (mapServiceGroup.blacklist && mapServiceGroup.blacklist[layer.name]) {
      console.log(
        "Provider Feedback: Filtering out " +
          layer.name +
          " (" +
          layer.id +
          ") because it is blacklisted."
      );
      continue;
    }

    if (layer.type === "Group Layer") {
      var subGroup = new CatalogGroup(mapServiceGroup.terria);
      subGroup.name = replaceUnderscores(layer.name);
      thisGroup.add(subGroup);
      addLayersRecursively(
        mapServiceGroup,
        topLevelJson,
        topLevelLayersJson,
        topLevelLegendJson,
        layer.id,
        layers,
        subGroup,
        dataCustodian
      );
    } else if (
      layer.type === "Feature Layer" ||
      layer.type === "Raster Layer" ||
      layer.type === "Mosaic Layer"
    ) {
      thisGroup.add(
        createDataSource(
          mapServiceGroup,
          topLevelJson,
          topLevelLayersJson,
          topLevelLegendJson,
          layer,
          dataCustodian
        )
      );
    }
  }
}

function createDataSource(
  mapServiceGroup,
  topLevelJson,
  topLevelLayersJson,
  topLevelLegendJson,
  layer,
  dataCustodian
) {
  var result = new ArcGisMapServerCatalogItem(mapServiceGroup.terria);

  result.name = replaceUnderscores(layer.name);
  result.dataCustodian = dataCustodian;
  result.url = mapServiceGroup.url;
  result.layers = layer.id.toString();
  result.maximumScale = layer.maxScale;

  result.updateFromMetadata(
    topLevelJson,
    topLevelLayersJson,
    topLevelLegendJson,
    true,
    layer
  );

  if (typeof mapServiceGroup.itemProperties === "object") {
    result.updateFromJson(mapServiceGroup.itemProperties);
  }

  return result;
}

module.exports = ArcGisMapServerCatalogGroup;