diff --git a/public/model/geo/filter.test.ts b/public/model/geo/filter.test.ts new file mode 100644 index 00000000..d35ca6c8 --- /dev/null +++ b/public/model/geo/filter.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LngLat } from 'maplibre-gl'; +import { GeoBounds } from '../map/boundary'; +import { FilterMeta, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; +import { buildBBoxFilter } from './filter'; + +describe('test bounding box filter', function () { + it('should return valid bounding box', function () { + const mockBounds: GeoBounds = { + bottomRight: new LngLat(-2.340000000000032, 27.67), + topLeft: new LngLat(-135.18, 71.01), + }; + const mockFilterMeta: FilterMeta = { + alias: null, + disabled: true, + negate: false, + }; + const actualGeoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter( + 'field-name', + mockBounds, + mockFilterMeta + ); + const expectedBounds = { + bottom_right: { + lat: mockBounds.bottomRight.lat, + lon: mockBounds.bottomRight.lng, + }, + top_left: { + lat: mockBounds.topLeft.lat, + lon: mockBounds.topLeft.lng, + }, + }; + expect(actualGeoBoundingBoxFilter.geo_bounding_box).toEqual({ + ['field-name']: expectedBounds, + }); + expect(actualGeoBoundingBoxFilter.meta.params).toEqual(expectedBounds); + }); +}); diff --git a/public/model/geo/filter.ts b/public/model/geo/filter.ts new file mode 100644 index 00000000..b41f219c --- /dev/null +++ b/public/model/geo/filter.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LatLon } from '@opensearch-project/opensearch/api/types'; +import { FilterMeta, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; +import { GeoBounds } from '../map/boundary'; + +export const buildBBoxFilter = ( + fieldName: string, + mapBounds: GeoBounds, + filterMeta: FilterMeta +): GeoBoundingBoxFilter => { + const bottomRight: LatLon = { + lon: mapBounds.bottomRight.lng, + lat: mapBounds.bottomRight.lat, + }; + + const topLeft: LatLon = { + lon: mapBounds.topLeft.lng, + lat: mapBounds.topLeft.lat, + }; + + const boundingBox = { + bottom_right: bottomRight, + top_left: topLeft, + }; + return { + meta: { + ...filterMeta, + params: boundingBox, + }, + geo_bounding_box: { + [fieldName]: boundingBox, + }, + }; +}; diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 98802cf4..7c11bb2d 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { LngLatBounds, Map as Maplibre } from 'maplibre-gl'; +import { Map as Maplibre } from 'maplibre-gl'; import { DocumentLayerSpecification, MapLayerSpecification } from './mapLayerType'; -import { DASHBOARDS_MAPS_LAYER_TYPE, MAX_LONGITUDE, MIN_LONGITUDE } from '../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; import { buildOpenSearchQuery, Filter, + FilterMeta, GeoBoundingBoxFilter, getTime, IOpenSearchDashboardsSearchResponse, @@ -17,34 +18,13 @@ import { import { layersFunctionMap } from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; +import {GeoBounds, getBounds} from './map/boundary'; +import { buildBBoxFilter } from './geo/filter'; interface MaplibreRef { current: Maplibre | null; } -// calculate lng limits based on map bounds -// maps can render more than 1 copies of map at lower zoom level and displays -// one side from 1 copy and other side from other copy at higher zoom level if -// screen crosses internation dateline -function calculateBoundingBoxLngLimit(bounds: LngLatBounds) { - const boundsMinLng = bounds.getNorthWest().lng; - const boundsMaxLng = bounds.getSouthEast().lng; - // if bounds expands more than 360 then, consider complete globe is visible - if (boundsMaxLng - boundsMinLng >= MAX_LONGITUDE - MIN_LONGITUDE) { - return { - right: MAX_LONGITUDE, - left: MIN_LONGITUDE, - }; - } - // wrap bounds if only portion of globe is visible - // wrap() returns a new LngLat object whose longitude is - // wrapped to the range (-180, 180). - return { - right: bounds.getSouthEast().wrap().lng, - left: bounds.getNorthWest().wrap().lng, - }; -} - export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, @@ -117,36 +97,14 @@ export const handleDataLayerRender = ( const geoFieldType = mapLayer.source.geoFieldType; // geo bounding box query supports geo_point fields - if ( - geoFieldType === 'geo_point' && - mapLayer.source.useGeoBoundingBoxFilter && - maplibreRef.current - ) { - const mapBounds = maplibreRef.current.getBounds(); - const lngLimit = calculateBoundingBoxLngLimit(mapBounds); - const filterBoundingBox = { - bottom_right: { - lon: lngLimit.right, - lat: mapBounds.getSouthEast().lat, - }, - top_left: { - lon: lngLimit.left, - lat: mapBounds.getNorthWest().lat, - }, - }; - const geoBoundingBoxFilter: GeoBoundingBoxFilter = { - meta: { - disabled: false, - negate: false, - alias: null, - params: filterBoundingBox, - }, - geo_bounding_box: { - [geoField]: filterBoundingBox, - }, - }; - filters.push(geoBoundingBoxFilter); - } + const mapBounds: GeoBounds = getBounds(maplibreRef.current!); + const meta: FilterMeta = { + alias: null, + disabled: !mapLayer.source.useGeoBoundingBoxFilter || geoFieldType !== 'geo_point', + negate: false, + }; + const geoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter(geoField, mapBounds, meta); + filters.push(geoBoundingBoxFilter); return prepareDataLayerSource(mapLayer, mapState, services, filters).then((result) => { const { layer, dataSource } = result; diff --git a/public/model/map/__mocks__/map.ts b/public/model/map/__mocks__/map.ts index 1f662def..df965093 100644 --- a/public/model/map/__mocks__/map.ts +++ b/public/model/map/__mocks__/map.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { LayerSpecification } from 'maplibre-gl'; +import { LayerSpecification, LngLatBounds } from 'maplibre-gl'; import { MockLayer } from './layer'; export type Source = any; @@ -14,11 +14,16 @@ export class MockMaplibreMap { sources: Map; }; - constructor(layers: MockLayer[]) { + private _bounds: LngLatBounds; + + constructor(layers: MockLayer[], bounds?: LngLatBounds) { this._styles = { layers: new Array(...layers), sources: new Map(), }; + if (bounds) { + this._bounds = bounds; + } } public addSource(sourceId: string, source: Source) { @@ -109,4 +114,12 @@ export class MockMaplibreMap { (layer) => !(layer.getProperty('id') as string).includes(layerId) ); } + + setBounds = (bounds: LngLatBounds) => { + this._bounds = bounds; + }; + + getBounds = (): LngLatBounds => { + return this._bounds; + }; } diff --git a/public/model/map/bondary.test.ts b/public/model/map/bondary.test.ts new file mode 100644 index 00000000..39513a5a --- /dev/null +++ b/public/model/map/bondary.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LngLat, LngLatBounds, Map as Maplibre } from 'maplibre-gl'; +import { MockMaplibreMap } from './__mocks__/map'; +import { GeoBounds, getBounds } from './boundary'; + +describe('verify get bounds', function () { + it('should cover complete map if more than on copy is visible', function () { + const ne: LngLat = new LngLat(333.811, 82.8); + const sw: LngLat = new LngLat(-248.8, -79.75); + const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); + const expectedBounds: GeoBounds = { + bottomRight: new LngLat(180, -79.75), + topLeft: new LngLat(-180, 82.8), + }; + expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); + }); + + it('should wrap if map crosses international date line', function () { + const ne: LngLat = new LngLat(11.56, 80.85); + const sw: LngLat = new LngLat(-220.77, 21.52); + const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); + const expectedBounds: GeoBounds = { + bottomRight: new LngLat(11.559999999999945, 21.52), + topLeft: new LngLat(139.23000000000002, 80.85), + }; + expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); + }); + it('should give same value as map bounds for other cases', function () { + const sw: LngLat = new LngLat(-135.18, 27.67); + const ne: LngLat = new LngLat(-2.34, 71.01); + const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); + const expectedBounds: GeoBounds = { + bottomRight: new LngLat(-2.340000000000032, 27.67), + topLeft: new LngLat(-135.18, 71.01), + }; + expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); + }); +}); diff --git a/public/model/map/boundary.ts b/public/model/map/boundary.ts new file mode 100644 index 00000000..3b272429 --- /dev/null +++ b/public/model/map/boundary.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LngLat, LngLatBounds, Map as Maplibre } from 'maplibre-gl'; +import { MAX_LONGITUDE, MIN_LONGITUDE } from '../../../common'; + +export interface GeoBounds { + bottomRight: LngLat; + topLeft: LngLat; +} + +// calculate lng limits based on map bounds +// maps can render more than 1 copies of map at lower zoom level and displays +// one side from 1 copy and other side from other copy at higher zoom level if +// screen crosses internation dateline +export function getBounds(map: Maplibre): GeoBounds { + const mapBounds: LngLatBounds = map.getBounds(); + const boundsMinLng: number = mapBounds.getNorthWest().lng; + const boundsMaxLng: number = mapBounds.getSouthEast().lng; + + let right: number; + let left: number; + // if bounds expands more than 360 then, consider complete globe is visible + if (boundsMaxLng - boundsMinLng >= MAX_LONGITUDE - MIN_LONGITUDE) { + right = MAX_LONGITUDE; + left = MIN_LONGITUDE; + } else { + // wrap bounds if only portion of globe is visible + // wrap() returns a new LngLat object whose longitude is + // wrapped to the range (-180, 180). + right = mapBounds.getSouthEast().wrap().lng; + left = mapBounds.getNorthWest().wrap().lng; + } + return { + bottomRight: new LngLat(right, mapBounds.getSouthEast().lat), + topLeft: new LngLat(left, mapBounds.getNorthWest().lat), + }; +} diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 14ffc02e..53456619 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -7,10 +7,13 @@ import { addLineLayer, addPolygonLayer, getLayers, - hasLayer, moveLayers, removeLayers, + hasLayer, + moveLayers, + removeLayers, updateCircleLayer, updateLineLayer, - updatePolygonLayer, updateLayerVisibility, + updatePolygonLayer, + updateLayerVisibility, } from './layer_operations'; import { Map as Maplibre } from 'maplibre-gl'; import { MockMaplibreMap } from './__mocks__/map'; diff --git a/test/setup.jest.js b/test/setup.jest.js index e5fbbdf9..8bcca072 100644 --- a/test/setup.jest.js +++ b/test/setup.jest.js @@ -34,3 +34,4 @@ beforeEach(() => { afterEach(() => { console.error.mockRestore(); }); +window.URL.createObjectURL = function () {};