Skip to content

Commit

Permalink
[Maps] Enable gridding/clustering/heatmaps for geo_shape fields (#67886)
Browse files Browse the repository at this point in the history
Enables heatmap, clusters, and grid layers for index-patterns with geo_shape field. This feature is only available for Gold+ users.
  • Loading branch information
thomasneirynck authored Jun 8, 2020
1 parent e2f11e9 commit 0189ae5
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { NoIndexPatternCallout } from '../../../components/no_index_pattern_call
import { i18n } from '@kbn/i18n';

import { EuiFormRow, EuiSpacer } from '@elastic/eui';
import { AGGREGATABLE_GEO_FIELD_TYPES, getFieldsWithGeoTileAgg } from '../../../index_pattern_util';
import { getAggregatableGeoFieldTypes, getFieldsWithGeoTileAgg } from '../../../index_pattern_util';
import { RenderAsSelect } from './render_as_select';

export class CreateSourceEditor extends Component {
Expand Down Expand Up @@ -176,7 +176,7 @@ export class CreateSourceEditor extends Component {
placeholder={i18n.translate('xpack.maps.source.esGeoGrid.indexPatternPlaceholder', {
defaultMessage: 'Select index pattern',
})}
fieldTypes={AGGREGATABLE_GEO_FIELD_TYPES}
fieldTypes={getAggregatableGeoFieldTypes()}
onNoIndexPatterns={this._onNoIndexPatterns}
/>
</EuiFormRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { AbstractESAggSource } from '../es_agg_source';
import { DataRequestAbortError } from '../../util/data_request';
import { registerSource } from '../source_registry';
import { makeESBbox } from '../../../elasticsearch_geo_utils';

export const MAX_GEOTILE_LEVEL = 29;

Expand Down Expand Up @@ -146,6 +147,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
registerCancelCallback,
bucketsPerGrid,
isRequestStillActive,
bufferedExtent,
}) {
const gridsPerRequest = Math.floor(DEFAULT_MAX_BUCKETS_LIMIT / bucketsPerGrid);
const aggs = {
Expand All @@ -156,6 +158,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
{
gridSplit: {
geotile_grid: {
bounds: makeESBbox(bufferedExtent),
field: this._descriptor.geoField,
precision,
},
Expand Down Expand Up @@ -234,10 +237,12 @@ export class ESGeoGridSource extends AbstractESAggSource {
precision,
layerName,
registerCancelCallback,
bufferedExtent,
}) {
searchSource.setField('aggs', {
gridSplit: {
geotile_grid: {
bounds: makeESBbox(bufferedExtent),
field: this._descriptor.geoField,
precision,
},
Expand Down Expand Up @@ -282,6 +287,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
precision: searchFilters.geogridPrecision,
layerName,
registerCancelCallback,
bufferedExtent: searchFilters.buffer,
})
: await this._compositeAggRequest({
searchSource,
Expand All @@ -291,6 +297,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
registerCancelCallback,
bucketsPerGrid,
isRequestStillActive,
bufferedExtent: searchFilters.buffer,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';

import { EuiFormRow, EuiCallOut } from '@elastic/eui';
import { AGGREGATABLE_GEO_FIELD_TYPES, getFieldsWithGeoTileAgg } from '../../../index_pattern_util';
import { getFieldsWithGeoTileAgg } from '../../../index_pattern_util';
import { ES_GEO_FIELD_TYPE } from '../../../../common/constants';

export class CreateSourceEditor extends Component {
static propTypes = {
Expand Down Expand Up @@ -177,7 +178,7 @@ export class CreateSourceEditor extends Component {
placeholder={i18n.translate('xpack.maps.source.pewPew.indexPatternPlaceholder', {
defaultMessage: 'Select index pattern',
})}
fieldTypes={AGGREGATABLE_GEO_FIELD_TYPES}
fieldTypes={[ES_GEO_FIELD_TYPE.GEO_POINT]}
/>
</EuiFormRow>
);
Expand Down
45 changes: 21 additions & 24 deletions x-pack/plugins/maps/public/elasticsearch_geo_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,39 +225,36 @@ export function geoShapeToGeometry(value, accumulator) {
accumulator.push(geoJson);
}

function createGeoBoundBoxFilter({ maxLat, maxLon, minLat, minLon }, geoFieldName) {
const top = clampToLatBounds(maxLat);
export function makeESBbox({ maxLat, maxLon, minLat, minLon }) {
const bottom = clampToLatBounds(minLat);

// geo_bounding_box does not support ranges outside of -180 and 180
// When the area crosses the 180° meridian,
// the value of the lower left longitude will be greater than the value of the upper right longitude.
// http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#30
let boundingBox;
const top = clampToLatBounds(maxLat);
let esBbox;
if (maxLon - minLon >= 360) {
boundingBox = {
esBbox = {
top_left: [-180, top],
bottom_right: [180, bottom],
};
} else if (maxLon > 180) {
const overflow = maxLon - 180;
boundingBox = {
top_left: [minLon, top],
bottom_right: [-180 + overflow, bottom],
};
} else if (minLon < -180) {
const overflow = Math.abs(minLon) - 180;
boundingBox = {
top_left: [180 - overflow, top],
bottom_right: [maxLon, bottom],
};
} else {
boundingBox = {
top_left: [minLon, top],
bottom_right: [maxLon, bottom],
// geo_bounding_box does not support ranges outside of -180 and 180
// When the area crosses the 180° meridian,
// the value of the lower left longitude will be greater than the value of the upper right longitude.
// http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#30
//
// This ensures bbox goes West->East in the happy case,
// but will be formatted East->West in case it crosses the date-line
const newMinlon = ((minLon + 180 + 360) % 360) - 180;
const newMaxlon = ((maxLon + 180 + 360) % 360) - 180;
esBbox = {
top_left: [newMinlon, top],
bottom_right: [newMaxlon, bottom],
};
}

return esBbox;
}

function createGeoBoundBoxFilter({ maxLat, maxLon, minLat, minLon }, geoFieldName) {
const boundingBox = makeESBbox({ maxLat, maxLon, minLat, minLon });
return {
geo_bounding_box: {
[geoFieldName]: boundingBox,
Expand Down
93 changes: 93 additions & 0 deletions x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
createExtentFilter,
roundCoordinates,
extractFeaturesFromFilters,
makeESBbox,
} from './elasticsearch_geo_utils';
import { indexPatterns } from '../../../../src/plugins/data/public';

Expand Down Expand Up @@ -594,3 +595,95 @@ describe('extractFeaturesFromFilters', () => {
expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]);
});
});

describe('makeESBbox', () => {
it('Should invert Y-axis', () => {
const bbox = makeESBbox({
minLon: 10,
maxLon: 20,
minLat: 0,
maxLat: 1,
});
expect(bbox).toEqual({ bottom_right: [20, 0], top_left: [10, 1] });
});

it('Should snap to 360 width', () => {
const bbox = makeESBbox({
minLon: 10,
maxLon: 400,
minLat: 0,
maxLat: 1,
});
expect(bbox).toEqual({ bottom_right: [180, 0], top_left: [-180, 1] });
});

it('Should clamp latitudes', () => {
const bbox = makeESBbox({
minLon: 10,
maxLon: 400,
minLat: -100,
maxLat: 100,
});
expect(bbox).toEqual({ bottom_right: [180, -89], top_left: [-180, 89] });
});

it('Should swap West->East orientation to East->West orientation when crossing dateline (West extension)', () => {
const bbox = makeESBbox({
minLon: -190,
maxLon: 20,
minLat: -100,
maxLat: 100,
});
expect(bbox).toEqual({ bottom_right: [20, -89], top_left: [170, 89] });
});

it('Should swap West->East orientation to East->West orientation when crossing dateline (West extension) (overrated)', () => {
const bbox = makeESBbox({
minLon: -190 + 360 + 360,
maxLon: 20 + 360 + 360,
minLat: -100,
maxLat: 100,
});
expect(bbox).toEqual({ bottom_right: [20, -89], top_left: [170, 89] });
});

it('Should swap West->East orientation to East->West orientation when crossing dateline (east extension)', () => {
const bbox = makeESBbox({
minLon: 175,
maxLon: 190,
minLat: -100,
maxLat: 100,
});
expect(bbox).toEqual({ bottom_right: [-170, -89], top_left: [175, 89] });
});

it('Should preserve West->East orientation when _not_ crossing dateline', () => {
const bbox = makeESBbox({
minLon: 20,
maxLon: 170,
minLat: -100,
maxLat: 100,
});
expect(bbox).toEqual({ bottom_right: [170, -89], top_left: [20, 89] });
});

it('Should preserve West->East orientation when _not_ crossing dateline _and_ snap longitudes (west extension)', () => {
const bbox = makeESBbox({
minLon: -190,
maxLon: -185,
minLat: -100,
maxLat: 100,
});
expect(bbox).toEqual({ bottom_right: [175, -89], top_left: [170, 89] });
});

it('Should preserve West->East orientation when _not_ crossing dateline _and_ snap longitudes (east extension)', () => {
const bbox = makeESBbox({
minLon: 185,
maxLon: 190,
minLat: -100,
maxLat: 100,
});
expect(bbox).toEqual({ bottom_right: [-170, -89], top_left: [-175, 89] });
});
});
13 changes: 9 additions & 4 deletions x-pack/plugins/maps/public/index_pattern_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { getIndexPatternService } from './kibana_services';
import { getIndexPatternService, getIsGoldPlus } from './kibana_services';
import { indexPatterns } from '../../../../src/plugins/data/public';
import { ES_GEO_FIELD_TYPE } from '../common/constants';

Expand All @@ -30,19 +30,24 @@ export function getTermsFields(fields) {
});
}

export const AGGREGATABLE_GEO_FIELD_TYPES = [ES_GEO_FIELD_TYPE.GEO_POINT];
export function getAggregatableGeoFieldTypes() {
const aggregatableFieldTypes = [ES_GEO_FIELD_TYPE.GEO_POINT];
if (getIsGoldPlus()) {
aggregatableFieldTypes.push(ES_GEO_FIELD_TYPE.GEO_SHAPE);
}
return aggregatableFieldTypes;
}

export function getFieldsWithGeoTileAgg(fields) {
return fields.filter(supportsGeoTileAgg);
}

export function supportsGeoTileAgg(field) {
// TODO add geo_shape support with license check
return (
field &&
field.aggregatable &&
!indexPatterns.isNestedField(field) &&
field.type === ES_GEO_FIELD_TYPE.GEO_POINT
getAggregatableGeoFieldTypes().includes(field.type)
);
}

Expand Down
81 changes: 80 additions & 1 deletion x-pack/plugins/maps/public/index_pattern_util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

jest.mock('./kibana_services', () => ({}));

import { getSourceFields } from './index_pattern_util';
import {
getSourceFields,
getAggregatableGeoFieldTypes,
supportsGeoTileAgg,
} from './index_pattern_util';
import { ES_GEO_FIELD_TYPE } from '../common/constants';

describe('getSourceFields', () => {
test('Should remove multi fields from field list', () => {
Expand All @@ -27,3 +32,77 @@ describe('getSourceFields', () => {
expect(sourceFields).toEqual([{ name: 'agent' }]);
});
});

describe('Gold+ licensing', () => {
const testStubs = [
{
field: {
type: 'geo_point',
aggregatable: true,
},
supportedInBasic: true,
supportedInGold: true,
},
{
field: {
type: 'geo_shape',
aggregatable: false,
},
supportedInBasic: false,
supportedInGold: false,
},
{
field: {
type: 'geo_shape',
aggregatable: true,
},
supportedInBasic: false,
supportedInGold: true,
},
];

describe('basic license', () => {
beforeEach(() => {
require('./kibana_services').getIsGoldPlus = () => false;
});

describe('getAggregatableGeoFieldTypes', () => {
test('Should only include geo_point fields ', () => {
const aggregatableGeoFieldTypes = getAggregatableGeoFieldTypes();
expect(aggregatableGeoFieldTypes).toEqual([ES_GEO_FIELD_TYPE.GEO_POINT]);
});
});

describe('supportsGeoTileAgg', () => {
testStubs.forEach((stub, index) => {
test(`stub: ${index}`, () => {
const supported = supportsGeoTileAgg(stub.field);
expect(supported).toEqual(stub.supportedInBasic);
});
});
});
});

describe('gold license', () => {
beforeEach(() => {
require('./kibana_services').getIsGoldPlus = () => true;
});
describe('getAggregatableGeoFieldTypes', () => {
test('Should add geo_shape field', () => {
const aggregatableGeoFieldTypes = getAggregatableGeoFieldTypes();
expect(aggregatableGeoFieldTypes).toEqual([
ES_GEO_FIELD_TYPE.GEO_POINT,
ES_GEO_FIELD_TYPE.GEO_SHAPE,
]);
});
});
describe('supportsGeoTileAgg', () => {
testStubs.forEach((stub, index) => {
test(`stub: ${index}`, () => {
const supported = supportsGeoTileAgg(stub.field);
expect(supported).toEqual(stub.supportedInGold);
});
});
});
});
});
2 changes: 2 additions & 0 deletions x-pack/plugins/maps/public/kibana_services.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function getShowMapsInspectorAdapter(): boolean;
export function getPreserveDrawingBuffer(): boolean;
export function getEnableVectorTiles(): boolean;
export function getProxyElasticMapsServiceInMaps(): boolean;
export function getIsGoldPlus(): boolean;

export function setLicenseId(args: unknown): void;
export function setInspector(args: unknown): void;
Expand All @@ -74,3 +75,4 @@ export function setSearchService(args: DataPublicPluginStart['search']): void;
export function setKibanaCommonConfig(config: MapsLegacyConfigType): void;
export function setMapAppConfig(config: MapsConfigType): void;
export function setKibanaVersion(version: string): void;
export function setIsGoldPlus(isGoldPlus: boolean): void;
Loading

0 comments on commit 0189ae5

Please sign in to comment.