"use strict";
/*global require*/
var ArcGisFeatureServerCatalogItem = require("./ArcGisFeatureServerCatalogItem");
var ArcGisMapServerCatalogItem = require("./ArcGisMapServerCatalogItem");
var CatalogItem = require("./CatalogItem");
var clone = require("terriajs-cesium/Source/Core/clone").default;
var createRegexDeserializer = require("./createRegexDeserializer");
var createRegexSerializer = require("./createRegexSerializer");
var CsvCatalogItem = require("./CsvCatalogItem");
var CzmlCatalogItem = require("./CzmlCatalogItem");
var defined = require("terriajs-cesium/Source/Core/defined").default;
var GeoJsonCatalogItem = require("./GeoJsonCatalogItem");
var inherit = require("../Core/inherit");
var KmlCatalogItem = require("./KmlCatalogItem");
var loadJson = require("../Core/loadJson");
var Metadata = require("./Metadata");
var TerriaError = require("../Core/TerriaError");
var proxyCatalogItemUrl = require("./proxyCatalogItemUrl");
var Rectangle = require("terriajs-cesium/Source/Core/Rectangle").default;
var URI = require("urijs");
var WebMapServiceCatalogGroup = require("./WebMapServiceCatalogGroup");
var WebMapServiceCatalogItem = require("./WebMapServiceCatalogItem");
var WebFeatureServiceCatalogGroup = require("./WebFeatureServiceCatalogGroup");
var WebFeatureServiceCatalogItem = require("./WebFeatureServiceCatalogItem");
var when = require("terriajs-cesium/Source/ThirdParty/when").default;
var i18next = require("i18next").default;
/**
* A {@link CatalogItem} that queries a CKAN server for a resource, and then accesses
* that resource as WMS, GeoJSON, etc. depending on what it finds.
*
* @alias CkanCatalogItem
* @constructor
* @extends CatalogItem
*
* @param {Terria} terria The Terria instance.
*/
function CkanCatalogItem(terria) {
CatalogItem.call(this, terria);
/**
* Gets or sets the ID of the CKAN resource referred to by this catalog item. Either this property
* is {@see CkanCatalogItem#datasetId} must be specified. If {@see CkanCatalogItem#datasetId} is
* specified too, and this resource is not found, _any_ supported resource may be used instead,
* depending on the value of {@see CkanCatalogItem#allowAnyResourceIfResourceIdNotFound}.
* @type {String}
*/
this.resourceId = undefined;
/**
* Gets or sets the ID of the CKAN dataset referred to by this catalog item. Either this property
* is {@see CkanCatalogItem#resourceId} must be specified. The first resource of a supported type
* in this dataset will be used.
* @type {String}
*/
this.datasetId = undefined;
/**
* Gets or sets a value indicating whether any supported resource may be used if both {@see CkanCatalogItem#datasetId} and
* {@see CkanCatalogItem#resourceId} are specified and the {@see CkanCatalogItem#resourceId} is not found.
* @type {Boolean}
* @default true
*/
this.allowAnyResourceIfResourceIdNotFound = true;
/**
* Gets or sets a value indicating whether this may be a WMS resource.
* @type {Boolean}
* @default true
*/
this.allowWms = true;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is a WMS resource.
* @type {RegExp}
*/
this.wmsResourceFormat = /^wms$/i;
/**
* Gets or sets a value indicating whether this may be a WFS resource.
* @type {Boolean}
* @default true
*/
this.allowWfs = true;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is a WFS resource.
* @type {RegExp}
*/
this.wfsResourceFormat = /^wfs$/i;
/**
* Gets or sets a value indicating whether this may be a KML resource.
* @type {Boolean}
* @default true
*/
this.allowKml = true;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is a KML resource.
* @type {RegExp}
*/
this.kmlResourceFormat = /^kml$/i;
/**
* Gets or sets a value indicating whether this may be a CSV resource.
* @type {Boolean}
* @default true
*/
this.allowCsv = true;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is a CSV resource.
* @type {RegExp}
*/
this.csvResourceFormat = /^csv-geo-/i;
/**
* Gets or sets a value indicating whether this may be an Esri MapServer resource.
* @type {Boolean}
* @default true
*/
this.allowEsriMapServer = true;
/**
* Gets or sets a value indicating whether this may be an Esri FeatureServer resource.
* @type {Boolean}
* @default true
*/
this.allowEsriFeatureServer = true;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is an Esri MapServer resource.
* A valid MapServer resource must also have `MapServer` in its URL.
* @type {RegExp}
*/
this.esriMapServerResourceFormat = /^esri rest$/i;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is an Esri
* MapServer or FeatureServer resource. A valid FeatureServer resource must also have `FeatureServer` in its URL.
* @type {RegExp}
*/
this.esriFeatureServerResourceFormat = /^esri rest$/i;
/**
* Gets or sets a value indicating whether this may be a GeoJSON resource.
* @type {Boolean}
* @default true
*/
this.allowGeoJson = true;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is a GeoJSON resource.
* @type {RegExp}
*/
this.geoJsonResourceFormat = /^geojson$/i;
/**
* Gets or sets a value indicating whether this may be a CZML resource.
* @type {Boolean}
* @default true
*/
this.allowCzml = true;
/**
* Gets or sets a regular expression that, when it matches a resource's format, indicates that the resource is a CZML resource.
* @type {RegExp}
*/
this.czmlResourceFormat = /^czml$/i;
/**
* Gets or sets a hash of properties that will be set on the item created from the CKAN resource.
* For example, { "treat404AsError": false }
* @type {Object}
*/
this.itemProperties = undefined;
}
inherit(CatalogItem, CkanCatalogItem);
Object.defineProperties(CkanCatalogItem.prototype, {
/**
* Gets the type of data member represented by this instance.
* @memberOf CkanCatalogItem.prototype
* @type {String}
*/
type: {
get: function() {
return "ckan-resource";
}
},
/**
* Gets a human-readable name for this type of data source, 'CKAN Resource'.
* @memberOf CkanCatalogItem.prototype
* @type {String}
*/
typeName: {
get: function() {
return i18next.t("models.ckan.name");
}
},
/**
* Gets the metadata associated with this data source and the server that provided it, if applicable.
* @memberOf CkanCatalogItem.prototype
* @type {Metadata}
*/
metadata: {
get: function() {
var result = new Metadata();
result.isLoading = false;
result.dataSourceErrorMessage = i18next.t(
"models.ckan.dataSourceErrorMessage"
);
result.serviceErrorMessage = i18next.t("models.ckan.serviceErrorMessage");
return result;
}
},
/**
* 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 CkanCatalogItem.prototype
* @type {Object}
*/
updaters: {
get: function() {
return CkanCatalogItem.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 CkanCatalogItem.prototype
* @type {Object}
*/
serializers: {
get: function() {
return CkanCatalogItem.defaultSerializers;
}
}
});
/**
* Gets or sets the set of default updater functions to use in {@link CatalogMember#updateFromJson}. Types derived from this type
* should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#updaters} property.
* @type {Object}
*/
CkanCatalogItem.defaultUpdaters = clone(CatalogItem.defaultUpdaters);
CkanCatalogItem.defaultUpdaters.wmsResourceFormat = createRegexDeserializer(
"wmsResourceFormat"
);
CkanCatalogItem.defaultUpdaters.wfsResourceFormat = createRegexDeserializer(
"wfsResourceFormat"
);
CkanCatalogItem.defaultUpdaters.kmlResourceFormat = createRegexDeserializer(
"kmlResourceFormat"
);
CkanCatalogItem.defaultUpdaters.csvResourceFormat = createRegexDeserializer(
"csvResourceFormat"
);
CkanCatalogItem.defaultUpdaters.esriMapServerResourceFormat = createRegexDeserializer(
"esriMapServerResourceFormat"
);
CkanCatalogItem.defaultUpdaters.esriFeatureServerResourceFormat = createRegexDeserializer(
"esriFeatureServerResourceFormat"
);
CkanCatalogItem.defaultUpdaters.geoJsonResourceFormat = createRegexDeserializer(
"geoJsonResourceFormat"
);
CkanCatalogItem.defaultUpdaters.czmlResourceFormat = createRegexDeserializer(
"czmlResourceFormat"
);
Object.freeze(CkanCatalogItem.defaultUpdaters);
/**
* 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}
*/
CkanCatalogItem.defaultSerializers = clone(CatalogItem.defaultSerializers);
CkanCatalogItem.defaultSerializers.wmsResourceFormat = createRegexSerializer(
"wmsResourceFormat"
);
CkanCatalogItem.defaultSerializers.wfsResourceFormat = createRegexSerializer(
"wfsResourceFormat"
);
CkanCatalogItem.defaultSerializers.kmlResourceFormat = createRegexSerializer(
"kmlResourceFormat"
);
CkanCatalogItem.defaultSerializers.csvResourceFormat = createRegexSerializer(
"csvResourceFormat"
);
CkanCatalogItem.defaultSerializers.esriMapServerResourceFormat = createRegexSerializer(
"esriMapServerResourceFormat"
);
CkanCatalogItem.defaultSerializers.esriFeatureServerResourceFormat = createRegexSerializer(
"esriFeatureServerResourceFormat"
);
CkanCatalogItem.defaultSerializers.geoJsonResourceFormat = createRegexSerializer(
"geoJsonResourceFormat"
);
CkanCatalogItem.defaultSerializers.czmlResourceFormat = createRegexSerializer(
"czmlResourceFormat"
);
Object.freeze(CkanCatalogItem.defaultSerializers);
/**
* Creates a catalog item from a CKAN resource.
*
* @param {Terria} options.terria The Terria instance.
* @param {Object} options.resource The CKAN resource JSON.
* @param {Object} options.itemData The CKAN dataset JSON.
* @param {String} options.ckanBaseUrl The base URL of the CKAN server.
* @param {Object} [options.extras] The parsed version of `options.itemData`, if available. If not provided, it will be parsed as needed.
* @param {String} [options.parent] The parent of this catalog item.
* @param {RegExp} [options.wmsResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is a WMS resource. If undefined, WMS resources will not be returned.
* @param {RegExp} [options.wfsResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is a WFS resource. If undefined, WFS resources will not be returned.
* @param {RegExp} [options.esriMapServerResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is an Esri MapServer resource. If undefined, Esri MapServer resources will not be returned.
* @param {RegExp} [options.esriFeatureServerResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is an Esri FeatureServer resource. If undefined, Esri FeatureServer resources will not be returned.
* @param {RegExp} [options.kmlResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is a KML resource. If undefined, KML resources will not be returned.
* @param {RegExp} [options.geoJsonResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is a GeoJSON resource. If undefined, GeoJSON resources will not be returned.
* @param {RegExp} [options.csvResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is a CSV resource. If undefined, CSV resources will not be returned.
* @param {RegExp} [options.czmlResourceFormat] A regular expression that, when it matches a resource's format, indicates that the resource
* is a CZML resource. If undefined, CZML resources will not be returned.
* @param {Boolean} [options.allowWmsGroups=false] True to allow this function to return WMS groups in addition to items. For example if the resource
* refers to a WMS server but no layer is available, a {@see WebMapServiceCatalogGroup} for the
* server will be returned.
* @param {Boolean} [options.allowWfsGroups=false] True to allow this function to return WFS groups in addition to items. For example if the resource
* refers to a WFS server but no layer is available, a {@see WebFeatureServiceCatalogGroup} for the
* server will be returned.
* @param {Boolean} [options.useResourceName=false] True to use the name of the resource for the name of the catalog item; false to use the
* name of the dataset.
* @param {Boolean} [options.useCombinationNameWhereMultipleResources=true] Use a combination of the name and the resource and dataset where
there are multiple resources for a single dataset.
* @param {String} [options.dataCustodian] The data custodian to use, overriding any that might be inferred from the CKAN dataset.
* @param {Object} [options.itemProperties] Additional properties to apply to the item once created.
* @return {CatalogMember} The created catalog member, or undefined if no catalog member could be created from the resource.
*/
CkanCatalogItem.createCatalogItemFromResource = function(options) {
var resource = options.resource;
var itemData = options.itemData;
var extras = options.extras;
var parent = options.parent;
if (resource.__filtered) {
return;
}
if (!defined(extras)) {
extras = {};
if (defined(itemData.extras)) {
for (var idx = 0; idx < itemData.extras.length; idx++) {
extras[itemData.extras[idx].key] = itemData.extras[idx].value;
}
}
}
var formats = [
// Format Regex, Catalog Item, (optional) URL regex
[options.wmsResourceFormat, WebMapServiceCatalogItem],
[options.wfsResourceFormat, WebFeatureServiceCatalogItem],
[
options.esriMapServerResourceFormat,
ArcGisMapServerCatalogItem,
/MapServer/
],
[
options.esriFeatureServerResourceFormat,
ArcGisFeatureServerCatalogItem,
/FeatureServer/
],
[options.kmlResourceFormat, KmlCatalogItem, undefined, /\.zip$/],
[options.geoJsonResourceFormat, GeoJsonCatalogItem],
[options.czmlResourceFormat, CzmlCatalogItem],
[options.csvResourceFormat, CsvCatalogItem]
].filter(function(format) {
return defined(format[0]);
});
var baseUrl = resource.wms_url;
if (!defined(baseUrl)) {
baseUrl = resource.url;
if (!defined(baseUrl)) {
return undefined;
}
}
var matchingFormats = formats.filter(function(format) {
// Matching formats must match the format regex,
// and also the URL regex if it exists and not the URL exclusion regex if it exists.
return (
resource.format.match(format[0]) &&
(!defined(format[2]) || baseUrl.match(format[2])) &&
(!defined(format[3]) || !baseUrl.match(format[3]))
);
});
if (matchingFormats.length === 0) {
return undefined;
}
var isWms = matchingFormats[0][1] === WebMapServiceCatalogItem;
var isWfs = matchingFormats[0][1] === WebFeatureServiceCatalogItem;
// Extract the layer name from the URL.
var uri = new URI(baseUrl);
var params = uri.search(true);
// Remove the query portion of the WMS URL.
var url = baseUrl;
var newItem;
if (isWms || isWfs) {
var layerName =
resource.wms_layer || params.LAYERS || params.layers || params.typeName;
if (defined(layerName)) {
newItem = isWms
? new WebMapServiceCatalogItem(options.terria)
: new WebFeatureServiceCatalogItem(options.terria);
newItem.layers = layerName;
} else {
if (isWms && options.allowWmsGroups) {
newItem = new WebMapServiceCatalogGroup(options.terria);
} else if (isWfs && options.allowWfsGroups) {
newItem = new WebFeatureServiceCatalogGroup(options.terria);
} else {
return undefined;
}
}
uri.search("");
url = uri.toString();
} else {
newItem = new matchingFormats[0][1](options.terria);
}
if (!newItem) {
return undefined;
}
if (options.useResourceName) {
newItem.name = resource.name;
} else if (
options.useCombinationNameWhereMultipleResources &&
itemData.resources.length > 1
) {
newItem.name = `${itemData.title} - ${resource.name}`;
} else {
newItem.name = itemData.title;
}
if (itemData.notes) {
newItem.info.push({
name: i18next.t("models.ckan.datasetDescription"),
content: itemData.notes
});
// Prevent a description - often the same one - from also coming from the WMS server.
newItem.info.push({
name: i18next.t("models.ckan.datasetDescription"),
content: ""
});
}
if (defined(resource.description)) {
newItem.info.push({
name: i18next.t("models.ckan.resourceDescription"),
content: resource.description
});
}
if (defined(itemData.license_url)) {
newItem.info.push({
name: i18next.t("models.ckan.licence"),
content:
"[" +
(itemData.license_title || itemData.license_url) +
"](" +
itemData.license_url +
")"
});
} else if (defined(itemData.license_title)) {
newItem.info.push({
name: i18next.t("models.ckan.licence"),
content: itemData.license_title
});
}
if (defined(itemData.author)) {
newItem.info.push({
name: i18next.t("models.ckan.author"),
content: itemData.author
});
}
if (defined(itemData.contact_point)) {
newItem.info.push({
name: i18next.t("models.ckan.contact_point"),
content: itemData.c
});
}
// If the date string is of format 'dddd-dd-dd*' extract the first part, otherwise we retain the entire date string.
function prettifyDate(date) {
if (date.match(/^\d\d\d\d-\d\d-\d\d.*/)) {
return date.substr(0, 10);
} else {
return date;
}
}
if (defined(itemData.metadata_created)) {
newItem.info.push({
name: i18next.t("models.ckan.metadata_created"),
content: prettifyDate(itemData.metadata_created)
});
}
if (defined(itemData.metadata_modified)) {
newItem.info.push({
name: i18next.t("models.ckan.metadata_modified"),
content: prettifyDate(itemData.metadata_modified)
});
}
if (defined(itemData.update_freq)) {
newItem.info.push({
name: i18next.t("models.ckan.update_freq"),
content: itemData.update_freq
});
}
newItem.url = url;
var bboxString = itemData.geo_coverage || extras.geo_coverage;
if (defined(bboxString)) {
var parts = bboxString.split(",");
if (parts.length === 4) {
newItem.rectangle = Rectangle.fromDegrees(
parts[0],
parts[1],
parts[2],
parts[3]
);
}
}
newItem.dataUrl = new URI(options.ckanBaseUrl)
.segment("dataset")
.segment(itemData.name)
.toString();
newItem.dataUrlType = "direct";
if (defined(options.dataCustodian)) {
newItem.dataCustodian = options.dataCustodian;
} else if (itemData.organization && itemData.organization.title) {
newItem.dataCustodian =
itemData.organization.description || itemData.organization.title;
}
if (typeof options.itemProperties === "object") {
newItem.updateFromJson(options.itemProperties);
}
if (defined(parent)) {
newItem.id = parent.uniqueId + "/" + resource.id;
}
return newItem;
};
/**
* Maps catalog item `type` to a short, human-readable identifier of the
* type of resource accessed (e.g. `wms` maps to `WMS` and `esri-mapServer`
* maps to `MapServer`).
* @type {Object}
*/
CkanCatalogItem.shortHumanReadableTypeNames = {
wms: "WMS",
"wms-getCapabilities": "WMS",
wfs: "WFS",
"wfs-getCapabilities": "WFS",
"esri-mapServer": "MapServer",
"esri-featureServer": "FeatureServer",
kml: "KML",
geojson: "GeoJSON",
czml: "CZML",
csv: "CSV"
};
CkanCatalogItem.prototype._load = function() {
var baseUri = new URI(this.url).segment("api/3/action");
if (!defined(this.resourceId) && !defined(this.datasetId)) {
throw new TerriaError({
sender: this,
title: i18next.t("models.ckan.idsNotSpecifiedTitle"),
message: i18next.t("models.ckan.idsNotSpecifiedMessage")
});
}
var that = this;
var datasetIdPromise;
// If we don't know the dataset ID, query the resource for it.
if (defined(this.datasetId)) {
datasetIdPromise = when(this.datasetId);
} else {
var resourceUri = baseUri
.clone()
.segment("resource_show")
.addQuery({ id: this.resourceId });
var resourceUrl = proxyCatalogItemUrl(this, resourceUri.toString(), "1d");
datasetIdPromise = loadJson(resourceUrl).then(function(resourceJson) {
if (!resourceJson.success) {
throw new TerriaError({
sender: that,
title: i18next.t("models.ckan.errorRetrievingUrlTitle"),
message: i18next.t("models.ckan.errorRetrievingUrlMessage", {
url: that.url
})
});
}
if (
!defined(resourceJson.result) ||
!defined(resourceJson.result.package_id)
) {
throw new TerriaError({
sender: that,
title: i18next.t("models.ckan.invalidCkanTitle"),
message: i18next.t("models.ckan.invalidCkanMessage")
});
}
return resourceJson.result.package_id;
});
}
return datasetIdPromise.then(function(datasetId) {
var datasetUri = baseUri
.clone()
.segment("package_show")
.addQuery({ id: datasetId });
var datasetUrl = proxyCatalogItemUrl(that, datasetUri.toString(), "1d");
return loadJson(datasetUrl).then(function(json) {
if (!json.success) {
throw new TerriaError({
sender: that,
title: i18next.t("models.ckan.errorRetrievingUrlTitle"),
message: i18next.t("models.ckan.errorRetrievingUrlMessage", {
url: datasetUrl
})
});
}
var resources = json.result.resources;
var resourcesToConsider = resources;
// Prefer the specified resourceId, optionally allow any resourceId.
if (defined(that.resourceId)) {
resourcesToConsider = resources.filter(function(resource) {
return resource.id === that.resourceId;
});
if (
resourcesToConsider.length === 0 &&
that.allowAnyResourceIfResourceIdNotFound
) {
resourcesToConsider = resources;
}
}
for (var i = 0; i < resourcesToConsider.length; ++i) {
var catalogItem = CkanCatalogItem.createCatalogItemFromResource({
terria: that.terria,
resource: resourcesToConsider[i],
itemData: json.result,
ckanBaseUrl: that.url, // TODO
wmsResourceFormat: that.allowWms ? that.wmsResourceFormat : undefined,
kmlResourceFormat: that.allowKml ? that.kmlResourceFormat : undefined,
csvResourceFormat: that.allowCsv ? that.csvResourceFormat : undefined,
esriMapServerResourceFormat: that.allowEsriMapServer
? that.esriMapServerResourceFormat
: undefined,
geoJsonResourceFormat: that.allowGeoJson
? that.geoJsonResourceFormat
: undefined,
czmlResourceFormat: that.allowCzml
? that.czmlResourceFormat
: undefined,
dataCustodian: that.dataCustodian,
itemProperties: that.itemProperties
});
if (defined(catalogItem)) {
catalogItem.name = that.name;
return catalogItem;
}
}
throw new TerriaError({
sender: that,
title: i18next.t("models.ckan.notCompatibleTitle"),
message: defined(that.resourceId)
? i18next.t("models.ckan.notCompatibleMessageI", {
resourceId: that.resourceId
})
: i18next.t("models.ckan.notCompatibleMessageII")
});
});
});
};
module.exports = CkanCatalogItem;