Models/AbsIttCatalogGroup.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 objectToQuery = require("terriajs-cesium/Source/Core/objectToQuery")
  .default;

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

/**
 * A {@link CatalogGroup} representing a collection of items from an Australian Bureau of Statistics
 * (ABS) ITT server, formed by querying for all the codes in a given dataset and concept.
 *
 * @alias AbsIttCatalogGroup
 * @constructor
 * @extends CatalogGroup
 *
 * @param {Terria} terria The Terria instance.
 */
var AbsIttCatalogGroup = function(terria) {
  CatalogGroup.call(this, terria, "abs-itt-by-concept");

  /**
   * Gets or sets the URL of the ABS ITT API, typically http://stat.abs.gov.au/itt/query.jsp.
   * This property is observable.
   * @type {String}
   */
  this.url = undefined;

  /**
   * Gets or sets the filter for the ABS dataset.  You can obtain a list of all datasets by querying
   * http://stat.abs.gov.au/itt/query.jsp?method=GetDatasetList (or equivalent).  This property
   * is observable.
   * @type {String[]}
   */
  this.filter = undefined;

  /**
   * Gets or sets the styling for the abs data.  This property is observable.
   * @type {object}
   * @default undefined
   */
  // this.tableStyle = undefined;

  /**
   * Gets or sets the URL of a JSON file containing human-readable names of Australian Bureau of Statistics concept codes.
   * @type {String}
   */
  this.conceptNamesUrl = undefined;

  /**
   * Gets or sets the start of a URL of a csv file containing the total number of people in each region, eg.
   * SA4,Tot_P_M,Tot_P_F,Tot_P_P
   * 101,100000,23450,123450
   * 102,130000,100000,234560
   * The region code and '.csv' are appended to the end of this URL for the request, eg.
   * 'data/2011Census_TOT_' -> 'data/2011Census_TOT_SA4.csv' (and other region types).
   * @type {String}
   */
  this.regionPopulationsUrlPrefix = undefined;

  /**
   * 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 datasets.  A dataset that appears in this hash
   * will not be shown to the user.  In this hash, the keys should be the name of the dataset to blacklist,
   * and the values should be "true".  This property is observable.
   * @type {Object}
   */
  this.blacklist = undefined;

  /**
   * Gets or sets a hash of names of whitelisted datasets.  A dataset that doesn't appears in this hash
   * will not be shown to the user.  In this hash, the keys should be the name of the dataset to blacklist,
   * and the values should be "true".  This property is observable.
   * @type {Object}
   */
  this.whitelist = 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",
    "filter",
    "dataCustodian",
    "blacklist",
    "whitelist",
    "itemProperties"
  ]);
};

inherit(CatalogGroup, AbsIttCatalogGroup);

Object.defineProperties(AbsIttCatalogGroup.prototype, {
  /**
   * Gets the type of data member represented by this instance.
   * @memberOf AbsIttCatalogGroup.prototype
   * @type {String}
   */
  type: {
    get: function() {
      return "abs-itt-dataset-list";
    }
  },

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

  /**
   * 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 AbsIttCatalogGroup.prototype
   * @type {Object}
   */
  serializers: {
    get: function() {
      return AbsIttCatalogGroup.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}
 */
AbsIttCatalogGroup.defaultSerializers = clone(CatalogGroup.defaultSerializers);

AbsIttCatalogGroup.defaultSerializers.items =
  CatalogGroup.enabledShareableItemsSerializer;

Object.freeze(AbsIttCatalogGroup.defaultSerializers);

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

AbsIttCatalogGroup.prototype._load = function() {
  var baseUrl = cleanAndProxyUrl(this, this.url);
  var parameters = {
    method: "GetDatasetList",
    format: "json"
  };

  var that = this;

  var url = baseUrl + "?" + objectToQuery(parameters);

  return loadJson(url)
    .then(function(json) {
      var datasets = json.datasets;

      var p;

      var blacklistWildcardTerms = [];
      for (p in that.blacklist) {
        if (that.blacklist.hasOwnProperty(p)) {
          if (p[0] === "?") {
            blacklistWildcardTerms.push(p.substring(1));
          }
        }
      }

      var whitelistWildcardTerms = [];
      for (p in that.whitelist) {
        if (that.whitelist.hasOwnProperty(p)) {
          if (p[0] === "?") {
            whitelistWildcardTerms.push(p.substring(1));
          }
        }
      }

      for (var i = 0; i < datasets.length - 1; ++i) {
        var dataset = datasets[i];

        var w;

        //apply blacklist
        var blacklist =
          defined(that.blacklist) &&
          defined(that.blacklist[dataset.description]);
        for (w = 0; w < blacklistWildcardTerms.length && !blacklist; w++) {
          if (dataset.id.indexOf(blacklistWildcardTerms[w]) !== -1) {
            blacklist = true;
          }
        }
        //now apply whitelist
        for (w = 0; w < whitelistWildcardTerms.length && !blacklist; w++) {
          if (dataset.id.indexOf(whitelistWildcardTerms[w]) === -1) {
            blacklist = true;
          }
        }
        if (
          defined(that.whitelist) &&
          defined(that.whitelist[dataset.description])
        ) {
          blacklist = true;
        }
        if (blacklist) {
          console.log(
            "Provider Feedback: Filtering out " +
              dataset.description +
              " (" +
              dataset.id +
              ") because it is blacklisted."
          );
          continue;
        }

        that.add(createItemForDataset(that, dataset));
      }
    })
    .otherwise(function(e) {
      console.log(e.message);
      throw new TerriaError({
        sender: that,
        title: i18next.t("models.abs.groupNotAvailableTitle"),
        message: i18next.t("models.abs.groupNotAvailableMessage", {
          email:
            '<a href="mailto:' +
            that.terria.supportEmail +
            '">' +
            that.terria.supportEmail +
            "</a>"
        })
      });
    });
};

function cleanAndProxyUrl(catalogGroup, url) {
  // Strip off the search portion of the URL
  var uri = new URI(url);
  uri.search("");

  var cleanedUrl = uri.toString();
  return proxyCatalogItemUrl(catalogGroup, cleanedUrl, "1d");
}

function createItemForDataset(absGroup, dataset) {
  var result = new AbsIttCatalogItem(absGroup.terria);

  result.name = dataset.description;
  result.description = dataset.description;
  result.dataCustodian = absGroup.dataCustodian;
  result.url = absGroup.url;
  result.datasetId = dataset.id;
  result.filter = absGroup.filter;
  result.conceptNamesUrl = absGroup.conceptNamesUrl;
  result.regionPopulationsUrlPrefix = absGroup.regionPopulationsUrlPrefix;
  // result.tableStyle = absGroup.tableStyle;  // TODO: do we need to be able to define TableStyle on the group?

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

  return result;
}

module.exports = AbsIttCatalogGroup;