412 lines
12 KiB
JavaScript
412 lines
12 KiB
JavaScript
|
var $ = require('jquery');
|
||
|
var _ = require('underscore');
|
||
|
var Backbone = require('backbone');
|
||
|
var LayerDefinitionModel = require('./layer-definition-model');
|
||
|
var layerLetters = require('./layer-letters');
|
||
|
var layerColors = require('./layer-colors');
|
||
|
var nodeIds = require('builder/value-objects/analysis-node-ids');
|
||
|
var layerTypesAndKinds = require('./layer-types-and-kinds');
|
||
|
var SQLUtils = require('builder/helpers/sql-utils');
|
||
|
var TableNameUtils = require('builder/helpers/table-name-utils');
|
||
|
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||
|
var REQUIRED_OPTS = [
|
||
|
'analysisDefinitionNodesCollection',
|
||
|
'configModel',
|
||
|
'mapId',
|
||
|
'stateDefinitionModel',
|
||
|
'userModel'
|
||
|
];
|
||
|
|
||
|
var generateLabelsLayerName = function (layerName) {
|
||
|
return layerName + ' Labels';
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Collection of layer definitions
|
||
|
*/
|
||
|
module.exports = Backbone.Collection.extend({
|
||
|
|
||
|
initialize: function (models, opts) {
|
||
|
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||
|
},
|
||
|
|
||
|
url: function () {
|
||
|
var baseUrl = this._configModel.get('base_url');
|
||
|
return baseUrl + '/api/v1/maps/' + this._mapId + '/layers';
|
||
|
},
|
||
|
|
||
|
parse: function (r) {
|
||
|
return r.layers;
|
||
|
},
|
||
|
|
||
|
save: function (options) {
|
||
|
this.each(function (layerDefModel, index) {
|
||
|
layerDefModel.set('order', index, { silent: true });
|
||
|
});
|
||
|
Backbone.sync('update', this, options);
|
||
|
},
|
||
|
|
||
|
toJSON: function () {
|
||
|
return {
|
||
|
layers: Backbone.Collection.prototype.toJSON.apply(this, arguments)
|
||
|
};
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Intended to be called from entry point, to make sure initial layers are taken into account
|
||
|
*/
|
||
|
resetByLayersData: function (layersData) {
|
||
|
this.reset(layersData, {
|
||
|
silent: true,
|
||
|
initialLetters: _.chain(layersData)
|
||
|
.pluck('options')
|
||
|
.pluck('letter')
|
||
|
.compact()
|
||
|
.value()
|
||
|
});
|
||
|
|
||
|
this._sanitizeLabels();
|
||
|
},
|
||
|
|
||
|
comparator: function (model) {
|
||
|
return model.get('order');
|
||
|
},
|
||
|
|
||
|
model: function (d, opts) {
|
||
|
var self = opts.collection;
|
||
|
|
||
|
// Add data required for new editor if not set (e.g. a vis created on old editor doesn't contain letter and source)
|
||
|
var o = _.clone(d.options) || {};
|
||
|
var attrs = _.defaults(
|
||
|
{ options: o },
|
||
|
_.omit(d, ['options']
|
||
|
));
|
||
|
|
||
|
if (!isNaN(opts.at)) {
|
||
|
attrs.options.order = opts.at;
|
||
|
}
|
||
|
|
||
|
if (layerTypesAndKinds.isKindDataLayer(attrs.kind)) {
|
||
|
o.letter = o.letter || layerLetters.next(self._takenLetters(opts.initialLetters));
|
||
|
o.color = o.color || layerColors.getColorForLetter(o.letter);
|
||
|
|
||
|
// Create source attr if it does not already exist
|
||
|
var sourceId = nodeIds.next(o.letter);
|
||
|
|
||
|
if (o.table_name && (!o.source || o.source === sourceId)) {
|
||
|
var tableName = TableNameUtils.getUnqualifiedName(o.table_name);
|
||
|
var userName = o.user_name || TableNameUtils.getUsername(o.table_name);
|
||
|
|
||
|
var qualifyTableName = (userName && self._configModel.get('user_name') !== userName) || self._userModel.isInsideOrg();
|
||
|
tableName = TableNameUtils.getQualifiedTableName(o.table_name, userName, qualifyTableName);
|
||
|
|
||
|
o.source = o.source || sourceId;
|
||
|
o.query = o.query || SQLUtils.getDefaultSQLFromTableName(tableName);
|
||
|
|
||
|
// Add analysis definition unless already existing
|
||
|
self._analysisDefinitionNodesCollection.createSourceNode({
|
||
|
id: sourceId,
|
||
|
tableName: tableName,
|
||
|
query: o.query
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var parseAttrs = typeof opts.parse === 'boolean' ? opts.parse : true;
|
||
|
var m = new LayerDefinitionModel(attrs, {
|
||
|
parse: parseAttrs,
|
||
|
collection: self,
|
||
|
configModel: self._configModel
|
||
|
});
|
||
|
|
||
|
return m;
|
||
|
},
|
||
|
|
||
|
nextLetter: function () {
|
||
|
return layerLetters.next(this._takenLetters());
|
||
|
},
|
||
|
|
||
|
findAnalysisDefinitionNodeModel: function (id) {
|
||
|
return this._analysisDefinitionNodesCollection.get(id);
|
||
|
},
|
||
|
|
||
|
findOwnerOfAnalysisNode: function (nodeDefModel) {
|
||
|
return this.find(function (layerDefModel) {
|
||
|
return layerDefModel.isOwnerOfAnalysisNode(nodeDefModel);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
findPrimaryParentLayerToAnalysisNode: function (nodeDefModel, opts) {
|
||
|
if (!nodeDefModel) return;
|
||
|
|
||
|
opts = opts || {};
|
||
|
if (!_.isArray(opts.exclude)) {
|
||
|
opts.exclude = [opts.exclude];
|
||
|
}
|
||
|
var owner = this.findOwnerOfAnalysisNode(nodeDefModel);
|
||
|
|
||
|
return this.find(function (layerDefModel) {
|
||
|
if (layerDefModel === owner) return;
|
||
|
if (_.contains(opts.exclude, layerDefModel)) return;
|
||
|
|
||
|
var layerHeadNode = layerDefModel.getAnalysisDefinitionNodeModel();
|
||
|
if (nodeDefModel === layerHeadNode) return true;
|
||
|
|
||
|
if (layerHeadNode) {
|
||
|
var primarySourceOfLastOwnNode = _.last(layerHeadNode.linkedListBySameLetter()).getPrimarySource();
|
||
|
return nodeDefModel === primarySourceOfLastOwnNode;
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
|
||
|
isThereAnyGeometryData: function () {
|
||
|
var dataLayers = this._getDataLayers();
|
||
|
var allQueryGeometryModels = this._getAllQueryGeometryModels(dataLayers);
|
||
|
var queryGeometryModelsValuePromises = _.map(allQueryGeometryModels, function (queryGeometryModel) {
|
||
|
return queryGeometryModel.hasValueAsync();
|
||
|
});
|
||
|
|
||
|
return Promise.all(queryGeometryModelsValuePromises)
|
||
|
.then(function (hasGeomValues) {
|
||
|
return _.some(hasGeomValues);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
loadAllQueryGeometryModels: function (callback) {
|
||
|
var promises = this.filter(function (layerDefModel) {
|
||
|
return layerDefModel.isDataLayer();
|
||
|
}).map(function (layerDefModel) {
|
||
|
var queryGeometryModel = layerDefModel.getAnalysisDefinitionNodeModel().queryGeometryModel;
|
||
|
var status = queryGeometryModel.get('status');
|
||
|
var deferred = new $.Deferred();
|
||
|
|
||
|
if (queryGeometryModel.isFetched()) {
|
||
|
deferred.resolve();
|
||
|
} else if (queryGeometryModel.canFetch()) {
|
||
|
if (status !== 'fetching') {
|
||
|
queryGeometryModel.fetch({
|
||
|
success: function () {
|
||
|
deferred.resolve();
|
||
|
},
|
||
|
error: function () {
|
||
|
deferred.reject();
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
deferred.resolve();
|
||
|
}
|
||
|
} else {
|
||
|
deferred.reject();
|
||
|
}
|
||
|
|
||
|
return deferred.promise();
|
||
|
}, this);
|
||
|
|
||
|
$.when.apply($, promises).done(callback);
|
||
|
},
|
||
|
|
||
|
isThereAnyTorqueLayer: function () {
|
||
|
return this.any(function (model) {
|
||
|
return layerTypesAndKinds.isTorqueType(model.get('type'));
|
||
|
});
|
||
|
},
|
||
|
|
||
|
isThereAnyCartoDBLayer: function () {
|
||
|
return this.any(function (model) {
|
||
|
return layerTypesAndKinds.isCartoDBType(model.get('type'));
|
||
|
});
|
||
|
},
|
||
|
|
||
|
anyContainsNode: function (nodeDefModel) {
|
||
|
return this.any(function (layerDefModel) {
|
||
|
return layerDefModel.containsNode(nodeDefModel);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
isDataLayerOnTop: function (lyrModel) {
|
||
|
var currentPos = this.indexOf(lyrModel);
|
||
|
return currentPos === this.getTopDataLayerIndex();
|
||
|
},
|
||
|
|
||
|
getTopDataLayerIndex: function () {
|
||
|
var layerOnTop = this.getLayerOnTop();
|
||
|
var at = this.indexOf(layerOnTop);
|
||
|
var hasLabels = layerOnTop &&
|
||
|
!layerTypesAndKinds.isCartoDBType(layerOnTop.get('type')) &&
|
||
|
!layerTypesAndKinds.isTorqueType(layerOnTop.get('type'));
|
||
|
|
||
|
if (hasLabels) { at--; }
|
||
|
|
||
|
return at;
|
||
|
},
|
||
|
|
||
|
getNumberOfDataLayers: function () {
|
||
|
return this.select(function (model) {
|
||
|
return layerTypesAndKinds.isTypeDataLayer(model.get('type'));
|
||
|
}).length;
|
||
|
},
|
||
|
|
||
|
_takenLetters: function (otherLetters) {
|
||
|
var valuesFromAddedModels = _.compact(this.pluck('letter'));
|
||
|
|
||
|
// When adding multiple items the models created so far are stored in the internal object this._byId,
|
||
|
// need to make sure to take them into account when returning already taken letter.
|
||
|
var valuesFromModelsNotYetAdded = _.chain(this._byId).values().invoke('get', 'letter').value();
|
||
|
|
||
|
return _.union(valuesFromAddedModels, valuesFromModelsNotYetAdded, otherLetters);
|
||
|
},
|
||
|
|
||
|
// This ensures that we don't end up with more than one labels layer
|
||
|
_sanitizeLabels: function () {
|
||
|
var troubled = this.models.filter(function (layer) {
|
||
|
return layerTypesAndKinds.isTiledType(layer.get('type'));
|
||
|
}).filter(function (layer) {
|
||
|
var index = this.models.indexOf(layer);
|
||
|
return (index > 0 && index < this.length - 1);
|
||
|
}.bind(this));
|
||
|
|
||
|
troubled.length > 0 && _.each(troubled, function (layer) {
|
||
|
layer.destroy();
|
||
|
});
|
||
|
},
|
||
|
|
||
|
setBaseLayer: function (newBaseLayerAttrs) {
|
||
|
this.trigger('changingBaseLayer');
|
||
|
|
||
|
newBaseLayerAttrs = _.clone(newBaseLayerAttrs);
|
||
|
if (this.isBaseLayerAdded(newBaseLayerAttrs)) {
|
||
|
this.trigger('baseLayerChanged');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var newBaseLayerHasLabels = !!(newBaseLayerAttrs.labels && newBaseLayerAttrs.labels.urlTemplate);
|
||
|
|
||
|
var labelsLayerOptions = {
|
||
|
silent: true,
|
||
|
wait: true, // do not add/remove the layer unless it's created/removed/updated successfully
|
||
|
success: function () {
|
||
|
this._sanitizeLabels();
|
||
|
this.trigger('baseLayerChanged');
|
||
|
}.bind(this),
|
||
|
error: function () {
|
||
|
this.trigger('baseLayerFailed');
|
||
|
}.bind(this)
|
||
|
};
|
||
|
|
||
|
var baseLayerOptions = {
|
||
|
silent: true,
|
||
|
wait: true, // do not update the layer unless it's created/updated successfully
|
||
|
success: function () {
|
||
|
var labelsLayer = this.getLabelsLayer();
|
||
|
if (newBaseLayerHasLabels) {
|
||
|
if (labelsLayer) {
|
||
|
this._updateLabelsLayer(newBaseLayerAttrs, labelsLayerOptions);
|
||
|
} else {
|
||
|
this._addLabelsLayer(newBaseLayerAttrs, labelsLayerOptions);
|
||
|
}
|
||
|
} else {
|
||
|
if (labelsLayer) {
|
||
|
this._destroyLabelsLayer(labelsLayerOptions);
|
||
|
} else {
|
||
|
this._sanitizeLabels();
|
||
|
this.trigger('baseLayerChanged');
|
||
|
}
|
||
|
}
|
||
|
}.bind(this),
|
||
|
error: function () {
|
||
|
this.trigger('baseLayerFailed');
|
||
|
}.bind(this)
|
||
|
};
|
||
|
|
||
|
if (this.getBaseLayer()) {
|
||
|
this._updateBaseLayer(newBaseLayerAttrs, baseLayerOptions);
|
||
|
} else {
|
||
|
this._createBaseLayer(newBaseLayerAttrs, baseLayerOptions);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
isBaseLayerAdded: function (basemapAttrs) {
|
||
|
var baseLayerDefinitionModel = this.getBaseLayer();
|
||
|
return baseLayerDefinitionModel.matchesAttrs(basemapAttrs);
|
||
|
},
|
||
|
|
||
|
_updateBaseLayer: function (basemapAttrs, opts) {
|
||
|
var currentBaseLayer = this.getBaseLayer();
|
||
|
var newBaseLayerAttrs = _.omit(basemapAttrs, [ 'id', 'order', 'labels', 'template' ]);
|
||
|
|
||
|
currentBaseLayer.attributes = _.extend({
|
||
|
order: 0
|
||
|
}, _.pick(currentBaseLayer.attributes, [
|
||
|
'id',
|
||
|
'type',
|
||
|
'category'
|
||
|
]));
|
||
|
|
||
|
currentBaseLayer.save(newBaseLayerAttrs, opts);
|
||
|
},
|
||
|
|
||
|
_createBaseLayer: function (newBaseLayerAttrs, opts) {
|
||
|
newBaseLayerAttrs = _.extend(newBaseLayerAttrs, {
|
||
|
order: 0
|
||
|
});
|
||
|
return this.create(newBaseLayerAttrs, _.extend({ at: 0 }, opts));
|
||
|
},
|
||
|
|
||
|
_updateLabelsLayer: function (newBaseLayerAttrs, opts) {
|
||
|
var labelsLayer = this.getLabelsLayer();
|
||
|
var newAttrs = _.omit(newBaseLayerAttrs, 'labels');
|
||
|
|
||
|
newAttrs = _.pick(newAttrs, _.identity);
|
||
|
newAttrs = _.extend(newAttrs, newBaseLayerAttrs.labels, {
|
||
|
name: generateLabelsLayerName(newBaseLayerAttrs.name)
|
||
|
});
|
||
|
|
||
|
labelsLayer.save(newAttrs, opts);
|
||
|
},
|
||
|
|
||
|
_addLabelsLayer: function (newBaseLayerAttrs, opts) {
|
||
|
var optionsAttribute = _.pick(newBaseLayerAttrs, ['type', 'subdomains', 'minZoom', 'maxZoom', 'name', 'className', 'attribution', 'category']);
|
||
|
optionsAttribute = _.extend(optionsAttribute, newBaseLayerAttrs.labels, {
|
||
|
name: generateLabelsLayerName(newBaseLayerAttrs.name)
|
||
|
});
|
||
|
|
||
|
var labelsLayerAttrs = {
|
||
|
order: this.getLayerOnTop().get('order') + 1,
|
||
|
options: optionsAttribute
|
||
|
};
|
||
|
|
||
|
this.create(labelsLayerAttrs, opts);
|
||
|
},
|
||
|
|
||
|
_destroyLabelsLayer: function (opts) {
|
||
|
this.getLayerOnTop().destroy(opts);
|
||
|
},
|
||
|
|
||
|
_getAllQueryGeometryModels: function (dataLayers) {
|
||
|
return _.compact(_.map(dataLayers, function (dataLayer) {
|
||
|
return dataLayer.getAnalysisDefinitionNodeModel().queryGeometryModel;
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
_getDataLayers: function () {
|
||
|
return this.filter(function (layerDefinitionModel) {
|
||
|
return layerDefinitionModel.isDataLayer();
|
||
|
});
|
||
|
},
|
||
|
|
||
|
getBaseLayer: function () {
|
||
|
return this.first();
|
||
|
},
|
||
|
|
||
|
getLabelsLayer: function () {
|
||
|
var layerOnTop = this.getLayerOnTop();
|
||
|
if (this.size() > 1 && layerTypesAndKinds.isTiledType(layerOnTop.get('type'))) {
|
||
|
return layerOnTop;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
getLayerOnTop: function () {
|
||
|
return this.last();
|
||
|
}
|
||
|
});
|