From 05b863dfeb5b93dc0d7a788bd0957aba61439b8b Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Sat, 7 Jan 2023 08:11:36 +0800 Subject: [PATCH] feat: query with geo bounding box (#148) * feat: allow user to enable geo bounding box query on layers - allow user to turn on/off geo bounding box query display a switch in map filter config panel to allow user to switch on/off geo bounding box query - search requests will only be sent for layers which have geo bounding query enabled - geo bounding box query will applied global filters Bug fixes: - fixed wrong maplibre instance passed to addTooltip() function which results in a runtime error - fixed empty onHover popup rendered on layer which has tooltip disabled Signed-off-by: Yulong Ruan * fix: ts type error Signed-off-by: Yulong Ruan Signed-off-by: Yulong Ruan Co-authored-by: Junqiu Lei --- .../document_layer_source.tsx | 22 ++++++- .../map_container/map_container.tsx | 43 ++++++++++++-- .../components/tooltip/create_tooltip.tsx | 6 +- .../public/model/layerRenderController.ts | 59 +++++++++++++++++-- maps_dashboards/public/model/mapLayerType.ts | 1 + 5 files changed, 121 insertions(+), 10 deletions(-) diff --git a/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx b/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx index be518b5b..905252cb 100644 --- a/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx @@ -193,6 +193,11 @@ export const DocumentLayerSource = ({ setSelectedLayerConfig({ ...selectedLayerConfig, source }); }; + const onToggleGeoBoundingBox = (e: React.ChangeEvent) => { + const source = { ...selectedLayerConfig.source, useGeoBoundingBoxFilter: e.target.checked }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + const shouldTooltipSectionOpen = () => { return ( selectedLayerConfig.source.showTooltips && @@ -200,6 +205,10 @@ export const DocumentLayerSource = ({ ); }; + const filterPanelInitialIsOpen = + selectedLayerConfig.source.filters?.length > 0 || + selectedLayerConfig.source.useGeoBoundingBoxFilter; + return (
@@ -281,7 +290,7 @@ export const DocumentLayerSource = ({ title="Filters" titleSize="xxs" isCollapsible={true} - initialIsOpen={selectedLayerConfig.source.filters?.length > 0} + initialIsOpen={filterPanelInitialIsOpen} > + + + + diff --git a/maps_dashboards/public/components/map_container/map_container.tsx b/maps_dashboards/public/components/map_container/map_container.tsx index 63261a42..cbad568b 100644 --- a/maps_dashboards/public/components/map_container/map_container.tsx +++ b/maps_dashboards/public/components/map_container/map_container.tsx @@ -5,14 +5,18 @@ import React, { useEffect, useRef, useState } from 'react'; import { EuiPanel } from '@elastic/eui'; -import { LngLat, Map as Maplibre, MapMouseEvent, NavigationControl, Popup } from 'maplibre-gl'; +import { LngLat, Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; +import { debounce } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; -import { MAP_INITIAL_STATE, MAP_GLYPHS } from '../../../common'; +import { MAP_INITIAL_STATE, MAP_GLYPHS, DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { IndexPattern } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { createPopup, getPopupLngLat, isTooltipEnabledLayer } from '../tooltip/create_tooltip'; +import { handleDataLayerRender } from '../../model/layerRenderController'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -31,6 +35,7 @@ export const MapContainer = ({ maplibreRef, mapState, }: MapContainerProps) => { + const { services } = useOpenSearchDashboards(); const mapContainer = useRef(null); const [mounted, setMounted] = useState(false); const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); @@ -62,6 +67,7 @@ export const MapContainer = ({ }); }, []); + // Create onClick tooltip for each layer features that has tooltip enabled useEffect(() => { let clickPopup: Popup | null = null; let hoverPopup: Popup | null = null; @@ -69,7 +75,7 @@ export const MapContainer = ({ // We don't want to show layer information in the popup for the map tile layer const tooltipEnabledLayers = layers.filter(isTooltipEnabledLayer); - function onClickMap(e: MapMouseEvent) { + function onClickMap(e: MapEventType['click']) { // remove previous popup clickPopup?.remove(); @@ -82,7 +88,7 @@ export const MapContainer = ({ } } - function onMouseMoveMap(e: MapMouseEvent) { + function onMouseMoveMap(e: MapEventType['mousemove']) { setCoordinates(e.lngLat.wrap()); // remove previous popup @@ -126,6 +132,35 @@ export const MapContainer = ({ }; }, [layers]); + // Handle map bounding box change, it should update the search if "request data around map extent" was enabled + useEffect(() => { + function renderLayers() { + layers.forEach((layer: MapLayerSpecification) => { + // We don't send search query if the layer doesn't have "request data around map extent" enabled + if ( + layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS && + layer.source.useGeoBoundingBoxFilter + ) { + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); + } + }); + } + + // Rerender layers with 200ms debounce to avoid calling the search API too frequently, especially when + // resizing the window, the "moveend" event could be fired constantly + const debouncedRenderLayers = debounce(renderLayers, 200); + + if (maplibreRef.current) { + maplibreRef.current.on('moveend', debouncedRenderLayers); + } + + return () => { + if (maplibreRef.current) { + maplibreRef.current.off('moveend', debouncedRenderLayers); + } + }; + }, [layers, mapState, services]); + return (
diff --git a/maps_dashboards/public/components/tooltip/create_tooltip.tsx b/maps_dashboards/public/components/tooltip/create_tooltip.tsx index e77bd0ca..7d86fc66 100644 --- a/maps_dashboards/public/components/tooltip/create_tooltip.tsx +++ b/maps_dashboards/public/components/tooltip/create_tooltip.tsx @@ -16,7 +16,11 @@ type Options = { export function isTooltipEnabledLayer( layer: MapLayerSpecification ): layer is DocumentLayerSpecification { - return layer.type !== 'opensearch_vector_tile_map' && layer.source.showTooltips === true; + return ( + layer.type !== 'opensearch_vector_tile_map' && + layer.type !== 'custom_map' && + layer.source.showTooltips === true + ); } export function groupFeaturesByLayers( diff --git a/maps_dashboards/public/model/layerRenderController.ts b/maps_dashboards/public/model/layerRenderController.ts index 5e710d17..2f34cde3 100644 --- a/maps_dashboards/public/model/layerRenderController.ts +++ b/maps_dashboards/public/model/layerRenderController.ts @@ -4,10 +4,12 @@ */ import { Map as Maplibre } from 'maplibre-gl'; -import { MapLayerSpecification } from './mapLayerType'; +import { DocumentLayerSpecification, MapLayerSpecification } from './mapLayerType'; import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; import { buildOpenSearchQuery, + Filter, + GeoBoundingBoxFilter, getTime, IOpenSearchDashboardsSearchResponse, isCompleteResponse, @@ -20,10 +22,23 @@ interface MaplibreRef { current: Maplibre | null; } +// OpenSearch only accepts longitude in range [-180, 180] +// Maplibre could return value out of the range +function adjustLongitudeForSearch(lon: number) { + if (lon < -180) { + return -180; + } + if (lon > 180) { + return 180; + } + return lon; +} + export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, - { data, notifications }: MapServices + { data, notifications }: MapServices, + filters: Filter[] = [] ): Promise => { return new Promise(async (resolve, reject) => { if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { @@ -42,6 +57,7 @@ export const prepareDataLayerSource = ( indexPattern, [], [ + ...filters, ...(layer.source.filters ? layer.source.filters : []), ...(timeFilters ? [timeFilters] : []), ] @@ -79,13 +95,48 @@ export const prepareDataLayerSource = ( }; export const handleDataLayerRender = ( - mapLayer: MapLayerSpecification, + mapLayer: DocumentLayerSpecification, mapState: MapState, services: MapServices, maplibreRef: MaplibreRef, beforeLayerId: string | undefined ) => { - return prepareDataLayerSource(mapLayer, mapState, services).then((result) => { + const filters: Filter[] = []; + const geoField = mapLayer.source.geoFieldName; + 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 filterBoundingBox = { + bottom_right: { + lon: adjustLongitudeForSearch(mapBounds.getSouthEast().lng), + lat: mapBounds.getSouthEast().lat, + }, + top_left: { + lon: adjustLongitudeForSearch(mapBounds.getNorthWest().lng), + lat: mapBounds.getNorthWest().lat, + }, + }; + const geoBoundingBoxFilter: GeoBoundingBoxFilter = { + meta: { + disabled: false, + negate: false, + alias: null, + params: filterBoundingBox, + }, + geo_bounding_box: { + [geoField]: filterBoundingBox, + }, + }; + filters.push(geoBoundingBoxFilter); + } + + return prepareDataLayerSource(mapLayer, mapState, services, filters).then((result) => { const { layer, dataSource } = result; if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); diff --git a/maps_dashboards/public/model/mapLayerType.ts b/maps_dashboards/public/model/mapLayerType.ts index e792c2f4..0d558103 100644 --- a/maps_dashboards/public/model/mapLayerType.ts +++ b/maps_dashboards/public/model/mapLayerType.ts @@ -43,6 +43,7 @@ export type DocumentLayerSpecification = { documentRequestNumber: number; showTooltips: boolean; tooltipFields: string[]; + useGeoBoundingBoxFilter: boolean; filters: Filter[]; }; style: {