UNPKG

terriajs

Version:

Geospatial data visualization platform.

286 lines (244 loc) 11.6 kB
'use strict'; /*global require*/ var clone = require('terriajs-cesium/Source/Core/clone'); var defined = require('terriajs-cesium/Source/Core/defined'); var defineProperties = require('terriajs-cesium/Source/Core/defineProperties'); var formatError = require('terriajs-cesium/Source/Core/formatError'); var freezeObject = require('terriajs-cesium/Source/Core/freezeObject'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); var loadJson = require('../Core/loadJson'); var proxyCatalogItemUrl = require('./proxyCatalogItemUrl'); var Rectangle = require('terriajs-cesium/Source/Core/Rectangle'); var when = require('terriajs-cesium/Source/ThirdParty/when'); var inherit = require('../Core/inherit'); var WebMapServiceCatalogItem = require('./WebMapServiceCatalogItem'); var GeoJsonCatalogItem = require('./GeoJsonCatalogItem'); var CatalogGroup = require('./CatalogGroup'); var TerriaError = require('../Core/TerriaError'); /** * A {@link CatalogGroup} representing a collection of layers from a [Socrata](http://Socrata.org) server. Only spatial layers with a defined Map * visualisation are shown, using WMS. * * @alias SocrataCatalogGroup * @constructor * @extends CatalogGroup * * @param {Terria} terria The Terria instance. */ var SocrataCatalogGroup = function(terria) { CatalogGroup.call(this, terria, 'socrata'); /** * Gets or sets the URL of the Socrata server. This property is observable. * @type {String} */ this.url = ''; /** * Gets or sets the filter query to pass to Socrata when querying the available data sources and their groups. Each string in the * array is passed to Socrata as an independent search string and the results are concatenated to create the complete list. * @type {String[]} */ this.filterQuery = ['limitTo=MAPS']; /** * 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 value indicating how datasets should be grouped. Valid values are: * * `none` - Datasets are put in a flat list; they are not grouped at all. * * `category` - Datasets are grouped according to their category in Socrata. * @type {String} */ this.groupBy = 'category'; knockout.track(this, ['url', 'filterQuery', 'dataCustodian','category']); }; inherit(CatalogGroup, SocrataCatalogGroup); defineProperties(SocrataCatalogGroup.prototype, { /** * Gets the type of data member represented by this instance. * @memberOf SocrataCatalogGroup.prototype * @type {String} */ type : { get : function() { return 'socrata'; } }, /** * Gets a human-readable name for this type of data source, such as 'Web Map Service (WMS)'. * @memberOf SocrataCatalogGroup.prototype * @type {String} */ typeName : { get : function() { return 'Socrata Server'; } }, /** * 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 SocrataCatalogGroup.prototype * @type {Object} */ updaters : { get : function() { return SocrataCatalogGroup.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 SocrataCatalogGroup.prototype * @type {Object} */ serializers : { get : function() { return SocrataCatalogGroup.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} */ SocrataCatalogGroup.defaultUpdaters = clone(CatalogGroup.defaultUpdaters); freezeObject(SocrataCatalogGroup.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} */ SocrataCatalogGroup.defaultSerializers = clone(CatalogGroup.defaultSerializers); SocrataCatalogGroup.defaultSerializers.items = CatalogGroup.enabledShareableItemsSerializer; SocrataCatalogGroup.defaultSerializers.isLoading = function(socrataGroup, json, propertyName, options) {}; SocrataCatalogGroup.prototype._getValuesThatInfluenceLoad = function() { return [this.url, this.filterQuery, this.groupBy, this.dataCustodian]; }; SocrataCatalogGroup.prototype._load = function() { if (!defined(this.url) || this.url.length === 0) { return undefined; } var that = this; var promises = []; for (var i = 0; i < this.filterQuery.length; i++) { // Socrata always has CORS enabled, but we may proxy anyway in IE9 or if we want to cache. var url = proxyCatalogItemUrl(that, this.url + '/api/search/views?' + this.filterQuery[i], '1d'); var promise = loadJson(url); promises.push(promise); } return when.all(promises).then( function(queryResults) { if (!defined(queryResults)) { return; } var allResults = queryResults[0]; for (var p = 1; p < queryResults.length; p++) { allResults.result.results = allResults.result.results.concat(queryResults[p].result.results); } populateGroupFromResults(that, allResults); }).otherwise(function(e) { throw new TerriaError({ sender: that, title: that.name, message: '\ Couldn\'t retrieve packages from this Socrata server.<br/><br/>\ If you entered the URL manually, please double-check it.<br/><br/>\ Otherwise, if reloading doesn\'t fix it, please report the problem by sending an email to <a href="mailto:'+that.terria.supportEmail+'">'+that.terria.supportEmail+'</a> with the technical details below. Thank you!<br/><br/>\ <pre>' + formatError(e) + '</pre>' }); }); }; function populateGroupFromResults(socrataGroup, json) { var items = json.results; for (var itemIndex = 0; itemIndex < items.length; ++itemIndex) { var item = items[itemIndex].view; var geo = item.metadata.geo; // Currently we only support spatial layers, which are identified by a geo {} object. TODO support other kinds of layers? if (!geo || !defined(item.childViews)) { // items without a 'childViews' seem to be themselves child views continue; } var id = item.category ? socrataGroup.uniqueId + '/' + item.category + '/' + item.id : socrataGroup.uniqueId + '/' + item.id; var newItem = socrataGroup.terria.catalog.shareKeyIndex[id]; var alreadyExists = defined(newItem); if (!alreadyExists) { // Socrata is currently transitioning from a Geoserver/OWS tiling system to a "new backend" with GeoJSON download but no // reliable public tiling endpoint. if (item.newBackend) { newItem = new GeoJsonCatalogItem(socrataGroup.terria); // We have to choose a number of features to truncate to. We can go as high as 50,000 but in the case of Melbourne's // urban forest canopies dataset, the file becomes 71MB. newItem.url = socrataGroup.url + '/resource/' + item.childViews[0] + '.geojson' + '?$limit=10000'; } else { newItem = new WebMapServiceCatalogItem(socrataGroup.terria); newItem.url = socrataGroup.url + geo.owsUrl; newItem.layers = geo.layers; if (geo.namespace) { // Socrata gives us a list of layers like 'geo_foo,geo_bar', but we need to prepend them with the WMS namespace. newItem.layers = geo.namespace + ':' + newItem.layers.replace(/,/g, ',' + geo.namespace + ':'); } if (geo.bboxCrs === 'EPSG:4326' && defined(geo.bbox)) { var parts = geo.bbox.split(','); if (parts.length === 4) { newItem.rectangle = Rectangle.fromDegrees(parts[0], parts[1], parts[2], parts[3]); } } } } newItem.name = item.name; newItem.id = id; if (defined(item.description)) { newItem.info.push({ name: 'Description', content: item.description }); } if (defined(item.license) && defined(item.license.name)) { newItem.info.push({ name:'Licence', content: (item.license.logoUrl ? '<img src=' + socrataGroup.url + '/' + item.license.logoUrl + ' /> &nbsp;' : '') + (item.license.termsLink ? '<a href="' + item.license.termsLink + '">' + item.license.name + '</a>' : item.license.name) }); } if (item.columns.length > 0) { newItem.info.push({ name: 'Attributes', content: item.columns.map(function(e) { return e.name; }).join() }); } if (defined(item.tags) && item.tags.length > 0) { newItem.info.push({ name: 'Tags', content: item.tags.join() }); } newItem.dataUrlType = 'direct'; // should really be landingpage or something newItem.dataUrl = socrataGroup.url + '/resource/' + item.id; if (defined(socrataGroup.dataCustodian)) { newItem.dataCustodian = socrataGroup.dataCustodian; } else { newItem.dataCustodian = item.attribution; // not quite right } if (socrataGroup.groupBy === 'category' && defined(item.category)) { var existingGroup = socrataGroup.findFirstItemByName(item.category); if (!defined(existingGroup)) { existingGroup = new CatalogGroup(socrataGroup.terria); existingGroup.name = item.category; existingGroup.id = item.category; socrataGroup.add(existingGroup); } existingGroup.add(newItem); } else { socrataGroup.add(newItem); } } } module.exports = SocrataCatalogGroup;