[Maps] remove dateline check for geo_shape queries, split geo_bounding_box queries that cross dateline into 2 boxes (#64598)

* remove dateline check for geo_shape queries

* fix jest test

* split bounding box

* replace convertMapExtentToPolygon with formatEnvelopeAsPolygon

* clamp latitudes

* use clampToLatBounds

* use single box where left lon is greater then right lon when crossing 180 meridian

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-05-11 08:50:20 -06:00 committed by GitHub
parent 7526db98e8
commit 431a7eb07a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 165 additions and 221 deletions

View file

@ -19,7 +19,7 @@ import {
} from '../../selectors/map_selectors';
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors';
import { convertMapExtentToPolygon } from '../../elasticsearch_geo_utils';
import { formatEnvelopeAsPolygon } from '../../elasticsearch_geo_utils';
import { copyPersistentState } from '../../reducers/util';
import { extractReferences, injectReferences } from '../../../common/migrations/references';
@ -107,7 +107,7 @@ export function createSavedGisMapClass(services) {
openTOCDetails: getOpenTOCDetails(state),
});
this.bounds = convertMapExtentToPolygon(getMapExtent(state));
this.bounds = formatEnvelopeAsPolygon(getMapExtent(state));
}
}
return SavedGisMap;

View file

@ -225,41 +225,62 @@ export function geoShapeToGeometry(value, accumulator) {
accumulator.push(geoJson);
}
function createGeoBoundBoxFilter(geometry, geoFieldName, filterProps = {}) {
ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON]);
function createGeoBoundBoxFilter({ maxLat, maxLon, minLat, minLon }, geoFieldName) {
const top = clampToLatBounds(maxLat);
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;
if (maxLon - minLon >= 360) {
boundingBox = {
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],
};
}
const TOP_LEFT_INDEX = 0;
const BOTTOM_RIGHT_INDEX = 2;
const verticies = geometry.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX];
return {
geo_bounding_box: {
[geoFieldName]: {
top_left: verticies[TOP_LEFT_INDEX],
bottom_right: verticies[BOTTOM_RIGHT_INDEX],
},
[geoFieldName]: boundingBox,
},
...filterProps,
};
}
export function createExtentFilter(mapExtent, geoFieldName, geoFieldType) {
ensureGeoField(geoFieldType);
const safePolygon = convertMapExtentToPolygon(mapExtent);
// Extent filters are used to dynamically filter data for the current map view port.
// Continue to use geo_bounding_box queries for extent filters
// 1) geo_bounding_box queries are faster than polygon queries
// 2) geo_shape benefits of pre-indexed shapes and
// compatability across multi-indices with geo_point and geo_shape do not apply to this use case.
if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) {
return createGeoBoundBoxFilter(safePolygon, geoFieldName);
return createGeoBoundBoxFilter(mapExtent, geoFieldName);
}
return {
geo_shape: {
[geoFieldName]: {
shape: safePolygon,
shape: formatEnvelopeAsPolygon(mapExtent),
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
},
},
@ -376,16 +397,16 @@ export function getBoundingBoxGeometry(geometry) {
extent.maxLat = Math.max(exterior[i][LAT_INDEX], extent.maxLat);
}
return convertMapExtentToPolygon(extent);
return formatEnvelopeAsPolygon(extent);
}
function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) {
export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) {
// GeoJSON mandates that the outer polygon must be counterclockwise to avoid ambiguous polygons
// when the shape crosses the dateline
const left = minLon;
const right = maxLon;
const top = maxLat > 90 ? 90 : maxLat;
const bottom = minLat < -90 ? -90 : minLat;
const top = clampToLatBounds(maxLat);
const bottom = clampToLatBounds(minLat);
const topLeft = [left, top];
const bottomLeft = [left, bottom];
const bottomRight = [right, bottom];
@ -396,45 +417,6 @@ function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) {
};
}
/*
* Convert map bounds to polygon
*/
export function convertMapExtentToPolygon({ maxLat, maxLon, minLat, minLon }) {
const lonDelta = maxLon - minLon;
if (lonDelta >= 360) {
return formatEnvelopeAsPolygon({
maxLat,
maxLon: 180,
minLat,
minLon: -180,
});
}
if (maxLon > 180) {
// bounds cross dateline east to west
const overlapWestOfDateLine = maxLon - 180;
return formatEnvelopeAsPolygon({
maxLat,
maxLon: -180 + overlapWestOfDateLine,
minLat,
minLon,
});
}
if (minLon < -180) {
// bounds cross dateline west to east
const overlapEastOfDateLine = Math.abs(minLon) - 180;
return formatEnvelopeAsPolygon({
maxLat,
maxLon,
minLat,
minLon: 180 - overlapEastOfDateLine,
});
}
return formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon });
}
export function clampToLatBounds(lat) {
return clamp(lat, -89, 89);
}

View file

@ -17,19 +17,12 @@ import {
geoPointToGeometry,
geoShapeToGeometry,
createExtentFilter,
convertMapExtentToPolygon,
roundCoordinates,
extractFeaturesFromFilters,
} from './elasticsearch_geo_utils';
import { indexPatterns } from '../../../../src/plugins/data/public';
const geoFieldName = 'location';
const mapExtent = {
maxLat: 39,
maxLon: -83,
minLat: 35,
minLon: -89,
};
const flattenHitMock = hit => {
const properties = {};
@ -317,174 +310,143 @@ describe('geoShapeToGeometry', () => {
});
describe('createExtentFilter', () => {
it('should return elasticsearch geo_bounding_box filter for geo_point field', () => {
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point');
expect(filter).toEqual({
geo_bounding_box: {
location: {
bottom_right: [-83, 35],
top_left: [-89, 39],
},
},
});
});
it('should return elasticsearch geo_shape filter for geo_shape field', () => {
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape');
expect(filter).toEqual({
geo_shape: {
location: {
relation: 'INTERSECTS',
shape: {
coordinates: [
[
[-89, 39],
[-89, 35],
[-83, 35],
[-83, 39],
[-89, 39],
],
],
type: 'Polygon',
describe('geo_point field', () => {
it('should return elasticsearch geo_bounding_box filter for geo_point field', () => {
const mapExtent = {
maxLat: 39,
maxLon: -83,
minLat: 35,
minLon: -89,
};
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point');
expect(filter).toEqual({
geo_bounding_box: {
location: {
top_left: [-89, 39],
bottom_right: [-83, 35],
},
},
},
});
});
});
it('should clamp longitudes to -180 to 180', () => {
const mapExtent = {
maxLat: 39,
maxLon: 209,
minLat: 35,
minLon: -191,
};
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape');
expect(filter).toEqual({
geo_shape: {
location: {
relation: 'INTERSECTS',
shape: {
coordinates: [
[
[-180, 39],
[-180, 35],
[180, 35],
[180, 39],
[-180, 39],
],
],
type: 'Polygon',
it('should clamp longitudes to -180 to 180 and latitudes to -90 to 90', () => {
const mapExtent = {
maxLat: 120,
maxLon: 200,
minLat: -100,
minLon: -190,
};
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point');
expect(filter).toEqual({
geo_bounding_box: {
location: {
top_left: [-180, 89],
bottom_right: [180, -89],
},
},
},
});
});
});
});
describe('convertMapExtentToPolygon', () => {
it('should convert bounds to envelope', () => {
const bounds = {
maxLat: 10,
maxLon: 100,
minLat: -10,
minLon: 90,
};
expect(convertMapExtentToPolygon(bounds)).toEqual({
type: 'Polygon',
coordinates: [
[
[90, 10],
[90, -10],
[100, -10],
[100, 10],
[90, 10],
],
],
it('should make left longitude greater then right longitude when area crosses 180 meridian east to west', () => {
const mapExtent = {
maxLat: 39,
maxLon: 200,
minLat: 35,
minLon: 100,
};
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point');
const leftLon = filter.geo_bounding_box.location.top_left[0];
const rightLon = filter.geo_bounding_box.location.bottom_right[0];
expect(leftLon).toBeGreaterThan(rightLon);
expect(filter).toEqual({
geo_bounding_box: {
location: {
top_left: [100, 39],
bottom_right: [-160, 35],
},
},
});
});
it('should make left longitude greater then right longitude when area crosses 180 meridian west to east', () => {
const mapExtent = {
maxLat: 39,
maxLon: -100,
minLat: 35,
minLon: -200,
};
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point');
const leftLon = filter.geo_bounding_box.location.top_left[0];
const rightLon = filter.geo_bounding_box.location.bottom_right[0];
expect(leftLon).toBeGreaterThan(rightLon);
expect(filter).toEqual({
geo_bounding_box: {
location: {
top_left: [160, 39],
bottom_right: [-100, 35],
},
},
});
});
});
it('should clamp longitudes to -180 to 180', () => {
const bounds = {
maxLat: 10,
maxLon: 200,
minLat: -10,
minLon: -400,
};
expect(convertMapExtentToPolygon(bounds)).toEqual({
type: 'Polygon',
coordinates: [
[
[-180, 10],
[-180, -10],
[180, -10],
[180, 10],
[-180, 10],
],
],
describe('geo_shape field', () => {
it('should return elasticsearch geo_shape filter', () => {
const mapExtent = {
maxLat: 39,
maxLon: -83,
minLat: 35,
minLon: -89,
};
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape');
expect(filter).toEqual({
geo_shape: {
location: {
relation: 'INTERSECTS',
shape: {
coordinates: [
[
[-89, 39],
[-89, 35],
[-83, 35],
[-83, 39],
[-89, 39],
],
],
type: 'Polygon',
},
},
},
});
});
});
it('should clamp longitudes to -180 to 180 when bounds span entire globe (360)', () => {
const bounds = {
maxLat: 10,
maxLon: 170,
minLat: -10,
minLon: -400,
};
expect(convertMapExtentToPolygon(bounds)).toEqual({
type: 'Polygon',
coordinates: [
[
[-180, 10],
[-180, -10],
[180, -10],
[180, 10],
[-180, 10],
],
],
});
});
it('should handle bounds that cross dateline(east to west)', () => {
const bounds = {
maxLat: 10,
maxLon: 190,
minLat: -10,
minLon: 170,
};
expect(convertMapExtentToPolygon(bounds)).toEqual({
type: 'Polygon',
coordinates: [
[
[170, 10],
[170, -10],
[-170, -10],
[-170, 10],
[170, 10],
],
],
});
});
it('should handle bounds that cross dateline(west to east)', () => {
const bounds = {
maxLat: 10,
maxLon: -170,
minLat: -10,
minLon: -190,
};
expect(convertMapExtentToPolygon(bounds)).toEqual({
type: 'Polygon',
coordinates: [
[
[170, 10],
[170, -10],
[-170, -10],
[-170, 10],
[170, 10],
],
],
it('should not clamp longitudes to -180 to 180', () => {
const mapExtent = {
maxLat: 39,
maxLon: 209,
minLat: 35,
minLon: -191,
};
const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape');
expect(filter).toEqual({
geo_shape: {
location: {
relation: 'INTERSECTS',
shape: {
coordinates: [
[
[-191, 39],
[-191, 35],
[209, 35],
[209, 39],
[-191, 39],
],
],
type: 'Polygon',
},
},
},
});
});
});
});