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.
|
@ -87,6 +87,5 @@ VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmaps
|
|||
])
|
||||
},
|
||||
responseHandler: 'tabify'
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 () {
|
|||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 43 KiB |
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 () {});
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { AggResponseGeoJsonProvider } from './geo_json';
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
105
src/ui/public/vis/map/convert_to_geojson.js
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ const CourierRequestHandlerProvider = function (Private, courier, timefilter) {
|
|||
|
||||
courier.fetch();
|
||||
} else {
|
||||
resolve(_.cloneDeep(searchSource.finalResponse));
|
||||
resolve(searchSource.finalResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|