var Backbone = require('backbone'); var _ = require('underscore'); var CoreView = require('backbone/core-view'); var template = require('./data-layer.tpl'); var fetchingTemplate = require('./data-layer-fetching.tpl'); var ContextMenuView = require('builder/components/context-menu/context-menu-view'); var CustomListCollection = require('builder/components/custom-list/custom-list-collection'); var renameLayer = require('builder/editor/layers/operations/rename-layer'); var DeleteLayerConfirmationView = require('builder/components/modals/remove-layer/delete-layer-confirmation-view'); var ModalExportDataView = require('builder/components/modals/export-data/modal-export-data-view'); var InlineEditorView = require('builder/components/inline-editor/inline-editor-view'); var TipsyTooltipView = require('builder/components/tipsy-tooltip-view'); var zoomToData = require('builder/editor/map-operations/zoom-to-data'); var templateInlineEditor = require('./inline-editor.tpl'); var geometryNoneTemplate = require('./geometry-none.tpl'); var geometryPointsTemplate = require('./geometry-points.tpl'); var geometryLinesTemplate = require('./geometry-lines.tpl'); var geometryPolygonsTemplate = require('./geometry-polygons.tpl'); var checkAndBuildOpts = require('builder/helpers/required-opts'); var fetchAllQueryObjects = require('builder/helpers/fetch-all-query-objects'); var Router = require('builder/routes/router'); var IconView = require('builder/components/icon/icon-view'); var STATES = require('builder/data/query-base-status'); const { nodeHasTradeArea, nodeHasSQLFunction } = require('builder/helpers/analysis-node-utils'); var REQUIRED_OPTS = [ 'userActions', 'layerDefinitionsCollection', 'modals', 'configModel', 'stateDefinitionModel', 'widgetDefinitionsCollection', 'visDefinitionModel', 'analysisDefinitionNodesCollection' ]; module.exports = CoreView.extend({ module: 'editor:layers:layer-views:data-layer-view', tagName: 'li', className: 'Editor-ListLayer-item js-layer', events: { 'click': '_onEditLayer', 'click .js-base': '_onClickSource', 'click .js-title': '_onClickTitle', 'click .js-toggle-menu': '_onToggleContextMenuClicked', 'click .js-toggle': '_onToggleLayerClicked', 'click .js-analysis-node': '_onAnalysisNodeClicked' }, initialize: function (opts) { checkAndBuildOpts(opts, REQUIRED_OPTS, this); if (!_.isFunction(opts.newAnalysesView)) throw new Error('newAnalysesView is required as a function'); this._newAnalysesView = opts.newAnalysesView; this._styleModel = this.model.styleModel; var nodeDefModel = this.model.getAnalysisDefinitionNodeModel(); this._queryGeometryModel = nodeDefModel.queryGeometryModel; this._queryRowsStatus = nodeDefModel.queryRowsCollection.statusModel; this._initViewState(); this._bindEvents(); fetchAllQueryObjects({ queryRowsCollection: nodeDefModel.queryRowsCollection, queryGeometryModel: this._queryGeometryModel, querySchemaModel: nodeDefModel.querySchemaModel }).then(function () { this.model.fetchQueryRowsIfRequired(); this._setViewState(); }.bind(this)); }, render: function () { this.clearSubViews(); var isAnimation = this._styleModel.isAnimation(); var fetching = this._viewState.get('isFetchingRows'); if (fetching) { this.$el.html(fetchingTemplate()); this.$el.addClass('Editor-ListLayer-item--fetching'); return this; } else { this.$el.removeClass('Editor-ListLayer-item--fetching'); } this.$el.html(template({ layerId: this.model.id, title: this.model.getName(), color: this.model.getColor(), isVisible: this.model.get('visible'), isAnimated: isAnimation, isTorque: this._isTorque(), hasError: this._hasError(), isCollapsed: this._isCollapsed(), numberOfAnalyses: this.model.getNumberOfAnalyses(), numberOfWidgets: this._widgetDefinitionsCollection.widgetsOwnedByLayer(this.model.id), needsGeocoding: this._viewState.get('needsGeocoding'), hasGeom: this._viewState.get('queryGeometryHasGeom'), isEmpty: this._viewState.get('isLayerEmpty'), brokenLayer: false })); this._initViews(); return this; }, _initViewState: function () { this._viewState = new Backbone.Model({ needsGeocoding: false, isLayerEmpty: false, queryGeometryHasGeom: true, isFetchingRows: false }); this._setViewState(); }, _isTorque: function () { return this.model.isTorqueLayer(); }, _initViews: function () { var geometryTemplate = this._getGeometryTemplate(this._queryGeometryModel.get('simple_geom')); this._inlineEditor = new InlineEditorView({ template: templateInlineEditor, renderOptions: { title: this.model.getName() }, onClick: this._onEditLayer.bind(this), onEdit: this._renameLayer.bind(this) }); this.addView(this._inlineEditor); this.$('.js-thumbnail').append(geometryTemplate({ letter: this.model.get('letter') })); this.$('.js-header').html(this._inlineEditor.render().el); this.$el.toggleClass('is-unavailable', this.model.isNew()); this.$el.toggleClass('js-sortable-item', !this._isTorque()); this.$el.toggleClass('is-animated', this._isTorque()); this.$el.toggleClass('is-empty', this._viewState.get('needsGeocoding')); this.$('.js-thumbnail').toggleClass('is-hidden', this._isHidden()); this.$('.js-title').toggleClass('is-hidden', this._isHidden()); this.$('.js-analyses-widgets-info').toggleClass('is-hidden', this._isHidden()); if (this._isTorque()) { var torqueTooltip = new TipsyTooltipView({ el: this.$('.js-torqueIcon'), gravity: 's', offset: 0 }); this.addView(torqueTooltip); } if (this._viewState.get('needsGeocoding')) { var georeferenceTooltip = new TipsyTooltipView({ el: this.$('.js-geocode'), gravity: 'w', offset: 0 }); this.addView(georeferenceTooltip); } if (this._viewState.get('isLayerEmpty')) { var warningIcon = new IconView({ placeholder: this.$el.find('.js-emptylayer'), icon: 'warning' }); warningIcon.render(); this.addView(warningIcon); var emptyLayerTooltip = new TipsyTooltipView({ el: this.$el.find('.js-emptylayer'), gravity: 'w', title: function () { return _t('editor.layers.layer.empty-layer'); } }); this.addView(emptyLayerTooltip); } this._toggleClickEventsOnCapturePhase('remove'); // remove any if rendered previously if (this.model.isNew()) { this._toggleClickEventsOnCapturePhase('add'); } if (this.model.get('source')) { var analysesView = this._newAnalysesView(this.$('.js-analyses'), this.model); this.addView(analysesView); analysesView.render(); } if (this._hasError()) { var errorTooltip = new TipsyTooltipView({ el: this.$('.js-error'), gravity: 's', offset: 0, title: function () { return this.model.get('error') && this.model.get('error').message; }.bind(this) }); this.addView(errorTooltip); } var toggleTooltip = new TipsyTooltipView({ el: this.$('.js-toggle-menu'), gravity: 'w', title: function () { return _t('more-options'); } }); this.addView(toggleTooltip); var toggleMenuTooltip = new TipsyTooltipView({ el: this.$('.js-toggle'), gravity: 's', title: function () { return this._isHidden() ? _t('editor.layers.options.show') : _t('editor.layers.options.hide'); }.bind(this) }); this.addView(toggleMenuTooltip); }, _bindEvents: function () { this.listenTo(this.model, 'change', this.render); this.listenToOnce(this.model, 'destroy', this._onDestroy); this.listenTo(this._queryGeometryModel, 'change:simple_geom', this._setViewState); this.listenTo(this._queryRowsStatus, 'change:status', this._onQueryRowsStatusChanged); this.listenTo(this._viewState, 'change', this.render); }, _getGeometryTemplate: function (geometry) { switch (geometry) { case 'line': return geometryLinesTemplate; case 'point': return geometryPointsTemplate; case 'polygon': return geometryPolygonsTemplate; default: return geometryNoneTemplate; } }, _isHidden: function () { return !this.model.get('visible'); }, _hasError: function () { return !!this.model.get('error'); }, _isCollapsed: function () { return !!this.model.get('collapsed'); }, _onClickTitle: function (event) { // event is handled with inlineEditor event.stopPropagation(); }, _onAnalysisNodeClicked: function (event) { event.stopPropagation(); var nodeId = event.currentTarget && event.currentTarget.dataset.analysisNodeId; if (!nodeId) throw new Error('missing data-analysis-node-id on element to edit analysis node, the element was: ' + event.currentTarget.outerHTML); var nodeDefModel = this._analysisDefinitionNodesCollection.get(nodeId); var layerDefModel = this._layerDefinitionsCollection.findOwnerOfAnalysisNode(nodeDefModel); if (!layerDefModel) throw new Error('no owning layer found for node ' + nodeId); Router.goToAnalysisNode(this.model.get('id'), nodeId); }, _onClickSource: function (event) { event.stopPropagation(); Router.goToDataTab(this.model.get('id')); }, _onEditLayer: function (event) { event && event.stopPropagation(); var self = this; this.model.canBeGeoreferenced() .then(function (canBeGeoreferenced) { if (canBeGeoreferenced) { Router.goToAnalysisTab(self.model.get('id')); } }); Router.goToStyleTab(self.model.get('id')); // Speculative execution. We bet on not needing geocoding to give quick feedback to the user. }, _onToggleCollapsedLayer: function () { this.model.toggleCollapse(); }, _onToggleLayerClicked: function (event) { event.stopPropagation(); var savingOptions = { shouldPreserveAutoStyle: true }; this.model.toggleVisible(); this._userActions.saveLayer(this.model, savingOptions); }, _onToggleContextMenuClicked: function (event) { event.stopPropagation(); if (this._hasContextMenu()) { this._hideContextMenu(); } else { this._showContextMenu({ x: event.pageX, y: event.pageY }); } }, _hasContextMenu: function () { return this._menuView; }, _hideContextMenu: function () { this.removeView(this._menuView); this._menuView.clean(); delete this._menuView; }, _getContextMenuOptions: function () { var menuItems = [{ label: _t('editor.layers.options.edit'), val: 'edit-layer', action: this._onEditLayer.bind(this) }]; if (!this._viewState.get('needsGeocoding')) { menuItems = menuItems.concat([{ label: this._isCollapsed() ? _t('editor.layers.options.expand') : _t('editor.layers.options.collapse'), val: 'collapse-expand-layer', action: this._onToggleCollapsedLayer.bind(this) }, { label: _t('editor.layers.options.rename'), val: 'rename-layer', action: function () { this._inlineEditor.edit(); }.bind(this) }]); if (!this._viewState.get('isLayerEmpty')) { menuItems = menuItems.concat([ { label: _t('editor.layers.options.export'), val: 'export-data', action: this._exportLayer.bind(this) }, { label: _t('editor.layers.options.center-map'), val: 'center-map', action: this._centerMap.bind(this) } ]); } } if (this.model.canBeDeletedByUser()) { menuItems.push({ label: _t('editor.layers.options.delete'), val: 'delete-layer', action: this._confirmDeleteLayer.bind(this), destructive: true }); } return menuItems; }, _showContextMenu: function (position) { var menuItems = new CustomListCollection(this._getContextMenuOptions()); var triggerElementID = 'context-menu-trigger-' + this.model.cid; this.$('.js-toggle-menu').attr('id', triggerElementID); this._menuView = new ContextMenuView({ collection: menuItems, triggerElementID: triggerElementID, position: position }); menuItems.bind('change:selected', function (menuItem) { menuItem.has('action') && menuItem.get('action')(); }, this); this._menuView.model.bind('change:visible', function (model, isContextMenuVisible) { if (this._hasContextMenu() && !isContextMenuVisible) { this._hideContextMenu(); } }, this); this._menuView.show(); this.addView(this._menuView); }, _exportLayer: function () { var nodeDefModel = this.model.getAnalysisDefinitionNodeModel(); const { queryGeometryModel, querySchemaModel } = nodeDefModel; const canHideColumns = nodeHasTradeArea(nodeDefModel) && !nodeHasSQLFunction(nodeDefModel); this._modals.create(function (modalModel) { return new ModalExportDataView({ fromView: 'main', modalModel: modalModel, queryGeometryModel, querySchemaModel, canHideColumns, configModel: this._configModel, layerModel: this.model, filename: this.model.getName() }); }.bind(this)); }, _confirmDeleteLayer: function () { this._modals.create(function (modalModel) { var deleteLayerConfirmationView = new DeleteLayerConfirmationView({ userActions: this._userActions, modals: this._modals, layerModel: this.model, modalModel: modalModel, visDefinitionModel: this._visDefinitionModel, widgetDefinitionsCollection: this._widgetDefinitionsCollection }); return deleteLayerConfirmationView; }.bind(this)); }, _renameLayer: function () { var newName = this._inlineEditor.getValue(); if (newName !== '') { // Optimistic this._onSaveSuccess(newName); renameLayer({ newName: newName, userActions: this._userActions, layerDefinitionsCollection: this._layerDefinitionsCollection, layerDefinitionModel: this.model, onError: this._onSaveError.bind(this) }); } }, _onSaveSuccess: function (newName) { this.$('.js-title').text(newName).show(); this._inlineEditor.hide(); }, _onSaveError: function (oldName) { this.$('.js-title').text(oldName).show(); this._inlineEditor.hide(); }, _onDestroy: function () { this.clean(); }, _disableEventsOnCapturePhase: function (evt) { evt.stopPropagation(); evt.preventDefault(); }, _toggleClickEventsOnCapturePhase: function (str) { var addOrRemove = str === 'add' ? 'add' : 'remove'; this.el[addOrRemove + 'EventListener']('click', this._disableEventsOnCapturePhase, true); }, _centerMap: function () { var nodeModel = this.model.getAnalysisDefinitionNodeModel(); var query = nodeModel.querySchemaModel.get('query'); zoomToData(this._configModel, this._stateDefinitionModel, query); }, _isFetchingRows: function () { var nodeDefModel = this.model.getAnalysisDefinitionNodeModel(); var queryRowsStatus = nodeDefModel.queryRowsCollection.statusModel; return queryRowsStatus.get('status') === STATES.fetching; }, _onQueryRowsStatusChanged: function () { this._viewState.set('isFetchingRows', this._isFetchingRows()); this._setViewState(); }, _setViewState: function () { var self = this; var georeferencePromise = this.model.canBeGeoreferenced(); var isLayerEmptyPromise = this.model.isEmptyAsync(); var hasGeomPromise = this._queryGeometryModel.hasValueAsync(); Promise.all([georeferencePromise, isLayerEmptyPromise, hasGeomPromise]) .then(function (values) { self._viewState.set({ needsGeocoding: values[0], isLayerEmpty: values[1], queryGeometryHasGeom: values[2], isFetchingRows: self._isFetchingRows() }); }); }, clean: function () { this._toggleClickEventsOnCapturePhase('remove'); CoreView.prototype.clean.apply(this); } });