Models/WebFeatureServiceCatalogItem.js

"use strict";

/*global require*/
var URI = require("urijs");
var combine = require("terriajs-cesium/Source/Core/combine").default;

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 loadXML = require("../Core/loadXML");

var GeoJsonCatalogItem = require("./GeoJsonCatalogItem");
var CatalogItem = require("./CatalogItem");
var inherit = require("../Core/inherit");
var gmlToGeoJson = require("../Map/gmlToGeoJson");
var overrideProperty = require("../Core/overrideProperty");
var proxyCatalogItemUrl = require("./proxyCatalogItemUrl");
var i18next = require("i18next").default;

/**
 * A {@link CatalogItem} representing a layer from a Web Feature Service (WFS) server.
 *
 * @alias WebFeatureServiceCatalogItem
 * @constructor
 * @extends CatalogItem
 *
 * @param {Terria} terria The Terria instance.
 */
var WebFeatureServiceCatalogItem = function(terria) {
  CatalogItem.call(this, terria);

  this._dataUrl = undefined;
  this._dataUrlType = undefined;
  this._metadataUrl = undefined;
  this._geoJsonItem = undefined;

  /**
   * Gets or sets the WFS feature type names.
   * @type {String}
   */
  this.typeNames = "";

  /**
   * Gets or sets a value indicating whether we should request GeoJSON from the WFS server.  If this property
   * and {@link WebFeatureServiceCatalogItem#requestGeoJson} are both true, we'll request GeoJSON first and
   * only fall back on trying GML if the GeoJSON request fails.
   * @type {Boolean}
   * @default true
   */
  this.requestGeoJson = true;

  /**
   * Gets or sets a value indicating whether we should request GML from the WFS server.  If this property
   * and {@link WebFeatureServiceCatalogItem#requestGeoJson} are both true, we'll request GeoJSON first and
   * only fall back on trying GML if the GeoJSON request fails.
   * @type {Boolean}
   * @default true
   */
  this.requestGml = true;

  /**
   * Gets or sets the additional parameters to pass to the WFS server when requesting geometry.
   * All parameter names must be entered in lowercase in order to be consistent with references in TerrisJS code.
   * @type {Object}
   */
  this.parameters = {};

  knockout.track(this, [
    "_dataUrl",
    "_dataUrlType",
    "_metadataUrl",
    "typeNames",
    "requestGeoJson",
    "requestGml"
  ]);

  // dataUrl, metadataUrl, and legendUrl are derived from url if not explicitly specified.
  overrideProperty(this, "dataUrl", {
    get: function() {
      var url = this._dataUrl;
      if (!defined(url)) {
        url = this.url;
      }

      if (this.dataUrlType === "wfs") {
        url = new URI(cleanUrl(url))
          .setQuery(
            combine(
              {
                service: "WFS",
                version: "1.1.0",
                request: "GetFeature",
                typeName: this.typeNames,
                srsName: "EPSG:4326",
                maxFeatures: "1000"
              },
              this.parameters
            )
          )
          .toString();
      }

      return url;
    },
    set: function(value) {
      this._dataUrl = value;
    }
  });

  overrideProperty(this, "dataUrlType", {
    get: function() {
      if (defined(this._dataUrlType)) {
        return this._dataUrlType;
      } else {
        return "wfs";
      }
    },
    set: function(value) {
      this._dataUrlType = value;
    }
  });

  overrideProperty(this, "metadataUrl", {
    get: function() {
      if (defined(this._metadataUrl)) {
        return this._metadataUrl;
      }

      return new URI(cleanUrl(this.url))
        .setQuery(
          combine(
            {
              service: "WFS",
              version: "1.1.0",
              request: "GetCapabilities"
            },
            this.parameters
          )
        )
        .toString();
    },
    set: function(value) {
      this._metadataUrl = value;
    }
  });
};

inherit(CatalogItem, WebFeatureServiceCatalogItem);

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

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

  /**
   * Gets the set of functions used to update individual properties in {@link CatalogMember#updateFromJson}.
   * When a property name in the returned object literal matches the name of a property on this instance, the value
   * will be called as a function and passed a reference to this instance, a reference to the source JSON object
   * literal, and the name of the property.
   * @memberOf WebFeatureServiceCatalogItem.prototype
   * @type {Object}
   */
  updaters: {
    get: function() {
      return WebFeatureServiceCatalogItem.defaultUpdaters;
    }
  },

  /**
   * 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 WebFeatureServiceCatalogItem.prototype
   * @type {Object}
   */
  serializers: {
    get: function() {
      return WebFeatureServiceCatalogItem.defaultSerializers;
    }
  },
  /**
   * Gets the data source associated with this catalog item.
   * @memberOf WebFeatureServiceCatalogItem.prototype
   * @type {DataSource}
   */
  dataSource: {
    get: function() {
      return defined(this._geoJsonItem)
        ? this._geoJsonItem.dataSource
        : undefined;
    }
  }
});

WebFeatureServiceCatalogItem.defaultUpdaters = clone(
  CatalogItem.defaultUpdaters
);
Object.freeze(WebFeatureServiceCatalogItem.defaultUpdaters);

WebFeatureServiceCatalogItem.defaultSerializers = clone(
  CatalogItem.defaultSerializers
);

// Serialize the underlying properties instead of the public views of them.
WebFeatureServiceCatalogItem.defaultSerializers.dataUrl = function(
  wfsItem,
  json,
  propertyName
) {
  json.dataUrl = wfsItem._dataUrl;
};
WebFeatureServiceCatalogItem.defaultSerializers.dataUrlType = function(
  wfsItem,
  json,
  propertyName
) {
  json.dataUrlType = wfsItem._dataUrlType;
};
WebFeatureServiceCatalogItem.defaultSerializers.metadataUrl = function(
  wfsItem,
  json,
  propertyName
) {
  json.metadataUrl = wfsItem._metadataUrl;
};
Object.freeze(WebFeatureServiceCatalogItem.defaultSerializers);

WebFeatureServiceCatalogItem.prototype._getValuesThatInfluenceLoad = function() {
  return [
    this.url,
    this.typeNames,
    this.requestGeoJson,
    this.requestGml,
    this.parameters
  ];
};

WebFeatureServiceCatalogItem.prototype._load = function() {
  this._geoJsonItem = new GeoJsonCatalogItem(this.terria);

  var promise;
  if (this.requestGeoJson) {
    promise = loadGeoJson(this);
  } else if (this.requestGml) {
    promise = loadGml(this);
  } else {
    return;
  }

  this._geoJsonItem.data = promise;

  var that = this;
  return that._geoJsonItem.load().then(function() {
    if (!defined(that.rectangle)) {
      that.rectangle = that._geoJsonItem.rectangle;
    }
  });
};

WebFeatureServiceCatalogItem.prototype._enable = function() {
  if (defined(this._geoJsonItem)) {
    this._geoJsonItem._enable();
  }
};

WebFeatureServiceCatalogItem.prototype._disable = function() {
  if (defined(this._geoJsonItem)) {
    this._geoJsonItem._disable();
  }
};

WebFeatureServiceCatalogItem.prototype._show = function() {
  if (defined(this._geoJsonItem)) {
    this._geoJsonItem._show();
  }
};

WebFeatureServiceCatalogItem.prototype._hide = function() {
  if (defined(this._geoJsonItem)) {
    this._geoJsonItem._hide();
  }
};

WebFeatureServiceCatalogItem.prototype.showOnSeparateMap = function(
  globeOrMap
) {
  if (defined(this._geoJsonItem)) {
    return this._geoJsonItem.showOnSeparateMap(globeOrMap);
  }
};

function loadGeoJson(wfsItem) {
  var promise = loadJson(buildGeoJsonUrl(wfsItem)).then(function(json) {
    return json;
  });

  if (wfsItem.requestGml) {
    promise = promise.otherwise(function() {
      return loadGml(wfsItem);
    });
  }

  return promise;
}

function loadGml(wfsItem) {
  return loadXML(buildGmlUrl(wfsItem)).then(function(xml) {
    return gmlToGeoJson(xml);
  });
}

function buildGeoJsonUrl(wfsItem) {
  var url = cleanAndProxyUrl(wfsItem, wfsItem.url);
  return new URI(url)
    .setQuery(
      combine(
        {
          service: "WFS",
          request: "GetFeature",
          typeName: wfsItem.typeNames,
          version: "1.1.0",
          outputFormat: "JSON",
          srsName: "EPSG:4326"
        },
        wfsItem.parameters
      )
    )
    .toString();
}

function buildGmlUrl(wfsItem) {
  var url = cleanAndProxyUrl(wfsItem, wfsItem.url);
  return new URI(url)
    .setQuery(
      combine(
        {
          service: "WFS",
          request: "GetFeature",
          typeName: wfsItem.typeNames,
          version: "1.1.0",
          srsName: "EPSG:4326"
        },
        wfsItem.parameters
      )
    )
    .toString();
}

function cleanAndProxyUrl(catalogItem, url) {
  return proxyCatalogItemUrl(catalogItem, cleanUrl(url));
}

function cleanUrl(url) {
  // Strip off the search portion of the URL
  var uri = new URI(url);
  uri.search("");
  return uri.toString();
}

module.exports = WebFeatureServiceCatalogItem;