Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Maps] Enable gridding/clustering/heatmaps for geo_shape fields #67886

Merged
merged 21 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
thomasneirynck marked this conversation as resolved.
Show resolved Hide resolved
})
: 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] });
thomasneirynck marked this conversation as resolved.
Show resolved Hide resolved
});

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