Improve map-data handling (#17263)

- Improve performance by avoiding unnecessary ES-response conversion. 
- Prepare the code-internals visualization to use intermediate tabular representation. This sets this up for a possible refactor to use the Canvas data pipeline.
- Do not clone ES-responses when not necessary.
This commit is contained in:
Thomas Neirynck 2018-04-19 22:43:11 -04:00 committed by GitHub
parent dea6062f9d
commit 5c38b474ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1996 additions and 3583 deletions

View file

@ -87,6 +87,5 @@ VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmaps
])
},
responseHandler: 'tabify'
});
});

View file

@ -30,6 +30,7 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) {
async _updateData(tableGroup) {
this._chartData = tableGroup;
let results;
if (!tableGroup || !tableGroup.tables || !tableGroup.tables.length || tableGroup.tables[0].columns.length !== 2) {
results = [];

View file

@ -109,6 +109,28 @@ describe('CoordinateMapsVisualizationTest', function () {
mapZoom: 2,
mapCenter: [0, 0],
};
const mockAggs = [
{
type: {
type: 'metrics'
},
fieldFormatter: (x) => {
return x;
},
makeLabel: () => {
return 'foobar';
}
}, {
type: {
type: 'buckets'
},
params: { useGeoCentroid: true }
}];
vis.getAggConfig = function () {
return mockAggs;
};
vis.aggs = mockAggs;
});
afterEach(function () {
@ -133,7 +155,6 @@ describe('CoordinateMapsVisualizationTest', function () {
});
it('should toggle to Heatmap OK', async function () {
const coordinateMapVisualization = new CoordinateMapsVisualization(domNode, vis);
@ -203,7 +224,6 @@ describe('CoordinateMapsVisualizationTest', function () {
async function compareImage(expectedImageSource, index) {
const elementList = domNode.querySelectorAll('canvas');
// expect(elementList.length).to.equal(1);
const firstCanvasOnMap = elementList[index];
return imageComparator.compareImage(firstCanvasOnMap, expectedImageSource, THRESHOLD);
}

View file

@ -1,13 +1,13 @@
import expect from 'expect.js';
import { KibanaMap } from 'ui/vis/map/kibana_map';
import { GeohashLayer } from '../geohash_layer';
import { GeoHashSampleData } from './geohash_sample_data';
import heatmapPng from './heatmap.png';
import scaledCircleMarkersPng from './scaledCircleMarkers.png';
import shadedCircleMarkersPng from './shadedCircleMarkers.png';
import { ImageComparator } from 'test_utils/image_comparator';
import GeoHashSampleData from './dummy_es_response.json';
describe('kibana_map tests', function () {
describe('geohash_layer', function () {
let domNode;
let expectCanvas;
@ -79,7 +79,9 @@ describe('kibana_map tests', function () {
it(test.options.mapType, async function () {
const geohashGridOptions = test.options;
const geohashLayer = new GeohashLayer(GeoHashSampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
const geohashLayer = new GeohashLayer(
GeoHashSampleData.featureCollection,
GeoHashSampleData.meta, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
kibanaMap.addLayer(geohashLayer);
const elementList = domNode.querySelectorAll('canvas');
@ -93,10 +95,11 @@ describe('kibana_map tests', function () {
});
it('should not throw when fitting on empty-data layer', function () {
const geohashLayer = new GeohashLayer({
type: 'FeatureCollection',
features: []
}, { 'mapType': 'Scaled Circle Markers' }, kibanaMap.getZoomLevel(), kibanaMap);
const geohashLayer = new GeohashLayer(
{
type: 'FeatureCollection',
features: []
}, {}, { 'mapType': 'Scaled Circle Markers' }, kibanaMap.getZoomLevel(), kibanaMap);
kibanaMap.addLayer(geohashLayer);
expect(() => {
@ -113,7 +116,8 @@ describe('kibana_map tests', function () {
}
};
const geohashLayer = new GeohashLayer(GeoHashSampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
const geohashLayer = new GeohashLayer(GeoHashSampleData.featureCollection,
GeoHashSampleData.meta, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
kibanaMap.addLayer(geohashLayer);
domNode.style.width = 0;
domNode.style.height = 0;
@ -124,8 +128,5 @@ describe('kibana_map tests', function () {
});
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -5,7 +5,7 @@ import 'ui/vis/map/service_settings';
const MINZOOM = 0;
const MAXZOOM = 18;
const MAXZOOM = 22;//increase this to 22. Better for WMS
export function BaseMapsVisualizationProvider(serviceSettings) {
@ -20,6 +20,7 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
this.vis = vis;
this._container = element;
this._kibanaMap = null;
this._chartData = null; //reference to data currently on the map.
this._baseLayerDirty = true;
this._mapIsLoaded = this._makeKibanaMap();
}
@ -31,14 +32,6 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
}
}
/**
* checks whether the data is usable.
* @return {boolean}
*/
isDataUsable() {
return true;
}
/**
* Implementation of Visualization#render.
* Child-classes can extend this method if the render-complete function requires more time until rendering has completed.
@ -47,7 +40,6 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
* @return {Promise}
*/
async render(esResponse, status) {
if (!this._kibanaMap) {
//the visualization has been destroyed;
return;
@ -62,11 +54,7 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
await this._updateParams();
}
if (!this.isDataUsable(esResponse)) {
return;
}
if (status.data) {
if (this._hasESResponseChanged(esResponse)) {
await this._updateData(esResponse);
}
if (status.uiState) {
@ -114,12 +102,10 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
async _updateBaseLayer() {
if (!this._kibanaMap) {
return;
}
const mapParams = this._getMapsParams();
if (!this._baseLayerConfigured()) {
try {
@ -141,6 +127,7 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
try {
if (mapParams.wms.enabled) {
if (MINZOOM > this._kibanaMap.getMaxZoomLevel()) {
this._kibanaMap.setMinZoom(MINZOOM);
this._kibanaMap.setMaxZoom(MAXZOOM);
@ -189,6 +176,10 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
throw new Error('Child should implement this method to respond to data-update');
}
_hasESResponseChanged(data) {
return this._chartData !== data;
}
/**
* called on options change (vis.params change)
*/

View file

@ -3,10 +3,13 @@ import { GeohashLayer } from './geohash_layer';
import { BaseMapsVisualizationProvider } from './base_maps_visualization';
import { AggConfig } from 'ui/vis/agg_config';
import './styles/_tilemap.less';
import { TileMapTooltipFormatterProvider } from './editors/_tooltip_formatter';
export function CoordinateMapsVisualizationProvider(Notifier, Private) {
const BaseMapsVisualization = Private(BaseMapsVisualizationProvider);
const tooltipFormatter = Private(TileMapTooltipFormatterProvider);
class CoordinateMapsVisualization extends BaseMapsVisualization {
constructor(element, vis) {
@ -15,10 +18,6 @@ export function CoordinateMapsVisualizationProvider(Notifier, Private) {
this._notify = new Notifier({ location: 'Coordinate Map' });
}
isDataUsable(esResponse) {
return !(esResponse && typeof esResponse.geohashGridAgg === 'undefined');
}
async _makeKibanaMap() {
@ -26,65 +25,81 @@ export function CoordinateMapsVisualizationProvider(Notifier, Private) {
this.vis.sessionState.mapBounds = this._kibanaMap.getUntrimmedBounds();
let previousPrecision = this._kibanaMap.getAutoPrecision();
let previousPrecision = this._kibanaMap.getGeohashPrecision();
let precisionChange = false;
this._kibanaMap.on('zoomchange', () => {
precisionChange = (previousPrecision !== this._kibanaMap.getAutoPrecision());
previousPrecision = this._kibanaMap.getAutoPrecision();
const agg = this._getGeoHashAgg();
const isAutoPrecision = _.get(this._chartData, 'geohashGridAgg.params.autoPrecision', true);
if (agg && isAutoPrecision) {
agg.params.precision = previousPrecision;
precisionChange = (previousPrecision !== this._kibanaMap.getGeohashPrecision());
previousPrecision = this._kibanaMap.getGeohashPrecision();
const geohashAgg = this._getGeoHashAgg();
const isAutoPrecision = typeof geohashAgg.params.autoPrecision === 'boolean' ? geohashAgg.params.autoPrecision : true;
if (geohashAgg && isAutoPrecision) {
geohashAgg.params.precision = previousPrecision;
}
});
this._kibanaMap.on('zoomend', () => {
const isAutoPrecision = _.get(this._chartData, 'geohashGridAgg.params.autoPrecision', true);
const agg = this._getGeoHashAgg();
const isAutoPrecision = typeof agg.params.autoPrecision === 'boolean' ? agg.params.autoPrecision : true;
if (!isAutoPrecision) {
return;
}
if (precisionChange) {
this.vis.updateState();
} else {
this._updateData(this._chartData);
//when we filter queries by collar
this._updateData(this._geoJsonFeatureCollectionAndMeta);
}
});
this._kibanaMap.addDrawControl();
this._kibanaMap.on('drawCreated:rectangle', event => {
this.addSpatialFilter(_.get(this._chartData, 'geohashGridAgg'), 'geo_bounding_box', event.bounds);
const geoAgg = this._getGeoHashAgg();
this.addSpatialFilter(geoAgg, 'geo_bounding_box', event.bounds);
});
this._kibanaMap.on('drawCreated:polygon', event => {
this.addSpatialFilter(_.get(this._chartData, 'geohashGridAgg'), 'geo_polygon', { points: event.points });
const geoAgg = this._getGeoHashAgg();
this.addSpatialFilter(geoAgg, 'geo_polygon', { points: event.points });
});
}
async _updateData(esResponse) {
async _updateData(geojsonFeatureCollectionAndMeta) {
// Only recreate geohash layer when there is new aggregation data
// Exception is Heatmap: which needs to be redrawn every zoom level because the clustering is based on meters per pixel
if (this._getMapsParams().mapType !== 'Heatmap' && esResponse === this._chartData) {
if (
this._getMapsParams().mapType !== 'Heatmap' &&
geojsonFeatureCollectionAndMeta === this._geoJsonFeatureCollectionAndMeta) {
return;
}
this._chartData = esResponse;
if (this._geohashLayer) {
this._kibanaMap.removeLayer(this._geohashLayer);
this._geohashLayer = null;
}
if (!this._chartData || !this._chartData.geoJson) {
if (!geojsonFeatureCollectionAndMeta) {
this._geoJsonFeatureCollectionAndMeta = null;
this._kibanaMap.removeLayer(this._geohashLayer);
this._geohashLayer = null;
return;
}
this._recreateGeohashLayer(this._chartData.geoJson);
this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta;
this._recreateGeohashLayer();
}
_recreateGeohashLayer(geojsonData) {
_recreateGeohashLayer() {
if (this._geohashLayer) {
this._kibanaMap.removeLayer(this._geohashLayer);
this._geohashLayer = null;
}
const geohashOptions = this._getGeohashOptions();
this._geohashLayer = new GeohashLayer(geojsonData, geohashOptions, this._kibanaMap.getZoomLevel(), this._kibanaMap);
this._geohashLayer = new GeohashLayer(
this._geoJsonFeatureCollectionAndMeta.featureCollection,
this._geoJsonFeatureCollectionAndMeta.meta,
geohashOptions,
this._kibanaMap.getZoomLevel(),
this._kibanaMap);
this._kibanaMap.addLayer(this._geohashLayer);
}
@ -98,18 +113,23 @@ export function CoordinateMapsVisualizationProvider(Notifier, Private) {
//e.g. tooltip-visibility, legend position, basemap-desaturation, ...
const geohashOptions = this._getGeohashOptions();
if (!this._geohashLayer || !this._geohashLayer.isReusable(geohashOptions)) {
if (this._chartData && this._chartData.geoJson) {
this._recreateGeohashLayer(this._chartData.geoJson);
if (this._geoJsonFeatureCollectionAndMeta) {
this._recreateGeohashLayer();
}
this._updateData(this._geoJsonFeatureCollectionAndMeta);
}
}
_getGeohashOptions() {
const newParams = this._getMapsParams();
const metricAgg = this._getMetricAgg();
const boundTooltipFormatter = tooltipFormatter.bind(null, this.vis.getAggConfig(), metricAgg);
return {
valueFormatter: this._chartData ? this._chartData.valueFormatter : null,
tooltipFormatter: this._chartData ? this._chartData.tooltipFormatter : null,
label: metricAgg ? metricAgg.makeLabel() : '',
valueFormatter: this._geoJsonFeatureCollectionAndMeta ? (metricAgg && metricAgg.fieldFormatter()) : null,
tooltipFormatter: this._geoJsonFeatureCollectionAndMeta ? boundTooltipFormatter : null,
mapType: newParams.mapType,
isFilteredByCollar: this._isFilteredByCollar(),
fetchBounds: this.getGeohashBounds.bind(this),
@ -164,9 +184,16 @@ export function CoordinateMapsVisualizationProvider(Notifier, Private) {
});
}
_getMetricAgg() {
return this.vis.getAggConfig().find((agg) => {
return agg.type.type === 'metrics';
});
}
_isFilteredByCollar() {
const DEFAULT = false;
const agg = this._getGeoHashAgg();
if (agg) {
return _.get(agg, 'params.isFilteredByCollar', DEFAULT);

View file

@ -0,0 +1,25 @@
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson';
export function makeGeoJsonResponseHandler() {
let lastEsResponse;
let lastGeoJsonResponse;
return function (vis, esResponse) {
if (lastEsResponse === esResponse) {
return lastGeoJsonResponse;
}
lastEsResponse = esResponse;
//double conversion, first to table, then to geojson
//This is to future-proof this code for Canvas-refactoring
const tabifiedResponse = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), esResponse, {
asAggConfigResults: false
});
lastGeoJsonResponse = convertToGeoJson(tabifiedResponse);
return lastGeoJsonResponse;
};
}

View file

@ -0,0 +1,34 @@
import $ from 'jquery';
export function TileMapTooltipFormatterProvider($compile, $rootScope) {
const $tooltipScope = $rootScope.$new();
const $el = $('<div>').html(require('./_tooltip.html'));
$compile($el)($tooltipScope);
return function tooltipFormatter(aggConfig, metricAgg, feature) {
if (!feature) {
return '';
}
$tooltipScope.details = [
{
label: metricAgg.makeLabel(),
value: metricAgg.fieldFormatter()(feature.properties.value)
},
{
label: 'Latitude',
value: feature.geometry.coordinates[1]
},
{
label: 'Longitude',
value: feature.geometry.coordinates[0]
}
];
$tooltipScope.$apply();
return $el.html();
};
}

View file

@ -9,15 +9,17 @@ import { GeohashGridMarkers } from './markers/geohash_grid';
export class GeohashLayer extends KibanaMapLayer {
constructor(featureCollection, options, zoom, kibanaMap) {
constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap) {
super();
this._geohashGeoJson = featureCollection;
this._featureCollection = featureCollection;
this._featureCollectionMetaData = featureCollectionMetaData;
this._geohashOptions = options;
this._zoom = zoom;
this._kibanaMap = kibanaMap;
const geojson = L.geoJson(this._geohashGeoJson);
const geojson = L.geoJson(this._featureCollection);
this._bounds = geojson.getBounds();
this._createGeohashMarkers();
this._lastBounds = null;
@ -27,33 +29,38 @@ export class GeohashLayer extends KibanaMapLayer {
const markerOptions = {
isFilteredByCollar: this._geohashOptions.isFilteredByCollar,
valueFormatter: this._geohashOptions.valueFormatter,
tooltipFormatter: this._geohashOptions.tooltipFormatter
tooltipFormatter: this._geohashOptions.tooltipFormatter,
label: this._geohashOptions.label
};
switch (this._geohashOptions.mapType) {
case 'Scaled Circle Markers':
this._geohashMarkers = new ScaledCirclesMarkers(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
this._geohashMarkers = new ScaledCirclesMarkers(this._featureCollection,
this._featureCollectionMetaData, markerOptions, this._zoom, this._kibanaMap);
break;
case 'Shaded Circle Markers':
this._geohashMarkers = new ShadedCirclesMarkers(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
this._geohashMarkers = new ShadedCirclesMarkers(this._featureCollection,
this._featureCollectionMetaData, markerOptions, this._zoom, this._kibanaMap);
break;
case 'Shaded Geohash Grid':
this._geohashMarkers = new GeohashGridMarkers(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
this._geohashMarkers = new GeohashGridMarkers(this._featureCollection,
this._featureCollectionMetaData, markerOptions, this._zoom, this._kibanaMap);
break;
case 'Heatmap':
let radius = 15;
if (this._geohashGeoJson.properties.geohashGridDimensionsAtEquator) {
const minGridLength = _.min(this._geohashGeoJson.properties.geohashGridDimensionsAtEquator);
if (this._featureCollectionMetaData.geohashGridDimensionsAtEquator) {
const minGridLength = _.min(this._featureCollectionMetaData.geohashGridDimensionsAtEquator);
const metersPerPixel = this._kibanaMap.getMetersPerPixel();
radius = (minGridLength / metersPerPixel) / 2;
}
radius = radius * parseFloat(this._geohashOptions.heatmap.heatClusterSize);
this._geohashMarkers = new HeatmapMarkers(this._geohashGeoJson, {
this._geohashMarkers = new HeatmapMarkers(this._featureCollection, {
radius: radius,
blur: radius,
maxZoom: this._kibanaMap.getZoomLevel(),
minOpacity: 0.1,
tooltipFormatter: this._geohashOptions.tooltipFormatter
}, this._zoom, this._kibanaMap);
}, this._zoom, this._featureCollectionMetaData.max);
break;
default:
throw new Error(`${this._geohashOptions.mapType} mapType not recognized`);
@ -79,8 +86,7 @@ export class GeohashLayer extends KibanaMapLayer {
if (geoHashBounds) {
const northEast = L.latLng(geoHashBounds.top_left.lat, geoHashBounds.bottom_right.lon);
const southWest = L.latLng(geoHashBounds.bottom_right.lat, geoHashBounds.top_left.lon);
const leaftetBounds = L.latLngBounds(southWest, northEast);
return leaftetBounds;
return L.latLngBounds(southWest, northEast);
}
}

View file

@ -4,7 +4,7 @@ import { ScaledCirclesMarkers } from './scaled_circles';
export class GeohashGridMarkers extends ScaledCirclesMarkers {
getMarkerFunction() {
return function (feature) {
const geohashRect = feature.properties.rectangle;
const geohashRect = feature.properties.geohash_meta.rectangle;
// get bounds from northEast[3] and southWest[1]
// corners in geohash rectangle
const corners = [

View file

@ -12,11 +12,11 @@ import { EventEmitter } from 'events';
*/
export class HeatmapMarkers extends EventEmitter {
constructor(featureCollection, options, zoom) {
constructor(featureCollection, options, zoom, max) {
super();
this._geojsonFeatureCollection = featureCollection;
const points = dataToHeatArray(featureCollection);
const points = dataToHeatArray(featureCollection, max);
this._leafletLayer = L.heatLayer(points, options);
this._tooltipFormatter = options.tooltipFormatter;
this._zoom = zoom;
@ -170,8 +170,7 @@ export class HeatmapMarkers extends EventEmitter {
* @param featureCollection {Array}
* @return {Array}
*/
function dataToHeatArray(featureCollection) {
const max = _.get(featureCollection, 'properties.max');
function dataToHeatArray(featureCollection, max) {
return featureCollection.features.map((feature) => {
const lat = feature.geometry.coordinates[1];

View file

@ -6,13 +6,17 @@ import { EventEmitter } from 'events';
export class ScaledCirclesMarkers extends EventEmitter {
constructor(featureCollection, options, targetZoom, kibanaMap) {
constructor(featureCollection, featureCollectionMetaData, options, targetZoom, kibanaMap, metricAgg) {
super();
this._geohashGeoJson = featureCollection;
this._featureCollection = featureCollection;
this._featureCollectionMetaData = featureCollectionMetaData;
this._zoom = targetZoom;
this._metricAgg = metricAgg;
this._valueFormatter = options.valueFormatter || ((x) => {x;});
this._tooltipFormatter = options.tooltipFormatter || ((x) => {x;});
this._label = options.label;
this._legendColors = null;
this._legendQuantizer = null;
@ -29,13 +33,13 @@ export class ScaledCirclesMarkers extends EventEmitter {
// Filter leafletlayer on client when results are not filtered on the server
if (!options.isFilteredByCollar) {
layerOptions.filter = (feature) => {
const bucketRectBounds = _.get(feature, 'properties.rectangle');
const bucketRectBounds = feature.properties.geohash_meta.rectangle;
return kibanaMap.isInside(bucketRectBounds);
};
}
this._leafletLayer = L.geoJson(null, layerOptions);
this._leafletLayer.addData(this._geohashGeoJson);
this._leafletLayer.addData(this._featureCollection);
}
getLeafletLayer() {
@ -44,8 +48,9 @@ export class ScaledCirclesMarkers extends EventEmitter {
getStyleFunction() {
const min = _.get(this._geohashGeoJson, 'properties.min', 0);
const max = _.get(this._geohashGeoJson, 'properties.max', 1);
const min = _.get(this._featureCollectionMetaData, 'min', 0);
const max = _.get(this._featureCollectionMetaData, 'max', 1);
const quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
this._legendColors = makeCircleMarkerLegendColors(min, max);
@ -60,12 +65,11 @@ export class ScaledCirclesMarkers extends EventEmitter {
getLabel() {
if (this._popups.length) {
return this._popups[0].feature.properties.aggConfigResult.aggConfig.makeLabel();
return this._label;
}
return '';
}
appendLegendContents(jqueryDiv) {
if (!this._legendColors || !this._legendQuantizer) {
@ -130,20 +134,16 @@ export class ScaledCirclesMarkers extends EventEmitter {
*
* @method _showTooltip
* @param feature {LeafletFeature}
* @param latLng? {Leaflet latLng}
* @return undefined
*/
_showTooltip(feature, latLng) {
const lat = _.get(feature, 'geometry.coordinates.1');
const lng = _.get(feature, 'geometry.coordinates.0');
latLng = latLng || L.latLng(lat, lng);
_showTooltip(feature) {
const content = this._tooltipFormatter(feature);
if (!content) {
return;
}
const latLng = L.latLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]);
this.emit('showTooltip', {
content: content,
position: latLng
@ -168,16 +168,15 @@ export class ScaledCirclesMarkers extends EventEmitter {
* @return {Number}
*/
_radiusScale(value) {
//magic numbers
const precisionBiasBase = 5;
const precisionBiasNumerator = 200;
const precision = _.max(this._geohashGeoJson.features.map((feature) => {
const precision = _.max(this._featureCollection.features.map((feature) => {
return String(feature.properties.geohash).length;
}));
const pct = Math.abs(value) / Math.abs(this._geohashGeoJson.properties.max);
const pct = Math.abs(value) / Math.abs(this._featureCollectionMetaData.max);
const zoomRadius = 0.5 * Math.pow(2, this._zoom);
const precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);

View file

@ -22,8 +22,8 @@ export class ShadedCirclesMarkers extends ScaledCirclesMarkers {
* @return {Number}
*/
_geohashMinDistance(feature) {
const centerPoint = _.get(feature, 'properties.center');
const geohashRect = _.get(feature, 'properties.rectangle');
const centerPoint = feature.properties.geohash_meta.center;
const geohashRect = feature.properties.geohash_meta.rectangle;
// centerPoint is an array of [lat, lng]
// geohashRect is the 4 corners of the geoHash rectangle

View file

@ -5,15 +5,14 @@ import { CATEGORY } from 'ui/vis/vis_category';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CoordinateMapsVisualizationProvider } from './coordinate_maps_visualization';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { AggResponseGeoJsonProvider } from 'ui/agg_response/geo_json/geo_json';
import image from './images/icon-tilemap.svg';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { Status } from 'ui/vis/update_status';
import { makeGeoJsonResponseHandler } from './coordinatemap_response_handler';
VisTypesRegistryProvider.register(function TileMapVisType(Private, getAppState, courier, config) {
const geoJsonConverter = Private(AggResponseGeoJsonProvider);
const VisFactory = Private(VisFactoryProvider);
const CoordinateMapsVisualization = Private(CoordinateMapsVisualizationProvider);
@ -36,9 +35,8 @@ VisTypesRegistryProvider.register(function TileMapVisType(Private, getAppState,
wms: config.get('visualization:tileMap:WMSdefaults')
}
},
requiresUpdateStatus: [Status.AGGS, Status.PARAMS, Status.RESIZE, Status.DATA, Status.UI_STATE],
responseConverter: geoJsonConverter,
responseHandler: 'basic',
requiresUpdateStatus: [Status.AGGS, Status.PARAMS, Status.RESIZE, Status.UI_STATE],
responseHandler: makeGeoJsonResponseHandler(),
visualization: CoordinateMapsVisualization,
editorConfig: {
collections: {

View file

@ -177,7 +177,8 @@ export default () => Joi.object({
map: Joi.object({
manifestServiceUrl: Joi.when('$dev', {
is: true,
then: Joi.string().default('https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v2/manifest'),
// then: Joi.string().default('https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v2/manifest'),
then: Joi.string().default('https://catalogue.maps.elastic.co/v2/manifest'),
otherwise: Joi.string().default('https://catalogue.maps.elastic.co/v2/manifest')
}),
includeElasticMapsService: Joi.boolean().default(true)

View file

@ -1,263 +0,0 @@
import _ from 'lodash';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { VisProvider } from '../../../vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import FixturesAggRespGeohashGridProvider from 'fixtures/agg_resp/geohash_grid';
import { tabifyAggResponse } from '../../tabify/tabify';
import { AggResponseGeoJsonProvider } from '../geo_json';
describe('GeoJson Agg Response Converter', function () {
let vis;
let convert;
let esResponse;
let expectedAggs;
let createVis;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
const Vis = Private(VisProvider);
const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
esResponse = Private(FixturesAggRespGeohashGridProvider);
convert = Private(AggResponseGeoJsonProvider);
createVis = function (useGeocentroid) {
vis = new Vis(indexPattern, {
type: 'tile_map',
aggs: [
{ schema: 'metric', type: 'avg', params: { field: 'bytes' } },
{ schema: 'segment', type: 'geohash_grid', params: { field: 'geo.coordinates', precision: 3, useGeocentroid: useGeocentroid } }
],
params: {
isDesaturated: true,
mapType: 'Scaled%20Circle%20Markers'
}
});
expectedAggs = {
metric: vis.aggs[0],
geo: vis.aggs[1]
};
if (useGeocentroid) {
expectedAggs.centroid = vis.aggs[2];
}
};
createVis(false);
}));
[ { asAggConfigResults: true }, { asAggConfigResults: false } ].forEach(function (tableOpts) {
function makeTable() {
return _.sample(tabifyAggResponse(vis.getAggConfig().getResponseAggs(), esResponse, tableOpts).tables);
}
function makeSingleChart(table) {
return convert(vis, table || makeTable(), tableOpts);
}
function makeGeoJson() {
return makeSingleChart().geoJson;
}
describe('with table ' + JSON.stringify(tableOpts), function () {
it('outputs a chart', function () {
const table = makeTable();
const chart = makeSingleChart(table);
expect(chart).to.only.have.keys(
'title',
'tooltipFormatter',
'valueFormatter',
'geohashGridAgg',
'geoJson'
);
expect(chart.title).to.be(table.title());
expect(chart.tooltipFormatter).to.be.a('function');
expect(chart.valueFormatter).to.be(expectedAggs.metric.fieldFormatter());
expect(chart.geohashGridAgg).to.be(expectedAggs.geo);
expect(chart.geoJson).to.be.an('object');
});
it('outputs geohash points as features in a feature collection', function () {
const table = makeTable();
const chart = makeSingleChart(table);
const geoJson = chart.geoJson;
expect(geoJson.type).to.be('FeatureCollection');
expect(geoJson.features).to.be.an('array');
expect(geoJson.features).to.have.length(table.rows.length);
});
it('exports a bunch of properties about the geo hash grid', function () {
const geoJson = makeGeoJson();
const props = geoJson.properties;
// props
expect(props).to.be.an('object');
expect(props).to.have.keys('min', 'max');
// props.min
expect(props.min).to.be.a('number');
expect(props.min).to.be.greaterThan(0);
// props.max
expect(props.max).to.be.a('number');
expect(props.max).to.be.greaterThan(0);
});
describe('properties', function () {
describe('includes one feature per row in the table', function () {
this.timeout(60000);
let table;
let chart;
let geoColI;
let metricColI;
before(function () {
table = makeTable();
chart = makeSingleChart(table);
geoColI = _.findIndex(table.columns, { aggConfig: expectedAggs.geo });
metricColI = _.findIndex(table.columns, { aggConfig: expectedAggs.metric });
});
it('should be geoJson format', function () {
table.rows.forEach(function (row, i) {
const feature = chart.geoJson.features[i];
expect(feature).to.have.property('geometry');
expect(feature.geometry).to.be.an('object');
expect(feature).to.have.property('properties');
expect(feature.properties).to.be.an('object');
});
});
it('should have valid geometry data', function () {
table.rows.forEach(function (row, i) {
const geometry = chart.geoJson.features[i].geometry;
expect(geometry.type).to.be('Point');
expect(geometry).to.have.property('coordinates');
expect(geometry.coordinates).to.be.an('array');
expect(geometry.coordinates).to.have.length(2);
expect(geometry.coordinates[0]).to.be.a('number');
expect(geometry.coordinates[1]).to.be.a('number');
});
});
it('should have value properties data', function () {
table.rows.forEach(function (row, i) {
const props = chart.geoJson.features[i].properties;
const keys = ['value', 'geohash', 'aggConfigResult', 'rectangle', 'center'];
expect(props).to.be.an('object');
expect(props).to.only.have.keys(keys);
expect(props.geohash).to.be.a('string');
if (props.value != null) expect(props.value).to.be.a('number');
});
});
it('should use latLng in properties and lngLat in geometry', function () {
table.rows.forEach(function (row, i) {
const geometry = chart.geoJson.features[i].geometry;
const props = chart.geoJson.features[i].properties;
expect(props.center).to.eql(geometry.coordinates.slice(0).reverse());
});
});
it('should handle both AggConfig and non-AggConfig results', function () {
table.rows.forEach(function (row, i) {
const props = chart.geoJson.features[i].properties;
if (tableOpts.asAggConfigResults) {
expect(props.aggConfigResult).to.be(row[metricColI]);
expect(props.value).to.be(row[metricColI].value);
expect(props.geohash).to.be(row[geoColI].value);
} else {
expect(props.aggConfigResult).to.be(null);
expect(props.value).to.be(row[metricColI]);
expect(props.geohash).to.be(row[geoColI]);
}
});
});
});
describe('geocentroid', function () {
const createEsResponse = function (position = { lat: 37, lon: -122 }) {
esResponse = {
took: 1,
timed_out: false,
_shards: {
total: 4,
successful: 4,
failed: 0
},
hits: {
total: 61005,
max_score: 0.0,
hits: []
},
aggregations: {
2: {
buckets: [{
key: '9q',
doc_count: 10307,
1: {
value: 10307
},
3: {
location: position
}
}]
}
}
};
};
beforeEach(function () {
createEsResponse();
createVis(true);
});
it('should use geocentroid', function () {
const chart = makeSingleChart();
expect(chart.geoJson.features[0].geometry.coordinates).to.eql([ -122, 37 ]);
});
// 9q has latitude boundaries of 33.75 to 39.375
// 9q has longituted boundaries of -123.75 to -112.5
[
{
lat: 30,
lon: -122,
expected: [ -122, 33.75 ]
},
{
lat: 45,
lon: -122,
expected: [ -122, 39.375 ]
},
{
lat: 37,
lon: -130,
expected: [ -123.75, 37 ]
},
{
lat: 37,
lon: -110,
expected: [ -112.5, 37 ]
}
].forEach(function (position) {
it('should clamp geocentroid ' + JSON.stringify(position), function () {
createEsResponse(position);
const chart = makeSingleChart();
expect(chart.geoJson.features[0].geometry.coordinates).to.eql(position.expected);
});
});
});
});
});
});
describe('geoJson tooltip formatter', function () {});
});

View file

@ -1,42 +0,0 @@
import $ from 'jquery';
import _ from 'lodash';
import { fieldFormats } from '../../registry/field_formats';
export function TileMapTooltipFormatterProvider($compile, $rootScope) {
const $tooltipScope = $rootScope.$new();
const $el = $('<div>').html(require('ui/agg_response/geo_json/_tooltip.html'));
$compile($el)($tooltipScope);
return function tooltipFormatter(feature) {
if (!feature) return '';
const value = feature.properties.value;
const acr = feature.properties.aggConfigResult;
const vis = acr.aggConfig.vis;
const metricAgg = acr.aggConfig;
let geoFormat = _.get(vis.getAggConfig(), 'byTypeName.geohash_grid[0].format');
if (!geoFormat) geoFormat = fieldFormats.getDefaultInstance('geo_point');
$tooltipScope.details = [
{
label: metricAgg.makeLabel(),
value: metricAgg.fieldFormatter()(value)
},
{
label: 'Latitude',
value: feature.geometry.coordinates[1]
},
{
label: 'Longitude',
value: feature.geometry.coordinates[0]
}
];
$tooltipScope.$apply();
return $el.html();
};
}

View file

@ -1,49 +0,0 @@
import _ from 'lodash';
import { convertRowsToFeatures } from './rows_to_features';
import { TileMapTooltipFormatterProvider } from './_tooltip_formatter';
import { gridDimensions } from './grid_dimensions';
export function AggResponseGeoJsonProvider(Private) {
const tooltipFormatter = Private(TileMapTooltipFormatterProvider);
return function (vis, table) {
function columnIndex(schema) {
return _.findIndex(table.columns, function (col) {
return col.aggConfig.schema.name === schema;
});
}
const geoI = columnIndex('segment');
const metricI = columnIndex('metric');
const centroidI = _.findIndex(table.columns, (col) => col.aggConfig.type.name === 'geo_centroid');
const geoAgg = _.get(table.columns, [geoI, 'aggConfig']);
const metricAgg = _.get(table.columns, [metricI, 'aggConfig']);
const features = convertRowsToFeatures(table, geoI, metricI, centroidI);
const values = features.map(function (feature) {
return feature.properties.value;
});
return {
title: table.title(),
valueFormatter: metricAgg && metricAgg.fieldFormatter(),
tooltipFormatter: tooltipFormatter,
geohashGridAgg: geoAgg,
geoJson: {
type: 'FeatureCollection',
features: features,
properties: {
min: _.min(values),
max: _.max(values),
zoom: geoAgg && geoAgg.vis.uiStateVal('mapZoom'),
center: geoAgg && geoAgg.vis.uiStateVal('mapCenter'),
geohashPrecision: geoAgg && geoAgg.params.precision,
geohashGridDimensionsAtEquator: geoAgg && gridDimensions(geoAgg.params.precision)
}
}
};
};
}

View file

@ -1 +0,0 @@
export { AggResponseGeoJsonProvider } from './geo_json';

View file

@ -1,69 +0,0 @@
import { decodeGeoHash } from '../../utils/decode_geo_hash';
import AggConfigResult from '../../vis/agg_config_result';
import _ from 'lodash';
function getAcr(val) {
return val instanceof AggConfigResult ? val : null;
}
function unwrap(val) {
return getAcr(val) ? val.value : val;
}
function clampGrid(val, min, max) {
if (val > max) val = max;
else if (val < min) val = min;
return val;
}
export function convertRowsToFeatures(table, geoI, metricI, centroidI) {
return _.transform(table.rows, function (features, row) {
const geohash = unwrap(row[geoI]);
if (!geohash) return;
// fetch latLn of northwest and southeast corners, and center point
const location = decodeGeoHash(geohash);
const centerLatLng = [
location.latitude[2],
location.longitude[2]
];
//courtsey of @JacobBrandt: https://github.com/elastic/kibana/pull/9676/files#diff-c7c9f237e673ff486654f6cc6caa89f6
let point = centerLatLng;
const centroid = unwrap(row[centroidI]);
if (centroid) {
// see https://github.com/elastic/elasticsearch/issues/24694 for why clampGrid is used
point = [
clampGrid(centroid.lat, location.latitude[0], location.latitude[1]),
clampGrid(centroid.lon, location.longitude[0], location.longitude[1])
];
}
// order is nw, ne, se, sw
const rectangle = [
[location.latitude[0], location.longitude[0]],
[location.latitude[0], location.longitude[1]],
[location.latitude[1], location.longitude[1]],
[location.latitude[1], location.longitude[0]],
];
// geoJson coords use LngLat, so we reverse the centerLatLng
// See here for details: http://geojson.org/geojson-spec.html#positions
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: point.slice(0).reverse()
},
properties: {
geohash: geohash,
value: unwrap(row[metricI]),
aggConfigResult: getAcr(row[metricI]),
center: centerLatLng,
rectangle: rectangle
}
});
}, []);
}

View file

@ -1,13 +1,11 @@
import { BuildHierarchicalDataProvider } from './hierarchical/build_hierarchical_data';
import { AggResponsePointSeriesProvider } from './point_series/point_series';
import { tabifyAggResponse } from './tabify/tabify';
import { AggResponseGeoJsonProvider } from './geo_json/geo_json';
export function AggResponseIndexProvider(Private) {
return {
hierarchical: Private(BuildHierarchicalDataProvider),
pointSeries: Private(AggResponsePointSeriesProvider),
tabify: tabifyAggResponse,
geoJson: Private(AggResponseGeoJsonProvider)
tabify: tabifyAggResponse
};
}

View file

@ -0,0 +1,105 @@
import { decodeGeoHash } from 'ui/utils/decode_geo_hash';
import { gridDimensions } from './grid_dimensions';
export function convertToGeoJson(tabifiedResponse) {
let features;
let min = Infinity;
let max = -Infinity;
let geoAgg;
if (tabifiedResponse && tabifiedResponse.tables && tabifiedResponse.tables[0] && tabifiedResponse.tables[0].rows) {
const table = tabifiedResponse.tables[0];
const geohashIndex = table.columns.findIndex(column => column.aggConfig.type.dslName === 'geohash_grid');
geoAgg = table.columns.find(column => column.aggConfig.type.dslName === 'geohash_grid');
if (geohashIndex === -1) {
features = [];
} else {
const metricIndex = table.columns.findIndex(column => column.aggConfig.type.type === 'metrics');
const geocentroidIndex = table.columns.findIndex(column => column.aggConfig.type.dslName === 'geo_centroid');
features = table.rows.map(row => {
const geohash = row[geohashIndex];
const geohashLocation = decodeGeoHash(geohash);
let pointCoordinates;
if (geocentroidIndex > -1) {
const location = row[geocentroidIndex];
pointCoordinates = [location.lon, location.lat];
} else {
pointCoordinates = [geohashLocation.longitude[2], geohashLocation.latitude[2]];
}
const rectangle = [
[geohashLocation.latitude[0], geohashLocation.longitude[0]],
[geohashLocation.latitude[0], geohashLocation.longitude[1]],
[geohashLocation.latitude[1], geohashLocation.longitude[1]],
[geohashLocation.latitude[1], geohashLocation.longitude[0]],
];
const centerLatLng = [
geohashLocation.latitude[2],
geohashLocation.longitude[2]
];
if (geoAgg.aggConfig.params.useGeocentroid) {
// see https://github.com/elastic/elasticsearch/issues/24694 for why clampGrid is used
pointCoordinates[0] = clampGrid(pointCoordinates[0], geohashLocation.longitude[0], geohashLocation.longitude[1]);
pointCoordinates[1] = clampGrid(pointCoordinates[1], geohashLocation.latitude[0], geohashLocation.latitude[1]);
}
const value = row[metricIndex];
min = Math.min(min, value);
max = Math.max(max, value);
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: pointCoordinates
},
properties: {
geohash: geohash,
geohash_meta: {
center: centerLatLng,
rectangle: rectangle
},
value: value
}
};
});
}
} else {
features = [];
}
const featureCollection = {
type: 'FeatureCollection',
features: features
};
return {
featureCollection: featureCollection,
meta: {
min: min,
max: max,
geohashPrecision: geoAgg && geoAgg.aggConfig.params.precision,
geohashGridDimensionsAtEquator: geoAgg && gridDimensions(geoAgg.aggConfig.params.precision)
}
};
}
function clampGrid(val, min, max) {
if (val > max) val = max;
else if (val < min) val = min;
return val;
}

View file

@ -331,7 +331,7 @@ export class KibanaMap extends EventEmitter {
return this._leafletMap.getMaxZoom();
}
getAutoPrecision() {
getGeohashPrecision() {
return zoomToPrecision(this._leafletMap.getZoom(), 12, this._leafletMap.getMaxZoom());
}
@ -667,6 +667,12 @@ export class KibanaMap extends EventEmitter {
visualization.sessionState.mapBounds = this.getUntrimmedBounds();
}
this._leafletMap.on('resize', () => {
visualization.sessionState.mapBounds = this.getUntrimmedBounds();
});
this._leafletMap.on('load', () => {
visualization.sessionState.mapBounds = this.getUntrimmedBounds();
});
this.on('dragend', persistMapStateInUiState);
this.on('zoomend', persistMapStateInUiState);
}

View file

@ -119,7 +119,7 @@ const CourierRequestHandlerProvider = function (Private, courier, timefilter) {
courier.fetch();
} else {
resolve(_.cloneDeep(searchSource.finalResponse));
resolve(searchSource.finalResponse);
}
});
}