Merge pull request #3671 from rashidkpc/experiment/leaflet-filter

Tile map geo_bounding_box filter
This commit is contained in:
Spencer 2015-05-01 12:04:06 -07:00
commit 96ed33043d
14 changed files with 498 additions and 145 deletions

View file

@ -49,7 +49,8 @@
"requirejs-text": "~2.0.10",
"lodash-deep": "spenceralger/lodash-deep#compat",
"marked": "~0.3.2",
"numeral": "~1.5.3"
"numeral": "~1.5.3",
"leaflet-draw": "~0.2.4"
},
"devDependencies": {}
}

View file

@ -1,5 +1,6 @@
define(function (require) {
var decodeGeoHash = require('utils/decode_geo_hash');
var _ = require('lodash');
function readRows(table, agg, index, chart) {
var geoJson = chart.geoJson;
@ -8,6 +9,7 @@ define(function (require) {
props.length = table.rows.length;
props.min = null;
props.max = null;
props.agg = agg;
table.rows.forEach(function (row) {
var geohash = row[index.geo].value;

View file

@ -26,6 +26,7 @@ define(function (require) {
Private(require('./mapExists')),
Private(require('./mapMissing')),
Private(require('./mapQueryString')),
Private(require('./mapGeoBoundingBox')),
Private(require('./mapScript')),
Private(require('./mapDefault')) // ProTip: last one to get applied
];

View file

@ -0,0 +1,21 @@
define(function (require) {
var _ = require('lodash');
return function mapGeoBoundBoxProvider(Promise, courier) {
return function (filter) {
var key, value, topLeft, bottomRight, field;
if (filter.geo_bounding_box) {
return courier
.indexPatterns
.get(filter.meta.index).then(function (indexPattern) {
key = _.keys(filter.geo_bounding_box)[0];
field = indexPattern.fields.byName[key];
topLeft = field.format.convert(filter.geo_bounding_box[field.name].top_left);
bottomRight = field.format.convert(filter.geo_bounding_box[field.name].bottom_right);
value = topLeft + ' to ' + bottomRight;
return { key: key, value: value };
});
}
return Promise.reject(filter);
};
};
});

View file

@ -0,0 +1,19 @@
define(function (require) {
var _ = require('lodash');
return function () {
return function ($state) {
if (!_.isObject($state)) throw new Error ('pushFilters requires a state object');
return function (filter, negate, index) {
// Hierarchical and tabular data set their aggConfigResult parameter
// differently because of how the point is rewritten between the two. So
// we need to check if the point.orig is set, if not use try the point.aggConfigResult
var filters = _.clone($state.filters || []);
var pendingFilter = { meta: { negate: negate, index: index }};
_.extend(pendingFilter, filter);
filters.push(pendingFilter);
$state.filters = filters;
};
};
};
});

View file

@ -13,13 +13,17 @@ define(function (require) {
*/
function Dispatch(handler) {
var stockEvents = ['brush', 'click', 'hover', 'mouseup', 'mousedown', 'mouseover', 'mouseout'];
var customEvents = _.deepGet(handler, 'vis.eventTypes.enabled');
var eventTypes = customEvents ? stockEvents.concat(customEvents) : stockEvents;
if (!(this instanceof Dispatch)) {
return new Dispatch(handler);
}
this.handler = handler;
this.dispatch = d3.dispatch('brush', 'click', 'hover', 'mouseup',
'mousedown', 'mouseover', 'mouseout');
this.dispatch = d3.dispatch.apply(this, eventTypes);
}
/**

View file

@ -108,6 +108,10 @@
padding: 0 !important;
}
.leaflet-draw-tooltip {
display: none;
}
/* filter to desaturate mapquest tiles */
img.leaflet-tile {
@ -116,4 +120,4 @@ img.leaflet-tile {
img.leaflet-tile.filters-off {
.filter(none);
}
}

View file

@ -3,7 +3,9 @@ define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
var L = require('leaflet');
require('leaflet-draw');
var Dispatch = Private(require('components/vislib/lib/dispatch'));
var Chart = Private(require('components/vislib/visualizations/_chart'));
var errors = require('errors');
@ -32,6 +34,8 @@ define(function (require) {
// track the map objects
this.maps = [];
this.events = new Dispatch(handler);
// add allmin and allmax to geoJson
chartData.geoJson.properties.allmin = chartData.geoJson.properties.min;
chartData.geoJson.properties.allmax = chartData.geoJson.properties.max;
@ -76,6 +80,21 @@ define(function (require) {
subdomains: '1234'
});
var drawOptions = {draw: {}};
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
if (!self.events.dispatch[drawShape]) {
drawOptions.draw[drawShape] = false;
} else {
drawOptions.draw[drawShape] = {
shapeOptions: {
stroke: false,
color: '#000'
}
};
}
});
var mapOptions = {
minZoom: 1,
maxZoom: 18,
@ -85,11 +104,15 @@ define(function (require) {
noWrap: true,
maxBounds: worldBounds,
scrollWheelZoom: false,
fadeAnimation: false
fadeAnimation: false,
};
var map = L.map(div[0], mapOptions);
if (data.geoJson.features.length) {
map.addControl(new L.Control.Draw(drawOptions));
}
tileLayer.on('tileload', function () {
self.saturateTiles();
});
@ -105,6 +128,29 @@ define(function (require) {
mapCenter = self._attr.mapCenter = map.getCenter();
});
map.on('draw:created', function (e) {
var drawType = e.layerType;
if (!self.events.dispatch[drawType]) return;
// TODO: Different drawTypes need differ info. Need a switch on the object creation
var bounds = e.layer.getBounds();
self.events.dispatch[drawType]({
e: e,
data: self.chartData,
bounds: {
top_left: {
lat: bounds.getNorthWest().lat,
lon: bounds.getNorthWest().lng
},
bottom_right: {
lat: bounds.getSouthEast().lat,
lon: bounds.getSouthEast().lng
}
}
});
});
// add label for splits
if (mapData.properties.label) {
self.addLabel(mapData.properties.label, map);

View file

@ -1,8 +1,9 @@
define(function (require) {
return function TileMapVisType(Private) {
return function TileMapVisType(Private, getAppState) {
var VislibVisType = Private(require('plugins/vis_types/vislib/_vislib_vis_type'));
var Schemas = Private(require('plugins/vis_types/_schemas'));
var geoJsonConverter = Private(require('components/agg_response/geo_json/geo_json'));
var _ = require('lodash');
return new VislibVisType({
name: 'tile_map',
@ -18,6 +19,20 @@ define(function (require) {
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid'],
editor: require('text!plugins/vis_types/vislib/editors/tile_map.html')
},
listeners: {
rectangle: function (event) {
var agg = _.deepGet(event, 'data.geoJson.properties.agg');
if (!agg) return;
var pushFilter = Private(require('components/filter_bar/push_filter'))(getAppState());
var indexPatternName = agg.geo.vis.indexPattern.id;
var field = agg.geo.fieldName();
var filter = {geo_bounding_box: {}};
filter.geo_bounding_box[field] = event.bounds;
pushFilter(filter, false, indexPatternName);
}
},
responseConverter: geoJsonConverter,
schemas: new Schemas([
{

View file

@ -28,6 +28,7 @@ require.config({
inflection: 'bower_components/inflection/lib/inflection',
jquery: 'bower_components/jquery/dist/jquery',
leaflet: 'bower_components/leaflet/dist/leaflet',
'leaflet-draw': 'bower_components/leaflet-draw/dist/leaflet.draw',
lodash_src: 'bower_components/lodash/dist/lodash',
'lodash-deep': 'bower_components/lodash-deep/factory',
moment: 'bower_components/moment/moment',
@ -56,6 +57,9 @@ require.config({
file_saver: {
exports: 'saveAs'
},
'leaflet-draw': {
deps: ['leaflet', 'css!bower_components/leaflet-draw/dist/leaflet.draw.css']
},
leaflet: {
deps: ['css!bower_components/leaflet/dist/leaflet.css']
},

View file

@ -0,0 +1,62 @@
define(function (require) {
describe('Filter Bar Directive', function () {
describe('mapGeoBoundingBox()', function () {
var sinon = require('test_utils/auto_release_sinon');
var mapGeoBoundingBox, $rootScope, indexPattern, getIndexPatternStub;
beforeEach(module('kibana'));
beforeEach(function () {
getIndexPatternStub = sinon.stub();
module('kibana/courier', function ($provide) {
$provide.service('courier', function () {
var courier = { indexPatterns: { get: getIndexPatternStub } };
return courier;
});
});
});
beforeEach(inject(function (Private, _$rootScope_, Promise) {
mapGeoBoundingBox = Private(require('components/filter_bar/lib/mapGeoBoundingBox'));
$rootScope = _$rootScope_;
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
getIndexPatternStub.returns(Promise.resolve(indexPattern));
}));
it('should return the key and value for matching filters with bounds', function (done) {
var filter = {
meta: {
index: 'logstash-*'
},
geo_bounding_box: {
point: { // field name
top_left: {
lat: 5,
lon: 10
},
bottom_right: {
lat: 15,
lon: 20
}
}
}
};
mapGeoBoundingBox(filter).then(function (result) {
expect(result).to.have.property('key', 'point');
expect(result).to.have.property('value', '{"lat":5,"lon":10} to {"lat":15,"lon":20}');
done();
});
$rootScope.$apply();
});
it('should return undefined for none matching', function (done) {
var filter = { meta: { index: 'logstash-*' }, query: { query_string: { query: 'foo:bar' } } };
mapGeoBoundingBox(filter).catch(function (result) {
expect(result).to.be(filter);
done();
});
$rootScope.$apply();
});
});
});
});

View file

@ -0,0 +1,68 @@
define(function (require) {
describe('Filter Bar pushFilter()', function () {
var _ = require('lodash');
var pushFilterFn;
beforeEach(module('kibana'));
beforeEach(inject(function (Private, $injector) {
pushFilterFn = Private(require('components/filter_bar/push_filter'));
}));
it('is a function that returns a function', function () {
expect(pushFilterFn).to.be.a(Function);
expect(pushFilterFn({})).to.be.a(Function);
});
it('throws an error if passed something besides an object', function () {
expect(pushFilterFn).withArgs(true).to.throwError();
});
describe('pushFilter($state)()', function () {
var $state;
var pushFilter;
var filter;
beforeEach(inject(function (Private, $injector) {
$state = {filters:[]};
pushFilter = pushFilterFn($state);
filter = {query: {query_string: {query: ''}}};
}));
it('should create the filters property it needed', function () {
var altState = {};
pushFilterFn(altState)(filter);
expect(altState.filters).to.be.an(Array);
});
it('should replace the filters property instead of modifying it', function () {
// If we push directly instead of using pushFilter a $watch('filters') does not trigger
var oldFilters;
oldFilters = $state.filters;
$state.filters.push(filter);
expect($state.filters).to.equal(oldFilters); // Same object
oldFilters = $state.filters;
pushFilter(filter);
expect($state.filters).to.not.equal(oldFilters); // New object!
});
it('should add meta data to the filter', function () {
pushFilter(filter, true, 'myIndex');
expect($state.filters[0].meta).to.be.an(Object);
expect($state.filters[0].meta.negate).to.be(true);
expect($state.filters[0].meta.index).to.be('myIndex');
pushFilter(filter, false, 'myIndex');
expect($state.filters[1].meta.negate).to.be(false);
});
});
});
});

View file

@ -9,91 +9,118 @@ define(function (require) {
angular.module('DispatchClass', ['kibana']);
describe('VisLib Dispatch Class Test Suite', function () {
var vis;
beforeEach(function () {
module('AreaChartFactory');
});
beforeEach(function () {
inject(function (Private) {
vis = Private(require('vislib_fixtures/_vis_fixture'))();
require('css!components/vislib/styles/main');
vis.on('brush', _.noop);
vis.render(data);
});
});
afterEach(function () {
function destroyVis(vis) {
$(vis.el).remove();
vis = null;
});
}
describe('addEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var addEvent = chart.events.addEvent;
expect(_.isFunction(addEvent('click', _.noop))).to.be(true);
describe('Stock event handlers', function () {
var vis;
beforeEach(function () {
module('AreaChartFactory');
});
beforeEach(function () {
inject(function (Private) {
vis = Private(require('vislib_fixtures/_vis_fixture'))();
require('css!components/vislib/styles/main');
vis.on('brush', _.noop);
vis.render(data);
});
});
afterEach(function () {
destroyVis(vis);
});
describe('addEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var addEvent = chart.events.addEvent;
expect(_.isFunction(addEvent('click', _.noop))).to.be(true);
});
});
});
describe('addHoverEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var hover = chart.events.addHoverEvent;
expect(_.isFunction(hover)).to.be(true);
});
});
it('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.hover)).to.be(true);
});
});
});
describe('addClickEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var click = chart.events.addClickEvent;
expect(_.isFunction(click)).to.be(true);
});
});
it('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.click)).to.be(true);
});
});
});
describe('addBrushEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var brush = chart.events.addBrushEvent;
expect(_.isFunction(brush)).to.be(true);
});
});
it('should attach a brush event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.brush)).to.be(true);
});
});
});
describe('addMousePointer method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var pointer = chart.events.addMousePointer;
expect(_.isFunction(pointer)).to.be(true);
});
});
});
});
describe('addHoverEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var hover = chart.events.addHoverEvent;
describe('Custom event handlers', function () {
it('should attach whatever gets passed on vis.on() to dispatch', function (done) {
var vis;
var chart;
module('AreaChartFactory');
inject(function (Private) {
vis = Private(require('vislib_fixtures/_vis_fixture'))();
vis.on('someEvent', _.noop);
vis.render(data);
expect(_.isFunction(hover)).to.be(true);
});
});
vis.handler.charts.forEach(function (chart) {
expect(chart.events.dispatch.someEvent).to.be.a(Function);
});
it('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.hover)).to.be(true);
});
});
});
describe('addClickEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var click = chart.events.addClickEvent;
expect(_.isFunction(click)).to.be(true);
});
});
it('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.click)).to.be(true);
});
});
});
describe('addBrushEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var brush = chart.events.addBrushEvent;
expect(_.isFunction(brush)).to.be(true);
});
});
it('should attach a brush event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.brush)).to.be(true);
});
});
});
describe('addMousePointer method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var pointer = chart.events.addMousePointer;
expect(_.isFunction(pointer)).to.be(true);
destroyVis(vis);
done();
});
});
});

View file

@ -11,90 +11,169 @@ define(function (require) {
require('vislib_fixtures/mock_data/geohash/_rows')
];
var names = ['geojson', 'columns', 'rows'];
var mapTypes = ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Pins'];
// TODO: Test the specific behavior of each these
var mapTypes = ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid'];
angular.module('TileMapFactory', ['kibana']);
dataArray.forEach(function (data, i) {
mapTypes.forEach(function (type, j) {
function bootstrapAndRender(data, type) {
var vis;
var visLibParams = {
isDesaturated: true,
type: 'tile_map',
mapType: type
};
describe('TileMap Test Suite for ' + mapTypes[j] + ' with ' + names[i] + ' data', function () {
var vis;
var visLibParams = {
isDesaturated: true,
type: 'tile_map',
mapType: type
};
module('TileMapFactory');
inject(function (Private) {
vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams);
require('css!components/vislib/styles/main');
vis.render(data);
});
beforeEach(function () {
module('TileMapFactory');
});
return vis;
beforeEach(function () {
inject(function (Private) {
vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams);
require('css!components/vislib/styles/main');
vis.render(data);
});
});
}
afterEach(function () {
$(vis.el).remove();
vis = null;
});
function destroyVis(vis) {
$(vis.el).remove();
vis = null;
}
describe('draw method', function () {
var leafletContainer;
var isDrawn;
describe('TileMap Tests', function () {
describe('Rendering each types of tile map', function () {
dataArray.forEach(function (data, i) {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.draw())).to.be(true);
mapTypes.forEach(function (type, j) {
describe('draw() ' + mapTypes[j] + ' with ' + names[i], function () {
var vis;
beforeEach(function () {
vis = bootstrapAndRender(data, type);
});
});
it('should draw a map', function () {
leafletContainer = $(vis.el).find('.leaflet-container');
isDrawn = (leafletContainer.length > 0);
expect(isDrawn).to.be(true);
});
});
describe('geohashMinDistance method', function () {
it('should return a number', function () {
vis.handler.charts.forEach(function (chart) {
var feature = chart.chartData.geoJson.features[0];
expect(_.isNumber(chart.geohashMinDistance(feature))).to.be(true);
afterEach(function () {
destroyVis(vis);
});
});
});
describe('radiusScale method', function () {
it('should return a number', function () {
vis.handler.charts.forEach(function (chart) {
var count = Math.random() * 50;
var max = 50;
var precision = 1;
var feature = chart.chartData.geoJson.features[0];
expect(_.isNumber(chart.radiusScale(count, max, feature))).to.be(true);
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
expect(chart.draw()).to.be.a(Function);
});
});
});
});
describe('quantizeColorScale method', function () {
it('should return a hex color', function () {
vis.handler.charts.forEach(function (chart) {
var reds = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
var count = Math.random() * 300;
var min = 0;
var max = 300;
expect(_.indexOf(reds, chart.quantizeColorScale(count, min, max))).to.not.be(-1);
it('should create .leaflet-container as a by product of map rendering', function () {
expect($(vis.el).find('.leaflet-container').length).to.be.above(0);
});
});
});
});
});
describe('Leaflet controls', function () {
var vis;
var leafletContainer;
beforeEach(function () {
vis = bootstrapAndRender(dataArray[0], 'Scaled Circle Markers');
leafletContainer = $(vis.el).find('.leaflet-container');
});
afterEach(function () {
destroyVis(vis);
});
it('should attach the zoom controls', function () {
expect(leafletContainer.find('.leaflet-control-zoom-in').length).to.be(1);
expect(leafletContainer.find('.leaflet-control-zoom-out').length).to.be(1);
});
it('should attach the filter drawing button', function () {
expect(leafletContainer.find('.leaflet-draw').length).to.be(1);
});
it('should attach the crop button', function () {
expect(leafletContainer.find('.leaflet-control-fit').length).to.be(1);
});
it('should not attach the filter or crop buttons if no data is present', function () {
var noData = {
geoJson: {
features: [],
properties: {
label: null,
length: 30,
min: 1,
max: 608,
precision: 1,
allmin: 1,
allmax: 608
},
hits: 20
}
};
vis.render(noData);
leafletContainer = $(vis.el).find('.leaflet-container');
expect(leafletContainer.find('.leaflet-control-fit').length).to.be(0);
expect(leafletContainer.find('.leaflet-draw').length).to.be(0);
});
});
// Probably only neccesary to test one of these as we already know the the map will render
describe('Methods', function () {
var vis;
var leafletContainer;
beforeEach(function () {
vis = bootstrapAndRender(dataArray[0], 'Scaled Circle Markers');
leafletContainer = $(vis.el).find('.leaflet-container');
});
afterEach(function () {
destroyVis(vis);
});
describe('geohashMinDistance method', function () {
it('should return a number', function () {
vis.handler.charts.forEach(function (chart) {
var feature = chart.chartData.geoJson.features[0];
expect(_.isNumber(chart.geohashMinDistance(feature))).to.be(true);
});
});
});
describe('radiusScale method', function () {
it('should return a number', function () {
vis.handler.charts.forEach(function (chart) {
var count = Math.random() * 50;
var max = 50;
var precision = 1;
var feature = chart.chartData.geoJson.features[0];
expect(_.isNumber(chart.radiusScale(count, max, feature))).to.be(true);
});
});
});
describe('quantizeColorScale method', function () {
it('should return a hex color', function () {
vis.handler.charts.forEach(function (chart) {
var reds = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
var count = Math.random() * 300;
var min = 0;
var max = 300;
expect(_.indexOf(reds, chart.quantizeColorScale(count, min, max))).to.not.be(-1);
});
});
});
});
});
});