Skip to content

Commit

Permalink
feat: query with geo bounding box (#148)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* fix: ts type error

Signed-off-by: Yulong Ruan <[email protected]>

Signed-off-by: Yulong Ruan <[email protected]>
Co-authored-by: Junqiu Lei <[email protected]>
  • Loading branch information
ruanyl and junqiu-lei authored Jan 7, 2023
1 parent b46e7f4 commit 05b863d
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,22 @@ export const DocumentLayerSource = ({
setSelectedLayerConfig({ ...selectedLayerConfig, source });
};

const onToggleGeoBoundingBox = (e: React.ChangeEvent<HTMLInputElement>) => {
const source = { ...selectedLayerConfig.source, useGeoBoundingBoxFilter: e.target.checked };
setSelectedLayerConfig({ ...selectedLayerConfig, source });
};

const shouldTooltipSectionOpen = () => {
return (
selectedLayerConfig.source.showTooltips &&
selectedLayerConfig.source.tooltipFields?.length > 0
);
};

const filterPanelInitialIsOpen =
selectedLayerConfig.source.filters?.length > 0 ||
selectedLayerConfig.source.useGeoBoundingBoxFilter;

return (
<div>
<EuiPanel paddingSize="s">
Expand Down Expand Up @@ -281,7 +290,7 @@ export const DocumentLayerSource = ({
title="Filters"
titleSize="xxs"
isCollapsible={true}
initialIsOpen={selectedLayerConfig.source.filters?.length > 0}
initialIsOpen={filterPanelInitialIsOpen}
>
<SearchBar
appName="maps-dashboards"
Expand All @@ -290,6 +299,17 @@ export const DocumentLayerSource = ({
filters={selectedLayerConfig.source.filters ?? []}
onFiltersUpdated={onFiltersUpdated}
/>
<EuiSpacer />
<EuiFormRow>
<EuiCheckbox
id={`${selectedLayerConfig.id}-bounding-box-filter`}
disabled={selectedLayerConfig.source.geoFieldType !== 'geo_point'}
label={'Only request data around map extent'}
checked={selectedLayerConfig.source.useGeoBoundingBoxFilter ? true : false}
onChange={onToggleGeoBoundingBox}
compressed
/>
</EuiFormRow>
</EuiCollapsibleNavGroup>
</EuiPanel>
<EuiSpacer size="m" />
Expand Down
43 changes: 39 additions & 4 deletions maps_dashboards/public/components/map_container/map_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +35,7 @@ export const MapContainer = ({
maplibreRef,
mapState,
}: MapContainerProps) => {
const { services } = useOpenSearchDashboards<MapServices>();
const mapContainer = useRef(null);
const [mounted, setMounted] = useState(false);
const [zoom, setZoom] = useState<number>(MAP_INITIAL_STATE.zoom);
Expand Down Expand Up @@ -62,14 +67,15 @@ 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;

// 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();

Expand All @@ -82,7 +88,7 @@ export const MapContainer = ({
}
}

function onMouseMoveMap(e: MapMouseEvent) {
function onMouseMoveMap(e: MapEventType['mousemove']) {
setCoordinates(e.lngLat.wrap());

// remove previous popup
Expand Down Expand Up @@ -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 (
<div>
<EuiPanel hasShadow={false} hasBorder={false} color="transparent" className="zoombar">
Expand Down
6 changes: 5 additions & 1 deletion maps_dashboards/public/components/tooltip/create_tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
59 changes: 55 additions & 4 deletions maps_dashboards/public/model/layerRenderController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<any> => {
return new Promise(async (resolve, reject) => {
if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) {
Expand All @@ -42,6 +57,7 @@ export const prepareDataLayerSource = (
indexPattern,
[],
[
...filters,
...(layer.source.filters ? layer.source.filters : []),
...(timeFilters ? [timeFilters] : []),
]
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions maps_dashboards/public/model/mapLayerType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type DocumentLayerSpecification = {
documentRequestNumber: number;
showTooltips: boolean;
tooltipFields: string[];
useGeoBoundingBoxFilter: boolean;
filters: Filter[];
};
style: {
Expand Down

0 comments on commit 05b863d

Please sign in to comment.