Models/WebFeatureServiceCatalogGroup.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 loadXML = require("../Core/loadXML");
var Rectangle = require("terriajs-cesium/Source/Core/Rectangle").default;

var TerriaError = require("../Core/TerriaError");
var CatalogGroup = require("./CatalogGroup");
var inherit = require("../Core/inherit");
var proxyCatalogItemUrl = require("./proxyCatalogItemUrl");
var unionRectangles = require("../Map/unionRectangles");
var WebFeatureServiceCatalogItem = require("./WebFeatureServiceCatalogItem");
var xml2json = require("../ThirdParty/xml2json");
var i18next = require("i18next").default;

/**
 * A {@link CatalogGroup} representing a collection of feature types from a Web Feature Service (WFS) server.
 *
 * @alias WebFeatureServiceCatalogGroup
 * @constructor
 * @extends CatalogGroup
 *
 * @param {Terria} terria The Terria instance.
 */
var WebFeatureServiceCatalogGroup = function(terria) {
  CatalogGroup.call(this, terria, "wfs-getCapabilities");

  /**
   * Gets or sets the URL of the WFS 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;

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

inherit(CatalogGroup, WebFeatureServiceCatalogGroup);

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

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

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

WebFeatureServiceCatalogGroup.defaultSerializers.items =
  CatalogGroup.enabledShareableItemsSerializer;

WebFeatureServiceCatalogGroup.defaultSerializers.isLoading = function(
  wfsGroup,
  json,
  propertyName,
  options
) {};

Object.freeze(WebFeatureServiceCatalogGroup.defaultSerializers);

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

WebFeatureServiceCatalogGroup.prototype._load = function() {
  var url =
    cleanAndProxyUrl(this, this.url) +
    "?service=WFS&version=1.1.0&request=GetCapabilities";

  var that = this;
  return loadXML(url)
    .then(function(xml) {
      // Is this really a GetCapabilities response?
      if (
        !xml ||
        !xml.documentElement ||
        xml.documentElement.localName !== "WFS_Capabilities"
      ) {
        throw new TerriaError({
          title: i18next.t(
            "models.webFeatureServiceCatalogGroup.invalidWFSServerTitle"
          ),
          message: i18next.t(
            "models.webFeatureServiceCatalogGroup.invalidWFSServerMessage",
            {
              email:
                '<a href="mailto:' +
                that.terria.supportEmail +
                '">' +
                that.terria.supportEmail +
                "</a>."
            }
          )
        });
      }

      var json = xml2json(xml);

      var supportsJsonGetFeature = false;
      if (defined(json.ServiceIdentification)) {
        if (defined(json.ServiceIdentification.Title)) {
          if (that.name === that.url) {
            that.name = json.ServiceIdentification.Title;
          }
        }
      }
      if (defined(json.OperationsMetadata)) {
        var getFeatureOperation = findElementByName(
          json.OperationsMetadata.Operation,
          "GetFeature"
        );
        if (defined(getFeatureOperation)) {
          var outputFormatParameter = findElementByName(
            getFeatureOperation.Parameter,
            "outputFormat"
          );
          if (
            defined(outputFormatParameter) &&
            defined(outputFormatParameter.Value)
          ) {
            supportsJsonGetFeature =
              outputFormatParameter.Value.indexOf("json") >= 0 ||
              outputFormatParameter.Value.indexOf("JSON") >= 0 ||
              outputFormatParameter.Value.indexOf("application/json") >= 0;
          }
        }
      }

      var dataCustodian = that.dataCustodian;
      if (
        !defined(dataCustodian) &&
        defined(json.ServiceProvider) &&
        defined(json.ServiceProvider.ProviderName)
      ) {
        dataCustodian = json.ServiceProvider.ProviderName;

        if (
          defined(json.ServiceProvider.ProviderSite) &&
          defined(json.ServiceProvider.ProviderSite.href)
        ) {
          dataCustodian =
            "[" +
            dataCustodian +
            "](" +
            json.ServiceProvider.ProviderSite.href +
            ")";
        }

        if (
          defined(json.ServiceProvider.ServiceContact) &&
          defined(json.ServiceProvider.ServiceContact.Address) &&
          defined(
            json.ServiceProvider.ServiceContact.Address.ElectronicMailAddress
          )
        ) {
          dataCustodian += "<br/>";
          dataCustodian +=
            "[" +
            json.ServiceProvider.ServiceContact.Address.ElectronicMailAddress +
            "](mailto:" +
            json.ServiceProvider.ServiceContact.Address.ElectronicMailAddress +
            ")<br/>";
        }
      }

      if (defined(json.FeatureTypeList)) {
        addFeatureTypes(
          that,
          json.FeatureTypeList.FeatureType,
          that.items,
          undefined,
          supportsJsonGetFeature,
          dataCustodian
        );
      }
    })
    .otherwise(function(e) {
      throw new TerriaError({
        title: i18next.t(
          "models.webFeatureServiceCatalogGroup.groupNotAvailableTitle"
        ),
        message: i18next.t(
          "models.webFeatureServiceCatalogGroup.groupNotAvailableMessage",
          {
            email:
              '<a href="mailto:' +
              that.terria.supportEmail +
              '">' +
              that.terria.supportEmail +
              "</a>.",
            appName: that.terria.appName
          }
        )
      });
    });
};

function findElementByName(list, name) {
  if (!defined(list)) {
    return undefined;
  }

  for (var i = 0; i < list.length; ++i) {
    if (list[i].name === name) {
      return list[i];
    }
  }

  return undefined;
}

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 addFeatureTypes(
  wfsGroup,
  featureTypes,
  items,
  parent,
  supportsJsonGetFeature,
  dataCustodian
) {
  if (!defined(featureTypes)) {
    return;
  }
  if (!(featureTypes instanceof Array)) {
    featureTypes = [featureTypes];
  }

  for (var i = 0; i < featureTypes.length; ++i) {
    var featureType = featureTypes[i];

    if (wfsGroup.blacklist && wfsGroup.blacklist[featureType.Title]) {
      console.log(
        "Provider Feedback: Filtering out " +
          featureType.Title +
          " (" +
          featureType.Name +
          ") because it is blacklisted."
      );
      continue;
    }

    wfsGroup.add(
      createWfsDataSource(
        wfsGroup,
        featureType,
        supportsJsonGetFeature,
        dataCustodian
      )
    );
  }
}

function createWfsDataSource(
  wfsGroup,
  featureType,
  supportsJsonGetFeature,
  dataCustodian
) {
  var result = new WebFeatureServiceCatalogItem(wfsGroup.terria);

  result.name = featureType.Title;
  result.description =
    defined(featureType.Abstract) && featureType.Abstract.length > 0
      ? featureType.Abstract
      : wfsGroup.description;
  result.dataCustodian = dataCustodian;
  result.url = wfsGroup.url;
  result.typeNames = featureType.Name;

  result.description = "";

  var wfsGroupHasDescription =
    defined(wfsGroup.description) && wfsGroup.description.length > 0;
  var layerHasAbstract =
    defined(featureType.Abstract) && featureType.Abstract.length > 0;

  if (wfsGroupHasDescription) {
    result.description += wfsGroup.description;
  }

  if (wfsGroupHasDescription && layerHasAbstract) {
    result.description += "<br/>";
  }

  if (layerHasAbstract) {
    result.description += featureType.Abstract;
  }

  result.requestGeoJson = supportsJsonGetFeature;
  result.requestGml = true;

  var boundingBoxes = featureType.WGS84BoundingBox;

  var rectangle;
  if (boundingBoxes instanceof Array) {
    rectangle = wgs84BoundingBoxToRectangle(boundingBoxes[0]);
    for (var i = 1; i < boundingBoxes.length; ++i) {
      rectangle = unionRectangles(
        rectangle,
        wgs84BoundingBoxToRectangle(boundingBoxes[i])
      );
    }
  } else if (defined(boundingBoxes)) {
    rectangle = wgs84BoundingBoxToRectangle(boundingBoxes);
  } else {
    rectangle = Rectangle.MAX_VALUE;
  }

  result.rectangle = rectangle;

  return result;
}

function wgs84BoundingBoxToRectangle(boundingBox) {
  if (!defined(boundingBox)) {
    return Rectangle.MAX_VALUE;
  }

  var lowerCorner = boundingBox.LowerCorner;
  var upperCorner = boundingBox.UpperCorner;
  if (!defined(lowerCorner) || !defined(upperCorner)) {
    return Rectangle.MAX_VALUE;
  }

  var lowerCoordinates = lowerCorner.split(" ");
  var upperCoordinates = upperCorner.split(" ");
  if (lowerCoordinates.length !== 2 || upperCoordinates.length !== 2) {
    return Rectangle.MAX_VALUE;
  }

  return Rectangle.fromDegrees(
    lowerCoordinates[0],
    lowerCoordinates[1],
    upperCoordinates[0],
    upperCoordinates[1]
  );
}

module.exports = WebFeatureServiceCatalogGroup;