Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
var $ = require('jquery');
var cdb = require('internal-carto.js');
var CoreView = require('backbone/core-view');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view.js');
var NotificationErrorMessageHandler = require('builder/editor/layers/notification-error-message-handler');
module.exports = CoreView.extend({
initialize: function (opts) {
if (!opts.analysisNode) throw new Error('analysisNode is required');
if (!opts.element) throw new Error('element is required');
if (!opts.triggerSelector) throw new Error('triggerSelector is required');
this.analysisNode = opts.analysisNode;
this.$el = opts.element;
this.selector = opts.triggerSelector;
this.$el.on('mouseover', this.selector, this._showTooltip.bind(this));
this.$el.on('mouseout', this.selector, this._destroyTooltip.bind(this));
this.$el.on('mouseleave', this.selector, this._destroyTooltip.bind(this));
},
_showTooltip: function (e) {
var status = this.analysisNode.get('status');
if (status === 'failed') {
var message = NotificationErrorMessageHandler.extractErrorFromAnalysisNode(this.analysisNode);
this.tooltip = this._createTooltip({
$el: $(e.target),
msg: message.message
});
this.tooltip.showTipsy();
}
},
_createTooltip: function (opts) {
return new TipsyTooltipView({
el: opts.$el,
title: function () {
return opts.msg;
}
});
},
_destroyTooltip: function () {
if (this.tooltip) {
this.tooltip.hideTipsy();
this.tooltip.destroyTipsy();
delete this.tooltip;
}
},
clean: function () {
this.$el.off('mouseover', this.selector);
this.$el.off('mouseout', this.selector);
this.$el.off('mouseleave', this.selector);
this._destroyTooltip();
cdb.core.View.prototype.clean.call(this);
}
});

View File

@@ -0,0 +1,130 @@
var _ = require('underscore');
var Notifier = require('builder/components/notifier/notifier');
var NotificationErrorMessageHandler = require('builder/editor/layers/notification-error-message-handler');
var DEFAULT_DELAY = Notifier.DEFAULT_DELAY;
var STATUS_READY = 'ready';
var STATUS_PENDING = 'pending';
var STATUS_RUNNING = 'running';
var STATUS_WAITING = 'waiting';
var STATUS_FAILED = 'failed';
var AnalysisNotifications = {
track: function (analysisNode, layerDefModel) {
analysisNode.on('change:status', this._addStatusChangedNotification, this);
analysisNode.on('change:error', this._onErrorChanged, this);
analysisNode.once('destroy', this._addRemovedNotification, this);
this._layerDefModel = layerDefModel;
if (analysisNode.get('status') && analysisNode.get('status') !== STATUS_READY) {
this._addStatusChangedNotification(analysisNode);
}
},
_addRemovedNotification: function (analysisNode) {
if (analysisNode.get('avoidNotification') === true) {
return;
}
var nodeId = analysisNode.get('id');
var notificationAttrs = {
status: 'success',
info: _t('notifications.analysis.removed', {
nodeId: nodeId.toUpperCase()
}),
closable: true,
delay: DEFAULT_DELAY
};
this._addOrUpdateNotification(analysisNode.cid, notificationAttrs);
},
_addStatusChangedNotification: function (analysisNode) {
var status = analysisNode.get('status');
if (status === STATUS_WAITING || status === STATUS_PENDING) {
this._addAnalysisWaitingNotification(analysisNode);
}
if (status === STATUS_RUNNING) {
this._addAnalysisRunningNotification(analysisNode);
}
if (status === STATUS_READY) {
this._addAnalysisReadyNotification(analysisNode);
}
if (status === STATUS_FAILED) {
this._addAnalysisFailedNotification(analysisNode);
}
},
_addAnalysisWaitingNotification: function (analysisNode) {
var notificationAttrs = {
status: 'loading',
info: _t('notifications.analysis.waiting', {
nodeId: analysisNode.get('id').toUpperCase()
})
};
this._addOrUpdateNotification(analysisNode.cid, notificationAttrs);
},
_addAnalysisRunningNotification: function (analysisNode) {
var notificationAttrs = {
status: 'loading',
info: _t('notifications.analysis.running', {
nodeId: analysisNode.get('id').toUpperCase()
})
};
this._addOrUpdateNotification(analysisNode.cid, notificationAttrs);
},
_addAnalysisReadyNotification: function (analysisNode) {
var notificationAttrs = {
status: 'success',
info: _t('notifications.analysis.completed', {
nodeId: analysisNode.get('id').toUpperCase()
}),
closable: true,
delay: DEFAULT_DELAY
};
this._addOrUpdateNotification(analysisNode.cid, notificationAttrs);
},
_addAnalysisFailedNotification: function (analysisNode) {
var message = NotificationErrorMessageHandler.extractErrorFromAnalysisNode(analysisNode, this._layerDefModel);
var notificationAttrs = {
status: message.type,
info: message.message,
closable: true,
autoclosable: false
};
this._addOrUpdateNotification(analysisNode.cid, notificationAttrs);
},
_addOrUpdateNotification: function (notificationId, notificationAttrs) {
var notification = this._getNotification(notificationId);
if (notification) {
notification.set(notificationAttrs);
} else {
this._addNotification(notificationId, notificationAttrs);
}
},
_addNotification: function (notificationId, notificationAttrs) {
notificationAttrs = _.extend({
id: notificationId
}, notificationAttrs);
Notifier.addNotification(notificationAttrs);
},
_getNotification: function (notificationId) {
return Notifier.getNotification(notificationId);
},
_onErrorChanged: function (analysisNode, error) {
if (error) {
this._addAnalysisFailedNotification(analysisNode);
}
}
};
module.exports = AnalysisNotifications;

View File

@@ -0,0 +1,82 @@
var CoreView = require('backbone/core-view');
var SourceLayerAnalysisView = require('./source-layer-analysis-view');
var DefaultLayerAnalysisView = require('./default-layer-analysis-view');
var template = require('./composite-layer-analysis-view.tpl');
/**
* View for an analysis node which have two source nodes as input.
* The primary source node is rendered separately, this view renders the own node + the secondary one.
* _____________ ___________________
* | own node | ------ | secondary node |
* |____________| |__________________|
* |
* ________________
* | primary node |
* |________________|
*
* this.model is expected to be a analysis-definition-node-nodel
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'Editor-ListAnalysis-item',
initialize: function (opts) {
if (!opts.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
this._analysisDefinitionNodesCollection = opts.analysisDefinitionNodesCollection;
this._layerDefinitionModel = opts.layerDefinitionModel;
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._renderOwnNode();
this._renderSecondaryNode();
return this;
},
_renderOwnNode: function () {
var view = new DefaultLayerAnalysisView({
tagName: 'div',
model: this.model,
analysisNode: this._analysisDefinitionNodesCollection.get(this.model.id),
layerDefinitionModel: this._layerDefinitionModel
});
this.addView(view);
this.$('.js-primary-source').append(view.render().el);
},
_renderSecondaryNode: function () {
var nodeDefModel = this.model.getSecondarySource();
var layerDefModel = this._layerDefinitionModel.collection.findOwnerOfAnalysisNode(nodeDefModel);
if (nodeDefModel) {
var view = nodeDefModel.get('type') === 'source'
? new SourceLayerAnalysisView({
model: nodeDefModel,
analysisNode: this._analysisDefinitionNodesCollection.get(nodeDefModel.id),
layerDefinitionModel: this._layerDefinitionModel,
showId: !!(layerDefModel && this._isOwnedByOtherLayer(layerDefModel))
})
: new DefaultLayerAnalysisView({
tagName: 'div',
model: nodeDefModel,
analysisNode: this._analysisDefinitionNodesCollection.get(nodeDefModel.id),
layerDefinitionModel: layerDefModel
});
this.addView(view);
this.$('.js-secondary-source').append(view.render().el);
}
},
_isOwnedByOtherLayer: function (layerDefModel) {
return layerDefModel !== this._layerDefinitionModel;
}
});

View File

@@ -0,0 +1,4 @@
<ul class="Editor-ListAnalysis-inner">
<li class="Editor-ListAnalysis-innerItem js-primary-source"></li>
<li class="Editor-ListAnalysis-innerItem js-secondary-source"></li>
</ul>

View File

@@ -0,0 +1,102 @@
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var layerColors = require('builder/data/layer-colors');
var template = require('./default-layer-analysis-view.tpl');
var Analyses = require('builder/data/analyses');
var AnalysisTooltip = require('./analyses-tooltip-error');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
/**
* View for an analysis node with a single input
*
* this.model is expected to be a analysis-definition-node-nodel
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'Editor-ListAnalysis-item Editor-ListAnalysis-layer CDB-Text is-semibold CDB-Size-small js-analysis-node',
events: {
'mouseenter': '_onMouseEnter',
'mouseleave': '_onMouseLeave'
},
initialize: function (opts) {
if (!opts.analysisNode) throw new Error('analysisNode is required');
this._analysisNode = opts.analysisNode;
this._analysisNode.on('change:status', this.render, this);
this.add_related_model(this._analysisNode);
this._analysisTooltip = new AnalysisTooltip({
analysisNode: this._analysisNode,
element: this.$el,
triggerSelector: '.Editor-ListAnalysis-itemError'
});
this.addView(this._analysisTooltip);
this._stateModel = new Backbone.Model({
highlighted: false
});
this._initBinds();
},
_initBinds: function () {
this._stateModel.on('change:highlighted', this._toggleHover, this);
this.add_related_model(this._stateModel);
},
render: function () {
var status = this._analysisNode.get('status');
this.$el.html(template({
id: this.model.id,
bgColor: this._bgColor(),
isDone: status === 'ready' || status === 'failed',
title: Analyses.title(this.model),
hasError: status === 'failed'
}));
this.$el.toggleClass('has-error', status === 'failed');
this.el.dataset.analysisNodeId = this.model.id;
this._initViews();
return this;
},
_initViews: function () {
var sourceTooltip = new TipsyTooltipView({
el: this.$el,
gravity: 'w',
title: function () {
return _t('edit-analysis');
}
});
this.addView(sourceTooltip);
},
_bgColor: function () {
var letter = nodeIds.letter(this._analysisNode.id);
return layerColors.getColorForLetter(letter);
},
_onMouseEnter: function () {
this._stateModel.set('highlighted', true);
},
_onMouseLeave: function () {
this._stateModel.set('highlighted', false);
},
_toggleHover: function () {
var $layer = this.$el.closest('.js-layer');
var $title = $layer.find('.js-Editor-ListLayer-titleText');
var highlighted = this._stateModel.get('highlighted');
$layer.toggleClass('is-hover', highlighted);
$title.toggleClass('is-hover', highlighted);
}
});

View File

@@ -0,0 +1,19 @@
<div class="Editor-ListAnalysis-itemInfo u-rSpace--m CDB-Text is-semibold CDB-Size-small u-upperCase" style="background: <%- bgColor %>; color: #fff">
<% if (isDone) { %>
<span class="CDB-Text u-rSpace">
<%- id %>
</span>
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-medium"></i>
<% } else { %>
<div class="CDB-LoaderIcon">
<svg class="CDB-LoaderIcon-spinner" viewBox="0 0 50 50">
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"></circle>
</svg>
</div>
<% } %>
</div>
<p class="Editor-ListAnalysis-title CDB-Text CDB-Size-small u-secondaryTextColor u-ellipsis" title="<%- title %>"><%- title %></p>
<% if (hasError) { %>
<div class="Editor-ListAnalysis-itemError"></div>
<% } %>

View File

@@ -0,0 +1,79 @@
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var analyses = require('builder/data/analyses');
var template = require('./ref-layer-analysis-view.tpl');
/**
* View for an analysis node that belongs to another layer
*
* this.model is expected to be a analysis-definition-node-nodel
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'Editor-ListAnalysis-item Editor-ListAnalysis-layer CDB-Text is-semibold CDB-Size-small js-analysis-node',
events: {
'mouseenter': '_onMouseEnter',
'mouseleave': '_onMouseLeave'
},
initialize: function (opts) {
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
if (!opts.analysisNode) throw new Error('analysisNode is required');
this._layerDefinitionModel = opts.layerDefinitionModel;
this._analysisNode = opts.analysisNode;
this._stateModel = new Backbone.Model({
highlighted: false
});
this._initBinds();
},
_initBinds: function () {
this.listenTo(this._analysisNode, 'change:status', this.render);
this.add_related_model(this._analysisNode);
this.listenTo(this._layerDefinitionModel, 'change', this.render);
this.add_related_model(this._layerDefinitionModel);
this.listenTo(this._stateModel, 'change:highlighted', this._toggleHover);
this.add_related_model(this._stateModel);
},
render: function () {
var status = this._analysisNode.get('status');
this.$el.html(template({
id: this.model.id,
layerName: this._layerDefinitionModel.getName(),
bgColor: this.model.getColor(),
isDone: status === 'ready' || status === 'failed',
title: analyses.title(this.model)
}));
this.$el.toggleClass('has-error', status === 'failed');
this.el.dataset.analysisNodeId = this.model.id;
return this;
},
_onMouseEnter: function () {
this._stateModel.set('highlighted', true);
},
_onMouseLeave: function () {
this._stateModel.set('highlighted', false);
},
_toggleHover: function () {
var $layer = this.$el.closest('.js-layer');
var $title = $layer.find('.js-Editor-ListLayer-titleText');
var highlighted = this._stateModel.get('highlighted');
$layer.toggleClass('is-hover', highlighted);
$title.toggleClass('is-hover', highlighted);
}
});

View File

@@ -0,0 +1,17 @@
<div class="Editor-ListAnalysis-itemInfo u-rSpace--m CDB-Text is-semibold CDB-Size-small u-upperCase" style="background: <%- bgColor %>; color: #fff">
<% if (isDone) { %>
<span class="CDB-Text u-rSpace">
<%- id %>
</span>
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-medium"></i>
<% } else { %>
<div class="CDB-LoaderIcon">
<svg class="CDB-LoaderIcon-spinner" viewBox="0 0 50 50">
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"></circle>
</svg>
</div>
<% } %>
</div>
<p class="Editor-ListAnalysis-title CDB-Text CDB-Size-small u-secondaryTextColor u-ellipsis u-flex" title="<%- title %>">
<%- title %> <span class="u-altTextColor u-lSpace u-ellipsis"><%- layerName %></span>
</p>

View File

@@ -0,0 +1,137 @@
var $ = require('jquery');
var _ = require('underscore');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var template = require('./source-layer-analysis-view.tpl');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var moment = require('moment');
/**
* View for a analysis source (i.e. SQL query).
*
* this.model is expected to be a analysis-definition-node-model and belong to the given layer-definition-model
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'Editor-ListAnalysis-item Editor-ListAnalysis-layer js-base is-base u-flex u-justifySpace',
events: {
'mouseenter': '_onMouseEnter',
'mouseleave': '_onMouseLeave'
},
initialize: function (opts) {
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
if (!opts.analysisNode) throw new Error('analysisNode is required');
this._layerDefinitionModel = opts.layerDefinitionModel;
this._analysisNode = opts.analysisNode;
this._tableNodeModel = this._analysisNode.getTableModel();
this._stateModel = new Backbone.Model({
highlighted: false
});
this._initBinds();
},
options: {
showId: true
},
render: function () {
this.clearSubViews();
var isSync = this._tableNodeModel && this._tableNodeModel.isSync();
var syncData = {};
if (isSync) {
var syncModel = this._tableNodeModel.getSyncModel();
var runAt = syncModel.get('run_at');
syncData = {
ranAt: moment(syncModel.get('ran_at') || new Date()).fromNow(),
runAt: moment(runAt).fromNow(),
state: syncModel.get('state'),
errorCode: syncModel.get('error_code'),
errorMessage: syncModel.get('error_message')
};
// Due to the time we need to polling, we have to display to the user
// that the sync will be in a moment
if (!runAt || (new Date(runAt) <= new Date())) {
syncData.runAt = _t('dataset.sync.in-a-moment');
}
}
var isCustomQueryApplied = this._analysisNode.isCustomQueryApplied();
this.$el.html(template(_.extend({
id: this.options.showId ? this.model.id : '',
tableName: this.model.get('table_name'),
customQueryApplied: isCustomQueryApplied,
isSync: isSync,
bgColor: this._analysisNode.getColor()
}, syncData)));
var sourceTooltip = new TipsyTooltipView({
el: this.$el,
gravity: 'w',
title: function () {
return _t('data-source');
}
});
this.addView(sourceTooltip);
if (isSync) {
var syncTooltipTitle = (syncData.errorCode || syncData.errorMessage) ? _t('dataset.sync.error-code', { errorCode: syncData.errorCode }) + ':' + syncData.errorMessage : $(this).data('tooltip');
var syncTooltip = new TipsyTooltipView({
el: this.$('.js-sync'),
gravity: 's',
offset: 0,
title: syncTooltipTitle
});
this.addView(syncTooltip);
}
if (isCustomQueryApplied) {
var sqlTooltip = new TipsyTooltipView({
el: this.$('.js-sql'),
gravity: 'w',
title: function () {
return _t('sql-applied');
},
mouseEnterAction: function () {
sourceTooltip.hideTipsy();
},
mouseLeaveAction: function () {
sourceTooltip.showTipsy();
}
});
this.addView(sqlTooltip);
}
return this;
},
_initBinds: function () {
this.listenTo(this._stateModel, 'change:highlighted', this._toggleHover);
this.listenTo(this._tableNodeModel, 'change:synchronization', this.render);
},
_onMouseEnter: function () {
this._stateModel.set('highlighted', true);
},
_onMouseLeave: function () {
this._stateModel.set('highlighted', false);
},
_toggleHover: function () {
var $layer = this.$el.closest('.js-layer');
var $title = $layer.find('.js-Editor-ListLayer-titleText');
var highlighted = this._stateModel.get('highlighted');
$layer.toggleClass('is-hover', highlighted);
$title.toggleClass('is-hover', highlighted);
}
});

View File

@@ -0,0 +1,21 @@
<p class="Editor-ListAnalysis-itemInfoTitle CDB-Text CDB-Size-small u-ellipsis u-flex" title="<%- tableName %>">
<span class="CDB-Text is-semibold CDB-Size-small u-rSpace u-upperCase" style="color: <%- bgColor %>">
<%- id %>
</span>
Source
<span class="Editor-ListAnalysis-title u-altTextColor u-lSpace u-ellipsis">
<%- tableName %>
</span>
</p>
<% if (isSync) { %>
<span class="Editor-ListAnalysis-itemInfoIcon">
<div class="u-flex u-alignCenter CDB-Text CDB-Size-small u-altTextColor SyncInfo-message--<%- state %> js-sync" data-tooltip="<% if (errorCode || errorMessage) { %><%- _t('dataset.sync.sync-failed') %><% } else { %><%- ranAt %><% } %>">
<i class="CDB-IconFont CDB-IconFont-wifi"></i>
</div>
</span>
<% } %>
<% if (customQueryApplied) { %>
<span class="Editor-ListAnalysis-itemInfoIcon Tag Tag--outline Tag-outline--dark CDB-Text CDB-Size-small js-sql">
SQL
</span>
<% } %>

View File

@@ -0,0 +1,259 @@
var Backbone = require('backbone');
var _ = require('underscore');
var CoreView = require('backbone/core-view');
var CarouselCollection = require('builder/components/custom-carousel/custom-carousel-collection');
var template = require('./basemap-content.tpl');
var BasemapHeaderView = require('./basemap-content-views/basemap-header-view');
var BasemapModelFactory = require('./basemap-content-views/basemap-model-factory');
var BasemapsCollection = require('./basemap-content-views/basemaps-collection');
var BasemapInnerView = require('./basemap-content-views/basemap-inner-view');
var ScrollView = require('builder/components/scroll/scroll-view');
var Notifier = require('builder/components/notifier/notifier');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var Router = require('builder/routes/router');
var NOTIFICATION_ID = 'basemapNotification';
var BASEMAP_ICONS = {
gmaps: require('./basemap-content-views/basemap-icons/basemap-gmaps.tpl'),
carto: require('./basemap-content-views/basemap-icons/basemap-carto.tpl'),
stamen: require('./basemap-content-views/basemap-icons/basemap-stamen.tpl'),
color: require('./basemap-content-views/basemap-icons/basemap-color.tpl'),
mapbox: require('./basemap-content-views/basemap-icons/basemap-mapbox.tpl'),
custom: require('./basemap-content-views/basemap-icons/basemap-custom.tpl'),
here: require('./basemap-content-views/basemap-icons/basemap-here.tpl'),
wms: require('./basemap-content-views/basemap-icons/basemap-wms.tpl'),
tilejson: require('./basemap-content-views/basemap-icons/basemap-tilejson.tpl'),
nasa: require('./basemap-content-views/basemap-icons/basemap-nasa.tpl')
};
var REQUIRED_OPTS = [
'layerDefinitionsCollection',
'stackLayoutModel',
'customBaselayersCollection',
'basemaps',
'modals',
'configModel'
];
module.exports = CoreView.extend({
events: {
'click .js-back': '_onClickBack'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._baseLayer = this._layerDefinitionsCollection.getBaseLayer();
this.model = new Backbone.Model({
disabled: false
});
this._basemapModelFactory = new BasemapModelFactory(this._layerDefinitionsCollection, this._configModel);
this._initCollections(opts.basemaps);
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
return this;
},
_initBinds: function () {
this.model.bind('change:disabled', function () {
this.$('.js-basemapContent').toggleClass('is-disabled', this.model.get('disabled'));
}, this);
this._basemapsCollection.bind('change:category', function (mdl) {
var userLayersMdl = this._customBaselayersCollection.get(mdl.get('id'));
userLayersMdl.set('category', mdl.get('category'));
userLayersMdl.save();
}, this);
this._basemapsCollection.bind('remove', function (mdl) {
var userLayersMdl = this._customBaselayersCollection.get(mdl.get('id'));
userLayersMdl.destroy();
}, this);
this._basemapsCollection.bind('add', this._onAddBasemap, this);
this.add_related_model(this._basemapsCollection);
this._layerDefinitionsCollection.bind('changingBaseLayer', this._onChangingBaseLayer, this);
this._layerDefinitionsCollection.bind('baseLayerChanged', this._onBaseLayerChanged, this);
this._layerDefinitionsCollection.bind('baseLayerFailed', this._onBaseLayerFailed, this);
this._layerDefinitionsCollection.bind('change', function () {
this._renderHeader();
this._baseLayer = this._layerDefinitionsCollection.getBaseLayer();
this._updateSelectedCategory(this._baseLayer.get('category'));
}, this);
this.add_related_model(this._layerDefinitionsCollection);
},
_initViews: function () {
this._renderHeader();
this._renderContent();
},
_initCollections: function (basemaps) {
this._basemapsCollection = new BasemapsCollection();
var basemapModel;
// Basemaps defined in app_config.yml
_(basemaps).each(function (categoryBasemaps, category) {
_.map(categoryBasemaps, function (basemap) {
basemapModel = this._basemapModelFactory.createBasemapModel(category, basemap);
this._basemapsCollection.add(basemapModel);
}, this);
}, this);
// userlayers basemaps
this._customBaselayersCollection.each(function (customBaseLayerDefinitionModel) {
var category = customBaseLayerDefinitionModel.get('category') || 'Custom';
basemapModel = this._basemapModelFactory.createBasemapModel(category, customBaseLayerDefinitionModel.attributes);
this._basemapsCollection.add(basemapModel);
}, this);
// Plain color or image basemap
basemapModel = this._basemapModelFactory.createBasemapModel('Color', {
color: this._baseLayer.get('color') || '',
image: this._baseLayer.get('image') || '',
selected: this._baseLayer.get('className') === 'plain'
});
this._basemapsCollection.add(basemapModel);
this._categoriesCollection = new CarouselCollection(
_.map(this._basemapsCollection.getCategories(), function (category) {
return {
selected: this._getBaseLayerCategory() === category,
val: category,
label: category,
template: function () {
var template = this._getTemplateForCategory(category.toLowerCase());
return template();
}.bind(this)
};
}, this)
);
},
_getTemplateForCategory: function (categoryName) {
var template = BASEMAP_ICONS[categoryName.toLowerCase()];
if (!template) {
template = function () {
return categoryName;
};
}
return template;
},
_onChangingBaseLayer: function () {
this.model.set('disabled', true);
if (Notifier.getNotification(NOTIFICATION_ID)) {
Notifier.removeNotification(NOTIFICATION_ID);
}
this.notification = Notifier.addNotification({
id: NOTIFICATION_ID,
status: 'loading',
info: _t('editor.layers.basemap.saving.loading'),
closable: false
});
},
_onBaseLayerChanged: function () {
this.model.set('disabled', false);
this.notification.set({
status: 'success',
info: _t('editor.layers.basemap.saving.success'),
closable: true
});
},
_onBaseLayerFailed: function () {
this.model.set('disabled', false);
this.notification.set({
status: 'error',
info: _t('editor.layers.basemap.saving.error'),
closable: true
});
},
_updateSelectedCategory: function (category) {
var newCategory = this._categoriesCollection.findWhere({ val: category });
newCategory && newCategory.set({ selected: true });
},
_onChangeSelectedCategory: function (mdl, isSelected) {
if (isSelected) {
this._renderContent();
}
},
_getBaseLayerCategory: function () {
var category = this._basemapsCollection.getDefaultCategory();
if (this._baseLayer.get('category')) {
category = this._baseLayer.get('category');
} else if (this._baseLayer.get('type') === 'Plain') {
category = 'Color';
} else if (this._customBaselayersCollection.hasCustomBaseLayer(this._baseLayer.get('className'))) {
category = 'Custom';
}
return category;
},
_renderContent: function () {
var self = this;
if (this._contentView) {
this.removeView(this._contentView);
this._contentView.clean();
}
this._contentView = new ScrollView({
createContentView: function () {
return new BasemapInnerView({
model: self.model,
categoriesCollection: self._categoriesCollection,
layerDefinitionsCollection: self._layerDefinitionsCollection,
basemapsCollection: self._basemapsCollection,
customBaselayersCollection: self._customBaselayersCollection,
modals: self._modals
});
}
});
this.addView(this._contentView);
this.$('.js-basemapContent').html(this._contentView.render().el);
},
_renderHeader: function () {
if (this._headerView) {
this.removeView(this._headerView);
this._headerView.clean();
}
this._headerView = new BasemapHeaderView({
model: this._layerDefinitionsCollection.getBaseLayer(),
category: this._getBaseLayerCategory()
});
this.addView(this._headerView);
this.$('.js-basemapHeader').html(this._headerView.render().el);
},
_onAddBasemap: function (mdl) {
this._basemapsCollection.updateSelected(mdl.getValue());
},
_onClickBack: function () {
Router.goToLayerList();
}
});

View File

@@ -0,0 +1,6 @@
<div class="js-selector">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.layers.basemap.category.title-label') %></h2>
</div>
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--m js-highlight"><%- _t('editor.layers.basemap.category.select-category') %></p>
</div>

View File

@@ -0,0 +1,32 @@
var CoreView = require('backbone/core-view');
var template = require('./basemap-categories.tpl');
var CarouselFormView = require('builder/components/carousel-form-view');
module.exports = CoreView.extend({
initialize: function (opts) {
if (!opts.categoriesCollection) throw new Error('categoriesCollection is required');
this._categoriesCollection = opts.categoriesCollection;
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._renderCarousel();
return this;
},
_renderCarousel: function () {
var view = new CarouselFormView({
collection: this._categoriesCollection,
template: require('./basemap-carousel.tpl')
});
this.addView(view);
this.$('.js-select').append(view.render().el);
}
});

View File

@@ -0,0 +1,4 @@
<div class="Editor-HeaderInfo">
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">1</div>
<div class="Editor-HeaderInfo-inner CDB-Text js-select"></div>
</div>

View File

@@ -0,0 +1,68 @@
var _ = require('underscore');
var Backbone = require('backbone');
var DEBOUNCE_TIME = 650;
module.exports = Backbone.Model.extend({
initialize: function (attrs, opts) {
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._basemapsCollection = opts.basemapsCollection;
this._disabled = opts.disabled;
this._generateSchema();
this._initBinds();
},
_initBinds: function () {
this._onChange = this._onChange.bind(this);
this.bind('change', _.debounce(this._onChange, DEBOUNCE_TIME), this);
},
_onChange: function () {
if (this.hasChanged('color')) {
var color = this.get('color').color.fixed;
this._updatePlainBasemap(color);
}
},
_updatePlainBasemap: function (color) {
var plainBasemap = this._basemapsCollection.find(function (mdl) {
return mdl.get('className') === 'plain';
});
var isAlreadySelected = plainBasemap.get('selected');
plainBasemap.set({
color: color,
image: '',
maxZoom: 32
});
if (isAlreadySelected) {
this._layerDefinitionsCollection.setBaseLayer(plainBasemap.toJSON());
} else {
this._basemapsCollection.updateSelected(plainBasemap.getValue());
}
},
_generateSchema: function () {
this.schema = {
color: {
type: 'Fill',
title: _t('editor.layers.basemap.style.color'),
options: [],
editorAttrs: {
color: {
hidePanes: ['value'],
disableOpacity: true
},
disabled: this._disabled
},
validators: ['required']
}
};
}
});

View File

@@ -0,0 +1,68 @@
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var template = require('./basemap-form.tpl');
var BasemapFormModel = require('./basemap-form-model');
module.exports = CoreView.extend({
initialize: function (opts) {
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._basemapsCollection = opts.basemapsCollection;
this._disabled = opts.disabled;
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._generateForms();
return this;
},
_generateForms: function () {
if (this._formView) {
this._formView.remove();
}
var color = {
color: {
fixed: '',
opacity: 1
}
};
if (typeof this.model.get('color') === 'string') {
color.color.fixed = this.model.get('color');
}
this._formModel = new BasemapFormModel({
color: color,
image: this.model.get('image') || ''
}, {
basemapsCollection: this._basemapsCollection,
layerDefinitionsCollection: this._layerDefinitionsCollection,
disabled: this._disabled
});
this._formView = new Backbone.Form({
model: this._formModel
});
this._formView.bind('change', function () {
this.commit();
});
this.$('.js-form').append(this._formView.render().$el);
},
clean: function () {
if (this._formView) {
this._formView.remove();
}
}
});

View File

@@ -0,0 +1,8 @@
<div class="js-selector">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.layers.infowindow.style.title-label') %></h2>
</div>
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--m js-highlight"><%- _t('editor.layers.infowindow.style.select-style') %></p>
</div>
<div class="js-form"></div>

View File

@@ -0,0 +1,36 @@
var CoreView = require('backbone/core-view');
var template = require('./basemap-header.tpl');
var DEFAULT_NAME = _t('editor.layers.basemap.custom-basemap');
module.exports = CoreView.extend({
initialize: function (opts) {
if (!opts.category) throw new Error('category is required');
this.category = opts.category;
},
render: function () {
this.$el.html(
template({
title: _t('editor.layers.basemap.title-label'),
description: this._getName()
})
);
return this;
},
_getName: function () {
var name = this.model.getName();
if (!name) {
name = DEFAULT_NAME;
} else {
name = this.model.getName().replace(/_/g, '') + ' ' + _t('editor.layers.basemap.by') + ' ' + this.category;
}
return name;
}
});

View File

@@ -0,0 +1,24 @@
<ul class="Editor-breadcrumb">
<li class="Editor-breadcrumbItem CDB-Text CDB-Size-medium u-actionTextColor">
<button class="js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev Size-large u-rSpace"></i>
<span class="Editor-breadcrumbLink">
<%- _t('back') %>
</span>
</button>
</li>
<li class="Editor-breadcrumbItem CDB-Text CDB-Size-medium">
<span class="Editor-breadcrumbSep"> / </span>
<%- _t('editor.layers.breadcrumb.basemap-options') %>
</li>
</ul>
<div class="Editor-HeaderInfo u-alignStart">
<div class="Editor-HeaderInfo-inner">
<div class="Editor-HeaderInfo-title u-bSpace">
<h2 class="Editor-HeaderInfo-titleText CDB-Text CDB-Size-huge is-light u-ellipsis"><%- description %></h2>
</div>
</div>
</div>

View File

@@ -0,0 +1 @@
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAAA4CAYAAAB5YT9uAAAAAXNSR0IArs4c6QAAB51JREFUeAHtW+tv21QUP3HeTtykTdt1a9d1GxNIwBAggRD/O1+RkPgAQhogNq2dlnVdm+Yd23lxfm5v5zqO48dtfTX1SpGvr69Pzv35+ryd+f3Pv+akYGuevKfPHx/Q7s62NO6mpkVjy6JSbS0xTXs8ppFpUrVcpsl0Ru1ej3IZjcp6mfRy6Yp+7qqnWKdULEoFF8vLloo0nUxp0O5QpV5LtOIJ08lqWRrwQ0O7t9lg2hPK5q5Dev0s0V/Ku3kyndJGrS6PoItSoaqTeXziGonXBY8VfmBuQN19QVUTHZWO/eGA9h/cU4mlBV5mDLAfoN6JSgIMJt1yzMt0knO8xrlCMQkJ514tm6X3p2cr6SgHMF697cb6SsbjThictkjfSCZ/8d9r1QrlsznqD4aOslvGj3IAdwd92ttRWzwIMMvlImmaRqPhhaIT4+6jckoun9WokM+7eYzchxjATvW2DGv9yuaGdzj2ebl0YY6Zl5aEHyH1AM4nZ0konzWJNrQfeBizLJsKheUbQikRcd7p0MGDB8vWEmkcdm5Xgjm26k+LxQLZ1tixgf3mKgNwq9Nm02yH6hK8LCwUTsVtgbzBSrPTHRC8O9GGI9PpJn8fBcWYx5OzU+fO/d37tCNRPoIoQL6tBpBbrQ6bgDma2BMa2ZZjaqYOsFGt0nrNoINdOaLBCygUm58L650n4xwgQyaXWGxow4xDMnURUS6W6LzTlbE+XxrGdsPXovCdLGEQMjmbuQAX5FIHGEwUJFgOoBPU7P4w6LK0a3hbII8HLIPRTx3gdq9LTx7uSVugHyGYa2a/T4Oz86Xa3u++OGOd3oAgKkSYNXWAs1qGhMHuXtAqF9Q9N0wfIJdY1vs5IGHuDzMHPNuTsSOHMR/2eOpKTmMl5G0IZMN4T+rReekKB8Q7LvO8qusEOSxa6jt4NltMqHR6fengYsEQEUkD7QI4v2O1orPsHV2zh1MF2LJt2mpcD6y3zjv07+sjP/4TjUHhTNkRuEnbuNsfOOLO/ealCnCn31uwf9+dfqAJg+H2ihIhyzdbrNWH522pgR4/nuBgIIzpbqnLYAStNzluMGID3bbH1O0NnWSnexe4GY7Tt60RGVubcW5deQ/ejHanxwotS9m8tuDUZNLMKk9nYzI5UGJaphO4/vn7b6UrNwAwZu0uI5PsRRu0YfNWDd3RGfDi+iwm6mytCIWa6g7OanmqlPHTWXbp1B8OaadyfadhEYJZ7wLDnI/Z4Nc48yC7IZhjjiyqrVWu+IP1kON4do8fKJT3xnotfUdDLFzn4HWbd4Nor982nZzX4btjMRTraPNikUmW2aAfEGSHQ+F9+DhHRLBmVOiMrRb5jzbmSs7aLVZ4uwRgm+8/UI89r7379xwR0qp2nN0Qk7TU2+BMQAljdwa1C6ANSlUGuxlEshPp+qpe4dfswvlAAP6n7567p0XuQ8TAe5OV3YAZuQpcN5OpmmluRgBq3Vi7AhfXZvOpe0qsPnZSiUOipqSIXY4DU1FMSGUA9kMvw7Ve2IFJG2SwfZlhQH0atH3chmoeuPJhmzIyOCzDcechmyxydJpRdcypRoj6C2GKzeczyrOVgIoev+DUMr7U3sGUWdDSyxayahyiQshhxAyQ2oE8xS+oweSCKQZwx7zzda6edAdzgu7FNWV3MJTeei1YU69anLiOXWiyt4VYROGytLRmGM5lXIM5RRw2ZaFP7l0NUYBqTOfhVBkqjxss6AcdlQUYydAfnn8VxHvoa7AiEEXzC/QAPAEqlJd7R6NyR1wL/WeeicqYaR6+uKh56tjCMsy0m3KVvTz7nSsrg2G2ZS/tYT/Gw45h995EHCLs/ysLMBYwntiJzDRYDTJr0cKC6p6nNMAWhy+n88WMh3sBy/pQXlBokLFpNqUBbtTXY6eOhlxlk2dzLO2mNMBDzm/FbfNZuBL/uPTD3qc0wBnYpjEbzLLbKjYJYlFpgEvF5XW3QYvCNdi81jD+G7CKftjrygKMip/He7th1+E7D2Ii7aYswPCq8BXlTbQOf5V5Wy1dGyZglQZbAElMLDwgwNh8eUimPaLpdO4kV/FFZo5NNxGLCGBByiUlAe4NBlwQ+LFeuM3BclQrbnF4EaWhq4AXaScgtFY1qFS4eBOQXEUu7bD5mgacYP3i8cFKWklRVjIWgUDPj998TW+Oj+kEiUPOCussLpCnE1kOBOPzXIvw9OHDqxQOdu0fL/7hHZon1B0HNcQ6ulz48uWzJze6m5UEGF/aF7n4r2bUrqWQ/AA7ar6lCsdoSwwo6isa9WifaUGZbnIC87NH+37kE48pCXDUVb08OqT6mhEZXPE/qJErFgr07NGu8/U8PgGQVVmkpAwWCw97fLr/yAlvhp3vnQdw8ZXTyNpaqC3zzo16rqyZFnUhItUf9T73fJTNym6fDMAygBFl/zJoCRp3AF8iATHx3+FRpJS8ADHoeAfwJTqwkbv9Ef3y62+RCkuCwD3jmuRPQskFLTLKNVgO+DByyEGiQi16oAmAvnrTpDzXCc/nGcepuQPY9QSEooQrHaX9ze44ihcL+SIZlY8V7nB27kSEB8ntjQa9ePnKM7r8FG457Gg4OG5wxR3/AwJZx54EdH2PAAAAAElFTkSuQmCC" />

View File

@@ -0,0 +1,6 @@
<svg width="23px" height="25px" viewBox="1383 664 23 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="color" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(1383.000000, 664.000000)">
<path d="M19.04725,12.691 C19.12525,12.504 19.08225,12.289 18.93925,12.146 L9.93925,3.146 C9.84125,3.049 9.71325,3 9.58525,3 L9.58525,2.5 C9.58525,1.122 8.46325,0 7.08525,0 C5.70725,0 4.58525,1.122 4.58525,2.5 L4.58525,7.793 L0.73125,11.647 C-0.24375,12.622 -0.24375,14.208 0.73125,15.183 L5.40325,19.854 C5.87525,20.326 6.50325,20.586 7.17125,20.586 C7.83925,20.586 8.46625,20.326 8.93925,19.854 L15.79225,13 L18.58525,13 C18.78725,13 18.97025,12.878 19.04725,12.691 L19.04725,12.691 Z M5.58525,2.5 C5.58525,1.673 6.25825,1 7.08525,1 C7.91225,1 8.58525,1.673 8.58525,2.5 L8.58525,3.793 L5.58525,6.793 L5.58525,2.5 L5.58525,2.5 Z M15.58525,12 C15.45225,12 15.32525,12.053 15.23125,12.146 L8.23125,19.146 C7.66525,19.712 6.67625,19.712 6.11025,19.146 L1.43825,14.475 C0.85325,13.89 0.85325,12.938 1.43825,12.353 L5.38925,8.401 C5.42025,8.377 5.44725,8.351 5.47125,8.32 L8.58525,5.207 L8.58525,10.5 C8.58525,10.776 8.80925,11 9.08525,11 C9.36125,11 9.58525,10.776 9.58525,10.5 L9.58525,4.207 L17.37825,12 L15.58525,12 L15.58525,12 Z" id="Shape" fill="#AAAAAA"></path>
<path d="M20.01225,15.241 C19.83125,14.942 19.33825,14.942 19.15825,15.241 C18.89425,15.674 16.58525,19.522 16.58525,21 C16.58525,22.654 17.93125,24 19.58525,24 C21.23925,24 22.58525,22.654 22.58525,21 C22.58525,19.522 20.27625,15.674 20.01225,15.241 L20.01225,15.241 Z M19.58525,23 C18.48225,23 17.58525,22.103 17.58525,21 C17.58525,20.225 18.68125,18.061 19.58525,16.485 C20.48925,18.061 21.58525,20.225 21.58525,21 C21.58525,22.103 20.68825,23 19.58525,23 L19.58525,23 Z" id="Shape" fill="#AAAAAA"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="22px" height="24px" viewBox="966 769 22 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M973.5,791.5 L966.5,786.5 L966.5,770.5 L973.5,775.5 L980.5,770.5 L987.5,775.5 L987.5,791.5 L980.5,786.5 L973.5,791.5 Z M973.5,775.5 L973.5,791.5 L973.5,775.5 Z M980.5,786.5 L980.5,770.531 L980.5,786.5 Z" id="Shape" stroke="#AAAAAA" stroke-width="1" fill="none"></path>
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@@ -0,0 +1 @@
<p class="CDB-Text CDB-Size-small u-upperCase u-altTextColor">Google maps</p>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<p class="CDB-Text CDB-Size-small u-upperCase u-altTextColor">Mapbox</p>

View File

@@ -0,0 +1 @@
<p class="CDB-Text CDB-Size-small u-upperCase u-altTextColor">nasa</p>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<p class="CDB-Text CDB-Size-small u-upperCase u-altTextColor">tilejson</p>

View File

@@ -0,0 +1 @@
<p class="CDB-Text CDB-Size-small u-upperCase u-altTextColor">wms/wmts</p>

View File

@@ -0,0 +1,85 @@
var CoreView = require('backbone/core-view');
var BasemapCategoriesView = require('./basemap-categories-view');
var BasemapSelectView = require('./basemap-select-view');
var template = require('./basemap-inner.tpl');
module.exports = CoreView.extend({
className: 'Editor-inner',
initialize: function (opts) {
if (!opts.categoriesCollection) throw new Error('categoriesCollection is required');
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
if (!opts.modals) throw new Error('modals is required');
this._categoriesCollection = opts.categoriesCollection;
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._basemapsCollection = opts.basemapsCollection;
this._customBaselayersCollection = opts.customBaselayersCollection;
this._modals = opts.modals;
this._initBinds();
},
_initBinds: function () {
this.model.bind('change:disabled', function () {
this._renderSelect();
}, this);
this._basemapsCollection.bind('add remove', function () {
this._renderSelect();
}, this);
this.add_related_model(this._basemapsCollection);
this._categoriesCollection.bind('change:selected', this._renderSelect, this);
this.add_related_model(this._categoriesCollection);
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
return this;
},
_initViews: function () {
this._renderCategories();
this._renderSelect();
},
_renderCategories: function () {
if (this._categoriesView) {
this.removeView(this._categoriesView);
this._categoriesView.clean();
}
this._categoriesView = new BasemapCategoriesView({
categoriesCollection: this._categoriesCollection
});
this.addView(this._categoriesView);
this.$('.js-basemapCategory').html(this._categoriesView.render().el);
},
_renderSelect: function () {
if (this._selectView) {
this.removeView(this._selectView);
this._selectView.clean();
}
this._selectView = new BasemapSelectView({
layerDefinitionsCollection: this._layerDefinitionsCollection,
basemapsCollection: this._basemapsCollection,
customBaselayersCollection: this._customBaselayersCollection,
selectedCategoryVal: this._categoriesCollection.getSelectedValue(),
modals: this._modals,
disabled: this.model.get('disabled')
});
this.addView(this._selectView);
this.$('.js-basemapSelect').html(this._selectView.render().el);
}
});

View File

@@ -0,0 +1,2 @@
<div class="js-basemapCategory"></div>
<div class="js-basemapSelect"></div>

View File

@@ -0,0 +1,188 @@
var _ = require('underscore');
var basemapProvidersAndCategories = require('builder/data/basemap-providers-and-categories');
var mosaicThumbnail = require('builder/components/mosaic/mosaic-thumbnail.tpl');
var BasemapModel = require('./basemap-model');
var DEFAULT_SUBDOMAIN = 'a';
var DEFAULT_X_POSITION = 30;
var DEFAULT_Y_POSISTION = 24;
var DEFAULT_ZOOM = 6;
// 'CartoDB', 'Stamen', 'Here'
var createLeafletBasemapModel = function (category, basemap, baseLayerDefinition, assetsBaseURL) {
var value;
var currentValue;
var label;
var imageURL;
var type = basemapProvidersAndCategories.getLayerType(category);
value = basemap.className;
currentValue = baseLayerDefinition.get('className');
label = basemap.name;
imageURL = lowerXYZ(basemap.urlTemplate, basemap.subdomains);
_.extend(basemap, {
type: type,
name: basemap.name,
category: category,
selected: currentValue === value,
val: value,
label: label,
template: function () {
return mosaicThumbnail({
imgURL: imageURL
});
}
});
return new BasemapModel(basemap);
};
var lowerXYZ = function (urlTemplate, subdomains) {
return urlTemplate
.replace('{s}', getSubdomain(subdomains))
.replace('{z}', DEFAULT_ZOOM)
.replace('{x}', DEFAULT_X_POSITION)
.replace('{y}', DEFAULT_Y_POSISTION);
};
var getSubdomain = function (subdomains) {
return (subdomains && subdomains.length) ? subdomains[0] : DEFAULT_SUBDOMAIN;
};
// Google Maps
var createGoogleMapsBasemapModel = function (category, basemapAttrs, baseLayerDefinition, assetsBaseURL) {
var value;
var currentValue;
var label;
var imageURL;
var type = basemapProvidersAndCategories.getLayerType(category);
var generateValue = function (baseType, baseName) {
var value = baseType;
if (baseName) {
value = value + '_' + baseName;
}
return value;
};
value = generateValue(basemapAttrs.baseType, basemapAttrs.baseName);
currentValue = generateValue(baseLayerDefinition.get('baseType'), baseLayerDefinition.get('baseName'));
label = basemapAttrs.name.replace('GMaps ', '');
imageURL = [
assetsBaseURL,
'unversioned/images/google-maps-basemap-icons',
value + '.jpg'
].join('/');
_.extend(basemapAttrs, {
type: type,
name: basemapAttrs.name,
category: category,
selected: currentValue === value,
val: value,
label: label,
template: function () {
return mosaicThumbnail({
imgURL: imageURL
});
}
});
return new BasemapModel(basemapAttrs);
};
// Custom
var createCustomBasemapModel = function (category, customBaseLayerAttrs, baseLayerDefinition, configModel) {
var name = customBaseLayerAttrs.name ? customBaseLayerAttrs.name : 'Custom basemap ' + customBaseLayerAttrs.order;
var className = customBaseLayerAttrs.className;
var urlTemplate = customBaseLayerAttrs.urlTemplate;
return new BasemapModel({
id: customBaseLayerAttrs.id,
urlTemplate: urlTemplate,
minZoom: customBaseLayerAttrs.minZoom || 0,
maxZoom: customBaseLayerAttrs.maxZoom || 21,
name: name,
className: className,
attribution: customBaseLayerAttrs.attribution,
category: category || 'Custom',
tms: customBaseLayerAttrs.tms,
selected: baseLayerDefinition.get('className') === className,
val: className,
label: name,
template: function (imgURL) {
return mosaicThumbnail({
imgURL: imgURL
});
}
});
};
// Color
var createColorBasemapModel = function (category, basemapAttrs, baseLayerDefinition, configModel) {
return new BasemapModel({
default: false,
color: basemapAttrs.color || '',
image: basemapAttrs.image || '',
maxZoom: 32,
className: 'plain',
category: 'Color',
type: 'Plain',
selected: baseLayerDefinition.get('className') === 'plain',
val: 'plain',
label: 'plain',
template: function () {
return 'plain';
}
});
};
// Factory
var CATEGORY_CARTODB = 'CARTO';
var CATEGORY_STAMEN = 'Stamen';
var CATEGORY_HERE = 'Here';
var CATEGORY_GMAPS = 'GMaps';
var CATEGORY_CUSTOM = 'Custom';
var CATEGORY_NASA = 'NASA';
var CATEGORY_TILEJSON = 'TileJSON';
var CATEGORY_MAPBOX = 'Mapbox';
var CATEGORY_WMS = 'WMS';
var CATEGORY_COLOR = 'Color';
var CREATE_METHODS = {};
CREATE_METHODS[CATEGORY_CARTODB] = createLeafletBasemapModel;
CREATE_METHODS[CATEGORY_STAMEN] = createLeafletBasemapModel;
CREATE_METHODS[CATEGORY_HERE] = createLeafletBasemapModel;
CREATE_METHODS[CATEGORY_GMAPS] = createGoogleMapsBasemapModel;
CREATE_METHODS[CATEGORY_CUSTOM] = createCustomBasemapModel;
CREATE_METHODS[CATEGORY_NASA] = createCustomBasemapModel;
CREATE_METHODS[CATEGORY_TILEJSON] = createCustomBasemapModel;
CREATE_METHODS[CATEGORY_MAPBOX] = createCustomBasemapModel;
CREATE_METHODS[CATEGORY_WMS] = createCustomBasemapModel;
CREATE_METHODS[CATEGORY_COLOR] = createColorBasemapModel;
var BasemapModelFactory = function (layerDefinitionsCollection, configModel) {
if (!layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!configModel) throw new Error('configModel is required');
this._layerDefinitionsCollection = layerDefinitionsCollection;
this._configModel = configModel;
};
BasemapModelFactory.prototype.createBasemapModel = function (basemapCategory, basemapAttrs) {
var baseLayer = this._layerDefinitionsCollection.getBaseLayer();
var assetsBaseURL = this._configModel.get('app_assets_base_url');
var createMethod = CREATE_METHODS[basemapCategory];
if (!createMethod) {
throw new Error("Can't create basemapModel for basemap of type: " + basemapCategory);
}
return createMethod(basemapCategory, basemapAttrs, baseLayer, assetsBaseURL);
};
module.exports = BasemapModelFactory;

View File

@@ -0,0 +1,28 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
defaults: {
default: false,
minZoom: 0,
maxZoom: 21,
name: '',
category: 'Custom',
type: 'Tiled',
selected: false,
val: '',
label: '',
template: function () {
return '';
}
},
getName: function () {
return this.get('label') || this.getValue();
},
getValue: function () {
return this.get('val');
}
});

View File

@@ -0,0 +1,74 @@
var CoreView = require('backbone/core-view');
var template = require('builder/components/mosaic/mosaic-item.tpl');
var AddBasemapView = require('builder/components/modals/add-basemap/add-basemap-view');
var AddBasemapModel = require('builder/components/modals/add-basemap/add-basemap-model');
module.exports = CoreView.extend({
className: 'Mosaic-item Mosaic-item--dashed',
tagName: 'li',
events: {
'mouseenter': '_onMouseEnter',
'mouseleave': '_onMouseLeave',
'click': '_onClick'
},
initialize: function (opts) {
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.modals) throw new Error('modals is required');
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
if (!opts.mosaicModel) throw new Error('mosaicModel is required');
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._basemapsCollection = opts.basemapsCollection;
this._customBaselayersCollection = opts.customBaselayersCollection;
this._modals = opts.modals;
this._currentTab = opts.currentTab;
this._mosaicModel = opts.mosaicModel;
this.add_related_model(this._mosaicModel);
},
render: function () {
this.$el.html(
template({
name: this.model.getName(),
template: this.model.get('template')()
})
);
return this;
},
_onMouseEnter: function () {
this._mosaicModel.set('highlightedAdd', true);
},
_onMouseLeave: function () {
this._mosaicModel.set('highlightedAdd', false);
},
_onClick: function () {
var self = this;
var modal = this._modals.create(function (modalModel) {
var addBasemapModel = new AddBasemapModel({}, {
layerDefinitionsCollection: self._layerDefinitionsCollection,
basemapsCollection: self._basemapsCollection,
customBaselayersCollection: self._customBaselayersCollection,
currentTab: self._currentTab
});
return new AddBasemapView({
modalModel: modalModel,
createModel: addBasemapModel
});
}, {
breadcrumbsEnabled: true
});
modal.show();
}
});

View File

@@ -0,0 +1,59 @@
var BasemapMosaicView = require('./basemap-mosaic-view');
var BasemapMosaicModel = require('./basemap-mosaic-model');
var MosaicFormView = require('builder/components/mosaic-form-view');
/**
* Mosaic form view
*/
module.exports = MosaicFormView.extend({
initialize: function (opts) {
if (!opts.collection) throw new Error('Mosaic collection is required');
if (!opts.template) throw new Error('template is required');
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
if (!opts.modals) throw new Error('modals is required');
this.template = opts.template;
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._basemapsCollection = opts.basemapsCollection;
this._customBaselayersCollection = opts.customBaselayersCollection;
this._modals = opts.modals;
this._disabled = opts.disabled;
this._currentTab = opts.currentTab;
this._initBinds();
},
_initViews: function () {
this.mosaic = new BasemapMosaicView({
model: new BasemapMosaicModel(),
collection: this.collection,
layerDefinitionsCollection: this._layerDefinitionsCollection,
basemapsCollection: this._basemapsCollection,
customBaselayersCollection: this._customBaselayersCollection,
modals: this._modals,
disabled: this._disabled,
currentTab: this._currentTab
});
this.mosaic.model.bind('change:highlightedAdd', this._onChangeHighlighted, this);
if (!this.$('.js-selector').length) throw new Error('HTML element with js-selector class is required');
this.$('.js-selector').append(this.mosaic.render().el);
this.addView(this.mosaic);
},
_onChangeHighlighted: function () {
var item = this.collection.getHighlighted() || this.mosaic.model.getHighlighted() || this.collection.getSelected();
if (item) {
var $el = this.$('.js-highlight');
if ($el) {
$el.text(item.getName());
}
}
}
});

View File

@@ -0,0 +1,88 @@
/* global Image */
var template = require('builder/components/mosaic/mosaic-item.tpl');
var MosaicItemView = require('builder/components/mosaic/mosaic-item-view');
var BasemapMosaicRemoveView = require('./basemap-mosaic-remove-view');
var DEFAULT_SUBDOMAIN = 'a';
var DEFAULT_X_POSITION = 30;
var DEFAULT_Y_POSISTION = 24;
var DEFAULT_ZOOM = 6;
var DEFAULT_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAAXNSR0IArs4c6QAAAZ9JREFUWAntWUtugzAUNKbAJl1EYtcz9Ba5UE6SC+UWPUN3SNlkw78zVhJRCeIXbCup6ic5gP08Ho/x40ESBauq6iNJkgPKDpdb1j3RTuM4HlH2ZVl+JySXpukXygaksiViRVEsNd3qu65Tfd/frpdO8jxXEGO2GcTUMAxtXddnYH1qKqe1fof3IrlZpECVJA6xMgiyMdzws0N5CzTealiIlpGbBsKz77nZSYAc67ck+NIWCbouj1cFL/eNK6df/UW7FztKMT7ZTOJDDMQ5RUyJGYK2IMyBm6ax4nFQGxZBEIStWFcH2TSu3pZjiCX2StDCf1VzJLhKtkmnqOBEjFWn/0tBaaB+REoTqJkJ2wxJpM3FtPvGMgQlabrkCUEcCcF7Kf9UBT5xvN6DIZbYK8Hp7H2dR4KuSkYFo4KPKBAiYTWBmoHznvEdQpKm82ljw+I4DObElJghaJu59AWHA9qw6CMlR9+4i6mCi0UFXdRj3z+hIL8Ju040VP+TBrkjtn0bagQH3JbcSHB/+WDNCgc8r11bZOdncjPfWV/5b4gfOaaYLUG/QVoAAAAASUVORK5CYII=';
var DEFAULT_NAME = _t('editor.layers.basemap.custom-basemap');
module.exports = MosaicItemView.extend({
initialize: function (opts) {
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
this._basemapsCollection = opts.basemapsCollection;
this._disabled = opts.disabled;
this._initBinds();
},
render: function () {
this.$el.html(
template({
name: this._getName(),
template: this.model.get('template')(this._getImageURL())
})
);
this.$el.addClass('js-' + this.model.getValue());
this.$el.toggleClass('is-selected', !!this.model.get('selected'));
this._initViews();
return this;
},
_initViews: function () {
var basemapMosaicRemoveView = new BasemapMosaicRemoveView({
model: this.model,
basemapsCollection: this._basemapsCollection
});
this.addView(basemapMosaicRemoveView);
this.$el.append(basemapMosaicRemoveView.render().el);
},
_getImageURL: function () {
var self = this;
var url = this._lowerXYZ();
var image = new Image();
image.onerror = function () {
self.$('.js-thumbnailImg').attr('src', DEFAULT_IMG);
};
image.src = url;
return url;
},
_getSubdomain: function () {
var subdomains = this.model.get('subdomains'); // eg: 'abcd' or '1234'
return (subdomains && subdomains.length) ? subdomains[0] : DEFAULT_SUBDOMAIN;
},
_lowerXYZ: function () {
return this.model.get('urlTemplate')
.replace('{s}', this._getSubdomain())
.replace('{z}', DEFAULT_ZOOM)
.replace('{x}', DEFAULT_X_POSITION)
.replace('{y}', DEFAULT_Y_POSISTION);
},
_getName: function () {
var name = this.model.getName();
if (!name) {
name = this.model.get('order') ? DEFAULT_NAME + ' ' + this.model.get('order') : DEFAULT_NAME;
} else {
name.replace(/_/g, '');
}
return name;
}
});

View File

@@ -0,0 +1,22 @@
var Backbone = require('backbone');
/*
* List item model
*
*/
module.exports = Backbone.Model.extend({
defaults: {
highlightedAdd: false
},
getHighlighted: function () {
return this.get('highlightedAdd') && this;
},
getName: function () {
return 'Add';
}
});

View File

@@ -0,0 +1,86 @@
var CoreView = require('backbone/core-view');
var template = require('./basemap-mosaic-remove.tpl');
var CustomListCollection = require('builder/components/custom-list/custom-list-collection');
var ContextMenuView = require('builder/components/context-menu/context-menu-view');
module.exports = CoreView.extend({
tagName: 'span',
className: 'Mosaic-remove CDB-Shape-threePoints is-white is-small js-Mosaic-remove',
events: {
'click': '_onToggleContextMenuClicked'
},
initialize: function (opts) {
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
this._basemapsCollection = opts.basemapsCollection;
},
render: function () {
this.$el.html(template());
return this;
},
_hasContextMenu: function () {
return this._menuView;
},
_hideContextMenu: function () {
this.removeView(this._menuView);
this._menuView.clean();
delete this._menuView;
},
_showContextMenu: function (position) {
this._menuItems = new CustomListCollection([{
label: _t('editor.layers.basemap.remove-baselayer'),
val: 'remove-baselayer',
destructive: true
}]);
var triggerElementID = 'context-menu-trigger-' + this.model.cid;
this.$('.js-Mosaic-remove').attr('id', triggerElementID);
this._menuView = new ContextMenuView({
collection: this._menuItems,
triggerElementID: triggerElementID,
position: position
});
this._menuItems.bind('change:selected', function (menuItem) {
if (menuItem.get('val') === 'remove-baselayer') {
this._deleteBaselayer();
}
}, this);
this.add_related_model(this._menuItems);
this._menuView.model.bind('change:visible', function (model, isContextMenuVisible) {
if (this._hasContextMenu() && !isContextMenuVisible) {
this._hideContextMenu();
}
}, this);
this._menuView.show();
this.addView(this._menuView);
},
_deleteBaselayer: function () {
this._basemapsCollection.remove(this.model);
},
_onToggleContextMenuClicked: function (e) {
this.killEvent(e);
if (this._hasContextMenu()) {
this._hideContextMenu();
} else {
this._showContextMenu({
x: e.pageX + 215, // CustomList--small width
y: e.pageY
});
}
}
});

View File

@@ -0,0 +1,3 @@
<div class="CDB-Shape-threePointsItem"></div>
<div class="CDB-Shape-threePointsItem"></div>
<div class="CDB-Shape-threePointsItem"></div>

View File

@@ -0,0 +1,61 @@
var template = require('builder/components/mosaic/mosaic.tpl');
var MosaicView = require('builder/components/mosaic/mosaic-view');
var MosaicItemModel = require('builder/components/mosaic/mosaic-item-model');
var BasemapMosaicItemView = require('./basemap-mosaic-item-view');
var MosaicAddItemView = require('./basemap-mosaic-add-item-view');
module.exports = MosaicView.extend({
initialize: function (opts) {
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
if (!opts.modals) throw new Error('modals is required');
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._basemapsCollection = opts.basemapsCollection;
this._customBaselayersCollection = opts.customBaselayersCollection;
this._modals = opts.modals;
this._disabled = opts.disabled;
this._currentTab = opts.currentTab;
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._renderList();
this._createAddItem();
return this;
},
_createItem: function (mdl) {
var view = new BasemapMosaicItemView({
model: mdl,
disabled: this._disabled,
basemapsCollection: this._basemapsCollection
});
this.$('.js-mosaic').append(view.render().el);
this.addView(view);
},
_createAddItem: function () {
var view = new MosaicAddItemView({
model: new MosaicItemModel({
label: 'Add',
val: 'add',
template: function () {
return '<div class="Mosaic-button">+</div>';
}
}),
layerDefinitionsCollection: this._layerDefinitionsCollection,
basemapsCollection: this._basemapsCollection,
customBaselayersCollection: this._customBaselayersCollection,
mosaicModel: this.model,
modals: this._modals,
currentTab: this._currentTab
});
this.$('.js-mosaic').append(view.render().el);
this.addView(view);
}
});

View File

@@ -0,0 +1,6 @@
<div class="js-selector">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.layers.basemap.style.title-label') %></h2>
</div>
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--m js-highlight"><%- _t('editor.layers.basemap.style.select-style') %></p>
</div>

View File

@@ -0,0 +1,114 @@
var CoreView = require('backbone/core-view');
var template = require('./basemap-select.tpl');
var MosaicFormView = require('builder/components/mosaic-form-view');
var BasemapFormView = require('./basemap-form-view');
var MosaicCollection = require('builder/components/mosaic/mosaic-collection');
var BasemapMosaicFormView = require('./basemap-mosaic-form-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'layerDefinitionsCollection',
'basemapsCollection',
'customBaselayersCollection',
'selectedCategoryVal',
'modals'
];
module.exports = CoreView.extend({
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._baseLayer = this._layerDefinitionsCollection.getBaseLayer();
this._disabled = opts.disabled;
this._initCollection();
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
var category = this._selectedCategoryVal.toLowerCase();
switch (category) {
case 'color':
this._renderForm();
break;
case 'custom':
this._renderCustomMosaic();
break;
case 'nasa':
case 'tilejson':
case 'mapbox':
case 'wms':
this._renderCustomMosaic(category);
break;
default:
this._renderMosaic();
}
return this;
},
_initCollection: function () {
this._filteredBasemapsCollection = new MosaicCollection(
this._basemapsCollection.findByCategory(this._selectedCategoryVal)
);
},
_initBinds: function () {
this._filteredBasemapsCollection.bind('change:selected', function (mdl) {
if (mdl.get('selected')) {
var value = mdl.getValue();
this._changeBasemap(value);
}
}, this);
this.add_related_model(this._filteredBasemapsCollection);
},
_changeBasemap: function (value) {
var basemap = this._basemapsCollection.getByValue(value);
this._layerDefinitionsCollection.setBaseLayer(basemap.toJSON());
},
_renderMosaic: function () {
var view = new MosaicFormView({
collection: this._filteredBasemapsCollection,
template: require('./basemap-mosaic.tpl'),
disabled: this._disabled
});
this.addView(view);
this.$('.js-select').append(view.render().el);
},
_renderCustomMosaic: function (category) {
var view = new BasemapMosaicFormView({
collection: this._filteredBasemapsCollection,
template: require('./basemap-mosaic.tpl'),
layerDefinitionsCollection: this._layerDefinitionsCollection,
basemapsCollection: this._basemapsCollection,
customBaselayersCollection: this._customBaselayersCollection,
modals: this._modals,
disabled: this._disabled,
currentTab: category
});
this.addView(view);
this.$('.js-select').append(view.render().el);
},
_renderForm: function () {
var view = new BasemapFormView({
model: this._baseLayer,
basemapsCollection: this._basemapsCollection,
layerDefinitionsCollection: this._layerDefinitionsCollection,
disabled: this._disabled
});
this.addView(view);
this.$('.js-select').append(view.render().el);
}
});

View File

@@ -0,0 +1,4 @@
<div class="Editor-HeaderInfo">
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">2</div>
<div class="Editor-HeaderInfo-inner CDB-Text js-select"></div>
</div>

View File

@@ -0,0 +1,78 @@
var Backbone = require('backbone');
var BasemapModel = require('./basemap-model');
var _ = require('underscore');
/*
* Basemap collection, extends Mosaic collection
*/
module.exports = Backbone.Collection.extend({
model: BasemapModel,
initialize: function () {
this._initBinds();
},
_initBinds: function () {
this.bind('change:selected', this._onSelectedChange, this);
},
_onSelectedChange: function (changedModel, isSelected) {
if (isSelected) {
this.each(function (m) {
if (m.cid !== changedModel.cid && m.get('selected')) {
m.set('selected', false);
}
});
}
},
findByCategory: function (category) {
return this.where({ category: category });
},
getDefaultCategory: function () {
var defaultCategory = this.findWhere({ default: true });
defaultCategory = defaultCategory || this.first();
return defaultCategory.get('category');
},
getCategories: function () {
var categories = this.chain()
.map(function (model) { return model.get('category'); })
.concat('Custom')
.concat('NASA')
.concat('TileJSON')
.concat('Mapbox')
.concat('WMS')
.uniq()
.value();
return categories;
},
getSelected: function () {
return this.findWhere({ selected: true });
},
updateSelected: function (value) {
var newSelected = this.getByValue(value);
newSelected.set({ selected: true });
},
updateCategory: function (value, category) {
var newCategory = this.getByValue(value);
newCategory.set({ category: category });
},
getByValue: function (value) {
return _.first(this.where({ val: value }));
},
getThumbnailImage: function (urlTemplate, subdomains) {
return this._lowerXYZ(urlTemplate, subdomains);
}
});

View File

@@ -0,0 +1,2 @@
<div class="js-basemapHeader"></div>
<div class="js-basemapContent Editor-content"></div>

View File

@@ -0,0 +1,28 @@
<div class="Editor-contextSwitchers js-switchers
<% if (context === 'table') { %>Editor-contextSwitchers--table<% } %> ">
<ul class="u-flex u-alignRight Editor-contextSwitcher Editor-contextSwitcher--geom js-newGeometryView">
<% if (queryGeometryModel === 'point' || !queryGeometryModel) { %>
<li class="Editor-contextSwitcherItem js-newGeometryItem <% if (isReadOnly || !isVisible) { %>is-disabled<% } %>">
<div class="Editor-contextSwitcherButton js-newGeometry" data-feature-type="point" data-tooltip="<%- _t('editor.edit-feature.add-point') %>">
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="a"/><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="c"/><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="e"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M4 3h3v1H4v3H3V4H0V3h3V0h1"/><g transform="rotate(180 2.5 6)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" mask="url(#b)" stroke="#FFF" stroke-width="2"/></g><g transform="rotate(180 6 6)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" mask="url(#d)" stroke="#FFF" stroke-width="2"/></g><g transform="rotate(180 6 2.5)"><mask id="f" fill="#fff"><use xlink:href="#e"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" mask="url(#f)" stroke="#FFF" stroke-width="2"/></g></g></svg>
</div>
</li>
<% } %>
<% if (queryGeometryModel === 'line' || !queryGeometryModel) { %>
<li class="Editor-contextSwitcherItem js-newGeometryItem <% if (isReadOnly || !isVisible) { %>is-disabled<% } %>">
<div class="Editor-contextSwitcherButton js-newGeometry" data-feature-type="line" data-tooltip="<%- _t('editor.edit-feature.add-line') %>">
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="a"/><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="c"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M11.5 5l-.707-.707-6.354 6.853.704.708"/><g transform="translate(11 2)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" mask="url(#b)" stroke="#FFF" stroke-width="2"/></g><g transform="translate(2 11)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" mask="url(#d)" stroke="#FFF" stroke-width="2"/></g><path fill="#FFF" d="M3 4H0V3h3V0h1v3h3v1H4v3H3"/></g></svg>
</div>
</li>
<% } %>
<% if (queryGeometryModel === 'polygon' || !queryGeometryModel) { %>
<li class="Editor-contextSwitcherItem js-newGeometryItem <% if (isReadOnly || !isVisible) { %>is-disabled<% } %>">
<div class="Editor-contextSwitcherButton js-newGeometry" data-feature-type="polygon" data-tooltip="<%- _t('editor.edit-feature.add-polygon') %>">
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="a"/><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="c"/><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" id="e"/></defs><g fill="none" fill-rule="evenodd"><path d="M13 11.5V5h-1v7h1m-.5-7.5l-.707-.707-7.647 7.353-.353.354.707.707.354-.353M4 3h3v1H4v3H3V4H0V3h3V0h1" fill="#FFF"/><path fill="#FFF" d="M4.5 13H12v-1H4v1"/><g transform="rotate(180 7 7)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" stroke="#FFF" stroke-width="2" mask="url(#b)"/></g><g transform="rotate(180 7 2.5)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" stroke="#FFF" stroke-width="2" mask="url(#d)"/></g><g transform="rotate(180 2.5 7)"><mask id="f" fill="#fff"><use xlink:href="#e"/></mask><path d="M0 2h3v1H0V2zm0-2h3v1H0V0zm2 1h1v1H2V1zM0 1h1v1H0V1z" stroke="#FFF" stroke-width="2" mask="url(#f)"/></g></g></svg>
</div>
</li>
<% } %>
</ul>
<div class="u-flex u-alignCenter Editor-contextSwitcher Editor-contextSwitcher--view js-mapTableView"></div>
</div>

View File

@@ -0,0 +1,49 @@
<div class="u-flex u-justifyCenter">
<div class="Modal-inner Modal-inner--grid u-flex u-justifyCenter">
<div class="Modal-icon">
<svg width="24px" height="25px" viewbox="521 436 24 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M524.5,440 L540.5,440 L540.5,460 L524.5,460 L524.5,440 Z M528.5,437 L536.5,437 L536.5,440 L528.5,440 L528.5,437 Z M522,440 L544,440 L522,440 Z M528.5,443.5 L528.5,455.5 L528.5,443.5 Z M532.5,443.5 L532.5,455.5 L532.5,443.5 Z M536.5,443.5 L536.5,455.5 L536.5,443.5 Z" id="Shape" stroke="#F19243" stroke-width="1" fill="none"/>
</svg>
</div>
<div>
<h2 class="CDB-Text CDB-Size-huge is-light u-bSpace--xl"><%- _t('editor.layers.delete.title', { layerName: layerName }) %></h2>
<p class="CDB-Text CDB-Size-large u-altTextColor"><%= _t('editor.layers.delete.desc', { layerVisName: layerVisName }) %></p>
<ul class="Modal-listText">
<% if (affectedItemsMessages.length > 0) { %>
<li class="Modal-listTextItem">
<p class="CDB-Text CDB-Size-large">
<%= _t('editor.layers.delete.affected-items') %>
<% for (var i = 0; i < affectedItemsMessages.length; i++) { %>
<span class="CDB-Text is-semibold"><%- affectedItemsMessages[i] %></span>
<% if (i < (affectedItemsMessages.length - 2)) { %>
,
<% } else if (i == (affectedItemsMessages.length - 2)) { %>
<%= _t('editor.layers.delete.and') %>
<% } %>
<% } %>
.</p>
</li>
<% } %>
<li class="Modal-listTextItem">
<p class="CDB-Text CDB-Size-large"><%= _t('editor.layers.delete.link-to-export') %></p>
</li>
</ul>
<ul class="Modal-listActions u-flex u-alignCenter">
<li class="Modal-listActionsitem">
<button class="CDB-Button CDB-Button--secondary CDB-Button--big js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">
<%- _t('editor.layers.delete.cancel') %>
</span>
</button>
</li>
<li class="Modal-listActionsitem">
<button class="CDB-Button CDB-Button--primary CDB-Button--big js-confirm">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">
<%- _t('editor.layers.delete.confirm') %>
</span>
</button>
</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,403 @@
var _ = require('underscore');
var $ = require('jquery');
var CoreView = require('backbone/core-view');
var template = require('./edit-feature-content.tpl');
var EditFeatureActionView = require('./edit-feature-content-views/edit-feature-action-view');
var EditFeatureControlView = require('./edit-feature-content-views/edit-feature-control-view');
var EditFeatureHeaderView = require('./edit-feature-content-views/edit-feature-header-view');
var VisTableModel = require('builder/data/visualization-table-model');
var EditFeatureInnerView = require('./edit-feature-content-views/edit-feature-inner-view');
var PanelWithOptionsView = require('builder/components/view-options/panel-with-options-view');
var ScrollView = require('builder/components/scroll/scroll-view');
var Notifier = require('builder/components/notifier/notifier');
var EditFeatureGeometryFormModel = require('./edit-feature-content-views/edit-feature-geometry-form-model');
var EditFeatureGeometryPointFormModel = require('./edit-feature-content-views/edit-feature-geometry-point-form-model');
var EditFeatureAttributesFormModel = require('./edit-feature-content-views/edit-feature-attributes-form-model');
var StyleHelper = require('builder/helpers/style');
var StyleConstants = require('builder/components/form-components/_constants/_style');
var StylesFactory = require('builder/editor/style/styles-factory');
var Router = require('builder/routes/router');
var NOTIFICATION_ID = 'editFeatureNotification';
var NOTIFICATION_ERROR_TEMPLATE = _.template("<span class='u-errorTextColor'><%- title %></span>");
var REQUIRED_OPTS = [
'stackLayoutModel',
'layerDefinitionModel',
'configModel',
'mapModeModel',
'editorModel',
'modals'
];
var GEOMETRY_TYPE = 'simple_geom';
module.exports = CoreView.extend({
className: 'Editor-content',
events: {
'click .js-back': 'cleanAndGoBack'
},
initialize: function (opts) {
_.each(REQUIRED_OPTS, function (item) {
if (opts[item] === undefined) throw new Error(item + ' is required');
this['_' + item] = opts[item];
}, this);
this._featureModel = this._mapModeModel.getFeatureDefinition();
this._tableName = '';
this._url = '';
this._editorModel.set({
edition: false
});
this._getTable();
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
return this;
},
_initBinds: function () {
if (this._querySchemaModel.get('status') !== 'fetched') {
// status can be: fetched, unavailable, fetching
this._querySchemaModel.bind('change:status', this.render, this);
this._querySchemaModel.fetch();
}
this._queryGeometryModel.bind('change:' + GEOMETRY_TYPE, this._checkNewLayerGeometryTypeDefinition, this);
this.listenTo(this._editorModel, 'cancelPreviousEditions', this.clean);
this.listenTo(this._featureModel, 'updateFeature', this._updateForms);
this.listenTo(this._featureModel, 'destroyFeature', this._onDestroyFeature);
this.listenTo(this._featureModel, 'destroyFeatureSuccess', this._onDestroyFeatureSuccess);
this.listenTo(this._featureModel, 'destroyFeatureFailed', this._onDestroyFeatureFailed);
this.listenTo(this._featureModel, 'addFeature', this._onAddFeature);
this.listenTo(this._featureModel, 'saveFeature', this._onSaveFeature);
this.listenTo(this._featureModel, 'saveFeatureSuccess', this._onSaveFeatureSuccess);
this.listenTo(this._featureModel, 'saveFeatureFailed', this._onSaveFeatureFailed);
},
_initViews: function () {
if (this._featureModel.isNew()) {
this._addRow();
} else {
this._renderInfo();
}
},
_updateForms: function () {
this._updateGeometry();
this._attributesFormModel.trigger('updateFeature');
},
_updateGeometry: function () {
var geojson = null;
try {
geojson = JSON.parse(this._featureModel.get('the_geom'));
} catch (err) {
// if the geom is not a valid json value
}
var attrs;
if (this._featureModel.isPoint()) {
attrs = {
lng: geojson && geojson.coordinates[0],
lat: geojson && geojson.coordinates[1]
};
} else {
attrs = {
the_geom: this._featureModel.get('the_geom')
};
}
this._geometryFormModel.set(attrs);
this._geometryFormModel.trigger('updateFeature', attrs);
},
_addRow: function () {
this._renderInfo();
},
_renderInfo: function () {
this._renderHeader();
this._renderContent();
},
_renderHeader: function () {
if (this._headerView) {
this.removeView(this._headerView);
this._headerView.clean();
}
this._headerView = new EditFeatureHeaderView({
url: this._url,
tableName: this._tableName,
layerDefinitionModel: this._layerDefinitionModel,
model: this._featureModel,
modals: this._modals,
isNew: this._featureModel.isNew(),
backAction: this.cleanAndGoBack.bind(this)
});
this.addView(this._headerView);
this.$('.js-editFeatureHeader').html(this._headerView.render().el);
},
_renderContent: function () {
if (this._contentView) {
this.removeView(this._contentView);
this._contentView.clean();
}
var geojson = null;
try {
geojson = JSON.parse(this._featureModel.get('the_geom'));
} catch (err) {
// if the geom is not a valid json value
}
if (this._featureModel.isPoint()) {
this._geometryFormModel = new EditFeatureGeometryPointFormModel({
lng: geojson && geojson.coordinates[0],
lat: geojson && geojson.coordinates[1]
}, {
featureModel: this._featureModel
});
} else {
this._geometryFormModel = new EditFeatureGeometryFormModel({
the_geom: this._featureModel.get('the_geom')
}, {
featureModel: this._featureModel
});
}
this._attributesFormModel = new EditFeatureAttributesFormModel(this._featureModel.toJSON(), {
featureModel: this._featureModel,
columnsCollection: this._sourceNode.querySchemaModel.columnsCollection,
configModel: this._configModel,
nodeDefModel: this._layerDefinitionModel.getAnalysisDefinitionNodeModel()
});
this._contentView = new PanelWithOptionsView({
className: 'Editor-content',
editorModel: this._editorModel,
createContentView: function () {
return new ScrollView({
createContentView: function () {
return new EditFeatureInnerView({
featureModel: this._featureModel,
geometryFormModel: this._geometryFormModel,
attributesFormModel: this._attributesFormModel
});
}.bind(this)
});
}.bind(this),
createControlView: function () {
return new EditFeatureControlView();
},
createActionView: function () {
return new EditFeatureActionView({
model: this.model,
featureModel: this._featureModel,
geometryFormModel: this._geometryFormModel,
attributesFormModel: this._attributesFormModel
});
}.bind(this)
});
this.addView(this._contentView);
this.$('.js-editFeatureContent').html(this._contentView.render().el);
},
_onDestroyFeature: function () {
if (Notifier.getNotification(NOTIFICATION_ID)) {
Notifier.removeNotification(NOTIFICATION_ID);
}
this.notification = Notifier.addNotification({
id: NOTIFICATION_ID,
status: 'loading',
info: _t('notifications.edit-feature.destroy.loading'),
closable: false
});
},
_onDestroyFeatureSuccess: function () {
this._onUpdateFeature('destroy');
this.notification.set({
status: 'success',
info: _t('notifications.edit-feature.destroy.success'),
closable: true
});
var layerId = this._layerDefinitionModel.get('id');
Router.goToStyleTab(layerId);
},
_onDestroyFeatureFailed: function (mdl, error) {
this.notification.set({
status: 'error',
info: NOTIFICATION_ERROR_TEMPLATE({
title: _t('notifications.edit-feature.destroy.error')
}),
closable: true
});
},
_onAddFeature: function () {
if (Notifier.getNotification(NOTIFICATION_ID)) {
Notifier.removeNotification(NOTIFICATION_ID);
}
this.notification = Notifier.addNotification({
id: NOTIFICATION_ID,
status: 'loading',
info: _t('notifications.edit-feature.adding'),
closable: false
});
},
_onSaveFeature: function () {
if (Notifier.getNotification(NOTIFICATION_ID)) {
Notifier.removeNotification(NOTIFICATION_ID);
}
this.notification = Notifier.addNotification({
id: NOTIFICATION_ID,
status: 'loading',
info: _t('notifications.edit-feature.save.loading'),
closable: false
});
},
_onSaveFeatureSuccess: function (operation, model) {
this._onUpdateFeature(operation);
this._needReloadVis() && this._mapModeModel.trigger('reloadVis');
this.notification.set({
status: 'success',
info: _t('notifications.edit-feature.save.success'),
closable: true
});
Router.editFeature(model);
},
_checkNewLayerGeometryTypeDefinition: function (queryGeometryModel, currentGeometryType) {
var layerGeometryTypeHasBeenDefined = currentGeometryType && !(queryGeometryModel.previous(GEOMETRY_TYPE));
if (layerGeometryTypeHasBeenDefined) {
var defaultStyle = _.extend({ type: StyleConstants.Type.SIMPLE },
StylesFactory.getDefaultStyleAttrsByType(StyleConstants.Type.SIMPLE, currentGeometryType)
);
this._layerDefinitionModel.styleModel.set(defaultStyle);
}
},
_onSaveFeatureFailed: function (mdl, error) {
this.notification.set({
status: 'error',
info: NOTIFICATION_ERROR_TEMPLATE({
title: _t('notifications.edit-feature.save.error')
}),
closable: true
});
},
_onUpdateFeature: function (operation) {
var self = this;
this._queryGeometryModel.hasValueAsync()
.then(function (hasGeom) {
if (operation !== 'save' || !hasGeom) {
self._queryGeometryModel.resetFetch();
}
});
if (operation === 'add' || operation === 'destroy') {
this._rowsCollection.resetFetch();
}
},
_needReloadVis: function () {
var style = this._layerDefinitionModel.styleModel;
var color = StyleHelper.getColorAttribute(style);
var size = StyleHelper.getSizeAttribute(style);
var changed = _.keys(this._featureModel.changedAttributes());
return (color && _.contains(changed, color) || size && _.contains(changed, size));
},
_getTable: function () {
this._sourceNode = this._getSourceNode();
if (this._sourceNode) {
var tableName = this._sourceNode.get('table_name');
this._visTableModel = new VisTableModel({
id: tableName,
table: {
name: tableName
}
}, {
configModel: this._configModel
});
this._querySchemaModel = this._sourceNode.querySchemaModel;
this._queryGeometryModel = this._sourceNode.queryGeometryModel;
this._rowsCollection = this._sourceNode.queryRowsCollection;
}
if (this._visTableModel) {
var tableModel = this._visTableModel.getTableModel();
this._tableName = tableModel.getUnquotedName();
this._url = this._visTableModel && this._visTableModel.datasetURL();
}
},
_getSourceNode: function () {
var node = this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
var source;
if (node.get('type') === 'source') {
source = node;
} else {
var primarySource = node.getPrimarySource();
if (primarySource && primarySource.get('type') === 'source') {
source = primarySource;
}
}
return source;
},
_destroyContextOverlay: function () {
$('.js-editOverlay').fadeOut(200, function () {
$('.js-editOverlay').remove();
});
},
cleanAndGoBack: function () {
this.clean();
this._mapModeModel.enterViewingMode();
Router.goToPreviousRoute({
options: { replace: true }
});
},
clean: function () {
this._destroyContextOverlay();
CoreView.prototype.clean.apply(this);
}
});

View File

@@ -0,0 +1,29 @@
<div class="u-flex u-justifyCenter">
<div class="Modal-inner Modal-inner--grid u-flex u-justifyCenter">
<div class="Modal-icon">
<svg width="24px" height="25px" viewbox="521 436 24 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M524.5,440 L540.5,440 L540.5,460 L524.5,460 L524.5,440 Z M528.5,437 L536.5,437 L536.5,440 L528.5,440 L528.5,437 Z M522,440 L544,440 L522,440 Z M528.5,443.5 L528.5,455.5 L528.5,443.5 Z M532.5,443.5 L532.5,455.5 L532.5,443.5 Z M536.5,443.5 L536.5,455.5 L536.5,443.5 Z" id="Shape" stroke="#F19243" stroke-width="1" fill="none"/>
</svg>
</div>
<div>
<h2 class=" CDB-Text CDB-Size-huge is-light u-bSpace--xl"><%- _t('components.modals.edit-feature.delete.title') %></h2>
<p class="CDB-Text CDB-Size-large u-altTextColor"><%- _t('components.modals.edit-feature.delete.desc') %></p>
<ul class="Modal-listActions u-flex u-alignCenter">
<li class="Modal-listActionsitem">
<button class="CDB-Button CDB-Button--secondary CDB-Button--big js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">
<%- _t('components.modals.edit-feature.delete.cancel') %>
</span>
</button>
</li>
<li class="Modal-listActionsitem">
<button class="CDB-Button CDB-Button--primary CDB-Button--big js-confirm">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">
<%- _t('components.modals.edit-feature.delete.confirm') %>
</span>
</button>
</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,84 @@
var CoreView = require('backbone/core-view');
var template = require('./edit-feature-action.tpl');
/**
* View representing the apply button for a form
*/
module.exports = CoreView.extend({
events: {
'click .js-save': '_onSaveClicked'
},
initialize: function (opts) {
if (!opts.featureModel) throw new Error('featureModel is required');
if (!opts.geometryFormModel) throw new Error('geometryFormModel is required');
if (!opts.attributesFormModel) throw new Error('attributesFormModel is required');
this._featureModel = opts.featureModel;
this._geometryFormModel = opts.geometryFormModel;
this._attributesFormModel = opts.attributesFormModel;
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template({
label: this._featureModel.isNew() ? _t('editor.edit-feature.add') : _t('editor.edit-feature.save'),
isDisabled: !this._canSave()
}));
return this;
},
_initBinds: function () {
this.model.on('change', this.render, this);
this._featureModel.on('change', function () {
this.model.set('hasChanges', true);
}, this);
this.add_related_model(this._featureModel);
this._geometryFormModel.bind('validate', function (isValid) {
this.model.set('isValidGeometry', !isValid);
}, this);
this.add_related_model(this._geometryFormModel);
this._attributesFormModel.bind('validate', function (isValid) {
this.model.set('isValidAttributes', !isValid);
}, this);
this.add_related_model(this._attributesFormModel);
},
_canSave: function () {
var hasChanges = this.model.get('hasChanges');
var isValidGeometry = this.model.get('isValidGeometry');
var isValidAttributes = this.model.get('isValidAttributes');
return isValidGeometry && isValidAttributes && hasChanges;
},
_onSaveClicked: function () {
if (this._canSave()) {
this._saveFeature();
}
},
_saveFeature: function () {
var self = this;
var operation = this._featureModel.isNew() ? 'add' : 'save';
var event = operation + 'Feature';
this._featureModel.trigger(event);
this._featureModel.save({
success: function () {
self._featureModel.trigger('saveFeatureSuccess', operation, self._featureModel);
self.model.set('hasChanges', false);
},
error: function () {
self._featureModel.trigger('saveFeatureFailed');
}
});
}
});

View File

@@ -0,0 +1,5 @@
<button class="CDB-Button CDB-Button--primary js-save <% if (isDisabled) { %>is-disabled<% } %>">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">
<%- label %>
</span>
</button>

View File

@@ -0,0 +1,135 @@
var _ = require('underscore');
var Backbone = require('backbone');
var ColumnRowData = require('builder/data/column-row-data');
var DialogConstants = require('builder/components/form-components/_constants/_dialogs');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var FIELDTYPE_TO_FORMTYPE = {
string: 'Text',
boolean: 'Radio',
number: 'Number',
date: 'DateTime',
geometry: 'Text'
};
var BLACKLISTED_COLUMNS = ['created_at', 'the_geom', 'the_geom_webmercator', 'updated_at'];
var REQUIRED_OPTS = [
'featureModel',
'columnsCollection',
'configModel',
'nodeDefModel'
];
module.exports = Backbone.Model.extend({
initialize: function (attrs, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._generateSchema();
this._initBinds();
},
_initBinds: function () {
this.bind('change', this._onChange, this);
},
_onChange: function () {
_.each(this.changed, function (val, key) {
this._featureModel.set(key, val);
}, this);
},
_getInputType: function (columnRowData) {
var rows = columnRowData.getRows();
return rows && rows.length ? 'Suggest' : 'Text';
},
_getSupportedColumnType: function (columnType) {
var supportedColumnType = _.find(_.keys(FIELDTYPE_TO_FORMTYPE), function (fieldType) {
return fieldType === columnType;
});
return supportedColumnType || 'string';
},
_generateSchema: function () {
var self = this;
this.schema = {};
this._columnsCollection.each(function (model) {
var columnName = model.get('name');
var columnType = this._getSupportedColumnType(model.get('type'));
if (!_.contains(BLACKLISTED_COLUMNS, columnName)) {
// default field type
this.schema[columnName] = {
type: FIELDTYPE_TO_FORMTYPE[columnType]
};
if (columnType === 'date') {
this.schema[columnName].dialogMode = DialogConstants.Mode.FLOAT;
}
if (columnType === 'string') {
var columnRowData = new ColumnRowData({
column: columnName
}, {
nodeDefModel: this._nodeDefModel,
configModel: this._configModel
});
columnRowData.bind('columnsFetched', function (rows) {
var type = self._getInputType(this);
if (self.schema[columnName].type !== type) {
self.schema[columnName].type = type;
if (type === 'Suggest') {
self.schema[columnName].dialogMode = DialogConstants.Mode.FLOAT;
}
self.schema[columnName].editorAttrs = {
showSearch: true,
defaultValue: true,
allowFreeTextInput: true,
collectionData: rows
};
self._onChangeSchema();
}
});
columnRowData.fetch();
}
if (columnType === 'number') {
this.schema[columnName].isFormatted = true;
this.schema[columnName].showSlider = false;
}
if (columnType === 'boolean') {
this.schema[columnName].options = [
{
label: _t('form-components.editors.radio.true'),
val: true
}, {
label: _t('form-components.editors.radio.false'),
val: false
}, {
label: _t('form-components.editors.radio.null'),
val: null
}
];
}
if (columnName === 'cartodb_id') {
this.schema[columnName].isFormatted = false;
this.schema[columnName].editorAttrs = {
disabled: true
};
}
}
}, this);
},
_onChangeSchema: function () {
this.trigger('changeSchema');
}
});

View File

@@ -0,0 +1,48 @@
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var template = require('./edit-feature-attributes-form.tpl');
module.exports = CoreView.extend({
module: 'editor:layers:edit-feature-content-views:edit-feature-attributes-form-view',
render: function () {
this.clearSubViews();
this.$el.html(template());
this._generateForms();
this.model.bind('changeSchema', this._generateForms, this);
return this;
},
_generateForms: function () {
var self = this;
if (this._formView) {
this._formView.remove();
}
this._formView = new Backbone.Form({
model: this.model
});
this._formView.bind('change', function () {
var validate = this.validate();
self.model.trigger('validate', !!validate);
if (!validate) {
this.commit();
}
});
this.$('.js-form').append(this._formView.render().$el);
},
clean: function () {
if (this._formView) {
this._formView.remove();
}
}
});

View File

@@ -0,0 +1,11 @@
<div class="Editor-HeaderInfo">
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">2</div>
<div class="Editor-HeaderInfo-inner CDB-Text">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.edit-feature.attributes') %></h2>
</div>
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--m"><%- _t('editor.edit-feature.attributes-columns') %></p>
<div class="js-form"></div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
var CoreView = require('backbone/core-view');
var template = require('./edit-feature-control.tpl');
module.exports = CoreView.extend({
render: function () {
this.$el.html(template());
return this;
}
});

View File

@@ -0,0 +1,3 @@
<button class="CDB-Button u-upperCase js-back">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-actionTextColor"><%- _t('editor.edit-feature.cancel') %></span>
</button>

View File

@@ -0,0 +1,96 @@
var _ = require('underscore');
var Backbone = require('backbone');
var cdb = require('internal-carto.js');
module.exports = Backbone.Model.extend({
_COORDINATES_OPTIONS: {
min_lat: -90,
max_lat: 90,
min_lng: -180,
max_lng: 180
},
initialize: function (attrs, opts) {
if (!opts.featureModel) throw new Error('featureModel is required');
this._featureModel = opts.featureModel;
this._generateSchema();
},
_formatCoordinate: function (coordinate) {
if (this._isValidCoordinate(coordinate)) {
return parseFloat(parseFloat(coordinate).toFixed(8));
}
},
_isValidCoordinate: function (coordinate) {
return _.isNumber(coordinate) || _.isString(coordinate);
},
_getLatitude: function (pos) {
return pos && pos[0];
},
_getLongitude: function (pos) {
return pos && pos[1];
},
_validateGeom: function (value) {
var validation;
var coordinates;
var geoJSON = JSON.parse(value);
if (this._featureModel.isLine()) {
coordinates = cdb.helpers.GeoJSONHelper.getPolylineLatLngsFromGeoJSONCoords(geoJSON);
} else {
coordinates = cdb.helpers.GeoJSONHelper.getPolygonLatLngsFromGeoJSONCoords(geoJSON);
}
var errors = _.chain(coordinates)
.reduce(function (memo, coordinate) {
var latitude = this._formatCoordinate(this._getLatitude(coordinate));
var longitude = this._formatCoordinate(this._getLongitude(coordinate));
var latError = (latitude > this._COORDINATES_OPTIONS.max_lat || latitude < this._COORDINATES_OPTIONS.min_lat);
if (latError) {
memo.push(_t('editor.edit-feature.out-of-bounds-lat'));
}
var lngError = (longitude > this._COORDINATES_OPTIONS.max_lng || longitude < this._COORDINATES_OPTIONS.min_lng);
if (lngError) {
memo.push(_t('editor.edit-feature.out-of-bounds-lng'));
}
return memo;
}, [], this)
.uniq()
.value();
if (errors.length) {
validation = {
message: errors.join(' ')
};
}
return validation;
},
_generateSchema: function () {
this.schema = {};
this.schema.the_geom = {
type: 'Text',
validators: ['required', this._validateGeom.bind(this)],
editorAttrs: {
disabled: true
},
hasCopyButton: this._featureModel.has('the_geom')
};
}
});

View File

@@ -0,0 +1,53 @@
var Backbone = require('backbone');
var _ = require('underscore');
var CoreView = require('backbone/core-view');
var template = require('./edit-feature-geometry-form.tpl');
module.exports = CoreView.extend({
render: function () {
this.clearSubViews();
this.$el.html(template());
this._generateForms();
return this;
},
_generateForms: function () {
if (this._formView) {
this._formView.remove();
}
this._formView = new Backbone.Form({
model: this.model
});
this._formView.bind('change', this._validateForm, this);
this.model.bind('updateFeature', function (attrs) {
_.each(attrs, function (value, key) {
this._formView.fields[key].editor.setValue(value);
}, this);
this._validateForm();
}, this);
this.$('.js-form').append(this._formView.render().$el);
},
_validateForm: function () {
var validate = this._formView.validate();
this.model.trigger('validate', !!validate);
if (!validate) {
this._formView.commit();
}
},
clean: function () {
if (this._formView) {
this._formView.remove();
}
}
});

View File

@@ -0,0 +1,10 @@
<div class="Editor-HeaderInfo">
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">1</div>
<div class="Editor-HeaderInfo-inner CDB-Text">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.edit-feature.geometry') %></h2>
</div>
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--m"><%- _t('editor.edit-feature.geometry-edit') %></p>
<div class="js-form"></div>
</div>
</div>

View File

@@ -0,0 +1,130 @@
var _ = require('underscore');
var EditFeatureGeometryFormModel = require('./edit-feature-geometry-form-model');
var getCoordinatesFromGeoJSON = function (geoJSON) {
if (geoJSON && geoJSON.coordinates && geoJSON.coordinates.length === 2) {
return geoJSON.coordinates;
}
};
module.exports = EditFeatureGeometryFormModel.extend({
initialize: function () {
EditFeatureGeometryFormModel.prototype.initialize.apply(this, arguments);
this._initBinds();
},
_initBinds: function () {
this.bind('change:lat', this._onLatLngChange, this);
this.bind('change:lng', this._onLatLngChange, this);
},
_onLatLngChange: function () {
var newGeoJSON = this._toGeoJSON();
var currentGeoJSON = {};
if (this._featureModel.get('the_geom')) {
currentGeoJSON = JSON.parse(this._featureModel.get('the_geom'));
}
// Only set `the_geom` if there's new geoJSON
if (!_.isEqual(newGeoJSON, currentGeoJSON)) {
this._featureModel.set('the_geom', JSON.stringify(newGeoJSON));
}
},
_toGeoJSON: function () {
var latitude = this._formatCoordinate(this._getLatitude());
var longitude = this._formatCoordinate(this._getLongitude());
if (this._isValidCoordinate(latitude) && this._isValidCoordinate(longitude)) {
return {
'type': 'Point',
'coordinates': [ longitude, latitude ]
};
}
},
_getLatitude: function () {
if (this.has('lat')) {
return this.get('lat');
}
var geoJSON = JSON.parse(this._featureModel.get('the_geom'));
var geoJSONCoordinates = getCoordinatesFromGeoJSON(geoJSON);
var latitudeFromGeoJSON = geoJSONCoordinates && geoJSONCoordinates[1];
if (latitudeFromGeoJSON) {
return latitudeFromGeoJSON;
}
},
_getLongitude: function () {
if (this.has('lng')) {
return this.get('lng');
}
var geoJSON = JSON.parse(this._featureModel.get('the_geom'));
var geoJSONCoordinates = getCoordinatesFromGeoJSON(geoJSON);
var longitudeFromGeoJSON = geoJSONCoordinates && geoJSONCoordinates[1];
if (longitudeFromGeoJSON) {
return longitudeFromGeoJSON;
}
},
_validateLatitude: function (value, formValues) {
var numericValue = +value;
var error = {
message: _t('editor.edit-feature.valid-lat')
};
if (_.isNumber(numericValue) &&
(numericValue >= this._COORDINATES_OPTIONS.min_lat && numericValue <= this._COORDINATES_OPTIONS.max_lat)) {
return null; // valid latitude
}
if (_.isNumber(numericValue) &&
(numericValue > this._COORDINATES_OPTIONS.max_lat || numericValue < this._COORDINATES_OPTIONS.min_lat)) {
error.message = _t('editor.edit-feature.out-of-bounds-lat');
}
return error;
},
_validateLongitude: function (value, formValues) {
var numericValue = +value;
var error = {
message: _t('editor.edit-feature.valid-lng')
};
if (_.isNumber(numericValue) &&
(numericValue >= this._COORDINATES_OPTIONS.min_lng && numericValue <= this._COORDINATES_OPTIONS.max_lng)) {
return null; // valid longitude
}
if (_.isNumber(numericValue) &&
(numericValue > this._COORDINATES_OPTIONS.max_lng || numericValue < this._COORDINATES_OPTIONS.min_lng)) {
error.message = _t('editor.edit-feature.out-of-bounds-lng');
}
return error;
},
_generateSchema: function () {
this.schema = {};
this.schema.lng = {
type: 'Number',
validators: ['required', this._validateLongitude.bind(this)],
showSlider: false
};
this.schema.lat = {
type: 'Number',
validators: ['required', this._validateLatitude.bind(this)],
showSlider: false
};
}
});

View File

@@ -0,0 +1,87 @@
var CoreView = require('backbone/core-view');
var template = require('./edit-feature-header.tpl');
var ContextMenuFactory = require('builder/components/context-menu-factory-view');
var ConfirmationView = require('builder/components/modals/confirmation/modal-confirmation-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var templateConfirmation = require('./delete-feature-confirmation.tpl');
var REQUIRED_OPTS = [
'url',
'tableName',
'modals',
'isNew',
'layerDefinitionModel'
];
module.exports = CoreView.extend({
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
var letter = this._layerDefinitionModel.get('letter');
var featureType = this.model.getFeatureType() ? _t('editor.edit-feature.features.' + this.model.getFeatureType()) : _t('editor.edit-feature.features.geometry');
var breadcrumbLabel = this._isNew ? _t('editor.edit-feature.add-' + featureType) : _t('editor.edit-feature.edit', { featureType: featureType });
this.$el.html(
template({
url: this._url,
layerName: this._layerDefinitionModel.getName(),
tableName: this._tableName,
bgColor: this._layerDefinitionModel.getColor(),
letter: letter,
featureType: featureType,
breadcrumbLabel: breadcrumbLabel
})
);
if (!this._isNew) {
this._initViews();
}
return this;
},
_initViews: function () {
var menuItems = [{
label: _t('editor.edit-feature.delete', { featureType: _t('editor.edit-feature.features.' + this.model.getFeatureType()) }),
val: 'delete-feature',
destructive: true,
action: this._confirmDeleteFeature.bind(this)
}];
this._contextMenuFactory = new ContextMenuFactory({
menuItems: menuItems
});
this.$('.js-context-menu').append(this._contextMenuFactory.render().el);
this.addView(this._contextMenuFactory);
},
_confirmDeleteFeature: function () {
this._modals.create(function (modalModel) {
return new ConfirmationView({
modalModel: modalModel,
template: templateConfirmation,
runAction: this._destroyFeature.bind(this)
});
}.bind(this));
},
_destroyFeature: function () {
this.model.trigger('destroyFeature');
this.model.destroy({
success: function () {
this.model.trigger('destroyFeatureSuccess');
}.bind(this),
error: function () {
this.model.trigger('destroyFeatureFailed');
}.bind(this)
});
}
});

View File

@@ -0,0 +1,38 @@
<ul class="Editor-breadcrumb">
<li class="Editor-breadcrumbItem CDB-Text CDB-Size-medium u-actionTextColor">
<button class="js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev Size-large u-rSpace"></i>
<span class="Editor-breadcrumbLink"><%- _t('back') %></span>
</button>
</li>
<li class="Editor-breadcrumbItem CDB-Text CDB-Size-medium">
<span class="Editor-breadcrumbSep"> / </span>
<%- breadcrumbLabel %>
</li>
</ul>
<div class="Editor-HeaderInfoEditor Editor-HeaderInfoEditor--layer">
<div class="Editor-HeaderInfo-inner Editor-HeaderInfo-inner--wide u-ellipsis">
<div class="Editor-HeaderInfo-title u-bSpace">
<span class="CDB-SelectorLayer-letter CDB-Text CDB-Size-small u-whiteTextColor u-tSpace--m u-rSpace--m u-upperCase" style="background-color: <%- bgColor %>;">
<%- letter %>
</span>
<h2 class="Inline-editor">
<div class="CDB-Text CDB-Size-huge is-light u-ellipsis">
<%- layerName %>
</div>
</h2>
</div>
<div class="Editor-HeaderInfo-source u-flex">
<p class="CDB-Text CDB-Size-small u-ellipsis">
<a href="<%- url %>" target="_blank" title="<%- tableName %>" class="Editor-headerLayerName"><%- tableName %></a>
</p>
</div>
</div>
<div class="u-flex u-tSpace-xl js-context-menu"></div>
</div>

View File

@@ -0,0 +1,47 @@
var CoreView = require('backbone/core-view');
var EditFeatureGeometryFormView = require('./edit-feature-geometry-form-view');
var EditFeatureAttributesFormView = require('./edit-feature-attributes-form-view');
module.exports = CoreView.extend({
initialize: function (opts) {
if (!opts.featureModel) throw new Error('featureModel is required');
if (!opts.geometryFormModel) throw new Error('geometryFormModel is required');
if (!opts.attributesFormModel) throw new Error('attributesFormModel is required');
this._featureModel = opts.featureModel;
this._geometryFormModel = opts.geometryFormModel;
this._attributesFormModel = opts.attributesFormModel;
},
render: function () {
if (this._editFeatureGeometryFormView) {
this._editFeatureGeometryFormView.clean();
}
if (this._editFeatureAttributesFormView) {
this._editFeatureAttributesFormView.clean();
}
this.clearSubViews();
this.$el.empty();
this._editFeatureGeometryFormView = new EditFeatureGeometryFormView({
model: this._geometryFormModel
});
this.addView(this._editFeatureGeometryFormView);
this.$el.append(this._editFeatureGeometryFormView.render().el);
this._editFeatureAttributesFormView = new EditFeatureAttributesFormView({
model: this._attributesFormModel
});
this.addView(this._editFeatureAttributesFormView);
this.$el.append(this._editFeatureAttributesFormView.render().el);
return this;
}
});

View File

@@ -0,0 +1,2 @@
<div class="js-editFeatureHeader"></div>
<div class="Editor-content js-editFeatureContent"></div>

View File

@@ -0,0 +1 @@
<div class="EditOverlay js-editOverlay is-hidden"><p class="EditOverlay-inner CDB-Text CDB-Size-medium u-whiteTextColor"><%- text %></p></div>

View File

@@ -0,0 +1,4 @@
<h2 class="Editor-HeaderInfo-titleText Inline-editor js-title-editor" title="<%- alias %>">
<div class="CDB-Text CDB-Size-huge is-light u-ellipsis js-title Inline-editor-text"><%- alias %></div>
<input type="text" name="text" class="Inline-editor-input Inline-editor-input--small CDB-Text CDB-InputText js-input" value="<%- alias %>" readonly>
</h2>

View File

@@ -0,0 +1,65 @@
var CoreView = require('backbone/core-view');
var LayerAnalysisDraggableView = require('./layer-analysis-draggable-view');
/**
* View of analyses within a layer
* this.model is the layer-definition-model
*/
module.exports = CoreView.extend({
tagName: 'ul',
options: {
sortableSelector: ''
},
initialize: function (opts) {
if (!this.options.sortableSelector) throw new Error('sortableSelector is required');
if (!opts.layerAnalysisViewFactory) throw new Error('layerAnalysisViewFactory is required');
if (!this.model) throw new Error('layerDefinitionModel is required');
this._layerAnalysisViewFactory = opts.layerAnalysisViewFactory;
},
render: function () {
this.clearSubViews();
var nodeDefModel = this.model.getAnalysisDefinitionNodeModel();
this._renderNode(nodeDefModel);
return this;
},
/**
* @param {Object} current instance of a analysis-layer-node-model
*/
_renderNode: function (current) {
this._createNodeView(current);
var next = current.getPrimarySource();
if (next && this.model.isOwnerOfAnalysisNode(current)) {
this._renderNode(next);
}
},
_createNodeView: function (current) {
var view = this._layerAnalysisViewFactory.createView(current, this.model);
this.addView(view);
this.$el.append(view.render().el);
this._attachDraggableToNodeView(view, current);
},
_attachDraggableToNodeView: function (nodeView, nodeDefModel) {
if (!this.model.isOwnerOfAnalysisNode(nodeDefModel)) return;
var draggableView = new LayerAnalysisDraggableView({
model: nodeDefModel,
layerDefinitionModel: this.model,
getNextLetter: this.model.collection.nextLetter.bind(this.model.collection),
$nodeViewElement: nodeView.$el,
sortableSelector: this.options.sortableSelector
});
this.addView(draggableView);
}
});

View File

@@ -0,0 +1,56 @@
require('jquery-ui');
var CoreView = require('backbone/core-view');
var template = require('./layer-analysis-draggable.tpl');
var analyses = require('builder/data/analyses');
var layerColors = require('builder/data/layer-colors');
/**
* Attach draggable behavior to an analysis node
* Implemented as a view to hook into Backbone's lifecycle handling, but really it's just attaching the behavior to the
* provided $nodeViewElement
*/
module.exports = CoreView.extend({
options: {
getNextLetter: function () { return 'x'; },
sortableSelector: '',
$nodeViewElement: null
},
initialize: function (opts) {
if (!opts.sortableSelector) throw new Error('sortableSelector is required');
if (!opts.$nodeViewElement) throw new Error('$nodeViewElement is required');
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
this.options.$nodeViewElement.draggable({
appendTo: this.options.sortableSelector,
connectToSortable: this.options.sortableSelector,
cursor: 'move',
axis: 'y',
cursorAt: { top: 40 },
helper: this._createHelper.bind(this),
zIndex: 1000
});
},
_createHelper: function () {
var nextLetter = this.options.getNextLetter();
var layerLetter = this.options.layerDefinitionModel.get('letter');
return template({
nodeId: this.model.id,
nextLetter: nextLetter,
layerLetter: layerLetter,
nextBgColor: layerColors.getColorForLetter(nextLetter),
title: analyses.title(this.model)
});
},
clean: function () {
if (this.options.$nodeViewElement && this.options.$nodeViewElement.data('ui-draggable')) {
this.options.$nodeViewElement.draggable('destroy');
}
this.options.$nodeViewElement = null; // remove DOM reference to avoid memory leak
CoreView.prototype.clean.apply(this, arguments);
}
});

View File

@@ -0,0 +1,18 @@
<li class="Editor-newLayerContainer" data-layer-letter="<%- layerLetter %>" data-analysis-node-id="<%- nodeId %>" style="width: 294px;">
<div class="Editor-ListLayer-item">
<div class="Editor-ListLayer-itemHeader">
<div class="Editor-ListLayer-media u-rSpace--m" style="background: <%- nextBgColor %>; color: #fff">
<p class="CDB-Text CDB-Size-large is-semibold u-upperCase"><%- nextLetter %></p>
</div>
<div class="Editor-ListLayer-inner">
<div class="Editor-ListLayer-title">
<h2 class="Editor-ListLayer-titleText CDB-Text CDB-Size-large u-ellipsis"><%- title %></h2>
</div>
<button class="CDB-Text CDB-Size-small u-actionTextColor u-upperCase"><%- _t('editor.layers.layer.add-analysis') %></button>
</div>
</div>
</div>
<div class="Editor-layerInfo CDB-Text CDB-Size-small u-whiteTextColor">
Created new layer based on [<%- nodeId %>]
</div>
</li>

View File

@@ -0,0 +1,57 @@
var RefLayerAnalysisView = require('./analysis-views/ref-layer-analysis-view');
var DefaultLayerAnalysisView = require('./analysis-views/default-layer-analysis-view');
var SourceLayerAnalysisView = require('./analysis-views/source-layer-analysis-view');
var CompositeLayerAnalysisView = require('./analysis-views/composite-layer-analysis-view');
var LayerAnalysisViewFactory = function (analysisDefinitionNodesCollection) {
this._analysisDefinitionNodesCollection = analysisDefinitionNodesCollection;
};
/**
* @param {Object} nodeDefModel - an analysis-definition-node-model
* @param {Object} layerDefinitionModel
* @param {String} [tagName = 'li']
* @return {Object} instance of CoreView
* @throws {Error} if node model can not be found, or view can not be found for the given mode
*/
LayerAnalysisViewFactory.prototype.createView = function (nodeDefModel, layerDefinitionModel) {
if (!nodeDefModel) throw new Error('nodeDefModel is required');
if (!layerDefinitionModel) throw new Error('layerDefinitionModel is required');
var analysisNode = this._analysisDefinitionNodesCollection.get(nodeDefModel.id);
var sourcesCount = nodeDefModel.sourceIds().length;
var ownerLayer = layerDefinitionModel.collection.findOwnerOfAnalysisNode(nodeDefModel);
if (ownerLayer !== layerDefinitionModel) {
sourcesCount = null; // to force default ref view
}
switch (sourcesCount) {
case 2:
return new CompositeLayerAnalysisView({
model: nodeDefModel,
analysisDefinitionNodesCollection: this._analysisDefinitionNodesCollection,
layerDefinitionModel: layerDefinitionModel
});
case 0:
return new SourceLayerAnalysisView({
model: nodeDefModel,
analysisNode: analysisNode,
layerDefinitionModel: layerDefinitionModel
});
case 1:
return new DefaultLayerAnalysisView({
model: nodeDefModel,
analysisNode: analysisNode,
layerDefinitionModel: layerDefinitionModel
});
default:
return new RefLayerAnalysisView({
model: nodeDefModel,
analysisNode: analysisNode,
layerDefinitionModel: ownerLayer || layerDefinitionModel // use same layer model as fallback
});
}
};
module.exports = LayerAnalysisViewFactory;

View File

@@ -0,0 +1,655 @@
var _ = require('underscore');
var $ = require('jquery');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var createTextLabelsTabPaneRouted = require('builder/components/tab-pane/create-text-labels-tab-pane-routed');
var TabPaneTemplate = require('./layer-tab-pane.tpl');
var LayerHeaderView = require('./layer-header-view');
var TablesCollection = require('builder/data/tables-collection');
var AnalysisSourceOptionsModel = require('./layer-content-views/analyses/analysis-source-options-model');
var AnalysisFormsCollection = require('./layer-content-views/analyses/analysis-forms-collection');
var TableManager = require('builder/components/table/table-manager');
var changeViewButtons = require('./change-view-buttons.tpl');
var editOverlay = require('./edit-overlay.tpl');
var DataView = require('./layer-content-views/data/data-view');
var LayerContentAnalysesView = require('./layer-content-views/analyses/analyses-view');
var StyleView = require('builder/editor/style/style-view');
var InfowindowsView = require('./layer-content-views/infowindow/infowindows-view');
var LegendsView = require('./layer-content-views/legend/legends-view');
var FeatureDefinitionModel = require('builder/data/feature-definition-model');
var AnalysesService = require('./layer-content-views/analyses/analyses-service');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var Toggler = require('builder/components/toggler/toggler-view');
var Router = require('builder/routes/router');
var fetchAllQueryObjects = require('builder/helpers/fetch-all-query-objects');
var LayerContentModel = require('builder/data/layer-content-model');
var TABS = {
data: 'data',
analyses: 'analyses',
style: 'style',
popups: 'popups',
legends: 'legends'
};
var DISABLED_TABS = [
TABS.style,
TABS.popups,
TABS.legends
];
var ROUTER_ADD_GEOMETRY = {
point: 'addPoint',
line: 'addLine',
polygon: 'addPolygon'
};
var CONTEXTS = LayerContentModel.CONTEXTS;
var REQUIRED_OPTS = [
'userActions',
'analysisDefinitionNodesCollection',
'layerDefinitionsCollection',
'layerDefinitionModel',
'widgetDefinitionsCollection',
'legendDefinitionsCollection',
'stackLayoutModel',
'modals',
'onboardings',
'configModel',
'editorModel',
'userModel',
'mapDefinitionModel',
'mapModeModel',
'visDefinitionModel',
'stateDefinitionModel',
'onboardingNotification'
];
module.exports = CoreView.extend({
module: 'editor:layers:layer-content-view',
events: {
'click .js-back': '_onClickBack',
'click .js-fix-sql': '_onClickFixSQL'
},
options: {
selectedTabItem: null,
analysisPayload: null // id or a new analysis node attrs (may not be complete)
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.model = new LayerContentModel({}, {
querySchemaModel: this._getQuerySchemaModel(),
queryGeometryModel: this._getQueryGeometryModel(),
queryRowsCollection: this._getQueryRowsCollection()
});
this._initViewState();
this._onClickNewGeometry = this._onClickNewGeometry.bind(this);
this._togglerModel = new Backbone.Model({
labels: [_t('editor.context-switcher.map'), _t('editor.context-switcher.data')],
active: this._isContextTable()
});
AnalysesService.setLayerId(this._layerDefinitionModel.get('id'));
this._updateViewState();
this._initBinds();
},
render: function () {
this._unbindEvents();
this.clearSubViews();
this._destroyTable();
this.$el.empty();
this._renderHeaderView();
this._renderLayerTabPaneView();
if (this._isContextTable()) {
this._initTable();
}
this._renderContextButtons();
return this;
},
_initViewState: function () {
this._viewState = new Backbone.Model({
isLayerEmpty: false,
canBeGeoreferenced: false,
isLayerDone: false
});
this._setViewState();
},
_initBinds: function () {
var querySchemaModel = this._getQuerySchemaModel();
var queryRowsCollection = this._getQueryRowsCollection();
var queryGeometryModel = this._getQueryGeometryModel();
var layerDefinitionModel = this._layerDefinitionModel;
this.listenTo(this.model, 'change:context', this._renderContextButtons);
this.listenTo(this.model, 'change:state', this._updateViewState);
this.listenTo(this._getAnalysisDefinitionNodeModel(), 'change:error', this.render);
this.listenTo(layerDefinitionModel, 'remove', this._onClickBack);
this.listenTo(layerDefinitionModel, 'change:source', this._updateViewState);
this.listenTo(layerDefinitionModel, 'change:visible', this._renderContextButtons);
this.listenTo(this._viewState, 'change', this.render);
this.listenTo(this._editorModel, 'change:edition', this._changeStyle);
this.listenTo(this._togglerModel, 'change:active', this._onTogglerChanged);
this.listenTo(this._widgetDefinitionsCollection, 'add remove', this._renderContextButtons);
this.listenTo(this._onboardings, 'style', function () {
this._layerTabPaneView.collection.select('name', 'style');
});
this.listenTo(querySchemaModel, 'change:query', function () {
this._renderContextButtons();
if (this._editorModel.isEditing()) {
this.listenToOnce(querySchemaModel, 'sync', function (e) {
queryRowsCollection.fetch({
success: function () {
Router.goToDataTab(layerDefinitionModel.get('id'));
}
});
});
}
});
this.listenToOnce(queryGeometryModel, 'change:simple_geom', function (model, simpleGeom) {
this._renderContextButtons();
}, this);
this.listenTo(Router.getRouteModel(), 'change:currentRoute', this._handleRoute);
},
_renderLayerTabPaneView: function () {
var self = this;
var analysisPayload = this.options.analysisPayload;
var layerId = this._layerDefinitionModel.get('id');
var canBeGeoreferenced = this._viewState.get('canBeGeoreferenced');
var isDone = this._viewState.get('isLayerDone');
var isEmpty = this._viewState.get('isLayerEmpty') && isDone;
var selectedTabItem = this.options.selectedTabItem;
var analysisFailed = this._getAnalysisDefinitionNodeModel().hasFailed();
var isDisabled = canBeGeoreferenced || isEmpty || analysisFailed;
if (isDisabled && (!selectedTabItem || _.contains(DISABLED_TABS, selectedTabItem))) {
if ((!isEmpty && canBeGeoreferenced) || analysisFailed) {
selectedTabItem = TABS.analyses;
} else {
selectedTabItem = TABS.data;
}
}
var tabPaneTabs = [{
label: _t('editor.layers.menu-tab-pane-labels.data'),
name: TABS.data,
selected: selectedTabItem === TABS.data,
onClick: function () { Router.goToDataTab(layerId); },
disabled: false,
createContentView: function () {
return new DataView({
className: 'Editor-content',
widgetDefinitionsCollection: self._widgetDefinitionsCollection,
layerDefinitionModel: self._layerDefinitionModel,
stackLayoutModel: self._stackLayoutModel,
userActions: self._userActions,
configModel: self._configModel,
editorModel: self._editorModel,
userModel: self._userModel,
onboardings: self._onboardings,
onboardingNotification: self._onboardingNotification,
layerContentModel: self.model
});
}
}, {
label: _t('editor.layers.menu-tab-pane-labels.analyses'),
name: TABS.analyses,
selected: selectedTabItem === TABS.analyses,
disabled: isEmpty && !analysisFailed,
onClick: function () { Router.goToAnalysisTab(layerId); },
createContentView: function () {
var layerDefinitionModel = self._layerDefinitionModel;
var analysisSourceOptionsModel = new AnalysisSourceOptionsModel(null, {
analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
layerDefinitionsCollection: self._layerDefinitionsCollection,
tablesCollection: new TablesCollection(null, {
configModel: self._configModel
})
});
analysisSourceOptionsModel.fetch();
var analysisFormsCollection = new AnalysisFormsCollection(null, {
userActions: self._userActions,
configModel: self._configModel,
layerDefinitionModel: self._layerDefinitionModel,
analysisSourceOptionsModel: analysisSourceOptionsModel
});
analysisFormsCollection.resetByLayerDefinition();
if (!self._layerDefinitionModel.hasAnalyses() && canBeGeoreferenced && !analysisPayload) {
analysisPayload = AnalysesService.generateGeoreferenceAnalysis();
}
// e.g. when selected from layers view
var selectedNodeId;
if (_.isString(analysisPayload) && analysisFormsCollection.get(analysisPayload)) {
selectedNodeId = analysisPayload;
} else if (_.isObject(analysisPayload)) {
// payload passed after continue when an option was selected in add-analysis-view
selectedNodeId = analysisPayload.id;
// Detect when an analysis finishes so we can redirect and / or re-enable the tabs
self.listenTo(self._analysisDefinitionNodesCollection, 'add', function (model) {
var nodeSource = model.getPrimarySource();
// This logic only applies to a geocoding analysis that's the first, on a layer that lacks geocoding
if (nodeSource && nodeSource.get('type') === 'source' &&
model.get('type').indexOf('georeference') === 0 &&
canBeGeoreferenced) {
var callback = function (model) {
if (model.isDone() || model.hasFailed()) {
this.stopListening(model, 'change:status', callback);
}
if (model.isDone()) {
Router.goToStyleTab(layerId);
}
if (model.hasFailed()) {
self.render();
}
};
self.listenTo(model, 'change:status', callback);
}
});
analysisFormsCollection.addHead(analysisPayload);
} else {
var lastAnalysisNode = analysisFormsCollection.at(0);
if (lastAnalysisNode) {
selectedNodeId = lastAnalysisNode.get('id');
Router.goToAnalysisNode(layerDefinitionModel.get('id'), selectedNodeId, { trigger: false, replace: true });
} else {
Router.goToAnalysisTab(layerDefinitionModel.get('id'), { trigger: false, replace: true });
}
}
// remove payload once we have used it, to not have it being invoked again when switching tabs
selectedTabItem = null;
analysisPayload = null;
return new LayerContentAnalysesView({
className: 'Editor-content',
userActions: self._userActions,
analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
editorModel: self._editorModel,
userModel: self._userModel,
analysisFormsCollection: analysisFormsCollection,
configModel: self._configModel,
layerDefinitionModel: self._layerDefinitionModel,
stackLayoutModel: self._stackLayoutModel,
selectedNodeId: selectedNodeId,
onboardings: self._onboardings,
onboardingNotification: self._onboardingNotification,
layerContentModel: self.model
});
}
}, {
label: _t('editor.layers.menu-tab-pane-labels.style'),
name: TABS.style,
selected: selectedTabItem === TABS.style,
layerId: layerId,
onClick: function () { Router.goToStyleTab(layerId); },
disabled: isDisabled,
createContentView: function () {
// remove payload once we have used it, to not have it being invoked again when switching tabs
selectedTabItem = null;
var styleView = new StyleView({
className: 'Editor-content',
configModel: self._configModel,
userModel: self._userModel,
userActions: self._userActions,
analysisDefinitionsCollection: self.analysisDefinitionsCollection,
queryGeometryModel: self._getQueryGeometryModel(),
querySchemaModel: self._getQuerySchemaModel(),
queryRowsCollection: self._getQueryRowsCollection(),
layerDefinitionsCollection: self._layerDefinitionsCollection,
layerDefinitionModel: self._layerDefinitionModel,
editorModel: self._editorModel,
modals: self._modals,
onboardings: self._onboardings,
onboardingNotification: self._onboardingNotification,
layerContentModel: self.model
});
return styleView;
}
}, {
label: _t('editor.layers.menu-tab-pane-labels.popups'),
name: TABS.popups,
selected: selectedTabItem === TABS.popups,
onClick: function () { Router.goToPopupsTab(layerId); },
disabled: isDisabled,
createContentView: function () {
var nodeDefModel = self._layerDefinitionModel.getAnalysisDefinitionNodeModel();
return new InfowindowsView({
className: 'Editor-content',
userActions: self._userActions,
layerDefinitionModel: self._layerDefinitionModel,
querySchemaModel: nodeDefModel.querySchemaModel,
editorModel: self._editorModel,
layerContentModel: self.model
});
}
}, {
label: _t('editor.layers.menu-tab-pane-labels.legends'),
name: TABS.legends,
selected: selectedTabItem === TABS.legends,
onClick: function () { Router.goToLegendsTab(layerId); },
disabled: isDisabled,
createContentView: function () {
var nodeDefModel = self._layerDefinitionModel.getAnalysisDefinitionNodeModel();
return new LegendsView({
className: 'Editor-content',
mapDefinitionModel: self._mapDefinitionModel,
userActions: self._userActions,
layerDefinitionModel: self._layerDefinitionModel,
querySchemaModel: nodeDefModel.querySchemaModel,
queryRowsCollection: nodeDefModel.queryRowsCollection,
legendDefinitionsCollection: self._legendDefinitionsCollection,
editorModel: self._editorModel,
userModel: self._userModel,
configModel: self._configModel,
modals: self._modals,
layerContentModel: self.model
});
}
}];
var tabPaneOptions = {
tabPaneOptions: {
className: 'Tab-pane js-editorPanelContent',
template: TabPaneTemplate,
tabPaneItemOptions: {
tagName: 'li',
klassName: 'CDB-NavMenu-item'
}
},
tabPaneItemLabelOptions: {
tagName: 'button',
className: 'CDB-NavMenu-link u-upperCase'
}
};
this._layerTabPaneView = createTextLabelsTabPaneRouted(tabPaneTabs, tabPaneOptions);
this.$el.append(this._layerTabPaneView.render().el);
this.addView(this._layerTabPaneView);
this.listenTo(this._layerTabPaneView.collection, 'change:selected', this._quitEditing, this);
this._changeStyle();
},
_renderHeaderView: function () {
var analysisDefinitionNodeModel = this._getAnalysisDefinitionNodeModel();
var tableNodeModel = analysisDefinitionNodeModel.isSourceType() && analysisDefinitionNodeModel.getTableModel();
var layerHeaderView = new LayerHeaderView({
layerDefinitionsCollection: this._layerDefinitionsCollection,
layerDefinitionModel: this._layerDefinitionModel,
userActions: this._userActions,
configModel: this._configModel,
modals: this._modals,
tableNodeModel: tableNodeModel,
editorModel: this._editorModel,
stateDefinitionModel: this._stateDefinitionModel,
visDefinitionModel: this._visDefinitionModel,
userModel: this._userModel,
widgetDefinitionsCollection: this._widgetDefinitionsCollection
});
this.addView(layerHeaderView);
this.$el.append(layerHeaderView.render().$el);
},
_updateViewState: function () {
var setViewStateFn = this._setViewState.bind(this);
if (this.model.isDone()) {
setViewStateFn();
return false;
}
this._fetchAllQueryObjects({
queryRowsCollection: this._getQueryRowsCollection(),
queryGeometryModel: this._getQueryGeometryModel(),
querySchemaModel: this._getQuerySchemaModel()
}).then(function () {
setViewStateFn();
});
},
_fetchAllQueryObjects: fetchAllQueryObjects.bind(this),
_destroyTable: function () {
if (this._tableView) {
TableManager.destroy(this._tableView);
delete this._tableView;
}
},
_initTable: function () {
if (this._tableView) {
this._destroyTable();
}
this._tableView = TableManager.create({
analysisDefinitionNodeModel: this._getAnalysisDefinitionNodeModel(),
configModel: this._configModel,
modals: this._modals,
userModel: this._userModel
});
$('.js-editor .CDB-Dashboard-canvas').append(this._tableView.render().el);
},
_isContextTable: function () {
return this.model.get('context') === CONTEXTS.table;
},
_renderContextButtons: function () {
this._destroyContextButtons();
var analysisDefinitionNodeModel = this._getAnalysisDefinitionNodeModel();
var isReadOnly = analysisDefinitionNodeModel.isReadOnly();
var isVisible = this._layerDefinitionModel.get('visible');
var queryGeometryModel = this._getQueryGeometryModel();
$('.CDB-Dashboard-canvas .CDB-Map-canvas').append(
changeViewButtons({
context: this.model.get('context'),
isThereOtherWidgets: this._widgetDefinitionsCollection.isThereOtherWidgets(),
isThereTimeSeries: this._widgetDefinitionsCollection.isThereTimeSeries(),
isThereAnimatedTimeSeries: this._widgetDefinitionsCollection.isThereTimeSeries({ animated: true }),
queryGeometryModel: queryGeometryModel.get('simple_geom'),
isSourceType: analysisDefinitionNodeModel.isSourceType(),
isVisible: isVisible,
isReadOnly: isReadOnly
})
);
var togglerView = new Toggler({
model: this._togglerModel
});
$('.js-mapTableView').append(togglerView.render().el);
$('.js-newGeometry').on('click', this._onClickNewGeometry);
this._addContextButtonsTooltips(isReadOnly, isVisible);
},
_addContextButtonsTooltips: function (isReadOnly, isVisible) {
if (isReadOnly || !isVisible) {
var disabledButtonTooltip = new TipsyTooltipView({
el: $('.js-newGeometryView'),
title: function () {
return _t('editor.edit-feature.geometry-disabled');
}
});
this.addView(disabledButtonTooltip);
return;
}
var tooltips = $('.js-newGeometry').map(function () {
return new TipsyTooltipView({
el: $(this),
title: function () {
return $(this).data('tooltip');
}
});
});
_.forEach(tooltips, this.addView.bind(this));
},
_destroyContextButtons: function () {
$('.js-newGeometry').off('click', this._onClickNewGeometry);
$('.js-switchers').remove();
},
_onTogglerChanged: function () {
var context = this._togglerModel.get('active') ? CONTEXTS.table : CONTEXTS.map;
context === CONTEXTS.table
? this._initTable()
: this._destroyTable();
this.model.set('context', context);
},
_unbindEvents: function () {
if (this._layerTabPaneView && this._layerTabPaneView.collection) {
this._layerTabPaneView.collection.off('change:selected', this._quitEditing, this);
}
},
_onClickBack: function () {
this._editorModel.set({ edition: false });
Router.goToLayerList();
},
_onClickNewGeometry: function (ev) {
var $target = $(ev.target).closest('.js-newGeometry');
var featureType = $target.data('feature-type');
var addGeometry = ROUTER_ADD_GEOMETRY[featureType];
if ($target.closest('.js-newGeometryItem').hasClass('is-disabled')) return false;
if (Router[addGeometry]) {
Router[addGeometry](this._layerDefinitionModel.get('id'));
}
},
_handleRoute: function (routeModel) {
var currentRoute = routeModel.get('currentRoute');
var routeName = currentRoute[0];
var OVERLAY_FADE_MS = 200;
if (routeName.indexOf('add_feature_') === 0) {
var featureType = routeName.split('add_feature_')[1];
var editOverlayText = _t('editor.edit-feature.overlay-text', { featureType: _t('editor.edit-feature.features.' + featureType) });
$('.CDB-Dashboard-canvas .CDB-Map-canvas').append(editOverlay({
text: editOverlayText
}));
$('.js-editOverlay').fadeIn(OVERLAY_FADE_MS, function () {
$('.js-editOverlay').removeClass('is-hidden');
});
var feature = this._newFeatureDefinitionModel({ featureType: featureType });
this._mapModeModel.enterDrawingFeatureMode(feature);
this._stackLayoutModel.goToStep(2, feature.getLayerDefinition(), !feature.isNew());
}
},
_newFeatureDefinitionModel: function (opts) {
return new FeatureDefinitionModel({}, {
configModel: this._configModel,
layerDefinitionModel: this._layerDefinitionModel,
userModel: this._userModel,
featureType: opts.featureType
});
},
_onClickFixSQL: function () {
this._editorModel.set({ edition: true });
this._layerTabPaneView.getTabPane('data').set({ selected: true });
},
_changeStyle: function () {
this._layerTabPaneView && this._layerTabPaneView.changeStyleMenu(this._editorModel);
},
_quitEditing: function () {
if (this._layerTabPaneView.getSelectedTabPaneName() !== TABS.style &&
this._layerTabPaneView.getSelectedTabPaneName() !== TABS.popups &&
this._layerTabPaneView.getSelectedTabPaneName() !== TABS.legends) {
this._editorModel.set({ edition: false });
}
},
_getAnalysisDefinitionNodeModel: function () {
return this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
},
_getQueryGeometryModel: function () {
return this._getAnalysisDefinitionNodeModel().queryGeometryModel;
},
_getQuerySchemaModel: function () {
return this._getAnalysisDefinitionNodeModel().querySchemaModel;
},
_getQueryRowsCollection: function () {
return this._getAnalysisDefinitionNodeModel().queryRowsCollection;
},
_setViewState: function () {
var self = this;
var emptyPromise = this._layerDefinitionModel.isEmptyAsync();
var geoReferencePromise = this._layerDefinitionModel.canBeGeoreferenced();
this._viewState.set('isLayerDone', self._layerDefinitionModel.isDone());
Promise.all([emptyPromise, geoReferencePromise])
.then(function (values) {
self._viewState.set({
isLayerEmpty: values[0],
canBeGeoreferenced: values[1],
isLayerDone: self._layerDefinitionModel.isDone()
});
});
},
clean: function () {
this._destroyContextButtons();
this._destroyTable();
CoreView.prototype.clean.apply(this);
}
});

View File

@@ -0,0 +1,210 @@
var CoreView = require('backbone/core-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var ScrollView = require('builder/components/scroll/scroll-view');
var ViewFactory = require('builder/components/view-factory');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var IconView = require('builder/components/icon/icon-view');
var analysisPlaceholderTemplate = require('builder/editor/layers/layer-content-views/analyses/analyses-placeholder.tpl');
var analysisSQLErrorTemplate = require('builder/editor/layers/layer-content-views/analyses/analyses-sql-error.tpl');
var AnalysisFormView = require('builder/editor/layers/layer-content-views/analyses/analysis-form-view');
var AnalysesWorkflowView = require('builder/editor/layers/layer-content-views/analyses/analyses-workflow-view');
var AnalysisControlsView = require('builder/editor/layers/layer-content-views/analyses/analysis-controls-view');
var actionErrorTemplate = require('builder/editor/layers/sql-error-action.tpl');
var AnalysesQuotaInfo = require('./analyses-quota/analyses-quota-info');
var AnalysesService = require('builder/editor/layers/layer-content-views/analyses/analyses-service');
var Router = require('builder/routes/router');
var utils = require('builder/helpers/utils');
var REQUIRED_OPTS = [
'userActions',
'analysisFormsCollection',
'layerDefinitionModel',
'analysisDefinitionNodesCollection',
'configModel',
'userModel',
'stackLayoutModel',
'overlayModel',
'layerContentModel',
'deleteAnalysisModel'
];
/**
* AnalysesContentView:
* ├── ScrollView
* | └── ListView:
* | ├── WorkflowView
* | └── FormView
* └── ControlsView
*/
module.exports = CoreView.extend({
module: 'editor:layers:layer-content-views:analyses:analyses-content-view',
options: {
selectedNodeId: false // or an id, e.g. 'a1'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._selectedNodeId = this.options.selectedNodeId;
this._querySchemaModel = this._layerDefinitionModel.getAnalysisDefinitionNodeModel().querySchemaModel;
this._quotaInfo = AnalysesQuotaInfo.get(opts.configModel);
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.empty();
if (this._isErrored()) {
this._renderError();
} else {
this._initViews();
}
this._toggleOverlay();
return this;
},
_isErrored: function () {
return this._layerContentModel.isErrored();
},
_renderError: function () {
this.$el.html(
analysisSQLErrorTemplate({
body: _t('editor.error-query.body', {
action: actionErrorTemplate({
label: _t('editor.error-query.label')
})
})
})
);
},
_initViews: function () {
if (this._analysisFormsCollection.isEmpty()) {
this.$el.html(analysisPlaceholderTemplate());
this._renderTooltip();
} else {
var formModel = this._getFormModel();
this._renderScrollableListView(formModel);
this._renderControlsView(formModel);
}
var plusIcon = new IconView({
placeholder: this.$el.find('.js-plus-icon'),
icon: 'plus'
});
plusIcon.render();
this.addView(plusIcon);
},
_toggleOverlay: function () {
var isDisabled = this._overlayModel.get('visible');
this.$el.toggleClass('is-disabled', isDisabled);
},
_renderTooltip: function () {
var tooltip = new TipsyTooltipView({
el: this.$('.js-add-analysis'),
gravity: 'w',
title: function () {
return utils.replaceLastSpaceWithNbsp(_t('editor.layers.analysis-form.add-analysis.tooltip'));
},
offset: 8
});
this.addView(tooltip);
},
_initBinds: function () {
this.listenTo(this._layerDefinitionModel, 'change:source', this._onLayerDefSourceChanged);
this.listenTo(this._analysisFormsCollection, 'change:persisted', this._onAnalysisFormsCollectionSync);
this.listenTo(this._analysisFormsCollection, 'destroyedModels remove', this._setDefaultSelectedNodeId);
this.listenTo(this._overlayModel, 'change:visible', this._toggleOverlay);
},
_renderScrollableListView: function (formModel) {
var self = this;
var view = new ScrollView({
createContentView: function () {
return ViewFactory.createListView([
function () {
return new AnalysesWorkflowView({
analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
layerDefinitionModel: self._layerDefinitionModel,
analysisFormsCollection: self._analysisFormsCollection,
selectedNodeId: self._selectedNodeId
});
},
function () {
return new AnalysisFormView({
formModel: formModel,
configModel: self._configModel,
userModel: self._userModel
});
}
]);
}
});
this.$el.append(view.render().el);
this.addView(view);
},
_renderControlsView: function (formModel) {
var analysisNode = this._analysisDefinitionNodesCollection.get(formModel.id);
var view = new AnalysisControlsView({
layerDefinitionModel: this._layerDefinitionModel,
analysisFormsCollection: this._analysisFormsCollection,
analysisNode: analysisNode,
configModel: this._configModel,
formModel: formModel,
quotaInfo: this._quotaInfo,
querySchemaModel: this._getQuerySchemaModelForEstimation(),
stackLayoutModel: this._stackLayoutModel,
userActions: this._userActions,
userModel: this._userModel,
deleteAnalysisModel: this._deleteAnalysisModel
});
this.addView(view);
this.$el.append(view.render().el);
},
_getFormModel: function () {
return this._analysisFormsCollection.get(this._selectedNodeId) || this._analysisFormsCollection.first();
},
_getQuerySchemaModelForEstimation: function () {
var node = this._analysisDefinitionNodesCollection.get(this._selectedNodeId);
var index;
if (!node) {
node = this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
} else {
index = this._analysisDefinitionNodesCollection.indexOf(node);
node = this._analysisDefinitionNodesCollection.at(index - 1);
}
return node.querySchemaModel;
},
_onLayerDefSourceChanged: function () {
this._analysisFormsCollection.removeUselessModels();
},
_onAnalysisFormsCollectionSync: function (model) {
AnalysesService.clearNotAppliedAnalysis(model.get('id'));
},
_setDefaultSelectedNodeId: function () {
var selectedNodeId = this._analysisFormsCollection.pluck('id')[0];
AnalysesService.clearNotAppliedAnalysis(selectedNodeId);
Router.goToAnalysisNode(this._layerDefinitionModel.get('id'), selectedNodeId);
}
});

View File

@@ -0,0 +1,6 @@
<button class="CDB-Button CDB-Button--dashed CDB-Button--wide CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase u-bSpace--xl u-flex u-justifyCenter u-alignCenter js-add-analysis">
<span class="js-plus-icon u-flex u-justifyCenter u-alignCenter u-rSpace--m"></span>
<%- _t('editor.layers.analysis-form.add-analysis.label') %>
</button>
<h2 class="CDB-Text CDB-Size-huge is-light u-secondaryTextColor"><%- _t('editor.layers.analysis-form.placeholder-text') %></h2>

View File

@@ -0,0 +1,45 @@
var _ = require('underscore');
var $ = require('jquery');
var cdb = require('internal-carto.js');
var errorParse = require('builder/helpers/error-parser');
var ENOUGH_QUOTA_QUERY = _.template("select cdb_dataservices_client.cdb_enough_quota('<%= analysis %>', <%= rows %>);");
module.exports = {
init: function (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')
});
},
fetch: function (service, rows) {
var query = ENOUGH_QUOTA_QUERY({
analysis: service,
rows: rows
});
if (!this.deferred || this.deferred.state() !== 'pending') {
var deferred = $.Deferred();
this.deferred = deferred;
var errorCallback = function (err) {
deferred.reject(errorParse(err));
};
var successCallback = function (data) {
// response is something like:
// {"rows":[{"cdb_enough_quota":false}],"time":0.024,"fields":{"cdb_enough_quota":{"type":"boolean"}},"total_rows":1}
deferred.resolve(data.rows[0].cdb_enough_quota);
};
this.SQL.execute(query, null, {
success: successCallback,
error: errorCallback
});
}
return this.deferred.promise();
}
};

View File

@@ -0,0 +1,46 @@
var _ = require('underscore');
var $ = require('jquery');
var cdb = require('internal-carto.js');
var errorParse = require('builder/helpers/error-parser');
var ESTIMATION_QUOTA_QUERY = _.template('SELECT CDB_EstimateRowCount($$<%= query %>;$$) AS row_count;');
module.exports = {
init: function (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')
});
},
fetch: function (query) {
var estimationQuery = ESTIMATION_QUOTA_QUERY({
query: query
});
if (!this.deferred || this.deferred.state() !== 'pending') {
var deferred = $.Deferred();
this.deferred = deferred;
var parseResponse = function (response) {
return response.rows[0].row_count;
};
var errorCallback = function (err) {
deferred.reject(errorParse(err));
};
var successCallback = function (data) {
deferred.resolve(parseResponse(data));
};
this.SQL.execute(estimationQuery, null, {
success: successCallback,
error: errorCallback
});
}
return this.deferred.promise();
}
};

View File

@@ -0,0 +1,85 @@
var _ = require('underscore');
var Backbone = require('backbone');
var cdb = require('internal-carto.js');
var errorParse = require('builder/helpers/error-parser');
var SERVICE_QUOTA_QUERY = 'select * from cdb_dataservices_client.cdb_service_quota_info();';
var QuotaInfo = Backbone.Collection.extend({
initialize: function (models, options) {
if (!options || !options.configModel) throw new Error('configModel is required');
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._state = 'unfetched';
this._ready = 'notready';
},
fetch: function (options) {
this._success = options && options.success;
this._error = options && options.error;
if (!this.isFetching()) {
this._state = 'fetching';
this.SQL.execute(SERVICE_QUOTA_QUERY, null, {
success: function (data) {
this._state = 'fetched';
this._ready = 'ready';
this._onFetchSuccess(data);
this._success && this._success();
}.bind(this),
error: function (err) {
this._state = 'error';
this._error && this._error(errorParse(err));
}.bind(this)
});
}
},
_onFetchSuccess: function (data) {
var models = data.rows;
_.each(models, function (model) {
var m = this.getService(model.service);
m ? m.set(model) : this.add(model);
}, this);
},
getService: function (service) {
return this.findWhere({service: service});
},
isFetching: function () {
return this.getState() === 'fetching';
},
isReady: function () {
return this._ready === 'ready';
},
needsCheck: function () {
var state = this.getState();
return state === 'unfetched' || state === 'fetching';
},
getState: function () {
return this._state;
}
});
var collection;
module.exports = {
get: function (configModel) {
if (collection === undefined) {
collection = new QuotaInfo([], {
configModel: configModel
});
}
return collection;
}
};

View File

@@ -0,0 +1,112 @@
var _ = require('underscore');
/**
* Control which analysis requires quota
*/
var unity = function (input) {
return input;
};
var routingAnalysisQuota = {
name: _t('editor.layers.analysis-form.quota.analysis-type.routing'),
api: 'routing',
user: 'mapzen_routing',
needsQuota: true,
transform: unity
};
var dataObservatoryQuota = {
name: _t('editor.layers.analysis-form.quota.analysis-type.data-observatory-measure'),
api: 'observatory',
user: 'obs_general',
needsQuota: true,
transform: unity
};
var ANALYSES_WITH_QUOTA_MAP = {
'trade-area': {
name: _t('editor.layers.analysis-form.quota.analysis-type.trade-area'),
api: 'isolines',
user: 'here_isolines',
transform: function (input, formModel) {
// the isoline analysis consume N rows x M tracts
return input * formModel.get('isolines');
},
needsQuota: function (quotaInfo) {
var info = quotaInfo.getService('isolines');
var provider = info && info.get('provider');
return !!provider;
}
},
'georeference-street-address': {
name: _t('editor.layers.analysis-form.quota.analysis-type.georeference-street-address'),
api: 'hires_geocoder',
user: 'geocoding',
transform: unity,
needsQuota: function (quotaInfo) {
var info = quotaInfo.getService('hires_geocoder');
var provider = info && info.get('provider');
return provider !== 'google';
}
},
'georeference-city': {
name: _t('editor.layers.analysis-form.quota.analysis-type.georeference-cities'),
api: 'hires_geocoder',
user: 'geocoding',
transform: unity,
needsQuota: function (quotaInfo) {
var info = quotaInfo.getService('hires_geocoder');
var provider = info && info.get('provider');
return provider !== 'google';
}
},
'data-observatory-measure': dataObservatoryQuota,
'routing-sequential': routingAnalysisQuota,
'routing-to-layer-all-to-all': routingAnalysisQuota,
'routing-to-single-point': routingAnalysisQuota
};
module.exports = {
requiresQuota: function (type, quotaInfo) {
if (ANALYSES_WITH_QUOTA_MAP[type] !== undefined) {
var needsQuota = ANALYSES_WITH_QUOTA_MAP[type].needsQuota;
if (_.isFunction(needsQuota)) {
return needsQuota(quotaInfo);
} else {
return needsQuota;
}
}
return false;
},
transformInput: function (type, input, formModel) {
if (ANALYSES_WITH_QUOTA_MAP[type] !== undefined) {
var transform = ANALYSES_WITH_QUOTA_MAP[type].transform;
return transform(input, formModel);
}
return input;
},
getAnalysisName: function (type) {
if (ANALYSES_WITH_QUOTA_MAP[type]) {
return ANALYSES_WITH_QUOTA_MAP[type].name;
}
return null;
},
getServiceName: function (type) {
if (ANALYSES_WITH_QUOTA_MAP[type]) {
return ANALYSES_WITH_QUOTA_MAP[type].api;
}
return null;
},
getUserDataName: function (type) {
if (ANALYSES_WITH_QUOTA_MAP[type]) {
return ANALYSES_WITH_QUOTA_MAP[type].user;
}
return null;
}
};

View File

@@ -0,0 +1,145 @@
var _ = require('underscore');
var Utils = require('builder/helpers/utils');
var AnalysesQuotaOptions = require('./analyses-quota-options');
var CONTACT_LINK_TEMPLATE = _.template("<a href='mailto:<%- mail %>'><%- contact %></a>");
function getOwnerEmail (userModel) {
return userModel._organizationModel && userModel._organizationModel.get('admin_email');
}
function getEmail (configModel, userModel) {
var isSaas = configModel.get('cartodb_com_hosted') === false;
var insideOrganization = userModel.isInsideOrg();
var ownerOrganization = userModel.isOrgOwner();
var email = _t('editor.layers.analysis-form.quota.emails.support');
if (isSaas) {
if (insideOrganization) {
email = ownerOrganization ? _t('editor.layers.analysis-form.quota.emails.support') : getOwnerEmail(userModel);
} else {
email = _t('editor.layers.analysis-form.quota.emails.saas');
}
}
return email;
}
function getMessage (configModel, userModel, notAssignedQuota) {
var insideOrganization = userModel.isInsideOrg();
var ownerOrganization = userModel.isOrgOwner();
var message;
if (notAssignedQuota) {
if (insideOrganization && !ownerOrganization) {
message = _t('editor.layers.analysis-form.quota.contact-message.no-quota-assigned.organization');
} else {
message = _t('editor.layers.analysis-form.quota.contact-message.no-quota-assigned.regular');
}
} else {
if (insideOrganization && !ownerOrganization) {
message = _t('editor.layers.analysis-form.quota.contact-message.no-credits.organization');
} else {
message = _t('editor.layers.analysis-form.quota.contact-message.no-credits.regular');
}
}
return message;
}
var getNoCreditMessage = function (quota, type, configModel, userModel) {
var link;
var message;
link = CONTACT_LINK_TEMPLATE({
mail: getEmail(configModel, userModel),
contact: getMessage(configModel, userModel, quota.totalQuota === 0)
});
if (quota.totalQuota === 0) {
message = _t('editor.layers.analysis-form.quota.no-quota-assigned-body', {
analysis: AnalysesQuotaOptions.getAnalysisName(type),
contact: link
});
} else {
message = _t('editor.layers.analysis-form.quota.no-credits-body', {
contact: link
});
}
return message;
};
module.exports = {
make: function (data, configModel, userModel) {
var blockPrice = Utils.formatNumber(data.quotaInfo.blockPrice / 100);
var estimation = data.estimation;
var formattedEstimation = Utils.formatNumber(estimation);
var blockSize = Utils.formatNumber(data.quotaInfo.blockSize);
var body = '';
var type = '';
var link;
if (data.quotaInfo.hardLimit) {
if (data.creditsLeft > 0) {
if (data.quotaInfo.enoughQuota) {
// You need to use approximately xxx of your credits.
body = _t('editor.layers.analysis-form.quota.enough-quota', {
credits: formattedEstimation
});
type = 'success';
} else {
// We're sorry the current quota is insufficient to enrich your data. Rows will be set to null and analysis may not complete. Please contact us to extend your quota for this function.
link = CONTACT_LINK_TEMPLATE({
mail: getEmail(configModel, userModel),
contact: getMessage(configModel, userModel, false)
});
body = _t('editor.layers.analysis-form.quota.hard-limit-not-enough-quota', {
contact: link
});
type = 'alert';
}
} else {
// To get access to the [service] you will need more credits. Your current quota is not enough. Contact us.
body = getNoCreditMessage(data.quotaInfo, data.type, configModel, userModel);
type = 'alert';
}
} else {
if (data.creditsLeft > 0) {
// enoughQuota wil be true when soft_limit, becuase in this case, the api asume true
if (data.creditsLeft >= estimation) {
// You need to use some of your credits for this (data.estimation)
body = _t('editor.layers.analysis-form.quota.enough-quota', {
credits: formattedEstimation
});
type = 'success';
} else {
// Using this analysis component may incur extra fees depending on your account quota. Please review your current quota and/or request a larger allocation.
body = _t('editor.layers.analysis-form.quota.soft-limit-enough-quota', {
credits: formattedEstimation,
blockSize: blockSize,
blockPrice: blockPrice
});
type = 'alert';
}
} else {
if (data.quotaInfo.totalQuota === 0) {
body = getNoCreditMessage(data.quotaInfo, data.type, configModel, userModel);
} else {
// Using this analysis component may incur extra fees depending on your account quota. Please review your current quota and/or request a larger allocation.
body = _t('editor.layers.analysis-form.quota.soft-limit-enough-quota', {
credits: formattedEstimation,
blockSize: blockSize,
blockPrice: blockPrice
});
}
type = 'alert';
}
}
return {
type: type,
body: body
};
}
};

View File

@@ -0,0 +1,37 @@
var _ = require('underscore');
var AnalysesQuotaOptions = require('./analyses-quota-options');
/**
* Provide info about quota
* It handles the source of data: api and user model
*/
var QUOTA_DEFAULTS = {
totalQuota: 0,
usedQuota: 0,
blockSize: 1000,
blockPrice: 0,
enoughQuota: true
};
module.exports = function (type, userModel, quotaInfo, enoughQuota) {
var apiData = quotaInfo.getService(AnalysesQuotaOptions.getServiceName(type)) || {};
var userData = userModel.get(AnalysesQuotaOptions.getUserDataName(type)) || {};
// Remove null | undefined | falsy values
userData = _.pick(userData, _.identity);
return _.defaults(
{
blockSize: userData.block_size,
blockPrice: userData.block_price
},
{
totalQuota: apiData.get('monthly_quota'),
usedQuota: apiData.get('used_quota'),
hardLimit: !apiData.get('soft_limit')
},
{
enoughQuota: enoughQuota
},
QUOTA_DEFAULTS
);
};

View File

@@ -0,0 +1,91 @@
// Make common analyses actions available whin editor context
var _ = require('underscore');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var AddAnalysisView = require('builder/components/modals/add-analysis/add-analysis-view');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var REQUIRED_OPTS = [
'onboardings',
'layerDefinitionsCollection',
'modals',
'userModel',
'configModel'
];
module.exports = (function () {
return {
init: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._notAppliedAnalysis = null;
},
setLayerId: function (layerId) {
var layerDefinitionModel = this._layerDefinitionsCollection.get(layerId);
if (!layerDefinitionModel) {
throw new Error('no layer-definition found for id' + layerId + ', available layer ids are: ' + this._layerDefinitionsCollection.pluck('id') + ')');
}
this._layerId = layerId;
},
setStackLayoutView: function (stackLayoutView) {
if (!stackLayoutView) {
throw new Error('stackLayoutView is required');
}
this._stackLayoutView = stackLayoutView;
},
generateGeoreferenceAnalysis: function () {
var layerDefinitionModel = this._layerDefinitionsCollection.get(this._layerId);
var letter = layerDefinitionModel.get('letter');
var sourceId = layerDefinitionModel.get('source');
return {
source: sourceId,
id: letter === nodeIds.letter(sourceId)
? nodeIds.next(sourceId)
: letter + '1',
type: 'georeference-long-lat'
};
},
addAnalysis: function () {
this._onboardings.destroy();
var layerDefinitionModel = this._layerDefinitionsCollection.get(this._layerId);
this._modals.create(function (modalModel) {
return new AddAnalysisView({
userModel: this._userModel,
configModel: this._configModel,
modalModel: modalModel,
layerDefinitionModel: layerDefinitionModel
});
}.bind(this), {
breadcrumbsEnabled: true
});
this._modals.onDestroyOnce(function (analysisFormAttrs) {
if (analysisFormAttrs) {
// A analysis option was seleted, redirect to layer-content view with new attrs
this._stackLayoutView.model.goToStep(1, layerDefinitionModel, 'layer-content', 'analyses', analysisFormAttrs);
}
}, this);
},
saveNotAppliedAnalysis: function (analysis) {
this._notAppliedAnalysis = _.extend({ persisted: false }, analysis);
},
getNotAppliedAnalysis: function () {
return this._notAppliedAnalysis;
},
clearNotAppliedAnalysis: function () {
this._notAppliedAnalysis = null;
}
};
})();

View File

@@ -0,0 +1,3 @@
<h2 class="CDB-Text CDB-Size-huge is-light u-secondaryTextColor">
<%= body %>
</h2>

View File

@@ -0,0 +1,200 @@
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var Infobox = require('builder/components/infobox/infobox-factory');
var InfoboxModel = require('builder/components/infobox/infobox-model');
var InfoboxCollection = require('builder/components/infobox/infobox-collection');
var PanelWithOptionsView = require('builder/components/view-options/panel-with-options-view');
var panelTemplate = require('./panel-with-options.tpl');
var AnalysesContentView = require('./analyses-content-view');
var OnboardingLauncher = require('builder/components/onboardings/generic/generic-onboarding-launcher');
var OnboardingView = require('builder/components/onboardings/layers/analysis-onboarding/analysis-onboarding-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var ONBOARDING_KEY = 'layer-analyses-onboarding';
var REQUIRED_OPTS = [
'userActions',
'analysisFormsCollection',
'layerDefinitionModel',
'analysisDefinitionNodesCollection',
'configModel',
'userModel',
'editorModel',
'stackLayoutModel',
'onboardings',
'onboardingNotification',
'layerContentModel'
];
module.exports = CoreView.extend({
module: 'editor/layers/layer-content-views/analyses/analyses-view',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._selectedNodeId = opts.selectedNodeId;
this._deleteAnalysisModel = new Backbone.Model({
analysisId: null
});
this._infoboxModel = new InfoboxModel({
state: ''
});
this._overlayModel = new Backbone.Model({
visible: this._isLayerHidden()
});
this._onboardingLauncher = new OnboardingLauncher({
view: OnboardingView,
onboardingNotification: this._onboardingNotification,
notificationKey: ONBOARDING_KEY,
onboardings: this._onboardings
}, {
editorModel: this._editorModel,
selector: 'LayerOnboarding'
});
this._getQueryAndCheckState();
this._initBinds();
},
render: function () {
this._launchOnboarding();
this.clearSubViews();
this.$el.empty();
this._initViews();
this._infoboxState();
return this;
},
_initBinds: function () {
this.listenTo(this._layerDefinitionModel, 'change:visible', this._infoboxState);
this.listenTo(this._deleteAnalysisModel, 'change:analysisId', this._infoboxState);
this.listenTo(this._analysisFormsCollection, 'reset remove', this._getQueryAndCheckState);
},
_initViews: function () {
var self = this;
var infoboxSstates = [
{
state: 'layer-hidden',
createContentView: function () {
return Infobox.createWithAction({
type: 'alert',
title: _t('editor.messages.layer-hidden.title'),
body: _t('editor.messages.layer-hidden.body'),
action: {
label: _t('editor.messages.layer-hidden.show')
}
});
},
onAction: self._showHiddenLayer.bind(self)
},
{
state: 'deleting-analysis',
createContentView: function () {
return Infobox.createWithAction({
type: 'alert',
title: _t('editor.messages.deleting-analysis.title'),
body: _t('editor.messages.deleting-analysis.body'),
action: {
label: _t('editor.messages.deleting-analysis.delete')
}
});
},
onClose: self._resetDeletingAnalysis.bind(self),
onAction: self._deleteAnalysis.bind(self)
}
];
var infoboxCollection = new InfoboxCollection(infoboxSstates);
var panelWithOptionsView = new PanelWithOptionsView({
template: panelTemplate,
className: 'Editor-content',
editorModel: self._editorModel,
infoboxModel: self._infoboxModel,
infoboxCollection: infoboxCollection,
createContentView: function () {
return new AnalysesContentView({
className: 'Editor-content',
userActions: self._userActions,
analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
userModel: self._userModel,
analysisFormsCollection: self._analysisFormsCollection,
configModel: self._configModel,
layerDefinitionModel: self._layerDefinitionModel,
stackLayoutModel: self._stackLayoutModel,
selectedNodeId: self._selectedNodeId,
overlayModel: self._overlayModel,
layerContentModel: self._layerContentModel,
deleteAnalysisModel: self._deleteAnalysisModel
});
}
});
this.$el.append(panelWithOptionsView.render().el);
this.addView(panelWithOptionsView);
},
_getQueryAndCheckState: function () {
if (!this._analysisFormsCollection.isEmpty()) {
this._infoboxState();
}
},
_launchOnboarding: function () {
if (this._onboardingNotification.getKey(ONBOARDING_KEY)) {
return;
}
if (this._analysisFormsCollection.isEmpty()) {
this._onboardingLauncher.launch();
}
this._layerDefinitionModel.canBeGeoreferenced()
.then(function (canBeGeoreferenced) {
if (canBeGeoreferenced) {
this._onboardingNotification.setKey(ONBOARDING_KEY, true);
}
}.bind(this));
},
_infoboxState: function () {
if (this._isLayerHidden()) {
this._infoboxModel.set({ state: 'layer-hidden' });
this._overlayModel.set({ visible: true });
} else if (this._deleteAnalysisModel.get('analysisId')) {
this._infoboxModel.set({ state: 'deleting-analysis' });
this._overlayModel.set({ visible: true });
} else {
this._infoboxModel.set({ state: '' });
this._overlayModel.set({ visible: false });
}
},
_showHiddenLayer: function () {
this._layerDefinitionModel.toggleVisible();
this._userActions.saveLayer(this._layerDefinitionModel);
this._infoboxState();
},
_isLayerHidden: function () {
return this._layerDefinitionModel.get('visible') === false;
},
_resetDeletingAnalysis: function () {
this._deleteAnalysisModel.unset('analysisId');
},
_deleteAnalysis: function () {
this._analysisFormsCollection.deleteNode(this._deleteAnalysisModel.get('analysisId'));
this._resetDeletingAnalysis();
}
});

View File

@@ -0,0 +1,97 @@
var CoreView = require('backbone/core-view');
var template = require('./analyses-workflow-item.tpl');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var Analyses = require('builder/data/analyses');
var NotificationErrorMessageHandler = require('builder/editor/layers/notification-error-message-handler');
var nodeIds = require('builder/value-objects/analysis-node-ids');
var layerColors = require('builder/data/layer-colors');
var Router = require('builder/routes/router');
var linkTemplate = require('./more-info-link.tpl');
module.exports = CoreView.extend({
tagName: 'li',
className: 'VerticalRadioList-item',
events: {
'click': '_onNodeClick'
},
initialize: function (opts) {
if (!opts.analysisNode && !opts.formModel) throw new Error('analysisNode or formModel is required');
this._isNew = !opts.analysisNode;
this._analysisNode = opts.analysisNode || opts.formModel; // if form model represents a new node there is no analysis yet; use form model as fallback
this._layerId = opts.layerId;
this._selectedNodeId = opts.selectedNodeId;
this.listenTo(this._analysisNode, 'change:status', this.render);
},
render: function () {
var status = this._analysisNode.get('status');
var isDone = this._isNew || status === 'ready' || status === 'failed';
var isSelected = this._analysisNode.id === this._selectedNodeId;
var hasFailed = status === 'failed';
var tooltipMessage = this._tooltipMessage(hasFailed);
var letter = nodeIds.letter(this._analysisNode.id);
var bgColor = layerColors.getColorForLetter(letter);
var helpURL = Analyses.link(this._analysisNode.get('type'));
var linkContent;
if (isSelected) {
linkContent = linkTemplate({
link: helpURL,
type: this._analysisNode.get('type')
});
}
this.clearSubViews();
this.$el.html(
template({
isNew: this._isNew,
isDone: isDone,
bgColor: bgColor,
isSelected: isSelected,
nodeId: this._analysisNode.id,
hasError: hasFailed,
name: this._analysisName(),
linkContent: linkContent
})
);
if (!this._isNew && !isSelected) {
this._tooltip = new TipsyTooltipView({
el: this.$('.js-tooltip'),
className: hasFailed ? 'is-error' : '',
gravity: 'w',
title: function () {
return tooltipMessage;
}
});
this.addView(this._tooltip);
}
return this;
},
_onNodeClick: function () {
Router.goToAnalysisNode(this._layerId, this._analysisNode.id);
},
_analysisName: function () {
return Analyses.title(this._analysisNode.get('type') || '');
},
_tooltipMessage: function (hasFailed) {
if (hasFailed) {
var error = this._analysisNode.get('error');
var errorMessage = error && error.message && NotificationErrorMessageHandler.extractError(error.message).message;
return errorMessage;
}
return _t('editor.layers.analysis-form.edit-analysis-tooltip');
}
});

View File

@@ -0,0 +1,30 @@
<div class="
VerticalRadioList-itemInner
<% if (hasError) { %> has-error<% } %>
">
<% if (isDone) { %>
<div class="VerticalRadioList-radio u-rSpace--m">
<input class="CDB-Radio" type="radio" <%- isDone && isSelected ? 'checked' : '' %>>
<span class="u-iBlock CDB-Radio-face"></span>
</div>
<% } else { %>
<div class="CDB-LoaderIcon is-blue u-rSpace--m">
<svg class="CDB-LoaderIcon-spinner" viewbox="0 0 50 50">
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"/>
</svg>
</div>
<% } %>
<div class="CDB-Text CDB-Size-small is-semibold u-rSpace u-upperCase u-flex u-alignCenter" style="color: <%- bgColor %>;">
<span class="u-rSpace--s"><%- nodeId %></span>
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-small"></i>
</div>
<div class="VerticalRadioList-label CDB-Text CDB-Size-medium js-analysis-name js-tooltip">
<%- name %>
</div>
<div class="VerticalRadioList-spacer"></div>
<%= linkContent %>
</div>

View File

@@ -0,0 +1,54 @@
var CoreView = require('backbone/core-view');
var AnalysesWorkflowItemView = require('./analyses-workflow-item-view');
var template = require('./analyses-workflow-list.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var utils = require('builder/helpers/utils');
var REQUIRED_OPTS = [
'analysisDefinitionNodesCollection',
'analysisFormsCollection',
'layerId',
'selectedNodeId'
];
module.exports = CoreView.extend({
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
this.$el.html(template({
layerId: this._layerId
}));
this._initViews();
return this;
},
_initViews: function () {
this._analysisFormsCollection.each(this._renderWorkflowItem, this);
var tooltip = new TipsyTooltipView({
el: this.$('.js-add-analysis'),
gravity: 'w',
title: function () {
return utils.replaceLastSpaceWithNbsp(_t('editor.layers.analysis-form.add-analysis.tooltip'));
},
offset: 8
});
this.addView(tooltip);
},
_renderWorkflowItem: function (formModel) {
var view = new AnalysesWorkflowItemView({
analysisNode: this._analysisDefinitionNodesCollection.get(formModel.id),
formModel: formModel,
layerId: this._layerId,
selectedNodeId: this._selectedNodeId
});
this.$('.js-analyes').append(view.render().el);
this.addView(view);
}
});

View File

@@ -0,0 +1,7 @@
<button class="CDB-Button CDB-Button--dashed CDB-Button--wide CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase u-bSpace--xl u-flex u-justifyCenter u-alignCenter js-add-analysis" data-layer-id="<%- layerId %>">
<span class="js-plus-icon u-flex u-justifyCenter u-alignCenter u-rSpace--m"></span>
<%- _t('editor.layers.analysis-form.add-analysis.label') %>
</button>
<ul class="VerticalRadioList u-tSpace js-analyes"></ul>

View File

@@ -0,0 +1,45 @@
var CoreView = require('backbone/core-view');
var AnalysesWorkflowListView = require('./analyses-workflow-list-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'analysisDefinitionNodesCollection',
'analysisFormsCollection',
'layerDefinitionModel',
'selectedNodeId'
];
module.exports = CoreView.extend({
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.listenTo(this._analysisDefinitionNodesCollection, 'add', function () {
// change:source happens when a node is deleted as well, so we can't just listen to it all the time
this._layerDefinitionModel.once('change:source', this.render, this);
});
},
render: function () {
this.clearSubViews();
this.$el.empty();
this._renderAnalysesView();
return this;
},
_renderAnalysesView: function () {
var self = this;
var view = new AnalysesWorkflowListView({
analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
analysisFormsCollection: self._analysisFormsCollection,
layerId: self._layerDefinitionModel.id,
selectedNodeId: self._selectedNodeId
});
this.$el.append(view.render().el);
this.addView(view);
}
});

View File

@@ -0,0 +1,28 @@
var CoreView = require('backbone/core-view');
var template = require('./analysis-buttons.tpl');
module.exports = CoreView.extend({
className: 'u-flex u-justifySpace',
initialize: function (opts) {
this.template = template;
this.model = opts.model;
this._initBinds();
},
render: function () {
var html = this.template(this.model.attributes);
this.clearSubViews();
this.$el.empty();
this.$el.append(html);
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:isDisabled', this.render);
this.listenTo(this.model, 'change:disableDelete', this.render);
// listenTo drops events if view is removed from DOM
this.model.on('change:isDone', this.render, this);
}
});

View File

@@ -0,0 +1,21 @@
<button class="
CDB-Button
CDB-Button--noPadding
js-delete
<% if (disableDelete) { %> is-disabled<% } %>
">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase
<% if (isDelete) { %> u-errorTextColor<% } else { %> u-actionTextColor<% } %>">
<%- labelDelete %>
</span>
</button>
<button class="CDB-Button CDB-Button--loading CDB-Button--primary js-save
<% if (isDisabled) { %> is-disabled<% } %>
<% if (!isDone) { %> is-loading<% } %>">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase"><%- labelSave %></span>
<div class="CDB-Button-loader CDB-LoaderIcon is-white">
<svg class="CDB-LoaderIcon-spinner" viewbox="0 0 50 50">
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"/>
</svg>
</div>
</button>

View File

@@ -0,0 +1,402 @@
var _ = require('underscore');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var Utils = require('builder/helpers/utils');
var InfoboxFactory = require('builder/components/infobox/infobox-factory');
var InfoboxModel = require('builder/components/infobox/infobox-model');
var InfoboxCollection = require('builder/components/infobox/infobox-collection');
var AnalysesQuotaPresenter = require('./analyses-quota/analyses-quota-presenter');
var AnalysesQuotaOptions = require('./analyses-quota/analyses-quota-options');
var AnalysesQuotaProvider = require('./analyses-quota/analyses-quota-provider');
var AnalysesQuotaEstimation = require('./analyses-quota/analyses-quota-estimation-input');
var AnalysesQuotaEnough = require('./analyses-quota/analyses-quota-enough');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var AnalysisButtonsView = require('./analysis-buttons-view');
var AnalysisQuotaView = require('./analysis-quota-view');
var REQUIRED_OPTS = [
'layerDefinitionModel',
'configModel',
'formModel',
'quotaInfo',
'querySchemaModel',
'stackLayoutModel',
'userActions',
'userModel',
'analysisFormsCollection',
'deleteAnalysisModel'
];
/**
* View representing the apply and cancel/delete buttons for a form
*/
module.exports = CoreView.extend({
module: 'editor:layers:layer-content-views:analyses:analysis-controls-view',
className: 'Options-bar',
events: {
'click .js-save': '_onSaveClicked',
'click .js-delete': '_onDeleteClicked'
},
_buttonsView: null,
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._analysisNode = opts.analysisNode;
AnalysesQuotaEstimation.init(opts.configModel);
AnalysesQuotaEnough.init(opts.configModel);
this._viewModel = new Backbone.Model({
isNewAnalysis: !opts.analysisNode,
hasChanges: !opts.analysisNode,
isDone: true,
type: this._formModel.get('type')
});
this._infoboxModel = new InfoboxModel({
state: this._quotaInfo.getState(),
visible: true
});
this._initViewState();
this.render = this.render.bind(this);
this._initBinds();
if (!opts.analysisNode) {
this._fetchQuotaIfNeeded();
}
this._onFormModelChanged = this._onFormModelChanged.bind(this);
},
render: function () {
var type, isValid, requiresQuota, view;
this.clearSubViews();
this.$el.empty();
type = this._formModel.get('type');
isValid = this._formModel.isValid();
// If no dataservice, requiresQuota returns false because the model doesn't exist and it's converted to boolean
requiresQuota = AnalysesQuotaOptions.requiresQuota(type, this._quotaInfo);
if (this._isAnalysisDone() && !this._hasChanges()) {
view = this._createButtonsView();
} else {
// If the analysis doesn't require quota, let's not wait or check the fetching state
if (!requiresQuota || !isValid) {
view = this._createButtonsView();
} else {
view = this._createQuotaView();
}
}
this.addView(view);
this.$el.append(view.render().el);
this._setTrackingClass();
return this;
},
_initViewState: function () {
this._viewState = new Backbone.Model({
canBeGeoreferenced: false
});
this._setViewState();
},
_setTrackingClass: function () {
this.$('.js-save').addClass('track-' + this._formModel.get('type') + '-analysis track-apply');
},
_initBinds: function () {
this.listenTo(this._formModel, 'change', this._onFormModelChanged);
if (this._analysisNode) {
this._bindAnalysisNodeStatusChanges();
}
this.listenTo(this._viewState, 'change', this.render);
},
_bindAnalysisNodeStatusChanges: function () {
this.listenTo(this._analysisNode, 'change:status', function () {
this._viewModel.set('isDone', this._isAnalysisDone());
var isAnalysisDone = this._isAnalysisDone();
if (isAnalysisDone) {
this.render();
}
});
},
_onFormModelChanged: function () {
var isAnalysisDone = this._isAnalysisDone();
var isDone = this._viewModel.get('isNewAnalysis') || isAnalysisDone;
var canSave = this._formModel.isValid() && isDone;
this._viewModel.set({
hasChanges: true,
type: this._formModel.get('type'),
isDisabled: !canSave
});
this.render();
this._fetchQuotaIfNeeded();
},
_fetchQuotaIfNeeded: function () {
var type = this._formModel.get('type');
var isValid = this._formModel.isValid();
var quota = this._quotaInfo;
if ((quota.needsCheck() || AnalysesQuotaOptions.requiresQuota(type, quota)) && isValid) {
this._fetchQuota();
}
},
_fetchQuota: function () {
this._infoboxModel.set('state', 'fetching');
this._quotaInfo.fetch({
success: function () {
this._buildQuotaViewInfo();
}.bind(this),
error: function (error) {
this._viewModel.set({error: error});
this._infoboxModel.set('state', 'error');
}.bind(this)
});
},
_createButtonsView: function () {
this._viewModel.set({
labelSave: _t('editor.layers.analysis-form.apply-btn'),
labelDelete: this._analysisNode
? _t('editor.layers.analysis-form.delete-btn')
: _t('editor.layers.analysis-form.cancel-btn'),
isDelete: this._analysisNode,
disableDelete: !this._canDelete(),
isDisabled: !this._canSave()
});
if (this._buttonsView === null) {
this._buttonsView = new AnalysisButtonsView({
model: this._viewModel
});
}
return this._buttonsView;
},
_buildQuotaViewInfo: function () {
var formModel = this._formModel;
var userModel = this._userModel;
var type = formModel.get('type');
var service = AnalysesQuotaOptions.getServiceName(type);
var query = this._querySchemaModel.get('query');
var quotaData = this._quotaInfo;
var payload = {};
payload.type = type;
var errorCallback = function (error) {
this._viewModel.set('error', error);
this._infoboxModel.set('state', 'error');
}.bind(this);
var checkEnoughQuota = function (data) {
// Apply transformation to the input
// for example isolines takes tracts in in order to calculate estimation
var rows = AnalysesQuotaOptions.transformInput(type, data, formModel);
payload.estimation = rows;
// with the input, we calculate wether the quota is enough
this.enoughQuotaPromise = AnalysesQuotaEnough.fetch(service, rows);
return this.enoughQuotaPromise;
}.bind(this);
var buildPayload = function (enoughQuota) {
// then we can get the full set of data
payload.quotaInfo = AnalysesQuotaProvider(type, userModel, quotaData, enoughQuota);
payload.creditsLeft = payload.quotaInfo.totalQuota - payload.quotaInfo.usedQuota;
payload.canRunAnalysis = ((enoughQuota && payload.quotaInfo.totalQuota > 0) ||
(payload.quotaInfo.hardLimit === false && payload.quotaInfo.totalQuota > 0));
// we need to check the request's state because it could be another render while permforming the current one
// so, to make sure we have all data returned by the apis, we don't raise the green flag until both stats are resolved.
var estimationState = this.estimationPromise.state();
var enoughQuotaState = this.enoughQuotaPromise.state();
if (estimationState === 'resolved' && enoughQuotaState === 'resolved') {
if (this._infoboxModel.get('state') !== 'ready') {
this.payload = payload;
// finally we are ready to show the information
this._infoboxModel.set('state', 'ready');
}
}
}.bind(this);
var dsReady = quotaData.isReady();
// Only checks api if analysis belongs to a service with quota
if (service !== null && dsReady) {
if (formModel.isValid()) {
// get the estimation for this analysis
this.estimationPromise = AnalysesQuotaEstimation.fetch(query);
this.estimationPromise
.then(checkEnoughQuota, errorCallback)
.then(buildPayload, errorCallback);
} else {
// to render the simple buttons view.
this.render();
}
} else if (!dsReady) {
errorCallback(_t('editor.layers.analysis-form.quota.quota-dataservice-down'));
}
},
_createQuotaView: function () {
var states = [
{
state: 'fetching',
createContentView: function () {
return InfoboxFactory.createLoading({
body: _t('editor.layers.analysis-form.quota.loading')
});
}
},
{
state: 'ready',
createContentView: function () {
var payload = this.payload;
var quotaMessage = '';
if (payload.creditsLeft > 0) {
quotaMessage = _t('editor.layers.analysis-form.quota.credits-left-message', {
smart_count: Utils.formatNumber(payload.creditsLeft)
});
} else {
quotaMessage = _t('editor.layers.analysis-form.quota.no-credits-message');
}
payload.quotaInfo.quotaMessage = quotaMessage;
var dataInfobox = _.extend(AnalysesQuotaPresenter.make(payload, this._configModel, this._userModel), {
title: _t('editor.layers.analysis-form.quota.title'),
action: {
label: _t('editor.layers.analysis-form.confirm-analysis'),
type: 'primary',
disabled: !this._canSave() || !payload.canRunAnalysis
},
quota: payload.quotaInfo,
closable: false
});
return InfoboxFactory.createQuota(dataInfobox);
}.bind(this),
onAction: function () {
var payload = this.payload;
if (this._canSave() && payload.canRunAnalysis) {
this._saveAnalysis();
}
}.bind(this)
},
{
state: 'error',
createContentView: function () {
return InfoboxFactory.createWithAction({
title: _t('editor.layers.analysis-form.quota.title'),
type: 'error',
body: _t('editor.layers.analysis-form.quota.quota-fetch-error', {
error: this._viewModel.get('error')
}),
action: {
label: _t('editor.layers.analysis-form.quota.cancel')
}
});
}.bind(this),
onAction: function () {
this._stackLayoutModel.prevStep('layers');
}.bind(this)
}
];
return new AnalysisQuotaView({
className: 'Infobox-wrapper',
infoboxModel: this._infoboxModel,
infoboxCollection: new InfoboxCollection(states)
});
},
_canSave: function () {
var isAnalysisDone = this._isAnalysisDone();
var isDone = this._viewModel.get('isNewAnalysis') || isAnalysisDone;
var hasChanges = this._viewModel.get('hasChanges');
return this._formModel.isValid() && isDone && hasChanges;
},
_onSaveClicked: function () {
if (this._canSave()) {
this._saveAnalysis();
}
},
_canDelete: function () {
var nodeDefModel = this._analysisNode;
var canDeleteNode = (!nodeDefModel || nodeDefModel.canBeDeletedByUser());
var canBeGeoreferenced = this._viewState.get('canBeGeoreferenced');
var hasAnalyses = this._layerDefinitionModel.hasAnalyses();
return (hasAnalyses && canDeleteNode) || (canDeleteNode && !canBeGeoreferenced);
},
_onDeleteClicked: function () {
if (this._canDelete()) {
this._isLastAnalysisNodeSelected()
? this._deleteAnalysisModel.set('analysisId', this._formModel.get('id'))
: this._deleteAnalysis();
}
},
_hasChanges: function () {
return this._viewModel.get('hasChanges') === true;
},
_isAnalysisDone: function () {
return this._analysisNode && (this._analysisNode.isDone() || this._analysisNode.hasFailed());
},
_isLastAnalysisNodeSelected: function () {
return this._analysisFormsCollection.first() !== this._formModel;
},
_saveAnalysis: function () {
this._infoboxModel.set('state', 'idle');
var newNode = this._userActions.saveAnalysis(this._formModel);
if (!this._analysisNode) {
this._analysisNode = newNode;
this._bindAnalysisNodeStatusChanges();
}
// For cached analysis, if the user edits the analysis to a previous version of it
// the request returns the status:ready but the analysis node already has this state
// so no changes are triggered
this._analysisNode.set({ status: 'launched' }, { silent: true });
this._viewModel.set({ hasChanges: false, isDone: false });
this.render();
},
_deleteAnalysis: function () {
this._analysisFormsCollection.deleteNode(this._formModel.get('id'));
},
_setViewState: function () {
return this._layerDefinitionModel.canBeGeoreferenced()
.then(function (canBeGeoreferenced) {
this._viewState.set('canBeGeoreferenced', canBeGeoreferenced);
}.bind(this));
}
});

View File

@@ -0,0 +1,127 @@
var _ = require('underscore');
var BaseAnalysisFormModel = require('./base-analysis-form-model');
var template = require('./aggregate-intersection-form.tpl');
var ColumnOptions = require('builder/editor/layers/layer-content-views/analyses/column-options');
var DialogConstants = require('builder/components/form-components/_constants/_dialogs');
var AGGREGATE_FORMAT_FIELDS_NAMES = [
'type',
'aggregate_column',
'aggregate_function'
];
/**
* Form model for the aggregate intersection analysis
*/
module.exports = BaseAnalysisFormModel.extend({
parse: function (attrs) {
if (!attrs.aggregate_column) {
attrs.aggregate_column = 'cartodb_id';
}
if (!attrs.aggregate_function) {
attrs.aggregate_function = 'count';
}
attrs.aggregate = {
operator: attrs.aggregate_function,
attribute: attrs.aggregate_function === 'count' ? '' : attrs.aggregate_column
};
return attrs;
},
initialize: function () {
BaseAnalysisFormModel.prototype.initialize.apply(this, arguments);
this._columnOptions = new ColumnOptions({}, {
configModel: this._configModel
});
this.listenTo(this._analysisSourceOptionsModel, 'change:fetching', this._setSchema);
this.listenTo(this._columnOptions, 'columnsFetched', this._setSchema);
this.on('change:aggregate', this._onChangeAggregate, this);
this.on('change:target', this._onChangeTarget, this);
this._setSchema();
this._fetchColumns();
},
_formatAttrs: function (formAttrs) {
var customFormattedFormAttrs = _.pick(formAttrs, ['id', 'source', 'target'].concat(AGGREGATE_FORMAT_FIELDS_NAMES));
return BaseAnalysisFormModel.prototype._formatAttrs.call(this, customFormattedFormAttrs);
},
getTemplate: function () {
return template;
},
getTemplateData: function () {
return {
dataFields: 'aggregate',
hasTarget: this.get('target') !== undefined
};
},
_filterSchemaFieldsByType: function (schema) {
// Always include the source and target fields in addition to the type-specific fields
return _.pick(schema, ['source', 'target', 'aggregate']);
},
/**
* @override {BaseAnalysisFormModel._setSchema}
*/
_setSchema: function () {
BaseAnalysisFormModel.prototype._setSchema.call(this, this._filterSchemaFieldsByType({
source: this._primarySourceSchemaItem(_t('editor.layers.analysis-form.base-layer')),
target: {
type: 'NodeDataset',
title: _t('editor.layers.analysis-form.target-layer'),
options: this._getSourceOptionsForSource({
sourceAttrName: 'target'
}),
validators: ['required'],
dialogMode: DialogConstants.Mode.FLOAT,
editorAttrs: {
disabled: this._isSourceDisabled('target')
}
},
aggregate: {
type: 'Operators',
title: _t('editor.layers.analysis-form.operation'),
dialogMode: DialogConstants.Mode.FLOAT,
options: this._columnOptions.filterByType('number')
}
}));
},
_onChangeAggregate: function () {
var aggregate = this.get('aggregate');
this.set({
aggregate_column: aggregate.attribute || 'cartodb_id',
aggregate_function: aggregate.operator
});
},
_onChangeTarget: function () {
this.set('aggregate', {
operator: 'count',
attribute: ''
});
this._analysisSourceOptionsModel.createSourceNodeUnlessExisting(this.get('target'));
this._fetchColumns();
},
_fetchColumns: function () {
var target = this.get('target');
var nodeDefModel = this._layerDefinitionModel.findAnalysisDefinitionNodeModel(target);
if (nodeDefModel) {
this._columnOptions.setNode(nodeDefModel);
} else if (target) {
this._columnOptions.setDataset(target);
}
},
_isSourceDisabled: function (sourceAttrName) {
return this._isPrimarySource(sourceAttrName) || this._isFetchingOptions();
}
});

View File

@@ -0,0 +1,22 @@
<form>
<div class="Editor-HeaderInfo">
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">1</div>
<div class="Editor-HeaderInfo-inner CDB-Text" data-fields="source,target">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.layers.analysis-form.select-layer') %></h2>
</div>
<div class="Editor-HeaderInfo-subtitle u-bSpace--m">
<p class="CDB-Text CDB-FontSize-small u-upperCase u-altTextColor"><%- _t('editor.layers.analysis-form.intersect-step-one-desc') %></p>
</div>
</div>
</div>
<div class="Editor-HeaderInfo <%- hasTarget ? '' : 'is-disabled' %>">
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">2</div>
<div class="Editor-HeaderInfo-inner CDB-Text" data-fields="<%- dataFields %>">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.layers.analysis-form.measure-by') %></h2>
</div>
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--xl"><%- _t('editor.layers.analysis-form.filter-aggregate') %></p>
</div>
</div>
</form>

View File

@@ -0,0 +1,37 @@
var _ = require('underscore');
var DistanceConverter = require('builder/value-objects/distance-converter');
var DEFAULT_DISTANCE = 'meters';
module.exports = {
type: 'buffer',
label: _t('editor.layers.analysis-form.distance'),
parametersDataFields: 'type,distance,radius,isolines,dissolved',
parse: function (nodeAttrs) {
var formAttrs = _.extend({}, nodeAttrs);
try {
formAttrs.radius = DistanceConverter.toDistance(nodeAttrs.radius, nodeAttrs.distance);
formAttrs.distance = nodeAttrs.distance || DEFAULT_DISTANCE;
formAttrs.dissolved = nodeAttrs.dissolved === undefined ? false : nodeAttrs.dissolved;
formAttrs.isolines = nodeAttrs.isolines || 1;
} catch (err) {
formAttrs.radius = 100;
formAttrs.isolines = 1;
formAttrs.distance = DEFAULT_DISTANCE;
formAttrs.dissolved = false;
}
return formAttrs;
},
formatAttrs: function (formAttrs) {
return _.defaults(
{
radius: DistanceConverter.toMeters(formAttrs.radius, formAttrs.distance)
},
formAttrs
);
}
};

View File

@@ -0,0 +1,248 @@
var _ = require('underscore');
var BaseAnalysisFormModel = require('./base-analysis-form-model');
var template = require('./area-of-influence-form.tpl');
var DistanceConverter = require('builder/value-objects/distance-converter');
var DialogConstants = require('builder/components/form-components/_constants/_dialogs');
var AREA_OF_INFLUENCE_TYPES = require('./area-of-influence-types');
var TYPE_TO_META_MAP = {};
AREA_OF_INFLUENCE_TYPES.map(function (d) {
TYPE_TO_META_MAP[d.type] = d;
});
module.exports = BaseAnalysisFormModel.extend({
parse: function (attrs) {
return _.defaults(
_.pick(attrs, 'id', 'source'), // maintain default attrs
this._typeDef(attrs.type).parse(attrs)
);
},
initialize: function () {
BaseAnalysisFormModel.prototype.initialize.apply(this, arguments);
this._setSchema();
this.on('change:type', this._onTypeChanged, this);
this.on('change:distance', this._onDistanceChanged, this);
this.on('change:kind', this._onKindChanged, this);
},
getTemplate: function () {
return template;
},
getTemplateData: function () {
return {parametersDataFields: this._typeDef().parametersDataFields};
},
/**
* @override {BaseAnalysisFormModel.updateNodeDefinition}
*/
updateNodeDefinition: function (nodeDefModel) {
var attrs = this._formatAttrs(this.attributes);
nodeDefModel.clear({silent: true});
nodeDefModel.set(attrs);
},
_formatAttrs: function (formAttrs) {
var customFormattedFormAttrs = this._typeDef().formatAttrs(formAttrs);
return BaseAnalysisFormModel.prototype._formatAttrs.call(this, customFormattedFormAttrs);
},
/**
* @override {BaseAnalysisFormModel._setSchema}
*/
_setSchema: function () {
BaseAnalysisFormModel.prototype._setSchema.call(this,
this._filterSchemaFieldsByType({
source: this._primarySourceSchemaItem(),
type: {
type: 'Radio',
text: _t('editor.layers.analysis-form.type'),
options: AREA_OF_INFLUENCE_TYPES.map(function (d) {
var isAnalysisTypeEnabled = this._analyses.isAnalysisValidByType(d.type, { configModel: this._configModel });
var canChangeToType = this._canChangeToType(d.type);
var isDisabled = !isAnalysisTypeEnabled || !canChangeToType;
var helpMessage = !isAnalysisTypeEnabled ? _t('editor.layers.analysis-form.disabled-by-config') : _t('editor.layers.analysis-form.valid-type');
return {
val: d.type,
label: d.label,
disabled: isDisabled,
help: isDisabled ? helpMessage : ''
};
}, this)
},
distance: {
type: 'Radio',
title: _t('editor.layers.analysis-form.units'),
options: _.map(DistanceConverter.OPTIONS, function (d) {
return {
val: d.distance,
label: this._distanceLabel(d.distance)
};
}, this)
},
radius: {
type: 'Number',
label: _t('editor.layers.analysis-form.radius'),
validators: ['required', this._radiusInterval()]
},
kind: {
type: 'Select',
title: _t('editor.layers.analysis-form.by'),
dialogMode: DialogConstants.Mode.FLOAT,
options: [
{
val: 'walk',
label: _t('editor.layers.analysis-form.by-walk')
}, {
val: 'car',
label: _t('editor.layers.analysis-form.by-car')
}
]
},
isolines: {
type: 'Number',
title: _t('editor.layers.analysis-form.tracts'),
help: _t('editor.layers.analysis-form.tracts-help'),
validators: ['required', {
type: 'interval',
min: 1,
max: 4
}]
},
time: {
type: 'Number',
title: _t('editor.layers.analysis-form.time-seconds'),
validators: ['required', this._timeInterval()]
},
dissolved: {
type: 'Radio',
title: _t('editor.layers.analysis-form.boundaries'),
options: [
{
val: 'false',
label: _t('editor.layers.analysis-form.intact'),
help: _t('editor.layers.analysis-form.intact-help')
}, {
val: 'true',
label: _t('editor.layers.analysis-form.dissolved'),
help: _t('editor.layers.analysis-form.dissolved-help')
}
]
}
})
);
},
_canChangeToType: function (type) {
if (type === this.get('type')) return true;
var source = this._layerDefinitionModel.findAnalysisDefinitionNodeModel(this.get('source'));
return source.isValidAsInputForType(type);
},
_filterSchemaFieldsByType: function (schema) {
// Always include the source field in addition to the type-specific fields
var fields = ['source'].concat(this._typeDef().parametersDataFields.split(','));
return _.pick(schema, fields);
},
_onTypeChanged: function () {
this._replaceAttrs();
this._setSchema();
},
_onDistanceChanged: function () {
this._fitParamToRange('radius', this._radiusInterval());
this._setSchema();
},
_onKindChanged: function () {
this._fitParamToRange('time', this._timeInterval());
this._setSchema();
},
_fitParamToRange: function (paramName, range) {
var val = this.get(paramName);
if (val < range.min) {
val = range.min;
} else if (val > range.max) {
val = range.max;
}
this.set(paramName, val, {silent: true});
},
_replaceAttrs: function () {
var attrs = this.parse(this.attributes);
this.clear({silent: true});
this.set('type', attrs.type, {silent: true}); // re-set type to avoid change:type event to trigger again
this.set(attrs);
},
_typeDef: function (type) {
type = type || this.get('type');
return TYPE_TO_META_MAP[type];
},
_radiusInterval: function () {
switch (this.get('distance')) {
case 'meters':
return {
type: 'interval',
min: 10,
max: 10000
};
case 'kilometers':
case 'miles':
return {
type: 'interval',
min: 0.5,
max: 100,
step: 0.5
};
default:
return {
type: 'interval',
min: 1,
max: 100
};
}
},
_timeInterval: function () {
switch (this.get('kind')) {
case 'car':
return {
type: 'interval',
min: 60,
max: 14400,
step: 10
};
case 'walk':
default:
return {
type: 'interval',
min: 60,
max: 3600
};
}
},
_distanceLabel: function (distance) {
switch (distance) {
case 'meters':
return _t('editor.layers.analysis-form.meters');
case 'kilometers':
return _t('editor.layers.analysis-form.kilometers');
case 'miles':
return _t('editor.layers.analysis-form.miles');
default:
return '';
}
}
});

View File

@@ -0,0 +1,18 @@
<form>
<div class="Editor-HeaderInfo">
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">1</div>
<div class="Editor-HeaderInfo-inner CDB-Text">
<div class="Editor-HeaderInfo-title u-bSpace--m">
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.layers.analysis-form.parameters') %></h2>
</div>
<div class="Editor-HeaderInfo-subtitle u-bSpace--m">
<p class="CDB-Text CDB-FontSize-small u-upperCase u-altTextColor"><%- _t('editor.layers.analysis-form.parameters-description') %></p>
</div>
<div class="u-tSpace-xl CDB-Text CDB-Fieldset">
<p class="CDB-Legend u-upperCase u-iBlock CDB-Text is-semibold CDB-Size-small u-rSpace--m"><%- _t('editor.layers.analysis-form.base-layer') %></p>
<div class="Editor-formInput u-ellipsis" data-editors="source"></div>
</div>
<div data-fields="<%- parametersDataFields %>"></div>
</div>
</div>
</form>

View File

@@ -0,0 +1,19 @@
module.exports = {
type: 'trade-area',
label: _t('editor.layers.analysis-form.time'),
parametersDataFields: 'type,kind,time,isolines,dissolved',
parse: function (nodeAttrs) {
return {
type: 'trade-area',
kind: nodeAttrs.kind || 'walk',
isolines: nodeAttrs.isolines || 1,
time: nodeAttrs.time || 100,
dissolved: nodeAttrs.dissolved === 'true' || nodeAttrs.dissolved === true
};
},
formatAttrs: function (formAttrs) {
return formAttrs;
}
};

View File

@@ -0,0 +1,4 @@
module.exports = [
require('./area-of-influence-buffer'),
require('./area-of-influence-trade-area')
];

View File

@@ -0,0 +1,41 @@
WITH dep_fns AS (
SELECT oid, proname, proargnames, proargtypes FROM pg_catalog.pg_proc WHERE proname ~* '^dep_ext_'
),
fns_argstypes AS (SELECT oid, array_agg((
SELECT
CASE
WHEN typcategory = 'A' THEN 'array'
WHEN typcategory = 'N' THEN 'number'
WHEN typcategory = 'S' THEN 'string'
WHEN typcategory = 'B' THEN 'boolean'
ELSE 'unknown' END
FROM pg_catalog.pg_type
WHERE argtype = pg_catalog.pg_type.oid
)) as argtypes
FROM (
SELECT pg_proc.oid, unnest(pg_proc.proargtypes) as argtype
FROM pg_catalog.pg_proc, dep_fns
WHERE pg_proc.oid = dep_fns.oid
) _q
group by oid
),
fns_meta AS (
SELECT
dep_fns.proname,
dep_fns.proargnames,
fns_argstypes.argtypes,
CASE WHEN fns_argstypes.argtypes[6] = 'array' THEN
-- [whether it has a secondary node or not, where the extra parameters start, where to stop]
ARRAY[1,7,array_length(argtypes,1)]
ELSE
ARRAY[0,5,array_length(argtypes,1)]
END as p
FROM dep_fns, fns_argstypes
WHERE dep_fns.oid = fns_argstypes.oid
)
SELECT
proname as fn_name,
proargnames[p[2]:p[3]] as params_names,
argtypes[p[2]:p[3]] as params_types,
p[1]::boolean as has_secondary_node
FROM fns_meta

View File

@@ -0,0 +1,200 @@
var _ = require('underscore');
var Backbone = require('backbone');
var DialogConstants = require('builder/components/form-components/_constants/_dialogs');
var camshaftReference = require('builder/data/camshaft-reference');
/**
* Base model for all analysis form models.
*/
module.exports = Backbone.Model.extend({
initialize: function (attrs, opts) {
if (!opts.analyses) throw new Error('analyses is required');
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
if (!opts.analysisSourceOptionsModel) throw new Error('analysisSourceOptionsModel is required');
this._analyses = opts.analyses;
this._configModel = opts.configModel;
this._layerDefinitionModel = opts.layerDefinitionModel;
this._analysisSourceOptionsModel = opts.analysisSourceOptionsModel;
this.schema = {};
},
isNew: function () {
return true; // so one can call .destroy() on it to clean up bindings etc. and have it removed from its collection
},
getTemplate: function () {
return undefined; // override to return custom template
},
getTemplateData: function () {
return {}; // override to return custom template data
},
_getSourceOption: function () {
var sourceNodeDefModel = this._getSourceNodeDefModel();
var layerName = sourceNodeDefModel.isSourceType()
? this._layerDefinitionModel.getTableName()
: this._layerDefinitionModel.getName();
return sourceNodeDefModel && [{
val: sourceNodeDefModel.id,
type: 'node',
nodeTitle: this._analyses.short_title(sourceNodeDefModel),
layerName: layerName,
color: sourceNodeDefModel.getColor(),
isSourceType: sourceNodeDefModel.isSourceType()
}];
},
_getSourceNodeDefModel: function () {
var sourceNames = ['source', 'primary_source'];
var sourceNodeDefModel = _.reduce(sourceNames, function (memo, sourceName) {
if (!memo) {
memo = this._layerDefinitionModel.findAnalysisDefinitionNodeModel(this.get(sourceName));
}
return memo;
}, null, this);
return sourceNodeDefModel;
},
_primarySourceSchemaItem: function (customLabel) {
return {
type: 'NodeDataset',
title: customLabel || _t('editor.layers.analysis-form.base-layer'),
dialogMode: DialogConstants.Mode.FLOAT,
options: this._getSourceOption(),
editorAttrs: {disabled: true}
};
},
setFormValidationErrors: function (errors) {
this._formValidationErrors = errors;
this.trigger('change', this);
},
validate: function () {
var errors = _.extend(
{},
camshaftReference.validate(this.attributes),
this._formValidationErrors
);
if (!_.isEmpty(errors)) {
return errors;
}
},
/**
* @override {Backbone.Model.prototype.set} Maintain persisted attr if model is clear'ed
*/
set: function (key, val, options) {
if (key == null) return this;
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
var persisted = attrs.persisted || this.get('persisted');
Backbone.Model.prototype.set.call(this, attrs, options);
if (persisted !== undefined) {
Backbone.Model.prototype.set.call(this, 'persisted', persisted, {silent: true});
}
return this;
},
updateNodeDefinition: function (nodeDefModel) {
var nodeAttrs = this._formatAttrs(this.attributes);
nodeDefModel.set(nodeAttrs);
},
/**
* @deprecated use updateNodeDefinition instead
*/
_updateNodeDefinition: function () {
this.updateNodeDefinition.apply(this, arguments);
},
createNodeDefinition: function (userActions) {
var nodeAttrs = this._formatAttrs(this.attributes);
this.set('persisted', true);
return userActions.createAnalysisNode(nodeAttrs, this._layerDefinitionModel);
},
_formatAttrs: function (formAttrs) {
return _.omit(camshaftReference.parse(formAttrs), 'persisted');
},
_setSchema: function (schema) {
this.schema = schema;
this.trigger('changeSchema', this);
},
_isPrimarySource: function (sourceAttrName) {
return sourceAttrName === this.get('primary_source_name');
},
_isFetchingOptions: function () {
return this._analysisSourceOptionsModel.get('fetching');
},
_getSourceOptionsForSource: function (options) {
options = options || {};
var sourceAttrName = null;
var requiredSimpleGeometryType = '*';
var ignorePrimarySource = false;
var includeSourceNode = options.includeSourceNode || false;
// options validation and defaults
if (options.sourceAttrName === void 0) {
throw new Error('sourceAttrName is required.');
} else {
sourceAttrName = options.sourceAttrName;
}
if (options.requiredSimpleGeometryType !== void 0) {
requiredSimpleGeometryType = options.requiredSimpleGeometryType;
}
if (options.ignorePrimarySource) {
ignorePrimarySource = true;
}
var currentSource = this.get(sourceAttrName);
if (!ignorePrimarySource && this._isPrimarySource(sourceAttrName)) {
return [currentSource];
}
if (this._isFetchingOptions()) {
return [{
val: currentSource,
label: _t('editor.layers.analysis-form.loading'),
isLoading: true
}];
} else {
// fetched
var sourceId = this._layerDefinitionModel.get('source');
return this._analysisSourceOptionsModel
.getSelectOptions(requiredSimpleGeometryType)
.filter(function (d) {
return (
(includeSourceNode ? true : d.val !== sourceId) &&
(options.onlyNodes ? d.type === 'node' : true)
);
});
}
}
});

View File

@@ -0,0 +1,178 @@
var _ = require('underscore');
var BaseAnalysisFormModel = require('./base-analysis-form-model');
var DialogConstants = require('builder/components/form-components/_constants/_dialogs');
var template = require('./centroid-form.tpl');
var ColumnOptions = require('builder/editor/layers/layer-content-views/analyses/column-options');
var FORMAT_FIELD_NAMES = 'category_column,aggregation,aggregation_column,weight_column';
var PARAMETERS_DATA_FIELDS = 'source,category,weight';
/**
* Form model for a centroid and weighted-centroid analysis
* It has a rather complicated schema, that depends on several data points and state.
* https://github.com/CartoDB/camshaft/blob/master/reference/versions/0.38.0/reference.json
*/
module.exports = BaseAnalysisFormModel.extend({
parse: function (attrs) {
var operator = {
operator: attrs.aggregation || 'count',
attribute: attrs.aggregation === 'count' ? '' : attrs.aggregation_column
};
var aggregate_value = (!!attrs.aggregation_column || !!attrs.aggregation) ? operator : '';
return _.extend(attrs, {
category: attrs.category_column || '',
weight: attrs.weight_column || '',
aggregate: aggregate_value
});
},
initialize: function () {
BaseAnalysisFormModel.prototype.initialize.apply(this, arguments);
this._columnOptions = new ColumnOptions({}, {
configModel: this._configModel,
nodeDefModel: this._layerDefinitionModel.findAnalysisDefinitionNodeModel(this.get('source'))
});
this.listenTo(this._columnOptions, 'columnsFetched', this._setSchema);
this.on('change:category', this._onChangeCategory, this);
this.on('change:aggregate', this._updateAggregation, this);
this.on('change:weight', this._onChangeWeight, this);
this.on('change:type', this._setSchema, this);
this._updateAggregation();
this._setSchema();
},
updateNodeDefinition: function (nodeDefModel) {
var attrs = this._formatAttrs(this.attributes);
nodeDefModel.clear({silent: true});
nodeDefModel.set(attrs);
},
_getFormatFieldNames: function () {
var fields = FORMAT_FIELD_NAMES.split(',');
if (this.get('aggregate') === '') {
fields = _.difference(fields, ['aggregation', 'aggregation_column']);
}
return fields.join(',');
},
_formatAttrs: function (formAttrs) {
var customFormattedFormAttrs = _.pick(formAttrs, ['id', 'source', 'type'].concat(this._getFormatFieldNames().split(',')));
return BaseAnalysisFormModel.prototype._formatAttrs.call(this, customFormattedFormAttrs);
},
getTemplate: function () {
return template;
},
getTemplateData: function () {
return {
parametersDataFields: PARAMETERS_DATA_FIELDS
};
},
/*
/**
* @override {BaseAnalysisFormModel._setSchema}
*/
_setSchema: function () {
var operator = {
operator: this.get('aggregation'),
attribute: this.get('aggregation_column')
};
BaseAnalysisFormModel.prototype._setSchema.call(this, {
source: this._primarySourceSchemaItem(),
category: {
type: 'EnablerEditor',
title: '',
label: _t('editor.layers.analysis-form.categorize-by'),
help: _t('editor.layers.analysis-form.categorize-by-help'),
editor: {
type: 'Select',
options: this._columnOptions.all(),
dialogMode: DialogConstants.Mode.FLOAT,
label: _t('editor.layers.analysis-form.column')
}
},
weight: {
type: 'EnablerEditor',
title: '',
label: _t('editor.layers.analysis-form.weighted-by'),
help: _t('editor.layers.analysis-form.weighted-by-help'),
validators: ['required'],
editor: {
type: 'Select',
options: this._columnOptions.filterByType('number'),
dialogMode: DialogConstants.Mode.FLOAT,
label: _t('editor.layers.analysis-form.column')
}
},
aggregate: {
type: 'EnablerEditor',
title: '',
label: _t('editor.layers.analysis-form.operation'),
help: _t('editor.layers.analysis-form.aggregate-by-help'),
defaultValue: operator,
editor: {
type: 'Operators',
options: this._columnOptions.filterByType('number'),
dialogMode: DialogConstants.Mode.FLOAT,
label: _t('editor.layers.analysis-form.column')
}
}
});
},
_onChangeWeight: function () {
if (this.get('weight') !== '') {
this.set('type', 'weighted-centroid');
this.set('weight_column', this.get('weight'));
} else {
this.set('type', 'centroid');
this.unset('weight_column');
}
this._setSchema();
},
_onChangeCategory: function () {
// category_column is optional but if we send it empty,
// it has influence in the final result
if (this.get('category') === '') {
this.unset('category_column');
} else {
this.set('category_column', this.get('category'));
}
this._setSchema();
},
_updateAggregation: function () {
var aggregate = this.get('aggregate');
if (aggregate === '') {
this.set({
aggregation: 'count',
aggregation_column: ''
});
} else {
this.set({
aggregation_column: aggregate.attribute,
aggregation: aggregate.operator
});
}
this._setSchema();
}
});

Some files were not shown because too many files have changed in this diff Show More