Merge branch 'master' into ZEPPELIN-960

This commit is contained in:
CloverHearts 2016-08-18 11:14:00 +09:00
commit b9e197c939
13 changed files with 419 additions and 12 deletions

View file

@ -75,6 +75,7 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand
private ByteArrayOutputStream input;
private String scriptPath;
boolean pythonscriptRunning = false;
private static final int MAX_TIMEOUT_SEC = 10;
public PySparkInterpreter(Properties property) {
super(property);
@ -316,7 +317,7 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand
long startTime = System.currentTimeMillis();
while (pythonScriptInitialized == false
&& pythonscriptRunning
&& System.currentTimeMillis() - startTime < 10 * 1000) {
&& System.currentTimeMillis() - startTime < MAX_TIMEOUT_SEC * 1000) {
try {
pythonScriptInitializeNotifier.wait(1000);
} catch (InterruptedException e) {
@ -423,8 +424,15 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand
}
synchronized (statementFinishedNotifier) {
while (statementOutput == null) {
long startTime = System.currentTimeMillis();
while (statementOutput == null
&& pythonScriptInitialized == false
&& pythonscriptRunning) {
try {
if (System.currentTimeMillis() - startTime < MAX_TIMEOUT_SEC * 1000) {
logger.error("pyspark completion didn't have response for {}sec.", MAX_TIMEOUT_SEC);
break;
}
statementFinishedNotifier.wait(1000);
} catch (InterruptedException e) {
// not working

View file

@ -114,6 +114,7 @@ The following components are provided under Apache License.
(Apache 2.0) Utility classes for Jetty (org.mortbay.jetty:jetty-util:6.1.26 - http://javadox.com/org.mortbay.jetty/jetty/6.1.26/overview-tree.html)
(Apache 2.0) Servlet API (org.mortbay.jetty:servlet-api:2.5-20081211 - https://en.wikipedia.org/wiki/Jetty_(web_server))
(Apache 2.0) Google HTTP Client Library for Java (com.google.http-client:google-http-client-jackson2:1.21.0 - https://github.com/google/google-http-java-client/tree/dev/google-http-client-jackson2)
(Apache 2.0) angular-esri-map (https://github.com/Esri/angular-esri-map)
========================================================================
MIT licenses

View file

@ -32,7 +32,8 @@
"bootstrap3-dialog": "bootstrap-dialog#~1.34.7",
"handsontable": "~0.24.2",
"moment-duration-format": "^1.3.0",
"select2": "^4.0.3"
"select2": "^4.0.3",
"angular-esri-map": "~2.0.0"
},
"devDependencies": {
"angular-mocks": "1.5.0"

View file

@ -33,7 +33,8 @@
'xeditable',
'ngToast',
'focus-if',
'ngResource'
'ngResource',
'esri.map'
])
.filter('breakFilter', function() {
return function(text) {

View file

@ -47,6 +47,11 @@ limitations under the License.
ng-class="{'active': isGraphMode('scatterChart')}"
ng-click="setGraphMode('scatterChart', true)"><i class="cf cf-scatter-chart"></i>
</button>
<button type="button" class="btn btn-default btn-sm"
ng-if="paragraph.result.type == 'TABLE'"
ng-class="{'active': isGraphMode('map')}"
ng-click="setGraphMode('map', true)"><i class="fa fa-map-marker"></i>
</button>
<button type="button"
ng-if="paragraph.result.type != 'TABLE'"

View file

@ -51,4 +51,12 @@ limitations under the License.
id="p{{paragraph.id}}_scatterChart">
<svg></svg>
</div>
<div ng-if="getGraphMode()=='map'" id="p{{paragraph.id}}_map"
ng-switch="paragraph.config.graph.map.isOnline">
<div ng-switch-when="true"></div>
<span class="map-offline-text" ng-switch-default>
<span>Maps require internet connectivity.</span>
</span>
</div>
</div>

View file

@ -28,4 +28,19 @@ limitations under the License.
show line chart with focus
</label>
</div>
<div ng-if="isGraphMode('map')">
<label>Basemap</label>
<span class="dropdown">
<button type="button" class="btn btn-default btn-sm dropdown-toggle" style="min-width: 0px;" data-toggle="dropdown">
<span ng-bind="paragraph.config.graph.map.baseMapType"></span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" style="min-width: 70px;">
<li ng-repeat="opt in baseMapOption">
<a ng-click="setMapBaseMap(opt)">{{opt}}</a>
</li>
</ul>
</span>
<br/>
</div>
</div>

View file

@ -32,7 +32,7 @@ limitations under the License.
</ul>
</div>
<div class="row" ng-if="getGraphMode()!='scatterChart'">
<div class="row" ng-if="getGraphMode()!='scatterChart' && getGraphMode()!='map'">
<div class="col-md-4">
<span class="columns lightBold">
Keys
@ -165,4 +165,52 @@ limitations under the License.
</span>
</div>
</div>
<div class="row" ng-if="getGraphMode()=='map'">
<div class="col-md-4">
<span class="columns lightBold">
Latitude
<ul data-drop="true"
ng-model="paragraph.config.graph.map.lat"
jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
class="list-unstyled">
<li ng-if="paragraph.config.graph.map.lat">
<div class="btn btn-primary btn-xs">
{{paragraph.config.graph.map.lat.name}} <span class="fa fa-close" ng-click="removeMapOptionLat($index)"></span>
</div>
</li>
</ul>
</span>
</div>
<div class="col-md-4">
<span class="columns lightBold">
Longitude
<ul data-drop="true"
ng-model="paragraph.config.graph.map.lng"
jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
class="list-unstyled">
<li ng-if="paragraph.config.graph.map.lng">
<div class="btn btn-primary btn-xs">
{{paragraph.config.graph.map.lng.name}} <span class="fa fa-close" ng-click="removeMapOptionLng($index)"></span>
</div>
</li>
</ul>
</span>
</div>
<div class="col-md-4">
<span class="columns lightBold">
Pin contents
<ul data-drop="true"
ng-model="paragraph.config.graph.map.pinCols"
jqyoui-droppable="{multiple:true, onDrop:'onGraphOptionChange()'}"
class="list-unstyled">
<li ng-repeat="col in paragraph.config.graph.map.pinCols">
<div class="btn btn-primary btn-xs">
{{col.name}} <span class="fa fa-close" ng-click="removeMapOptionPinInfo($index)"></span>
</div>
</li>
</ul>
</span>
</div>
</div>
</div>

View file

@ -16,7 +16,7 @@
angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $rootScope, $route, $window,
$routeParams, $location, $timeout, $compile,
$http, websocketMsgSrv, baseUrlSrv, ngToast,
saveAsService) {
saveAsService, esriLoader) {
var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_';
$scope.parentNote = null;
$scope.paragraph = null;
@ -93,6 +93,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$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) {
@ -245,6 +246,22 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
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;
}
@ -907,6 +924,8 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
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);
}
@ -1131,6 +1150,236 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$timeout(retryRenderer);
};
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: []
}]
}
});
// 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];
}
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();
}
}
});
};
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();
}
};
$scope.isGraphMode = function(graphName) {
var activeAppId = _.get($scope.paragraph.config, 'helium.activeApp');
if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode() === graphName && !activeAppId) {
@ -1193,6 +1442,24 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeMapOptionLat = function(idx) {
$scope.paragraph.config.graph.map.lat = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeMapOptionLng = function(idx) {
$scope.paragraph.config.graph.map.lng = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeMapOptionPinInfo = function(idx) {
$scope.paragraph.config.graph.map.pinCols.splice(idx, 1);
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
/* Clear unknown columns from graph option */
var clearUnknownColsFromGraphOption = function() {
var unique = function(list) {
@ -1223,7 +1490,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
}
};
var removeUnknownFromScatterSetting = function(fields) {
var removeUnknownFromFields = function(fields) {
for (var f in fields) {
if (fields[f]) {
var found = false;
@ -1235,7 +1502,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
break;
}
}
if (!found) {
if (!found && (fields[f] instanceof Object) && !(fields[f] instanceof Array)) {
fields[f] = null;
}
}
@ -1250,7 +1517,11 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
unique($scope.paragraph.config.graph.groups);
removeUnknown($scope.paragraph.config.graph.groups);
removeUnknownFromScatterSetting($scope.paragraph.config.graph.scatter);
removeUnknownFromFields($scope.paragraph.config.graph.scatter);
unique($scope.paragraph.config.graph.map.pinCols);
removeUnknown($scope.paragraph.config.graph.map.pinCols);
removeUnknownFromFields($scope.paragraph.config.graph.map);
};
/* select default key and value if there're none selected */
@ -1271,6 +1542,23 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$scope.paragraph.config.graph.scatter.xAxis = $scope.paragraph.result.columnNames[0];
}
}
/* try to find columns for the map logitude and latitude */
var findDefaultMapCol = function(settingName, keyword) {
var col;
if (!$scope.paragraph.config.graph.map[settingName]) {
for (var i = 0; i < $scope.paragraph.result.columnNames.length; ++i) {
col = $scope.paragraph.result.columnNames[i];
if (col.name.toUpperCase().indexOf(keyword) !== -1) {
$scope.paragraph.config.graph.map[settingName] = col;
break;
}
}
}
};
findDefaultMapCol('lat', 'LAT');
findDefaultMapCol('lng', 'LONG');
};
var pivot = function(data) {

View file

@ -327,12 +327,41 @@ table.dataTable.table-condensed .sorting_desc:after {
.tableDisplay div {
}
.tableDisplay img {
.tableDisplay img:not(.esri-bitmap) {
display: block;
max-width: 100%;
height: auto;
}
.esri-display-object > svg {
overflow: visible;
}
.esri-popup > .esri-docked.esri-dock-to-bottom {
padding: 8px;
margin-top: 0px;
}
.esri-popup-main {
max-height: 100%;
}
span.map-offline-text {
display: table;
width: 100%;
height: 100%;
text-align: center;
}
span.map-offline-text > span {
display: table-cell;
vertical-align: middle;
font-size: 18px;
font-weight: 700;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #212121;
}
.tableDisplay .btn-group span {
margin: 10px 0 0 10px;
font-size: 12px;
@ -350,7 +379,8 @@ table.dataTable.table-condensed .sorting_desc:after {
}
.tableDisplay .option .columns {
.tableDisplay .option .columns,
div.esri-view {
height: 100%;
}

View file

@ -146,6 +146,7 @@ limitations under the License.
<script src="bower_components/handsontable/dist/handsontable.js"></script>
<script src="bower_components/moment-duration-format/lib/moment-duration-format.js"></script>
<script src="bower_components/select2/dist/js/select2.js"></script>
<script src="bower_components/angular-esri-map/dist/angular-esri-map.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- build:js({.tmp,src}) scripts/scripts.js -->

View file

@ -66,6 +66,7 @@ module.exports = function(config) {
'bower_components/moment-duration-format/lib/moment-duration-format.js',
'bower_components/select2/dist/js/select2.js',
'bower_components/angular-mocks/angular-mocks.js',
'bower_components/angular-esri-map/dist/angular-esri-map.js'
// endbower
'src/app/app.js',
'src/app/app.controller.js',

View file

@ -39,7 +39,7 @@ describe('Controller: ParagraphCtrl', function() {
'getResultType', 'loadTableData', 'setGraphMode', 'isGraphMode', 'onGraphOptionChange',
'removeGraphOptionKeys', 'removeGraphOptionValues', 'removeGraphOptionGroups', 'setGraphOptionValueAggr',
'removeScatterOptionXaxis', 'removeScatterOptionYaxis', 'removeScatterOptionGroup',
'removeScatterOptionSize'];
'removeScatterOptionSize', 'removeMapOptionLat', 'removeMapOptionLng', 'removeMapOptionPinInfo'];
functions.forEach(function(fn) {
it('check for scope functions to be defined : ' + fn, function() {