You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by co...@apache.org on 2016/10/03 08:58:34 UTC
[2/3] zeppelin git commit: [Zeppelin-1496] Apply Zeppelin-Web Good
Practice Guide #1 to the code
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/a9e7bc38/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
index 971f515..6eaba82 100644
--- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
+++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
@@ -12,2740 +12,2762 @@
* limitations under the License.
*/
'use strict';
+(function() {
+
+ angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl);
+
+ ParagraphCtrl.$inject = [
+ '$scope',
+ '$rootScope',
+ '$route',
+ '$window',
+ '$routeParams',
+ '$location',
+ '$timeout',
+ '$compile',
+ '$http',
+ '$q',
+ 'websocketMsgSrv',
+ 'baseUrlSrv',
+ 'ngToast',
+ 'saveAsService',
+ 'esriLoader'
+ ];
+
+ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $location,
+ $timeout, $compile, $http, $q, websocketMsgSrv,
+ baseUrlSrv, ngToast, saveAsService, esriLoader) {
+ var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_';
+ $scope.parentNote = null;
+ $scope.paragraph = null;
+ $scope.originalText = '';
+ $scope.editor = null;
+ $scope.magic = null;
+
+ var paragraphScope = $rootScope.$new(true, $rootScope);
+
+ // to keep backward compatibility
+ $scope.compiledScope = paragraphScope;
+
+ paragraphScope.z = {
+ // z.runParagraph('20150213-231621_168813393')
+ runParagraph: function(paragraphId) {
+ if (paragraphId) {
+ var filtered = $scope.parentNote.paragraphs.filter(function(x) {
+ return x.id === paragraphId;});
+ if (filtered.length === 1) {
+ var paragraph = filtered[0];
+ websocketMsgSrv.runParagraph(paragraph.id, paragraph.title, paragraph.text,
+ paragraph.config, paragraph.settings.params);
+ } else {
+ ngToast.danger({content: 'Cannot find a paragraph with id \'' + paragraphId + '\'',
+ verticalPosition: 'top', dismissOnTimeout: false});
+ }
+ } else {
+ ngToast.danger({
+ content: 'Please provide a \'paragraphId\' when calling z.runParagraph(paragraphId)',
+ verticalPosition: 'top', dismissOnTimeout: false});
+ }
+ },
-angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $rootScope, $route, $window,
- $routeParams, $location, $timeout, $compile,
- $http, $q, websocketMsgSrv, baseUrlSrv, ngToast,
- saveAsService, esriLoader) {
- var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_';
- $scope.parentNote = null;
- $scope.paragraph = null;
- $scope.originalText = '';
- $scope.editor = null;
- $scope.magic = null;
-
- var paragraphScope = $rootScope.$new(true, $rootScope);
-
- // to keep backward compatibility
- $scope.compiledScope = paragraphScope;
-
- paragraphScope.z = {
- // z.runParagraph('20150213-231621_168813393')
- runParagraph: function(paragraphId) {
- if (paragraphId) {
- var filtered = $scope.parentNote.paragraphs.filter(function(x) {
- return x.id === paragraphId;});
- if (filtered.length === 1) {
- var paragraph = filtered[0];
- websocketMsgSrv.runParagraph(paragraph.id, paragraph.title, paragraph.text,
- paragraph.config, paragraph.settings.params);
+ // Example: z.angularBind('my_var', 'Test Value', '20150213-231621_168813393')
+ angularBind: function(varName, value, paragraphId) {
+ // Only push to server if there paragraphId is defined
+ if (paragraphId) {
+ websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId);
} else {
- ngToast.danger({content: 'Cannot find a paragraph with id \'' + paragraphId + '\'',
+ ngToast.danger({
+ content: 'Please provide a \'paragraphId\' when calling ' +
+ 'z.angularBind(varName, value, \'PUT_HERE_PARAGRAPH_ID\')',
+ verticalPosition: 'top', dismissOnTimeout: false});
+ }
+ },
+
+ // Example: z.angularUnBind('my_var', '20150213-231621_168813393')
+ angularUnbind: function(varName, paragraphId) {
+ // Only push to server if paragraphId is defined
+ if (paragraphId) {
+ websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId);
+ } else {
+ ngToast.danger({
+ content: 'Please provide a \'paragraphId\' when calling ' +
+ 'z.angularUnbind(varName, \'PUT_HERE_PARAGRAPH_ID\')',
verticalPosition: 'top', dismissOnTimeout: false});
}
- } else {
- ngToast.danger({
- content: 'Please provide a \'paragraphId\' when calling z.runParagraph(paragraphId)',
- verticalPosition: 'top', dismissOnTimeout: false});
}
- },
+ };
- // Example: z.angularBind('my_var', 'Test Value', '20150213-231621_168813393')
- angularBind: function(varName, value, paragraphId) {
- // Only push to server if there paragraphId is defined
- if (paragraphId) {
- websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId);
- } else {
- ngToast.danger({
- content: 'Please provide a \'paragraphId\' when calling ' +
- 'z.angularBind(varName, value, \'PUT_HERE_PARAGRAPH_ID\')',
- verticalPosition: 'top', dismissOnTimeout: false});
- }
- },
-
- // Example: z.angularUnBind('my_var', '20150213-231621_168813393')
- angularUnbind: function(varName, paragraphId) {
- // Only push to server if paragraphId is defined
- if (paragraphId) {
- websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId);
- } else {
- ngToast.danger({
- content: 'Please provide a \'paragraphId\' when calling ' +
- 'z.angularUnbind(varName, \'PUT_HERE_PARAGRAPH_ID\')',
- verticalPosition: 'top', dismissOnTimeout: false});
+ var angularObjectRegistry = {};
+
+ // Controller init
+ $scope.init = function(newParagraph, note) {
+ $scope.paragraph = newParagraph;
+ $scope.parentNote = note;
+ $scope.originalText = angular.copy(newParagraph.text);
+ $scope.chart = {};
+ $scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain'];
+ $scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
+ $scope.paragraphFocused = false;
+ if (newParagraph.focus) {
+ $scope.paragraphFocused = true;
+ }
+ if (!$scope.paragraph.config) {
+ $scope.paragraph.config = {};
}
- }
- };
-
- var angularObjectRegistry = {};
-
- // Controller init
- $scope.init = function(newParagraph, note) {
- $scope.paragraph = newParagraph;
- $scope.parentNote = note;
- $scope.originalText = angular.copy(newParagraph.text);
- $scope.chart = {};
- $scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain'];
- $scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
- $scope.paragraphFocused = false;
- if (newParagraph.focus) {
- $scope.paragraphFocused = true;
- }
- if (!$scope.paragraph.config) {
- $scope.paragraph.config = {};
- }
- initializeDefault();
-
- if ($scope.getResultType() === 'TABLE') {
- $scope.loadTableData($scope.paragraph.result);
- $scope.setGraphMode($scope.getGraphMode(), false, false);
- } else if ($scope.getResultType() === 'HTML') {
- $scope.renderHtml();
- } else if ($scope.getResultType() === 'ANGULAR') {
- $scope.renderAngular();
- } else if ($scope.getResultType() === 'TEXT') {
- $scope.renderText();
- }
+ initializeDefault();
- getApplicationStates();
- getSuggestions();
+ if ($scope.getResultType() === 'TABLE') {
+ $scope.loadTableData($scope.paragraph.result);
+ $scope.setGraphMode($scope.getGraphMode(), false, false);
+ } else if ($scope.getResultType() === 'HTML') {
+ $scope.renderHtml();
+ } else if ($scope.getResultType() === 'ANGULAR') {
+ $scope.renderAngular();
+ } else if ($scope.getResultType() === 'TEXT') {
+ $scope.renderText();
+ }
- var activeApp = _.get($scope.paragraph.config, 'helium.activeApp');
- if (activeApp) {
- var app = _.find($scope.apps, {id: activeApp});
- renderApp(app);
- }
- };
+ getApplicationStates();
+ getSuggestions();
- $scope.renderHtml = function() {
- var retryRenderer = function() {
- if (angular.element('#p' + $scope.paragraph.id + '_html').length) {
- try {
- angular.element('#p' + $scope.paragraph.id + '_html').html($scope.paragraph.result.msg);
+ var activeApp = _.get($scope.paragraph.config, 'helium.activeApp');
+ if (activeApp) {
+ var app = _.find($scope.apps, {id: activeApp});
+ renderApp(app);
+ }
+ };
+
+ $scope.renderHtml = function() {
+ var retryRenderer = function() {
+ if (angular.element('#p' + $scope.paragraph.id + '_html').length) {
+ try {
+ angular.element('#p' + $scope.paragraph.id + '_html').html($scope.paragraph.result.msg);
- angular.element('#p' + $scope.paragraph.id + '_html').find('pre code').each(function(i, e) {
- hljs.highlightBlock(e);
+ angular.element('#p' + $scope.paragraph.id + '_html').find('pre code').each(function(i, e) {
+ hljs.highlightBlock(e);
+ });
+ } catch (err) {
+ console.log('HTML rendering error %o', err);
+ }
+ } else {
+ $timeout(retryRenderer, 10);
+ }
+ };
+ $timeout(retryRenderer);
+ };
+
+ $scope.renderAngular = function() {
+ var retryRenderer = function() {
+ if (angular.element('#p' + $scope.paragraph.id + '_angular').length) {
+ try {
+ angular.element('#p' + $scope.paragraph.id + '_angular').html($scope.paragraph.result.msg);
+
+ $compile(angular.element('#p' + $scope.paragraph.id + '_angular').contents())(paragraphScope);
+ } catch (err) {
+ console.log('ANGULAR rendering error %o', err);
+ }
+ } else {
+ $timeout(retryRenderer, 10);
+ }
+ };
+ $timeout(retryRenderer);
+ };
+
+ $scope.renderText = function() {
+ var retryRenderer = function() {
+
+ var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
+ if (textEl.length) {
+ // clear all lines before render
+ $scope.clearTextOutput();
+
+ if ($scope.paragraph.result && $scope.paragraph.result.msg) {
+ $scope.appendTextOutput($scope.paragraph.result.msg);
+ }
+
+ angular.element('#p' + $scope.paragraph.id + '_text').bind('mousewheel', function(e) {
+ $scope.keepScrollDown = false;
});
- } catch (err) {
- console.log('HTML rendering error %o', err);
+ $scope.flushStreamingOutput = true;
+ } else {
+ $timeout(retryRenderer, 10);
}
- } else {
- $timeout(retryRenderer, 10);
- }
+ };
+ $timeout(retryRenderer);
};
- $timeout(retryRenderer);
- };
- $scope.renderAngular = function() {
- var retryRenderer = function() {
- if (angular.element('#p' + $scope.paragraph.id + '_angular').length) {
- try {
- angular.element('#p' + $scope.paragraph.id + '_angular').html($scope.paragraph.result.msg);
+ $scope.clearTextOutput = function() {
+ var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
+ if (textEl.length) {
+ textEl.children().remove();
+ }
+ };
- $compile(angular.element('#p' + $scope.paragraph.id + '_angular').contents())(paragraphScope);
- } catch (err) {
- console.log('ANGULAR rendering error %o', err);
+ $scope.appendTextOutput = function(msg) {
+ var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
+ if (textEl.length) {
+ var lines = msg.split('\n');
+ for (var i = 0; i < lines.length; i++) {
+ textEl.append(angular.element('<div></div>').text(lines[i]));
}
+ }
+ if ($scope.keepScrollDown) {
+ var doc = angular.element('#p' + $scope.paragraph.id + '_text');
+ doc[0].scrollTop = doc[0].scrollHeight;
+ }
+ };
+
+ var initializeDefault = function() {
+ var config = $scope.paragraph.config;
+
+ if (!config.colWidth) {
+ config.colWidth = 12;
+ }
+
+ if (!config.graph) {
+ config.graph = {};
+ }
+
+ if (!config.graph.mode) {
+ config.graph.mode = 'table';
+ }
+
+ if (!config.graph.height) {
+ config.graph.height = 300;
+ }
+
+ if (!config.graph.optionOpen) {
+ config.graph.optionOpen = false;
+ }
+
+ if (!config.graph.keys) {
+ config.graph.keys = [];
+ }
+
+ if (!config.graph.values) {
+ config.graph.values = [];
+ }
+
+ if (!config.graph.groups) {
+ config.graph.groups = [];
+ }
+
+ if (!config.graph.scatter) {
+ config.graph.scatter = {};
+ }
+
+ if (!config.graph.map) {
+ config.graph.map = {};
+ }
+
+ if (!config.graph.map.baseMapType) {
+ config.graph.map.baseMapType = $scope.baseMapOption[0];
+ }
+
+ if (!config.graph.map.isOnline) {
+ config.graph.map.isOnline = true;
+ }
+
+ if (!config.graph.map.pinCols) {
+ config.graph.map.pinCols = [];
+ }
+
+ if (config.enabled === undefined) {
+ config.enabled = true;
+ }
+ };
+
+ $scope.getIframeDimensions = function() {
+ if ($scope.asIframe) {
+ var paragraphid = '#' + $routeParams.paragraphId + '_container';
+ var height = angular.element(paragraphid).height();
+ return height;
+ }
+ return 0;
+ };
+
+ $scope.$watch($scope.getIframeDimensions, function(newValue, oldValue) {
+ if ($scope.asIframe && newValue) {
+ var message = {};
+ message.height = newValue;
+ message.url = $location.$$absUrl;
+ $window.parent.postMessage(angular.toJson(message), '*');
+ }
+ });
+
+ var isEmpty = function(object) {
+ return !object;
+ };
+
+ $scope.isRunning = function() {
+ if ($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING') {
+ return true;
} else {
- $timeout(retryRenderer, 10);
+ return false;
}
};
- $timeout(retryRenderer);
- };
- $scope.renderText = function() {
- var retryRenderer = function() {
+ $scope.cancelParagraph = function() {
+ console.log('Cancel %o', $scope.paragraph.id);
+ websocketMsgSrv.cancelParagraphRun($scope.paragraph.id);
+ };
- var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
- if (textEl.length) {
- // clear all lines before render
- $scope.clearTextOutput();
+ $scope.runParagraph = function(data) {
+ websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title,
+ data, $scope.paragraph.config, $scope.paragraph.settings.params);
+ $scope.originalText = angular.copy(data);
+ $scope.dirtyText = undefined;
+ };
- if ($scope.paragraph.result && $scope.paragraph.result.msg) {
- $scope.appendTextOutput($scope.paragraph.result.msg);
+ $scope.saveParagraph = function() {
+ if ($scope.dirtyText === undefined || $scope.dirtyText === $scope.originalText) {
+ return;
+ }
+ commitParagraph($scope.paragraph.title, $scope.dirtyText, $scope.paragraph.config,
+ $scope.paragraph.settings.params);
+ $scope.originalText = angular.copy($scope.dirtyText);
+ $scope.dirtyText = undefined;
+ };
+
+ $scope.toggleEnableDisable = function() {
+ $scope.paragraph.config.enabled = $scope.paragraph.config.enabled ? false : true;
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
+
+ $scope.run = function() {
+ var editorValue = $scope.editor.getValue();
+ if (editorValue) {
+ if (!($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING')) {
+ $scope.runParagraph(editorValue);
}
+ }
+ };
+
+ $scope.moveUp = function() {
+ $scope.$emit('moveParagraphUp', $scope.paragraph.id);
+ };
- angular.element('#p' + $scope.paragraph.id + '_text').bind('mousewheel', function(e) {
- $scope.keepScrollDown = false;
+ $scope.moveDown = function() {
+ $scope.$emit('moveParagraphDown', $scope.paragraph.id);
+ };
+
+ $scope.insertNew = function(position) {
+ $scope.$emit('insertParagraph', $scope.paragraph.id, position || 'below');
+ };
+
+ $scope.removeParagraph = function() {
+ var paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
+ if (paragraphs[paragraphs.length - 1].id.startsWith($scope.paragraph.id)) {
+ BootstrapDialog.alert({
+ closable: true,
+ message: 'The last paragraph can\'t be deleted.'
});
- $scope.flushStreamingOutput = true;
} else {
- $timeout(retryRenderer, 10);
+ BootstrapDialog.confirm({
+ closable: true,
+ title: '',
+ message: 'Do you want to delete this paragraph?',
+ callback: function(result) {
+ if (result) {
+ console.log('Remove paragraph');
+ websocketMsgSrv.removeParagraph($scope.paragraph.id);
+ }
+ }
+ });
}
};
- $timeout(retryRenderer);
- };
- $scope.clearTextOutput = function() {
- var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
- if (textEl.length) {
- textEl.children().remove();
- }
- };
+ $scope.clearParagraphOutput = function() {
+ websocketMsgSrv.clearParagraphOutput($scope.paragraph.id);
+ };
- $scope.appendTextOutput = function(msg) {
- var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
- if (textEl.length) {
- var lines = msg.split('\n');
- for (var i = 0; i < lines.length; i++) {
- textEl.append(angular.element('<div></div>').text(lines[i]));
+ $scope.toggleEditor = function() {
+ if ($scope.paragraph.config.editorHide) {
+ $scope.openEditor();
+ } else {
+ $scope.closeEditor();
}
- }
- if ($scope.keepScrollDown) {
- var doc = angular.element('#p' + $scope.paragraph.id + '_text');
- doc[0].scrollTop = doc[0].scrollHeight;
- }
- };
+ };
- var initializeDefault = function() {
- var config = $scope.paragraph.config;
+ $scope.closeEditor = function() {
+ console.log('close the note');
- if (!config.colWidth) {
- config.colWidth = 12;
- }
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.editorHide = true;
- if (!config.graph) {
- config.graph = {};
- }
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
- if (!config.graph.mode) {
- config.graph.mode = 'table';
- }
+ $scope.openEditor = function() {
+ console.log('open the note');
- if (!config.graph.height) {
- config.graph.height = 300;
- }
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.editorHide = false;
- if (!config.graph.optionOpen) {
- config.graph.optionOpen = false;
- }
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
- if (!config.graph.keys) {
- config.graph.keys = [];
- }
+ $scope.closeTable = function() {
+ console.log('close the output');
- if (!config.graph.values) {
- config.graph.values = [];
- }
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.tableHide = true;
- if (!config.graph.groups) {
- config.graph.groups = [];
- }
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
- if (!config.graph.scatter) {
- config.graph.scatter = {};
- }
+ $scope.openTable = function() {
+ console.log('open the output');
- if (!config.graph.map) {
- config.graph.map = {};
- }
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.tableHide = false;
- if (!config.graph.map.baseMapType) {
- config.graph.map.baseMapType = $scope.baseMapOption[0];
- }
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
- if (!config.graph.map.isOnline) {
- config.graph.map.isOnline = true;
- }
+ $scope.showTitle = function() {
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.title = true;
- if (!config.graph.map.pinCols) {
- config.graph.map.pinCols = [];
- }
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
- if (config.enabled === undefined) {
- config.enabled = true;
- }
- };
+ $scope.hideTitle = function() {
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.title = false;
- $scope.getIframeDimensions = function() {
- if ($scope.asIframe) {
- var paragraphid = '#' + $routeParams.paragraphId + '_container';
- var height = angular.element(paragraphid).height();
- return height;
- }
- return 0;
- };
-
- $scope.$watch($scope.getIframeDimensions, function(newValue, oldValue) {
- if ($scope.asIframe && newValue) {
- var message = {};
- message.height = newValue;
- message.url = $location.$$absUrl;
- $window.parent.postMessage(angular.toJson(message), '*');
- }
- });
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
- var isEmpty = function(object) {
- return !object;
- };
+ $scope.setTitle = function() {
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
+
+ $scope.showLineNumbers = function() {
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.lineNumbers = true;
+ $scope.editor.renderer.setShowGutter(true);
+
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
+
+ $scope.hideLineNumbers = function() {
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.lineNumbers = false;
+ $scope.editor.renderer.setShowGutter(false);
+
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
+
+ $scope.columnWidthClass = function(n) {
+ if ($scope.asIframe) {
+ return 'col-md-12';
+ } else {
+ return 'col-md-' + n;
+ }
+ };
+
+ $scope.changeColWidth = function(width) {
+ angular.element('.navbar-right.open').removeClass('open');
+ if (!width || width !== $scope.paragraph.config.colWidth) {
+ if (width) {
+ $scope.paragraph.config.colWidth = width;
+ }
+ var newParams = angular.copy($scope.paragraph.settings.params);
+ var newConfig = angular.copy($scope.paragraph.config);
+
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ }
+ };
+
+ $scope.toggleGraphOption = function() {
+ var newConfig = angular.copy($scope.paragraph.config);
+ if (newConfig.graph.optionOpen) {
+ newConfig.graph.optionOpen = false;
+ } else {
+ newConfig.graph.optionOpen = true;
+ }
+ var newParams = angular.copy($scope.paragraph.settings.params);
+
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
+
+ $scope.toggleOutput = function() {
+ var newConfig = angular.copy($scope.paragraph.config);
+ newConfig.tableHide = !newConfig.tableHide;
+ var newParams = angular.copy($scope.paragraph.settings.params);
+
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
+
+ $scope.toggleLineWithFocus = function() {
+ var mode = $scope.getGraphMode();
+
+ if (mode === 'lineWithFocusChart') {
+ $scope.setGraphMode('lineChart', true);
+ return true;
+ }
+
+ if (mode === 'lineChart') {
+ $scope.setGraphMode('lineWithFocusChart', true);
+ return true;
+ }
- $scope.isRunning = function() {
- if ($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING') {
- return true;
- } else {
return false;
- }
- };
-
- $scope.cancelParagraph = function() {
- console.log('Cancel %o', $scope.paragraph.id);
- websocketMsgSrv.cancelParagraphRun($scope.paragraph.id);
- };
-
- $scope.runParagraph = function(data) {
- websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title,
- data, $scope.paragraph.config, $scope.paragraph.settings.params);
- $scope.originalText = angular.copy(data);
- $scope.dirtyText = undefined;
- };
-
- $scope.saveParagraph = function() {
- if ($scope.dirtyText === undefined || $scope.dirtyText === $scope.originalText) {
- return;
- }
- commitParagraph($scope.paragraph.title, $scope.dirtyText, $scope.paragraph.config,
- $scope.paragraph.settings.params);
- $scope.originalText = angular.copy($scope.dirtyText);
- $scope.dirtyText = undefined;
- };
-
- $scope.toggleEnableDisable = function() {
- $scope.paragraph.config.enabled = $scope.paragraph.config.enabled ? false : true;
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
-
- $scope.run = function() {
- var editorValue = $scope.editor.getValue();
- if (editorValue) {
- if (!($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING')) {
- $scope.runParagraph(editorValue);
+ };
+
+ $scope.loadForm = function(formulaire, params) {
+ var value = formulaire.defaultValue;
+ if (params[formulaire.name]) {
+ value = params[formulaire.name];
}
- }
- };
-
- $scope.moveUp = function() {
- $scope.$emit('moveParagraphUp', $scope.paragraph.id);
- };
-
- $scope.moveDown = function() {
- $scope.$emit('moveParagraphDown', $scope.paragraph.id);
- };
-
- $scope.insertNew = function(position) {
- $scope.$emit('insertParagraph', $scope.paragraph.id, position || 'below');
- };
-
- $scope.removeParagraph = function() {
- var paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
- if (paragraphs[paragraphs.length - 1].id.startsWith($scope.paragraph.id)) {
- BootstrapDialog.alert({
- closable: true,
- message: 'The last paragraph can\'t be deleted.'
- });
- } else {
- BootstrapDialog.confirm({
- closable: true,
- title: '',
- message: 'Do you want to delete this paragraph?',
- callback: function(result) {
- if (result) {
- console.log('Remove paragraph');
- websocketMsgSrv.removeParagraph($scope.paragraph.id);
- }
+
+ $scope.paragraph.settings.params[formulaire.name] = value;
+ };
+
+ $scope.toggleCheckbox = function(formulaire, option) {
+ var idx = $scope.paragraph.settings.params[formulaire.name].indexOf(option.value);
+ if (idx > -1) {
+ $scope.paragraph.settings.params[formulaire.name].splice(idx, 1);
+ } else {
+ $scope.paragraph.settings.params[formulaire.name].push(option.value);
+ }
+ };
+
+ $scope.aceChanged = function() {
+ $scope.dirtyText = $scope.editor.getSession().getValue();
+ $scope.startSaveTimer();
+ setParagraphMode($scope.editor.getSession(), $scope.dirtyText, $scope.editor.getCursorPosition());
+ };
+
+ $scope.aceLoaded = function(_editor) {
+ var langTools = ace.require('ace/ext/language_tools');
+ var Range = ace.require('ace/range').Range;
+
+ _editor.$blockScrolling = Infinity;
+ $scope.editor = _editor;
+ $scope.editor.on('input', $scope.aceChanged);
+ if (_editor.container.id !== '{{paragraph.id}}_editor') {
+ $scope.editor.renderer.setShowGutter($scope.paragraph.config.lineNumbers);
+ $scope.editor.setShowFoldWidgets(false);
+ $scope.editor.setHighlightActiveLine(false);
+ $scope.editor.setHighlightGutterLine(false);
+ $scope.editor.getSession().setUseWrapMode(true);
+ $scope.editor.setTheme('ace/theme/chrome');
+ $scope.editor.setReadOnly($scope.isRunning());
+ if ($scope.paragraphFocused) {
+ $scope.editor.focus();
+ $scope.goToEnd();
}
- });
- }
- };
- $scope.clearParagraphOutput = function() {
- websocketMsgSrv.clearParagraphOutput($scope.paragraph.id);
- };
+ autoAdjustEditorHeight(_editor.container.id);
+ angular.element(window).resize(function() {
+ autoAdjustEditorHeight(_editor.container.id);
+ });
- $scope.toggleEditor = function() {
- if ($scope.paragraph.config.editorHide) {
- $scope.openEditor();
- } else {
- $scope.closeEditor();
- }
- };
+ if (navigator.appVersion.indexOf('Mac') !== -1) {
+ $scope.editor.setKeyboardHandler('ace/keyboard/emacs');
+ $rootScope.isMac = true;
+ } else if (navigator.appVersion.indexOf('Win') !== -1 ||
+ navigator.appVersion.indexOf('X11') !== -1 ||
+ navigator.appVersion.indexOf('Linux') !== -1) {
+ $rootScope.isMac = false;
+ // not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows.
+ }
- $scope.closeEditor = function() {
- console.log('close the note');
+ var remoteCompleter = {
+ getCompletions: function(editor, session, pos, prefix, callback) {
+ if (!$scope.editor.isFocused()) {
+ return;
+ }
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.editorHide = true;
+ pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length;
+ var buf = session.getValue();
+
+ websocketMsgSrv.completion($scope.paragraph.id, buf, pos);
+
+ $scope.$on('completionList', function(event, data) {
+ if (data.completions) {
+ var completions = [];
+ for (var c in data.completions) {
+ var v = data.completions[c];
+ completions.push({
+ name: v.name,
+ value: v.value,
+ score: 300
+ });
+ }
+ callback(null, completions);
+ }
+ });
+ }
+ };
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter,
+ langTools.textCompleter]);
- $scope.openEditor = function() {
- console.log('open the note');
+ $scope.editor.setOptions({
+ enableBasicAutocompletion: true,
+ enableSnippets: false,
+ enableLiveAutocompletion: false
+ });
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.editorHide = false;
+ $scope.handleFocus = function(value, isDigestPass) {
+ $scope.paragraphFocused = value;
+ if (isDigestPass === false || isDigestPass === undefined) {
+ // Protect against error in case digest is already running
+ $timeout(function() {
+ // Apply changes since they come from 3rd party library
+ $scope.$digest();
+ });
+ }
+ };
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ $scope.editor.on('focus', function() {
+ $scope.handleFocus(true);
+ });
- $scope.closeTable = function() {
- console.log('close the output');
+ $scope.editor.on('blur', function() {
+ $scope.handleFocus(false);
+ });
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.tableHide = true;
+ $scope.editor.getSession().on('change', function(e, editSession) {
+ autoAdjustEditorHeight(_editor.container.id);
+ });
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
- $scope.openTable = function() {
- console.log('open the output');
+ // autocomplete on '.'
+ /*
+ $scope.editor.commands.on("afterExec", function(e, t) {
+ if (e.command.name == "insertstring" && e.args == "." ) {
+ var all = e.editor.completers;
+ //e.editor.completers = [remoteCompleter];
+ e.editor.execCommand("startAutocomplete");
+ //e.editor.completers = all;
+ }
+ });
+ */
+
+ // remove binding
+ $scope.editor.commands.bindKey('ctrl-alt-n.', null);
+ $scope.editor.commands.removeCommand('showSettingsMenu');
+
+ // autocomplete on 'ctrl+.'
+ $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete');
+ $scope.editor.commands.bindKey('ctrl-space', null);
+
+ var keyBindingEditorFocusAction = function(scrollValue) {
+ var numRows = $scope.editor.getSession().getLength();
+ var currentRow = $scope.editor.getCursorPosition().row;
+ if (currentRow === 0 && scrollValue <= 0) {
+ // move focus to previous paragraph
+ $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id);
+ } else if (currentRow === numRows - 1 && scrollValue >= 0) {
+ $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id);
+ } else {
+ $scope.scrollToCursor($scope.paragraph.id, scrollValue);
+ }
+ };
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.tableHide = false;
+ // handle cursor moves
+ $scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey;
+ $scope.editor.keyBinding.onCommandKey = function(e, hashId, keyCode) {
+ if ($scope.editor.completer && $scope.editor.completer.activated) { // if autocompleter is active
+ } else {
+ // fix ace editor focus issue in chrome (textarea element goes to top: -1000px after focused by cursor move)
+ if (parseInt(angular.element('#' + $scope.paragraph.id + '_editor > textarea')
+ .css('top').replace('px', '')) < 0) {
+ var position = $scope.editor.getCursorPosition();
+ var cursorPos = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
+ angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top', cursorPos.top);
+ }
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ var ROW_UP = -1;
+ var ROW_DOWN = 1;
- $scope.showTitle = function() {
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.title = true;
+ switch (keyCode) {
+ case 38:
+ keyBindingEditorFocusAction(ROW_UP);
+ break;
+ case 80:
+ if (e.ctrlKey && !e.altKey) {
+ keyBindingEditorFocusAction(ROW_UP);
+ }
+ break;
+ case 40:
+ keyBindingEditorFocusAction(ROW_DOWN);
+ break;
+ case 78:
+ if (e.ctrlKey && !e.altKey) {
+ keyBindingEditorFocusAction(ROW_DOWN);
+ }
+ break;
+ }
+ }
+ this.origOnCommandKey(e, hashId, keyCode);
+ };
+ }
+ };
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ var getAndSetEditorSetting = function(session, interpreterName) {
+ var deferred = $q.defer();
+ websocketMsgSrv.getEditorSetting($scope.paragraph.id, interpreterName);
+ $timeout(
+ $scope.$on('editorSetting', function(event, data) {
+ if ($scope.paragraph.id === data.paragraphId) {
+ deferred.resolve(data);
+ }
+ }
+ ), 1000);
+ deferred.promise.then(function(editorSetting) {
+ if (!_.isEmpty(editorSetting.editor)) {
+ var mode = 'ace/mode/' + editorSetting.editor.language;
+ $scope.paragraph.config.editorMode = mode;
+ session.setMode(mode);
+ }
+ });
+ };
+
+ var setParagraphMode = function(session, paragraphText, pos) {
+ // Evaluate the mode only if the the position is undefined
+ // or the first 30 characters of the paragraph have been modified
+ // or cursor position is at beginning of second line.(in case user hit enter after typing %magic)
+ if ((typeof pos === 'undefined') || (pos.row === 0 && pos.column < 30) || (pos.row === 1 && pos.column === 0)) {
+ // If paragraph loading, use config value if exists
+ if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode) {
+ session.setMode($scope.paragraph.config.editorMode);
+ } else {
+ var magic;
+ // set editor mode to default interpreter syntax if paragraph text doesn't start with '%'
+ // TODO(mina): dig into the cause what makes interpreterBindings has no element
+ if (!paragraphText.startsWith('%') && ((typeof pos !== 'undefined') && pos.row === 0 && pos.column === 1) ||
+ (typeof pos === 'undefined') && $scope.$parent.interpreterBindings.length !== 0) {
+ magic = $scope.$parent.interpreterBindings[0].name;
+ getAndSetEditorSetting(session, magic);
+ } else {
+ var replNameRegexp = /%(.+?)\s/g;
+ var match = replNameRegexp.exec(paragraphText);
+ if (match && $scope.magic !== match[1]) {
+ magic = match[1].trim();
+ $scope.magic = magic;
+ getAndSetEditorSetting(session, magic);
+ }
+ }
+ }
+ }
+ };
- $scope.hideTitle = function() {
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.title = false;
+ var autoAdjustEditorHeight = function(id) {
+ var editor = $scope.editor;
+ var height = editor.getSession().getScreenLength() * editor.renderer.lineHeight +
+ editor.renderer.scrollBar.getWidth();
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ angular.element('#' + id).height(height.toString() + 'px');
+ editor.resize();
+ };
- $scope.setTitle = function() {
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ $rootScope.$on('scrollToCursor', function(event) {
+ // scroll on 'scrollToCursor' event only when cursor is in the last paragraph
+ var paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
+ if (paragraphs[paragraphs.length - 1].id.startsWith($scope.paragraph.id)) {
+ $scope.scrollToCursor($scope.paragraph.id, 0);
+ }
+ });
- $scope.showLineNumbers = function() {
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.lineNumbers = true;
- $scope.editor.renderer.setShowGutter(true);
+ /** scrollToCursor if it is necessary
+ * when cursor touches scrollTriggerEdgeMargin from the top (or bottom) of the screen, it autoscroll to place cursor around 1/3 of screen height from the top (or bottom)
+ * paragraphId : paragraph that has active cursor
+ * lastCursorMove : 1(down), 0, -1(up) last cursor move event
+ **/
+ $scope.scrollToCursor = function(paragraphId, lastCursorMove) {
+ if (!$scope.editor.isFocused()) {
+ // only make sense when editor is focused
+ return;
+ }
+ var lineHeight = $scope.editor.renderer.lineHeight;
+ var headerHeight = 103; // menubar, notebook titlebar
+ var scrollTriggerEdgeMargin = 50;
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ var documentHeight = angular.element(document).height();
+ var windowHeight = angular.element(window).height(); // actual viewport height
- $scope.hideLineNumbers = function() {
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.lineNumbers = false;
- $scope.editor.renderer.setShowGutter(false);
+ var scrollPosition = angular.element(document).scrollTop();
+ var editorPosition = angular.element('#' + paragraphId + '_editor').offset();
+ var position = $scope.editor.getCursorPosition();
+ var lastCursorPosition = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ var calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove;
- $scope.columnWidthClass = function(n) {
- if ($scope.asIframe) {
- return 'col-md-12';
- } else {
- return 'col-md-' + n;
- }
- };
+ var scrollTargetPos;
+ if (calculatedCursorPosition < scrollPosition + headerHeight + scrollTriggerEdgeMargin) {
+ scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) / 3);
+ if (scrollTargetPos < 0) {
+ scrollTargetPos = 0;
+ }
+ } else if (calculatedCursorPosition > scrollPosition + scrollTriggerEdgeMargin + windowHeight - headerHeight) {
+ scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) * 2 / 3);
- $scope.changeColWidth = function(width) {
- angular.element('.navbar-right.open').removeClass('open');
- if (!width || width !== $scope.paragraph.config.colWidth) {
- if (width) {
- $scope.paragraph.config.colWidth = width;
+ if (scrollTargetPos > documentHeight) {
+ scrollTargetPos = documentHeight;
+ }
}
- var newParams = angular.copy($scope.paragraph.settings.params);
- var newConfig = angular.copy($scope.paragraph.config);
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- }
- };
-
- $scope.toggleGraphOption = function() {
- var newConfig = angular.copy($scope.paragraph.config);
- if (newConfig.graph.optionOpen) {
- newConfig.graph.optionOpen = false;
- } else {
- newConfig.graph.optionOpen = true;
- }
- var newParams = angular.copy($scope.paragraph.settings.params);
+ // cancel previous scroll animation
+ var bodyEl = angular.element('body');
+ bodyEl.stop();
+ bodyEl.finish();
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ // scroll to scrollTargetPos
+ bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100});
+ };
- $scope.toggleOutput = function() {
- var newConfig = angular.copy($scope.paragraph.config);
- newConfig.tableHide = !newConfig.tableHide;
- var newParams = angular.copy($scope.paragraph.settings.params);
+ $scope.getEditorValue = function() {
+ return $scope.editor.getValue();
+ };
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
+ $scope.getProgress = function() {
+ return ($scope.currentProgress) ? $scope.currentProgress : 0;
+ };
- $scope.toggleLineWithFocus = function() {
- var mode = $scope.getGraphMode();
+ $scope.getExecutionTime = function() {
+ var pdata = $scope.paragraph;
+ var timeMs = Date.parse(pdata.dateFinished) - Date.parse(pdata.dateStarted);
+ if (isNaN(timeMs) || timeMs < 0) {
+ if ($scope.isResultOutdated()) {
+ return 'outdated';
+ }
+ return '';
+ }
+ var user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user;
+ var desc = 'Took ' + moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]') +
+ '. Last updated by ' + user + ' at ' + moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A') + '.';
+ if ($scope.isResultOutdated()) {
+ desc += ' (outdated)';
+ }
+ return desc;
+ };
- if (mode === 'lineWithFocusChart') {
- $scope.setGraphMode('lineChart', true);
- return true;
- }
+ $scope.getElapsedTime = function() {
+ return 'Started ' + moment($scope.paragraph.dateStarted).fromNow() + '.';
+ };
- if (mode === 'lineChart') {
- $scope.setGraphMode('lineWithFocusChart', true);
- return true;
- }
+ $scope.isResultOutdated = function() {
+ var pdata = $scope.paragraph;
+ if (pdata.dateUpdated !== undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)) {
+ return true;
+ }
+ return false;
+ };
- return false;
- };
+ $scope.goToEnd = function() {
+ $scope.editor.navigateFileEnd();
+ };
- $scope.loadForm = function(formulaire, params) {
- var value = formulaire.defaultValue;
- if (params[formulaire.name]) {
- value = params[formulaire.name];
- }
+ $scope.getResultType = function(paragraph) {
+ var pdata = (paragraph) ? paragraph : $scope.paragraph;
+ if (pdata.result && pdata.result.type) {
+ return pdata.result.type;
+ } else {
+ return 'TEXT';
+ }
+ };
- $scope.paragraph.settings.params[formulaire.name] = value;
- };
+ $scope.getBase64ImageSrc = function(base64Data) {
+ return 'data:image/png;base64,' + base64Data;
+ };
- $scope.toggleCheckbox = function(formulaire, option) {
- var idx = $scope.paragraph.settings.params[formulaire.name].indexOf(option.value);
- if (idx > -1) {
- $scope.paragraph.settings.params[formulaire.name].splice(idx, 1);
- } else {
- $scope.paragraph.settings.params[formulaire.name].push(option.value);
- }
- };
-
- $scope.aceChanged = function() {
- $scope.dirtyText = $scope.editor.getSession().getValue();
- $scope.startSaveTimer();
- setParagraphMode($scope.editor.getSession(), $scope.dirtyText, $scope.editor.getCursorPosition());
- };
-
- $scope.aceLoaded = function(_editor) {
- var langTools = ace.require('ace/ext/language_tools');
- var Range = ace.require('ace/range').Range;
-
- _editor.$blockScrolling = Infinity;
- $scope.editor = _editor;
- $scope.editor.on('input', $scope.aceChanged);
- if (_editor.container.id !== '{{paragraph.id}}_editor') {
- $scope.editor.renderer.setShowGutter($scope.paragraph.config.lineNumbers);
- $scope.editor.setShowFoldWidgets(false);
- $scope.editor.setHighlightActiveLine(false);
- $scope.editor.setHighlightGutterLine(false);
- $scope.editor.getSession().setUseWrapMode(true);
- $scope.editor.setTheme('ace/theme/chrome');
- $scope.editor.setReadOnly($scope.isRunning());
- if ($scope.paragraphFocused) {
- $scope.editor.focus();
- $scope.goToEnd();
+ $scope.getGraphMode = function(paragraph) {
+ var pdata = (paragraph) ? paragraph : $scope.paragraph;
+ if (pdata.config.graph && pdata.config.graph.mode) {
+ return pdata.config.graph.mode;
+ } else {
+ return 'table';
}
+ };
- autoAdjustEditorHeight(_editor.container.id);
- angular.element(window).resize(function() {
- autoAdjustEditorHeight(_editor.container.id);
- });
+ $scope.parseTableCell = function(cell) {
+ if (!isNaN(cell)) {
+ if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) {
+ return cell;
+ } else {
+ return Number(cell);
+ }
+ }
+ var d = moment(cell);
+ if (d.isValid()) {
+ return d;
+ }
+ return cell;
+ };
- if (navigator.appVersion.indexOf('Mac') !== -1) {
- $scope.editor.setKeyboardHandler('ace/keyboard/emacs');
- $rootScope.isMac = true;
- } else if (navigator.appVersion.indexOf('Win') !== -1 ||
- navigator.appVersion.indexOf('X11') !== -1 ||
- navigator.appVersion.indexOf('Linux') !== -1) {
- $rootScope.isMac = false;
- // not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows.
+ $scope.loadTableData = function(result) {
+ if (!result) {
+ return;
}
+ if (result.type === 'TABLE') {
+ var columnNames = [];
+ var rows = [];
+ var array = [];
+ var textRows = result.msg.split('\n');
+ result.comment = '';
+ var comment = false;
+
+ for (var i = 0; i < textRows.length; i++) {
+ var textRow = textRows[i];
+ if (comment) {
+ result.comment += textRow;
+ continue;
+ }
- var remoteCompleter = {
- getCompletions: function(editor, session, pos, prefix, callback) {
- if (!$scope.editor.isFocused()) {
- return;
+ if (textRow === '') {
+ if (rows.length > 0) {
+ comment = true;
+ }
+ continue;
+ }
+ var textCols = textRow.split('\t');
+ var cols = [];
+ var cols2 = [];
+ for (var j = 0; j < textCols.length; j++) {
+ var col = textCols[j];
+ if (i === 0) {
+ columnNames.push({name: col, index: j, aggr: 'sum'});
+ } else {
+ var parsedCol = $scope.parseTableCell(col);
+ cols.push(parsedCol);
+ cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, value: parsedCol});
+ }
+ }
+ if (i !== 0) {
+ rows.push(cols);
+ array.push(cols2);
}
+ }
+ result.msgTable = array;
+ result.columnNames = columnNames;
+ result.rows = rows;
+ }
+ };
- pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length;
- var buf = session.getValue();
+ $scope.setGraphMode = function(type, emit, refresh) {
+ if (emit) {
+ setNewMode(type);
+ } else {
+ clearUnknownColsFromGraphOption();
+ // set graph height
+ var height = $scope.paragraph.config.graph.height;
+ angular.element('#p' + $scope.paragraph.id + '_graph').height(height);
+
+ if (!type || type === 'table') {
+ setTable($scope.paragraph.result, refresh);
+ } else if (type === 'map') {
+ setMap($scope.paragraph.result, refresh);
+ } else {
+ setD3Chart(type, $scope.paragraph.result, refresh);
+ }
+ }
+ };
+
+ var setNewMode = function(newMode) {
+ var newConfig = angular.copy($scope.paragraph.config);
+ var newParams = angular.copy($scope.paragraph.settings.params);
- websocketMsgSrv.completion($scope.paragraph.id, buf, pos);
+ // graph options
+ newConfig.graph.mode = newMode;
+
+ // see switchApp()
+ _.set(newConfig, 'helium.activeApp', undefined);
+
+ commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+ };
- $scope.$on('completionList', function(event, data) {
- if (data.completions) {
- var completions = [];
- for (var c in data.completions) {
- var v = data.completions[c];
- completions.push({
- name: v.name,
- value: v.value,
- score: 300
- });
+ var commitParagraph = function(title, text, config, params) {
+ websocketMsgSrv.commitParagraph($scope.paragraph.id, title, text, config, params);
+ };
+
+ var setTable = function(data, refresh) {
+ var renderTable = function() {
+ var height = $scope.paragraph.config.graph.height;
+ var container = angular.element('#p' + $scope.paragraph.id + '_table').css('height', height).get(0);
+ var resultRows = data.rows;
+ var columnNames = _.pluck(data.columnNames, 'name');
+
+ if ($scope.hot) {
+ $scope.hot.destroy();
+ }
+
+ $scope.hot = new Handsontable(container, {
+ colHeaders: columnNames,
+ data: resultRows,
+ rowHeaders: false,
+ stretchH: 'all',
+ sortIndicator: true,
+ columnSorting: true,
+ contextMenu: false,
+ manualColumnResize: true,
+ manualRowResize: true,
+ readOnly: true,
+ readOnlyCellClassName: '', // don't apply any special class so we can retain current styling
+ fillHandle: false,
+ fragmentSelection: true,
+ disableVisualSelection: true,
+ cells: function(row, col, prop) {
+ var cellProperties = {};
+ cellProperties.renderer = function(instance, td, row, col, prop, value, cellProperties) {
+ if (value instanceof moment) {
+ td.innerHTML = value._i;
+ } else if (!isNaN(value)) {
+ cellProperties.format = '0,0.[00000]';
+ td.style.textAlign = 'left';
+ Handsontable.renderers.NumericRenderer.apply(this, arguments);
+ } else if (value.length > '%html'.length && '%html ' === value.substring(0, '%html '.length)) {
+ td.innerHTML = value.substring('%html'.length);
+ } else {
+ Handsontable.renderers.TextRenderer.apply(this, arguments);
}
- callback(null, completions);
- }
- });
+ };
+ return cellProperties;
+ }
+ });
+ };
+
+ var retryRenderer = function() {
+ if (angular.element('#p' + $scope.paragraph.id + '_table').length) {
+ try {
+ renderTable();
+ } catch (err) {
+ console.log('Chart drawing error %o', err);
+ }
+ } else {
+ $timeout(retryRenderer,10);
}
};
+ $timeout(retryRenderer);
- langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter,
- langTools.textCompleter]);
+ };
- $scope.editor.setOptions({
- enableBasicAutocompletion: true,
- enableSnippets: false,
- enableLiveAutocompletion: false
- });
+ var groupedThousandsWith3DigitsFormatter = function(x) {
+ return d3.format(',')(d3.round(x, 3));
+ };
- $scope.handleFocus = function(value, isDigestPass) {
- $scope.paragraphFocused = value;
- if (isDigestPass === false || isDigestPass === undefined) {
- // Protect against error in case digest is already running
- $timeout(function() {
- // Apply changes since they come from 3rd party library
- $scope.$digest();
- });
- }
- };
+ var customAbbrevFormatter = function(x) {
+ var s = d3.format('.3s')(x);
+ switch (s[s.length - 1]) {
+ case 'G': return s.slice(0, -1) + 'B';
+ }
+ return s;
+ };
- $scope.editor.on('focus', function() {
- $scope.handleFocus(true);
- });
+ var xAxisTickFormat = function(d, xLabels) {
+ if (xLabels[d] && (isNaN(parseFloat(xLabels[d])) || !isFinite(xLabels[d]))) { // to handle string type xlabel
+ return xLabels[d];
+ } else {
+ return d;
+ }
+ };
- $scope.editor.on('blur', function() {
- $scope.handleFocus(false);
- });
+ var yAxisTickFormat = function(d) {
+ if (Math.abs(d) >= Math.pow(10,6)) {
+ return customAbbrevFormatter(d);
+ }
+ return groupedThousandsWith3DigitsFormatter(d);
+ };
- $scope.editor.getSession().on('change', function(e, editSession) {
- autoAdjustEditorHeight(_editor.container.id);
- });
+ var setD3Chart = function(type, data, refresh) {
+ if (!$scope.chart[type]) {
+ var chart = nv.models[type]();
+ $scope.chart[type] = chart;
+ }
- setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
+ var d3g = [];
+ var xLabels;
+ var yLabels;
- // autocomplete on '.'
- /*
- $scope.editor.commands.on("afterExec", function(e, t) {
- if (e.command.name == "insertstring" && e.args == "." ) {
- var all = e.editor.completers;
- //e.editor.completers = [remoteCompleter];
- e.editor.execCommand("startAutocomplete");
- //e.editor.completers = all;
- }
- });
- */
+ if (type === 'scatterChart') {
+ var scatterData = setScatterChart(data, refresh);
- // remove binding
- $scope.editor.commands.bindKey('ctrl-alt-n.', null);
- $scope.editor.commands.removeCommand('showSettingsMenu');
+ xLabels = scatterData.xLabels;
+ yLabels = scatterData.yLabels;
+ d3g = scatterData.d3g;
- // autocomplete on 'ctrl+.'
- $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete');
- $scope.editor.commands.bindKey('ctrl-space', null);
+ $scope.chart[type].xAxis.tickFormat(function(d) {return xAxisTickFormat(d, xLabels);});
+ $scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d, yLabels);});
+
+ // configure how the tooltip looks.
+ $scope.chart[type].tooltipContent(function(key, x, y, graph, data) {
+ var tooltipContent = '<h3>' + key + '</h3>';
+ if ($scope.paragraph.config.graph.scatter.size &&
+ $scope.isValidSizeOption($scope.paragraph.config.graph.scatter, $scope.paragraph.result.rows)) {
+ tooltipContent += '<p>' + data.point.size + '</p>';
+ }
- var keyBindingEditorFocusAction = function(scrollValue) {
- var numRows = $scope.editor.getSession().getLength();
- var currentRow = $scope.editor.getCursorPosition().row;
- if (currentRow === 0 && scrollValue <= 0) {
- // move focus to previous paragraph
- $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id);
- } else if (currentRow === numRows - 1 && scrollValue >= 0) {
- $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id);
- } else {
- $scope.scrollToCursor($scope.paragraph.id, scrollValue);
+ return tooltipContent;
+ });
+
+ $scope.chart[type].showDistX(true)
+ .showDistY(true);
+ //handle the problem of tooltip not showing when muliple points have same value.
+ } else {
+ var p = pivot(data);
+ if (type === 'pieChart') {
+ var d = pivotDataToD3ChartFormat(p, true).d3g;
+
+ $scope.chart[type].x(function(d) { return d.label;})
+ .y(function(d) { return d.value;});
+
+ if (d.length > 0) {
+ for (var i = 0; i < d[0].values.length ; i++) {
+ var e = d[0].values[i];
+ d3g.push({
+ label: e.x,
+ value: e.y
+ });
+ }
+ }
+ } else if (type === 'multiBarChart') {
+ d3g = pivotDataToD3ChartFormat(p, true, false, type).d3g;
+ $scope.chart[type].yAxis.axisLabelDistance(50);
+ $scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d);});
+ } else if (type === 'lineChart' || type === 'stackedAreaChart' || type === 'lineWithFocusChart') {
+ var pivotdata = pivotDataToD3ChartFormat(p, false, true);
+ xLabels = pivotdata.xLabels;
+ d3g = pivotdata.d3g;
+ $scope.chart[type].xAxis.tickFormat(function(d) {return xAxisTickFormat(d, xLabels);});
+ if (type === 'stackedAreaChart') {
+ $scope.chart[type].yAxisTickFormat(function(d) {return yAxisTickFormat(d);});
+ } else {
+ $scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d, xLabels);});
+ }
+ $scope.chart[type].yAxis.axisLabelDistance(50);
+ if ($scope.chart[type].useInteractiveGuideline) { // lineWithFocusChart hasn't got useInteractiveGuideline
+ $scope.chart[type].useInteractiveGuideline(true); // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691)
+ }
+ if ($scope.paragraph.config.graph.forceY) {
+ $scope.chart[type].forceY([0]); // force y-axis minimum to 0 for line chart.
+ } else {
+ $scope.chart[type].forceY([]);
+ }
}
+ }
+
+ var renderChart = function() {
+ if (!refresh) {
+ // TODO force destroy previous chart
+ }
+
+ var height = $scope.paragraph.config.graph.height;
+
+ var animationDuration = 300;
+ var numberOfDataThreshold = 150;
+ // turn off animation when dataset is too large. (for performance issue)
+ // still, since dataset is large, the chart content sequentially appears like animated.
+ try {
+ if (d3g[0].values.length > numberOfDataThreshold) {
+ animationDuration = 0;
+ }
+ } catch (ignoreErr) {
+ }
+
+ d3.select('#p' + $scope.paragraph.id + '_' + type + ' svg')
+ .attr('height', $scope.paragraph.config.graph.height)
+ .datum(d3g)
+ .transition()
+ .duration(animationDuration)
+ .call($scope.chart[type]);
+ d3.select('#p' + $scope.paragraph.id + '_' + type + ' svg').style.height = height + 'px';
+ nv.utils.windowResize($scope.chart[type].update);
};
- // handle cursor moves
- $scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey;
- $scope.editor.keyBinding.onCommandKey = function(e, hashId, keyCode) {
- if ($scope.editor.completer && $scope.editor.completer.activated) { // if autocompleter is active
- } else {
- // fix ace editor focus issue in chrome (textarea element goes to top: -1000px after focused by cursor move)
- if (parseInt(angular.element('#' + $scope.paragraph.id + '_editor > textarea')
- .css('top').replace('px', '')) < 0) {
- var position = $scope.editor.getCursorPosition();
- var cursorPos = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
- angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top', cursorPos.top);
+ var retryRenderer = function() {
+ if (angular.element('#p' + $scope.paragraph.id + '_' + type + ' svg').length !== 0) {
+ try {
+ renderChart();
+ } catch (err) {
+ console.log('Chart drawing error %o', err);
}
+ } else {
+ $timeout(retryRenderer,10);
+ }
+ };
+ $timeout(retryRenderer);
+ };
- var ROW_UP = -1;
- var ROW_DOWN = 1;
+ var setMap = function(data, refresh) {
+ var createPinMapLayer = function(pins, cb) {
+ esriLoader.require(['esri/layers/FeatureLayer'], function(FeatureLayer) {
+ var pinLayer = new FeatureLayer({
+ id: 'pins',
+ spatialReference: $scope.map.spatialReference,
+ geometryType: 'point',
+ source: pins,
+ fields: [],
+ objectIdField: '_ObjectID',
+ renderer: $scope.map.pinRenderer,
+ popupTemplate: {
+ title: '[{_lng}, {_lat}]',
+ content: [{
+ type: 'fields',
+ fieldInfos: []
+ }]
+ }
+ });
- switch (keyCode) {
- case 38:
- keyBindingEditorFocusAction(ROW_UP);
- break;
- case 80:
- if (e.ctrlKey && !e.altKey) {
- keyBindingEditorFocusAction(ROW_UP);
- }
- break;
- case 40:
- keyBindingEditorFocusAction(ROW_DOWN);
- break;
- case 78:
- if (e.ctrlKey && !e.altKey) {
- keyBindingEditorFocusAction(ROW_DOWN);
+ // add user-selected pin info fields to popup
+ var pinInfoCols = $scope.paragraph.config.graph.map.pinCols;
+ for (var i = 0; i < pinInfoCols.length; ++i) {
+ pinLayer.popupTemplate.content[0].fieldInfos.push({
+ fieldName: pinInfoCols[i].name,
+ visible: true
+ });
+ }
+ cb(pinLayer);
+ });
+ };
+
+ var getMapPins = function(cb) {
+ esriLoader.require(['esri/geometry/Point'], function(Point, FeatureLayer) {
+ var latCol = $scope.paragraph.config.graph.map.lat;
+ var lngCol = $scope.paragraph.config.graph.map.lng;
+ var pinInfoCols = $scope.paragraph.config.graph.map.pinCols;
+ var pins = [];
+
+ // construct objects for pins
+ if (latCol && lngCol && data.rows) {
+ for (var i = 0; i < data.rows.length; ++i) {
+ var row = data.rows[i];
+ var lng = row[lngCol.index];
+ var lat = row[latCol.index];
+ var pin = {
+ geometry: new Point({
+ longitude: lng,
+ latitude: lat,
+ spatialReference: $scope.map.spatialReference
+ }),
+ attributes: {
+ _ObjectID: i,
+ _lng: lng,
+ _lat: lat
+ }
+ };
+
+ // add pin info from user-selected columns
+ for (var j = 0; j < pinInfoCols.length; ++j) {
+ var col = pinInfoCols[j];
+ pin.attributes[col.name] = row[col.index];
}
- break;
+ pins.push(pin);
+ }
+ }
+ cb(pins);
+ });
+ };
+
+ var updateMapPins = function() {
+ var pinLayer = $scope.map.map.findLayerById('pins');
+ $scope.map.popup.close();
+ if (pinLayer) {
+ $scope.map.map.remove(pinLayer);
+ }
+
+ // add pins to map as layer
+ getMapPins(function(pins) {
+ createPinMapLayer(pins, function(pinLayer) {
+ $scope.map.map.add(pinLayer);
+ if (pinLayer.source.length > 0) {
+ $scope.map.goTo(pinLayer.source);
+ }
+ });
+ });
+ };
+
+ var createMap = function(mapdiv) {
+ // prevent zooming with the scroll wheel
+ var disableZoom = function(e) {
+ var evt = e || window.event;
+ evt.cancelBubble = true;
+ evt.returnValue = false;
+ if (evt.stopPropagation) {
+ evt.stopPropagation();
+ }
+ };
+ var eName = window.WheelEvent ? 'wheel' : // Modern browsers
+ window.MouseWheelEvent ? 'mousewheel' : // WebKit and IE
+ 'DOMMouseScroll'; // Old Firefox
+ mapdiv.addEventListener(eName, disableZoom, true);
+
+ esriLoader.require(['esri/views/MapView',
+ 'esri/Map',
+ 'esri/renderers/SimpleRenderer',
+ 'esri/symbols/SimpleMarkerSymbol'],
+ function(MapView, Map, SimpleRenderer, SimpleMarkerSymbol) {
+ $scope.map = new MapView({
+ container: mapdiv,
+ map: new Map({
+ basemap: $scope.paragraph.config.graph.map.baseMapType.toLowerCase()
+ }),
+ center: [-106.3468, 56.1304], // Canada (lng, lat)
+ zoom: 2,
+ pinRenderer: new SimpleRenderer({
+ symbol: new SimpleMarkerSymbol({
+ 'color': [255, 0, 0, 0.5],
+ 'size': 16.5,
+ 'outline': {
+ 'color': [0, 0, 0, 1],
+ 'width': 1.125,
+ },
+ // map pin SVG path
+ 'path': 'M16,3.5c-4.142,0-7.5,3.358-7.5,7.5c0,4.143,7.5,18.121,7.5,' +
+ '18.121S23.5,15.143,23.5,11C23.5,6.858,20.143,3.5,16,3.5z ' +
+ 'M16,14.584c-1.979,0-3.584-1.604-3.584-3.584S14.021,7.416,' +
+ '16,7.416S19.584,9.021,19.584,11S17.979,14.584,16,14.584z'
+ })
+ })
+ });
+
+ $scope.map.on('click', function() {
+ // ArcGIS JS API 4.0 does not account for scrolling or position
+ // changes by default (this is a bug, to be fixed in the upcoming
+ // version 4.1; see https://geonet.esri.com/thread/177238#comment-609681).
+ // This results in a misaligned popup.
+
+ // Workaround: manually set popup position to match position of selected pin
+ if ($scope.map.popup.selectedFeature) {
+ $scope.map.popup.location = $scope.map.popup.selectedFeature.geometry;
+ }
+ });
+ $scope.map.then(updateMapPins);
+ });
+ };
+
+ var checkMapOnline = function(cb) {
+ // are we able to get a response from the ArcGIS servers?
+ var callback = function(res) {
+ var online = (res.status > 0);
+ $scope.paragraph.config.graph.map.isOnline = online;
+ cb(online);
+ };
+ $http.head('//services.arcgisonline.com/arcgis/', {
+ timeout: 5000,
+ withCredentials: false
+ }).then(callback, callback);
+ };
+
+ var renderMap = function() {
+ var mapdiv = angular.element('#p' + $scope.paragraph.id + '_map')
+ .css('height', $scope.paragraph.config.graph.height)
+ .children('div').get(0);
+
+ // on chart type change, destroy map to force reinitialization.
+ if ($scope.map && !refresh) {
+ $scope.map.map.destroy();
+ $scope.map.pinRenderer = null;
+ $scope.map = null;
+ }
+
+ var requireMapCSS = function() {
+ var url = '//js.arcgis.com/4.0/esri/css/main.css';
+ if (!angular.element('link[href="' + url + '"]').length) {
+ var link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.type = 'text/css';
+ link.href = url;
+ angular.element('head').append(link);
+ }
+ };
+
+ var requireMapJS = function(cb) {
+ if (!esriLoader.isLoaded()) {
+ esriLoader.bootstrap({
+ url: '//js.arcgis.com/4.0'
+ }).then(cb);
+ } else {
+ cb();
+ }
+ };
+
+ checkMapOnline(function(online) {
+ // we need an internet connection to use the map
+ if (online) {
+ // create map if not exists.
+ if (!$scope.map) {
+ requireMapCSS();
+ requireMapJS(function() {
+ createMap(mapdiv);
+ });
+ } else {
+ updateMapPins();
+ }
}
- }
- this.origOnCommandKey(e, hashId, keyCode);
+ });
};
- }
- };
- var getAndSetEditorSetting = function(session, interpreterName) {
- var deferred = $q.defer();
- websocketMsgSrv.getEditorSetting($scope.paragraph.id, interpreterName);
- $timeout(
- $scope.$on('editorSetting', function(event, data) {
- if ($scope.paragraph.id === data.paragraphId) {
- deferred.resolve(data);
- }
- }
- ), 1000);
- deferred.promise.then(function(editorSetting) {
- if (!_.isEmpty(editorSetting.editor)) {
- var mode = 'ace/mode/' + editorSetting.editor.language;
- $scope.paragraph.config.editorMode = mode;
- session.setMode(mode);
- }
- });
- };
-
- var setParagraphMode = function(session, paragraphText, pos) {
- // Evaluate the mode only if the the position is undefined
- // or the first 30 characters of the paragraph have been modified
- // or cursor position is at beginning of second line.(in case user hit enter after typing %magic)
- if ((typeof pos === 'undefined') || (pos.row === 0 && pos.column < 30) || (pos.row === 1 && pos.column === 0)) {
- // If paragraph loading, use config value if exists
- if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode) {
- session.setMode($scope.paragraph.config.editorMode);
- } else {
- var magic;
- // set editor mode to default interpreter syntax if paragraph text doesn't start with '%'
- // TODO(mina): dig into the cause what makes interpreterBindings has no element
- if (!paragraphText.startsWith('%') && ((typeof pos !== 'undefined') && pos.row === 0 && pos.column === 1) ||
- (typeof pos === 'undefined') && $scope.$parent.interpreterBindings.length !== 0) {
- magic = $scope.$parent.interpreterBindings[0].name;
- getAndSetEditorSetting(session, magic);
- } else {
- var replNameRegexp = /%(.+?)\s/g;
- var match = replNameRegexp.exec(paragraphText);
- if (match && $scope.magic !== match[1]) {
- magic = match[1].trim();
- $scope.magic = magic;
- getAndSetEditorSetting(session, magic);
+ var retryRenderer = function() {
+ if (angular.element('#p' + $scope.paragraph.id + '_map div').length) {
+ try {
+ renderMap();
+ } catch (err) {
+ console.log('Map drawing error %o', err);
}
+ } else {
+ $timeout(retryRenderer,10);
}
+ };
+ $timeout(retryRenderer);
+ };
+
+ $scope.setMapBaseMap = function(bm) {
+ $scope.paragraph.config.graph.map.baseMapType = bm;
+ if ($scope.map) {
+ $scope.map.map.basemap = bm.toLowerCase();
}
- }
- };
-
- var autoAdjustEditorHeight = function(id) {
- var editor = $scope.editor;
- var height = editor.getSession().getScreenLength() * editor.renderer.lineHeight +
- editor.renderer.scrollBar.getWidth();
-
- angular.element('#' + id).height(height.toString() + 'px');
- editor.resize();
- };
-
- $rootScope.$on('scrollToCursor', function(event) {
- // scroll on 'scrollToCursor' event only when cursor is in the last paragraph
- var paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
- if (paragraphs[paragraphs.length - 1].id.startsWith($scope.paragraph.id)) {
- $scope.scrollToCursor($scope.paragraph.id, 0);
- }
- });
-
- /** scrollToCursor if it is necessary
- * when cursor touches scrollTriggerEdgeMargin from the top (or bottom) of the screen, it autoscroll to place cursor around 1/3 of screen height from the top (or bottom)
- * paragraphId : paragraph that has active cursor
- * lastCursorMove : 1(down), 0, -1(up) last cursor move event
- **/
- $scope.scrollToCursor = function(paragraphId, lastCursorMove) {
- if (!$scope.editor.isFocused()) {
- // only make sense when editor is focused
- return;
- }
- var lineHeight = $scope.editor.renderer.lineHeight;
- var headerHeight = 103; // menubar, notebook titlebar
- var scrollTriggerEdgeMargin = 50;
+ };
- var documentHeight = angular.element(document).height();
- var windowHeight = angular.element(window).height(); // actual viewport height
+ $scope.isGraphMode = function(graphName) {
+ var activeAppId = _.get($scope.paragraph.config, 'helium.activeApp');
+ if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode() === graphName && !activeAppId) {
+ return true;
+ } else {
+ return false;
+ }
+ };
- var scrollPosition = angular.element(document).scrollTop();
- var editorPosition = angular.element('#' + paragraphId + '_editor').offset();
- var position = $scope.editor.getCursorPosition();
- var lastCursorPosition = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
+ $scope.onGraphOptionChange = function() {
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- var calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove;
+ $scope.removeGraphOptionKeys = function(idx) {
+ $scope.paragraph.config.graph.keys.splice(idx, 1);
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- var scrollTargetPos;
- if (calculatedCursorPosition < scrollPosition + headerHeight + scrollTriggerEdgeMargin) {
- scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) / 3);
- if (scrollTargetPos < 0) {
- scrollTargetPos = 0;
- }
- } else if (calculatedCursorPosition > scrollPosition + scrollTriggerEdgeMargin + windowHeight - headerHeight) {
- scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) * 2 / 3);
+ $scope.removeGraphOptionValues = function(idx) {
+ $scope.paragraph.config.graph.values.splice(idx, 1);
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- if (scrollTargetPos > documentHeight) {
- scrollTargetPos = documentHeight;
- }
- }
+ $scope.removeGraphOptionGroups = function(idx) {
+ $scope.paragraph.config.graph.groups.splice(idx, 1);
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- // cancel previous scroll animation
- var bodyEl = angular.element('body');
- bodyEl.stop();
- bodyEl.finish();
+ $scope.setGraphOptionValueAggr = function(idx, aggr) {
+ $scope.paragraph.config.graph.values[idx].aggr = aggr;
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- // scroll to scrollTargetPos
- bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100});
- };
+ $scope.removeScatterOptionXaxis = function(idx) {
+ $scope.paragraph.config.graph.scatter.xAxis = null;
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- $scope.getEditorValue = function() {
- return $scope.editor.getValue();
- };
+ $scope.removeScatterOptionYaxis = function(idx) {
+ $scope.paragraph.config.graph.scatter.yAxis = null;
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- $scope.getProgress = function() {
- return ($scope.currentProgress) ? $scope.currentProgress : 0;
- };
+ $scope.removeScatterOptionGroup = function(idx) {
+ $scope.paragraph.config.graph.scatter.group = null;
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- $scope.getExecutionTime = function() {
- var pdata = $scope.paragraph;
- var timeMs = Date.parse(pdata.dateFinished) - Date.parse(pdata.dateStarted);
- if (isNaN(timeMs) || timeMs < 0) {
- if ($scope.isResultOutdated()) {
- return 'outdated';
- }
- return '';
- }
- var user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user;
- var desc = 'Took ' + moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]') +
- '. Last updated by ' + user + ' at ' + moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A') + '.';
- if ($scope.isResultOutdated()) {
- desc += ' (outdated)';
- }
- return desc;
- };
+ $scope.removeScatterOptionSize = function(idx) {
+ $scope.paragraph.config.graph.scatter.size = null;
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- $scope.getElapsedTime = function() {
- return 'Started ' + moment($scope.paragraph.dateStarted).fromNow() + '.';
- };
+ $scope.removeMapOptionLat = function(idx) {
+ $scope.paragraph.config.graph.map.lat = null;
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- $scope.isResultOutdated = function() {
- var pdata = $scope.paragraph;
- if (pdata.dateUpdated !== undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)) {
- return true;
- }
- return false;
- };
-
- $scope.goToEnd = function() {
- $scope.editor.navigateFileEnd();
- };
-
- $scope.getResultType = function(paragraph) {
- var pdata = (paragraph) ? paragraph : $scope.paragraph;
- if (pdata.result && pdata.result.type) {
- return pdata.result.type;
- } else {
- return 'TEXT';
- }
- };
-
- $scope.getBase64ImageSrc = function(base64Data) {
- return 'data:image/png;base64,' + base64Data;
- };
-
- $scope.getGraphMode = function(paragraph) {
- var pdata = (paragraph) ? paragraph : $scope.paragraph;
- if (pdata.config.graph && pdata.config.graph.mode) {
- return pdata.config.graph.mode;
- } else {
- return 'table';
- }
- };
+ $scope.removeMapOptionLng = function(idx) {
+ $scope.paragraph.config.graph.map.lng = null;
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- $scope.parseTableCell = function(cell) {
- if (!isNaN(cell)) {
- if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) {
- return cell;
- } else {
- return Number(cell);
- }
- }
- var d = moment(cell);
- if (d.isValid()) {
- return d;
- }
- return cell;
- };
+ $scope.removeMapOptionPinInfo = function(idx) {
+ $scope.paragraph.config.graph.map.pinCols.splice(idx, 1);
+ clearUnknownColsFromGraphOption();
+ $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+ };
- $scope.loadTableData = function(result) {
- if (!result) {
- return;
- }
- if (result.type === 'TABLE') {
- var columnNames = [];
- var rows = [];
- var array = [];
- var textRows = result.msg.split('\n');
- result.comment = '';
- var comment = false;
-
- for (var i = 0; i < textRows.length; i++) {
- var textRow = textRows[i];
- if (comment) {
- result.comment += textRow;
- continue;
- }
-
- if (textRow === '') {
- if (rows.length > 0) {
- comment = true;
- }
- continue;
- }
- var textCols = textRow.split('\t');
- var cols = [];
- var cols2 = [];
- for (var j = 0; j < textCols.length; j++) {
- var col = textCols[j];
- if (i === 0) {
- columnNames.push({name: col, index: j, aggr: 'sum'});
- } else {
- var parsedCol = $scope.parseTableCell(col);
- cols.push(parsedCol);
- cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, value: parsedCol});
+ /* Clear unknown columns from graph option */
+ var clearUnknownColsFromGraphOption = function() {
+ var unique = function(list) {
+ for (var i = 0; i < list.length; i++) {
+ for (var j = i + 1; j < list.length; j++) {
+ if (angular.equals(list[i], list[j])) {
+ list.splice(j, 1);
+ }
}
}
- if (i !== 0) {
- rows.push(cols);
- array.push(cols2);
- }
- }
- result.msgTable = array;
- result.columnNames = columnNames;
- result.rows = rows;
- }
- };
+ };
- $scope.setGraphMode = function(type, emit, refresh) {
- if (emit) {
- setNewMode(type);
- } else {
- clearUnknownColsFromGraphOption();
- // set graph height
- var height = $scope.paragraph.config.graph.height;
- angular.element('#p' + $scope.paragraph.id + '_graph').height(height);
-
- if (!type || type === 'table') {
- setTable($scope.paragraph.result, refresh);
- } else if (type === 'map') {
- setMap($scope.paragraph.result, refresh);
- } else {
- setD3Chart(type, $scope.paragraph.result, refresh);
- }
- }
- };
-
- var setNewMode = function(newMode) {
- var newConfig = angular.copy($scope.paragraph.config);
- var newParams = angular.copy($scope.paragraph.settings.params);
-
- // graph options
- newConfig.graph.mode = newMode;
-
- // see switchApp()
- _.set(newConfig, 'helium.activeApp', undefined);
-
- commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
- };
-
- var commitParagraph = function(title, text, config, params) {
- websocketMsgSrv.commitParagraph($scope.paragraph.id, title, text, config, params);
- };
-
- var setTable = function(data, refresh) {
- var renderTable = function() {
- var height = $scope.paragraph.config.graph.height;
- var container = angular.element('#p' + $scope.paragraph.id + '_table').css('height', height).get(0);
- var resultRows = data.rows;
- var columnNames = _.pluck(data.columnNames, 'name');
-
- if ($scope.hot) {
- $scope.hot.destroy();
- }
-
- $scope.hot = new Handsontable(container, {
- colHeaders: columnNames,
- data: resultRows,
- rowHeaders: false,
- stretchH: 'all',
- sortIndicator: true,
- columnSorting: true,
- contextMenu: false,
- manualColumnResize: true,
- manualRowResize: true,
- readOnly: true,
- readOnlyCellClassName: '', // don't apply any special class so we can retain current styling
- fillHandle: false,
- fragmentSelection: true,
- disableVisualSelection: true,
- cells: function(row, col, prop) {
- var cellProperties = {};
- cellProperties.renderer = function(instance, td, row, col, prop, value, cellProperties) {
- if (value instanceof moment) {
- td.innerHTML = value._i;
- } else if (!isNaN(value)) {
- cellProperties.format = '0,0.[00000]';
- td.style.textAlign = 'left';
- Handsontable.renderers.NumericRenderer.apply(this, arguments);
- } else if (value.length > '%html'.length && '%html ' === value.substring(0, '%html '.length
<TRUNCATED>