Initial commit
This commit is contained in:
34
lib/assets/javascripts/builder/data/acl-item-model.js
Executable file
34
lib/assets/javascripts/builder/data/acl-item-model.js
Executable file
@@ -0,0 +1,34 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var PermissionModel = require('./permission-model');
|
||||
var OWN_ATTR_NAMES = ['id', 'username', 'avatar_url', 'name'];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
access: 'r'
|
||||
},
|
||||
|
||||
isOwn: function (model) {
|
||||
return model.id === this.get('entity').id;
|
||||
},
|
||||
|
||||
validate: function (attrs, options) {
|
||||
if (attrs.access !== PermissionModel.READ_ONLY && attrs.access !== PermissionModel.READ_WRITE) {
|
||||
return "access can't take 'r' or 'rw' values";
|
||||
}
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var entity = _.pick(this.get('entity').toJSON(), OWN_ATTR_NAMES);
|
||||
// translate name to username
|
||||
if (!entity.username) {
|
||||
entity.username = entity.name;
|
||||
delete entity.name;
|
||||
}
|
||||
return {
|
||||
type: this.get('type') || 'user',
|
||||
entity: entity,
|
||||
access: this.get('access')
|
||||
};
|
||||
}
|
||||
});
|
||||
527
lib/assets/javascripts/builder/data/analyses.js
Normal file
527
lib/assets/javascripts/builder/data/analyses.js
Normal file
@@ -0,0 +1,527 @@
|
||||
var _ = require('underscore');
|
||||
var camshaftReference = require('./camshaft-reference');
|
||||
var AreaOfInfluenceFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/area-of-influence-form-model');
|
||||
var AggregateIntersectionFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/aggregate-intersection-form-model');
|
||||
var FilterIntersectionFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-intersection-form-model');
|
||||
var FallbackFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/fallback-form-model');
|
||||
var UnknownTypeFormModel = require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/unknown-type-form-model');
|
||||
var MergeAnalysisOptionModel = require('builder/components/modals/add-analysis/analysis-option-models/merge-analysis-option-model');
|
||||
var DeprecatedSQLFunctionOptionModel = require('builder/components/modals/add-analysis/analysis-option-models/deprecated-sql-function-option-model');
|
||||
var DataServicesApiCheck = require('builder/editor/layers/layer-content-views/analyses/analyses-quota/analyses-quota-info');
|
||||
|
||||
var DAO_TYPES = ['age-and-gender', 'boundaries', 'education', 'employment', 'families', 'housing', 'income', 'language', 'migration', 'nationality', 'population-segments', 'race-and-ethnicity', 'religion', 'transportation'];
|
||||
|
||||
var MAP = {
|
||||
'aggregate-intersection': {
|
||||
title: _t('analyses.aggregate-intersection.title'),
|
||||
short_title: _t('analyses.aggregate-intersection.short-title'),
|
||||
FormModel: AggregateIntersectionFormModel,
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.aggregate-intersection.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.aggregate-intersection.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/intersect-and-aggregate/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/aggregate-intersection.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/intersect.tpl')
|
||||
},
|
||||
'bounding-box': {
|
||||
title: _t('analyses.bounding-box.title'),
|
||||
short_title: _t('analyses.bounding-box.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/group-points.tpl'),
|
||||
genericType: 'group-points'
|
||||
},
|
||||
'bounding-circle': {
|
||||
title: _t('analyses.bounding-circle.title'),
|
||||
short_title: _t('analyses.bounding-circle.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/group-points.tpl'),
|
||||
genericType: 'group-points'
|
||||
},
|
||||
'buffer': {
|
||||
title: _t('analyses.area-of-influence.title'),
|
||||
short_title: _t('analyses.area-of-influence.short-title'),
|
||||
FormModel: AreaOfInfluenceFormModel,
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.area-of-influence.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.area-of-influence.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/create-travel-distance-buffers/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/area-of-influence.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/aoi.tpl'),
|
||||
genericType: 'area-of-influence'
|
||||
},
|
||||
'centroid': {
|
||||
title: _t('analyses.centroid.title'),
|
||||
short_title: _t('analyses.centroid.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/centroid-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.centroid.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.centroid.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/create-centroids-of-geometries/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/centroid.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/centroid.tpl'),
|
||||
genericType: 'centroid'
|
||||
},
|
||||
'convex-hull': {
|
||||
title: _t('analyses.convex-hull.title'),
|
||||
short_title: _t('analyses.convex-hull.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.group-points.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.group-points.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/create-polygons-from-points/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses//group-points.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/group-points.tpl'),
|
||||
animationParams: {
|
||||
method: _t('analyses-onboarding.placeholders.method')
|
||||
},
|
||||
genericType: 'group-points'
|
||||
},
|
||||
'concave-hull': {
|
||||
title: _t('analyses.concave-hull.title'),
|
||||
short_title: _t('analyses.concave-hull.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/group-points-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/group-points.tpl'),
|
||||
genericType: 'group-points'
|
||||
},
|
||||
'data-observatory-measure': {
|
||||
title: _t('analyses.data-observatory-measure.title'),
|
||||
short_title: _t('analyses.data-observatory-measure.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/data-observatory-measure-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.data-observatory-measure.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.data-observatory-measure.desc'),
|
||||
modalCategory: 'create_clean',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/enrich-from-data-observatory',
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfDataObservatoryIsEnabled(options);
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/data-observatory-measure.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/data-observatory-measure.tpl'),
|
||||
genericType: 'data-observatory-measure'
|
||||
},
|
||||
'data-observatory-multiple-measures': {
|
||||
title: _t('analyses.data-observatory-measure.title'),
|
||||
short_title: _t('analyses.data-observatory-measure.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/data-observatory-multiple-measure-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.data-observatory-measure.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.data-observatory-measure.desc'),
|
||||
modalCategory: 'create_clean',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/enrich-from-data-observatory',
|
||||
checkIfEnabled: function (options) {
|
||||
var dataservicesApiHealth = DataServicesApiCheck.get(options.configModel);
|
||||
var doApiData = dataservicesApiHealth.getService('observatory');
|
||||
var assignedQuota = doApiData ? doApiData.get('monthly_quota') : 0;
|
||||
var softLimit = doApiData ? doApiData.get('soft_limit') : false;
|
||||
var hideDataObservatory = assignedQuota === 0 && !softLimit;
|
||||
|
||||
return checkIfDataObservatoryIsEnabled(options) && !hideDataObservatory;
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/data-observatory-multiple-measure.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/data-observatory-measure.tpl'),
|
||||
genericType: 'data-observatory-multiple-measures'
|
||||
},
|
||||
'filter-by-node-column': {
|
||||
title: _t('analyses.filter-by-node-column.title'),
|
||||
short_title: _t('analyses.filter-by-node-column.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-by-node-column'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.filter-by-node-column.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.filter-by-node-column.desc'),
|
||||
modalCategory: 'create_clean',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/link-second-layer/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/filter-by-node-column.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/filter-by-layer.tpl'),
|
||||
animationParams: {
|
||||
name_of_the_layer: _t('analyses-onboarding.placeholders.layer-name')
|
||||
}
|
||||
},
|
||||
'filter-category': {
|
||||
title: _t('analyses.filter.title'),
|
||||
short_title: _t('analyses.filter.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/filter.tpl'),
|
||||
genericType: 'filter'
|
||||
},
|
||||
'filter-range': {
|
||||
title: _t('analyses.filter.title'),
|
||||
short_title: _t('analyses.filter.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/filter-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.filter.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.filter.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/filter-by-column-value',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/filter.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/filter-by-column-value.tpl'),
|
||||
genericType: 'filter'
|
||||
},
|
||||
'georeference-city': {
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfInternalGeocodingIsEnabled(options);
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'georeference-ip-address': {
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfInternalGeocodingIsEnabled(options);
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'georeference-country': {
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfInternalGeocodingIsEnabled(options);
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'georeference-long-lat': {
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'georeference-placeholder': {
|
||||
dummy: true,
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.georeference.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.georeference.desc'),
|
||||
modalCategory: 'create_clean',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/georeference',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'georeference-postal-code': {
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfInternalGeocodingIsEnabled(options);
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'georeference-street-address': {
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfExternalGeocodingIsEnabled(options);
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'georeference-admin-region': {
|
||||
title: _t('analyses.georeference.title'),
|
||||
short_title: _t('analyses.georeference.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/georeference-form-model'),
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfInternalGeocodingIsEnabled(options);
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/georeference.tpl'),
|
||||
genericType: 'georeference'
|
||||
},
|
||||
'intersection': {
|
||||
title: _t('analyses.filter-intersection.title'),
|
||||
short_title: _t('analyses.filter-intersection.short-title'),
|
||||
FormModel: FilterIntersectionFormModel,
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.filter-intersection.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.filter-intersection.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/select-points-in-polygons/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/intersection.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/filter-by-polygon.tpl'),
|
||||
checkIfEnabled: function (options) {
|
||||
return options.queryGeometryModel.isPolygon();
|
||||
}
|
||||
},
|
||||
'kmeans': {
|
||||
title: _t('analyses.kmeans.title'),
|
||||
short_title: _t('analyses.kmeans.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/kmeans-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.kmeans.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.kmeans.desc'),
|
||||
modalCategory: 'analyze_predict',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/calculate-clusters-of-points',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/kmeans.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/kmeans.tpl'),
|
||||
animationParams: {
|
||||
method: _t('analyses-onboarding.placeholders.clusters')
|
||||
}
|
||||
},
|
||||
'line-to-single-point': {
|
||||
title: _t('analyses.connect-with-lines.title'),
|
||||
short_title: _t('analyses.connect-with-lines.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/connect-with-lines-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.connect-with-lines.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.connect-with-lines.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/create-lines-from-points/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/connect-with-lines.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/connect-with-lines.tpl'),
|
||||
genericType: 'connect-with-lines'
|
||||
},
|
||||
'line-source-to-target': {
|
||||
title: _t('analyses.connect-with-lines.title'),
|
||||
short_title: _t('analyses.connect-with-lines.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/connect-with-lines-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/connect-with-lines.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/connect-with-lines.tpl'),
|
||||
genericType: 'connect-with-lines'
|
||||
},
|
||||
'line-sequential': {
|
||||
title: _t('analyses.connect-with-lines.title'),
|
||||
short_title: _t('analyses.connect-with-lines.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/connect-with-lines-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/connect-with-lines.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/connect-with-lines.tpl'),
|
||||
genericType: 'connect-with-lines'
|
||||
},
|
||||
'merge': {
|
||||
title: _t('analyses.merge.title'),
|
||||
short_title: _t('analyses.merge.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/merge-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.merge.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.merge.desc'),
|
||||
ModalModel: MergeAnalysisOptionModel,
|
||||
modalCategory: 'create_clean',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/add-columns-from-second-dataset/',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/merge.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/join-two-columns.tpl')
|
||||
},
|
||||
'moran': {
|
||||
title: _t('analyses.moran-cluster.title'),
|
||||
short_title: _t('analyses.moran-cluster.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/moran-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.moran-cluster.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.moran-cluster.desc'),
|
||||
modalCategory: 'analyze_predict',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/detect-outliers-and-clusters',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/moran.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/outliers.tpl')
|
||||
},
|
||||
'routing-sequential': {
|
||||
title: _t('analyses.routing.title'),
|
||||
short_title: _t('analyses.routing.short-title'),
|
||||
FormModel: FallbackFormModel,
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfRoutingIsEnabled(options);
|
||||
}
|
||||
},
|
||||
'routing-to-layer-all-to-all': {
|
||||
title: _t('analyses.routing.title'),
|
||||
short_title: _t('analyses.routing.short-title'),
|
||||
FormModel: FallbackFormModel,
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfRoutingIsEnabled(options);
|
||||
}
|
||||
},
|
||||
'routing-to-single-point': {
|
||||
title: _t('analyses.routing.title'),
|
||||
short_title: _t('analyses.routing.short-title'),
|
||||
FormModel: FallbackFormModel,
|
||||
checkIfEnabled: function (options) {
|
||||
return checkIfRoutingIsEnabled(options);
|
||||
}
|
||||
},
|
||||
'sampling': {
|
||||
title: _t('analyses.sampling.title'),
|
||||
short_title: _t('analyses.sampling.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/sampling-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.sampling.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.sampling.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/subsample-percent-of-rows',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/sampling.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/sampling.tpl'),
|
||||
animationParams: {
|
||||
percentage: _t('analyses-onboarding.placeholders.percentage')
|
||||
},
|
||||
genericType: 'sampling'
|
||||
},
|
||||
'spatial-markov-trend': {
|
||||
title: _t('analyses.spatial-markov-trend.title'),
|
||||
short_title: _t('analyses.spatial-markov-trend.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/spatial-markov-trend-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.spatial-markov-trend.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.spatial-markov-trend.desc'),
|
||||
modalCategory: 'analyze_predict',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/predict-trends-and-volatility',
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/spatial-markov-trend.tpl'),
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/predict-trends.tpl')
|
||||
},
|
||||
'trade-area': {
|
||||
title: _t('analyses.area-of-influence.title'),
|
||||
short_title: _t('analyses.area-of-influence.short-title'),
|
||||
FormModel: AreaOfInfluenceFormModel,
|
||||
checkIfEnabled: function (options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
return !!options.configModel.dataServiceEnabled('isolines');
|
||||
},
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/area-of-influence.tpl'),
|
||||
genericType: 'area-of-influence'
|
||||
},
|
||||
'weighted-centroid': {
|
||||
title: _t('analyses.centroid.title'),
|
||||
short_title: _t('analyses.centroid.short-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/centroid-form-model'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/centroid.tpl'),
|
||||
genericType: 'centroid'
|
||||
},
|
||||
'closest': {
|
||||
title: _t('editor.layers.analysis-form.find-nearest.workflow-title'),
|
||||
short_title: _t('editor.layers.analysis-form.find-nearest.workflow-title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/closest-form-model'),
|
||||
modalTitle: _t('editor.layers.analysis-form.find-nearest.modal-title'),
|
||||
modalDesc: _t('editor.layers.analysis-form.find-nearest.modal-desc'),
|
||||
modalCategory: 'analyze_predict',
|
||||
modalLink: 'https://carto.com/learn/guides/analysis/find-nearest',
|
||||
animationTemplate: require('builder/components/modals/add-analysis/animation-templates/closest.tpl'),
|
||||
onboardingTemplate: require('builder/components/onboardings/analysis/analyses/closest.tpl')
|
||||
},
|
||||
'deprecated-sql-function': {
|
||||
title: _t('editor.layers.analysis-form.deprecated-sql-function.title'),
|
||||
short_title: _t('editor.layers.analysis-form.deprecated-sql-function.title'),
|
||||
FormModel: require('builder/editor/layers/layer-content-views/analyses/analysis-form-models/deprecated-sql-function-form-model'),
|
||||
modalTitle: _t('components.modals.add-analysis.option-types.deprecated-sql-function.title'),
|
||||
modalDesc: _t('components.modals.add-analysis.option-types.deprecated-sql-function.desc'),
|
||||
modalCategory: 'data_transformation',
|
||||
ModalModel: DeprecatedSQLFunctionOptionModel,
|
||||
checkIfEnabled: function (options) {
|
||||
return options.userModel.featureEnabled('deprecated-sql-function');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var checkIfInternalGeocodingIsEnabled = function (options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('geocoder_internal');
|
||||
};
|
||||
|
||||
var checkIfRoutingIsEnabled = function (options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('routing');
|
||||
};
|
||||
|
||||
var checkIfExternalGeocodingIsEnabled = function (options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('hires_geocoder');
|
||||
};
|
||||
|
||||
var checkIfDataObservatoryIsEnabled = function (options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
return isDataServicesReady(options.configModel) && !!options.configModel.dataServiceEnabled('data_observatory');
|
||||
};
|
||||
|
||||
var definitionByType = function (m) {
|
||||
var type = 'unknown';
|
||||
|
||||
if (typeof m === 'string' && m !== '') {
|
||||
type = m;
|
||||
} else if (m && m.get) {
|
||||
type = m.get('type');
|
||||
|
||||
if (type === 'data-observatory-measure' && m.has('measurement')) {
|
||||
var measurement = m.get('measurement').toLowerCase().split(' ').join('-');
|
||||
|
||||
if (_.contains(DAO_TYPES, measurement)) {
|
||||
var measurementType = _.clone(MAP[type]);
|
||||
measurementType.title = _t('analyses.data-observatory-measure.' + measurement);
|
||||
return measurementType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MAP[type] || {
|
||||
title: _t('analyses.' + type),
|
||||
short_title: _t('analyses.' + type),
|
||||
modalLink: '',
|
||||
FormModel: FallbackFormModel
|
||||
};
|
||||
};
|
||||
|
||||
var isDataServicesReady = function (configModel) {
|
||||
return DataServicesApiCheck.get(configModel).isReady();
|
||||
};
|
||||
|
||||
var _isAnalysisValidByType = function (type, options) {
|
||||
var analysis = MAP[type];
|
||||
return analysis && analysis.checkIfEnabled ? analysis.checkIfEnabled(options) : true;
|
||||
};
|
||||
|
||||
var DATA_OBSERVATORY_OLD = 'data-observatory-measure';
|
||||
|
||||
module.exports = {
|
||||
MAP: MAP,
|
||||
|
||||
findFormModelByType: function (type) {
|
||||
var isDummy = MAP[type] && MAP[type].dummy;
|
||||
return camshaftReference.hasType(type) || isDummy
|
||||
? definitionByType(type).FormModel
|
||||
: UnknownTypeFormModel;
|
||||
},
|
||||
|
||||
getAnalysesByModalCategory: function (category, options) {
|
||||
return _.reduce(MAP, function (memo, item, analysisType) {
|
||||
if (analysisType === DATA_OBSERVATORY_OLD) {
|
||||
return memo;
|
||||
}
|
||||
|
||||
if (item.modalCategory === category) {
|
||||
if (_isAnalysisValidByType(analysisType, options)) {
|
||||
var obj = {
|
||||
nodeAttrs: {
|
||||
type: analysisType
|
||||
},
|
||||
title: item.modalTitle,
|
||||
desc: item.modalDesc,
|
||||
link: item.modalLink
|
||||
};
|
||||
|
||||
if (item.ModalModel) {
|
||||
obj.Model = item.ModalModel;
|
||||
}
|
||||
|
||||
if (item.dummy) {
|
||||
obj.dummy = item.dummy;
|
||||
}
|
||||
|
||||
memo.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, []);
|
||||
},
|
||||
|
||||
isAnalysisValidByType: function (type, options) {
|
||||
return _isAnalysisValidByType(type, options);
|
||||
},
|
||||
|
||||
getAnalysisByType: function (type) {
|
||||
return MAP[type];
|
||||
},
|
||||
|
||||
title: function (model) {
|
||||
return definitionByType(model).title;
|
||||
},
|
||||
|
||||
short_title: function (model) {
|
||||
return definitionByType(model).short_title;
|
||||
},
|
||||
|
||||
link: function (model) {
|
||||
return definitionByType(model).modalLink;
|
||||
}
|
||||
};
|
||||
53
lib/assets/javascripts/builder/data/analysis-definition-model.js
Executable file
53
lib/assets/javascripts/builder/data/analysis-definition-model.js
Executable file
@@ -0,0 +1,53 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
|
||||
/**
|
||||
* Analysis definition model.
|
||||
* Points to a node that is the head of the particular analysis.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.sync} abort ongoing request if there is any
|
||||
*/
|
||||
sync: syncAbort,
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
|
||||
|
||||
this.USER_SAVED = false; // flag to indicate if the user saved the node and avoid triggering the onboarding model if they didn't
|
||||
|
||||
this._analysisDefinitionNodesCollection = opts.analysisDefinitionNodesCollection;
|
||||
},
|
||||
|
||||
parse: function (r, opts) {
|
||||
// If parse is called on a new model it have not yet called initialize and thus don't have the collection
|
||||
var nodesCollection = this._analysisDefinitionNodesCollection || opts.analysisDefinitionNodesCollection;
|
||||
|
||||
// Merge and parse are used to sync all nodes in a definition with the backend response
|
||||
nodesCollection.add(r.analysis_definition, {silent: true, merge: true, parse: true});
|
||||
|
||||
var attrs = _.omit(r, 'analysis_definition');
|
||||
attrs.node_id = r.analysis_definition.id;
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return {
|
||||
id: this.id,
|
||||
analysis_definition: this.getNodeDefinitionModel().toJSON()
|
||||
};
|
||||
},
|
||||
|
||||
containsNode: function (other) {
|
||||
var nodeDefModel = this.getNodeDefinitionModel();
|
||||
return !!(nodeDefModel && nodeDefModel.containsNode(other));
|
||||
},
|
||||
|
||||
getNodeDefinitionModel: function () {
|
||||
return this._analysisDefinitionNodesCollection.get(this.get('node_id'));
|
||||
}
|
||||
|
||||
});
|
||||
327
lib/assets/javascripts/builder/data/analysis-definition-node-model.js
Executable file
327
lib/assets/javascripts/builder/data/analysis-definition-node-model.js
Executable file
@@ -0,0 +1,327 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var camshaftReference = require('./camshaft-reference');
|
||||
var QueryGeometryModel = require('./query-geometry-model');
|
||||
var QuerySchemaModel = require('./query-schema-model');
|
||||
var nodeIds = require('builder/value-objects/analysis-node-ids');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var QueryRowsCollection = require('./query-rows-collection');
|
||||
var fetchAllQueryObjects = require('builder/helpers/fetch-all-query-objects');
|
||||
var layerColors = require('builder/data/layer-colors');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Base model for an analysis definition node.
|
||||
* May point to one or multiple nodes in turn (referenced by ids).
|
||||
*
|
||||
*
|
||||
* Analysis-definition-node-model
|
||||
* ├── Query-Schema-Model
|
||||
* ├── Query-Geometry-Model
|
||||
* └── Query-Rows-Collection
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!this.id) throw new Error('id is required');
|
||||
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
var modelOpts = {
|
||||
configModel: this._configModel,
|
||||
userModel: opts.userModel
|
||||
};
|
||||
|
||||
var simpleGeom = this.get('simple_geom');
|
||||
|
||||
this.querySchemaModel = new QuerySchemaModel(undefined, modelOpts);
|
||||
|
||||
this.queryGeometryModel = new QueryGeometryModel({
|
||||
simple_geom: simpleGeom,
|
||||
status: simpleGeom ? 'fetched' : 'unfetched'
|
||||
}, modelOpts);
|
||||
|
||||
this.queryRowsCollection = new QueryRowsCollection([], {
|
||||
configModel: this._configModel,
|
||||
userModel: opts.userModel,
|
||||
tableName: '',
|
||||
querySchemaModel: this.querySchemaModel
|
||||
});
|
||||
|
||||
this.listenTo(this.queryGeometryModel, 'change:simple_geom', this._onGeometryTypeChanged);
|
||||
},
|
||||
|
||||
fetchQueryObjects: function () {
|
||||
if (this.get('status') === 'ready' && !this.get('error')) {
|
||||
fetchAllQueryObjects({
|
||||
queryGeometryModel: this.queryGeometryModel,
|
||||
querySchemaModel: this.querySchemaModel,
|
||||
queryRowsCollection: this.queryRowsCollection
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.parse} flatten the provided analysis data and create source nodes if there are any.
|
||||
*/
|
||||
parse: function (r, opts) {
|
||||
// Allow omitting type when merging into an existing node
|
||||
var sourceNames = (!r.type && opts.merge) ? [] : camshaftReference.getSourceNamesForAnalysisType(r.type);
|
||||
|
||||
var parsedParams = _.reduce(r.params, function (memo, val, name) {
|
||||
var sourceName = sourceNames[sourceNames.indexOf(name)];
|
||||
|
||||
if (sourceName) {
|
||||
this.collection.add(val, opts);
|
||||
memo[name] = val.id;
|
||||
} else {
|
||||
memo[name] = val;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {}, this);
|
||||
|
||||
var parsedOptions = _.reduce(r.options, function (memo, val, name) {
|
||||
memo[name] = val;
|
||||
return memo;
|
||||
}, {}, this);
|
||||
|
||||
return _.defaults(
|
||||
_.omit(r, 'params', 'options'),
|
||||
parsedOptions,
|
||||
parsedParams
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.toJSON} unflatten the internal structure to the expected nested JSON data structure.
|
||||
* @param {Object} options
|
||||
* @param {Boolean} options.skipOptions - Add or not analysis options to the definition
|
||||
*/
|
||||
toJSON: function (options) {
|
||||
options = options || {};
|
||||
|
||||
var analysisParams = [];
|
||||
var paramsforJSON = {};
|
||||
var paramsForType = camshaftReference.paramsForType(this.get('type'));
|
||||
|
||||
// Undo the parsing of the params previously done in .parse() (when model was created)
|
||||
for (var name in paramsForType) {
|
||||
var param = paramsForType[name];
|
||||
var val = this.get(name);
|
||||
var includeParamInJSON = true;
|
||||
|
||||
if (param.type === 'node') {
|
||||
var node = this.collection.get(val);
|
||||
if (node) {
|
||||
val = node.toJSON(options);
|
||||
} else if (param.optional) { // Node has no value but is optional. We don't serialize it.
|
||||
includeParamInJSON = false;
|
||||
} else { // Node has no value and is required. Throw error.
|
||||
throw new Error('no node found for param "' + name + '" with id "' + val + '" in node ' + JSON.stringify(this.attributes));
|
||||
}
|
||||
}
|
||||
|
||||
if (includeParamInJSON) {
|
||||
paramsforJSON[name] = val;
|
||||
analysisParams.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
var json = {
|
||||
id: this.get('id'),
|
||||
type: this.get('type'),
|
||||
params: paramsforJSON
|
||||
};
|
||||
|
||||
// style_history is not sent at all (neither inside params nor options). It is always set by the backend.
|
||||
var optionsAttrs = _.omit(this.attributes, analysisParams.concat('id', 'type', 'status', 'style_history'));
|
||||
if (!(options.skipOptions || _.isEmpty(optionsAttrs))) {
|
||||
json['options'] = optionsAttrs;
|
||||
}
|
||||
|
||||
return json;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.isNew} for this.destroy() to work (not try to send DELETE request)
|
||||
*/
|
||||
isNew: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
isReadOnly: function () {
|
||||
// By default, any node should be read only by default, except the source node (with exceptions)
|
||||
return true;
|
||||
},
|
||||
|
||||
isCustomQueryApplied: function () {
|
||||
// By default, any node shouldn't have a custom query applied, except the source node
|
||||
return false;
|
||||
},
|
||||
|
||||
canBeDeletedByUser: function () {
|
||||
return this.hasPrimarySource();
|
||||
},
|
||||
|
||||
hasFailed: function () {
|
||||
return this.get('status') === 'failed';
|
||||
},
|
||||
|
||||
isDone: function () {
|
||||
return this.get('status') === 'ready';
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
if (this.querySchemaModel) {
|
||||
this.querySchemaModel.destroy();
|
||||
this.querySchemaModel = null;
|
||||
}
|
||||
|
||||
return Backbone.Model.prototype.destroy.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new node that have same params as the current node, with only the id differing.
|
||||
* @param {String} newId - e.g. 'b1'
|
||||
* @return {Object} instance of analysis-definition-node-model
|
||||
*/
|
||||
clone: function (newId) {
|
||||
if (!newId) throw new Error('newId is required');
|
||||
if (!_.isString(newId) || newId.length < 2) throw new Error('newId is required as a non-empty string');
|
||||
if (newId === this.id) throw new Error('newId must be different from current id, ' + this.id);
|
||||
|
||||
var attrs = this.toJSON();
|
||||
attrs.id = newId;
|
||||
return this.collection.add(attrs);
|
||||
},
|
||||
|
||||
linkedListBySameLetter: function () {
|
||||
var list = [this];
|
||||
var prev = this;
|
||||
var letter = this.letter();
|
||||
|
||||
while (true) {
|
||||
var current = prev.getPrimarySource();
|
||||
if (current && current.letter() === letter) {
|
||||
list.push(current);
|
||||
prev = current;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Boolean, null} null if the geoemtry type of this node is not known,
|
||||
* this is to indicate that the query model is not ready yet, up to caller to make the call or not
|
||||
*/
|
||||
isValidAsInputForType: function (analysisType) {
|
||||
if (analysisType === 'source') return false;
|
||||
|
||||
var geometry = this.queryGeometryModel.get('simple_geom');
|
||||
return geometry
|
||||
? camshaftReference.isValidInputGeometryForType(geometry, analysisType)
|
||||
: null;
|
||||
},
|
||||
|
||||
containsNode: function (other) {
|
||||
if (!other) return false;
|
||||
return this.id === other.id || this._sourcesContains(other);
|
||||
},
|
||||
|
||||
hasPrimarySource: function () {
|
||||
return !!this.getPrimarySource();
|
||||
},
|
||||
|
||||
hasSecondarySource: function () {
|
||||
return !!this.getSecondarySource();
|
||||
},
|
||||
|
||||
getPrimarySource: function () {
|
||||
return this.collection.get(this._getPrimarySourceId());
|
||||
},
|
||||
|
||||
getSecondarySource: function () {
|
||||
// Assumption: there are only 1-2 sources, so secondary is the one that's not the primary
|
||||
var primarySourceId = this._getPrimarySourceId();
|
||||
var secondarySourceId = _.find(this.sourceIds(), function (sourceId) {
|
||||
return sourceId !== primarySourceId;
|
||||
});
|
||||
return this.collection.get(secondarySourceId);
|
||||
},
|
||||
|
||||
// Default query by default is the query already applied in the node.
|
||||
// For source node is totally different
|
||||
getDefaultQuery: function () {
|
||||
return this.get('query');
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Array} e.g. ['c3', 'b2']
|
||||
*/
|
||||
sourceIds: function () {
|
||||
return _.map(this._sourceNames(), function (sourceName) {
|
||||
return this.get(sourceName);
|
||||
}, this);
|
||||
},
|
||||
|
||||
changeSourceIds: function (currentId, newId, silenty) {
|
||||
this._sourceNames().forEach(function (sourceName) {
|
||||
if (this.get(sourceName) === currentId) {
|
||||
this.set(sourceName, newId, { silent: silenty });
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
isSourceType: function () {
|
||||
return this.get('type') === 'source';
|
||||
},
|
||||
|
||||
letter: function () {
|
||||
return nodeIds.letter(this.id);
|
||||
},
|
||||
|
||||
getColor: function () {
|
||||
var letter = nodeIds.letter(this.id);
|
||||
return layerColors.getColorForLetter(letter);
|
||||
},
|
||||
|
||||
getStyleHistoryForLayer: function (layer_id) {
|
||||
var styleHistory = this.get('style_history');
|
||||
return styleHistory && styleHistory[layer_id];
|
||||
},
|
||||
|
||||
// Private methods
|
||||
|
||||
_sourcesContains: function (other) {
|
||||
var primarySource = this.getPrimarySource();
|
||||
var secondarySource = this.getSecondarySource();
|
||||
return !!(
|
||||
(primarySource && primarySource.containsNode(other)) ||
|
||||
(secondarySource && secondarySource.containsNode(other))
|
||||
);
|
||||
},
|
||||
|
||||
_getPrimarySourceId: function () {
|
||||
var primarySourceName = this.get('primary_source_name') || this._sourceNames()[0];
|
||||
return this.get(primarySourceName);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Array} e.g. ['polygons_source', 'points_source']
|
||||
*/
|
||||
_sourceNames: function () {
|
||||
return camshaftReference.getSourceNamesForAnalysisType(this.get('type'));
|
||||
},
|
||||
|
||||
_onGeometryTypeChanged: function (model, value) {
|
||||
this.set('simple_geom', value);
|
||||
}
|
||||
|
||||
});
|
||||
94
lib/assets/javascripts/builder/data/analysis-definition-node-source-model.js
Executable file
94
lib/assets/javascripts/builder/data/analysis-definition-node-source-model.js
Executable file
@@ -0,0 +1,94 @@
|
||||
var AnalysisDefinitionNodeModel = require('./analysis-definition-node-model');
|
||||
var TableModel = require('./table-model');
|
||||
var SQLUtils = require('builder/helpers/sql-utils');
|
||||
|
||||
/**
|
||||
* Case of a node model representing a source node.
|
||||
* - It should provide new info about if the node is read only or not.
|
||||
*
|
||||
* Analysis-definition-node-source-model
|
||||
* ├── Query-Schema-Model
|
||||
* ├── Query-Geometry-Model
|
||||
* └── Table-Model
|
||||
*/
|
||||
module.exports = AnalysisDefinitionNodeModel.extend({
|
||||
|
||||
defaults: {
|
||||
status: 'ready'
|
||||
},
|
||||
|
||||
/**
|
||||
* @override AnalysisDefinitionNodeModel.prototype.initialize
|
||||
*/
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
AnalysisDefinitionNodeModel.prototype.initialize.apply(this, arguments);
|
||||
|
||||
var query = this.get('query');
|
||||
this.querySchemaModel.set({
|
||||
query: query,
|
||||
ready: true
|
||||
}, { silent: true });
|
||||
|
||||
this.queryGeometryModel.set({
|
||||
query: query,
|
||||
ready: true
|
||||
}, { silent: true });
|
||||
|
||||
// TODO: we should check if it is necessary to check if we have to overwrite the whole
|
||||
// initialize or with this change is enough.
|
||||
this.queryRowsCollection._tableName = this.get('table_name');
|
||||
|
||||
var tableData = opts.tableData || { name: this.get('table_name') };
|
||||
this.tableModel = new TableModel(tableData, {
|
||||
configModel: opts.configModel,
|
||||
parse: true
|
||||
});
|
||||
},
|
||||
|
||||
getDefaultQuery: function () {
|
||||
return SQLUtils.getDefaultSQL(
|
||||
this.get('table_name'),
|
||||
this.tableModel.getOwnerName(),
|
||||
this._userModel.isInsideOrg()
|
||||
);
|
||||
},
|
||||
|
||||
isCustomQueryApplied: function () {
|
||||
return !SQLUtils.isSameQuery(
|
||||
this.querySchemaModel.get('query'),
|
||||
this.getDefaultQuery()
|
||||
);
|
||||
},
|
||||
|
||||
isReadOnly: function () {
|
||||
var isTableReadOnly = this.tableModel.isReadOnly(this._userModel);
|
||||
var hasCustomQuery = this.isCustomQueryApplied();
|
||||
return isTableReadOnly || hasCustomQuery;
|
||||
},
|
||||
|
||||
getTableModel: function () {
|
||||
return this.tableModel;
|
||||
},
|
||||
|
||||
fetchTable: function () {
|
||||
if (!this.tableModel.get('id')) {
|
||||
this.tableModel.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
setTableName: function (name) {
|
||||
if (!name) throw new Error('name is required');
|
||||
|
||||
this.set('table_name', name);
|
||||
this.tableModel.set('name', name);
|
||||
this.queryRowsCollection.setTableName(name);
|
||||
this.querySchemaModel.set({
|
||||
status: 'unfetched',
|
||||
query: this.getDefaultQuery()
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
103
lib/assets/javascripts/builder/data/analysis-definition-nodes-collection.js
Executable file
103
lib/assets/javascripts/builder/data/analysis-definition-nodes-collection.js
Executable file
@@ -0,0 +1,103 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var AnalysisDefinitionNodeSourceModel = require('./analysis-definition-node-source-model');
|
||||
var AnalysisDefinitionNodeModel = require('./analysis-definition-node-model');
|
||||
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 = [
|
||||
'userModel',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Collection of analysis definitions nodes.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._relatedTableData = opts.relatedTableData || [];
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.on('queryObjectsUpdated', this._onQueryObjectsUpdated);
|
||||
},
|
||||
|
||||
addRelatedTableData: function (tableData) {
|
||||
this._relatedTableData.push(tableData);
|
||||
},
|
||||
|
||||
model: function (r, opts) {
|
||||
var self = opts.collection;
|
||||
|
||||
opts = _.extend(
|
||||
{
|
||||
parse: true,
|
||||
configModel: self._configModel,
|
||||
userModel: self._userModel
|
||||
},
|
||||
opts
|
||||
);
|
||||
|
||||
if (r.type === 'source') {
|
||||
var tableData = _.find(self._relatedTableData, function (t) {
|
||||
return TableNameUtils.isSameTableName(r.options.table_name, t.name, self._configModel.get('user_name'));
|
||||
});
|
||||
if (tableData) {
|
||||
opts.tableData = tableData;
|
||||
}
|
||||
return new AnalysisDefinitionNodeSourceModel(r, opts);
|
||||
} else {
|
||||
return new AnalysisDefinitionNodeModel(r, opts);
|
||||
}
|
||||
},
|
||||
|
||||
createSourceNode: function (d) {
|
||||
d = d || {};
|
||||
if (!d.id) throw new Error('id is required');
|
||||
if (!d.tableName) throw new Error('tableName is required');
|
||||
|
||||
return this.add({
|
||||
id: d.id,
|
||||
type: 'source',
|
||||
params: {
|
||||
query: d.query || SQLUtils.getDefaultSQL(d.tableName, TableNameUtils.getUsername(d.tableName), true)
|
||||
},
|
||||
options: {
|
||||
table_name: d.tableName
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onQueryObjectsUpdated: function (model) {
|
||||
this._fetchQueryObjectsIfOnTop(model);
|
||||
},
|
||||
|
||||
_fetchQueryObjectsIfOnTop: function (model) {
|
||||
if (this._isNodeOnTop(model)) {
|
||||
model.fetchQueryObjects();
|
||||
}
|
||||
},
|
||||
|
||||
_isNodeOnTop: function (model) {
|
||||
var currentPos = this.indexOf(model);
|
||||
|
||||
return currentPos === this._getTopNodeIndex();
|
||||
},
|
||||
|
||||
_getTopNodeIndex: function () {
|
||||
var nodeOnTop = this._getNodeOnTop();
|
||||
var at = this.indexOf(nodeOnTop);
|
||||
|
||||
return at;
|
||||
},
|
||||
|
||||
_getNodeOnTop: function () {
|
||||
return this.last();
|
||||
}
|
||||
});
|
||||
88
lib/assets/javascripts/builder/data/analysis-definitions-collection.js
Executable file
88
lib/assets/javascripts/builder/data/analysis-definitions-collection.js
Executable file
@@ -0,0 +1,88 @@
|
||||
var Backbone = require('backbone');
|
||||
var nodeIds = require('builder/value-objects/analysis-node-ids');
|
||||
var AnalysisDefinitionModel = require('./analysis-definition-model');
|
||||
|
||||
/**
|
||||
* Collection of analysis definitions.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (d, opts) {
|
||||
var self = opts.collection;
|
||||
|
||||
var m = new AnalysisDefinitionModel(d, {
|
||||
parse: true,
|
||||
collection: self, // used implicitly, for model to use collection.url()
|
||||
analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection
|
||||
});
|
||||
|
||||
return m;
|
||||
},
|
||||
|
||||
initialize: function (models, options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
if (!options.vizId) throw new Error('vizId is required');
|
||||
if (!options.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
|
||||
|
||||
this._configModel = options.configModel;
|
||||
this._vizId = options.vizId;
|
||||
this._analysisDefinitionNodesCollection = options.analysisDefinitionNodesCollection;
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v3/viz/' + this._vizId + '/analyses';
|
||||
},
|
||||
|
||||
findAnalysisThatContainsNode: function (nodeDefModel) {
|
||||
return this.findWhere({node_id: nodeDefModel.id}) ||
|
||||
this.find(function (m) {
|
||||
return m.containsNode(nodeDefModel);
|
||||
});
|
||||
},
|
||||
|
||||
findAnalysisForLayer: function (layerDefModel) {
|
||||
var layerLetter = layerDefModel.get('letter');
|
||||
|
||||
return this.find(function (m) {
|
||||
var letter = nodeIds.letter(m.get('node_id'));
|
||||
return letter === layerLetter;
|
||||
});
|
||||
},
|
||||
|
||||
newAnalysisForNode: function (nodeDefModel) {
|
||||
return this.add({analysis_definition: nodeDefModel.toJSON()});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} layerDefModel - instance of an analysis-definition-node-model
|
||||
* @return {jqXHR, undefined} A promise if created a new analysis
|
||||
*/
|
||||
saveAnalysisForLayer: function (layerDefModel) {
|
||||
if (!layerDefModel) throw new Error('layerDefModel is required');
|
||||
|
||||
var nodeDefModel = layerDefModel.getAnalysisDefinitionNodeModel();
|
||||
var analysisDefModel = this.findAnalysisForLayer(layerDefModel);
|
||||
|
||||
if (layerDefModel.isOwnerOfAnalysisNode(nodeDefModel)) {
|
||||
if (analysisDefModel) {
|
||||
analysisDefModel.set('node_id', nodeDefModel.id);
|
||||
} else {
|
||||
analysisDefModel = this.add({analysis_definition: nodeDefModel.toJSON()});
|
||||
}
|
||||
} else {
|
||||
if (analysisDefModel) {
|
||||
analysisDefModel.destroy(); // layer's node is owned by another layer, so the existing analysis should be deleted
|
||||
}
|
||||
}
|
||||
|
||||
this.each(function (analysisDefModel) {
|
||||
if (analysisDefModel.containsNode(nodeDefModel)) {
|
||||
analysisDefModel.save();
|
||||
}
|
||||
});
|
||||
|
||||
return analysisDefModel;
|
||||
}
|
||||
|
||||
});
|
||||
132
lib/assets/javascripts/builder/data/asset-model.js
Executable file
132
lib/assets/javascripts/builder/data/asset-model.js
Executable file
@@ -0,0 +1,132 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
|
||||
require('backbone-model-file-upload');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
fileAttribute: 'filename',
|
||||
|
||||
defaults: {
|
||||
type: '',
|
||||
filename: '',
|
||||
interval: 0,
|
||||
progress: 0,
|
||||
state: 'idle',
|
||||
option: ''
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('progress', function (progress) {
|
||||
this.set({
|
||||
progress: progress * 100,
|
||||
state: 'uploading'
|
||||
});
|
||||
}, this);
|
||||
|
||||
this.bind('change:filename change:url', function () {
|
||||
if (this.get('state') === 'error') {
|
||||
this.set({ state: 'idle' });
|
||||
this.unset('get_error_text', { silent: true });
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.bind('error invalid', function (m, d) {
|
||||
this.set({
|
||||
state: 'error',
|
||||
error_code: (d && d.error_code) || '',
|
||||
get_error_text: {
|
||||
title: _t('componentes.data.asset-model.invalid-import'),
|
||||
what_about: (d && d.msg) || ''
|
||||
}
|
||||
}, { silent: true });
|
||||
// We need this, if not validate will run again and again and again... :(
|
||||
this.trigger('change');
|
||||
}, this);
|
||||
},
|
||||
|
||||
validate: function (attrs) {
|
||||
if (!attrs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attrs.type === 'filename') { // Number of files
|
||||
if (attrs.filename && attrs.filename.length) {
|
||||
return {
|
||||
msg: _t('componentes.data.asset-model.one-file-per-upload')
|
||||
};
|
||||
}
|
||||
// File extension
|
||||
var name = attrs.filename.name;
|
||||
var ext = name.substr(name.lastIndexOf('.') + 1);
|
||||
|
||||
if (ext) {
|
||||
ext = ext.toLowerCase();
|
||||
}
|
||||
|
||||
if (!_.contains(['jpg', 'png', 'gif', 'svg'], ext)) {
|
||||
return {
|
||||
msg: _t('componentes.data.asset-model.invalid-extension')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'url') { // Valid URL?
|
||||
if (!Utils.isURL(attrs.url)) {
|
||||
return {
|
||||
msg: _t('componentes.data.asset-model.invalid-url')
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isValid: function () {
|
||||
return (this.get('filename') || this.get('url')) && this.get('state') !== 'error';
|
||||
},
|
||||
|
||||
upload: function () {
|
||||
var self = this;
|
||||
|
||||
var options = {
|
||||
kind: this.get('kind')
|
||||
};
|
||||
|
||||
if (this.get('type') === 'file') {
|
||||
options.filename = this.get('filename');
|
||||
} else if (this.get('type') === 'url') {
|
||||
options.filename = this.get('filename');
|
||||
}
|
||||
|
||||
this.xhr = this.save(options, {
|
||||
success: function (m) {
|
||||
m.set('state', 'uploaded');
|
||||
},
|
||||
|
||||
error: function (m, msg) {
|
||||
var response;
|
||||
var message = _t('componentes.data.asset-model.connection-error');
|
||||
|
||||
if (msg && msg.status === 429) {
|
||||
response = JSON.parse(msg.responseText);
|
||||
message = response.error;
|
||||
} else if (msg && msg.status === 400) {
|
||||
response = JSON.parse(msg.responseText);
|
||||
message = response.error;
|
||||
}
|
||||
|
||||
self.set({
|
||||
state: 'error',
|
||||
get_error_text: { title: _t('componentes.data.asset-model.error'), what_about: message }
|
||||
});
|
||||
},
|
||||
|
||||
complete: function () {
|
||||
delete self.xhr;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
stopUpload: function () {
|
||||
if (this.xhr) this.xhr.abort();
|
||||
}
|
||||
});
|
||||
40
lib/assets/javascripts/builder/data/assets-collection.js
Executable file
40
lib/assets/javascripts/builder/data/assets-collection.js
Executable file
@@ -0,0 +1,40 @@
|
||||
var Backbone = require('backbone');
|
||||
var AssetModel = require('./asset-model');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
model: AssetModel,
|
||||
|
||||
url: function (method) {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('asset', method);
|
||||
return baseUrl + '/api/' + version + '/users/' + this._userModel.get('id') + '/assets';
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
parse: function (resp, xhr) {
|
||||
return resp.assets;
|
||||
},
|
||||
|
||||
selectAll: function () {
|
||||
this.each(function (mdl) {
|
||||
mdl.set('state', 'selected');
|
||||
});
|
||||
},
|
||||
|
||||
deselectAll: function (m) {
|
||||
this.each(function (mdl) {
|
||||
if (mdl !== m && mdl.get('state') === 'selected') {
|
||||
mdl.set('state', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
const $ = require('jquery');
|
||||
const _ = require('underscore');
|
||||
|
||||
/**
|
||||
* Network Interceptors
|
||||
*
|
||||
* Tool to easily intercept network responses in order
|
||||
* to override or add behaviours on top of the usual ones
|
||||
*/
|
||||
|
||||
function NetworkResponseInterceptor () {
|
||||
this._originalAjax = $.ajax;
|
||||
this._successInterceptors = [];
|
||||
this._errorInterceptors = [];
|
||||
this._urlPatterns = [];
|
||||
}
|
||||
|
||||
NetworkResponseInterceptor.prototype.start = function () {
|
||||
let onRequestStart = this._onRequestStart.bind(this);
|
||||
|
||||
this._onRequestStart = onRequestStart;
|
||||
$.ajax = onRequestStart;
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype.stop = function () {
|
||||
$.ajax = this._originalAjax;
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype.addURLPattern = function (pattern) {
|
||||
this._urlPatterns.push(pattern);
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype.addSuccessInterceptor = function (interceptorFn) {
|
||||
this._successInterceptors.push(interceptorFn);
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype.addErrorInterceptor = function (interceptorFn) {
|
||||
this._errorInterceptors.push(interceptorFn);
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype._shouldListenToRequest = function (url) {
|
||||
let patternChecks = this._urlPatterns.map(function (pattern) {
|
||||
return url.indexOf(pattern) > -1;
|
||||
});
|
||||
|
||||
return _.some(patternChecks);
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype._onRequestStart = function (url, options) {
|
||||
let requestUrl = null;
|
||||
let requestOptions = {};
|
||||
|
||||
if (typeof url === 'string') {
|
||||
requestUrl = url;
|
||||
} else if (typeof url === 'object' && url.url) {
|
||||
requestUrl = url.url;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
requestOptions = options;
|
||||
} else if (typeof url === 'object') {
|
||||
requestOptions = url;
|
||||
}
|
||||
|
||||
if (!this._shouldListenToRequest(requestUrl)) {
|
||||
return this._originalAjax.apply(this, arguments);
|
||||
}
|
||||
|
||||
if (requestOptions.error || requestOptions.success) {
|
||||
const errorCallback = requestOptions.error;
|
||||
const successCallback = requestOptions.success;
|
||||
|
||||
const interceptErrorFn = function () {
|
||||
this._applyErrorInterceptors.apply(this, arguments);
|
||||
errorCallback && errorCallback.apply(this, arguments);
|
||||
};
|
||||
|
||||
const interceptSuccessFn = function () {
|
||||
this._applySuccessInterceptors.apply(this, arguments);
|
||||
successCallback && successCallback.apply(this, arguments);
|
||||
};
|
||||
|
||||
requestOptions.error = interceptErrorFn.bind(this);
|
||||
requestOptions.success = interceptSuccessFn.bind(this);
|
||||
}
|
||||
|
||||
return this._originalAjax(requestUrl, requestOptions);
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype._applyErrorInterceptors = function (xhr, textStatus, errorThrown) {
|
||||
if (!this._errorInterceptors.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._errorInterceptors.map(function (interceptorFunction) {
|
||||
interceptorFunction(xhr, textStatus, errorThrown);
|
||||
});
|
||||
};
|
||||
|
||||
NetworkResponseInterceptor.prototype._applySuccessInterceptors = function (response, textStatus, xhr) {
|
||||
if (!this._successInterceptors.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._successInterceptors.map(function (interceptorFunction) {
|
||||
interceptorFunction(response, textStatus, xhr);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = new NetworkResponseInterceptor();
|
||||
module.exports.NetworkResponseInterceptor = NetworkResponseInterceptor;
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 403 Forbidden Network Error Interceptor
|
||||
*
|
||||
* This interceptor redirects 403 errors
|
||||
* to their corresponding routes
|
||||
*/
|
||||
|
||||
const LOGIN_ROUTE = '/login?error=session_expired';
|
||||
const SESSION_EXPIRED = 'session_expired';
|
||||
const MFA_REQUIRED = 'mfa_required';
|
||||
const MFA_ROUTE = '/multifactor_authentication';
|
||||
const MAINTENANCE_MODE = 'maintenance_mode';
|
||||
const MAINTENANCE_MODE_ROUTE = '/maintenance_mode';
|
||||
const LOCKOUT = 'lockout';
|
||||
const LOCKOUT_ROUTE = '/lockout';
|
||||
|
||||
const subdomainMatch = /https?:\/\/([^.]+)/;
|
||||
|
||||
const redirectRoutes = {
|
||||
[SESSION_EXPIRED]: LOGIN_ROUTE,
|
||||
[MFA_REQUIRED]: MFA_ROUTE,
|
||||
[MAINTENANCE_MODE]: MAINTENANCE_MODE_ROUTE,
|
||||
[LOCKOUT]: LOCKOUT_ROUTE
|
||||
};
|
||||
|
||||
module.exports = function (username) {
|
||||
return function (xhr, textStatus, errorThrown) {
|
||||
if (xhr.status !== 403) return;
|
||||
|
||||
const error = xhr.responseJSON && xhr.responseJSON.error;
|
||||
const redirectRoute = redirectRoutes[error];
|
||||
|
||||
if (!redirectRoute) {
|
||||
return;
|
||||
}
|
||||
|
||||
URLFunctions.redirectTo(
|
||||
URLFunctions.getRedirectURL(window.location.origin, redirectRoute, username)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const URLFunctions = {
|
||||
getRedirectURL: function getRedirectURL (currentURLOrigin, route, username) {
|
||||
if (!username) {
|
||||
return currentURLOrigin + route;
|
||||
}
|
||||
|
||||
const newURL = currentURLOrigin.replace(subdomainMatch, function () {
|
||||
return username;
|
||||
});
|
||||
|
||||
return `${window.location.protocol}//${newURL}${route}`;
|
||||
},
|
||||
|
||||
redirectTo: function redirectTo (url) {
|
||||
window.location = url;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.URLFunctions = URLFunctions;
|
||||
23
lib/assets/javascripts/builder/data/backbone/sync-abort.js
Executable file
23
lib/assets/javascripts/builder/data/backbone/sync-abort.js
Executable file
@@ -0,0 +1,23 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Custom sync method to only allow a single request at a time,
|
||||
* any prev ongoing request at a time of a sync call will be aborted.
|
||||
*
|
||||
* @example
|
||||
* var MyModel = Backbone.Model.extend({
|
||||
* // …
|
||||
* sync: syncAbort,
|
||||
*/
|
||||
module.exports = function (method, self, opts) {
|
||||
if (this._xhr) {
|
||||
this._xhr.abort();
|
||||
}
|
||||
|
||||
var xhr = this._xhr = Backbone.Model.prototype.sync.apply(this, arguments);
|
||||
xhr.always(function () {
|
||||
self._xhr = null;
|
||||
});
|
||||
|
||||
return xhr;
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var ImportsModel = require('./imports-model');
|
||||
var POLL_TIMER = 30000;
|
||||
|
||||
/**
|
||||
* Imports collection
|
||||
*
|
||||
* If it is fetched, it will add the import
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
},
|
||||
|
||||
model: function (attrs, opts) {
|
||||
return new ImportsModel(attrs, {
|
||||
userModel: opts.collection._userModel,
|
||||
configModel: opts.collection._configModel
|
||||
});
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('import');
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/' + version + '/imports';
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
var self = this;
|
||||
|
||||
if (r.imports.length === 0) {
|
||||
this.destroyCheck();
|
||||
} else {
|
||||
_.each(r.imports, function (id) {
|
||||
// Check if that import exists...
|
||||
var imports = self.filter(function (mdl) {
|
||||
return mdl._importModel.get('item_queue_id') === id;
|
||||
});
|
||||
|
||||
if (imports.length === 0) {
|
||||
var importsModel = new ImportsModel({
|
||||
id: id
|
||||
}, {
|
||||
userModel: self._userModel,
|
||||
configModel: self._configModel
|
||||
});
|
||||
|
||||
self.add(importsModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.models;
|
||||
},
|
||||
|
||||
canImport: function () {
|
||||
var importQuota = this._userModel.getMaxConcurrentImports();
|
||||
var total = this.size();
|
||||
var finished = 0;
|
||||
|
||||
this.each(function (m) {
|
||||
if (m.hasFailed() || m.hasCompleted()) {
|
||||
++finished;
|
||||
}
|
||||
});
|
||||
|
||||
return (total - finished) < importQuota;
|
||||
},
|
||||
|
||||
pollCheck: function (i) {
|
||||
if (this.pollTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.pollTimer = setInterval(function () {
|
||||
self.fetch();
|
||||
}, POLL_TIMER || 2000);
|
||||
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
clearInterval(this.pollTimer);
|
||||
delete this.pollTimer;
|
||||
},
|
||||
|
||||
failedItems: function () {
|
||||
return this.filter(function (item) {
|
||||
return item.hasFailed();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
var Backbone = require('backbone');
|
||||
var ImportsCollection = require('./background-importer-imports-collection');
|
||||
|
||||
var POLLINGS_TIMER = 3000;
|
||||
|
||||
/**
|
||||
* Background polling default model
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
showSuccessDetailsButton: true,
|
||||
importsPolling: false // enable imports polling
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this.importsCollection = opts.importsCollection || new ImportsCollection(null, {
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
this.startPollings();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.importsCollection.bind('change:state', function (mdl) {
|
||||
this.trigger('change', mdl, this);
|
||||
this._onImportsStateChange(mdl);
|
||||
}, this);
|
||||
|
||||
this.importsCollection.bind('remove', function (mdl) {
|
||||
this.trigger('importRemoved', mdl, this);
|
||||
}, this);
|
||||
|
||||
this.importsCollection.bind('add', function (mdl) {
|
||||
this.trigger('importAdded', mdl, this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
getTotalFailedItems: function () {
|
||||
return this.importsCollection.failedItems().length;
|
||||
},
|
||||
|
||||
removeImportItem: function (mdl) {
|
||||
if (!mdl) {
|
||||
return false;
|
||||
}
|
||||
this.importsCollection.remove(mdl);
|
||||
},
|
||||
|
||||
addImportItem: function (mdl) {
|
||||
if (!mdl) {
|
||||
return false;
|
||||
}
|
||||
this.importsCollection.add(mdl);
|
||||
},
|
||||
|
||||
canAddImport: function () {
|
||||
return this.importsCollection.canImport();
|
||||
},
|
||||
|
||||
getTotalImports: function () {
|
||||
return this.importsCollection.size();
|
||||
},
|
||||
|
||||
getTotalPollings: function () {
|
||||
return this.importsCollection.size();
|
||||
},
|
||||
|
||||
stopPollings: function () {
|
||||
if (this.get('importsPolling')) {
|
||||
this.importsCollection.destroyCheck();
|
||||
}
|
||||
},
|
||||
|
||||
startPollings: function () {
|
||||
var self = this;
|
||||
// Don't start pollings inmediately
|
||||
setTimeout(function () {
|
||||
if (self.get('importsPolling')) {
|
||||
self.importsCollection.pollCheck();
|
||||
}
|
||||
}, POLLINGS_TIMER);
|
||||
},
|
||||
|
||||
_onImportsStateChange: function () {},
|
||||
|
||||
clean: function () {
|
||||
this.importsCollection.unbind(null, null, this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
var _ = require('underscore');
|
||||
var Poller = require('./poller');
|
||||
|
||||
var ImportModelPoller = function (model) {
|
||||
var POLLING_INTERVAL = 2000; // Interval time between poll checkings
|
||||
var POLLING_INTERVAL_MULTIPLIER = 2.5; // Multiply interval by this number
|
||||
var POLLING_REQUESTS_BEFORE_INTERVAL_CHANGE = 30; // Max tries until interval change
|
||||
|
||||
var options = {
|
||||
interval: function (numberOfRequests) {
|
||||
if (numberOfRequests >= POLLING_REQUESTS_BEFORE_INTERVAL_CHANGE) {
|
||||
return POLLING_INTERVAL * POLLING_INTERVAL_MULTIPLIER;
|
||||
}
|
||||
return POLLING_INTERVAL;
|
||||
},
|
||||
stopWhen: function (model) {
|
||||
var state = model.get('state');
|
||||
return (state === 'complete' || state === 'failure');
|
||||
}
|
||||
};
|
||||
|
||||
Poller.call(this, model, options);
|
||||
};
|
||||
|
||||
ImportModelPoller.prototype = _.extend({}, Poller.prototype);
|
||||
|
||||
module.exports = ImportModelPoller;
|
||||
162
lib/assets/javascripts/builder/data/background-importer/import-model.js
Executable file
162
lib/assets/javascripts/builder/data/background-importer/import-model.js
Executable file
@@ -0,0 +1,162 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var ImportModelPoller = require('./import-model-poller');
|
||||
var SynchronizationModel = require('builder/data/synchronization-model');
|
||||
|
||||
/**
|
||||
* New import model that controls
|
||||
* the state of an import
|
||||
*
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
idAttribute: 'item_queue_id',
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this._initBinds();
|
||||
this.poller = new ImportModelPoller(this);
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
var version = this._configModel.urlVersion('import');
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
|
||||
return baseUrl + '/api/' + version + '/imports';
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:item_queue_id', this._checkQueueId, this);
|
||||
},
|
||||
|
||||
createImport: function (data) {
|
||||
var d = this._prepareData(data);
|
||||
this[d.interval === 0 ? '_createRegularImport' : '_createSyncImport'](d);
|
||||
},
|
||||
|
||||
_checkQueueId: function () {
|
||||
if (this.get('item_queue_id')) {
|
||||
this.pollCheck();
|
||||
}
|
||||
},
|
||||
|
||||
_prepareData: function (data) {
|
||||
var d = {
|
||||
create_vis: data.create_vis,
|
||||
privacy: data.privacy
|
||||
};
|
||||
|
||||
var type = data.type;
|
||||
|
||||
if (type !== 'remote') {
|
||||
_.extend(d, {
|
||||
type_guessing: data.type_guessing,
|
||||
content_guessing: data.content_guessing,
|
||||
interval: data.interval
|
||||
});
|
||||
}
|
||||
|
||||
var service = data.service_name;
|
||||
|
||||
if (type === 'url') {
|
||||
_.extend(d, {
|
||||
url: data.value
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'remote') {
|
||||
_.extend(d, {
|
||||
type: 'remote',
|
||||
interval: null,
|
||||
remote_visualization_id: data.remote_visualization_id,
|
||||
create_vis: false,
|
||||
value: data.value
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'sql') {
|
||||
_.extend(d, {
|
||||
table_name: data.table_name,
|
||||
sql: data.value
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'duplication') {
|
||||
_.extend(d, {
|
||||
table_name: data.table_name,
|
||||
table_copy: data.value
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'service') {
|
||||
// If service is Twitter, service_item_id should be
|
||||
// sent stringified
|
||||
var service_item_id = (service === 'twitter_search') ? JSON.stringify(data.service_item_id) : data.service_item_id;
|
||||
|
||||
if (data.user_defined_limits) {
|
||||
d.user_defined_limits = data.user_defined_limits;
|
||||
}
|
||||
|
||||
_.extend(d, {
|
||||
value: data.value,
|
||||
service_name: data.service_name,
|
||||
service_item_id: service_item_id
|
||||
});
|
||||
}
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
_createSyncImport: function (d) {
|
||||
var self = this;
|
||||
this._synchronizationModel = new SynchronizationModel(d, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
this._synchronizationModel.save(null, {
|
||||
success: function (m) {
|
||||
self.set('item_queue_id', m.get('data_import').item_queue_id);
|
||||
},
|
||||
error: function (mdl, r, opts) {
|
||||
self._setErrorState(r);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_createRegularImport: function (d) {
|
||||
var self = this;
|
||||
|
||||
this.save(d, {
|
||||
error: function (mdl, r, opts) {
|
||||
self._setErrorState(r);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_setErrorState: function (r) {
|
||||
var msg;
|
||||
try {
|
||||
msg = r && JSON.parse(r.responseText).errors.imports;
|
||||
} catch (err) {
|
||||
msg = _t('data.import-model.error-starting-import');
|
||||
}
|
||||
this.set({
|
||||
state: 'failure',
|
||||
get_error_text: {
|
||||
title: _t('data.import-model.error-title'),
|
||||
what_about: msg
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
pollCheck: function () {
|
||||
this.poller.start();
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
this.poller.stop();
|
||||
}
|
||||
});
|
||||
276
lib/assets/javascripts/builder/data/background-importer/imports-model.js
Executable file
276
lib/assets/javascripts/builder/data/background-importer/imports-model.js
Executable file
@@ -0,0 +1,276 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var ImportModel = require('./import-model');
|
||||
var UploadModel = require('builder/data/upload-model');
|
||||
var VisDefinitionModel = require('builder/data/vis-definition-model');
|
||||
var PermissionModel = require('builder/data/permission-model');
|
||||
|
||||
/**
|
||||
* Upload/import model
|
||||
*
|
||||
* It takes the control of the upload and import,
|
||||
* listening the change of any of these steps.
|
||||
*
|
||||
* Steps:
|
||||
* - upload
|
||||
* - import
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
step: 'upload',
|
||||
state: ''
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this._uploadModel = new UploadModel(opts.upload, {
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
if (_.isEmpty(opts)) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
this._importModel = new ImportModel(opts.import, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
this._initBinds();
|
||||
this._checkStatus();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:import', this._onImportChange, this);
|
||||
this.bind('change:upload', this._onUploadChange, this);
|
||||
this.bind('change:id', this._onIdChange, this);
|
||||
|
||||
this._importModel.bind('change', function () {
|
||||
this.trigger('change:import', this);
|
||||
this.trigger('change', this);
|
||||
}, this);
|
||||
|
||||
this._uploadModel.bind('change', function () {
|
||||
this.trigger('change:upload', this);
|
||||
this.trigger('change', this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_destroyBinds: function () {
|
||||
this._uploadModel.unbind(null, null, this);
|
||||
this._importModel.unbind(null, null, this);
|
||||
},
|
||||
|
||||
_onIdChange: function () {
|
||||
var item_queue_id = this.get('id');
|
||||
if (item_queue_id) this._importModel.set('item_queue_id', item_queue_id);
|
||||
this.set('step', 'import');
|
||||
},
|
||||
|
||||
_onUploadChange: function (m, i) {
|
||||
if (this.get('step') === 'upload') {
|
||||
var item_queue_id = this._uploadModel.get('item_queue_id');
|
||||
var state = this._uploadModel.get('state');
|
||||
|
||||
if (item_queue_id) this.set('id', item_queue_id);
|
||||
if (state) this.set('state', state);
|
||||
}
|
||||
},
|
||||
|
||||
_onImportChange: function () {
|
||||
if (this.get('step') === 'import') {
|
||||
var state = this._importModel.get('state');
|
||||
if (state) this.set('state', state);
|
||||
}
|
||||
},
|
||||
|
||||
_checkStatus: function () {
|
||||
if (!this.get('id') && !this._uploadModel.isValid()) {
|
||||
this.trigger('change:upload');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._uploadModel.get('type') === 'file') {
|
||||
this._uploadModel.upload();
|
||||
} else if (this.get('id')) {
|
||||
this.set('step', 'import');
|
||||
this._importModel.set('item_queue_id', this.get('id'));
|
||||
} else if (!this._importModel.get('item_queue_id') && this._uploadModel.get('type') !== '') {
|
||||
this.set('step', 'import');
|
||||
this._importModel.createImport(this._uploadModel.toJSON());
|
||||
}
|
||||
},
|
||||
|
||||
getImportModel: function () {
|
||||
return this._importModel;
|
||||
},
|
||||
|
||||
pause: function () {
|
||||
this.stopUpload();
|
||||
this.stopImport();
|
||||
},
|
||||
|
||||
hasFailed: function () {
|
||||
var state = this.get('state');
|
||||
var step = this.get('step');
|
||||
|
||||
return (step === 'import' && state === 'failure') || (step === 'upload' && state === 'error');
|
||||
},
|
||||
|
||||
hasCompleted: function () {
|
||||
return this.get('step') === 'import' && this._importModel && this._importModel.get('state') === 'complete';
|
||||
},
|
||||
|
||||
getWarnings: function () {
|
||||
return this.get('step') === 'import' ? this._importModel.get('warnings') : '';
|
||||
},
|
||||
|
||||
getError: function () {
|
||||
if (this.hasFailed()) {
|
||||
var step = this.get('step');
|
||||
return _.extend(
|
||||
{
|
||||
errorCode: this[step === 'upload' ? '_uploadModel' : '_importModel'].get('error_code'),
|
||||
itemQueueId: step === 'import' ? this._importModel.get('id') : '',
|
||||
originalUrl: step === 'import' ? this._importModel.get('original_url') : '',
|
||||
dataType: step === 'import' ? this._importModel.get('data_type') : '',
|
||||
httpResponseCode: step === 'import' ? this._importModel.get('http_response_code') : '',
|
||||
httpResponseCodeMessage: step === 'import' ? this._importModel.get('http_response_code_message') : ''
|
||||
},
|
||||
this[step === 'upload' ? '_uploadModel' : '_importModel'].get('get_error_text'));
|
||||
}
|
||||
|
||||
return {
|
||||
title: '',
|
||||
what_about: '',
|
||||
error_code: ''
|
||||
};
|
||||
},
|
||||
|
||||
importedVis: function () {
|
||||
if (this.get('import').derived_visualization_id) {
|
||||
return this._getMapVis();
|
||||
} else {
|
||||
return this._getDatasetVis();
|
||||
}
|
||||
},
|
||||
|
||||
getNumberOfTablesCreated: function () {
|
||||
return this._importModel.get('tables_created_count');
|
||||
},
|
||||
|
||||
_getServiceName: function () {
|
||||
return this._importModel.get('service_name');
|
||||
},
|
||||
|
||||
isTwitterImport: function () {
|
||||
return this._getServiceName() === 'twitter_search';
|
||||
},
|
||||
|
||||
isCartoImport: function () {
|
||||
return this._getDisplayName() && this._getDisplayName().match(/\.carto$/i);
|
||||
},
|
||||
|
||||
_getDisplayName: function () {
|
||||
return this._importModel.get('display_name');
|
||||
},
|
||||
|
||||
_getMapVis: function () {
|
||||
var derivedVisId = this._importModel.get('derived_visualization_id');
|
||||
|
||||
if (!derivedVisId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._createVis({
|
||||
type: 'derived',
|
||||
id: derivedVisId
|
||||
});
|
||||
},
|
||||
|
||||
_getDatasetVis: function () {
|
||||
var tableName = this._importModel.get('table_name');
|
||||
|
||||
if (!tableName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._createVis({
|
||||
type: 'table',
|
||||
table: {
|
||||
name: tableName
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_createVis: function (attrs) {
|
||||
var vis = new VisDefinitionModel(attrs, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
vis.permission = new PermissionModel({
|
||||
// TODO: check that this works in builder
|
||||
owner: this._userModel.attributes
|
||||
}, {
|
||||
configModel: this._configModel,
|
||||
userModel: this._userModel
|
||||
});
|
||||
|
||||
return vis;
|
||||
},
|
||||
|
||||
setError: function (opts) {
|
||||
var stepModel = this[this.get('step') === 'upload' ? '_uploadModel' : '_importModel'];
|
||||
|
||||
this.stopUpload();
|
||||
this.stopImport();
|
||||
|
||||
stepModel.set(opts);
|
||||
|
||||
this.set('state', 'error');
|
||||
},
|
||||
|
||||
stopUpload: function () {
|
||||
this._uploadModel.stopUpload();
|
||||
},
|
||||
|
||||
stopImport: function () {
|
||||
this._importModel.destroyCheck();
|
||||
},
|
||||
|
||||
get: function (attr) {
|
||||
if (attr === 'upload') {
|
||||
return this._uploadModel.toJSON();
|
||||
}
|
||||
|
||||
if (attr === 'import') {
|
||||
return this._importModel.toJSON();
|
||||
}
|
||||
|
||||
return Backbone.Model.prototype.get.call(this, attr);
|
||||
},
|
||||
|
||||
getRedirectUrl: function (user) {
|
||||
var vis = this.importedVis();
|
||||
if (vis) {
|
||||
return encodeURI(vis.viewUrl(user).edit());
|
||||
}
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return {
|
||||
step: this.get('step'),
|
||||
id: this.get('id'),
|
||||
state: this.get('state'),
|
||||
upload: this._uploadModel.toJSON(),
|
||||
import: this._importModel.toJSON()
|
||||
};
|
||||
}
|
||||
});
|
||||
83
lib/assets/javascripts/builder/data/background-importer/poller.js
Executable file
83
lib/assets/javascripts/builder/data/background-importer/poller.js
Executable file
@@ -0,0 +1,83 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
/*
|
||||
* Periodically fetches a model/collection. It waits for ongoing
|
||||
* fetch requests before trying to fetch again. A stop condition
|
||||
* can be specified.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* var poller = new Poller(model, {
|
||||
* interval: 1000,
|
||||
* stopWhen: function (model) {
|
||||
* return model.get('state') === 'completed';
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* poller.start();
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* poller.stop();
|
||||
*
|
||||
*/
|
||||
var Poller = function (model, opts) {
|
||||
this.model = model;
|
||||
this.numberOfRequests = 0;
|
||||
this.polling = false;
|
||||
this.interval = opts.interval;
|
||||
if (typeof this.interval !== 'function') {
|
||||
this.interval = function () { return opts.interval; };
|
||||
}
|
||||
this.stopWhen = opts.stopWhen;
|
||||
this.error = opts.error;
|
||||
this.autoStart = opts.autoStart;
|
||||
|
||||
if (this.autoStart) {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
Poller.prototype.start = function () {
|
||||
if (this.timeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scheduleFetch();
|
||||
};
|
||||
|
||||
Poller.prototype._scheduleFetch = function () {
|
||||
this.timeout = setTimeout(this._fetch.bind(this), this.interval(this.numberOfRequests));
|
||||
};
|
||||
|
||||
Poller.prototype._fetch = function () {
|
||||
var self = this;
|
||||
if (!self.polling) {
|
||||
self.polling = true;
|
||||
self.model.fetch({
|
||||
success: function () {
|
||||
self.polling = false;
|
||||
self.numberOfRequests++;
|
||||
if (self._continuePolling()) {
|
||||
self._scheduleFetch();
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
_.isFunction(self.error) && self.error(self.model);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Poller.prototype._continuePolling = function () {
|
||||
return !this.stopWhen ||
|
||||
(_.isFunction(this.stopWhen) && !this.stopWhen(this.model));
|
||||
};
|
||||
|
||||
Poller.prototype.stop = function () {
|
||||
this.polling = false;
|
||||
clearTimeout(this.timeout);
|
||||
delete this.timeout;
|
||||
};
|
||||
|
||||
module.exports = Poller;
|
||||
@@ -0,0 +1,25 @@
|
||||
var PROVIDER_LEAFLET = 'leaflet';
|
||||
var PROVIDER_GOOGLE_MAPS = 'googlemaps';
|
||||
var CATEGORY_GOOGLE_MAPS = 'GMaps';
|
||||
var LAYER_TYPE_GOOGLE_MAPS = 'GMapsBase';
|
||||
var LAYER_TYPE_TILED = 'Tiled';
|
||||
|
||||
module.exports = {
|
||||
getProvider: function (layerType) {
|
||||
if (layerType === LAYER_TYPE_GOOGLE_MAPS) {
|
||||
return PROVIDER_GOOGLE_MAPS;
|
||||
}
|
||||
return PROVIDER_LEAFLET;
|
||||
},
|
||||
|
||||
getLayerType: function (category) {
|
||||
if (this.isGoogleMapsCategory(category)) {
|
||||
return LAYER_TYPE_GOOGLE_MAPS;
|
||||
}
|
||||
return LAYER_TYPE_TILED;
|
||||
},
|
||||
|
||||
isGoogleMapsCategory: function (category) {
|
||||
return category === CATEGORY_GOOGLE_MAPS;
|
||||
}
|
||||
};
|
||||
169
lib/assets/javascripts/builder/data/camshaft-reference.js
Executable file
169
lib/assets/javascripts/builder/data/camshaft-reference.js
Executable file
@@ -0,0 +1,169 @@
|
||||
var _ = require('underscore');
|
||||
var camshaftReference = require('camshaft-reference').getVersion('latest');
|
||||
var DefaultCartography = require('./default-cartography.json');
|
||||
|
||||
var SOURCE_NAMES_MAP = {}; // string -> array, e.g. {'intersection': ['source', 'target']}
|
||||
var DEFAULT_MISSING_PARAM_VALUES = [undefined, null, '', NaN];
|
||||
|
||||
module.exports = {
|
||||
hasType: function (type) {
|
||||
return !!camshaftReference.analyses[type];
|
||||
},
|
||||
|
||||
paramsForType: function (type) {
|
||||
if (!this.hasType(type)) throw new Error('no analysis params found for type: ' + type);
|
||||
|
||||
return _.clone(camshaftReference.analyses[type].params);
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate raw form attrs
|
||||
* @param {Object} formAttrs - e.g. {type: 'buffer', source: 'a0', radius: 'meh'}
|
||||
* @return {Object, undefined} returns an object with keys as faulty input, e.g. {radius: 'invalid-value'} for above
|
||||
*/
|
||||
validate: function (formAttrs) {
|
||||
var errors = {};
|
||||
|
||||
var parsedAttrs = this.parse(formAttrs);
|
||||
var params = this.paramsForType(formAttrs.type);
|
||||
|
||||
for (var name in params) {
|
||||
var param = params[name];
|
||||
var val = parsedAttrs[name];
|
||||
|
||||
if (!param.optional || val !== undefined) {
|
||||
switch (param.type) {
|
||||
case 'node':
|
||||
if (!val) {
|
||||
errors[name] = _t('data.analysis-definition-node-model.validation.invalid-source');
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
if (isNaN(val)) {
|
||||
errors[name] = _t('data.analysis-definition-node-model.validation.invalid-value');
|
||||
}
|
||||
break;
|
||||
case 'enum':
|
||||
if (!_.contains(param.values, val)) {
|
||||
errors[name] = _t('data.analysis-definition-node-model.validation.invalid-enum', {expectedValues: JSON.stringify(param.values)});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (val === null) {
|
||||
errors[name] = _t('data.analysis-definition-node-model.validation.missing-required');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(errors)) {
|
||||
return errors;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get type-parsed attrs from the raw form attrs
|
||||
* @param {Object} formAttrs - e.g. {type: 'buffer', source: 'a0 ', radius: '123', dissolved: 'false'}
|
||||
* @return {Object} e.g. {type: 'buffer', source: 'a0', radius: 123, dissolved: false}
|
||||
*/
|
||||
parse: function (formAttrs) {
|
||||
var parsedAttrs = _.extend({}, formAttrs);
|
||||
var params = this.paramsForType(formAttrs.type);
|
||||
|
||||
for (var name in params) {
|
||||
var param = params[name];
|
||||
var val = parsedAttrs[name];
|
||||
|
||||
if (param.optional && _.contains(DEFAULT_MISSING_PARAM_VALUES, val)) {
|
||||
delete parsedAttrs[name];
|
||||
} else {
|
||||
switch (param.type) {
|
||||
case 'node':
|
||||
parsedAttrs[name] = (val || '').trim();
|
||||
break;
|
||||
case 'number':
|
||||
parsedAttrs[name] = parseFloat(val, 10);
|
||||
break;
|
||||
case 'boolean':
|
||||
parsedAttrs[name] = val === 'true' || val === true;
|
||||
break;
|
||||
default:
|
||||
if (_.contains(DEFAULT_MISSING_PARAM_VALUES, val)) {
|
||||
parsedAttrs[name] = null;
|
||||
} else if (_.isString(val)) {
|
||||
parsedAttrs[name] = val.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsedAttrs;
|
||||
},
|
||||
|
||||
getSourceNamesForAnalysisType: function (type) {
|
||||
if (!SOURCE_NAMES_MAP[type]) {
|
||||
var sourceNames = [];
|
||||
var params = this.paramsForType(type);
|
||||
|
||||
for (var name in params) {
|
||||
var param = params[name];
|
||||
if (param.type === 'node') {
|
||||
sourceNames.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
SOURCE_NAMES_MAP[type] = sourceNames;
|
||||
}
|
||||
|
||||
return SOURCE_NAMES_MAP[type];
|
||||
},
|
||||
|
||||
getDefaultCartoCSSForType: function () {
|
||||
return _.template([
|
||||
"#layer['mapnik::geometry_type'=1] {",
|
||||
' marker-width: <%= point.fill.size.fixed %>;',
|
||||
' marker-fill: <%= point.fill.color.fixed %>;',
|
||||
' marker-fill-opacity: <%= point.fill.color.opacity %>;',
|
||||
' marker-line-color: <%= point.stroke.color.fixed %>;',
|
||||
' marker-line-width: <%= point.stroke.size.fixed %>;',
|
||||
' marker-line-opacity: <%= point.stroke.color.opacity %>;',
|
||||
' marker-type: ellipse;',
|
||||
' marker-allow-overlap: true;',
|
||||
'}',
|
||||
"#layer['mapnik::geometry_type'=2] {",
|
||||
' line-color: <%= line.stroke.color.fixed %>;',
|
||||
' line-width: <%= line.stroke.size.fixed %>;',
|
||||
' line-opacity: 1;',
|
||||
'}',
|
||||
"#layer['mapnik::geometry_type'=3] {",
|
||||
' polygon-fill: <%= polygon.fill.color.fixed %>;',
|
||||
' polygon-opacity: <%= polygon.fill.color.opacity %>;',
|
||||
' ::outline {',
|
||||
' line-color: <%= polygon.stroke.color.fixed%>;',
|
||||
' line-width: <%= polygon.stroke.size.fixed %>;',
|
||||
' line-opacity: <%= polygon.stroke.color.opacity %>;',
|
||||
' }',
|
||||
'}'
|
||||
].join('\n'))(DefaultCartography.simple);
|
||||
},
|
||||
|
||||
isValidInputGeometryForType: function (simpleGeometryType, analysisType) {
|
||||
var validGeometries = this.getValidInputGeometriesForType(analysisType);
|
||||
return _.contains(validGeometries, simpleGeometryType) || _.contains(validGeometries, '*');
|
||||
},
|
||||
|
||||
getValidInputGeometriesForType: function (analysisType) {
|
||||
var params = this.paramsForType(analysisType);
|
||||
|
||||
var geometries = [];
|
||||
|
||||
for (var name in params) {
|
||||
var param = params[name];
|
||||
if (param.type === 'node') {
|
||||
geometries = _.union(geometries, param.geometry);
|
||||
}
|
||||
}
|
||||
|
||||
return geometries;
|
||||
}
|
||||
};
|
||||
5
lib/assets/javascripts/builder/data/column-model.js
Executable file
5
lib/assets/javascripts/builder/data/column-model.js
Executable file
@@ -0,0 +1,5 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
idAttribute: 'name'
|
||||
});
|
||||
67
lib/assets/javascripts/builder/data/column-row-data.js
Executable file
67
lib/assets/javascripts/builder/data/column-row-data.js
Executable file
@@ -0,0 +1,67 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CDB = require('internal-carto.js');
|
||||
|
||||
var queryTemplate = _.template('SELECT <%= column %> FROM (<%= sql %>) _table_sql GROUP BY <%= column %> ORDER BY <%= column %> ASC');
|
||||
var MAX_ROW_COUNT = 100;
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel param is required');
|
||||
if (!opts.nodeDefModel) throw new Error('nodeDefModel param is required');
|
||||
|
||||
this._query = opts.nodeDefModel.querySchemaModel.get('query');
|
||||
this._rowData = [];
|
||||
|
||||
this._SQL = new CDB.SQL({
|
||||
user: opts.configModel.get('user_name'),
|
||||
sql_api_template: opts.configModel.get('sql_api_template'),
|
||||
api_key: opts.configModel.get('api_key')
|
||||
});
|
||||
|
||||
this.on('change:column', this.fetch, this);
|
||||
},
|
||||
|
||||
_onQueryDone: function (r) {
|
||||
var resultCount = _.size(r.rows);
|
||||
|
||||
if (resultCount && resultCount < MAX_ROW_COUNT) {
|
||||
this._rowData = _.pluck(r.rows, this.get('column'));
|
||||
this.trigger('columnsFetched', this._rowData);
|
||||
}
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
if (this.get('column') && this._query) {
|
||||
this._SQL.execute(
|
||||
queryTemplate({
|
||||
sql: this._query,
|
||||
column: this.get('column')
|
||||
}),
|
||||
null,
|
||||
{
|
||||
extra_params: ['page', 'rows_per_page'],
|
||||
page: 0,
|
||||
rows_per_page: 40,
|
||||
success: this._onQueryDone.bind(this),
|
||||
error: function () {
|
||||
// TODO: what happens if fails?
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getRows: function () {
|
||||
var rowDataCount = _.size(this._rowData);
|
||||
if (rowDataCount > 0) {
|
||||
if (rowDataCount && rowDataCount < MAX_ROW_COUNT) {
|
||||
return _.reject(this._rowData, _.isNull);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
66
lib/assets/javascripts/builder/data/config-model.js
Executable file
66
lib/assets/javascripts/builder/data/config-model.js
Executable file
@@ -0,0 +1,66 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
|
||||
/**
|
||||
* Model for general frontend configuration.
|
||||
* Ported from old cdb.config, since we can't reuse the older model that's tied to v3 of cartodb.js
|
||||
*
|
||||
* Also, rather than putting it as a global object, it's intended to be instantiated at the entry point and passed as
|
||||
* a collaborator object the models that needs it, e.g.:
|
||||
* var myModel = new MyModel({ id: 123, … }, {
|
||||
* configModel: configModel
|
||||
* })
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
base_url: '/CHANGEME' // expected to be set when model is created
|
||||
},
|
||||
|
||||
urlVersion: function (modelName, method, defaultVersion) {
|
||||
method = method || '';
|
||||
var version = this.get(modelName + '_' + method + '_url_version');
|
||||
return version || defaultVersion || 'v1';
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {String} [version=v2] Version of API to use
|
||||
* @return {String} the full sql api url, including the api endpoint, e.g. http://user.carto.com/api/v2/sql
|
||||
*/
|
||||
getSqlApiUrl: function (version) {
|
||||
version = version || 'v2';
|
||||
return this._getSqlApiBaseUrl() + '/api/' + version + '/sql';
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {String} returns the base url to compose the final url, e.g. http://user.carto.com/
|
||||
*/
|
||||
_getSqlApiBaseUrl: function () {
|
||||
var url;
|
||||
if (this.get('sql_api_template')) {
|
||||
url = this.get('sql_api_template').replace('{user}', this.get('user_name'));
|
||||
} else {
|
||||
url = this.get('sql_api_protocol') + '://' +
|
||||
this.get('user_name') + '.' +
|
||||
this.get('sql_api_domain') + ':' +
|
||||
this.get('sql_api_port');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
dataServiceEnabled: function (name) {
|
||||
var dataServices = this.get('dataservices_enabled');
|
||||
if (!dataServices || !_.size(dataServices) || !name) {
|
||||
return false;
|
||||
}
|
||||
return !!dataServices[name];
|
||||
},
|
||||
|
||||
dataLibraryEnabled: function () {
|
||||
return this.get('data_library_enabled');
|
||||
},
|
||||
|
||||
isHosted: function () {
|
||||
return this.get('cartodb_com_hosted');
|
||||
}
|
||||
});
|
||||
66
lib/assets/javascripts/builder/data/custom-baselayer-model.js
Executable file
66
lib/assets/javascripts/builder/data/custom-baselayer-model.js
Executable file
@@ -0,0 +1,66 @@
|
||||
/* global Image */
|
||||
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
getAttributes: function () {
|
||||
return JSON.parse(JSON.stringify(this.attributes));
|
||||
},
|
||||
|
||||
_generateClassName: function (urlTemplate) {
|
||||
if (urlTemplate) {
|
||||
return urlTemplate.replace(/\s+/g, '').replace(/[^a-zA-Z_0-9 ]/g, '').toLowerCase();
|
||||
} else return '';
|
||||
},
|
||||
|
||||
parse: function (data) {
|
||||
var attrs = {};
|
||||
|
||||
_.extend(attrs, data.options, {
|
||||
id: data.id,
|
||||
className: this._generateClassName(data.options.urlTemplate),
|
||||
type: 'Tiled',
|
||||
order: data.order,
|
||||
parent_id: data.parent_id
|
||||
});
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var layerOptions = _.clone(_.omit(this.attributes, ['order', 'id']));
|
||||
var json = {
|
||||
kind: 'tiled',
|
||||
options: layerOptions,
|
||||
order: this.get('order'),
|
||||
id: this.id
|
||||
};
|
||||
|
||||
return json;
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return this.get('val');
|
||||
},
|
||||
|
||||
/**
|
||||
* validateTemplateURL - Validates current urlTemplate of layer.
|
||||
*
|
||||
* @param {Object} callbacks with success and error functions defined to be called depending on validation outcome.
|
||||
*/
|
||||
validateTemplateURL: function (callbacks) {
|
||||
var subdomains = ['a', 'b', 'c'];
|
||||
var image = new Image();
|
||||
image.onload = callbacks.success;
|
||||
image.onerror = callbacks.error;
|
||||
image.src = this.get('urlTemplate').replace(/\{s\}/g, function () {
|
||||
return subdomains[Math.floor(Math.random() * 3)];
|
||||
})
|
||||
.replace(/\{x\}/g, '0')
|
||||
.replace(/\{y\}/g, '0')
|
||||
.replace(/\{z\}/g, '0');
|
||||
}
|
||||
|
||||
});
|
||||
54
lib/assets/javascripts/builder/data/custom-baselayers-collection.js
Executable file
54
lib/assets/javascripts/builder/data/custom-baselayers-collection.js
Executable file
@@ -0,0 +1,54 @@
|
||||
var Backbone = require('backbone');
|
||||
var CustomBaselayerModel = require('./custom-baselayer-model');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'currentUserId'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (d, opts) {
|
||||
var self = opts.collection;
|
||||
|
||||
var m = new CustomBaselayerModel(d, {
|
||||
parse: true,
|
||||
collection: self
|
||||
});
|
||||
|
||||
return m;
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('user');
|
||||
return baseUrl + '/api/' + version + '/users/' + this._currentUserId + '/layers';
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
isCustomCategory: function () {
|
||||
var custom = this.where(function (mdl) {
|
||||
return ['NASA', 'TileJSON', 'WMS', 'Mapbox', 'Custom', undefined].some(function (category) {
|
||||
return mdl.get('category') === category;
|
||||
});
|
||||
});
|
||||
return custom;
|
||||
},
|
||||
|
||||
getSelected: function () {
|
||||
return this.findWhere({ selected: true });
|
||||
},
|
||||
|
||||
hasCustomBaseLayer: function (className) {
|
||||
if (!className) return;
|
||||
|
||||
var customBaseLayer = this.where({ className: className });
|
||||
|
||||
return customBaseLayer.length > 0;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
var BaseCollection = require('./data-observatory-base-collection');
|
||||
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
|
||||
|
||||
// Interpolation boundary tag filters geometries to only show full coverage geometries
|
||||
var FILTER_CLIPPED_GEOMS_IF_EXISTS = 'WHERE CASE WHEN EXISTS(SELECT 1 as cond FROM _data WHERE geom_tags ?\'boundary_type/tags.interpolation_boundary\' group by cond) THEN geom_tags ? \'boundary_type/tags.interpolation_boundary\' ELSE true END';
|
||||
|
||||
var ORDER_CLAUSE_GEOMS = 'ORDER BY geom_weight::numeric DESC;';
|
||||
|
||||
var COMMON_DATA_QUERY = 'bounds => (SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), numer_id => {{{ measurement }}}, number_geometries => (SELECT CDB_EstimateRowCount(\'{{{ query }}}\')::INTEGER)';
|
||||
|
||||
var BOUNDARIES_QUERY_PLAIN = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries( ' + COMMON_DATA_QUERY + ' ) denoms WHERE valid_numer IS TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
|
||||
|
||||
var BOUNDARIES_QUERY_NORMALIZE = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries(' + COMMON_DATA_QUERY + ', denom_id => {{{ denom_id }}}) denoms WHERE valid_numer IS TRUE AND valid_denom IS TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
|
||||
|
||||
var BOUNDARIES_QUERY_TIMESPAN = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries(' + COMMON_DATA_QUERY + ', timespan => {{{ timespan }}}) denoms WHERE valid_numer IS TRUE AND valid_timespan = TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
|
||||
|
||||
var BOUNDARIES_QUERY_NORMALIZE_TIMESPAN = 'WITH _data as (SELECT * FROM OBS_GetAvailableGeometries(' + COMMON_DATA_QUERY + ', denom_id => {{{ denom_id }}}, timespan => {{{ timespan }}}) denoms WHERE valid_numer IS TRUE AND valid_denom IS TRUE AND valid_timespan is TRUE) SELECT *, rank() OVER (ORDER BY score DESC) FROM _data ' + FILTER_CLIPPED_GEOMS_IF_EXISTS + ' ' + ORDER_CLAUSE_GEOMS;
|
||||
|
||||
module.exports = BaseCollection.extend({
|
||||
model: function (attrs, opts) {
|
||||
// label and val to custom list compatibility
|
||||
var o = {};
|
||||
o.val = attrs.geom_id;
|
||||
o.label = attrs.geom_name;
|
||||
o.rank = attrs.rank;
|
||||
|
||||
return new BaseModel(o);
|
||||
},
|
||||
|
||||
getLabels: function () {
|
||||
return this.pluck('label');
|
||||
},
|
||||
|
||||
getValues: function () {
|
||||
return this.pluck('val');
|
||||
},
|
||||
|
||||
getPreSelectedBoundaryValue: function () {
|
||||
var boundary = this.models.filter(
|
||||
function (boundary) { return boundary.attributes.rank === 1; }
|
||||
).shift();
|
||||
|
||||
return boundary && boundary.attributes && boundary.attributes.val;
|
||||
},
|
||||
|
||||
buildQuery: function (options) {
|
||||
if (options.denom_id != null && options.timespan != null) {
|
||||
return BOUNDARIES_QUERY_NORMALIZE_TIMESPAN;
|
||||
} else {
|
||||
if (options.denom_id != null) {
|
||||
return BOUNDARIES_QUERY_NORMALIZE;
|
||||
} else if (options.timespan != null) {
|
||||
return BOUNDARIES_QUERY_TIMESPAN;
|
||||
} else {
|
||||
return BOUNDARIES_QUERY_PLAIN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var cdb = require('internal-carto.js');
|
||||
var errorParse = require('builder/helpers/error-parser');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'nodeDefModel'
|
||||
];
|
||||
|
||||
var OBS_META_QUERY = 'SELECT OBS_GetMeta((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), {{{ metadata }}})';
|
||||
|
||||
var ColumnName = function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._querySchemaModel = this._nodeDefModel.querySchemaModel;
|
||||
this.isFetching = false;
|
||||
|
||||
var configModel = options.configModel;
|
||||
this.SQL = new cdb.SQL({
|
||||
user: configModel.get('user_name'),
|
||||
sql_api_template: configModel.get('sql_api_template'),
|
||||
api_key: configModel.get('api_key')
|
||||
});
|
||||
};
|
||||
|
||||
ColumnName.prototype = {
|
||||
buildQuery: function () {
|
||||
return OBS_META_QUERY;
|
||||
},
|
||||
|
||||
buildQueryOptions: function (options) {
|
||||
return {
|
||||
metadata: "'[" + JSON.stringify(options) + "]'",
|
||||
query: this._querySchemaModel.get('query')
|
||||
};
|
||||
},
|
||||
|
||||
fetch: function (options) {
|
||||
if (!options.numer_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._success = options && options.success;
|
||||
this._error = options && options.error;
|
||||
|
||||
var deferred = $.Deferred();
|
||||
|
||||
var queryOptions = this.buildQueryOptions(_.omit(options, 'sucess', 'error'));
|
||||
var sqlQuery = this.buildQuery();
|
||||
|
||||
if (!this.isFetching) {
|
||||
this.isFetching = true;
|
||||
this.SQL.execute(sqlQuery, queryOptions, {
|
||||
success: function (data) {
|
||||
this.isFetching = false;
|
||||
this._success && this._success(data);
|
||||
deferred.resolve();
|
||||
}.bind(this),
|
||||
error: function (err) {
|
||||
this.isFetching = false;
|
||||
this._error && this._error(errorParse(err));
|
||||
deferred.reject();
|
||||
}.bind(this)
|
||||
});
|
||||
} else {
|
||||
deferred.reject();
|
||||
}
|
||||
|
||||
return deferred.promise();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ColumnName;
|
||||
@@ -0,0 +1,105 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var cdb = require('internal-carto.js');
|
||||
var errorParse = require('builder/helpers/error-parser');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var CustomListCollection = require('builder/components/custom-list/custom-list-collection');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'nodeDefModel'
|
||||
];
|
||||
|
||||
module.exports = CustomListCollection.extend({
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._querySchemaModel = this._nodeDefModel.querySchemaModel;
|
||||
|
||||
var configModel = options.configModel;
|
||||
this.SQL = new cdb.SQL({
|
||||
user: configModel.get('user_name'),
|
||||
sql_api_template: configModel.get('sql_api_template'),
|
||||
api_key: configModel.get('api_key')
|
||||
});
|
||||
|
||||
this.stateModel = new Backbone.Model({
|
||||
state: 'unfetched'
|
||||
});
|
||||
|
||||
CustomListCollection.prototype.initialize.call(this, models, options);
|
||||
},
|
||||
|
||||
_onSelectedChange: function (changedModel, isSelected) {
|
||||
if (this.type === 'multiple') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
this.each(function (m) {
|
||||
if (m.cid !== changedModel.cid) {
|
||||
m.set({ selected: false }, { silent: true });
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
|
||||
buildQuery: function () {
|
||||
throw new Error('subclasses of DataObservatoryBaseCollection must implement buildQuery');
|
||||
},
|
||||
|
||||
fetch: function (options) {
|
||||
this._success = options && options.success;
|
||||
this._error = options && options.error;
|
||||
|
||||
var defaults = {
|
||||
filters: null,
|
||||
measurement: null,
|
||||
normalize: null,
|
||||
timespan: null,
|
||||
query: this._querySchemaModel.get('query')
|
||||
};
|
||||
|
||||
this.deferred = $.Deferred();
|
||||
var queryOptions = _.defaults(options, defaults);
|
||||
var sqlQuery = this.buildQuery(queryOptions);
|
||||
|
||||
this.stateModel.set('state', 'fetching');
|
||||
this.SQL.execute(sqlQuery, queryOptions, {
|
||||
success: function (data) {
|
||||
this._onFetchSuccess(data);
|
||||
this.stateModel.set('state', 'fetched');
|
||||
this.deferred.resolve();
|
||||
this._success && this._success();
|
||||
}.bind(this),
|
||||
error: function (err) {
|
||||
this.stateModel.set('state', 'error');
|
||||
this.deferred.reject();
|
||||
this._error && this._error(errorParse(err));
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
return this.deferred.promise();
|
||||
},
|
||||
|
||||
_onFetchSuccess: function (data) {
|
||||
var models = data.rows;
|
||||
this.reset(models);
|
||||
},
|
||||
|
||||
isFetching: function () {
|
||||
return this.getState() === 'fetching';
|
||||
},
|
||||
|
||||
getState: function () {
|
||||
return this.stateModel.get('state');
|
||||
},
|
||||
|
||||
getItem: function (value) {
|
||||
return this.findWhere({ val: value });
|
||||
},
|
||||
|
||||
isAsync: function () {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
var _ = require('underscore');
|
||||
var BaseCollection = require('./data-observatory-base-collection');
|
||||
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
|
||||
|
||||
var FILTERS_QUERY_WITH_REGION = "SELECT count(*) num_measurements, tag.key subsection_id, tag.value subsection_name FROM OBS_GetAvailableNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), {{{ region }}}) numers, Jsonb_Each(numers.numer_tags) tag WHERE tag.key like 'subsection%' GROUP BY tag.key, tag.value";
|
||||
|
||||
module.exports = BaseCollection.extend({
|
||||
initialize: function (models, options) {
|
||||
this.type = 'multiple';
|
||||
|
||||
BaseCollection.prototype.initialize.call(this, models, options);
|
||||
},
|
||||
|
||||
model: function (attrs, opts) {
|
||||
// label and val to custom list compatibility
|
||||
var o = {};
|
||||
o.val = attrs.subsection_id;
|
||||
o.label = attrs.subsection_name;
|
||||
o.items = attrs.num_measurements;
|
||||
|
||||
return new BaseModel(o);
|
||||
},
|
||||
|
||||
setSelected: function (value) {
|
||||
var silentTrue = { silent: true };
|
||||
|
||||
if (_.isArray(value)) {
|
||||
this.each(function (mdl) {
|
||||
if (_.contains(value, mdl.getValue())) {
|
||||
mdl.set({
|
||||
selected: true
|
||||
}, silentTrue);
|
||||
} else {
|
||||
mdl.set({
|
||||
selected: false
|
||||
}, silentTrue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.each(function (mdl) {
|
||||
if (mdl.getValue() === value) {
|
||||
mdl.set({
|
||||
selected: true
|
||||
}, silentTrue);
|
||||
} else {
|
||||
mdl.set({
|
||||
selected: false
|
||||
}, silentTrue);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getSelected: function () {
|
||||
return this.filter(function (mdl) {
|
||||
return mdl.get('selected') === true;
|
||||
});
|
||||
},
|
||||
|
||||
buildQuery: function (options) {
|
||||
return FILTERS_QUERY_WITH_REGION;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
var _ = require('underscore');
|
||||
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
|
||||
var $ = require('jquery');
|
||||
var cdb = require('internal-carto.js');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'nodeDefModel'
|
||||
];
|
||||
|
||||
var MEASUREMENT_ATTRIBUTES = ['aggregate', 'type', 'label', 'val', 'description', 'filter', 'license'];
|
||||
|
||||
var MEASUREMENT_BY_ID = 'SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ area }}}, ids => {{{ measurement }}})';
|
||||
|
||||
module.exports = BaseModel.extend({
|
||||
initialize: function (attrs, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._querySchemaModel = this._nodeDefModel.querySchemaModel;
|
||||
|
||||
var configModel = options.configModel;
|
||||
this.SQL = new cdb.SQL({
|
||||
user: configModel.get('user_name'),
|
||||
sql_api_template: configModel.get('sql_api_template'),
|
||||
api_key: configModel.get('api_key')
|
||||
});
|
||||
|
||||
BaseModel.prototype.initialize.call(this, attrs, options);
|
||||
},
|
||||
|
||||
fetch: function (options) {
|
||||
var defaults = {
|
||||
area: null,
|
||||
measurement: null,
|
||||
query: this._querySchemaModel.get('query')
|
||||
};
|
||||
|
||||
this.deferred = $.Deferred();
|
||||
var queryOptions = _.defaults(options, defaults);
|
||||
|
||||
this.set('state', 'fetching');
|
||||
this.SQL.execute(MEASUREMENT_BY_ID, queryOptions, {
|
||||
success: function (data) {
|
||||
this._onFetchSuccess(data);
|
||||
this.set('state', 'fetched');
|
||||
this.deferred.resolve();
|
||||
}.bind(this),
|
||||
error: function () {
|
||||
this.set('state', 'error');
|
||||
this.deferred.reject();
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
return this.deferred.promise();
|
||||
},
|
||||
|
||||
isFetching: function () {
|
||||
return this.getState() === 'fetching';
|
||||
},
|
||||
|
||||
getState: function () {
|
||||
return this.get('state');
|
||||
},
|
||||
|
||||
_onFetchSuccess: function (data) {
|
||||
var model = _.first(data.rows);
|
||||
|
||||
if (!model) {
|
||||
this._clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var o = {};
|
||||
// label and val to custom list compatibility
|
||||
o.val = model.numer_id;
|
||||
o.label = model.numer_name;
|
||||
o.description = model.numer_description;
|
||||
o.type = model.numer_type;
|
||||
o.aggregate = model.numer_aggregate;
|
||||
// a measurement can belong to more than one category (filter)
|
||||
o.filter = [];
|
||||
var tags = model.numer_tags;
|
||||
if (!_.isObject(tags)) {
|
||||
tags = JSON.parse(tags);
|
||||
}
|
||||
|
||||
for (var key in tags) {
|
||||
if (/^subsection/.test(key)) {
|
||||
o.filter.push({
|
||||
id: key,
|
||||
name: tags[key]
|
||||
});
|
||||
}
|
||||
|
||||
if (/^license/.test(key)) {
|
||||
o.license = tags[key];
|
||||
}
|
||||
}
|
||||
|
||||
this.set(o);
|
||||
},
|
||||
|
||||
_clear: function (options) {
|
||||
var attrs = MEASUREMENT_ATTRIBUTES.reduce(function (memo, attr) {
|
||||
memo[attr] = null;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
this.set(attrs, {silent: true});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
var _ = require('underscore');
|
||||
var BaseCollection = require('./data-observatory-base-collection');
|
||||
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
|
||||
|
||||
var MEASUREMENTS_QUERY_WITH_REGION = 'SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}}) ORDER BY numer_name ASC';
|
||||
|
||||
var MEASUREMENTS_QUERY_WITH_FILTERS = 'SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}}, subsection_tags => {{{ filters }}})';
|
||||
|
||||
var MEASUREMENTS_QUERY_SEARCH = "SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}}, name => '{{{ search }}}')";
|
||||
|
||||
var MEASUREMENTS_QUERY_SEARCH_AND_FILTERS = "SELECT numer_id, numer_name, numer_description, numer_type, numer_aggregate, numer_tags FROM _OBS_GetNumerators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), section_tags => {{{ region }}} , name => '{{{ search }}}', subsection_tags => {{{ filters }}})";
|
||||
|
||||
module.exports = BaseCollection.extend({
|
||||
model: function (attrs, opts) {
|
||||
return new BaseModel(attrs, opts);
|
||||
},
|
||||
|
||||
_onFetchSuccess: function (data) {
|
||||
var models = data.rows;
|
||||
|
||||
models = _.map(models, function (model) {
|
||||
var o = {};
|
||||
// label and val to custom list compatibility
|
||||
o.val = model.numer_id;
|
||||
o.label = model.numer_name;
|
||||
o.description = model.numer_description;
|
||||
o.type = model.numer_type;
|
||||
o.aggregate = model.numer_aggregate;
|
||||
// a measurement can belong to more than one category (filter)
|
||||
o.filter = [];
|
||||
var tags = model.numer_tags;
|
||||
if (!_.isObject(tags)) {
|
||||
tags = JSON.parse(tags);
|
||||
}
|
||||
|
||||
for (var key in tags) {
|
||||
if (/^subsection/.test(key)) {
|
||||
o.filter.push({
|
||||
id: key,
|
||||
name: tags[key]
|
||||
});
|
||||
}
|
||||
|
||||
if (/^license/.test(key)) {
|
||||
o.license = tags[key];
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}).filter(function (model) {
|
||||
return model.filter.length > 0;
|
||||
});
|
||||
|
||||
this.reset(models);
|
||||
},
|
||||
|
||||
buildQuery: function (options) {
|
||||
if (options && options.search && options.filters) {
|
||||
return MEASUREMENTS_QUERY_SEARCH_AND_FILTERS;
|
||||
} else if (options && options.filters) {
|
||||
return MEASUREMENTS_QUERY_WITH_FILTERS;
|
||||
} else if (options && options.search) {
|
||||
return MEASUREMENTS_QUERY_SEARCH;
|
||||
} else {
|
||||
return MEASUREMENTS_QUERY_WITH_REGION;
|
||||
}
|
||||
},
|
||||
|
||||
search: function () {
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
var BaseCollection = require('./data-observatory-base-collection');
|
||||
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
|
||||
|
||||
var NORMALIZE_QUERY_WITH_MEASUREMENT = 'SELECT * FROM OBS_GetAvailableDenominators((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), NULL, {{{ measurement }}}) denoms WHERE valid_numer IS TRUE';
|
||||
|
||||
module.exports = BaseCollection.extend({
|
||||
model: function (attrs, opts) {
|
||||
// label and val to custom list compatibility
|
||||
var o = {};
|
||||
o.val = attrs.denom_id;
|
||||
o.label = attrs.denom_name;
|
||||
|
||||
return new BaseModel(o);
|
||||
},
|
||||
|
||||
buildQuery: function (options) {
|
||||
return NORMALIZE_QUERY_WITH_MEASUREMENT;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
var BaseCollection = require('./data-observatory-base-collection');
|
||||
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
|
||||
|
||||
var REGIONS_QUERY = "SELECT count(*) num_measurements, tag.key region_id, tag.value region_name FROM (SELECT * FROM OBS_GetAvailableNumerators() WHERE jsonb_pretty(numer_tags) LIKE '%subsection/%') numers, Jsonb_Each(numers.numer_tags) tag WHERE tag.key like 'section%' GROUP BY tag.key, tag.value ORDER BY region_name";
|
||||
|
||||
module.exports = BaseCollection.extend({
|
||||
buildQuery: function () {
|
||||
return REGIONS_QUERY;
|
||||
},
|
||||
|
||||
model: function (attrs, opts) {
|
||||
// label and val to custom list compatibility
|
||||
var o = {};
|
||||
o.val = attrs.region_id;
|
||||
o.label = attrs.region_name.replace(/["]+/g, '');
|
||||
|
||||
o.renderOptions = {
|
||||
measurements: attrs.num_measurements
|
||||
};
|
||||
|
||||
return new BaseModel(o);
|
||||
},
|
||||
|
||||
fetch: function (options) {
|
||||
this.stateModel.set('state', 'fetching');
|
||||
// check if the geometry type is available, if not wait until it's finished
|
||||
if (this._nodeDefModel.queryGeometryModel.isFetched()) {
|
||||
BaseCollection.prototype.fetch.call(this, options);
|
||||
} else {
|
||||
this._nodeDefModel.queryGeometryModel.once('change:status', function () {
|
||||
BaseCollection.prototype.fetch.call(this, options);
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
var BaseCollection = require('./data-observatory-base-collection');
|
||||
var BaseModel = require('builder/components/custom-list/custom-list-item-model');
|
||||
|
||||
var TIMESPAN_QUERY_WITH_NORMALIZE = 'SELECT * FROM OBS_GetAvailableTimespans((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), NULL, {{{ measurement }}}, {{{ denom_id }}}) denoms WHERE valid_numer IS TRUE AND (valid_denom IS TRUE OR {{{ denom_id }}} IS NULL) ORDER BY timespan_name DESC';
|
||||
|
||||
var TIMESPAN_QUERY_WITHOUT_NORMALIZE = 'SELECT * FROM OBS_GetAvailableTimespans((SELECT ST_SetSRID(ST_Extent(the_geom), 4326) FROM ({{{ query }}}) q), NULL, {{{ measurement }}}, NULL) denoms WHERE valid_numer IS TRUE ORDER BY timespan_name DESC';
|
||||
|
||||
module.exports = BaseCollection.extend({
|
||||
model: function (attrs, opts) {
|
||||
// label and val to custom list compatibility
|
||||
var o = {};
|
||||
o.val = attrs.timespan_id;
|
||||
o.label = attrs.timespan_name;
|
||||
|
||||
return new BaseModel(o);
|
||||
},
|
||||
|
||||
buildQuery: function (options) {
|
||||
if (options.denom_id != null) {
|
||||
return TIMESPAN_QUERY_WITH_NORMALIZE;
|
||||
} else {
|
||||
return TIMESPAN_QUERY_WITHOUT_NORMALIZE;
|
||||
}
|
||||
},
|
||||
|
||||
selectFirstAsDefault: function () {
|
||||
var first = this.first();
|
||||
if (first) {
|
||||
this.setSelected(first.getValue());
|
||||
}
|
||||
}
|
||||
});
|
||||
56
lib/assets/javascripts/builder/data/default-cartography.json
Executable file
56
lib/assets/javascripts/builder/data/default-cartography.json
Executable file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"simple": {
|
||||
|
||||
"point": {
|
||||
"fill": {
|
||||
"size": {
|
||||
"fixed": 7
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#EE4D5A",
|
||||
"opacity": 0.9
|
||||
}
|
||||
},
|
||||
"stroke": {
|
||||
"size": {
|
||||
"fixed": 1
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#FFFFFF",
|
||||
"opacity": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"line": {
|
||||
"fill": {},
|
||||
"stroke": {
|
||||
"size": {
|
||||
"fixed": 1.5
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#4CC8A3",
|
||||
"opacity": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"polygon": {
|
||||
"fill": {
|
||||
"color": {
|
||||
"fixed": "#826DBA",
|
||||
"opacity": 0.9
|
||||
}
|
||||
},
|
||||
"stroke": {
|
||||
"size": {
|
||||
"fixed": 1
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#FFFFFF",
|
||||
"opacity": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
lib/assets/javascripts/builder/data/default-form-styles.json
Executable file
48
lib/assets/javascripts/builder/data/default-form-styles.json
Executable file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"fill": {
|
||||
"size": {
|
||||
"fixed": 7
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#EE4D5A",
|
||||
"opacity": 0.9
|
||||
}
|
||||
},
|
||||
"stroke": {
|
||||
"size": {
|
||||
"fixed": 0
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#FFFFFF",
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"blending": "none",
|
||||
"aggregation": {},
|
||||
"labels": {
|
||||
"enabled": false,
|
||||
"attribute": null,
|
||||
"font": "Source Han Sans CN Regular",
|
||||
"fill": {
|
||||
"size": {
|
||||
"fixed": 14
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#000000",
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"halo": {
|
||||
"size": {
|
||||
"fixed": 2
|
||||
},
|
||||
"color": {
|
||||
"fixed": "#FFFFFF",
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"offset": -10,
|
||||
"overlap": true,
|
||||
"placement": "point"
|
||||
}
|
||||
}
|
||||
37
lib/assets/javascripts/builder/data/editor-background-polling-model.js
Executable file
37
lib/assets/javascripts/builder/data/editor-background-polling-model.js
Executable file
@@ -0,0 +1,37 @@
|
||||
var BackgroundPollingModel = require('./background-importer/background-polling-model');
|
||||
var TableModel = require('./table-model');
|
||||
|
||||
/**
|
||||
* Background polling model for the editor context.
|
||||
*/
|
||||
|
||||
module.exports = BackgroundPollingModel.extend({
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
BackgroundPollingModel.prototype.initialize.apply(this, arguments);
|
||||
|
||||
if (!opts.userActions) { throw new Error('userActions is required'); }
|
||||
|
||||
this._userActions = opts.userActions;
|
||||
},
|
||||
|
||||
_onImportsStateChange: function (importsModel) {
|
||||
if (importsModel.hasCompleted()) {
|
||||
var tableName = importsModel.importedVis().get('table').name;
|
||||
var tableModel = new TableModel({
|
||||
name: tableName
|
||||
}, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
// Provide table geometry in order to set properly styles when
|
||||
// layer is added to the current map
|
||||
var onTableFetched = function () {
|
||||
this._userActions.createLayerFromTable(tableModel);
|
||||
this.trigger('importCompleted', importsModel, this);
|
||||
}.bind(this);
|
||||
|
||||
tableModel.fetch().always(onTableFetched);
|
||||
}
|
||||
}
|
||||
});
|
||||
17
lib/assets/javascripts/builder/data/editor-model.js
Executable file
17
lib/assets/javascripts/builder/data/editor-model.js
Executable file
@@ -0,0 +1,17 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Model for general editor configuration.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
edition: false,
|
||||
disabled: false
|
||||
},
|
||||
isEditing: function () {
|
||||
return !!this.get('edition');
|
||||
},
|
||||
isDisabled: function () {
|
||||
return !!this.get('disabled');
|
||||
}
|
||||
});
|
||||
66
lib/assets/javascripts/builder/data/export-map-definition-model.js
Executable file
66
lib/assets/javascripts/builder/data/export-map-definition-model.js
Executable file
@@ -0,0 +1,66 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Model that represents a visualization export (v3)
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!attrs.visualization_id) throw new Error('visualization_id is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
|
||||
return baseUrl + '/api/v3/visualization_exports';
|
||||
},
|
||||
|
||||
requestExport: function () {
|
||||
this.save(null, {
|
||||
success: this._requestExportSuccessHandler.bind(this)
|
||||
});
|
||||
|
||||
// TODO add metrics for private and public exports
|
||||
},
|
||||
|
||||
cancelExport: function () {
|
||||
this._interrupt();
|
||||
},
|
||||
|
||||
_requestExportSuccessHandler: function () {
|
||||
this._pollPID = setInterval(function () {
|
||||
this.fetch({
|
||||
success: this._checkState.bind(this),
|
||||
error: this._errorHandler.bind(this)
|
||||
});
|
||||
}.bind(this), 2000);
|
||||
},
|
||||
|
||||
_checkState: function () {
|
||||
if (this.get('state') === 'complete') {
|
||||
this._finishExport();
|
||||
} else if (this.get('state') === 'failure') {
|
||||
this._errorHandler();
|
||||
}
|
||||
},
|
||||
|
||||
_finishExport: function () {
|
||||
clearInterval(this._pollPID);
|
||||
},
|
||||
|
||||
_errorHandler: function () {
|
||||
this._interrupt();
|
||||
|
||||
throw new Error('There is a problem with your export. Please try again.');
|
||||
},
|
||||
|
||||
_interrupt: function () {
|
||||
clearInterval(this._pollPID);
|
||||
|
||||
this.set('state', 'failure');
|
||||
}
|
||||
});
|
||||
232
lib/assets/javascripts/builder/data/feature-definition-model.js
Executable file
232
lib/assets/javascripts/builder/data/feature-definition-model.js
Executable file
@@ -0,0 +1,232 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var QueryRowModel = require('./query-row-model');
|
||||
|
||||
var TYPES_POINT = 'point';
|
||||
var TYPES_LINE = 'line';
|
||||
var TYPES_POLYGON = 'polygon';
|
||||
|
||||
var GEOJSON_TO_FEATURE_TYPE = {
|
||||
Point: TYPES_POINT,
|
||||
MultiPoint: TYPES_POINT,
|
||||
LineString: TYPES_LINE,
|
||||
MultiLineString: TYPES_LINE,
|
||||
Polygon: TYPES_POLYGON,
|
||||
MultiPolygon: TYPES_POLYGON
|
||||
};
|
||||
|
||||
var convertGeoJSONTypeToFeatureType = function (the_geom) {
|
||||
var geoJSON = null;
|
||||
|
||||
try {
|
||||
geoJSON = JSON.parse(the_geom);
|
||||
} catch (err) {
|
||||
// if the geom is not a valid json value
|
||||
}
|
||||
|
||||
var geometryType = geoJSON && geoJSON.type;
|
||||
var featureType = GEOJSON_TO_FEATURE_TYPE[geometryType];
|
||||
if (!featureType) {
|
||||
throw new Error("unsupported geometry type: '" + geometryType + "'");
|
||||
}
|
||||
|
||||
return featureType;
|
||||
};
|
||||
|
||||
var BLACKLISTED_COLUMNS = ['cartodb_id', 'created_at', 'the_geom_webmercator', 'updated_at'];
|
||||
|
||||
var FeatureDefinitionModel = Backbone.Model.extend({
|
||||
|
||||
initialize: function (attrs, options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
if (!options.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
|
||||
if (!options.userModel) throw new Error('userModel is required');
|
||||
|
||||
this._configModel = options.configModel;
|
||||
this._layerDefinitionModel = options.layerDefinitionModel;
|
||||
this._userModel = options.userModel;
|
||||
this._featureType = options.featureType;
|
||||
|
||||
this._firstNode = this._getAnalysisDefinitionNodeModel();
|
||||
this._changesHistory = [];
|
||||
|
||||
if (this._firstNode.isSourceType()) {
|
||||
this._tableNodeModel = this._firstNode.getTableModel();
|
||||
}
|
||||
|
||||
this._onChange = _.debounce(this._onChange.bind(this), 500);
|
||||
|
||||
if (this.isNew()) {
|
||||
// Bind change event in order to "store" all changed columns after creation
|
||||
this._bindChangeEvent();
|
||||
}
|
||||
},
|
||||
|
||||
isNew: function () {
|
||||
return !this.has('cartodb_id');
|
||||
},
|
||||
|
||||
isPoint: function () {
|
||||
return this._isType(TYPES_POINT);
|
||||
},
|
||||
|
||||
isPolygon: function () {
|
||||
return this._isType(TYPES_POLYGON);
|
||||
},
|
||||
|
||||
isLine: function () {
|
||||
return this._isType(TYPES_LINE);
|
||||
},
|
||||
|
||||
_isType: function (type) {
|
||||
return this._getType() === type;
|
||||
},
|
||||
|
||||
isEditable: function () {
|
||||
var analysisDefinitionModel = this._getAnalysisDefinitionNodeModel();
|
||||
return !analysisDefinitionModel.isReadOnly();
|
||||
},
|
||||
|
||||
_getPermissionModel: function () {
|
||||
return this._tableNodeModel && this._tableNodeModel.getPermissionModel();
|
||||
},
|
||||
|
||||
isReadOnly: function () {
|
||||
return this._tableNodeModel && this._tableNodeModel.isReadOnly(this._userModel);
|
||||
},
|
||||
|
||||
hasAnalyses: function () {
|
||||
return this._layerDefinitionModel.hasAnalyses();
|
||||
},
|
||||
|
||||
isCustomQueryApplied: function () {
|
||||
var analysisDefinitionModel = this._getAnalysisDefinitionNodeModel();
|
||||
return analysisDefinitionModel.isCustomQueryApplied();
|
||||
},
|
||||
|
||||
_getAnalysisDefinitionNodeModel: function () {
|
||||
return this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
|
||||
},
|
||||
|
||||
isEqual: function (featureDefinition) {
|
||||
return this.get('cartodb_id') === featureDefinition.get('cartodb_id') &&
|
||||
this.getLayerId() === featureDefinition.getLayerId();
|
||||
},
|
||||
|
||||
getLayerId: function () {
|
||||
return this._layerDefinitionModel.id;
|
||||
},
|
||||
|
||||
getLayerDefinition: function () {
|
||||
return this._layerDefinitionModel;
|
||||
},
|
||||
|
||||
_cleanChangesHistory: function () {
|
||||
this._changesHistory = [];
|
||||
},
|
||||
|
||||
_bindChangeEvent: function () {
|
||||
this.on('change', this._onChange, this);
|
||||
},
|
||||
|
||||
_unbindChangeEvent: function () {
|
||||
this.off('change', this._onChange, this);
|
||||
},
|
||||
|
||||
_onChange: function () {
|
||||
_.each(this.changed, function (value, key) {
|
||||
if (!_.contains(this._changesHistory, key)) {
|
||||
this._changesHistory.push(key);
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
hasBeenChangedAfterLastSaved: function (key) {
|
||||
return _.contains(this._changesHistory, key);
|
||||
},
|
||||
|
||||
getFeatureType: function () {
|
||||
if (this.isPoint()) {
|
||||
return 'point';
|
||||
}
|
||||
if (this.isLine()) {
|
||||
return 'line';
|
||||
}
|
||||
if (this.isPolygon()) {
|
||||
return 'polygon';
|
||||
}
|
||||
},
|
||||
|
||||
fetch: function (options) {
|
||||
options = options || {};
|
||||
this._unbindChangeEvent();
|
||||
|
||||
this._getQueryRowModel().fetch({
|
||||
success: function (data) {
|
||||
this.set(data.toJSON());
|
||||
// Bind change event in order to "store" all changed columns after starting edition
|
||||
this._bindChangeEvent();
|
||||
options.success && options.success();
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
save: function (options) {
|
||||
options = options || {};
|
||||
var columns = this._layerDefinitionModel.getColumnNamesFromSchema();
|
||||
var attrs = _.pick(this.toJSON(), _.difference(columns, BLACKLISTED_COLUMNS));
|
||||
|
||||
this._getQueryRowModel().save(attrs, {
|
||||
success: function (queryRowModel) {
|
||||
if (this.isNew()) {
|
||||
this.set('cartodb_id', queryRowModel.get('cartodb_id'));
|
||||
}
|
||||
this.trigger('save', this, options);
|
||||
options.success && options.success();
|
||||
this._cleanChangesHistory();
|
||||
}.bind(this),
|
||||
error: function () {
|
||||
options.error && options.error();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function (options) {
|
||||
options = options || {};
|
||||
this._getQueryRowModel().destroy({
|
||||
success: function () {
|
||||
this.trigger('remove', this, options);
|
||||
options.success && options.success();
|
||||
}.bind(this),
|
||||
error: function () {
|
||||
options.error && options.error();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_getQueryRowModel: function () {
|
||||
var attrs = {};
|
||||
|
||||
if (this.has('cartodb_id')) {
|
||||
attrs.cartodb_id = this.get('cartodb_id');
|
||||
}
|
||||
|
||||
this._queryRowModel = this._queryRowModel || new QueryRowModel(attrs, {
|
||||
tableName: this._layerDefinitionModel.getTableName(),
|
||||
configModel: this._configModel,
|
||||
permissionModel: this._getPermissionModel(),
|
||||
userModel: this._userModel
|
||||
});
|
||||
|
||||
return this._queryRowModel;
|
||||
},
|
||||
|
||||
_getType: function () {
|
||||
var the_geom = this.get('the_geom');
|
||||
var type = the_geom && convertGeoJSONTypeToFeatureType(the_geom) || this._featureType;
|
||||
|
||||
return type;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = FeatureDefinitionModel;
|
||||
16
lib/assets/javascripts/builder/data/get-simple-geometry-type.js
Executable file
16
lib/assets/javascripts/builder/data/get-simple-geometry-type.js
Executable file
@@ -0,0 +1,16 @@
|
||||
var MAP = {
|
||||
'st_multipolygon': 'polygon',
|
||||
'st_polygon': 'polygon',
|
||||
'st_multilinestring': 'line',
|
||||
'st_linestring': 'line',
|
||||
'st_multipoint': 'point',
|
||||
'st_point': 'point'
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} val e.g. 'ST_MultiPoint'
|
||||
* @return {String} e.g. 'point'
|
||||
*/
|
||||
module.exports = function (val) {
|
||||
return MAP[val.toLowerCase()];
|
||||
};
|
||||
78
lib/assets/javascripts/builder/data/grantables-collection.js
Executable file
78
lib/assets/javascripts/builder/data/grantables-collection.js
Executable file
@@ -0,0 +1,78 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
var UserModel = require('./user-model');
|
||||
var GroupModel = require('./group-model');
|
||||
|
||||
/**
|
||||
* A collection of Grantable objects.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, options) {
|
||||
var type = attrs.type;
|
||||
var configModel = attrs.configModel;
|
||||
options = _.extend(options, {
|
||||
configModel: configModel
|
||||
});
|
||||
|
||||
if (attrs.model) {
|
||||
attrs = _.extend({}, attrs, attrs.model);
|
||||
}
|
||||
|
||||
if (type === 'user') {
|
||||
return new UserModel(_.omit(attrs, 'configModel'), options);
|
||||
} else if (type === 'group') {
|
||||
return new GroupModel(_.omit(attrs, 'configModel'), options);
|
||||
}
|
||||
},
|
||||
|
||||
sync: syncAbort,
|
||||
|
||||
url: function (method) {
|
||||
var baseUrl = this.configModel.get('base_url');
|
||||
var version = this.configModel.urlVersion('organizationGrantables', method);
|
||||
return baseUrl + '/api/' + version + '/organization/' + this.organization.id + '/grantables';
|
||||
},
|
||||
|
||||
initialize: function (users, opts) {
|
||||
if (!opts.organization) throw new Error('Organization is required');
|
||||
if (!opts.currentUserId) throw new Error('currentUserId is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this.configModel = opts.configModel;
|
||||
this.organization = opts.organization;
|
||||
this.currentUserId = opts.currentUserId;
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
this.trigger('fetched', this);
|
||||
this.total_entries = response.total_entries;
|
||||
|
||||
return _.reduce(response.grantables, function (memo, m) {
|
||||
if (m.id === this.currentUserId) {
|
||||
this.total_entries--;
|
||||
} else {
|
||||
m.organization = this.organization;
|
||||
m.configModel = this.configModel;
|
||||
memo.push(m);
|
||||
}
|
||||
return memo;
|
||||
}, [], this);
|
||||
},
|
||||
|
||||
fetch: function (opts) {
|
||||
opts = opts || {};
|
||||
this.trigger('fetching', this);
|
||||
opts.error = function (model, response) {
|
||||
this.trigger('error', this);
|
||||
}.bind(this);
|
||||
|
||||
return Backbone.Collection.prototype.fetch.call(this, opts);
|
||||
},
|
||||
|
||||
totalCount: function () {
|
||||
return this.total_entries;
|
||||
}
|
||||
|
||||
});
|
||||
29
lib/assets/javascripts/builder/data/group-model.js
Executable file
29
lib/assets/javascripts/builder/data/group-model.js
Executable file
@@ -0,0 +1,29 @@
|
||||
var Backbone = require('backbone');
|
||||
var UsersGroup = require('./users-group-collection');
|
||||
|
||||
/**
|
||||
* Model representing a group.
|
||||
* Expected to be used in the context of a groups collection (e.g. OrganizationGroups),
|
||||
* which defines its API endpoint path.
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
display_name: ''
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
this.parse(attrs || {}, opts); // handle given attrs in the same way as for .fetch()
|
||||
},
|
||||
|
||||
parse: function (attrs, opts) {
|
||||
this.users = new UsersGroup(attrs.users, {
|
||||
group: this,
|
||||
configModel: opts.configModel,
|
||||
organization: opts.organization
|
||||
});
|
||||
|
||||
return attrs;
|
||||
}
|
||||
});
|
||||
73
lib/assets/javascripts/builder/data/infowindow-click-model.js
Executable file
73
lib/assets/javascripts/builder/data/infowindow-click-model.js
Executable file
@@ -0,0 +1,73 @@
|
||||
var InfowindowDefinitionModel = require('./infowindow-definition-model');
|
||||
|
||||
module.exports = InfowindowDefinitionModel.extend({
|
||||
|
||||
defaults: {
|
||||
template_name: '',
|
||||
latlng: [0, 0],
|
||||
offset: [28, 0], // offset of the tip calculated from the bottom left corner
|
||||
maxHeight: 180, // max height of the content, not the whole infowindow
|
||||
autoPan: true,
|
||||
template: '',
|
||||
content: '',
|
||||
visibility: false,
|
||||
alternative_names: { },
|
||||
fields: null, // contains the fields displayed in the infowindow,
|
||||
width: 226,
|
||||
headerColor: {
|
||||
color: {
|
||||
fixed: '#35AAE5',
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
TEMPLATES: {
|
||||
infowindow_light: require('builder/mustache-templates/infowindows/infowindow_light.jst.mustache'),
|
||||
infowindow_dark: require('builder/mustache-templates/infowindows/infowindow_dark.jst.mustache'),
|
||||
infowindow_color: require('builder/mustache-templates/infowindows/infowindow_color.jst.mustache'),
|
||||
infowindow_header_with_image: require('builder/mustache-templates/infowindows/infowindow_header_with_image.jst.mustache'),
|
||||
infowindow_header_with_video: require('builder/mustache-templates/infowindows/infowindow_header_with_video.jst.mustache'),
|
||||
infowindow_header_with_url: require('builder/mustache-templates/infowindows/infowindow_header_with_url.jst.mustache'),
|
||||
custom_infowindow_light: require('builder/mustache-templates/infowindows_custom/infowindow_light.jst.mustache'),
|
||||
custom_infowindow_dark: require('builder/mustache-templates/infowindows_custom/infowindow_dark.jst.mustache'),
|
||||
custom_infowindow_color: require('builder/mustache-templates/infowindows_custom/infowindow_color.jst.mustache'),
|
||||
custom_infowindow_header_with_image: require('builder/mustache-templates/infowindows_custom/infowindow_header_with_image.jst.mustache')
|
||||
},
|
||||
|
||||
_transformTemplate: function (template) {
|
||||
var fixed = this.get('headerColor').color.fixed;
|
||||
|
||||
return template.replace('background: #35AAE5', 'background: ' + fixed);
|
||||
},
|
||||
|
||||
setTemplate: function (templateName) {
|
||||
var template = (typeof (this.TEMPLATES[templateName]) === 'undefined') ? '' : this.TEMPLATES[templateName];
|
||||
|
||||
if (templateName === 'infowindow_color') {
|
||||
template = this._transformTemplate(template);
|
||||
}
|
||||
|
||||
var attrs = {
|
||||
'template_name': templateName,
|
||||
'template': template,
|
||||
'template_type': 'mustache'
|
||||
};
|
||||
|
||||
if (templateName === '') {
|
||||
attrs.fields = [];
|
||||
}
|
||||
|
||||
this.set(attrs);
|
||||
},
|
||||
|
||||
isEmptyTemplate: function () {
|
||||
var isEmpty = this.fieldCount() === 0;
|
||||
return this.hasTemplate() && isEmpty;
|
||||
},
|
||||
|
||||
setDefault: function () {
|
||||
this.setTemplate(this.defaults.template_name);
|
||||
}
|
||||
|
||||
});
|
||||
184
lib/assets/javascripts/builder/data/infowindow-definition-model.js
Executable file
184
lib/assets/javascripts/builder/data/infowindow-definition-model.js
Executable file
@@ -0,0 +1,184 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
SYSTEM_COLUMNS: ['the_geom', 'the_geom_webmercator', 'created_at', 'updated_at', 'cartodb_id', 'cartodb_georef_status'],
|
||||
|
||||
TEMPLATES: {},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
},
|
||||
|
||||
clearFields: function () {
|
||||
this.set({ 'fields': [] });
|
||||
},
|
||||
|
||||
saveFields: function (where) {
|
||||
where = where || 'old_fields';
|
||||
this.set(where, _.clone(this.get('fields')));
|
||||
},
|
||||
|
||||
fieldCount: function () {
|
||||
var fields = this.get('fields');
|
||||
if (!fields) return 0;
|
||||
return fields.length;
|
||||
},
|
||||
|
||||
restoreFields: function (whiteList, from) {
|
||||
from = from || 'old_fields';
|
||||
var fields = this.get(from);
|
||||
if (whiteList) {
|
||||
fields = fields.filter(function (f) {
|
||||
return _.contains(whiteList, f.name);
|
||||
});
|
||||
}
|
||||
if (fields && fields.length) {
|
||||
this._setFields(fields);
|
||||
}
|
||||
this.unset(from);
|
||||
},
|
||||
|
||||
containsField: function (fieldName) {
|
||||
var fields = this.get('fields') || [];
|
||||
return _.contains(_(fields).pluck('name'), fieldName);
|
||||
},
|
||||
|
||||
getFieldProperty: function (fieldName, k) {
|
||||
if (this.containsField(fieldName)) {
|
||||
var fields = this.get('fields') || [];
|
||||
var idx = _.indexOf(_(fields).pluck('name'), fieldName);
|
||||
return fields[idx][k];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
setFieldProperty: function (fieldName, k, v) {
|
||||
if (this.containsField(fieldName)) {
|
||||
var fields = this._cloneFields() || [];
|
||||
var idx = _.indexOf(_(fields).pluck('name'), fieldName);
|
||||
fields[idx][k] = v;
|
||||
this._setFields(fields);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_cloneFields: function () {
|
||||
return _(this.get('fields')).map(function (v) {
|
||||
return _.clone(v);
|
||||
});
|
||||
},
|
||||
|
||||
_setFields: function (f) {
|
||||
f.sort(function (a, b) { return a.position - b.position; });
|
||||
this.set({ 'fields': f });
|
||||
},
|
||||
|
||||
sortFields: function () {
|
||||
this.get('fields').sort(function (a, b) { return a.position - b.position; });
|
||||
},
|
||||
|
||||
_addField: function (fieldName, at) {
|
||||
var dfd = $.Deferred();
|
||||
if (!this.containsField(fieldName)) {
|
||||
var fields = this.get('fields');
|
||||
|
||||
if (fields) {
|
||||
at = at === undefined ? fields.length : at;
|
||||
fields.push({ name: fieldName, title: true, position: at });
|
||||
} else {
|
||||
at = at === undefined ? 0 : at;
|
||||
this.set('fields', [{ name: fieldName, title: true, position: at }], { silent: true });
|
||||
}
|
||||
}
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
addField: function (fieldName, at) {
|
||||
var self = this;
|
||||
$.when(this._addField(fieldName, at)).then(function () {
|
||||
self.sortFields();
|
||||
self.trigger('add:fields');
|
||||
self.trigger('change:fields', self, self.get('fields'));
|
||||
self.trigger('change', self);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
removeField: function (fieldName) {
|
||||
if (this.containsField(fieldName)) {
|
||||
var fields = this._cloneFields() || [];
|
||||
var idx = _.indexOf(_(fields).pluck('name'), fieldName);
|
||||
if (idx >= 0) {
|
||||
fields.splice(idx, 1);
|
||||
}
|
||||
this._setFields(fields);
|
||||
this.trigger('remove:fields');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
getAlternativeName: function (fieldName) {
|
||||
return this.get('alternative_names') && this.get('alternative_names')[fieldName];
|
||||
},
|
||||
|
||||
setAlternativeName: function (fieldName, alternativeName) {
|
||||
var alternativeNames = _.clone(this.get('alternative_names') || {});
|
||||
|
||||
alternativeNames[fieldName] = alternativeName;
|
||||
this.set({ 'alternative_names': alternativeNames });
|
||||
this.trigger('change:alternative_names');
|
||||
},
|
||||
|
||||
getFieldPos: function (fieldName) {
|
||||
var p = this.getFieldProperty(fieldName, 'position');
|
||||
if (p === undefined) {
|
||||
return Number.MAX_VALUE;
|
||||
}
|
||||
return p;
|
||||
},
|
||||
|
||||
unsetTemplate: function () {
|
||||
this.setTemplate('');
|
||||
},
|
||||
|
||||
setTemplate: function (templateName) {
|
||||
var template = (typeof (this.TEMPLATES[templateName]) === 'undefined') ? '' : this.TEMPLATES[templateName];
|
||||
|
||||
var attrs = {
|
||||
'template_name': templateName,
|
||||
'template': template,
|
||||
'template_type': 'mustache'
|
||||
};
|
||||
|
||||
if (templateName === '') {
|
||||
attrs.fields = [];
|
||||
}
|
||||
|
||||
this.set(attrs);
|
||||
},
|
||||
|
||||
getTemplate: function () {
|
||||
var template_name = this.get('template_name');
|
||||
|
||||
if (template_name !== '') {
|
||||
return this.TEMPLATES['custom_' + template_name];
|
||||
} else {
|
||||
return this.get('template');
|
||||
}
|
||||
},
|
||||
|
||||
hasTemplate: function () {
|
||||
return !!this.get('template_name') && this.TEMPLATES[this.get('template_name')];
|
||||
},
|
||||
|
||||
isCustomTemplate: function () {
|
||||
return this.get('template_name') === '' && this.get('template') !== '';
|
||||
}
|
||||
|
||||
});
|
||||
24
lib/assets/javascripts/builder/data/infowindow-hover-model.js
Executable file
24
lib/assets/javascripts/builder/data/infowindow-hover-model.js
Executable file
@@ -0,0 +1,24 @@
|
||||
var InfowindowDefinitionModel = require('./infowindow-definition-model');
|
||||
|
||||
module.exports = InfowindowDefinitionModel.extend({
|
||||
|
||||
defaults: {
|
||||
vertical_offset: 0,
|
||||
horizontal_offset: 0,
|
||||
position: 'top|center',
|
||||
template_name: '',
|
||||
template: ''
|
||||
},
|
||||
|
||||
TEMPLATES: {
|
||||
tooltip_dark: require('builder/mustache-templates/tooltips/tooltip_dark.jst.mustache'),
|
||||
tooltip_light: require('builder/mustache-templates/tooltips/tooltip_light.jst.mustache'),
|
||||
custom_tooltip_dark: require('builder/mustache-templates/tooltips_custom/tooltip_dark.jst.mustache'),
|
||||
custom_tooltip_light: require('builder/mustache-templates/tooltips_custom/tooltip_light.jst.mustache')
|
||||
},
|
||||
|
||||
setDefault: function () {
|
||||
this.setTemplate(this.defaults.template_name);
|
||||
}
|
||||
|
||||
});
|
||||
29
lib/assets/javascripts/builder/data/layer-colors.js
Executable file
29
lib/assets/javascripts/builder/data/layer-colors.js
Executable file
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
COLORS: [
|
||||
'#11A579',
|
||||
'#E83f74',
|
||||
'#9F5DC8',
|
||||
'#EF8205',
|
||||
'#29B3B2',
|
||||
'#E54F3B',
|
||||
'#59BB12',
|
||||
'#EF57BA',
|
||||
'#E2AF00',
|
||||
'#12AADE'
|
||||
],
|
||||
|
||||
/**
|
||||
* Returns a color given a letter
|
||||
* @param {String} Letter. eg: 'a', 'b', 'c', etc.
|
||||
* @return {String} Hex color code: eg: '#7F3C8D'
|
||||
*/
|
||||
getColorForLetter: function (letter) {
|
||||
if (!letter) {
|
||||
return this.COLORS[0];
|
||||
}
|
||||
|
||||
var letterNumber = letter.charCodeAt(0) - 97;
|
||||
var colorIndex = ((letterNumber / this.COLORS.length % 1) * 10).toFixed();
|
||||
return this.COLORS[colorIndex] || this.COLORS[0];
|
||||
}
|
||||
};
|
||||
86
lib/assets/javascripts/builder/data/layer-content-model.js
Normal file
86
lib/assets/javascripts/builder/data/layer-content-model.js
Normal file
@@ -0,0 +1,86 @@
|
||||
var Backbone = require('backbone');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var STATES = require('./query-base-status');
|
||||
|
||||
var CONTEXTS = {
|
||||
map: 'map',
|
||||
table: 'table'
|
||||
};
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'querySchemaModel',
|
||||
'queryGeometryModel',
|
||||
'queryRowsCollection'
|
||||
];
|
||||
|
||||
var LayerContentModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
context: CONTEXTS.map, // map or table
|
||||
// This variable `state` is only used for the trigger change:state event.
|
||||
// It lives in the model so, as long as the model is recreated with the view every time,
|
||||
// this variable is not shared between views and can NOT use it to get the real state.
|
||||
state: STATES.initial
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._querySchemaModel, 'change:status', this._setState);
|
||||
this.listenTo(this._queryGeometryModel, 'change:status', this._setState);
|
||||
this.listenTo(this._queryRowsCollection.statusModel, 'change:status', this._setState);
|
||||
},
|
||||
|
||||
_setState: function () {
|
||||
this.set('state', this._getState());
|
||||
},
|
||||
|
||||
_getState: function () {
|
||||
if (this.isErrored()) return STATES.errored;
|
||||
if (this.isFetched()) return STATES.fetched;
|
||||
if (this.isFetching()) return STATES.fetching;
|
||||
if (this.isInitial()) return STATES.initial;
|
||||
|
||||
return STATES.unavailable;
|
||||
},
|
||||
|
||||
isErrored: function () {
|
||||
return this._querySchemaModel.hasRepeatedErrors() ||
|
||||
this._queryGeometryModel.hasRepeatedErrors() ||
|
||||
this._queryRowsCollection.hasRepeatedErrors();
|
||||
},
|
||||
|
||||
isFetching: function () {
|
||||
return this._querySchemaModel.isFetching() ||
|
||||
this._queryGeometryModel.isFetching() ||
|
||||
this._queryRowsCollection.isFetching();
|
||||
},
|
||||
|
||||
isFetched: function () {
|
||||
return this._querySchemaModel.isFetched() &&
|
||||
this._queryGeometryModel.isFetched() &&
|
||||
this._queryRowsCollection.isFetched();
|
||||
},
|
||||
|
||||
isInFinalStatus: function () {
|
||||
return this._querySchemaModel.isInFinalStatus() &&
|
||||
this._queryGeometryModel.isInFinalStatus() &&
|
||||
this._queryRowsCollection.isInFinalStatus();
|
||||
},
|
||||
|
||||
isInitial: function () {
|
||||
return this.get('state') === STATES.initial;
|
||||
},
|
||||
|
||||
isDone: function () {
|
||||
return this.isFetched() || this.isErrored();
|
||||
}
|
||||
});
|
||||
|
||||
LayerContentModel.CONTEXTS = CONTEXTS;
|
||||
LayerContentModel.STATES = STATES;
|
||||
|
||||
module.exports = LayerContentModel;
|
||||
489
lib/assets/javascripts/builder/data/layer-definition-model.js
Executable file
489
lib/assets/javascripts/builder/data/layer-definition-model.js
Executable file
@@ -0,0 +1,489 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
var StyleDefinitionModel = require('builder/editor/style/style-definition-model');
|
||||
var StyleCartoCSSModel = require('builder/editor/style/style-cartocss-model');
|
||||
var DataSQLModel = require('builder/editor/layers/layer-content-views/data/data-sql-model');
|
||||
var layerTypesAndKinds = require('./layer-types-and-kinds');
|
||||
var InfowindowModel = require('./infowindow-click-model');
|
||||
var TooltipModel = require('./infowindow-hover-model');
|
||||
var TableNameUtils = require('builder/helpers/table-name-utils');
|
||||
var layerColors = require('./layer-colors');
|
||||
|
||||
// from_layer_id and from_letter are not attributes for the model, but are sent to the layer creation
|
||||
// endpoint when creating a layer from an existing analysis node (see user-actions)
|
||||
var ATTR_NAMES = ['id', 'order', 'infowindow', 'tooltip', 'error', 'from_layer_id', 'from_letter'];
|
||||
|
||||
/**
|
||||
* Model to edit a layer definition.
|
||||
* Should always exist as part of a LayerDefinitionsCollection, so its URL is given from there.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.sync} abort ongoing request if there is any
|
||||
*/
|
||||
sync: syncAbort,
|
||||
|
||||
parse: function (response, opts) {
|
||||
response.options = response.options || {};
|
||||
|
||||
// Flatten the attrs, to avoid having this.get('options').foobar internally
|
||||
var attrs = _
|
||||
.defaults(
|
||||
_.pick(response, ATTR_NAMES),
|
||||
_.omit(response.options, ['query', 'tile_style'])
|
||||
);
|
||||
|
||||
// Only use type on the frontend, it will be mapped back when the model is serialized (see .toJSON)
|
||||
attrs.type = attrs.type || layerTypesAndKinds.getType(response.kind);
|
||||
|
||||
// Map API endpoint attrs to the new names used client-side (cartodb.js in particular)
|
||||
if (response.options.tile_style) {
|
||||
attrs.cartocss = response.options.tile_style;
|
||||
}
|
||||
if (response.options.query) {
|
||||
attrs.sql = response.options.query;
|
||||
}
|
||||
|
||||
if (response.infowindow) {
|
||||
if (!this.infowindowModel) {
|
||||
this.infowindowModel = new InfowindowModel(response.infowindow, {
|
||||
configModel: opts.configModel || this._configModel
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response.tooltip) {
|
||||
if (!this.tooltipModel) {
|
||||
this.tooltipModel = new TooltipModel(response.tooltip, {
|
||||
configModel: opts.configModel || this._configModel
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response.options.table_name) {
|
||||
// Set autostyle as false if it doesn't contain any id
|
||||
attrs.autoStyle = attrs.autoStyle || false;
|
||||
|
||||
if (!this.styleModel) {
|
||||
this.styleModel = new StyleDefinitionModel(response.options.style_properties, {
|
||||
parse: true
|
||||
});
|
||||
|
||||
this.cartocssModel = new StyleCartoCSSModel({
|
||||
content: attrs.cartocss
|
||||
}, {
|
||||
history: response.options.cartocss_history || response.options.tile_style_history
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.sqlModel) {
|
||||
this.sqlModel = new DataSQLModel({
|
||||
content: attrs.sql
|
||||
}, {
|
||||
history: response.options.sql_history
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten the rest of the attributes
|
||||
return attrs;
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this.on('change:source change:sql', this._onPosibleLayerSchemaChanged, this);
|
||||
|
||||
if (this.styleModel) {
|
||||
this.styleModel.bind('change:type change:animated', function () {
|
||||
if (this.styleModel.isAggregatedType() || this.styleModel.isAnimation()) {
|
||||
// setTemplate will clear fields
|
||||
this.infowindowModel && this.infowindowModel.unsetTemplate();
|
||||
this.tooltipModel && this.tooltipModel.unsetTemplate();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
|
||||
save: function (attrs, options) {
|
||||
attrs = attrs || {};
|
||||
options = options || {};
|
||||
|
||||
// We assume that if the layer is saved, we have to disable autostyle
|
||||
var autoStyleAttrs = {
|
||||
autoStyle: false
|
||||
};
|
||||
|
||||
// But if the layer is saved with shouldPreserveAutoStyle option, we should preserve autostyle
|
||||
if (options && options.shouldPreserveAutoStyle) {
|
||||
delete autoStyleAttrs.autoStyle;
|
||||
} else if (this.get('autoStyle')) {
|
||||
this.styleModel && this.styleModel.resetPropertiesFromAutoStyle();
|
||||
}
|
||||
|
||||
attrs = _.extend(
|
||||
{},
|
||||
autoStyleAttrs,
|
||||
attrs
|
||||
);
|
||||
|
||||
return Backbone.Model.prototype.save.call(this, attrs, options);
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
// Un-flatten the internal attrs to the datastructure that's expected by the API endpoint
|
||||
var options = _.omit(this.attributes, ATTR_NAMES.concat(['cartocss', 'sql', 'autoStyle']));
|
||||
|
||||
// Map back internal attrs to the expected attrs names by the API endpoint
|
||||
var cartocss = this.get('cartocss');
|
||||
|
||||
if (cartocss) {
|
||||
options.tile_style = cartocss;
|
||||
}
|
||||
var sql = this.get('sql');
|
||||
if (sql) {
|
||||
options.query = sql;
|
||||
}
|
||||
|
||||
var defaultAttributes = {
|
||||
kind: layerTypesAndKinds.getKind(this.get('type')),
|
||||
options: options
|
||||
};
|
||||
|
||||
var infowindowData = this.infowindowModel && this.infowindowModel.toJSON();
|
||||
if (!_.isEmpty(infowindowData)) {
|
||||
defaultAttributes.infowindow = this.infowindowModel.toJSON();
|
||||
}
|
||||
|
||||
var tooltipData = this.tooltipModel && this.tooltipModel.toJSON();
|
||||
if (!_.isEmpty(tooltipData)) {
|
||||
defaultAttributes.tooltip = this.tooltipModel.toJSON();
|
||||
}
|
||||
|
||||
if (this.styleModel && !this.styleModel.isAutogenerated()) {
|
||||
defaultAttributes.options.style_properties = this.styleModel.toJSON();
|
||||
}
|
||||
|
||||
if (this.cartocssModel) {
|
||||
defaultAttributes.options.cartocss_history = this.cartocssModel.getHistory();
|
||||
}
|
||||
|
||||
if (this.sqlModel) {
|
||||
defaultAttributes.options.sql_history = this.sqlModel.getHistory();
|
||||
}
|
||||
|
||||
var attributes = _.omit(this.attributes, 'infowindow', 'tooltip', 'options', 'error', 'autoStyle');
|
||||
|
||||
return _.defaults(
|
||||
defaultAttributes,
|
||||
_.pick(attributes, ATTR_NAMES)
|
||||
);
|
||||
},
|
||||
|
||||
canBeDeletedByUser: function () {
|
||||
return this.collection.getNumberOfDataLayers() > 1 && this.isDataLayer() &&
|
||||
(this._canBeFoldedUnderAnotherLayer() || !this._isAllDataLayersDependingOnAnyAnalysisOfThisLayer());
|
||||
},
|
||||
|
||||
isOwnerOfAnalysisNode: function (nodeModel) {
|
||||
return nodeModel && nodeModel.letter() === this.get('letter');
|
||||
},
|
||||
|
||||
ownedPrimaryAnalysisNodes: function () {
|
||||
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
|
||||
return this.isOwnerOfAnalysisNode(nodeDefModel)
|
||||
? nodeDefModel.linkedListBySameLetter()
|
||||
: [];
|
||||
},
|
||||
|
||||
getName: function () {
|
||||
return this.get('name') ||
|
||||
this.get('table_name_alias') ||
|
||||
this.get('table_name');
|
||||
},
|
||||
|
||||
getTableName: function () {
|
||||
return this.get('table_name') || '';
|
||||
},
|
||||
|
||||
getColor: function () {
|
||||
return layerColors.getColorForLetter(this.get('letter'));
|
||||
},
|
||||
|
||||
containsNode: function (other) {
|
||||
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
|
||||
return nodeDefModel && nodeDefModel.containsNode(other);
|
||||
},
|
||||
|
||||
getAnalysisDefinitionNodeModel: function () {
|
||||
return this.findAnalysisDefinitionNodeModel(this.get('source'));
|
||||
},
|
||||
|
||||
findAnalysisDefinitionNodeModel: function (id) {
|
||||
return this.collection && this.collection.findAnalysisDefinitionNodeModel(id);
|
||||
},
|
||||
|
||||
_onPosibleLayerSchemaChanged: function (eventName, attrs, options) {
|
||||
// Used to avoid resetting styles on source_id changes when we have saved styles for the node
|
||||
if (options && options.ignoreSchemaChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.infowindowModel) {
|
||||
this.infowindowModel.clearFields();
|
||||
}
|
||||
if (this.tooltipModel) {
|
||||
this.tooltipModel.clearFields();
|
||||
}
|
||||
if (this.styleModel) {
|
||||
this.styleModel.resetStyles();
|
||||
}
|
||||
},
|
||||
|
||||
toggleVisible: function () {
|
||||
this.set('visible', !this.get('visible'));
|
||||
},
|
||||
|
||||
toggleCollapse: function () {
|
||||
this.set('collapsed', !this.get('collapsed'));
|
||||
},
|
||||
|
||||
hasAnalyses: function () {
|
||||
return this.getNumberOfAnalyses() > 0;
|
||||
},
|
||||
|
||||
hasAggregatedStyles: function () {
|
||||
return this.styleModel && this.styleModel.isAggregatedType();
|
||||
},
|
||||
|
||||
getNumberOfAnalyses: function () {
|
||||
var analysisNode = this.getAnalysisDefinitionNodeModel();
|
||||
var count = 0;
|
||||
|
||||
while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
|
||||
analysisNode = analysisNode.getPrimarySource();
|
||||
|
||||
if (analysisNode) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
},
|
||||
|
||||
getQualifiedTableName: function () {
|
||||
var userName = this.get('user_name') || this.collection.userModel.get('username');
|
||||
return TableNameUtils.getQualifiedTableName(
|
||||
this.getTableName(),
|
||||
userName,
|
||||
this.collection.userModel.isInsideOrg()
|
||||
);
|
||||
},
|
||||
|
||||
getColumnNamesFromSchema: function () {
|
||||
return this._getQuerySchemaModel().getColumnNames();
|
||||
},
|
||||
|
||||
_getQuerySchemaModel: function () {
|
||||
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
|
||||
return nodeDefModel.querySchemaModel;
|
||||
},
|
||||
|
||||
isDataLayer: function () {
|
||||
var layerType = this.get('type');
|
||||
return layerTypesAndKinds.isCartoDBType(layerType) ||
|
||||
layerTypesAndKinds.isTorqueType(layerType);
|
||||
},
|
||||
|
||||
isTorqueLayer: function () {
|
||||
return this.get('type') === 'torque';
|
||||
},
|
||||
|
||||
isAutoStyleApplied: function () {
|
||||
var autoStyle = this.get('autoStyle');
|
||||
return (autoStyle != null && autoStyle !== false);
|
||||
},
|
||||
|
||||
_canBeFoldedUnderAnotherLayer: function () {
|
||||
var thisNodeDefModel = this.getAnalysisDefinitionNodeModel();
|
||||
|
||||
return this.collection.any(function (m) {
|
||||
if (m !== this && m.isDataLayer()) {
|
||||
var otherNodeDefModel = m.getAnalysisDefinitionNodeModel();
|
||||
if (otherNodeDefModel === thisNodeDefModel) return true;
|
||||
|
||||
var lastNode = _.last(otherNodeDefModel.linkedListBySameLetter());
|
||||
return lastNode.getPrimarySource() === thisNodeDefModel;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
_isAllDataLayersDependingOnAnyAnalysisOfThisLayer: function () {
|
||||
var nodeDefModel = this.getAnalysisDefinitionNodeModel();
|
||||
if (!nodeDefModel) return false;
|
||||
if (!this.isOwnerOfAnalysisNode(nodeDefModel)) return false;
|
||||
|
||||
var linkedNodesList = nodeDefModel.linkedListBySameLetter();
|
||||
|
||||
return this.collection.chain()
|
||||
.filter(function (m) {
|
||||
return m !== this && !!m.get('source');
|
||||
}, this)
|
||||
.all(function (m) {
|
||||
return _.any(linkedNodesList, function (node) {
|
||||
return m.containsNode(node);
|
||||
});
|
||||
}, this)
|
||||
.value();
|
||||
},
|
||||
|
||||
getAllDependentLayers: function () {
|
||||
var self = this;
|
||||
var layersCount = 0;
|
||||
|
||||
var layerDefinitionsCollectionModels = self.collection.models;
|
||||
|
||||
for (var i = 0; i < layerDefinitionsCollectionModels.length; i++) {
|
||||
var layer = layerDefinitionsCollectionModels[i];
|
||||
var dependentAnalysis = false;
|
||||
|
||||
if (layer !== self) {
|
||||
var analysisNode = layer.getAnalysisDefinitionNodeModel();
|
||||
|
||||
while (analysisNode) {
|
||||
if (self.isOwnerOfAnalysisNode(analysisNode)) {
|
||||
dependentAnalysis = true;
|
||||
}
|
||||
analysisNode = analysisNode.getPrimarySource();
|
||||
}
|
||||
|
||||
if (dependentAnalysis) {
|
||||
layersCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return layersCount;
|
||||
},
|
||||
|
||||
matchesAttrs: function (otherAttrs) {
|
||||
if (this.get('type') !== otherAttrs.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layerTypesAndKinds.isTiledType(otherAttrs.type)) {
|
||||
return this.get('name') === otherAttrs.name &&
|
||||
this.get('urlTemplate') === otherAttrs.urlTemplate;
|
||||
}
|
||||
|
||||
if (layerTypesAndKinds.isGMapsBase(otherAttrs.type)) {
|
||||
return this.get('name') === otherAttrs.name &&
|
||||
this.get('baseType') === otherAttrs.baseType &&
|
||||
this.get('style') === otherAttrs.style;
|
||||
}
|
||||
|
||||
if (layerTypesAndKinds.isPlainType(otherAttrs.type)) {
|
||||
return this.get('color') === otherAttrs.color;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
hasGeocodingAnalysisApplied: function () {
|
||||
var analysisNode = this.getAnalysisDefinitionNodeModel();
|
||||
|
||||
if (analysisNode && analysisNode.get('type') === 'geocoding') {
|
||||
return true;
|
||||
}
|
||||
|
||||
while (analysisNode && this.isOwnerOfAnalysisNode(analysisNode)) {
|
||||
analysisNode = analysisNode.getPrimarySource();
|
||||
|
||||
if (analysisNode && analysisNode.get('type') === 'geocoding') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_hasAnyAnalysisApplied: function () {
|
||||
var analysisNode = this.getAnalysisDefinitionNodeModel();
|
||||
|
||||
return analysisNode.get('type') !== 'source';
|
||||
},
|
||||
|
||||
isEmpty: function () {
|
||||
throw new Error('LayerDefinitionModel.isEmpty() is an async operation. Use `.isEmptyAsync` instead.');
|
||||
},
|
||||
|
||||
isEmptyAsync: function () {
|
||||
var nodeModel = this.getAnalysisDefinitionNodeModel();
|
||||
var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
|
||||
var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
nodeModel.queryRowsCollection.isEmptyAsync()
|
||||
.then(function (isEmpty) {
|
||||
resolve(!hasAnyAnalysisApplied && !hasCustomQueryApplied && isEmpty);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
isDataFiltered: function () {
|
||||
var nodeModel = this.getAnalysisDefinitionNodeModel();
|
||||
var hasAnyAnalysisApplied = this._hasAnyAnalysisApplied();
|
||||
var hasCustomQueryApplied = nodeModel.isCustomQueryApplied();
|
||||
return new Promise(function (resolve, reject) {
|
||||
nodeModel.queryRowsCollection.isEmptyAsync()
|
||||
.then(function (isEmpty) {
|
||||
resolve((hasAnyAnalysisApplied || hasCustomQueryApplied) && isEmpty);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
isDone: function () {
|
||||
var nodeModel = this.getAnalysisDefinitionNodeModel();
|
||||
return nodeModel.queryRowsCollection.isDone() &&
|
||||
nodeModel.queryGeometryModel.isDone() &&
|
||||
nodeModel.querySchemaModel.isDone();
|
||||
},
|
||||
|
||||
canBeGeoreferenced: function () {
|
||||
var self = this;
|
||||
var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
|
||||
var emptyPromise = self.isEmptyAsync();
|
||||
var geomPromise = analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync();
|
||||
|
||||
return Promise.all([emptyPromise, geomPromise])
|
||||
.then(function (values) {
|
||||
var isEmpty = values[0];
|
||||
var hasGeom = values[1];
|
||||
|
||||
var canBeGeoreferenced =
|
||||
!isEmpty &&
|
||||
!hasGeom &&
|
||||
!self.hasGeocodingAnalysisApplied() &&
|
||||
!self._hasAnyAnalysisApplied() &&
|
||||
!analysisDefinitionNodeModel.isCustomQueryApplied();
|
||||
|
||||
return canBeGeoreferenced;
|
||||
});
|
||||
},
|
||||
|
||||
fetchQueryRowsIfRequired: function () {
|
||||
var self = this;
|
||||
var analysisDefinitionNodeModel = this.getAnalysisDefinitionNodeModel();
|
||||
|
||||
analysisDefinitionNodeModel.queryGeometryModel.hasValueAsync()
|
||||
.then(function (geomHasValue) {
|
||||
if (!self.hasGeocodingAnalysisApplied() &&
|
||||
!geomHasValue &&
|
||||
!analysisDefinitionNodeModel.isCustomQueryApplied() &&
|
||||
analysisDefinitionNodeModel.queryRowsCollection.isUnavailable()) {
|
||||
analysisDefinitionNodeModel.queryRowsCollection.fetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
411
lib/assets/javascripts/builder/data/layer-definitions-collection.js
Executable file
411
lib/assets/javascripts/builder/data/layer-definitions-collection.js
Executable file
@@ -0,0 +1,411 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
23
lib/assets/javascripts/builder/data/layer-letters.js
Executable file
23
lib/assets/javascripts/builder/data/layer-letters.js
Executable file
@@ -0,0 +1,23 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* Get the next available letter.
|
||||
* @param {Array} letters e.g. ['a', 'b', 'd']
|
||||
* @return {String} e.g. 'c'
|
||||
*/
|
||||
next: function (letters) {
|
||||
return _.chain(letters)
|
||||
.sort()
|
||||
.reduce(function (memo, letter) {
|
||||
if (letter === memo) {
|
||||
return String.fromCharCode(letter.charCodeAt() + 1);
|
||||
} else {
|
||||
return memo;
|
||||
}
|
||||
}, 'a')
|
||||
.value();
|
||||
}
|
||||
|
||||
};
|
||||
73
lib/assets/javascripts/builder/data/layer-types-and-kinds.js
Executable file
73
lib/assets/javascripts/builder/data/layer-types-and-kinds.js
Executable file
@@ -0,0 +1,73 @@
|
||||
// Types
|
||||
var PLAIN = 'Plain';
|
||||
var CARTODB = 'CartoDB';
|
||||
var TILED = 'Tiled';
|
||||
var TORQUE = 'torque';
|
||||
var GMAPSBASE = 'GMapsBase';
|
||||
var WMS = 'WMS';
|
||||
|
||||
// Values of `kind` attribute that the backend/DB knows about
|
||||
var KIND_TO_TYPE_MAP = {
|
||||
'background': PLAIN,
|
||||
'carto': CARTODB,
|
||||
'gmapsbase': GMAPSBASE,
|
||||
'tiled': TILED,
|
||||
'torque': TORQUE,
|
||||
'wms': WMS
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getType: function (kind) {
|
||||
var type = KIND_TO_TYPE_MAP[kind];
|
||||
|
||||
if (!type) {
|
||||
throw new Error('no type found for given kind ' + kind);
|
||||
}
|
||||
|
||||
return type;
|
||||
},
|
||||
|
||||
getKind: function (type) {
|
||||
if (!type) {
|
||||
throw new Error('no kind found for given type ' + type);
|
||||
}
|
||||
|
||||
var kind;
|
||||
for (kind in KIND_TO_TYPE_MAP) {
|
||||
if (KIND_TO_TYPE_MAP[kind] === type) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return kind;
|
||||
},
|
||||
|
||||
isKindDataLayer: function (kind) {
|
||||
var type = KIND_TO_TYPE_MAP[kind];
|
||||
return this.isTypeDataLayer(type);
|
||||
},
|
||||
|
||||
isTypeDataLayer: function (type) {
|
||||
return this.isCartoDBType(type) || this.isTorqueType(type);
|
||||
},
|
||||
|
||||
isCartoDBType: function (type) {
|
||||
return type === CARTODB;
|
||||
},
|
||||
|
||||
isTorqueType: function (type) {
|
||||
return type === TORQUE;
|
||||
},
|
||||
|
||||
isTiledType: function (type) {
|
||||
return type === TILED;
|
||||
},
|
||||
|
||||
isPlainType: function (type) {
|
||||
return type === PLAIN;
|
||||
},
|
||||
|
||||
isGMapsBase: function (type) {
|
||||
return type === GMAPSBASE;
|
||||
}
|
||||
};
|
||||
110
lib/assets/javascripts/builder/data/legends/legend-base-definition-model.js
Executable file
110
lib/assets/javascripts/builder/data/legends/legend-base-definition-model.js
Executable file
@@ -0,0 +1,110 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var syncAbort = require('builder/data/backbone/sync-abort');
|
||||
var LegendsState = require('./legends-state');
|
||||
|
||||
/*
|
||||
Base model for a legend. It should have a reference to a layerDefinitionModel.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
type: 'none',
|
||||
title: '',
|
||||
postHTMLSnippet: '',
|
||||
preHTMLSnippet: ''
|
||||
},
|
||||
|
||||
sync: syncAbort,
|
||||
|
||||
parse: function (r) {
|
||||
var attrs = _.extend({},
|
||||
_.omit(r, 'created_at', 'updated_at', 'definition', 'pre_html', 'post_html')
|
||||
);
|
||||
|
||||
if (r.conf && r.conf.columns) {
|
||||
attrs.customState = r.conf.columns;
|
||||
}
|
||||
|
||||
attrs.preHTMLSnippet = r.pre_html;
|
||||
attrs.postHTMLSnippet = r.post_html;
|
||||
return attrs;
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
var baseUrl = this.configModel.get('base_url');
|
||||
return baseUrl + '/api/v3/viz/' + this.vizId + '/layer/' + this.layerDefinitionModel.id + '/legends';
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.vizId) throw new Error('vizId is required');
|
||||
|
||||
this.layerDefinitionModel = opts.layerDefinitionModel;
|
||||
this.configModel = opts.configModel;
|
||||
this.vizId = opts.vizId;
|
||||
},
|
||||
|
||||
getStyles: function () {
|
||||
return this.layerDefinitionModel && this.layerDefinitionModel.styleModel || null;
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
throw new Error('This model should not make any fetch calls. It should be created from the vizJSON.');
|
||||
},
|
||||
|
||||
getType: function () {
|
||||
return this.get('type');
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return _.extend(
|
||||
this.attributes,
|
||||
{
|
||||
type: this.getType()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getAttributes: function () {
|
||||
return _.extend(
|
||||
{},
|
||||
_.pick(this.attributes, _.keys(this.defaults)),
|
||||
_.omit(this.attributes, 'pre_html', 'post_html', 'customState')
|
||||
);
|
||||
},
|
||||
|
||||
setAttributes: function (attrs) {
|
||||
// Store the real values
|
||||
LegendsState.set(this.layerDefinitionModel, this.get('type') || attrs.type, attrs);
|
||||
this.set(attrs);
|
||||
},
|
||||
|
||||
// Overriding set
|
||||
set: function (key, val, options) {
|
||||
var attrs;
|
||||
if (typeof key === 'object') {
|
||||
attrs = key;
|
||||
options = val;
|
||||
} else {
|
||||
(attrs = {})[key] = val;
|
||||
}
|
||||
|
||||
options || (options = {});
|
||||
|
||||
var layerDefinitionModel = this.layerDefinitionModel || options.layerDefinitionModel;
|
||||
var type = this.get('type') || attrs.type;
|
||||
|
||||
// Override attributes with the current custom state
|
||||
var state = LegendsState.get(layerDefinitionModel, type);
|
||||
var attributes = _.extend({}, attrs, {
|
||||
customState: _.keys(state)
|
||||
}, state);
|
||||
|
||||
return Backbone.Model.prototype.set.call(this, attributes, options);
|
||||
},
|
||||
|
||||
hasCustomHtml: function () {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
var _ = require('underscore');
|
||||
var LegendBaseDefModel = require('./legend-base-definition-model');
|
||||
var LegendColorHelper = require('builder/editor/layers/layer-content-views/legend/form/legend-color-helper');
|
||||
var DEFAULT_BUBBLES_COLOR = '#999999';
|
||||
|
||||
module.exports = LegendBaseDefModel.extend({
|
||||
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
|
||||
{
|
||||
type: 'bubble',
|
||||
fillColor: DEFAULT_BUBBLES_COLOR,
|
||||
topLabel: '',
|
||||
bottomLabel: ''
|
||||
}
|
||||
),
|
||||
|
||||
parse: function (r, opts) {
|
||||
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
|
||||
|
||||
if (opts.layerDefinitionModel) {
|
||||
if (r.definition) {
|
||||
attrs.fillColor = r.definition.color;
|
||||
attrs.topLabel = r.definition.top_label;
|
||||
attrs.bottomLabel = r.definition.bottom_label;
|
||||
} else {
|
||||
attrs.fillColor = this._inheritStyleColor(opts.layerDefinitionModel.styleModel);
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return _.extend(
|
||||
{},
|
||||
_.omit(this.attributes, 'fill', 'fillColor', 'topLabel', 'bottomLabel', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
|
||||
{
|
||||
pre_html: this.get('preHTMLSnippet'),
|
||||
post_html: this.get('postHTMLSnippet')
|
||||
},
|
||||
{
|
||||
conf: {
|
||||
columns: this.get('customState')
|
||||
}
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
color: this.get('fillColor'),
|
||||
top_label: this.get('topLabel'),
|
||||
bottom_label: this.get('bottomLabel')
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_inheritStyleColor: function (styleModel) {
|
||||
var fill = styleModel.get('fill');
|
||||
var stroke = styleModel.get('stroke');
|
||||
var color = (fill && fill.color) || (stroke && stroke.color);
|
||||
|
||||
if (color && (color.fixed || color.range)) {
|
||||
return LegendColorHelper.getBubbles(color).color.fixed;
|
||||
} else {
|
||||
return DEFAULT_BUBBLES_COLOR;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
var _ = require('underscore');
|
||||
var LegendBaseDefModel = require('./legend-base-definition-model');
|
||||
|
||||
module.exports = LegendBaseDefModel.extend({
|
||||
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
|
||||
{
|
||||
type: 'category'
|
||||
}
|
||||
),
|
||||
|
||||
toJSON: function () {
|
||||
return _.extend(
|
||||
{},
|
||||
_.omit(this.attributes, 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
|
||||
{
|
||||
pre_html: this.get('preHTMLSnippet'),
|
||||
post_html: this.get('postHTMLSnippet')
|
||||
},
|
||||
{
|
||||
conf: {
|
||||
columns: this.get('customState')
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
var _ = require('underscore');
|
||||
var LegendBaseDefModel = require('./legend-base-definition-model');
|
||||
|
||||
module.exports = LegendBaseDefModel.extend({
|
||||
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
|
||||
{
|
||||
type: 'choropleth',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
leftLabel: '',
|
||||
rightLabel: ''
|
||||
}
|
||||
),
|
||||
|
||||
parse: function (r, opts) {
|
||||
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
|
||||
if (r.definition) {
|
||||
attrs.prefix = r.definition.prefix;
|
||||
attrs.suffix = r.definition.suffix;
|
||||
attrs.leftLabel = r.definition.left_label;
|
||||
attrs.rightLabel = r.definition.right_label;
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var definition = {
|
||||
prefix: this.get('prefix'),
|
||||
suffix: this.get('suffix'),
|
||||
left_label: this.get('leftLabel'),
|
||||
right_label: this.get('rightLabel')
|
||||
};
|
||||
|
||||
return _.extend(
|
||||
{},
|
||||
_.omit(this.attributes, 'prefix', 'suffix', 'leftLabel', 'rightLabel', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
|
||||
{
|
||||
pre_html: this.get('preHTMLSnippet'),
|
||||
post_html: this.get('postHTMLSnippet')
|
||||
},
|
||||
{
|
||||
conf: {
|
||||
columns: this.get('customState')
|
||||
}
|
||||
},
|
||||
{
|
||||
definition: _.pick(definition, _.identity)
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
var _ = require('underscore');
|
||||
var LegendBaseDefModel = require('./legend-base-definition-model');
|
||||
|
||||
module.exports = LegendBaseDefModel.extend({
|
||||
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
|
||||
{
|
||||
type: 'custom_choropleth',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
colors: [],
|
||||
leftLabel: '',
|
||||
rightLabel: ''
|
||||
}
|
||||
),
|
||||
|
||||
parse: function (r, opts) {
|
||||
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
|
||||
var fill;
|
||||
var stroke;
|
||||
var color;
|
||||
|
||||
if (r.definition) {
|
||||
attrs.colors = r.definition.colors;
|
||||
attrs.prefix = r.definition.prefix;
|
||||
attrs.suffix = r.definition.suffix;
|
||||
attrs.leftLabel = r.definition.left_label;
|
||||
attrs.rightLabel = r.definition.right_label;
|
||||
} else {
|
||||
fill = opts.layerDefinitionModel.styleModel.get('fill');
|
||||
stroke = opts.layerDefinitionModel.styleModel.get('stroke');
|
||||
color = fill.color || stroke.color;
|
||||
attrs.colors = this._inheritStyleColors(color);
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var definition = {
|
||||
prefix: this.get('prefix'),
|
||||
suffix: this.get('suffix'),
|
||||
left_label: this.get('leftLabel'),
|
||||
right_label: this.get('rightLabel'),
|
||||
colors: this.get('colors').map(function (item) {
|
||||
return {
|
||||
color: item.color
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
return _.extend(
|
||||
{},
|
||||
_.omit(this.attributes, 'prefix', 'suffix', 'leftLabel', 'rightLabel', 'colors', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
|
||||
{
|
||||
pre_html: this.get('preHTMLSnippet'),
|
||||
post_html: this.get('postHTMLSnippet')
|
||||
},
|
||||
{
|
||||
conf: {
|
||||
columns: this.get('customState')
|
||||
}
|
||||
},
|
||||
{
|
||||
definition: _.pick(definition, _.identity)
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_inheritStyleColors: function (color) {
|
||||
var range = 0;
|
||||
var colors = [];
|
||||
|
||||
if (color.range) {
|
||||
range = color.range.length;
|
||||
|
||||
colors = _.range(range).map(function (v, index) {
|
||||
return {
|
||||
color: color.range[index]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
var _ = require('underscore');
|
||||
var LegendBaseDefModel = require('./legend-base-definition-model');
|
||||
var StyleHelper = require('builder/helpers/style');
|
||||
|
||||
module.exports = LegendBaseDefModel.extend({
|
||||
defaults: _.extend({}, LegendBaseDefModel.prototype.defaults,
|
||||
{
|
||||
type: 'custom',
|
||||
items: [],
|
||||
html: ''
|
||||
}
|
||||
),
|
||||
|
||||
parse: function (r, opts) {
|
||||
var attrs = LegendBaseDefModel.prototype.parse.call(this, r);
|
||||
|
||||
if (r.definition) {
|
||||
attrs.items = r.definition.categories;
|
||||
attrs.html = r.definition.html || '';
|
||||
} else {
|
||||
attrs.items = StyleHelper.getStyleCategories(opts.layerDefinitionModel.styleModel);
|
||||
}
|
||||
attrs.type = 'custom';
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var definition = {
|
||||
categories: this.get('items').map(function (item) {
|
||||
return {
|
||||
title: (item.title || '').toString(),
|
||||
color: item.color,
|
||||
icon: item.image || item.icon
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
if (this.hasCustomHtml()) {
|
||||
definition.html = this.get('html');
|
||||
}
|
||||
|
||||
return _.extend(
|
||||
{},
|
||||
_.omit(this.attributes, 'items', 'html', 'postHTMLSnippet', 'preHTMLSnippet', 'customState'),
|
||||
{
|
||||
pre_html: this.get('preHTMLSnippet'),
|
||||
post_html: this.get('postHTMLSnippet')
|
||||
},
|
||||
{
|
||||
conf: {
|
||||
columns: this.get('customState')
|
||||
}
|
||||
},
|
||||
{
|
||||
definition: definition
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
hasCustomHtml: function () {
|
||||
return this.get('html') !== '';
|
||||
}
|
||||
});
|
||||
104
lib/assets/javascripts/builder/data/legends/legend-definitions-collection.js
Executable file
104
lib/assets/javascripts/builder/data/legends/legend-definitions-collection.js
Executable file
@@ -0,0 +1,104 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var LegendBaseDefModel = require('./legend-base-definition-model');
|
||||
var layerTypesAndKinds = require('builder/data/layer-types-and-kinds');
|
||||
|
||||
var CategoryModel = require('./legend-category-definition-model');
|
||||
var BubbleModel = require('./legend-bubble-definition-model');
|
||||
var ChoroplethModel = require('./legend-choropleth-definition-model');
|
||||
var CustomChoroplethModel = require('./legend-custom-choropleth-definition-model');
|
||||
var CustomModel = require('./legend-custom-definition-model');
|
||||
var TorqueModel = require('./legend-torque-definition-model');
|
||||
|
||||
var LEGEND_MODEL_TYPE = {
|
||||
'bubble': BubbleModel,
|
||||
'category': CategoryModel,
|
||||
'choropleth': ChoroplethModel,
|
||||
'custom_choropleth': CustomChoroplethModel,
|
||||
'custom': CustomModel,
|
||||
'html': CustomModel,
|
||||
'torque': TorqueModel
|
||||
};
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
model: LegendBaseDefModel,
|
||||
|
||||
initialize: function (models, options) {
|
||||
if (!options.configModel) throw new Error('configModel is required');
|
||||
if (!options.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
|
||||
if (!options.vizId) throw new Error('vizId is required');
|
||||
|
||||
this.configModel = options.configModel;
|
||||
this.layerDefinitionsCollection = options.layerDefinitionsCollection;
|
||||
this.vizId = options.vizId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Intended to be called from entry point, to make sure initial layers are taken into account
|
||||
*/
|
||||
resetByData: function (vizJSON) {
|
||||
var models = [];
|
||||
var layers = _.filter(vizJSON.layers, function (layer) {
|
||||
return this._isDataLayer(layer.type);
|
||||
}, this);
|
||||
|
||||
_.each(layers, function (layer) {
|
||||
var layerDefModel = this._findLayerDefinitionModel(layer.id);
|
||||
var legends = [];
|
||||
if (layer.legends) {
|
||||
legends = layer.legends.map(function (legend) {
|
||||
var Klass = LEGEND_MODEL_TYPE[legend.type];
|
||||
|
||||
return new Klass(legend, {
|
||||
layerDefinitionModel: layerDefModel,
|
||||
configModel: this.configModel,
|
||||
vizId: this.vizId,
|
||||
parse: true
|
||||
});
|
||||
}, this);
|
||||
}
|
||||
|
||||
models.push(legends);
|
||||
}, this);
|
||||
|
||||
this.reset(_.flatten(models));
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
throw new Error('This collection should not make any fetch calls. It should be populated from the vizJSON.');
|
||||
},
|
||||
|
||||
_isDataLayer: function (layerType) {
|
||||
return layerTypesAndKinds.isCartoDBType(layerType) ||
|
||||
layerTypesAndKinds.isTorqueType(layerType);
|
||||
},
|
||||
|
||||
_findLayerDefinitionModel: function (id) {
|
||||
return this.layerDefinitionsCollection.findWhere({id: id});
|
||||
},
|
||||
|
||||
findByLayerDefModel: function (layerDefModel) {
|
||||
var id = layerDefModel.id;
|
||||
return this.filter(function (legendDefModel) {
|
||||
var layerDefModel = legendDefModel.layerDefinitionModel;
|
||||
return layerDefModel.id === id;
|
||||
});
|
||||
},
|
||||
|
||||
findByLayerDefModelAndType: function (layerDefModel, type) {
|
||||
var id = layerDefModel.id;
|
||||
return this.find(function (legendDefModel) {
|
||||
var layerDefModel = legendDefModel.layerDefinitionModel;
|
||||
return layerDefModel.id === id && legendDefModel.get('type') === type;
|
||||
});
|
||||
},
|
||||
|
||||
findByLayerDefModelAndTypes: function (layerDefModel, types) {
|
||||
var id = layerDefModel.id;
|
||||
return this.filter(function (legendDefModel) {
|
||||
var layerDefModel = legendDefModel.layerDefinitionModel;
|
||||
var type = legendDefModel.get('type');
|
||||
return layerDefModel.id === id && types.indexOf(type) !== -1;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
var _ = require('underscore');
|
||||
var LegendBaseDefModel = require('./legend-custom-definition-model');
|
||||
|
||||
module.exports = LegendBaseDefModel.extend({
|
||||
defaults: function () {
|
||||
return _.extend({}, LegendBaseDefModel.prototype.defaults,
|
||||
{
|
||||
type: 'torque',
|
||||
items: [],
|
||||
html: ''
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
parse: function (r, opts) {
|
||||
var attrs = LegendBaseDefModel.prototype.parse.call(this, r, opts);
|
||||
attrs.type = 'torque';
|
||||
return attrs;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
var LEGENDS_METADATA = {
|
||||
bubble: {
|
||||
legendType: 'size'
|
||||
},
|
||||
category: {
|
||||
legendType: 'color'
|
||||
},
|
||||
choropleth: {
|
||||
legendType: 'color'
|
||||
},
|
||||
custom: {
|
||||
legendType: 'color'
|
||||
},
|
||||
custom_choropleth: {
|
||||
legendType: 'color'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = LEGENDS_METADATA;
|
||||
72
lib/assets/javascripts/builder/data/legends/legends-state.js
Executable file
72
lib/assets/javascripts/builder/data/legends/legends-state.js
Executable file
@@ -0,0 +1,72 @@
|
||||
var _ = require('underscore');
|
||||
var legendsMetadata = require('./legends-metadata');
|
||||
|
||||
var CUSTOM_ATTRIBUTES = ['title'];
|
||||
var instance;
|
||||
var legends;
|
||||
|
||||
var State = (function () {
|
||||
var getType = function (type) {
|
||||
var legend = legendsMetadata[type];
|
||||
return legend && legend.legendType;
|
||||
};
|
||||
|
||||
var onLayerAdded = function (layer) {
|
||||
instance[layer.id] = {};
|
||||
};
|
||||
|
||||
var onLayerRemoved = function (layer) {
|
||||
delete instance[layer.id];
|
||||
};
|
||||
|
||||
return {
|
||||
init: function (layersDefinitionCollection, legendsDefinitionCollection) {
|
||||
if (!instance) {
|
||||
instance = {};
|
||||
layersDefinitionCollection.on('add', onLayerAdded);
|
||||
layersDefinitionCollection.on('remove', onLayerRemoved);
|
||||
legends = legendsDefinitionCollection;
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
legends.each(function (legend) {
|
||||
var layer = legend.layerDefinitionModel;
|
||||
if (!instance[layer.id]) {
|
||||
instance[layer.id] = {};
|
||||
}
|
||||
|
||||
var legendType = getType(legend.get('type'));
|
||||
if (!instance[layer.id][legendType]) {
|
||||
instance[layer.id][legendType] = {};
|
||||
}
|
||||
|
||||
var state = _.pick(legend.attributes, legend.attributes.conf.columns);
|
||||
instance[layer.id][legendType] = state;
|
||||
});
|
||||
},
|
||||
|
||||
set: function (layer, type, attrs) {
|
||||
var attributes = _.pick(attrs, CUSTOM_ATTRIBUTES);
|
||||
var legendType = getType(type);
|
||||
if (instance && legendType) {
|
||||
if (!instance[layer.id]) {
|
||||
instance[layer.id] = {};
|
||||
}
|
||||
instance[layer.id][legendType] = attributes;
|
||||
}
|
||||
},
|
||||
|
||||
get: function (layer, type) {
|
||||
var legendType = getType(type);
|
||||
return instance && legendType && instance[layer.id] && instance[layer.id][legendType];
|
||||
},
|
||||
|
||||
getInstance: function () {
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
module.exports = State;
|
||||
101
lib/assets/javascripts/builder/data/map-definition-model.js
Executable file
101
lib/assets/javascripts/builder/data/map-definition-model.js
Executable file
@@ -0,0 +1,101 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var OWN_ATTRS = ['drag', 'id', 'keyboard'];
|
||||
var EMBED_ATTRS = ['legends', 'scrollwheel', 'layer_selector', 'dashboard_menu'];
|
||||
var basemapProvidersAndCategories = require('./basemap-providers-and-categories');
|
||||
/**
|
||||
* Model that represents a visualization's Map
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
drag: true,
|
||||
keyboard: true,
|
||||
cartodb_logo: false,
|
||||
legends: true,
|
||||
layer_selector: false,
|
||||
dashboard_menu: true,
|
||||
scrollwheel: true
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
return _.extend({
|
||||
provider: r['map_provider']
|
||||
},
|
||||
_.pick(r, OWN_ATTRS),
|
||||
_.pick(r.options, EMBED_ATTRS)
|
||||
);
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v2/maps/';
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
this._userModel = opts.userModel;
|
||||
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
|
||||
|
||||
this._layerDefinitionsCollection.bind('baseLayerChanged', this._onBaseLayerChanged, this);
|
||||
},
|
||||
|
||||
_onBaseLayerChanged: function () {
|
||||
var baseLayer = this._layerDefinitionsCollection.getBaseLayer();
|
||||
var newProvider = basemapProvidersAndCategories.getProvider(baseLayer.get('type'));
|
||||
|
||||
this.save({
|
||||
provider: newProvider,
|
||||
minZoom: baseLayer.get('minZoom'),
|
||||
maxZoom: baseLayer.get('maxZoom')
|
||||
});
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return _.extend(
|
||||
{},
|
||||
_.omit(this.attributes, EMBED_ATTRS),
|
||||
{
|
||||
attribution: _.unique(this._layerDefinitionsCollection.pluck('attribution')),
|
||||
user_id: this._userModel.get('id')
|
||||
},
|
||||
{
|
||||
options: _.pick(this.attributes, EMBED_ATTRS)
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setMapViewSize: function (size) {
|
||||
this._mapViewSize = size;
|
||||
},
|
||||
|
||||
getMapViewSize: function () {
|
||||
return this._mapViewSize;
|
||||
},
|
||||
|
||||
setStaticImageURLTemplate: function (staticImageURLTemplate) {
|
||||
this._staticImageURLTemplate = staticImageURLTemplate;
|
||||
},
|
||||
|
||||
getStaticImageURLTemplate: function () {
|
||||
return this._staticImageURLTemplate;
|
||||
},
|
||||
|
||||
setImageExportMetadata: function (metadata) {
|
||||
this._imageExportMetadata = metadata;
|
||||
},
|
||||
|
||||
getImageExportMetadata: function () {
|
||||
return this._imageExportMetadata;
|
||||
},
|
||||
|
||||
setConverters: function (converters) {
|
||||
_.each(converters, function (converter, key) {
|
||||
this[key] = converter;
|
||||
}, this);
|
||||
}
|
||||
});
|
||||
55
lib/assets/javascripts/builder/data/map-settings.js
Executable file
55
lib/assets/javascripts/builder/data/map-settings.js
Executable file
@@ -0,0 +1,55 @@
|
||||
module.exports = function (overlaysCollection, mapDefModel, userModel) {
|
||||
var overlays = overlaysCollection.pluck('type');
|
||||
var canRemoveLogo = userModel.get('actions').remove_logo;
|
||||
|
||||
return [{
|
||||
setting: 'search',
|
||||
label: _t('editor.settings.preview.options.elements.search'),
|
||||
related: 'overlays',
|
||||
disabled: false,
|
||||
enabler: overlays.indexOf('search') > 0
|
||||
},
|
||||
{
|
||||
setting: 'zoom',
|
||||
label: _t('editor.settings.preview.options.elements.zoom'),
|
||||
related: 'overlays',
|
||||
disabled: false,
|
||||
enabler: overlays.indexOf('zoom') > 0
|
||||
},
|
||||
{
|
||||
setting: 'logo',
|
||||
label: _t('editor.settings.preview.options.elements.logo'),
|
||||
related: 'overlays',
|
||||
disabled: false,
|
||||
hidden: !canRemoveLogo,
|
||||
enabler: overlays.indexOf('logo') > 0
|
||||
},
|
||||
{
|
||||
setting: 'legends',
|
||||
label: _t('editor.settings.preview.options.elements.legends'),
|
||||
related: 'map',
|
||||
disabled: false,
|
||||
enabler: mapDefModel.get('legends') === true
|
||||
},
|
||||
{
|
||||
setting: 'layer_selector',
|
||||
label: _t('editor.settings.preview.options.elements.layer_selector'),
|
||||
related: 'map',
|
||||
disabled: false,
|
||||
enabler: mapDefModel.get('layer_selector') === true
|
||||
},
|
||||
{
|
||||
setting: 'dashboard_menu',
|
||||
label: _t('editor.settings.preview.options.elements.dashboard_menu'),
|
||||
related: 'map',
|
||||
disabled: false,
|
||||
enabler: mapDefModel.get('dashboard_menu') === true
|
||||
},
|
||||
{
|
||||
setting: 'scrollwheel',
|
||||
label: _t('editor.settings.preview.options.elements.scrollwheel'),
|
||||
related: 'map',
|
||||
disabled: false,
|
||||
enabler: mapDefModel.get('scrollwheel') === true
|
||||
}];
|
||||
};
|
||||
9
lib/assets/javascripts/builder/data/mapcaps-collection.js
Executable file
9
lib/assets/javascripts/builder/data/mapcaps-collection.js
Executable file
@@ -0,0 +1,9 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.visDefinitionModel) throw new Error('visDefinitionModel is required');
|
||||
this.url = opts.visDefinitionModel.mapcapsURL();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
var Backbone = require('backbone');
|
||||
var AssetModel = require('./asset-model');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'orgId'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
model: AssetModel,
|
||||
|
||||
url: function (method) {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('organization-assets', method);
|
||||
return baseUrl + '/api/' + version + '/organization/' + this._orgId + '/assets';
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
deselectAll: function (m) {
|
||||
this.each(function (mdl) {
|
||||
if (mdl !== m && mdl.get('state') === 'selected') {
|
||||
mdl.set('state', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
40
lib/assets/javascripts/builder/data/organization-model.js
Executable file
40
lib/assets/javascripts/builder/data/organization-model.js
Executable file
@@ -0,0 +1,40 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
|
||||
/**
|
||||
* Organization info model
|
||||
*
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v1/org';
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
if (!_.isEmpty(this.get('owner'))) {
|
||||
this._ownerModel = new Backbone.Model(
|
||||
_.omit(this.get('owner'), 'organization')
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getOwnerId: function () {
|
||||
return this._ownerModel && this._ownerModel.get('id');
|
||||
},
|
||||
|
||||
getOwnerEmail: function () {
|
||||
return this._ownerModel && this._ownerModel.get('email');
|
||||
},
|
||||
|
||||
isOrgAdmin: function (user) {
|
||||
return this.getOwnerId() === user.get('id') || !!_.find(this.get('admins'), function (u) {
|
||||
return u.id === user.get('id');
|
||||
});
|
||||
}
|
||||
});
|
||||
83
lib/assets/javascripts/builder/data/overlays-definition-collection.js
Executable file
83
lib/assets/javascripts/builder/data/overlays-definition-collection.js
Executable file
@@ -0,0 +1,83 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.visId) throw new Error('visId is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
this._visId = opts.visId;
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('overlays');
|
||||
return baseUrl + '/api/' + version + '/viz/' + this._visId + '/overlays';
|
||||
},
|
||||
|
||||
hasFetched: function () {
|
||||
return this.length > 0;
|
||||
},
|
||||
|
||||
removeOverlay: function (type) {
|
||||
var overlay = this.findWhere({type: type});
|
||||
overlay && overlay.destroy();
|
||||
},
|
||||
|
||||
createOverlay: function (type) {
|
||||
var types = {
|
||||
'fullscreen': this._createFullScreenOverlay,
|
||||
'layer_selector': this._createLayerSelectorOverlay,
|
||||
'search': this._createSearchOverlay,
|
||||
'zoom': this._createZoomOverlay,
|
||||
'logo': this._createLogoOverlay
|
||||
};
|
||||
var fn = types[type];
|
||||
fn && fn.call(this);
|
||||
},
|
||||
|
||||
_createZoomOverlay: function () {
|
||||
var options = {
|
||||
type: 'zoom',
|
||||
order: 6,
|
||||
display: true,
|
||||
x: 20,
|
||||
y: 20
|
||||
};
|
||||
this.create(options);
|
||||
},
|
||||
|
||||
_createLogoOverlay: function () {
|
||||
var options = {
|
||||
type: 'logo',
|
||||
order: 10,
|
||||
display: true,
|
||||
x: 10,
|
||||
y: 40
|
||||
};
|
||||
this.create(options);
|
||||
},
|
||||
|
||||
_createSearchOverlay: function () {
|
||||
var options = {
|
||||
type: 'search',
|
||||
order: 3,
|
||||
display: true,
|
||||
x: 60,
|
||||
y: 20
|
||||
};
|
||||
this.create(options);
|
||||
},
|
||||
|
||||
_createFullScreenOverlay: function () {
|
||||
var options = {
|
||||
type: 'fullscreen',
|
||||
order: 7,
|
||||
display: true,
|
||||
x: 20,
|
||||
y: 172
|
||||
};
|
||||
|
||||
this.create(options);
|
||||
}
|
||||
});
|
||||
303
lib/assets/javascripts/builder/data/permission-model.js
Executable file
303
lib/assets/javascripts/builder/data/permission-model.js
Executable file
@@ -0,0 +1,303 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
var UserModel = require('./user-model');
|
||||
var OrganizationModel = require('./organization-model');
|
||||
var GroupModel = require('./group-model');
|
||||
var ACLItemModel = require('./acl-item-model');
|
||||
var READ_ONLY = 'r';
|
||||
var READ_WRITE = 'rw';
|
||||
|
||||
/**
|
||||
* manages a cartodb permission object, it contains:
|
||||
* - owner: an UserModel instance
|
||||
* - acl: a collection which includes the user and their permission.
|
||||
*
|
||||
* see https://github.com/Vizzuality/cartodb-management/wiki/multiuser-REST-API#permissions-object
|
||||
*
|
||||
* this object is not created to work alone, it should be a member of
|
||||
* an object like visualization table
|
||||
*
|
||||
*/
|
||||
var PermissionModel = module.exports = Backbone.Model.extend({
|
||||
urlRoot: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('perm');
|
||||
return baseUrl + '/api/' + version + '/perm';
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
this.acl = new Backbone.Collection();
|
||||
this.owner = opts._userModel;
|
||||
this._generateOwner();
|
||||
this._generateAcl();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:owner', this._generateOwner, this);
|
||||
this.bind('change:acl', this._generateAcl, this);
|
||||
},
|
||||
|
||||
_generateOwner: function () {
|
||||
if (!this.owner) {
|
||||
this.owner = new UserModel(null, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
}
|
||||
this.owner.set(this.get('owner'));
|
||||
},
|
||||
|
||||
getOwner: function () {
|
||||
return this.owner;
|
||||
},
|
||||
|
||||
_generateAcl: function () {
|
||||
var self = this;
|
||||
this.acl.reset([], { silent: true });
|
||||
_.each(this.get('acl'), function (aclItem) {
|
||||
var model;
|
||||
switch (aclItem.type) {
|
||||
case 'user':
|
||||
model = new UserModel(aclItem.entity, {
|
||||
configModel: self._configModel
|
||||
});
|
||||
break;
|
||||
case 'org':
|
||||
model = new OrganizationModel(aclItem.entity, {
|
||||
configModel: self._configModel
|
||||
});
|
||||
break;
|
||||
case 'group':
|
||||
model = new GroupModel(aclItem.entity, {
|
||||
configModel: self._configModel
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown ACL item type: ' + aclItem.type);
|
||||
}
|
||||
this._grantAccess(model, aclItem.access);
|
||||
}, this);
|
||||
},
|
||||
|
||||
cleanPermissions: function () {
|
||||
this.acl.reset();
|
||||
},
|
||||
|
||||
hasAccess: function (model) {
|
||||
// Having at least read access is the same as having any access
|
||||
return this.hasReadAccess(model);
|
||||
},
|
||||
|
||||
hasReadAccess: function (model) {
|
||||
// If there is a representable ACL item it must be one of at least READ_ONLY access
|
||||
return !!this.findRepresentableAclItem(model);
|
||||
},
|
||||
|
||||
hasWriteAccess: function (model) {
|
||||
var access = Utils.result(this.findRepresentableAclItem(model), 'get', 'access');
|
||||
return access === READ_WRITE;
|
||||
},
|
||||
|
||||
canChangeReadAccess: function (model) {
|
||||
return this._canChangeAccess(model);
|
||||
},
|
||||
|
||||
canChangeWriteAccess: function (model) {
|
||||
return (!model.isBuilder || model.isBuilder()) && this._canChangeAccess(model, function (representableAclItem) {
|
||||
return Utils.result(representableAclItem, 'get', 'access') !== READ_WRITE;
|
||||
});
|
||||
},
|
||||
|
||||
_canChangeAccess: function (model) {
|
||||
var representableAclItem = this.findRepresentableAclItem(model);
|
||||
return this.isOwner(model) || !representableAclItem ||
|
||||
representableAclItem === this._ownAclItem(model) || Utils.result(arguments, 1, representableAclItem) || false;
|
||||
},
|
||||
|
||||
grantWriteAccess: function (model) {
|
||||
this._grantAccess(model, READ_WRITE);
|
||||
},
|
||||
|
||||
grantReadAccess: function (model) {
|
||||
this._grantAccess(model, READ_ONLY);
|
||||
},
|
||||
|
||||
revokeWriteAccess: function (model) {
|
||||
// Effectively "downgrades" to READ_ONLY
|
||||
this.grantReadAccess(model);
|
||||
},
|
||||
|
||||
/**
|
||||
* Revokes access to a set of items
|
||||
* @param {Object} model A single model or an array of models
|
||||
*/
|
||||
revokeAccess: function (model) {
|
||||
var aclItem = this._ownAclItem(model);
|
||||
if (aclItem) {
|
||||
this.acl.remove(aclItem);
|
||||
}
|
||||
},
|
||||
|
||||
isOwner: function (model) {
|
||||
return _.result(this.owner, 'id') === _.result(model, 'id');
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return {
|
||||
entity: this.get('entity'),
|
||||
acl: this.acl.toJSON()
|
||||
};
|
||||
},
|
||||
|
||||
getUsersWithAnyPermission: function () {
|
||||
return this.acl.chain()
|
||||
.filter(this._hasTypeUser)
|
||||
.map(this._getEntity)
|
||||
.value();
|
||||
},
|
||||
|
||||
isSharedWithOrganization: function () {
|
||||
return this.acl.any(this._hasTypeOrg);
|
||||
},
|
||||
|
||||
clone: function () {
|
||||
var attrs = _.clone(this.attributes);
|
||||
delete attrs.id;
|
||||
return new PermissionModel(attrs, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Overwrite this ACL list from other permission object
|
||||
* @param otherPermission {Object} instance of PermissionModel
|
||||
*/
|
||||
overwriteAcl: function (otherPermission) {
|
||||
this.acl.reset(otherPermission.acl.models);
|
||||
},
|
||||
|
||||
// Note that this may return an inherited ACL item
|
||||
// use ._ownAclItem instead if only model's own is wanted (if there is any)
|
||||
findRepresentableAclItem: function (model) {
|
||||
if (this.isOwner(model)) {
|
||||
return this._newAclItem(model, READ_WRITE);
|
||||
} else {
|
||||
var checkList = ['_ownAclItem', '_organizationAclItem', '_mostPrivilegedGroupAclItem'];
|
||||
|
||||
return this._findMostPrivilegedAclItem(checkList, function (fnName) {
|
||||
return this[fnName](model);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_hasTypeUser: function (m) {
|
||||
return m.get('type') === 'user';
|
||||
},
|
||||
|
||||
_getEntity: function (m) {
|
||||
return m.get('entity');
|
||||
},
|
||||
|
||||
_hasTypeOrg: function (m) {
|
||||
return m.get('type') === 'org';
|
||||
},
|
||||
|
||||
_isOrganization: function (object) {
|
||||
return object instanceof OrganizationModel;
|
||||
},
|
||||
|
||||
_ownAclItem: function (model) {
|
||||
if (!model || !_.isFunction(model.isNew)) {
|
||||
console.log('model is required to find an ACL item');
|
||||
}
|
||||
|
||||
if (!model.isNew()) {
|
||||
return this.acl.find(function (aclItem) {
|
||||
return aclItem.get('entity').id === model.id;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_organizationAclItem: function (m) {
|
||||
// try to get the organization from the user groups collection,
|
||||
// then from the user model (Editor),
|
||||
// and finally from the user model (Builder)
|
||||
var org = _.result(m.collection, 'organization') || m.organization || m._organizationModel;
|
||||
|
||||
if (org) {
|
||||
return this._ownAclItem(org);
|
||||
}
|
||||
},
|
||||
|
||||
_mostPrivilegedGroupAclItem: function (m) {
|
||||
var groups = _.result(m.groups, 'models');
|
||||
|
||||
if (groups) {
|
||||
return this._findMostPrivilegedAclItem(groups, this._ownAclItem);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterates over a items in given list using the iteratee, stops and returns when found the ACL item with best access (i.e. READ_WRITE), or the
|
||||
* list is completed.
|
||||
* @param {Array} list
|
||||
* @param {Function} iteratee that takes an item from list and returns an access
|
||||
* iteratee is called in context of this model.
|
||||
* @Return {String} 'r', 'rw', or undefined if there were no access for given item
|
||||
*/
|
||||
_findMostPrivilegedAclItem: function (list, iteratee) {
|
||||
var aclItem;
|
||||
for (var i = 0, x = list[i]; x && Utils.result(aclItem, 'get', 'access') !== READ_WRITE; x = list[++i]) {
|
||||
// Keep last ACL item if iteratee returns nothing
|
||||
aclItem = iteratee.call(this, x) || aclItem;
|
||||
}
|
||||
return aclItem;
|
||||
},
|
||||
|
||||
/**
|
||||
* Grants access to a set of items
|
||||
* @param {Object} model
|
||||
* @param {String} access can take the following values:
|
||||
* - 'r': read only
|
||||
* - 'rw': read and write permission
|
||||
*/
|
||||
_grantAccess: function (model, access) {
|
||||
var aclItem = this._ownAclItem(model);
|
||||
if (aclItem) {
|
||||
aclItem.set('access', access);
|
||||
} else {
|
||||
aclItem = this._newAclItem(model, access);
|
||||
if (aclItem.isValid()) {
|
||||
this.acl.add(aclItem);
|
||||
} else {
|
||||
throw new Error(access + ' is not a valid ACL access');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_newAclItem: function (model, access) {
|
||||
var type;
|
||||
if (model instanceof UserModel) {
|
||||
type = 'user';
|
||||
} else if (model instanceof GroupModel) {
|
||||
type = 'group';
|
||||
} else if (this._isOrganization(model)) {
|
||||
type = 'org';
|
||||
} else {
|
||||
throw new Error('model not recognized as a valid ACL entity ' + model);
|
||||
}
|
||||
|
||||
return new ACLItemModel({
|
||||
type: type,
|
||||
entity: model,
|
||||
access: access
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.READ_ONLY = READ_ONLY;
|
||||
exports.READ_WRITE = READ_WRITE;
|
||||
117
lib/assets/javascripts/builder/data/query-base-model.js
Normal file
117
lib/assets/javascripts/builder/data/query-base-model.js
Normal file
@@ -0,0 +1,117 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
var STATUS = require('./query-base-status');
|
||||
var MAX_GET_LENGTH = 1024;
|
||||
var MAX_REPEATED_ERRORS = 3;
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
initialize: function (attrs, options) {
|
||||
this.repeatedErrors = 0;
|
||||
|
||||
this.listenTo(this, 'change:status', this._onStatusChanged);
|
||||
this.listenTo(this, 'change:ready', this._onReadyChanged);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.sync} abort ongoing request if there is any
|
||||
*/
|
||||
sync: syncAbort,
|
||||
|
||||
getStatusValue: function () {
|
||||
return this.get('status');
|
||||
},
|
||||
|
||||
isInInitialStatus: function () {
|
||||
return this.get('status') === STATUS.initial;
|
||||
},
|
||||
|
||||
isFetched: function () {
|
||||
return this.get('status') === STATUS.fetched;
|
||||
},
|
||||
|
||||
isFetching: function () {
|
||||
return this.get('status') === STATUS.fetching;
|
||||
},
|
||||
|
||||
isErrored: function () {
|
||||
return this.get('status') === STATUS.errored;
|
||||
},
|
||||
|
||||
isUnavailable: function () {
|
||||
return this.get('status') === STATUS.unavailable;
|
||||
},
|
||||
|
||||
isDone: function () {
|
||||
return this.isFetched() || this.isErrored();
|
||||
},
|
||||
|
||||
isInFinalStatus: function () {
|
||||
var finalStatuses = [STATUS.unavailable, STATUS.fetched, STATUS.errored];
|
||||
return _.contains(finalStatuses, this.get('status'));
|
||||
},
|
||||
|
||||
canFetch: function () {
|
||||
var hasQuery = this.hasQuery();
|
||||
var isReady = this.get('ready');
|
||||
|
||||
return hasQuery && isReady;
|
||||
},
|
||||
|
||||
hasQuery: function () {
|
||||
return !!this.get('query');
|
||||
},
|
||||
|
||||
shouldFetch: function () {
|
||||
return this.canFetch() && !this.isFetched() && !this.isFetching() && !this.isErrored();
|
||||
},
|
||||
|
||||
resetFetch: function () {
|
||||
this.set('status', STATUS.unfetched);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.isNew} for this.destroy() to work (not try to send DELETE request)
|
||||
*/
|
||||
isNew: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
hasRepeatedErrors: function () {
|
||||
return this.repeatedErrors >= MAX_REPEATED_ERRORS;
|
||||
},
|
||||
|
||||
_onStatusChanged: function () {
|
||||
if (this.isInFinalStatus()) {
|
||||
this.trigger('inFinalStatus');
|
||||
}
|
||||
},
|
||||
|
||||
_addChangeListener: function () {
|
||||
this.bind('change', this._onChange, this);
|
||||
},
|
||||
|
||||
_removeChangeListener: function () {
|
||||
this.unbind('change', this._onChange, this);
|
||||
},
|
||||
|
||||
_httpMethod: function () {
|
||||
return this._getSqlApiQueryParam().length > MAX_GET_LENGTH
|
||||
? 'POST'
|
||||
: 'GET';
|
||||
},
|
||||
|
||||
_incrementRepeatedError: function () {
|
||||
this.repeatedErrors++;
|
||||
},
|
||||
|
||||
_resetRepeatedError: function () {
|
||||
this.repeatedErrors = 0;
|
||||
},
|
||||
|
||||
_onReadyChanged: function () {
|
||||
if (this.get('ready') && this.shouldFetch()) {
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
8
lib/assets/javascripts/builder/data/query-base-status.js
Normal file
8
lib/assets/javascripts/builder/data/query-base-status.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
initial: 'initial',
|
||||
unavailable: 'unavailable',
|
||||
unfetched: 'unfetched',
|
||||
fetching: 'fetching',
|
||||
fetched: 'fetched',
|
||||
errored: 'errored'
|
||||
};
|
||||
101
lib/assets/javascripts/builder/data/query-column-model.js
Executable file
101
lib/assets/javascripts/builder/data/query-column-model.js
Executable file
@@ -0,0 +1,101 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var DEACTIVATE_MATRIX = {
|
||||
'number': ['date'],
|
||||
'boolean': ['date'],
|
||||
'date': ['boolean']
|
||||
};
|
||||
var DESTRUCTIVE_MATRIX = {
|
||||
'string': {
|
||||
'string': false,
|
||||
'number': true,
|
||||
'date': true,
|
||||
'boolean': true
|
||||
},
|
||||
'number': {
|
||||
'string': false,
|
||||
'number': false,
|
||||
'date': true,
|
||||
'boolean': true
|
||||
},
|
||||
'date': {
|
||||
'string': false,
|
||||
'number': true,
|
||||
'date': false,
|
||||
'boolean': true
|
||||
},
|
||||
'boolean': {
|
||||
'string': false,
|
||||
'number': false,
|
||||
'date': true,
|
||||
'boolean': false
|
||||
}
|
||||
};
|
||||
var NON_EDITABLE_ATTRIBUTES = [
|
||||
'the_geom_webmercator',
|
||||
'cartodb_id',
|
||||
'the_geom'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
url: function () {
|
||||
if (this._tableName) {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('column');
|
||||
var url = baseUrl + '/api/' + version + '/tables/' + this._tableName + '/columns/';
|
||||
|
||||
if (!this.isNew()) {
|
||||
// If name changes, we should point to the "old" name
|
||||
// in order to make the correct call
|
||||
url += this.previous('name') || this.get('name');
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
parse: function (attrs) {
|
||||
return {
|
||||
name: attrs.name,
|
||||
type: attrs.cartodb_type || attrs.type,
|
||||
isNew: false
|
||||
};
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._tableName = opts.tableName;
|
||||
this._configModel = opts.configModel;
|
||||
},
|
||||
|
||||
isNew: function () {
|
||||
return this.get('isNew');
|
||||
},
|
||||
|
||||
isEditable: function () {
|
||||
return !_.contains(NON_EDITABLE_ATTRIBUTES, this.get('name'));
|
||||
},
|
||||
|
||||
// This is relative becuase we could set any value to cartodb_id
|
||||
isCartoDBIDColumn: function () {
|
||||
return this.get('name') === 'cartodb_id' && this.get('type') === 'number';
|
||||
},
|
||||
|
||||
isGeometryColumn: function () {
|
||||
return this.get('type') === 'geometry';
|
||||
}
|
||||
}, {
|
||||
isTypeChangeDestructive: function (type, newType) {
|
||||
return DESTRUCTIVE_MATRIX[type][newType];
|
||||
},
|
||||
|
||||
isTypeChangeAllowed: function (type, newType) {
|
||||
var deactivated = DEACTIVATE_MATRIX[type] || [];
|
||||
deactivated = deactivated.concat([type]);
|
||||
return !_.contains(deactivated, newType);
|
||||
}
|
||||
});
|
||||
78
lib/assets/javascripts/builder/data/query-columns-collection.js
Executable file
78
lib/assets/javascripts/builder/data/query-columns-collection.js
Executable file
@@ -0,0 +1,78 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var QueryColumnModel = require('./query-column-model');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, opts) {
|
||||
var self = opts.collection;
|
||||
return new QueryColumnModel(attrs, {
|
||||
configModel: self._configModel,
|
||||
tableName: self._tableName
|
||||
});
|
||||
},
|
||||
|
||||
url: function () {
|
||||
if (this._tableName) {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('column');
|
||||
return baseUrl + '/api/' + version + '/tables/' + this._tableName + '/columns';
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.querySchemaModel) throw new Error('querySchemaModel is required');
|
||||
|
||||
this._tableName = opts.tableName;
|
||||
this._querySchemaModel = opts.querySchemaModel;
|
||||
this._configModel = opts.configModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('add remove change:type change:name', function () {
|
||||
this.reset();
|
||||
this._querySchemaModel.set('status', 'unfetched');
|
||||
this._querySchemaModel.fetch();
|
||||
}, this);
|
||||
this._querySchemaModel.bind('change:status', function (mdl, status) {
|
||||
if (status === 'fetched') {
|
||||
this.reset(this._querySchemaModel.columnsCollection.toJSON());
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
setTableName: function (name) {
|
||||
if (!name) return;
|
||||
|
||||
if (this._tableName) {
|
||||
this._tableName = name;
|
||||
|
||||
this.each(function (columnModel) {
|
||||
columnModel._tableName = name;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addColumn: function (opts) {
|
||||
opts = opts || {};
|
||||
this.create(
|
||||
{
|
||||
name: 'column_' + new Date().getTime(),
|
||||
type: 'string',
|
||||
isNew: true
|
||||
},
|
||||
_.extend(
|
||||
opts,
|
||||
{
|
||||
wait: true,
|
||||
parse: false
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
165
lib/assets/javascripts/builder/data/query-geometry-model.js
Executable file
165
lib/assets/javascripts/builder/data/query-geometry-model.js
Executable file
@@ -0,0 +1,165 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var BaseModel = require('./query-base-model');
|
||||
var STATUS = require('./query-base-status');
|
||||
/* eslint-disable */
|
||||
var template = _.template("" +
|
||||
"SELECT CASE LOWER(ST_GeometryType(<%= geom_column %>)) " +
|
||||
" WHEN 'st_polygon' THEN 'polygon' " +
|
||||
" WHEN 'st_multipolygon' THEN 'polygon' " +
|
||||
" WHEN 'st_multilinestring' THEN 'line' " +
|
||||
" WHEN 'st_linestring' THEN 'line' " +
|
||||
" WHEN 'st_multipoint' THEN 'point' " +
|
||||
" WHEN 'st_point' THEN 'point' " +
|
||||
" ELSE '' " +
|
||||
" END AS the_geom FROM (<%= sql %>) __wrapped WHERE <%= geom_column %> IS NOT NULL"
|
||||
/* eslint-enable */
|
||||
);
|
||||
|
||||
var THE_GEOM_NOT_FOUND_ERROR = 'column \"the_geom\" does not exist';
|
||||
var PARAMS = {
|
||||
sort_order: 'asc',
|
||||
rows_per_page: 40,
|
||||
page: 0
|
||||
};
|
||||
var GEOMETRIES = {
|
||||
LINE: 'line',
|
||||
POINT: 'point',
|
||||
POLYGON: 'polygon'
|
||||
};
|
||||
|
||||
/**
|
||||
* Model to represent a schema of a SQL query.
|
||||
*/
|
||||
module.exports = BaseModel.extend({
|
||||
|
||||
defaults: {
|
||||
query: '',
|
||||
status: STATUS.initial,
|
||||
simple_geom: '', // , 'point', 'polygon', 'line'
|
||||
ready: false // until true there's no data available on the table(s) used in the query
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
BaseModel.prototype.initialize.call(this, attrs, opts);
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this._addChangeListener();
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return this._configModel.getSqlApiUrl();
|
||||
},
|
||||
|
||||
fetch: function (opts) {
|
||||
if (!this.canFetch()) return;
|
||||
|
||||
this.set('status', STATUS.fetching);
|
||||
|
||||
opts = opts || {};
|
||||
opts.errorCallback = opts && (opts.originalError || opts.error);
|
||||
var successCallback = opts && (opts.success || opts.complete);
|
||||
|
||||
opts.data = _.extend(
|
||||
opts.data || {},
|
||||
{
|
||||
api_key: this._configModel.get('api_key'),
|
||||
q: this._getSqlApiQueryParam(opts.geomColumnName)
|
||||
},
|
||||
PARAMS
|
||||
);
|
||||
|
||||
opts.method = this._httpMethod();
|
||||
|
||||
opts.success = function (model, response) {
|
||||
this._resetRepeatedError();
|
||||
if (successCallback) { successCallback(response); }
|
||||
}.bind(this);
|
||||
|
||||
opts.error = function (model, response) {
|
||||
if (response && response.statusText !== 'abort') {
|
||||
var error = response.responseText ? JSON.parse(response.responseText).error : [];
|
||||
// in case we get an error of the_geom does not exists try with the_geom_webmercator to get the geometry type
|
||||
if (error.length === 1 && error[0] === THE_GEOM_NOT_FOUND_ERROR) {
|
||||
this.fetch(_.extend({}, _.omit(opts, 'success'), { originalError: opts.errorCallback, geomColumnName: 'the_geom_webmercator' }));
|
||||
} else {
|
||||
this._incrementRepeatedError();
|
||||
|
||||
this.set({
|
||||
simple_geom: '',
|
||||
query_errors: error,
|
||||
status: this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable
|
||||
});
|
||||
|
||||
if (opts.errorCallback) {
|
||||
opts.errorCallback({
|
||||
status: response.status || 'Unknown',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
return Backbone.Model.prototype.fetch.call(this, opts);
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
var simpleGeom;
|
||||
|
||||
_.some(r.rows, function (row) {
|
||||
return !!(simpleGeom = row.the_geom); // to stop when found a valid simple geom
|
||||
});
|
||||
|
||||
return {
|
||||
status: STATUS.fetched,
|
||||
simple_geom: simpleGeom || ''
|
||||
};
|
||||
},
|
||||
|
||||
hasValue: function () {
|
||||
throw new Error('QueryGeometryModel.hasValue() is an async operation. Use `.hasValueAsync` instead.');
|
||||
},
|
||||
|
||||
hasValueAsync: function () {
|
||||
var self = this;
|
||||
if (this.isFetched()) {
|
||||
return Promise.resolve(!!this.get('simple_geom'));
|
||||
} else {
|
||||
return new Promise(function (resolve) {
|
||||
self.listenToOnce(self, 'inFinalStatus', function () {
|
||||
resolve(!!self.get('simple_geom'));
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onChange: function () {
|
||||
this._removeChangeListener();
|
||||
|
||||
if (!this.hasChanged('status') && this.get('status') === STATUS.fetching) {
|
||||
// If it is already fetching just redo the fetch with latest attrs
|
||||
// in case the query has changed
|
||||
if (this.hasChanged('query')) {
|
||||
this.fetch();
|
||||
}
|
||||
} else if (this.hasChanged('query') || this.hasChanged('ready')) {
|
||||
this.set('status', this.get('query') ? STATUS.unfetched : STATUS.unavailable, { silent: true });
|
||||
}
|
||||
|
||||
this._addChangeListener();
|
||||
},
|
||||
|
||||
_getSqlApiQueryParam: function (geomColumnName) {
|
||||
return template({
|
||||
geom_column: geomColumnName || 'the_geom',
|
||||
sql: this.get('query')
|
||||
});
|
||||
},
|
||||
|
||||
isPolygon: function () {
|
||||
return this.get('simple_geom') === GEOMETRIES.POLYGON;
|
||||
}
|
||||
});
|
||||
111
lib/assets/javascripts/builder/data/query-row-model.js
Executable file
111
lib/assets/javascripts/builder/data/query-row-model.js
Executable file
@@ -0,0 +1,111 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
// In case that there is already an id attribute
|
||||
idAttribute: '__id',
|
||||
|
||||
url: function () {
|
||||
if (this._tableName && this._configModel) {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('record');
|
||||
var url = baseUrl + '/api/' + version + '/tables/';
|
||||
|
||||
if (this.hasWriteAccess(this._userModel) && !this.isOwner(this._userModel)) {
|
||||
var owner = this._permissionModel.getOwner();
|
||||
var ownerUsername = owner.get('username');
|
||||
|
||||
url += ownerUsername + '.' + this._tableName + '/records';
|
||||
} else {
|
||||
url += this._tableName + '/records';
|
||||
}
|
||||
|
||||
if (!this.isNew()) {
|
||||
url += '/' + this.get('cartodb_id');
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
this.collection = this.collection || {};
|
||||
|
||||
this._tableName = opts.tableName || this.collection._tableName;
|
||||
this._configModel = opts.configModel || this.collection._configModel;
|
||||
this._permissionModel = opts.permissionModel || this.collection._permissionModel;
|
||||
this._userModel = opts.userModel || this.collection._userModel;
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('error', this._onError, this);
|
||||
},
|
||||
|
||||
hasWriteAccess: function (userModel) {
|
||||
if (!userModel || !this._permissionModel) {
|
||||
return false;
|
||||
}
|
||||
return this._permissionModel.hasWriteAccess(userModel);
|
||||
},
|
||||
|
||||
isOwner: function (userModel) {
|
||||
if (!userModel || !this._permissionModel) {
|
||||
return false;
|
||||
}
|
||||
return this._permissionModel.isOwner(userModel);
|
||||
},
|
||||
|
||||
isNew: function () {
|
||||
return !this.has('cartodb_id');
|
||||
},
|
||||
|
||||
isGeomLoaded: function () {
|
||||
try {
|
||||
JSON.parse(this.get('the_geom'));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
fetchRowIfGeomIsNotLoaded: function (callback) {
|
||||
if (this.isGeomLoaded()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetch({
|
||||
success: callback
|
||||
});
|
||||
},
|
||||
|
||||
_onError: function () {
|
||||
this.set(this.previousAttributes());
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
this.trigger('loading', this);
|
||||
Backbone.Model.prototype.sync.apply(this, arguments);
|
||||
},
|
||||
|
||||
parse: function (attrs) {
|
||||
if (!attrs.__id) {
|
||||
attrs.__id = _.uniqueId();
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return _.omit(
|
||||
this.attributes,
|
||||
'__id',
|
||||
'the_geom_webmercator'
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
308
lib/assets/javascripts/builder/data/query-rows-collection.js
Executable file
308
lib/assets/javascripts/builder/data/query-rows-collection.js
Executable file
@@ -0,0 +1,308 @@
|
||||
var Backbone = require('backbone');
|
||||
var QueryRowModel = require('./query-row-model');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
var _ = require('underscore');
|
||||
var STATUS = require('./query-base-status');
|
||||
|
||||
var MAX_GET_LENGTH = 1024;
|
||||
var WRAP_SQL_TEMPLATE = 'select <%= selectedColumns %> from (<%= sql %>) __wrapped';
|
||||
var MAX_REPEATED_ERRORS = 3;
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
defaults: {
|
||||
empty: true
|
||||
},
|
||||
|
||||
DEFAULT_FETCH_OPTIONS: {
|
||||
rows_per_page: 40,
|
||||
sort_order: 'asc',
|
||||
page: 0
|
||||
},
|
||||
|
||||
// Due to a problem how Backbone checks if there is a duplicated model
|
||||
// or not, we can't create the model with a function + its necessary options
|
||||
model: QueryRowModel,
|
||||
|
||||
sync: syncAbort,
|
||||
|
||||
url: function () {
|
||||
return this._configModel.getSqlApiUrl();
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
this._repeatedErrors = 0;
|
||||
this._tableName = opts.tableName;
|
||||
this._querySchemaModel = opts.querySchemaModel;
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this.statusModel = new Backbone.Model({
|
||||
status: STATUS.initial
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._querySchemaModel, 'change:query', this._onQuerySchemaQueryChange);
|
||||
|
||||
if (this._querySchemaModel && this._querySchemaModel.columnsCollection) {
|
||||
this.listenTo(this._querySchemaModel.columnsCollection, 'reset', this._onColumnsCollectionReset);
|
||||
}
|
||||
|
||||
this.listenTo(this.statusModel, 'change:status', this._onStatusChanged);
|
||||
},
|
||||
|
||||
_onColumnsCollectionReset: function () {
|
||||
this._onQuerySchemaQueryChange();
|
||||
|
||||
if (this.shouldFetch()) {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
getStatusValue: function () {
|
||||
return this.statusModel.get('status');
|
||||
},
|
||||
|
||||
isInInitialStatus: function () {
|
||||
return this.getStatusValue() === STATUS.initial;
|
||||
},
|
||||
|
||||
isFetched: function () {
|
||||
return this.getStatusValue() === STATUS.fetched;
|
||||
},
|
||||
|
||||
isErrored: function () {
|
||||
return this.getStatusValue() === STATUS.errored;
|
||||
},
|
||||
|
||||
isFetching: function () {
|
||||
return this.getStatusValue() === STATUS.fetching;
|
||||
},
|
||||
|
||||
isDone: function () {
|
||||
return this.isFetched() || this.isErrored();
|
||||
},
|
||||
|
||||
isInFinalStatus: function () {
|
||||
var finalStatuses = [STATUS.unavailable, STATUS.fetched, STATUS.errored];
|
||||
return _.contains(finalStatuses, this.getStatusValue());
|
||||
},
|
||||
|
||||
isUnavailable: function () {
|
||||
return this.getStatusValue() === STATUS.unavailable;
|
||||
},
|
||||
|
||||
canFetch: function () {
|
||||
var hasQuery = !!this._querySchemaModel.get('query');
|
||||
var isReady = this._querySchemaModel.get('ready');
|
||||
var isFetched = this._querySchemaModel.isFetched();
|
||||
var isErrored = this._querySchemaModel.isErrored();
|
||||
|
||||
return hasQuery && isReady && isFetched && !isErrored;
|
||||
},
|
||||
|
||||
shouldFetch: function () {
|
||||
return !this.isFetched() && !this.isFetching() && this.canFetch() && !this.isErrored();
|
||||
},
|
||||
|
||||
resetFetch: function () {
|
||||
this.statusModel.set('status', STATUS.unfetched);
|
||||
},
|
||||
|
||||
isEmpty: function () {
|
||||
throw new Error('QueryRowsCollection.isEmpty() is an async operation. Use `.isEmptyAsync` instead.');
|
||||
},
|
||||
|
||||
isEmptyAsync: function () {
|
||||
if (this.isInFinalStatus()) {
|
||||
return Promise.resolve(this.size() === 0);
|
||||
} else {
|
||||
return new Promise(function (resolve) {
|
||||
this.listenToOnce(this, 'inFinalStatus', function () {
|
||||
resolve(this.size() === 0);
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
_onStatusChanged: function () {
|
||||
if (this.isInFinalStatus()) {
|
||||
this.trigger('inFinalStatus');
|
||||
}
|
||||
},
|
||||
|
||||
_onQuerySchemaQueryChange: function () {
|
||||
this.statusModel.set('status', 'unfetched');
|
||||
this.reset([], { silent: true });
|
||||
},
|
||||
|
||||
_geometryColumnSQL: function (column) {
|
||||
/* eslint-disable */
|
||||
return [
|
||||
"CASE",
|
||||
"WHEN GeometryType(" + column + ") = 'POINT' THEN",
|
||||
"ST_AsGeoJSON(" + column + ",8)",
|
||||
"WHEN (" + column + " IS NULL) THEN",
|
||||
"NULL",
|
||||
"ELSE",
|
||||
"GeometryType(" + column + ")",
|
||||
"END " + column
|
||||
].join(' ');
|
||||
/* eslint-enable */
|
||||
},
|
||||
|
||||
// return wrapped SQL removing the_geom and the_geom_webmercator
|
||||
// to avoid fetching those columns.
|
||||
// So for a sql like
|
||||
// select * from table the returned value is
|
||||
// select column1, column2, column3... from table
|
||||
_getWrappedSQL: function (excludeColumns) {
|
||||
var self = this;
|
||||
var schema = this._querySchemaModel.columnsCollection.toJSON();
|
||||
|
||||
var selectedColumns = _
|
||||
.chain(schema)
|
||||
.omit(function (item) {
|
||||
return _.contains(excludeColumns, item.name);
|
||||
})
|
||||
.map(function (item, index) {
|
||||
if (item.type === 'geometry') {
|
||||
return self._geometryColumnSQL(item.name);
|
||||
}
|
||||
return '"' + item.name + '"';
|
||||
})
|
||||
.value()
|
||||
.join(',');
|
||||
|
||||
return _.template(WRAP_SQL_TEMPLATE)({
|
||||
selectedColumns: selectedColumns,
|
||||
sql: this._querySchemaModel.get('query')
|
||||
});
|
||||
},
|
||||
|
||||
_httpMethod: function () {
|
||||
return this._querySchemaModel.get('query').length > MAX_GET_LENGTH
|
||||
? 'POST'
|
||||
: 'GET';
|
||||
},
|
||||
|
||||
_incrementRepeatedError: function () {
|
||||
this._repeatedErrors++;
|
||||
},
|
||||
|
||||
_resetRepeatedError: function () {
|
||||
this._repeatedErrors = 0;
|
||||
},
|
||||
|
||||
hasRepeatedErrors: function () {
|
||||
return this._repeatedErrors >= MAX_REPEATED_ERRORS;
|
||||
},
|
||||
|
||||
fetch: function (opts) {
|
||||
opts = opts || {};
|
||||
|
||||
var previousStatus = this.getStatusValue();
|
||||
this.statusModel.set('status', STATUS.fetching);
|
||||
|
||||
var excludeColumns = (opts.data && opts.data.exclude) || [];
|
||||
|
||||
opts = opts || {};
|
||||
opts.data = _.extend(
|
||||
{},
|
||||
this.DEFAULT_FETCH_OPTIONS,
|
||||
opts.data && _.omit(opts.data, 'exclude') || {},
|
||||
{
|
||||
api_key: this._configModel.get('api_key'),
|
||||
q: this._getWrappedSQL(excludeColumns)
|
||||
}
|
||||
);
|
||||
|
||||
opts.method = this._httpMethod();
|
||||
var errorCallback = opts.error;
|
||||
opts.error = function (coll, resp) {
|
||||
if (previousStatus === STATUS.unavailable) {
|
||||
this._incrementRepeatedError();
|
||||
}
|
||||
|
||||
if (resp && resp.error) {
|
||||
this._querySchemaModel.setError(resp.error);
|
||||
}
|
||||
|
||||
this.trigger('fail', coll, resp);
|
||||
this.statusModel.set('status', this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable);
|
||||
errorCallback && errorCallback(coll, resp);
|
||||
}.bind(this);
|
||||
|
||||
var successCallback = opts.success;
|
||||
opts.success = function (coll, resp, options) {
|
||||
if (resp && resp.error) {
|
||||
return opts.error.apply(this, arguments);
|
||||
}
|
||||
|
||||
this._resetRepeatedError();
|
||||
this.statusModel.set('status', STATUS.fetched);
|
||||
successCallback && successCallback(coll, resp);
|
||||
}.bind(this);
|
||||
|
||||
// Needed to reset the whole collection when a fetch is done
|
||||
opts.reset = true;
|
||||
|
||||
return Backbone.Collection.prototype.fetch.call(this, opts);
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
return this._parseWithID(r.rows);
|
||||
},
|
||||
|
||||
reset: function (result, opts) {
|
||||
var items = [];
|
||||
|
||||
if (result && result.rows) {
|
||||
// If reset comes from a fetch, we need to parse the rows
|
||||
items = result.rows;
|
||||
} else {
|
||||
// If it comes directly from a simple reset function
|
||||
items = result;
|
||||
}
|
||||
|
||||
Backbone.Collection.prototype.reset.apply(this, [this._parseWithID(items)]);
|
||||
},
|
||||
|
||||
_parseWithID: function (array) {
|
||||
return _.map(array, function (attrs) {
|
||||
attrs.__id = _.uniqueId();
|
||||
return attrs;
|
||||
});
|
||||
},
|
||||
|
||||
addRow: function (opts) {
|
||||
opts = opts || {};
|
||||
this.create(
|
||||
{
|
||||
__id: _.uniqueId()
|
||||
},
|
||||
_.extend(
|
||||
opts,
|
||||
{
|
||||
wait: true,
|
||||
parse: true
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
setTableName: function (name) {
|
||||
if (!name) return;
|
||||
|
||||
if (this._tableName) {
|
||||
this._tableName = name;
|
||||
|
||||
this.each(function (rowModel) {
|
||||
rowModel._tableName = name;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
175
lib/assets/javascripts/builder/data/query-schema-model.js
Executable file
175
lib/assets/javascripts/builder/data/query-schema-model.js
Executable file
@@ -0,0 +1,175 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var BaseModel = require('./query-base-model');
|
||||
var QueryRowsCollection = require('./query-rows-collection');
|
||||
var SQLUtils = require('builder/helpers/sql-utils');
|
||||
var STATUS = require('./query-base-status');
|
||||
|
||||
var DEFAULT_TABLE_QUERY_TEMPLATE = _.template('SELECT * FROM <%= tableName %>');
|
||||
var WRAPPED_SQL_QUERY_TEMPLATE = _.template('SELECT * FROM (<%= sql %>) __wrapped');
|
||||
|
||||
/**
|
||||
* Model to represent a schema of a SQL query.
|
||||
*/
|
||||
module.exports = BaseModel.extend({
|
||||
|
||||
defaults: {
|
||||
query: '',
|
||||
sort_order: 'asc',
|
||||
rows_per_page: 0,
|
||||
page: 0,
|
||||
status: STATUS.initial,
|
||||
ready: false // until true there's no data available on the table(s) used in the query
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
BaseModel.prototype.initialize.call(this, attrs, opts);
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
this.columnsCollection = new Backbone.Collection([]);
|
||||
this.rowsCollection = new QueryRowsCollection([]);
|
||||
|
||||
if (this.get('status') === STATUS.initial) {
|
||||
this._setStatusPerQueryValue();
|
||||
}
|
||||
|
||||
this._addChangeListener();
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return this._configModel.getSqlApiUrl();
|
||||
},
|
||||
|
||||
fetch: function (opts) {
|
||||
if (!this.canFetch()) return;
|
||||
|
||||
this.set('status', STATUS.fetching);
|
||||
|
||||
opts = opts || {};
|
||||
opts.errorCallback = opts && (opts.originalError || opts.error);
|
||||
var successCallback = opts && (opts.success || opts.complete);
|
||||
|
||||
opts.data = _.extend(
|
||||
opts.data || {},
|
||||
{
|
||||
sort_order: this.get('sort_order'),
|
||||
rows_per_page: this.get('rows_per_page'),
|
||||
page: this.get('page'),
|
||||
api_key: this._configModel.get('api_key'),
|
||||
q: this._getSqlApiQueryParam()
|
||||
}
|
||||
);
|
||||
|
||||
opts.method = this._httpMethod();
|
||||
|
||||
opts.success = function (model, response) {
|
||||
this._resetRepeatedError();
|
||||
successCallback && successCallback(response);
|
||||
}.bind(this);
|
||||
|
||||
opts.error = function (model, response) {
|
||||
if (response && response.statusText !== 'abort') {
|
||||
var error = response.responseText ? JSON.parse(response.responseText).error : [];
|
||||
|
||||
this._incrementRepeatedError();
|
||||
|
||||
this.set({
|
||||
query_errors: error,
|
||||
status: this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable
|
||||
});
|
||||
|
||||
opts.errorCallback && opts.errorCallback({
|
||||
status: response.status || 'Unknown',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
return Backbone.Model.prototype.fetch.call(this, opts);
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
this.rowsCollection.reset(r.rows);
|
||||
|
||||
this.columnsCollection.reset(
|
||||
_.map(r.fields, function (d, name) {
|
||||
return {
|
||||
name: name,
|
||||
type: d.type
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return { status: STATUS.fetched };
|
||||
},
|
||||
|
||||
hasDefaultQueryFor: function (tableName) {
|
||||
if (!this.hasQuery()) {
|
||||
return false;
|
||||
}
|
||||
var defaultQueryForTableName = DEFAULT_TABLE_QUERY_TEMPLATE({ tableName: tableName });
|
||||
return SQLUtils.isSameQuery(this.get('query'), defaultQueryForTableName);
|
||||
},
|
||||
|
||||
getColumnNames: function () {
|
||||
return this.columnsCollection.pluck('name');
|
||||
},
|
||||
|
||||
getColumnType: function (columnName) {
|
||||
return this.isFetched() ? this.columnsCollection.findWhere({ name: columnName }).get('type') : undefined;
|
||||
},
|
||||
|
||||
_onChange: function () {
|
||||
this._removeChangeListener();
|
||||
|
||||
if (!this.hasChanged('status') && this.get('status') === STATUS.fetching) {
|
||||
this.fetch(); // If is already fetching just redo the fetch with latest attrs
|
||||
} else {
|
||||
if (this.hasChanged('query')) {
|
||||
this.columnsCollection.reset();
|
||||
this._setStatusPerQueryValue();
|
||||
}
|
||||
}
|
||||
|
||||
this._addChangeListener();
|
||||
},
|
||||
|
||||
_setStatusPerQueryValue: function () {
|
||||
this.set('status', this.get('query') ? STATUS.unfetched : STATUS.unavailable, { silent: true });
|
||||
},
|
||||
|
||||
setError: function (error) {
|
||||
var queryErrors = this.get('query_errors');
|
||||
if (queryErrors && queryErrors.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._incrementRepeatedError();
|
||||
|
||||
this.set({
|
||||
query_errors: error,
|
||||
status: this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable
|
||||
});
|
||||
},
|
||||
|
||||
// Basically checks if an schema is different than the current one
|
||||
hasDifferentSchemaThan: function (schemaArray) {
|
||||
return !_.every(schemaArray, function (columnObj) {
|
||||
var columnModel = this.columnsCollection.findWhere({ name: columnObj.name });
|
||||
return columnModel && (columnModel.get('type') === columnObj.type || columnObj.type == null);
|
||||
}, this);
|
||||
},
|
||||
|
||||
resetDueToAlteredData: function () {
|
||||
this.set('status', STATUS.unfetched);
|
||||
this.trigger('resetDueToAlteredData');
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
_getSqlApiQueryParam: function () {
|
||||
return WRAPPED_SQL_QUERY_TEMPLATE({
|
||||
sql: this.get('query')
|
||||
});
|
||||
}
|
||||
});
|
||||
53
lib/assets/javascripts/builder/data/state-definition-model.js
Executable file
53
lib/assets/javascripts/builder/data/state-definition-model.js
Executable file
@@ -0,0 +1,53 @@
|
||||
var Backbone = require('backbone');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
|
||||
/**
|
||||
* State definition model.
|
||||
* Used to persist the dashboard state at any time
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.sync} abort ongoing request if there is any
|
||||
*/
|
||||
sync: syncAbort,
|
||||
|
||||
url: function () {
|
||||
return this._visDefinitionModel.stateURL();
|
||||
},
|
||||
|
||||
isNew: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!attrs.json) throw new Error('state json is required');
|
||||
if (!opts.visDefinitionModel) throw new Error('visDefinitionModel is required');
|
||||
|
||||
this._visDefinitionModel = opts.visDefinitionModel;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return {
|
||||
json: this.get('json')
|
||||
};
|
||||
},
|
||||
|
||||
updateState: function (state) {
|
||||
this.save({
|
||||
json: state
|
||||
});
|
||||
},
|
||||
|
||||
setBounds: function (bounds) {
|
||||
this.trigger('boundsSet', bounds);
|
||||
},
|
||||
|
||||
getZoom: function () {
|
||||
return this.get('json').map.zoom;
|
||||
},
|
||||
|
||||
getCenter: function () {
|
||||
return this.get('json').map.center;
|
||||
}
|
||||
});
|
||||
40
lib/assets/javascripts/builder/data/static-asset-model.js
Executable file
40
lib/assets/javascripts/builder/data/static-asset-model.js
Executable file
@@ -0,0 +1,40 @@
|
||||
var _ = require('underscore');
|
||||
var AssetModel = require('./asset-model');
|
||||
|
||||
module.exports = AssetModel.extend({
|
||||
|
||||
defaults: {
|
||||
ext: 'svg',
|
||||
folder: 'maki-icons',
|
||||
host: 'https://s3.amazonaws.com',
|
||||
bucket: 'com.cartodb.users-assets.production',
|
||||
kind: 'marker',
|
||||
name: '',
|
||||
public_url: '',
|
||||
size: '18',
|
||||
state: 'idle'
|
||||
},
|
||||
|
||||
getURLFor: function (name) {
|
||||
var url = this.get('host') + '/' + this.get('bucket') + '/' + this.get('folder') + '/' + name;
|
||||
var size = this.get('size') ? '-' + this.get('size') : '';
|
||||
return url + size + '.' + this.get('ext');
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var attributes = _.clone(this.attributes);
|
||||
|
||||
attributes['public_url'] = this.getURLFor(attributes['icon']);
|
||||
return attributes;
|
||||
},
|
||||
|
||||
get: function (attributeName) {
|
||||
var attribute = this.attributes[attributeName];
|
||||
|
||||
if (attributeName === 'public_url') {
|
||||
attribute = this.getURLFor(this.attributes['icon']);
|
||||
}
|
||||
|
||||
return attribute;
|
||||
}
|
||||
});
|
||||
18
lib/assets/javascripts/builder/data/static-assets-collection.js
Executable file
18
lib/assets/javascripts/builder/data/static-assets-collection.js
Executable file
@@ -0,0 +1,18 @@
|
||||
var StaticAssetModel = require('./static-asset-model');
|
||||
var AssetsCollection = require('./assets-collection');
|
||||
|
||||
module.exports = AssetsCollection.extend({
|
||||
model: StaticAssetModel,
|
||||
|
||||
url: function () {
|
||||
return '';
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
|
||||
},
|
||||
|
||||
parse: function (resp, xhr) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
163
lib/assets/javascripts/builder/data/synchronization-model.js
Executable file
163
lib/assets/javascripts/builder/data/synchronization-model.js
Executable file
@@ -0,0 +1,163 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var MULTIPLIER = 1.2; // Multiply current interval for this number
|
||||
var INTERVAL = 1500; // Interval time between poll checkings
|
||||
var SYNC_GAP = 900000; // Gap (in ms) necessary to perform next synchronization
|
||||
var POLLING_GAP = 15000; // Gap (in seconds) necessary to start polling and checking synchronization
|
||||
|
||||
/**
|
||||
* Synced table model
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
name: '',
|
||||
url: '',
|
||||
state: '',
|
||||
run_at: 0,
|
||||
ran_at: 0,
|
||||
retried_times: 0,
|
||||
interval: 0,
|
||||
error_code: 0,
|
||||
error_message: '',
|
||||
service_name: '',
|
||||
service_item_id: '',
|
||||
content_guessing: true,
|
||||
type_guessing: true
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
var version = this._configModel.urlVersion('synchronizations');
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/' + version + '/synchronizations/';
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this.bind('destroy', function () {
|
||||
this.unset('id');
|
||||
});
|
||||
|
||||
this._checkState();
|
||||
|
||||
this.bind('change:error_code change:error_message change:state', this._checkState, this);
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
var c = _.clone(this.attributes);
|
||||
|
||||
var d = {
|
||||
url: c.url,
|
||||
interval: c.interval,
|
||||
content_guessing: c.content_guessing,
|
||||
type_guessing: c.type_guessing,
|
||||
create_vis: c.create_vis
|
||||
};
|
||||
|
||||
if (c.type === 'remote') {
|
||||
_.extend(d, {
|
||||
remote_visualization_id: c.remote_visualization_id,
|
||||
create_vis: false,
|
||||
value: c.value
|
||||
});
|
||||
}
|
||||
|
||||
if (c.id !== undefined) {
|
||||
d.id = c.id;
|
||||
}
|
||||
|
||||
// Comes from a service?
|
||||
if (c.service_name) {
|
||||
d.service_name = c.service_name;
|
||||
d.service_item_id = c.service_item_id;
|
||||
}
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
_checkState: function () {
|
||||
if (this.get('error_code') || this.get('error_message')) {
|
||||
this.set('state', 'failure');
|
||||
}
|
||||
},
|
||||
|
||||
syncNow: function (callback) {
|
||||
if (!callback) throw new Error('callback is required');
|
||||
|
||||
$.ajax({
|
||||
url: this.url() + '/sync_now',
|
||||
type: 'PUT'
|
||||
}).always(callback);
|
||||
},
|
||||
|
||||
// Checks for poll to finish
|
||||
pollCheck: function (i) {
|
||||
var self = this;
|
||||
var interval = INTERVAL;
|
||||
|
||||
this.pollTimer = setInterval(request, interval);
|
||||
|
||||
function request () {
|
||||
self.destroyCheck();
|
||||
|
||||
self.fetch({
|
||||
error: function (m, e) {
|
||||
self.set({
|
||||
error_message: e.statusText || '',
|
||||
state: 'failure'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
interval = interval * MULTIPLIER;
|
||||
|
||||
self.pollTimer = setInterval(request, interval);
|
||||
}
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
clearInterval(this.pollTimer);
|
||||
},
|
||||
|
||||
isSync: function () {
|
||||
return !this.isNew() && this.get('interval') > 0;
|
||||
},
|
||||
|
||||
isSyncing: function () {
|
||||
return this.get('state') === 'syncing' || this.get('state') === 'queued';
|
||||
},
|
||||
|
||||
canSyncNow: function () {
|
||||
var ranAt = new Date(this.get('ran_at'));
|
||||
var now = new Date();
|
||||
var gap = SYNC_GAP; // Importer needs some time to perform the next sync, set 15 min as default.
|
||||
|
||||
if (this.isSyncing()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (now.getTime() - ranAt.getTime()) > gap;
|
||||
},
|
||||
|
||||
linkToTable: function (tableModel) {
|
||||
var self = this;
|
||||
if (tableModel.has('synchronization')) {
|
||||
this.set(tableModel.get('synchronization'));
|
||||
}
|
||||
|
||||
tableModel.bind('change:synchronization', function () {
|
||||
self.set(tableModel.get('synchronization'));
|
||||
}, tableModel);
|
||||
|
||||
tableModel.bind('destroy', function destroy () {
|
||||
self.unbind(null, null, tableModel);
|
||||
self.destroy();
|
||||
}, tableModel);
|
||||
}
|
||||
}, {
|
||||
SYNC_GAP: SYNC_GAP,
|
||||
POLLING_GAP: POLLING_GAP
|
||||
});
|
||||
25
lib/assets/javascripts/builder/data/table-columns-collection.js
Executable file
25
lib/assets/javascripts/builder/data/table-columns-collection.js
Executable file
@@ -0,0 +1,25 @@
|
||||
var Backbone = require('backbone');
|
||||
var ColumnModel = require('./column-model');
|
||||
|
||||
/**
|
||||
* A collection of table columns
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: ColumnModel,
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
if (!opts.tableModel) throw new Error('tableModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
this._tableModel = opts.tableModel;
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('column');
|
||||
return baseUrl + '/api/' + version + '/tables/' + this._tableModel.get('name') + '/columns';
|
||||
}
|
||||
|
||||
});
|
||||
171
lib/assets/javascripts/builder/data/table-model.js
Executable file
171
lib/assets/javascripts/builder/data/table-model.js
Executable file
@@ -0,0 +1,171 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var TableColumnsCollection = require('./table-columns-collection');
|
||||
var getSimpleGeometryType = require('./get-simple-geometry-type');
|
||||
var PermissionModel = require('./permission-model');
|
||||
var SyncModel = require('./synchronization-model');
|
||||
var TableNameUtils = require('builder/helpers/table-name-utils');
|
||||
|
||||
/**
|
||||
* Model representing a table.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
idAttribute: 'name',
|
||||
|
||||
defaults: {
|
||||
// always not fetched to begin with, since the only way to currently get table data is by an individual request
|
||||
fetched: false,
|
||||
schema: []
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('table');
|
||||
return baseUrl + '/api/' + version + '/tables';
|
||||
},
|
||||
|
||||
parse: function (r, opts) {
|
||||
var configModel = (opts && opts.configModel) || this._configModel;
|
||||
|
||||
// "Sometimes" table presenter returns the needed data within table_visualization
|
||||
// attribute, because it returns the canonical visualization data. So we have to take
|
||||
// the data from there.
|
||||
if (r.table_visualization && !_.isEmpty(r.table_visualization)) {
|
||||
r = _.extend(
|
||||
{},
|
||||
r.table_visualization,
|
||||
{
|
||||
geometry_types: r.geometry_types
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (r.synchronization) {
|
||||
if (!this._syncModel) {
|
||||
this._syncModel = new SyncModel(r.synchronization, {
|
||||
configModel: configModel
|
||||
});
|
||||
} else {
|
||||
this._syncModel.set(r.synchronization);
|
||||
}
|
||||
}
|
||||
|
||||
if (r.permission) {
|
||||
if (!this._permissionModel) {
|
||||
this._permissionModel = new PermissionModel(r.permission, {
|
||||
configModel: configModel
|
||||
});
|
||||
} else {
|
||||
this._permissionModel.set(r.permission);
|
||||
}
|
||||
}
|
||||
|
||||
var attrs = _.defaults({
|
||||
fetched: true
|
||||
}, r);
|
||||
|
||||
var columnsAttrs = _.map(r.schema, function (d) {
|
||||
return {
|
||||
name: d[0],
|
||||
type: d[1]
|
||||
};
|
||||
}, this);
|
||||
|
||||
if (!this.columnsCollection) {
|
||||
this.columnsCollection = new TableColumnsCollection(columnsAttrs, {
|
||||
configModel: configModel,
|
||||
tableModel: this
|
||||
});
|
||||
} else {
|
||||
this.columnsCollection.reset(columnsAttrs);
|
||||
}
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
isOwner: function (userModel) {
|
||||
if (!userModel || !this._permissionModel) {
|
||||
return false;
|
||||
}
|
||||
return this._permissionModel.isOwner(userModel);
|
||||
},
|
||||
|
||||
getUnqualifiedName: function () {
|
||||
return TableNameUtils.getUnqualifiedName(this.get('name'));
|
||||
},
|
||||
|
||||
getOwnerName: function () {
|
||||
var name = this.get('name');
|
||||
if (!name) return null;
|
||||
var tk = name.split('.');
|
||||
if (tk.length === 2) {
|
||||
return tk[0].replace(/"/g, '');
|
||||
} else if (this._permissionModel) {
|
||||
var ownerModel = this._permissionModel.getOwner();
|
||||
return ownerModel.get('username');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
// "user".table -> user.table
|
||||
getUnquotedName: function () {
|
||||
var name = this.get('name');
|
||||
return name && name.replace(/"/g, '');
|
||||
},
|
||||
|
||||
getGeometryType: function () {
|
||||
var types = this.get('geometry_types');
|
||||
var geomTypes = [];
|
||||
if (!_.isArray(types)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (var t in types) {
|
||||
var type = types[t];
|
||||
// when there are rows with no geo type null is returned as geotype
|
||||
if (type) {
|
||||
var a = getSimpleGeometryType(type.toLowerCase());
|
||||
if (a) {
|
||||
geomTypes.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.uniq(geomTypes);
|
||||
},
|
||||
|
||||
getPermissionModel: function () {
|
||||
return this._permissionModel;
|
||||
},
|
||||
|
||||
isReadOnly: function (userModel) {
|
||||
return this.isSync() || !this.hasWriteAccess(userModel);
|
||||
},
|
||||
|
||||
isSync: function () {
|
||||
var syncModel = this.getSyncModel();
|
||||
var isSync = syncModel && syncModel.get('id');
|
||||
|
||||
return !!isSync;
|
||||
},
|
||||
|
||||
hasWriteAccess: function (userModel) {
|
||||
if (!userModel || !this._permissionModel) {
|
||||
return false;
|
||||
}
|
||||
return this._permissionModel.hasWriteAccess(userModel);
|
||||
},
|
||||
|
||||
getSyncModel: function () {
|
||||
return this._syncModel;
|
||||
}
|
||||
|
||||
});
|
||||
87
lib/assets/javascripts/builder/data/tables-collection.js
Executable file
87
lib/assets/javascripts/builder/data/tables-collection.js
Executable file
@@ -0,0 +1,87 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var TableModel = require('./table-model');
|
||||
|
||||
var DEFAULT_FETCH_OPTIONS = {
|
||||
type: 'table',
|
||||
order: 'updated_at',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
exclude_shared: false,
|
||||
exclude_raster: true,
|
||||
tags: '',
|
||||
q: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* A collection that holds Table models
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (d, opts) {
|
||||
var configModel = opts.collection._configModel;
|
||||
return new TableModel(d, {
|
||||
configModel: configModel,
|
||||
parse: true
|
||||
});
|
||||
},
|
||||
|
||||
modelId: function (attrs) {
|
||||
return attrs.name;
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
this._configModel = opts.configModel;
|
||||
this._stats = {};
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var version = this._configModel.urlVersion('visualization');
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/' + version + '/viz';
|
||||
},
|
||||
|
||||
// Overrides the default fetch, to use the internal methods to construct parmas
|
||||
fetch: function (opts) {
|
||||
opts = opts || {
|
||||
data: {
|
||||
// If reaches this code path it's because there were no opts given, i.e. should do a 'full fetch'
|
||||
// Since there is no current way to really do a full fetch let's just set a really high number to get all…
|
||||
// TODO this is obviously bad for organization users, how can we do this differently
|
||||
per_page: 1000
|
||||
}
|
||||
};
|
||||
opts.data = _.extend({}, DEFAULT_FETCH_OPTIONS, opts.data);
|
||||
return Backbone.Collection.prototype.fetch.call(this, opts);
|
||||
},
|
||||
|
||||
getTotalStat: function (attribute) {
|
||||
return this._stats[attribute] || 0;
|
||||
},
|
||||
|
||||
getDefaultParam: function (param) {
|
||||
return DEFAULT_FETCH_OPTIONS[param];
|
||||
},
|
||||
|
||||
parse: function (res) {
|
||||
this._stats = _.omit(res, 'visualizations');
|
||||
|
||||
return _.map(res.visualizations, function (d) {
|
||||
var dt = d.table; // embedded table data in the vis response
|
||||
return {
|
||||
// From embedded table with same key/value
|
||||
id: dt.id,
|
||||
geometry_types: dt.geometry_types,
|
||||
name: dt.name,
|
||||
privacy: dt.privacy,
|
||||
|
||||
// From embedded table with same value, but different key
|
||||
// so re-map them to match what's returned from a GET /tables/t-id
|
||||
rows_counted: dt.row_count,
|
||||
table_size: dt.size,
|
||||
table_type: d.type // table or remote
|
||||
};
|
||||
}, this);
|
||||
}
|
||||
});
|
||||
306
lib/assets/javascripts/builder/data/timezones.js
Normal file
306
lib/assets/javascripts/builder/data/timezones.js
Normal file
@@ -0,0 +1,306 @@
|
||||
module.exports = {
|
||||
'international-date-line-west': {
|
||||
label: _t('timezones.international-date-line-west'),
|
||||
name: 'Etc/GMT+12'
|
||||
},
|
||||
'midway-island-samoa': {
|
||||
label: _t('timezones.midway-island-samoa'),
|
||||
name: 'Pacific/Midway'
|
||||
},
|
||||
'hawaii': {
|
||||
label: _t('timezones.hawaii'),
|
||||
name: 'Pacific/Honolulu'
|
||||
},
|
||||
'alaska': {
|
||||
label: _t('timezones.alaska'),
|
||||
name: 'America/Juneau'
|
||||
},
|
||||
'pacific-time-us-and-canada-tijuana': {
|
||||
label: _t('timezones.pacific-time-us-and-canada-tijuana'),
|
||||
name: 'America/Dawson'
|
||||
},
|
||||
'mountain-time-us-and-canada': {
|
||||
label: _t('timezones.mountain-time-us-and-canada'),
|
||||
name: 'America/Boise'
|
||||
},
|
||||
'chihuahua-la-paz-mazatlan': {
|
||||
label: _t('timezones.chihuahua-la-paz-mazatlan'),
|
||||
name: 'America/Chihuahua'
|
||||
},
|
||||
'arizona': {
|
||||
label: _t('timezones.arizona'),
|
||||
name: 'America/Phoenix'
|
||||
},
|
||||
'central-time-us-and-canada': {
|
||||
label: _t('timezones.central-time-us-and-canada'),
|
||||
name: 'America/Chicago'
|
||||
},
|
||||
'saskatchewan': {
|
||||
label: _t('timezones.saskatchewan'),
|
||||
name: 'America/Regina'
|
||||
},
|
||||
'guadalajara-mexico-city-monterrey': {
|
||||
label: _t('timezones.guadalajara-mexico-city-monterrey'),
|
||||
name: 'America/Mexico_City'
|
||||
},
|
||||
'central-america': {
|
||||
label: _t('timezones.central-america'),
|
||||
name: 'America/Belize'
|
||||
},
|
||||
'eastern-time-us-and-canada': {
|
||||
label: _t('timezones.eastern-time-us-and-canada'),
|
||||
name: 'America/Detroit'
|
||||
},
|
||||
'indiana-east': {
|
||||
label: _t('timezones.indiana-east'),
|
||||
name: 'America/Indiana/Indianapolis'
|
||||
},
|
||||
'bogota-lima-quito': {
|
||||
label: _t('timezones.bogota-lima-quito'),
|
||||
name: 'America/Bogota'
|
||||
},
|
||||
'atlantic-time-canada': {
|
||||
label: _t('timezones.atlantic-time-canada'),
|
||||
name: 'America/Glace_Bay'
|
||||
},
|
||||
'caracas-la-paz': {
|
||||
label: _t('timezones.caracas-la-paz'),
|
||||
name: 'America/Caracas'
|
||||
},
|
||||
'santiago': {
|
||||
label: _t('timezones.santiago'),
|
||||
name: 'America/Santiago'
|
||||
},
|
||||
// 'newfoundland-and-labrador': {
|
||||
// label: _t('timezones.newfoundland-and-labrador'),
|
||||
// name: 'America/St_Johns'
|
||||
// },
|
||||
'brasilia': {
|
||||
label: _t('timezones.brasilia'),
|
||||
name: 'America/Sao_Paulo'
|
||||
},
|
||||
'buenos-aires-georgetown': {
|
||||
label: _t('timezones.buenos-aires-georgetown'),
|
||||
name: 'America/Argentina/Buenos_Aires'
|
||||
},
|
||||
'greenland': {
|
||||
label: _t('timezones.greenland'),
|
||||
name: 'America/Godthab'
|
||||
},
|
||||
'mid-atlantic': {
|
||||
label: _t('timezones.mid-atlantic'),
|
||||
name: 'Etc/GMT+2'
|
||||
},
|
||||
'azores': {
|
||||
label: _t('timezones.azores'),
|
||||
name: 'Atlantic/Azores'
|
||||
},
|
||||
'cape-verde-islands': {
|
||||
label: _t('timezones.cape-verde-islands'),
|
||||
name: 'Atlantic/Cape_Verde'
|
||||
},
|
||||
'gmt-dublin-edinburgh-lisbon-london': {
|
||||
label: _t('timezones.gmt-dublin-edinburgh-lisbon-london'),
|
||||
name: ''
|
||||
},
|
||||
'casablanca-monrovia': {
|
||||
label: _t('timezones.casablanca-monrovia'),
|
||||
name: 'Africa/Casablanca'
|
||||
},
|
||||
'canary-islands': {
|
||||
label: _t('timezones.canary-islands'),
|
||||
name: 'Atlantic/Canary'
|
||||
},
|
||||
'belgrade-bratislava-budapest-ljubljana-prague': {
|
||||
label: _t('timezones.belgrade-bratislava-budapest-ljubljana-prague'),
|
||||
name: 'Europe/Belgrade'
|
||||
},
|
||||
'sarajevo-skopje-warsaw-zagreb': {
|
||||
label: _t('timezones.sarajevo-skopje-warsaw-zagreb'),
|
||||
name: 'Europe/Sarajevo'
|
||||
},
|
||||
'brussels-copenhagen-madrid-paris': {
|
||||
label: _t('timezones.brussels-copenhagen-madrid-paris'),
|
||||
name: 'Europe/Brussels'
|
||||
},
|
||||
'amsterdam-berlin-bern-rome-stockholm-vienna': {
|
||||
label: _t('timezones.amsterdam-berlin-bern-rome-stockholm-vienna'),
|
||||
name: 'Europe/Amsterdam'
|
||||
},
|
||||
'west-central-africa': {
|
||||
label: _t('timezones.west-central-africa'),
|
||||
name: 'Africa/Algiers'
|
||||
},
|
||||
'bucharest': {
|
||||
label: _t('timezones.bucharest'),
|
||||
name: 'Europe/Bucharest'
|
||||
},
|
||||
'cairo': {
|
||||
label: _t('timezones.cairo'),
|
||||
name: 'Africa/Cairo'
|
||||
},
|
||||
'helsinki-kiev-riga-sofia-tallinn-vilnius': {
|
||||
label: _t('timezones.helsinki-kiev-riga-sofia-tallinn-vilnius'),
|
||||
name: 'Europe/Helsinki'
|
||||
},
|
||||
'athens-istanbul-minsk': {
|
||||
label: _t('timezones.athens-istanbul-minsk'),
|
||||
name: 'Europe/Athens'
|
||||
},
|
||||
'jerusalem': {
|
||||
label: _t('timezones.jerusalem'),
|
||||
name: 'Asia/Jerusalem'
|
||||
},
|
||||
'harare-pretoria': {
|
||||
label: _t('timezones.harare-pretoria'),
|
||||
name: 'Africa/Harare'
|
||||
},
|
||||
'moscow-st-petersburg-volgograd': {
|
||||
label: _t('timezones.moscow-st-petersburg-volgograd'),
|
||||
name: 'Europe/Moscow'
|
||||
},
|
||||
'kuwait-riyadh': {
|
||||
label: _t('timezones.kuwait-riyadh'),
|
||||
name: 'Asia/Kuwait'
|
||||
},
|
||||
'nairobi': {
|
||||
label: _t('timezones.nairobi'),
|
||||
name: 'Africa/Nairobi'
|
||||
},
|
||||
'baghdad': {
|
||||
label: _t('timezones.baghdad'),
|
||||
name: 'Asia/Baghdad'
|
||||
},
|
||||
// 'tehran': {
|
||||
// label: _t('timezones.tehran'),
|
||||
// name: 'Asia/Tehran'
|
||||
// },
|
||||
'abu-dhabi-muscat': {
|
||||
label: _t('timezones.abu-dhabi-muscat'),
|
||||
name: 'Asia/Dubai'
|
||||
},
|
||||
'baku-tbilisi-yerevan': {
|
||||
label: _t('timezones.baku-tbilisi-yerevan'),
|
||||
name: 'Asia/Baku'
|
||||
},
|
||||
// 'kabul': {
|
||||
// label: _t('timezones.kabul'),
|
||||
// name: 'Asia/Kabul'
|
||||
// },
|
||||
'ekaterinburg': {
|
||||
label: _t('timezones.ekaterinburg'),
|
||||
name: 'Asia/Yekaterinburg'
|
||||
},
|
||||
'islamabad-karachi-tashkent': {
|
||||
label: _t('timezones.islamabad-karachi-tashkent'),
|
||||
name: 'Asia/Karachi'
|
||||
},
|
||||
// 'chennai-kolkata-mumbai-new-delhi': {
|
||||
// label: _t('timezones.chennai-kolkata-mumbai-new-delhi'),
|
||||
// name: 'Asia/Kolkata'
|
||||
// },
|
||||
// 'kathmandu': {
|
||||
// label: _t('timezones.kathmandu'),
|
||||
// name: 'Asia/Kathmandu'
|
||||
// },
|
||||
'astana-dhaka': {
|
||||
label: _t('timezones.astana-dhaka'),
|
||||
name: 'Asia/Dhaka'
|
||||
},
|
||||
// 'sri-jayawardenepura': {
|
||||
// label: _t('timezones.sri-jayawardenepura'),
|
||||
// name: 'Asia/Colombo'
|
||||
// },
|
||||
'almaty-novosibirsk': {
|
||||
label: _t('timezones.almaty-novosibirsk'),
|
||||
name: 'Asia/Almaty'
|
||||
},
|
||||
// 'yangon-rangoon': {
|
||||
// label: _t('timezones.yangon-rangoon'),
|
||||
// name: 'Asia/Rangoon'
|
||||
// },
|
||||
'bangkok-hanoi-jakarta': {
|
||||
label: _t('timezones.bangkok-hanoi-jakarta'),
|
||||
name: 'Asia/Bangkok'
|
||||
},
|
||||
'krasnoyarsk': {
|
||||
label: _t('timezones.krasnoyarsk'),
|
||||
name: 'Asia/Krasnoyarsk'
|
||||
},
|
||||
'beijing-chongqing-hong-kong-sar-urumqi': {
|
||||
label: _t('timezones.beijing-chongqing-hong-kong-sar-urumqi'),
|
||||
name: 'Asia/Shanghai'
|
||||
},
|
||||
'kuala-lumpur-singapore': {
|
||||
label: _t('timezones.kuala-lumpur-singapore'),
|
||||
name: 'Asia/Kuala_Lumpur'
|
||||
},
|
||||
'taipei': {
|
||||
label: _t('timezones.taipei'),
|
||||
name: 'Asia/Taipei'
|
||||
},
|
||||
'perth': {
|
||||
label: _t('timezones.perth'),
|
||||
name: 'Australia/Perth'
|
||||
},
|
||||
'irkutsk-ulaanbaatar': {
|
||||
label: _t('timezones.irkutsk-ulaanbaatar'),
|
||||
name: 'Asia/Irkutsk'
|
||||
},
|
||||
'seoul': {
|
||||
label: _t('timezones.seoul'),
|
||||
name: 'Asia/Seoul'
|
||||
},
|
||||
'osaka-sapporo-tokyo': {
|
||||
label: _t('timezones.osaka-sapporo-tokyo'),
|
||||
name: 'Asia/Tokyo'
|
||||
},
|
||||
'yakutsk': {
|
||||
label: _t('timezones.yakutsk'),
|
||||
name: 'Asia/Yakutsk'
|
||||
},
|
||||
// 'darwin': {
|
||||
// label: _t('timezones.darwin'),
|
||||
// name: 'Australia/Darwin'
|
||||
// },
|
||||
// 'adelaide': {
|
||||
// label: _t('timezones.adelaide'),
|
||||
// name: 'Australia/Adelaide'
|
||||
// },
|
||||
'canberra-melbourne-sydney': {
|
||||
label: _t('timezones.canberra-melbourne-sydney'),
|
||||
name: 'Australia/Sydney'
|
||||
},
|
||||
'brisbane': {
|
||||
label: _t('timezones.hobart'),
|
||||
name: 'Australia/Brisbane'
|
||||
},
|
||||
'hobart': {
|
||||
label: _t('timezones.hobart'),
|
||||
name: 'Australia/Hobart'
|
||||
},
|
||||
'vladivostok': {
|
||||
label: _t('timezones.vladivostok'),
|
||||
name: 'Asia/Vladivostok'
|
||||
},
|
||||
'guam-port-moresby': {
|
||||
label: _t('timezones.guam-port-moresby'),
|
||||
name: 'Pacific/Guam'
|
||||
},
|
||||
'magadan-solomon-islands-new-caledonia': {
|
||||
label: _t('timezones.magadan-solomon-islands-new-caledonia'),
|
||||
name: 'Asia/Magadan'
|
||||
},
|
||||
'fiji-islands-kamchatka-marshall-islands': {
|
||||
label: _t('timezones.fiji-islands-kamchatka-marshall-islands'),
|
||||
name: 'Pacific/Fiji'
|
||||
},
|
||||
'auckland-wellington': {
|
||||
label: _t('timezones.auckland-wellington'),
|
||||
name: 'Pacific/Auckland'
|
||||
},
|
||||
'nuku-alofa': {
|
||||
label: _t('timezones.nuku-alofa'),
|
||||
name: 'Pacific/Tongatapu'
|
||||
}
|
||||
};
|
||||
91
lib/assets/javascripts/builder/data/undo-manager.js
Executable file
91
lib/assets/javascripts/builder/data/undo-manager.js
Executable file
@@ -0,0 +1,91 @@
|
||||
var _ = require('underscore');
|
||||
var BackboneUndo = require('backbone-undo');
|
||||
var MAXIMUM_STACK_LENGTH = 30;
|
||||
|
||||
module.exports = {
|
||||
init: function (model, opts) {
|
||||
if (!model) throw new Error('model is required to initialize undoManager');
|
||||
|
||||
opts = opts || {};
|
||||
this.model = model;
|
||||
|
||||
this.model._undoManager = this.undoManager = new BackboneUndo({
|
||||
maximumStackLength: opts.maximumStackLength || MAXIMUM_STACK_LENGTH,
|
||||
register: this.model,
|
||||
track: opts.track
|
||||
});
|
||||
|
||||
this._trackEvents();
|
||||
this._addMethods();
|
||||
|
||||
if (!_.isEmpty(opts.history)) {
|
||||
opts.track && this.undoManager.stopTracking();
|
||||
this._addHistory(opts.history);
|
||||
opts.track && this.undoManager.startTracking();
|
||||
}
|
||||
},
|
||||
|
||||
_trackEvents: function () {
|
||||
_.each(['undo', 'redo'], function (eventType) {
|
||||
this.undoManager.bind(eventType, function () {
|
||||
this.trigger(eventType, this.changed, this);
|
||||
}, this.model);
|
||||
}, this);
|
||||
|
||||
this.undoManager.stack.bind('add remove reset', function () {
|
||||
this.trigger('unredoChanged', this.changed, this);
|
||||
}, this.model);
|
||||
},
|
||||
|
||||
_addHistory: function (history) {
|
||||
var stack = this.undoManager.stack;
|
||||
|
||||
_.each(history, function (attrs, i) {
|
||||
if (history[i + 1]) {
|
||||
stack.add({
|
||||
after: history[i + 1],
|
||||
before: attrs,
|
||||
type: 'change',
|
||||
undoTypes: this.undoManager.undoTypes,
|
||||
object: this.model,
|
||||
magicFusionIndex: 0
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
stack.pointer = stack.length - 1;
|
||||
},
|
||||
|
||||
_addMethods: function () {
|
||||
_.extend(
|
||||
this.model,
|
||||
{
|
||||
undo: function () {
|
||||
this._undoManager.undo();
|
||||
},
|
||||
|
||||
redo: function () {
|
||||
this._undoManager.redo();
|
||||
},
|
||||
|
||||
canUndo: function () {
|
||||
return this._undoManager.isAvailable('undo');
|
||||
},
|
||||
|
||||
canRedo: function () {
|
||||
return this._undoManager.isAvailable('redo');
|
||||
},
|
||||
|
||||
getUndoHistory: function () {
|
||||
var list = this._undoManager.stack.toJSON();
|
||||
var data = _.reduce(list, function (memo, attrs, i) {
|
||||
memo.push(attrs.before);
|
||||
return memo;
|
||||
}, [], this);
|
||||
data.push(this.attributes);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
260
lib/assets/javascripts/builder/data/upload-model.js
Executable file
260
lib/assets/javascripts/builder/data/upload-model.js
Executable file
@@ -0,0 +1,260 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var UploadConfig = require('builder/config/upload-config');
|
||||
var Utils = require('builder/helpers/utils.js');
|
||||
var moment = require('moment');
|
||||
require('backbone-model-file-upload');
|
||||
|
||||
/**
|
||||
* Model that allows uploading files to CartoDB endpoints.
|
||||
*
|
||||
* NOTE: this model extends Backbone.Model instead of Backbone.Model, because
|
||||
* it's required for the vendor/backbone-model-file-upload.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v1/imports';
|
||||
},
|
||||
|
||||
fileAttribute: 'filename',
|
||||
|
||||
defaults: {
|
||||
type: '',
|
||||
value: '',
|
||||
interval: 0,
|
||||
privacy: '',
|
||||
progress: 0,
|
||||
state: 'idle',
|
||||
service_item_id: '',
|
||||
service_name: '',
|
||||
option: '',
|
||||
content_guessing: true,
|
||||
type_guessing: true,
|
||||
create_vis: true
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.userModel) throw new Error('userModel is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._userModel = opts.userModel;
|
||||
this._configModel = opts.configModel;
|
||||
this._initBinds();
|
||||
// We need to validate entry attributes
|
||||
this._validate(this.attributes, { validate: true });
|
||||
},
|
||||
|
||||
setUpload: function (d) {
|
||||
this.set(d, {
|
||||
validate: true
|
||||
});
|
||||
},
|
||||
|
||||
isValidToUpload: function () {
|
||||
return this.get('value') && this.get('state') !== 'error';
|
||||
},
|
||||
|
||||
setFresh: function (d) {
|
||||
if (d && !_.isEmpty(d)) {
|
||||
// Set upload properties except create_vis (defined when created)
|
||||
this.set(_.omit(d, 'create_vis'));
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('progress', function (progress) {
|
||||
this.set({
|
||||
progress: progress * 100,
|
||||
state: 'uploading'
|
||||
});
|
||||
}, this);
|
||||
|
||||
this.bind('change:value', function () {
|
||||
if (this.get('state') === 'error') {
|
||||
this.set({ state: 'idle' });
|
||||
this.unset('get_error_text', { silent: true });
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.bind('error invalid', function (m, d) {
|
||||
this.set({
|
||||
state: 'error',
|
||||
error_code: (d && d.error_code) || '',
|
||||
get_error_text: {
|
||||
title: _t('data.upload-model.invalid-import'),
|
||||
what_about: (d && d.msg) || ''
|
||||
}
|
||||
});
|
||||
}, this);
|
||||
},
|
||||
|
||||
validate: function (attrs) {
|
||||
if (!attrs) return;
|
||||
|
||||
if (attrs.type === 'file') {
|
||||
// Number of files
|
||||
if (attrs.value && attrs.value.length) {
|
||||
return {
|
||||
msg: _t('data.upload-model.one-file')
|
||||
};
|
||||
}
|
||||
|
||||
// File name
|
||||
var name = attrs.value.name;
|
||||
if (!name) {
|
||||
return {
|
||||
msg: _t('data.upload-model.file-defined')
|
||||
};
|
||||
}
|
||||
|
||||
// File extension
|
||||
var ext = name.substr(name.lastIndexOf('.') + 1);
|
||||
if (ext) {
|
||||
ext = ext.toLowerCase();
|
||||
}
|
||||
if (!_.contains(UploadConfig.fileExtensions, ext)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.file-extension')
|
||||
};
|
||||
}
|
||||
// File size
|
||||
if ((this._userModel.get('remaining_byte_quota') * UploadConfig.fileTimesBigger) < attrs.value.size) {
|
||||
return {
|
||||
msg: _t('data.upload-model.file-size'),
|
||||
error_code: 8001
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'remote') {
|
||||
// Valid remote visualization id?
|
||||
if (!attrs.remote_visualization_id) {
|
||||
return {
|
||||
msg: _t('data.upload-model.visualization-id')
|
||||
};
|
||||
}
|
||||
// Remote size?
|
||||
if (attrs.size && ((this._userModel.get('remaining_byte_quota') * UploadConfig.fileTimesBigger) < attrs.size)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.remote-file-size'),
|
||||
error_code: 8001
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'url') {
|
||||
// Valid URL?
|
||||
if (!Utils.isURL(attrs.value)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.url-invalid')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'sql') {
|
||||
if (!attrs.value) {
|
||||
return {
|
||||
msg: _t('data.upload-model.query-undefined')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'duplication') {
|
||||
if (!attrs.value) {
|
||||
return {
|
||||
msg: _t('data.upload-model.dataset-copy-undefined')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.type === 'service' && attrs.service_name === 'twitter_search') {
|
||||
var service_item_id = attrs.service_item_id;
|
||||
|
||||
// Empty?
|
||||
if (!service_item_id || _.isEmpty(service_item_id)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-data')
|
||||
};
|
||||
}
|
||||
|
||||
// Categories?
|
||||
if (_.isEmpty(service_item_id.categories)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-categories-invalid')
|
||||
};
|
||||
}
|
||||
|
||||
// Dates?
|
||||
var dates = service_item_id.dates;
|
||||
if (!dates || _.isEmpty(dates)) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-dates-empty')
|
||||
};
|
||||
}
|
||||
var isToDateValid = moment(dates.fromDate) <= moment(new Date());
|
||||
if (!dates.fromDate || !dates.toDate || !isToDateValid) {
|
||||
return {
|
||||
msg: _t('data.upload-model.twitter-dates-invalid')
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isValid: function () {
|
||||
return this.get('value') && this.get('state') !== 'error';
|
||||
},
|
||||
|
||||
upload: function () {
|
||||
if (this.get('type') === 'file') {
|
||||
var self = this;
|
||||
this.xhr = this.save(
|
||||
{
|
||||
filename: this.get('value')
|
||||
},
|
||||
{
|
||||
success: function (m) {
|
||||
m.set('state', 'uploaded');
|
||||
},
|
||||
error: function (m, msg) {
|
||||
var message = _t('data.upload-model.connection-error');
|
||||
|
||||
if (msg && msg.status === 429) {
|
||||
var response = JSON.parse(msg.responseText);
|
||||
message = response.errors.imports;
|
||||
}
|
||||
|
||||
self.set({
|
||||
state: 'error',
|
||||
get_error_text: {
|
||||
title: _t('data.upload-model.error-happened'),
|
||||
what_about: message
|
||||
}
|
||||
});
|
||||
},
|
||||
complete: function () {
|
||||
delete self.xhr;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
stopUpload: function () {
|
||||
if (this.xhr) this.xhr.abort();
|
||||
},
|
||||
|
||||
setGuessing: function (val) {
|
||||
this.set({
|
||||
type_guessing: val,
|
||||
content_guessing: val
|
||||
});
|
||||
},
|
||||
|
||||
setPrivacy: function (val) {
|
||||
this.set('privacy', val);
|
||||
}
|
||||
|
||||
});
|
||||
857
lib/assets/javascripts/builder/data/user-actions.js
Executable file
857
lib/assets/javascripts/builder/data/user-actions.js
Executable file
@@ -0,0 +1,857 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var nodeIds = require('builder/value-objects/analysis-node-ids');
|
||||
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
|
||||
var MetricsTypes = require('builder/components/metrics/metrics-types');
|
||||
|
||||
var SimpleStyleDefaults = require('builder/editor/style/style-defaults/simple-style-defaults');
|
||||
var camshaftReference = require('./camshaft-reference');
|
||||
var layerTypesAndKinds = require('./layer-types-and-kinds');
|
||||
var layerColors = require('./layer-colors');
|
||||
var Notifier = require('builder/components/notifier/notifier');
|
||||
var resetStylePerNode = require('builder/helpers/reset-style-per-node');
|
||||
var Router = require('builder/routes/router');
|
||||
|
||||
var TABLE_ORIGIN = 'table';
|
||||
|
||||
/**
|
||||
* Coordinate side-effects done on explicit interactions.
|
||||
* @param {Object} params
|
||||
* @param {Object} params.userModel
|
||||
* @param {Object} params.analysisDefinitionsCollection
|
||||
* @param {Object} params.analysisDefinitionNodesCollection
|
||||
* @param {Object} params.widgetDefinitionsCollection
|
||||
* @return {Object} that contains all user-actions that the user may do related to a map
|
||||
*/
|
||||
module.exports = function (params) {
|
||||
if (!params.userModel) throw new Error('userModel is required');
|
||||
if (!params.analysisDefinitionsCollection) throw new Error('analysisDefinitionsCollection is required');
|
||||
if (!params.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
|
||||
if (!params.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
|
||||
if (!params.widgetDefinitionsCollection) throw new Error('widgetDefinitionsCollection is required');
|
||||
|
||||
var userModel = params.userModel;
|
||||
var analysisDefinitionsCollection = params.analysisDefinitionsCollection;
|
||||
var analysisDefinitionNodesCollection = params.analysisDefinitionNodesCollection;
|
||||
var layerDefinitionsCollection = params.layerDefinitionsCollection;
|
||||
var widgetDefinitionsCollection = params.widgetDefinitionsCollection;
|
||||
|
||||
var createDefaultCartoDBAttrs = function (oldLayerStyle) {
|
||||
return {
|
||||
kind: 'carto',
|
||||
options: {
|
||||
interactivity: '',
|
||||
tile_style: (oldLayerStyle && oldLayerStyle.options.tile_style) || camshaftReference.getDefaultCartoCSSForType(),
|
||||
cartocss: (oldLayerStyle && oldLayerStyle.options.tile_style) || camshaftReference.getDefaultCartoCSSForType(),
|
||||
style_version: '2.1.1',
|
||||
visible: true,
|
||||
style_properties: (oldLayerStyle && oldLayerStyle.options.style_properties),
|
||||
sql_wrap: (oldLayerStyle && oldLayerStyle.options.sql_wrap)
|
||||
},
|
||||
tooltip: oldLayerStyle ? oldLayerStyle.tooltip : {},
|
||||
infowindow: oldLayerStyle ? oldLayerStyle.infowindow : {}
|
||||
};
|
||||
};
|
||||
|
||||
var deleteOrphanedAnalyses = function () {
|
||||
analysisDefinitionNodesCollection
|
||||
.toArray()
|
||||
.forEach(function (nodeDefModel) {
|
||||
if (!layerDefinitionsCollection.anyContainsNode(nodeDefModel)) {
|
||||
nodeDefModel.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
analysisDefinitionsCollection
|
||||
.toArray()
|
||||
.forEach(function (analysisDefModel) {
|
||||
if (!analysisDefModel.getNodeDefinitionModel()) {
|
||||
analysisDefModel.destroy();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var restoreWidgetsFromLayer = function (affectedWidgets, layerDefModel, callback, callbackOptions) {
|
||||
if (!affectedWidgets || !_.isArray(affectedWidgets)) throw new Error('affectedWidgets is required');
|
||||
if (!layerDefModel) throw new Error('layerDefModel is required');
|
||||
if (!callback) throw new Error('callback is required');
|
||||
|
||||
var notification;
|
||||
var restoringError;
|
||||
var onWidgetFinished = _.after(affectedWidgets.length, function () {
|
||||
if (notification) {
|
||||
if (!restoringError) {
|
||||
notification.set({
|
||||
status: 'success',
|
||||
info: _t('notifications.widgets.restored'),
|
||||
closable: true
|
||||
});
|
||||
} else {
|
||||
Notifier.removeNotification(notification);
|
||||
}
|
||||
}
|
||||
|
||||
callback(layerDefModel, callbackOptions);
|
||||
});
|
||||
|
||||
if (affectedWidgets.length > 0) {
|
||||
notification = Notifier.addNotification({
|
||||
status: 'loading',
|
||||
info: _t('notifications.widgets.restoring'),
|
||||
closable: false
|
||||
});
|
||||
|
||||
// Create widgets with new source and layer_id
|
||||
_.each(affectedWidgets, function (attrs) {
|
||||
attrs.layer_id = attrs.layer_id || layerDefModel.id;
|
||||
|
||||
widgetDefinitionsCollection.create(
|
||||
attrs, {
|
||||
wait: true,
|
||||
success: onWidgetFinished,
|
||||
error: function (e, mdl) {
|
||||
notification = Notifier.addNotification({
|
||||
status: 'error',
|
||||
info: _t('notifications.widgets.error.title') +
|
||||
_t('notifications.widgets.error.body', {
|
||||
body: '',
|
||||
error: mdl && mdl.get('title')
|
||||
}),
|
||||
closable: true
|
||||
});
|
||||
restoringError = true;
|
||||
onWidgetFinished();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
callback(layerDefModel, callbackOptions);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
_resetStylePerNode: function (nodeDefModel, layerDefModel, forceStyleUpdate, resetQueryReady) {
|
||||
resetStylePerNode(nodeDefModel, layerDefModel, forceStyleUpdate, resetQueryReady);
|
||||
},
|
||||
|
||||
saveAnalysis: function (analysisFormModel) {
|
||||
if (!analysisFormModel.isValid()) return;
|
||||
|
||||
var nodeDefModel = analysisDefinitionNodesCollection.get(analysisFormModel.id);
|
||||
|
||||
if (nodeDefModel) {
|
||||
analysisFormModel.updateNodeDefinition(nodeDefModel);
|
||||
var layerDefModel = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
|
||||
MetricsTracker.track(MetricsTypes.MODIFIED_ANALYSIS, {
|
||||
analysis: {
|
||||
id: analysisFormModel.attributes['id'],
|
||||
natural_id: analysisFormModel.attributes['id'],
|
||||
type: analysisFormModel.attributes['type']
|
||||
}
|
||||
});
|
||||
|
||||
this._resetStylePerNode(nodeDefModel, layerDefModel, false, true);
|
||||
} else {
|
||||
nodeDefModel = analysisFormModel.createNodeDefinition(this); // delegate creation to the form, but expect it to call userActions.createAnalysisNode when done with its internal stuff
|
||||
}
|
||||
nodeDefModel.USER_SAVED = true;
|
||||
return nodeDefModel;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new analysis node on a particular layer.
|
||||
* It's assumed to be created on top of an existing node.
|
||||
* @param {Object} nodeAttrs
|
||||
* @param {Object} layerDefModel - instance of layer-definition-model
|
||||
* @return {Object} instance of analysis-definition-node-model
|
||||
*/
|
||||
createAnalysisNode: function (nodeAttrs, layerDefModel) {
|
||||
var nodeDefModel = analysisDefinitionNodesCollection.add(nodeAttrs, { parse: false });
|
||||
|
||||
layerDefModel.save({
|
||||
cartocss: camshaftReference.getDefaultCartoCSSForType(),
|
||||
source: nodeDefModel.id
|
||||
});
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
|
||||
MetricsTracker.track(MetricsTypes.CREATED_ANALYSIS, {
|
||||
analysis: {
|
||||
id: nodeDefModel.attributes['id'],
|
||||
natural_id: nodeDefModel.attributes['id'],
|
||||
type: nodeDefModel.attributes['type']
|
||||
}
|
||||
});
|
||||
|
||||
this._resetStylePerNode(nodeDefModel, layerDefModel);
|
||||
|
||||
return nodeDefModel;
|
||||
},
|
||||
|
||||
saveAnalysisSourceQuery: function (query, nodeDefModel, layerDefModel) {
|
||||
if (!nodeDefModel) throw new Error('nodeDefModel is required');
|
||||
if (nodeDefModel.get('type') !== 'source') throw new Error('nodeDefModel must be a source node');
|
||||
|
||||
nodeDefModel.set('query', query || '');
|
||||
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
this._resetStylePerNode(nodeDefModel, layerDefModel);
|
||||
layerDefModel.save(); // to make sure layer's source ref is persisted
|
||||
},
|
||||
|
||||
saveWidgetOption: function (widgetOptionModel) {
|
||||
if (!widgetOptionModel) throw new Error('widgetOptionModel is required');
|
||||
|
||||
if (widgetOptionModel.analysisDefinitionNodeModel()) { // Might not always have a node-definition, e.g. time-series none-option
|
||||
var layerDefModel = widgetOptionModel.layerDefinitionModel();
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
}
|
||||
|
||||
// delegate back to form model to decide how to save
|
||||
return widgetOptionModel.save(widgetDefinitionsCollection);
|
||||
},
|
||||
|
||||
goToEditWidget: function (widgets) {
|
||||
if (widgets.length === 1) {
|
||||
Router.goToWidget(widgets[0].id);
|
||||
}
|
||||
},
|
||||
|
||||
updateWidgetsOrder: function (widgets) {
|
||||
return widgetDefinitionsCollection.updateWidgetsOrder(widgets);
|
||||
},
|
||||
|
||||
saveWidget: function (widgetDefModel) {
|
||||
if (!widgetDefModel) throw new Error('widgetDefModel is required');
|
||||
|
||||
var widgetLayerId = widgetDefModel.get('layer_id');
|
||||
layerDefinitionsCollection.some(function (layerDefModel) {
|
||||
if (layerDefModel.id === widgetLayerId) {
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
return true; // aborts the "some"-iterator
|
||||
}
|
||||
});
|
||||
|
||||
widgetDefModel.save();
|
||||
},
|
||||
|
||||
deleteAnalysisNode: function (nodeId) {
|
||||
var nodeDefModel = analysisDefinitionNodesCollection.get(nodeId);
|
||||
if (!nodeDefModel) return false; // abort if there is no node-definition; nothing to delete/change
|
||||
if (!nodeDefModel.canBeDeletedByUser()) return false;
|
||||
|
||||
var layerDefModel = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
|
||||
var primarySourceNode = nodeDefModel.getPrimarySource();
|
||||
|
||||
var containsNode = function (m) {
|
||||
return m !== layerDefModel && m !== nodeDefModel && m.containsNode(nodeDefModel);
|
||||
};
|
||||
_
|
||||
.flatten([
|
||||
widgetDefinitionsCollection.filter(containsNode),
|
||||
layerDefinitionsCollection.filter(containsNode),
|
||||
analysisDefinitionsCollection.filter(containsNode),
|
||||
analysisDefinitionNodesCollection.filter(containsNode),
|
||||
nodeDefModel
|
||||
], true)
|
||||
.forEach(function (m) {
|
||||
m.destroy();
|
||||
});
|
||||
|
||||
// Try to restore old styles if we have them, reset them if not
|
||||
var oldNodeStyle = primarySourceNode.getStyleHistoryForLayer(layerDefModel.id);
|
||||
if (oldNodeStyle) {
|
||||
layerDefModel.set({ source: primarySourceNode.id }, { ignoreSchemaChange: true });
|
||||
layerDefModel.styleModel.set(layerDefModel.styleModel.parse(oldNodeStyle.options.style_properties));
|
||||
layerDefModel.set({
|
||||
cartocss: oldNodeStyle.options.tile_style,
|
||||
infowindow: oldNodeStyle.infowindow,
|
||||
tooltip: oldNodeStyle.tooltip,
|
||||
sql_wrap: oldNodeStyle.options.sql_wrap
|
||||
});
|
||||
} else {
|
||||
layerDefModel.set({ source: primarySourceNode.id });
|
||||
}
|
||||
layerDefModel.save();
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
|
||||
MetricsTracker.track(MetricsTypes.DELETED_ANALYSIS, {
|
||||
analysis: {
|
||||
id: nodeDefModel.attributes['id'],
|
||||
natural_id: nodeDefModel.attributes['id'],
|
||||
type: nodeDefModel.attributes['type']
|
||||
}
|
||||
});
|
||||
|
||||
this._resetStylePerNode(primarySourceNode, layerDefModel);
|
||||
|
||||
deleteOrphanedAnalyses();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new layer for a given table name
|
||||
* @param {model} [tableModel]
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.at]
|
||||
*/
|
||||
createLayerFromTable: function (tableModel, options) {
|
||||
options = options || {};
|
||||
|
||||
var tableName = tableModel.getUnqualifiedName();
|
||||
var ownerName = tableModel.getOwnerName() || userModel.get('username');
|
||||
var tableGeometry = tableModel.getGeometryType() && tableModel.getGeometryType()[0];
|
||||
|
||||
// Setting at assumes there to be at least one non-basemap layer
|
||||
var at = options.at || layerDefinitionsCollection.length;
|
||||
var layerOnTop = layerDefinitionsCollection.getLayerOnTop();
|
||||
var layerOnTopPos = layerDefinitionsCollection.indexOf(layerOnTop);
|
||||
var hasTorque = layerDefinitionsCollection.isThereAnyTorqueLayer();
|
||||
var hasLabels = layerOnTop &&
|
||||
!layerTypesAndKinds.isCartoDBType(layerOnTop.get('type')) &&
|
||||
!layerTypesAndKinds.isTorqueType(layerOnTop.get('type'));
|
||||
|
||||
if (hasTorque || hasLabels) {
|
||||
layerOnTop.set('order', layerOnTopPos + 1); // persisted on collection.save in success callback below
|
||||
|
||||
if (hasLabels) { at--; }
|
||||
if (hasTorque) { at--; }
|
||||
}
|
||||
|
||||
var newLetter = layerDefinitionsCollection.nextLetter();
|
||||
var attrs = createDefaultCartoDBAttrs();
|
||||
attrs.letter = newLetter;
|
||||
attrs.options.table_name = tableName;
|
||||
if (ownerName) {
|
||||
attrs.options.user_name = ownerName;
|
||||
}
|
||||
|
||||
if (tableGeometry) {
|
||||
attrs.options.style_properties = {
|
||||
type: 'simple',
|
||||
properties: SimpleStyleDefaults.generateAttributes(tableGeometry)
|
||||
};
|
||||
}
|
||||
|
||||
analysisDefinitionNodesCollection.addRelatedTableData(tableModel.toJSON());
|
||||
|
||||
return layerDefinitionsCollection.create(attrs, {
|
||||
wait: true,
|
||||
at: at,
|
||||
error: options.error,
|
||||
origin: TABLE_ORIGIN,
|
||||
success: function () {
|
||||
layerDefinitionsCollection.each(function (layerDefModel) {
|
||||
if (!layerDefModel.isDataLayer()) return;
|
||||
if (analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel)) return;
|
||||
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
});
|
||||
layerDefinitionsCollection.save();
|
||||
|
||||
var newLayerModel = _.last(layerDefinitionsCollection.models);
|
||||
options.success && options.success(newLayerModel);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* A layer for an existing node have different side-effects depending on the context in which the node exists.
|
||||
* @param {string} nodeid
|
||||
* @param {string} letter of the layer where the node change comes
|
||||
* @param {object} cfg
|
||||
* @param {number} cfg.at
|
||||
*/
|
||||
createLayerForAnalysisNode: function (nodeId, fromLayerLetter, cfg) {
|
||||
var userMaxLayers = userModel.get('limits').max_layers;
|
||||
if (layerDefinitionsCollection.getNumberOfDataLayers() >= userMaxLayers) {
|
||||
var err = new Error('max layers reached');
|
||||
err.userMaxLayers = userMaxLayers;
|
||||
throw err;
|
||||
}
|
||||
|
||||
var nodeDefModel = analysisDefinitionNodesCollection.get(nodeId);
|
||||
if (!nodeDefModel) throw new Error('node with id ' + nodeId + ' does not exist');
|
||||
if (!cfg) throw new Error('cfg is required');
|
||||
if (isNaN(cfg.at) || cfg.at <= 0) throw new Error('cfg.at must be on top of the base layer');
|
||||
|
||||
var newPosition = cfg.at;
|
||||
|
||||
var attrs;
|
||||
var tableName;
|
||||
var tableNameAlias;
|
||||
var ownerName;
|
||||
var oldNodeStyle;
|
||||
var movementType;
|
||||
var newLetter = layerDefinitionsCollection.nextLetter();
|
||||
var affectedWidgetAttrsBySourceChange = [];
|
||||
|
||||
var onNewLayerSaved = function (layer, forceResetStyles) {
|
||||
this._resetStylePerNode(layer.getAnalysisDefinitionNodeModel(), layer, forceResetStyles);
|
||||
layerDefinitionsCollection.save(); // to persist layers order
|
||||
MetricsTracker.track(MetricsTypes.DRAGGED_NODE);
|
||||
}.bind(this);
|
||||
|
||||
var prevLayer = layerDefinitionsCollection.findWhere({ source: nodeId, letter: fromLayerLetter });
|
||||
if (prevLayer) {
|
||||
if (nodeDefModel.hasPrimarySource()) {
|
||||
// Node is head of a layer, e.g. given nodeId A3 it should rename prev layer (A => B), and create a new layer (A)
|
||||
// where the prev layer was to take over its letter identity and its primary source (A2).
|
||||
// The motivation for this is to maintain the layer's state (styles, popup etc.) which really depends on the
|
||||
// last analysis output than the layer itself:
|
||||
// _______ _______ ______
|
||||
// | A | | A | | B | <-- note that B is really A which just got moved & had it's nodes renamed
|
||||
// | | | | | |
|
||||
// | {A3} | => | | | {B1} |
|
||||
// | [A2] | | [A2] | | [A2] |
|
||||
// | [A1] | | [A1] | | |
|
||||
// | [A0] | | [A0] | | |
|
||||
// |______| |______| |______|
|
||||
movementType = '1';
|
||||
var prevOrder = layerDefinitionsCollection.indexOf(prevLayer);
|
||||
var prevLetter = fromLayerLetter;
|
||||
|
||||
// Change identity of prevLayer (A) so it appears as the new layer (B), including its analysis
|
||||
var renamedNodeId = newLetter + '1';
|
||||
var renamedNodeDefModel = nodeDefModel.clone(renamedNodeId); // e.g. A3 => B1
|
||||
analysisDefinitionNodesCollection.invoke('changeSourceIds', nodeId, renamedNodeId);
|
||||
|
||||
// We have to manage all widgets associated with the layer due to the operations we do
|
||||
// creating a new one and managing the previous one
|
||||
var prevLayerNodeIds = _.pluck(prevLayer.ownedPrimaryAnalysisNodes(), 'id');
|
||||
var affectedWidgetsBySourceChange = widgetDefinitionsCollection.filter(function (widgetDefModel) {
|
||||
return _.contains(prevLayerNodeIds, widgetDefModel.get('source'));
|
||||
});
|
||||
|
||||
_.each(affectedWidgetsBySourceChange, function (widgetDefModel) {
|
||||
var attrs;
|
||||
var widgetSourceId = widgetDefModel.get('source');
|
||||
|
||||
// If this widget doesn't point to the "dragged" node, it will
|
||||
// keep the source but the layer_id will be totally different
|
||||
if (widgetSourceId !== nodeId) {
|
||||
attrs = _.extend(
|
||||
_.omit(widgetDefModel.toJSON(), 'id', 'layer_id', 'order'),
|
||||
{
|
||||
avoidNotification: true
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// On the other hand, if there is a widget pointing to the dragged node,
|
||||
// it will keep the layer_id but the source will be different
|
||||
attrs = _.extend(
|
||||
_.omit(widgetDefModel.toJSON(), 'id', 'source', 'order'),
|
||||
{
|
||||
source: {
|
||||
id: renamedNodeId
|
||||
},
|
||||
avoidNotification: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
affectedWidgetAttrsBySourceChange.push(attrs);
|
||||
widgetDefModel.attributes.avoidNotification = true;
|
||||
widgetDefModel.destroy();
|
||||
});
|
||||
|
||||
analysisDefinitionsCollection.newAnalysisForNode(renamedNodeDefModel); // will be saved by saveAnalysisForLayer later since containing that layer's node
|
||||
|
||||
// Modify all layers using the old node as top source and it doesn't have an analysis definition associated
|
||||
layerDefinitionsCollection.each(function (layerDefModel) {
|
||||
var containsAnalysisDefinition = analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel);
|
||||
if (!containsAnalysisDefinition && layerDefModel.get('source') === nodeId) {
|
||||
layerDefModel
|
||||
.set('source', renamedNodeId)
|
||||
.save();
|
||||
}
|
||||
});
|
||||
|
||||
// New layer takes over the identity of the old layer (A), and its primary source as its head node
|
||||
// We apply the styles from the new header node (A2) if available
|
||||
oldNodeStyle = nodeDefModel.getPrimarySource().getStyleHistoryForLayer(prevLayer.id);
|
||||
tableName = prevLayer.get('table_name');
|
||||
tableNameAlias = prevLayer.get('table_name_alias');
|
||||
ownerName = prevLayer.get('user_name') || userModel.get('username');
|
||||
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
|
||||
attrs.options = _.extend({}, attrs.options, {
|
||||
sql: 'SELECT * FROM ' + tableName,
|
||||
table_name: tableName,
|
||||
table_name_alias: tableNameAlias,
|
||||
letter: prevLetter,
|
||||
color: layerColors.getColorForLetter(prevLetter),
|
||||
source: nodeDefModel.getPrimarySource().id
|
||||
});
|
||||
|
||||
if (ownerName) {
|
||||
attrs.options.user_name = ownerName;
|
||||
}
|
||||
|
||||
// Tell the backend that this layer is a copy of the old one, and how to rename the analysis nodes.
|
||||
// This is used to keep the style_history valid in both the old a new layer
|
||||
attrs.from_layer_id = prevLayer.get('id');
|
||||
attrs.from_letter = prevLetter;
|
||||
var newLayerDefModel = layerDefinitionsCollection.add(attrs, { at: prevOrder });
|
||||
var saveAnalysis = analysisDefinitionsCollection.saveAnalysisForLayer(newLayerDefModel);
|
||||
|
||||
$.when(saveAnalysis)
|
||||
.done(function () {
|
||||
// Update the layer after the analyses request
|
||||
prevLayer.save({
|
||||
letter: newLetter,
|
||||
color: layerColors.getColorForLetter(newLetter),
|
||||
source: renamedNodeId
|
||||
}, { ignoreSchemaChange: true });
|
||||
})
|
||||
.fail(function () {
|
||||
Notifier.addNotification({
|
||||
status: 'error',
|
||||
info: _t('notifications.analysis.failed'),
|
||||
closable: true
|
||||
});
|
||||
});
|
||||
|
||||
// Remove and add prevLayer (A), to move the layers to expected positions
|
||||
layerDefinitionsCollection.remove(prevLayer, { silent: true }); // silent to avoid unwanted side-effects; re-added again later
|
||||
layerDefinitionsCollection.add(prevLayer, { at: newPosition });
|
||||
|
||||
// Reset styles from previous layer
|
||||
this._resetStylePerNode(prevLayer.getAnalysisDefinitionNodeModel(), prevLayer);
|
||||
|
||||
newLayerDefModel.save(null, {
|
||||
success: function () {
|
||||
restoreWidgetsFromLayer(affectedWidgetAttrsBySourceChange, newLayerDefModel, onNewLayerSaved, !oldNodeStyle);
|
||||
},
|
||||
error: function () {
|
||||
Notifier.addNotification({
|
||||
status: 'error',
|
||||
info: _t('notifications.layer.error'),
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
});
|
||||
nodeDefModel.destroy(); // replaced by new node
|
||||
} else {
|
||||
// Node is the source of a prevLayer
|
||||
// Create a new layer which simply points to that source
|
||||
// _______ _______ ______
|
||||
// | A | | A | | B |
|
||||
// | | | | | |
|
||||
// | [A0] | => | [A0] | | [A0] |
|
||||
// |______| |______| |______|
|
||||
|
||||
movementType = '2';
|
||||
// Keep the same style in the new layer
|
||||
oldNodeStyle = {
|
||||
options: {
|
||||
tile_style: prevLayer.get('cartocss'),
|
||||
sql_wrap: prevLayer.get('sql_wrap'),
|
||||
style_properties: prevLayer.get('style_properties')
|
||||
},
|
||||
infowindow: prevLayer.get('infowindow'),
|
||||
tooltip: prevLayer.get('tooltip')
|
||||
};
|
||||
tableName = prevLayer.get('table_name');
|
||||
tableNameAlias = prevLayer.get('table_name_alias');
|
||||
ownerName = prevLayer.get('user_name') || userModel.get('username');
|
||||
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
|
||||
attrs.options = _.extend({}, attrs.options, {
|
||||
sql: 'SELECT * FROM ' + tableName,
|
||||
table_name: tableName,
|
||||
table_name_alias: tableNameAlias,
|
||||
letter: newLetter,
|
||||
color: layerColors.getColorForLetter(newLetter),
|
||||
source: nodeId
|
||||
});
|
||||
if (ownerName) {
|
||||
attrs.options.user_name = ownerName;
|
||||
}
|
||||
newLayerDefModel = layerDefinitionsCollection.create(attrs, {
|
||||
at: newPosition,
|
||||
success: function () {
|
||||
onNewLayerSaved(newLayerDefModel, false);
|
||||
},
|
||||
error: function () {
|
||||
Notifier.addNotification({
|
||||
status: 'error',
|
||||
info: _t('notifications.layer.error'),
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Node is NOT a head of a node, e.g. given nodeId is 'a2' this would create a new layer B which takes over the
|
||||
// ownership of the given node and its underlying nodes
|
||||
// _______ _______ ______
|
||||
// | A | | A | | B |
|
||||
// | | | | | |
|
||||
// | [A3] | | [A3] | | {B2} |
|
||||
// | {A2} | => | {B2} | | [B1] |
|
||||
// | [A1] | | | | [B0] |
|
||||
// | [A0] | | | | |
|
||||
// |______| |______| |______|
|
||||
|
||||
movementType = '3';
|
||||
var linkedNodesList = nodeDefModel.linkedListBySameLetter();
|
||||
|
||||
var moveNode = function (oldNode) {
|
||||
var newId = nodeIds.changeLetter(oldNode.id, newLetter);
|
||||
var newNode = oldNode.clone(newId);
|
||||
var affectedWidgetsBySourceChange = widgetDefinitionsCollection.where({ source: oldNode.id });
|
||||
|
||||
analysisDefinitionNodesCollection.invoke('changeSourceIds', oldNode.id, newId);
|
||||
|
||||
_.each(affectedWidgetsBySourceChange, function (m) {
|
||||
// Store attrs from affected widget for creating a new
|
||||
// instance when layer is created
|
||||
affectedWidgetAttrsBySourceChange.push(
|
||||
_.extend(
|
||||
_.omit(m.toJSON(), 'id', 'layer_id', 'source', 'order'),
|
||||
{
|
||||
avoidNotification: true,
|
||||
source: {
|
||||
id: newId
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Destroy widgets pointing to that source until new layer is created
|
||||
m.attributes.avoidNotification = true;
|
||||
m.destroy();
|
||||
});
|
||||
|
||||
return newNode;
|
||||
};
|
||||
var newLayerHeadNode = moveNode(linkedNodesList[0]);
|
||||
_.rest(linkedNodesList).forEach(moveNode);
|
||||
|
||||
// Create the new layer (B). Copy the layer style from the dragged node.
|
||||
var ownerLayer = layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel);
|
||||
oldNodeStyle = nodeDefModel.getStyleHistoryForLayer(ownerLayer.id);
|
||||
tableName = ownerLayer.get('table_name');
|
||||
tableNameAlias = ownerLayer.get('table_name_alias');
|
||||
ownerName = ownerLayer.get('user_name') || userModel.get('username');
|
||||
attrs = createDefaultCartoDBAttrs(oldNodeStyle);
|
||||
attrs.options = _.extend({}, attrs.options, {
|
||||
sql: 'SELECT * FROM ' + tableName,
|
||||
table_name: tableName,
|
||||
table_name_alias: tableNameAlias,
|
||||
letter: newLetter,
|
||||
color: layerColors.getColorForLetter(newLetter),
|
||||
source: newLayerHeadNode.id
|
||||
});
|
||||
if (ownerName) {
|
||||
attrs.options.user_name = ownerName;
|
||||
}
|
||||
|
||||
// Tell the backend that this layer is a copy of the old one, and how to rename the analysis nodes.
|
||||
// This is used to keep the style_history valid in both the old a new layer
|
||||
attrs.from_layer_id = ownerLayer.get('id');
|
||||
attrs.from_letter = ownerLayer.get('letter');
|
||||
newLayerDefModel = layerDefinitionsCollection.add(attrs, { at: newPosition });
|
||||
$.when(analysisDefinitionsCollection.saveAnalysisForLayer(newLayerDefModel))
|
||||
.done(function () {
|
||||
newLayerDefModel.save()
|
||||
.done(function () {
|
||||
restoreWidgetsFromLayer(affectedWidgetAttrsBySourceChange, newLayerDefModel, onNewLayerSaved, !oldNodeStyle);
|
||||
})
|
||||
.fail(function () {
|
||||
Notifier.addNotification({
|
||||
status: 'error',
|
||||
info: _t('notifications.layer.error'),
|
||||
closable: true
|
||||
});
|
||||
});
|
||||
})
|
||||
.fail(function () {
|
||||
Notifier.addNotification({
|
||||
status: 'error',
|
||||
info: _t('notifications.analysis.failed'),
|
||||
closable: true
|
||||
});
|
||||
});
|
||||
nodeDefModel.destroy(); // replaced by new node
|
||||
}
|
||||
|
||||
deleteOrphanedAnalyses();
|
||||
|
||||
window.FS && window.FS.log('log', 'Node dragged out case - ' + movementType);
|
||||
},
|
||||
|
||||
moveLayer: function (d) {
|
||||
var from = d.from;
|
||||
var to = d.to;
|
||||
|
||||
var movingLayer = layerDefinitionsCollection.at(from);
|
||||
layerDefinitionsCollection.remove(movingLayer, { silent: true });
|
||||
layerDefinitionsCollection.add(movingLayer, { at: to, parse: false, silent: true });
|
||||
|
||||
var saveAnalysisPromises = layerDefinitionsCollection
|
||||
.chain()
|
||||
.map(function (layerDefModel) {
|
||||
var nodeDefModel = layerDefModel.getAnalysisDefinitionNodeModel();
|
||||
if (nodeDefModel) {
|
||||
return analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
}
|
||||
})
|
||||
.compact()
|
||||
.value();
|
||||
|
||||
$.when.apply($, saveAnalysisPromises).done(function () { // http://api.jquery.com/jQuery.when/
|
||||
layerDefinitionsCollection.save({
|
||||
success: function () {
|
||||
layerDefinitionsCollection.trigger('layerMoved', movingLayer, from, to);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteLayer: function (id) {
|
||||
var layerToDelete = layerDefinitionsCollection.get(id);
|
||||
if (!layerToDelete.canBeDeletedByUser()) return;
|
||||
|
||||
var parentLayer;
|
||||
var toDestroy = [];
|
||||
|
||||
// Iterate over each node in the list, to decide how to remove dependent objects or fold nodes (if possible)
|
||||
// under another layer's linked nodes list.
|
||||
var linkedNodesList = layerToDelete.ownedPrimaryAnalysisNodes();
|
||||
var nodeDefModel;
|
||||
|
||||
for (var i = 0; i < linkedNodesList.length; i++) {
|
||||
nodeDefModel = linkedNodesList[i];
|
||||
|
||||
parentLayer = layerDefinitionsCollection.findPrimaryParentLayerToAnalysisNode(nodeDefModel, { exclude: toDestroy });
|
||||
|
||||
// No parent layer? delete all dependent objects, since can't move current nodeDefModel elsewhere
|
||||
if (!parentLayer) {
|
||||
toDestroy.push(nodeDefModel);
|
||||
|
||||
layerDefinitionsCollection.each(function (layer) {
|
||||
if (layer.containsNode(nodeDefModel)) {
|
||||
if (!_.contains(toDestroy, layer)) {
|
||||
toDestroy.push(layer);
|
||||
}
|
||||
|
||||
widgetDefinitionsCollection.each(function (widget) {
|
||||
if (!_.contains(toDestroy, widget) && widget.containsNode(nodeDefModel)) {
|
||||
toDestroy.unshift(widget);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
continue; // to process all sequent nodes in the linekd list, and their (possibly) dependent objects
|
||||
}
|
||||
|
||||
var newParentLinkedNodesList = parentLayer
|
||||
.ownedPrimaryAnalysisNodes()
|
||||
.concat(nodeDefModel.linkedListBySameLetter());
|
||||
|
||||
// Since will reassign ids from the start of list need to calculate which sequence id to start on
|
||||
var idSequence = newParentLinkedNodesList.length;
|
||||
var firstSequence = nodeIds.sequence(newParentLinkedNodesList[0].id);
|
||||
|
||||
// If first node has a higher sequence start from that one to avoid issues on renaming nodes
|
||||
if (idSequence <= firstSequence) {
|
||||
idSequence = firstSequence;
|
||||
}
|
||||
|
||||
var lastNode = _.last(newParentLinkedNodesList);
|
||||
if (lastNode.get('type') !== 'source') {
|
||||
idSequence++; // to start on right id sequence, so last item in newParentLinkedNodesList gets assigned x1 as id, since its source will be a none-source node belong to another layer
|
||||
}
|
||||
|
||||
// Reassign ids one-by-one, from start of list to avoid ids overlapping (e.g. [c2,c1]+[b2,b1] = [c4,c3,c2,c1])
|
||||
var prevId = parentLayer.get('letter') + idSequence;
|
||||
var moveNodeToParentLayer = function (node) {
|
||||
var oldNodeId = node.id;
|
||||
var newId = nodeIds.prev(prevId);
|
||||
|
||||
node.set('id', newId);
|
||||
|
||||
// Update any depending objects' source
|
||||
analysisDefinitionNodesCollection.each(function (m) {
|
||||
if (!_.contains(toDestroy, m)) {
|
||||
m.changeSourceIds(oldNodeId, newId, true);
|
||||
}
|
||||
});
|
||||
var maybeUpdateSource = function (m) {
|
||||
if (m.get('source') === oldNodeId && !_.contains(toDestroy, m)) {
|
||||
m.save({ source: newId }, { silent: true });
|
||||
}
|
||||
};
|
||||
layerDefinitionsCollection.each(maybeUpdateSource);
|
||||
widgetDefinitionsCollection.each(maybeUpdateSource);
|
||||
|
||||
prevId = newId;
|
||||
|
||||
return node;
|
||||
};
|
||||
var newParentLayerNode = moveNodeToParentLayer(newParentLinkedNodesList[0]);
|
||||
_.rest(newParentLinkedNodesList).forEach(moveNodeToParentLayer);
|
||||
|
||||
parentLayer.save({ source: newParentLayerNode.id });
|
||||
|
||||
break; // since the remaining nodes have been move to a parent layer
|
||||
}
|
||||
|
||||
if (!_.contains(toDestroy, layerToDelete)) {
|
||||
toDestroy.push(layerToDelete);
|
||||
}
|
||||
|
||||
var promise = $.when.apply($, // http://api.jquery.com/jQuery.when/
|
||||
_.chain(toDestroy)
|
||||
.reduce(function (memo, m) {
|
||||
memo.push(m);
|
||||
var layerDefModel = layerDefinitionsCollection.get(m.id);
|
||||
if (layerDefModel) { // Also delete analyses associated with layers that are being deleted
|
||||
var aDefModel = analysisDefinitionsCollection.findAnalysisForLayer(layerDefModel);
|
||||
if (aDefModel) {
|
||||
memo.push(aDefModel);
|
||||
}
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, [])
|
||||
.unique()
|
||||
.map(function (m) {
|
||||
m.set({ avoidNotification: true }, { silent: true });
|
||||
return m.destroy();
|
||||
})
|
||||
.value());
|
||||
|
||||
if (parentLayer) {
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(parentLayer);
|
||||
}
|
||||
deleteOrphanedAnalyses();
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* E.g. for styles, infowindows etc.
|
||||
* @param {object} layerDefModel - layer-definition-model
|
||||
* @param {object} options
|
||||
*/
|
||||
saveLayer: function (layerDefModel, options) {
|
||||
if (!layerDefModel) throw new Error('layerDefModel is required');
|
||||
|
||||
if (layerDefModel.isDataLayer()) {
|
||||
analysisDefinitionsCollection.saveAnalysisForLayer(layerDefModel);
|
||||
}
|
||||
|
||||
deleteOrphanedAnalyses();
|
||||
|
||||
var saveDefaultOptions = {
|
||||
shouldPreserveAutoStyle: false
|
||||
};
|
||||
options = _.extend({}, saveDefaultOptions, options);
|
||||
|
||||
return layerDefModel.save(null, options);
|
||||
}
|
||||
};
|
||||
};
|
||||
13
lib/assets/javascripts/builder/data/user-groups-collection.js
Executable file
13
lib/assets/javascripts/builder/data/user-groups-collection.js
Executable file
@@ -0,0 +1,13 @@
|
||||
var Backbone = require('backbone');
|
||||
var GroupModel = require('./group-model');
|
||||
|
||||
/**
|
||||
* Collection of a User's groups.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
model: GroupModel,
|
||||
|
||||
initialize: function (models, opts) {
|
||||
this.organization = opts.organization;
|
||||
}
|
||||
});
|
||||
224
lib/assets/javascripts/builder/data/user-model.js
Executable file
224
lib/assets/javascripts/builder/data/user-model.js
Executable file
@@ -0,0 +1,224 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var OrganizationModel = require('./organization-model');
|
||||
var UserGroups = require('./user-groups-collection');
|
||||
var CustomBaselayersCollection = require('./custom-baselayers-collection');
|
||||
/**
|
||||
* User model
|
||||
*
|
||||
*/
|
||||
|
||||
var UserModel = Backbone.Model.extend({
|
||||
urlRoot: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v1/users';
|
||||
},
|
||||
|
||||
defaults: {
|
||||
avatar_url: 'http://cartodb.s3.amazonaws.com/static/public_dashboard_default_avatar.png',
|
||||
username: '',
|
||||
email: ''
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
this._configModel = opts.configModel;
|
||||
attrs = attrs || {};
|
||||
|
||||
if (!_.isEmpty(this.get('organization'))) {
|
||||
this._organizationModel = new OrganizationModel(
|
||||
this.get('organization'),
|
||||
{
|
||||
configModel: this._configModel,
|
||||
currentUserId: this.id
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get('layers')) {
|
||||
this.layers = new CustomBaselayersCollection(this.get('layers'), {
|
||||
configModel: this._configModel,
|
||||
currentUserId: this.id
|
||||
});
|
||||
}
|
||||
|
||||
this.groups = new UserGroups(attrs.groups, {
|
||||
organization: this._organizationModel
|
||||
});
|
||||
},
|
||||
|
||||
isViewer: function () {
|
||||
return this.get('viewer') === true;
|
||||
},
|
||||
|
||||
isBuilder: function () {
|
||||
return !this.isViewer();
|
||||
},
|
||||
|
||||
canCreateDatasets: function () {
|
||||
if (this.hasRemainingByteQuota() && this.hasRemainingTableCount()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
hasRemainingByteQuota: function () {
|
||||
return !(!this.get('remaining_byte_quota') || this.get('remaining_byte_quota') <= 0);
|
||||
},
|
||||
|
||||
isIndividualUser: function () {
|
||||
const proUsers = ['Individual', 'Annual Individual'];
|
||||
return proUsers.indexOf(this.get('account_type')) > -1;
|
||||
},
|
||||
|
||||
hasRemainingTableCount: function () {
|
||||
if (this.isIndividualUser()) {
|
||||
return !(!this.get('table_count') || this.get('table_count') >= this.get('table_quota'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
updateTableCount: function () {
|
||||
var currentTableCount = this.get('table_count');
|
||||
if (currentTableCount) {
|
||||
this.set('table_count', currentTableCount + 1);
|
||||
}
|
||||
},
|
||||
|
||||
hasCreateMapsFeature: function () {
|
||||
return this.isBuilder();
|
||||
},
|
||||
|
||||
hasRemainingPublicMaps: function () {
|
||||
if (this.isIndividualUser()) {
|
||||
return this.get('public_map_quota') > this.getTotalPublicMapsCount();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
getTotalPublicMapsCount: function () {
|
||||
var totalPublicPrivacyMapsCount = this.get('public_privacy_map_count') || 0;
|
||||
var totalPasswordPrivacyMapsCount = this.get('password_privacy_map_count') || 0;
|
||||
var totalLinkPrivacyMapsCount = this.get('link_privacy_map_count') || 0;
|
||||
|
||||
return totalPublicPrivacyMapsCount + totalPasswordPrivacyMapsCount + totalLinkPrivacyMapsCount;
|
||||
},
|
||||
|
||||
canCreateTwitterDataset: function () {
|
||||
var twitter = this.get('twitter');
|
||||
return !(twitter.quota - twitter.monthly_use) <= 0 && twitter.hard_limit;
|
||||
},
|
||||
|
||||
hasOwnTwitterCredentials: function () {
|
||||
var twitter = this.get('twitter');
|
||||
return (twitter && twitter.customized_config) || false;
|
||||
},
|
||||
|
||||
canSelectPremiumOptions: function (visModel) {
|
||||
return this.get('actions')[ visModel.isVisualization() ? 'private_maps' : 'private_tables' ];
|
||||
},
|
||||
|
||||
canStartTrial: function () {
|
||||
return !this.isInsideOrg() && this.get('account_type') === 'FREE' && this.get('table_count') > 0;
|
||||
},
|
||||
|
||||
isActionEnabled: function (action) {
|
||||
return this.get('actions') && this.get('actions')[action];
|
||||
},
|
||||
|
||||
canCreatePrivateDatasets: function () {
|
||||
var actions = this.get('actions');
|
||||
return actions && actions.private_tables;
|
||||
},
|
||||
|
||||
isInsideOrg: function () {
|
||||
if (this._organizationModel) {
|
||||
return !!this._organizationModel.id || this.isOrgOwner();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isOrgOwner: function () {
|
||||
if (this._organizationModel) {
|
||||
return this._organizationModel.getOwnerId() === this.get('id');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isOrgAdmin: function () {
|
||||
if (this._organizationModel) {
|
||||
return this._organizationModel.isOrgAdmin(this);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
hasAccountType: function (accountType) {
|
||||
return this.get('account_type') === accountType;
|
||||
},
|
||||
|
||||
featureEnabled: function (name) {
|
||||
var featureFlags = this.get('feature_flags');
|
||||
if (!featureFlags || featureFlags.length === 0 || !name) {
|
||||
return false;
|
||||
}
|
||||
return _.contains(featureFlags, name);
|
||||
},
|
||||
|
||||
upgradeContactEmail: function () {
|
||||
if (this.isInsideOrg()) {
|
||||
if (this.isOrgOwner()) {
|
||||
return 'enterprise-support@carto.com';
|
||||
} else {
|
||||
return this._organizationModel.getOwnerEmail();
|
||||
}
|
||||
} else {
|
||||
return 'support@carto.com';
|
||||
}
|
||||
},
|
||||
|
||||
nameOrUsername: function () {
|
||||
return this.fullName() || this.get('username');
|
||||
},
|
||||
|
||||
fullName: function () {
|
||||
var name = this.get('name') || '';
|
||||
var lastName = this.get('last_name') || '';
|
||||
if (name || lastName) {
|
||||
return name + (name && lastName ? ' ' : '') + lastName;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
getMaxConcurrentImports: function () {
|
||||
return (this.get('limits') && this.get('limits').concurrent_imports) || 1;
|
||||
},
|
||||
|
||||
getSchemaName: function () {
|
||||
return this.isInsideOrg() ? this.get('username') : 'public';
|
||||
},
|
||||
|
||||
renderData: function (currentUser) {
|
||||
var name = this.get('username');
|
||||
if (currentUser && currentUser.id === this.id) {
|
||||
name = _t('user.you');
|
||||
}
|
||||
return {
|
||||
username: name,
|
||||
avatar_url: this.get('avatar_url')
|
||||
};
|
||||
},
|
||||
|
||||
clone: function () {
|
||||
var attrs = _.clone(this.attributes);
|
||||
delete attrs.id;
|
||||
return new UserModel(attrs, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
},
|
||||
|
||||
getOrganization: function () {
|
||||
return this._organizationModel;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UserModel;
|
||||
39
lib/assets/javascripts/builder/data/user-notifications.js
Executable file
39
lib/assets/javascripts/builder/data/user-notifications.js
Executable file
@@ -0,0 +1,39 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* User Notifications
|
||||
*
|
||||
*/
|
||||
|
||||
var UserNotifications = Backbone.Model.extend({
|
||||
sync: function (method, model, options) {
|
||||
return Backbone.sync('update', model, options);
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v3/notifications/' + this.get('key');
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.key) throw new Error('key is required');
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
this.attributes = _.extend({ notifications: attrs }, { key: opts.key });
|
||||
},
|
||||
|
||||
getKey: function (key) {
|
||||
var notifications = this.get('notifications') || {};
|
||||
return notifications[key];
|
||||
},
|
||||
|
||||
setKey: function (key, value) {
|
||||
var notifications = this.get('notifications') || {};
|
||||
notifications[key] = value;
|
||||
this.set('notifications', notifications);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UserNotifications;
|
||||
35
lib/assets/javascripts/builder/data/users-group-collection.js
Executable file
35
lib/assets/javascripts/builder/data/users-group-collection.js
Executable file
@@ -0,0 +1,35 @@
|
||||
var Backbone = require('backbone');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
|
||||
/**
|
||||
* A collection representing a set of users in a group.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
sync: syncAbort,
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.group) throw new Error('group is required');
|
||||
this.group = opts.group;
|
||||
this.configModel = opts.configModel;
|
||||
this.organization = opts.organization;
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var baseUrl = this.configModel.get('base_url');
|
||||
var version = this.configModel.urlVersion('organizationGroups', method);
|
||||
return baseUrl + '/api/' + version + '/organization/' + this.organization.id + '/groups/' + this.group.id + '/users';
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
this.total_entries = response.total_entries;
|
||||
this.total_user_entries = response.total_user_entries;
|
||||
|
||||
return response.users;
|
||||
},
|
||||
|
||||
totalCount: function () {
|
||||
return this.total_user_entries;
|
||||
}
|
||||
|
||||
});
|
||||
50
lib/assets/javascripts/builder/data/users-group-fetcher.js
Executable file
50
lib/assets/javascripts/builder/data/users-group-fetcher.js
Executable file
@@ -0,0 +1,50 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var UsersGroup = require('./users-group-collection');
|
||||
|
||||
// This module fetch the user for every group present
|
||||
// in the ACL permission collection. The vizjson doesn't
|
||||
// provide and we need it for the sharing stats in the header.
|
||||
|
||||
module.exports = {
|
||||
track: function (opts) {
|
||||
this.acl = opts.acl;
|
||||
this.configModel = opts.configModel;
|
||||
this.userModel = opts.userModel;
|
||||
|
||||
this.acl.on('reset', this.fetchUsers, this);
|
||||
this.fetchUsers();
|
||||
},
|
||||
|
||||
fetchUsers: function () {
|
||||
var self = this;
|
||||
var promises = [];
|
||||
|
||||
promises = _.map(this.acl.where({type: 'group'}), function (group) {
|
||||
var entity = group.get('entity');
|
||||
var deferred = new $.Deferred();
|
||||
|
||||
if (!entity.users || entity.users.length === 0) {
|
||||
entity.users = new UsersGroup([], {
|
||||
group: entity,
|
||||
configModel: self.configModel,
|
||||
organization: self.userModel.getOrganization()
|
||||
});
|
||||
|
||||
entity.users.fetch({
|
||||
success: function () {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return deferred.promise();
|
||||
}, this);
|
||||
|
||||
$.when.apply($, promises).done(function () {
|
||||
self.acl.trigger('fetch');
|
||||
});
|
||||
}
|
||||
};
|
||||
95
lib/assets/javascripts/builder/data/vis-definition-model.js
Executable file
95
lib/assets/javascripts/builder/data/vis-definition-model.js
Executable file
@@ -0,0 +1,95 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var PermissionModel = require('./permission-model');
|
||||
|
||||
/**
|
||||
* Model that represents a visualization (v3)
|
||||
*
|
||||
* Even though a table might be represented as a Visualization in some cases, please use TableModel if you want to work
|
||||
* with the table data instead of adding table-specific methods here.
|
||||
*/
|
||||
|
||||
var PRIVACY_OPTIONS = {
|
||||
public: 'PUBLIC',
|
||||
link: 'LINK',
|
||||
private: 'PRIVATE',
|
||||
password: 'PASSWORD'
|
||||
};
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
visChanges: 0
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
if (this.get('permission')) {
|
||||
this._permissionModel = new PermissionModel(this.get('permission'), {
|
||||
configModel: opts.configModel
|
||||
});
|
||||
this.unset('permission');
|
||||
}
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('visualization');
|
||||
return baseUrl + '/api/' + version + '/viz';
|
||||
},
|
||||
|
||||
mapcapsURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v3/viz/' + this.id + '/mapcaps';
|
||||
},
|
||||
|
||||
embedURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/builder/' + this.id + '/embed';
|
||||
},
|
||||
|
||||
builderURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/builder/' + this.id + '/';
|
||||
},
|
||||
|
||||
vizjsonURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('vizjson', 'read', 'v3');
|
||||
return baseUrl + '/api/' + version + '/viz/' + this.id + '/viz.json';
|
||||
},
|
||||
|
||||
stateURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v3/viz/' + this.id + '/state';
|
||||
},
|
||||
|
||||
isVisualization: function () {
|
||||
return this.get('type') === 'derived';
|
||||
},
|
||||
|
||||
privacyOptions: function () {
|
||||
return _.values(PRIVACY_OPTIONS);
|
||||
},
|
||||
|
||||
recordChange: function () {
|
||||
var visChanges = this.get('visChanges');
|
||||
this.set('visChanges', visChanges + 1);
|
||||
},
|
||||
|
||||
resetChanges: function () {
|
||||
this.set('visChanges', 0);
|
||||
},
|
||||
|
||||
getPermissionModel: function () {
|
||||
return this._permissionModel;
|
||||
}
|
||||
}, {
|
||||
isPubliclyAvailable: function (privacyStatus) {
|
||||
return privacyStatus === PRIVACY_OPTIONS.password ||
|
||||
privacyStatus === PRIVACY_OPTIONS.link ||
|
||||
privacyStatus === PRIVACY_OPTIONS.public;
|
||||
}
|
||||
});
|
||||
22
lib/assets/javascripts/builder/data/vis-metadata-model.js
Executable file
22
lib/assets/javascripts/builder/data/vis-metadata-model.js
Executable file
@@ -0,0 +1,22 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Edit vis metadata dialog model
|
||||
* to control if name and metadata
|
||||
* are editable or not.
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
name: ''
|
||||
},
|
||||
|
||||
validate: function (attrs) {
|
||||
if (!attrs) return;
|
||||
if (!attrs.name) {
|
||||
return _t('components.modals.maps-metadata.validation.error.name');
|
||||
}
|
||||
}
|
||||
});
|
||||
93
lib/assets/javascripts/builder/data/visualization-table-model.js
Executable file
93
lib/assets/javascripts/builder/data/visualization-table-model.js
Executable file
@@ -0,0 +1,93 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var TableModel = require('./table-model');
|
||||
var PermissionModel = require('./permission-model');
|
||||
var SynchronizationModel = require('./synchronization-model');
|
||||
|
||||
/**
|
||||
* Model that represents a table visualization (v3)
|
||||
*
|
||||
*/
|
||||
|
||||
var PRIVACY_OPTIONS = ['PUBLIC', 'LINK', 'PRIVATE'];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
urlRoot: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('visualization');
|
||||
return baseUrl + '/api/' + version + '/viz';
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
this._initModels();
|
||||
},
|
||||
|
||||
_initModels: function () {
|
||||
var d = this.get('table');
|
||||
if (!_.isEmpty(this.get('external_source'))) {
|
||||
d = this.get('external_source');
|
||||
}
|
||||
this._tableModel = new TableModel(d, {
|
||||
configModel: this._configModel,
|
||||
parse: true
|
||||
});
|
||||
|
||||
if (this.get('permission')) {
|
||||
this._permissionModel = new PermissionModel(this.get('permission'), {
|
||||
configModel: this._configModel
|
||||
});
|
||||
this.unset('permission');
|
||||
}
|
||||
|
||||
if (this.get('synchronization')) {
|
||||
this._synchronizationModel = new SynchronizationModel(this.get('synchronization'), {
|
||||
configModel: this._configModel
|
||||
});
|
||||
this._synchronizationModel.linkToTable(this._tableModel);
|
||||
}
|
||||
},
|
||||
|
||||
isVisualization: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
getTableModel: function () {
|
||||
return this._tableModel;
|
||||
},
|
||||
|
||||
getPermissionModel: function () {
|
||||
return this._permissionModel;
|
||||
},
|
||||
|
||||
getSynchronizationModel: function () {
|
||||
return this._synchronizationModel;
|
||||
},
|
||||
|
||||
vizjsonURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var version = this._configModel.urlVersion('vizjson', 'read', 'v3');
|
||||
return baseUrl + '/api/' + version + '/viz/' + this.id + '/viz.json';
|
||||
},
|
||||
|
||||
isRaster: function () {
|
||||
return this.get('kind') === 'raster';
|
||||
},
|
||||
|
||||
datasetURL: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var tableName = this.getTableModel().getUnquotedName();
|
||||
return baseUrl + '/dataset/' + tableName;
|
||||
},
|
||||
|
||||
privacyOptions: function () {
|
||||
return PRIVACY_OPTIONS;
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return _.omit(this.attributes, 'synchronization', 'stats', 'table');
|
||||
}
|
||||
});
|
||||
78
lib/assets/javascripts/builder/data/visualizations-collection.js
Executable file
78
lib/assets/javascripts/builder/data/visualizations-collection.js
Executable file
@@ -0,0 +1,78 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var VisualizationTableModel = require('./visualization-table-model');
|
||||
var VisualizationModel = require('./vis-definition-model');
|
||||
|
||||
/**
|
||||
* A collection that holds visualization models
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
DEFAULT_FETCH_OPTIONS: {
|
||||
type: 'table',
|
||||
order: 'updated_at',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
exclude_shared: false,
|
||||
exclude_raster: true,
|
||||
tags: '',
|
||||
q: ''
|
||||
},
|
||||
|
||||
model: function (d, opts) {
|
||||
var configModel = opts.collection._configModel;
|
||||
var Klass;
|
||||
|
||||
if (d.type !== 'derived') {
|
||||
Klass = VisualizationTableModel;
|
||||
} else {
|
||||
Klass = VisualizationModel;
|
||||
}
|
||||
|
||||
return new Klass(d, {
|
||||
configModel: configModel
|
||||
});
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.configModel) throw new Error('configModel is required');
|
||||
this._configModel = opts.configModel;
|
||||
|
||||
// this.DEFAULT_FETCH_OPTIONS = _.extendOwn(this.DEFAULT_FETCH_OPTIONS, opts);
|
||||
this._stats = {};
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var version = this._configModel.urlVersion('visualization');
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/' + version + '/viz';
|
||||
},
|
||||
|
||||
// Overrides the default fetch, to use the internal methods to construct parmas
|
||||
fetch: function (opts) {
|
||||
this.trigger('loading', this);
|
||||
opts = opts || {
|
||||
data: {
|
||||
// If reaches this code path it's because there were no opts given, i.e. should do a 'full fetch'
|
||||
// Since there is no current way to really do a full fetch let's just set a really high number to get all…
|
||||
// TODO this is obviously bad for organization users, how can we do this differently
|
||||
per_page: 1000
|
||||
}
|
||||
};
|
||||
opts.data = _.extend({}, this.DEFAULT_FETCH_OPTIONS, opts.data);
|
||||
return Backbone.Collection.prototype.fetch.call(this, opts);
|
||||
},
|
||||
|
||||
getDefaultParam: function (param) {
|
||||
return this.DEFAULT_FETCH_OPTIONS[param];
|
||||
},
|
||||
|
||||
getTotalStat: function (attribute) {
|
||||
return this._stats[attribute] || 0;
|
||||
},
|
||||
|
||||
parse: function (res) {
|
||||
this._stats = _.omit(res, 'visualizations');
|
||||
return res.visualizations;
|
||||
}
|
||||
});
|
||||
36
lib/assets/javascripts/builder/data/visualizations-fetch-model.js
Executable file
36
lib/assets/javascripts/builder/data/visualizations-fetch-model.js
Executable file
@@ -0,0 +1,36 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Model that encapsulates params for fetching data in a cdb.admin.Visualizations collection.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
content_type: '',
|
||||
page: 1,
|
||||
q: '',
|
||||
tag: '',
|
||||
category: '',
|
||||
shared: 'no',
|
||||
locked: false,
|
||||
liked: false,
|
||||
library: false,
|
||||
order: 'updated_at',
|
||||
deepInsights: false
|
||||
},
|
||||
|
||||
isSearching: function () {
|
||||
return this.get('q') || this.get('tag');
|
||||
},
|
||||
|
||||
isDatasets: function () {
|
||||
return this.get('content_type') === 'datasets';
|
||||
},
|
||||
|
||||
isMaps: function () {
|
||||
return this.get('content_type') === 'maps';
|
||||
},
|
||||
|
||||
isDeepInsights: function () {
|
||||
return this.isMaps() && this.get('deepInsights');
|
||||
}
|
||||
});
|
||||
208
lib/assets/javascripts/builder/data/widget-definition-model.js
Executable file
208
lib/assets/javascripts/builder/data/widget-definition-model.js
Executable file
@@ -0,0 +1,208 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CartoColor = require('cartocolor');
|
||||
var syncAbort = require('./backbone/sync-abort');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'mapId'
|
||||
];
|
||||
|
||||
var ATTRS_AT_TOP_LEVEL = ['id', 'layer_id', 'source', 'title', 'type', 'order'];
|
||||
var STYLE_PROPERTIES = ['widget_style_definition', 'auto_style_definition', 'auto_style_allowed', 'widget_color_changed'];
|
||||
|
||||
var DEFAULT_WIDGET_COLOR = '#9DE0AD';
|
||||
var TIME_SERIES_WIDGET_COLOR = '#F2CC8F';
|
||||
var DEFAULT_WIDGET_STYLE = {
|
||||
color: {
|
||||
fixed: DEFAULT_WIDGET_COLOR,
|
||||
opacity: 1
|
||||
}
|
||||
};
|
||||
|
||||
var DEFAULT_RAMP_SIZE = 256;
|
||||
|
||||
var getDefaultCategoriesByRange = function (range) {
|
||||
var widgetDomain = [];
|
||||
for (var i = 0, l = range.length; i < l; i++) {
|
||||
widgetDomain.push(_t('form-components.editors.fill.quantification.methods.category') + ' #' + (i + 1));
|
||||
}
|
||||
return widgetDomain;
|
||||
};
|
||||
|
||||
/**
|
||||
* Widget definition Model
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
sync_on_bbox_change: true,
|
||||
auto_style_allowed: true,
|
||||
widget_style_definition: DEFAULT_WIDGET_STYLE,
|
||||
widget_color_changed: false
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {Backbone.prototype.sync} abort ongoing request if there is any
|
||||
*/
|
||||
sync: syncAbort,
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
// Since widget are stored under layers the collection can't figure out the URL by itself,
|
||||
// thus each widget sets its own urlRoot
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
var layerId = this.get('layer_id');
|
||||
return baseUrl + '/api/v3/maps/' + this._mapId + '/layers/' + layerId + '/widgets';
|
||||
},
|
||||
|
||||
/**
|
||||
* The API response format is a little bit different, and may
|
||||
* @param {Object} API response
|
||||
* @return {Object} attrs to be set on model
|
||||
*/
|
||||
parse: function (response) {
|
||||
var attrs = _.defaults(
|
||||
_.pick(response, ATTRS_AT_TOP_LEVEL),
|
||||
response.options
|
||||
);
|
||||
|
||||
if (!_.isEmpty(response.style)) {
|
||||
var style = JSON.parse(JSON.stringify(response.style));
|
||||
if (style.auto_style) {
|
||||
var autoStyle = style.auto_style;
|
||||
var autoStyleDefinition = autoStyle.definition;
|
||||
if (autoStyleDefinition && autoStyleDefinition.color && autoStyleDefinition.color.quantification === 'category') {
|
||||
autoStyle.definition.color.domain = getDefaultCategoriesByRange(autoStyleDefinition.color.range);
|
||||
}
|
||||
attrs.auto_style_definition = autoStyle.definition;
|
||||
attrs.auto_style_allowed = autoStyle.allowed;
|
||||
}
|
||||
attrs.widget_style_definition = response.style.widget_style.definition;
|
||||
attrs.widget_color_changed = response.style.widget_style.widget_color_changed;
|
||||
}
|
||||
|
||||
attrs.source = attrs.source && attrs.source.id;
|
||||
|
||||
// Fetch column type
|
||||
var columnTypeInSource;
|
||||
if (attrs.column && attrs.source) {
|
||||
columnTypeInSource = this.collection.getColumnType(attrs.column, attrs.source);
|
||||
}
|
||||
|
||||
if (columnTypeInSource && columnTypeInSource !== attrs.column_type) {
|
||||
attrs.column_type = columnTypeInSource;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override Backbone.Model.prototype.toJSON
|
||||
* Formats the JSON to match the server-side API
|
||||
*/
|
||||
toJSON: function () {
|
||||
var attributes = _.pick(this.attributes, ATTRS_AT_TOP_LEVEL);
|
||||
attributes.source = {id: this.get('source')};
|
||||
attributes.options = _.omit(this.attributes, ATTRS_AT_TOP_LEVEL, STYLE_PROPERTIES);
|
||||
|
||||
if (this.get('widget_style_definition')) {
|
||||
attributes.style = {
|
||||
widget_style: {
|
||||
definition: this.get('widget_style_definition'),
|
||||
widget_color_changed: this.get('widget_color_changed')
|
||||
}
|
||||
};
|
||||
|
||||
attributes.style.auto_style = {
|
||||
custom: !!this.get('auto_style_definition'),
|
||||
allowed: this.get('auto_style_allowed')
|
||||
};
|
||||
|
||||
if (this.get('auto_style_definition')) {
|
||||
var autoStyleDefinition = JSON.parse(JSON.stringify(this.get('auto_style_definition')));
|
||||
autoStyleDefinition.color = _.omit(autoStyleDefinition.color, 'domain', 'bins');
|
||||
attributes.style.auto_style.definition = autoStyleDefinition;
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
},
|
||||
|
||||
changeType: function (type) {
|
||||
var resetableAttrs = this.collection.resetableAttrsForTypeMap(type);
|
||||
_.each(resetableAttrs, function (attr) {
|
||||
this.unset(attr, { silent: true });
|
||||
}, this);
|
||||
|
||||
var attrsForNewType = _.defaults({ type: type }, this.collection.attrsForThisType(type, this));
|
||||
|
||||
// Unset now irrelevant attributes due to the type change
|
||||
_
|
||||
.chain(this.attributes)
|
||||
.keys()
|
||||
.difference(
|
||||
['sync_on_bbox_change'].concat(ATTRS_AT_TOP_LEVEL, STYLE_PROPERTIES),
|
||||
_.keys(attrsForNewType)
|
||||
)
|
||||
.each(function (key) {
|
||||
this.unset(key, { silent: true });
|
||||
}, this);
|
||||
|
||||
this.set(attrsForNewType);
|
||||
},
|
||||
|
||||
containsNode: function (otherNodeDefModel) {
|
||||
if (!otherNodeDefModel) return false;
|
||||
|
||||
var sourceId = this.get('source');
|
||||
var nodeDefModel = otherNodeDefModel.collection.get(sourceId);
|
||||
|
||||
return !!(sourceId === otherNodeDefModel.id ||
|
||||
nodeDefModel && nodeDefModel.containsNode(otherNodeDefModel));
|
||||
}
|
||||
}, {
|
||||
|
||||
getDefaultWidgetStyle: function (type) {
|
||||
var widgetColor = type === 'time-series' ? TIME_SERIES_WIDGET_COLOR : DEFAULT_WIDGET_COLOR;
|
||||
var defaultWidgetStyle = DEFAULT_WIDGET_STYLE;
|
||||
defaultWidgetStyle.color.fixed = widgetColor;
|
||||
return defaultWidgetStyle;
|
||||
},
|
||||
|
||||
getDefaultAutoStyle: function (type, columnName) {
|
||||
var defaultWidgetCategories = 10;
|
||||
var defaultWidgetRampSize = 7;
|
||||
var widgetRamp = _.first(
|
||||
_.compact(
|
||||
_.map(CartoColor, function (ramp) {
|
||||
return _.clone(ramp[defaultWidgetRampSize]);
|
||||
}, this)
|
||||
)
|
||||
);
|
||||
var widgetCategories = _.clone(CartoColor.Prism[defaultWidgetCategories]);
|
||||
var widgetRange = type === 'category' ? widgetCategories : widgetRamp;
|
||||
var widgetQuantification = type === 'category' ? 'category' : 'quantiles';
|
||||
|
||||
var attrs = {
|
||||
color: {
|
||||
attribute: columnName,
|
||||
quantification: widgetQuantification,
|
||||
range: widgetRange
|
||||
}
|
||||
};
|
||||
|
||||
if (type === 'category') {
|
||||
attrs.color.domain = getDefaultCategoriesByRange(widgetCategories);
|
||||
} else {
|
||||
attrs.color.bins = type === 'time-series'
|
||||
? DEFAULT_RAMP_SIZE
|
||||
: defaultWidgetRampSize;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
});
|
||||
220
lib/assets/javascripts/builder/data/widget-definitions-collection.js
Executable file
220
lib/assets/javascripts/builder/data/widget-definitions-collection.js
Executable file
@@ -0,0 +1,220 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var WidgetDefinitionModel = require('./widget-definition-model');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'mapId',
|
||||
'layerDefinitionsCollection',
|
||||
'analysisDefinitionNodesCollection'
|
||||
];
|
||||
|
||||
/**
|
||||
* Collection of widget definitions, synhronizes the internal definitions and the widget models.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
comparator: 'order',
|
||||
|
||||
model: function (attrs, opts) {
|
||||
var self = opts.collection;
|
||||
attrs.order = !_.isUndefined(attrs.order) ? attrs.order : 0;
|
||||
var model = new WidgetDefinitionModel(attrs, {
|
||||
parse: true, // make sure data is structured as expected
|
||||
collection: self,
|
||||
configModel: self._configModel,
|
||||
mapId: self._mapId
|
||||
});
|
||||
return model;
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
var self = this;
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._bindLayerStyleChanges();
|
||||
|
||||
this._attrsForThisTypeMap = {
|
||||
formula: function (model) {
|
||||
return {
|
||||
column: model.get('column'),
|
||||
operation: model.get('operation') || 'max'
|
||||
};
|
||||
},
|
||||
category: function (model) {
|
||||
var columnName = model.get('column');
|
||||
return {
|
||||
aggregation: model.get('aggregation') || 'count',
|
||||
aggregation_column: model.get('aggregation_column') || columnName,
|
||||
column: columnName,
|
||||
widget_style_definition: model.get('widget_style_definition') || WidgetDefinitionModel.getDefaultWidgetStyle('category'),
|
||||
auto_style_definition: undefined
|
||||
};
|
||||
},
|
||||
histogram: function (model) {
|
||||
return {
|
||||
column: model.get('column'),
|
||||
bins: model.get('bins') || 10,
|
||||
column_type: 'number',
|
||||
widget_style_definition: model.get('widget_style_definition') || WidgetDefinitionModel.getDefaultWidgetStyle('histogram'),
|
||||
auto_style_definition: undefined
|
||||
};
|
||||
},
|
||||
'time-series': function (model) {
|
||||
return {
|
||||
column: model.get('column'),
|
||||
bins: model.get('bins') || 256,
|
||||
aggregation: model.get('aggregation'),
|
||||
column_type: self.getColumnType(model.get('column'), model.get('source')),
|
||||
widget_style_definition: model.get('widget_style_definition') || WidgetDefinitionModel.getDefaultWidgetStyle('time-series')
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var baseUrl = this._configModel.get('base_url');
|
||||
return baseUrl + '/api/v3/maps/' + this._mapId + '/widgets';
|
||||
},
|
||||
|
||||
parse: function (response) {
|
||||
return response && response.widgets
|
||||
? response.widgets
|
||||
: [];
|
||||
},
|
||||
|
||||
_bindLayerStyleChanges: function () {
|
||||
this._layerDefinitionsCollection.bind('change:style_properties', this._onLayerStyleChanged, this);
|
||||
},
|
||||
|
||||
_onLayerStyleChanged: function (layerDefModel) {
|
||||
var styleModel = layerDefModel.styleModel;
|
||||
var isAllowed = styleModel && styleModel.canApplyAutoStyle();
|
||||
var affectedWidgets = this.where({ layer_id: layerDefModel.id });
|
||||
|
||||
_.each(affectedWidgets, function (widgetDefModel) {
|
||||
var isNowAllowed = widgetDefModel.get('auto_style_allowed');
|
||||
var willBeAllowed = isAllowed;
|
||||
var attrs = {
|
||||
auto_style_allowed: willBeAllowed
|
||||
};
|
||||
|
||||
if (isNowAllowed !== willBeAllowed) {
|
||||
// if autostyle is not allow, we should clean auto_style_definition
|
||||
if (willBeAllowed === false) {
|
||||
attrs = _.extend(attrs, { auto_style_definition: '' });
|
||||
}
|
||||
widgetDefModel.save(attrs);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addWidget: function (widgetModel, attrs) {
|
||||
var self = this;
|
||||
var ORDER_NUMBER_TO_APPEAR_ON_TOP = -1;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
_.extend(attrs, { order: ORDER_NUMBER_TO_APPEAR_ON_TOP });
|
||||
|
||||
self.create(attrs, {
|
||||
wait: true,
|
||||
success: function (model) {
|
||||
resolve(model);
|
||||
},
|
||||
error: function (response, error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateWidgetsOrder: function (numberOfWidgets) {
|
||||
this.each(function (widgetDefinitionModel, index) {
|
||||
widgetDefinitionModel.set({ order: index });
|
||||
});
|
||||
|
||||
return this.saveAsync(numberOfWidgets);
|
||||
},
|
||||
|
||||
saveAsync: function (updatedWidgets) {
|
||||
var self = this;
|
||||
|
||||
this.trigger('loading', updatedWidgets);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
self.save({
|
||||
wait: true,
|
||||
success: function (collection) {
|
||||
self.trigger('successAdd', updatedWidgets);
|
||||
resolve(collection);
|
||||
},
|
||||
error: function (response, error) {
|
||||
self.trigger('errorAdd', response);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
save: function (options) {
|
||||
options = options || {};
|
||||
Backbone.sync('update', this, options);
|
||||
},
|
||||
|
||||
attrsForThisType: function (newType, m) {
|
||||
return this._attrsForThisTypeMap[newType](m);
|
||||
},
|
||||
|
||||
resetableAttrsForTypeMap: function (type) {
|
||||
switch (type) {
|
||||
case 'formula':
|
||||
return [];
|
||||
case 'category':
|
||||
return ['aggregation'];
|
||||
case 'histogram':
|
||||
return ['bins', 'column_type'];
|
||||
case 'time-series':
|
||||
return ['bins', 'aggregation', 'column_type'];
|
||||
}
|
||||
},
|
||||
|
||||
isThereTimeSeries: function (opts) {
|
||||
opts = opts || {};
|
||||
|
||||
var timeSeries = opts.animated ? this.findWhere({ type: 'time-series', animated: opts.animated }) : this.findWhere({ type: 'time-series' });
|
||||
return !!timeSeries;
|
||||
},
|
||||
|
||||
isThereOtherWidgets: function () {
|
||||
return !!this.find(function (widgetModel) {
|
||||
return widgetModel.get('type') !== 'time-series';
|
||||
});
|
||||
},
|
||||
|
||||
_getNextOrder: function () {
|
||||
if (this.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
var lastItemByOrder = this.max(function (mdl) {
|
||||
return mdl.get('order');
|
||||
});
|
||||
return lastItemByOrder.get('order') + 1;
|
||||
}
|
||||
},
|
||||
|
||||
widgetsOwnedByLayer: function (layerId) {
|
||||
var widgets = this.where({ layer_id: layerId });
|
||||
|
||||
return widgets.length;
|
||||
},
|
||||
|
||||
getColumnType: function (columnName, source) {
|
||||
var column;
|
||||
var node = this._analysisDefinitionNodesCollection.get(source);
|
||||
var schemaModel = node && node.querySchemaModel;
|
||||
if (schemaModel && schemaModel.get('status') === 'fetched') {
|
||||
column = schemaModel.columnsCollection.findWhere({ name: columnName });
|
||||
}
|
||||
return column && column.get('type');
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user