/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ import React, { useEffect, useRef, useState } from 'react'; import { Map as Maplibre, NavigationControl } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; import { DrawFilterProperties, FILTER_DRAW_MODE, MAP_INITIAL_STATE } from '../../../common'; import { DataLayerSpecification, MapLayerSpecification } from '../../model/mapLayerType'; import { DrawFilterShape } from '../toolbar/spatial_filter/draw_filter_shape'; import { IndexPattern } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { renderDataLayers, renderBaseLayers, handleDataLayerRender, handleBaseLayerRender, orderLayers, } from '../../model/layerRenderController'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { ResizeChecker } from '../../../../../src/plugins/opensearch_dashboards_utils/public'; import { MapServices } from '../../types'; import { ConfigSchema } from '../../../common/config'; import { baseLayerTypeLookup } from '../../model/layersFunctions'; import { MapsFooter } from './maps_footer'; import { DisplayFeatures } from '../tooltip/display_features'; import { TOOLTIP_STATE } from '../../../common'; import { SpatialFilterToolbar } from '../toolbar/spatial_filter/filter_toolbar'; import { DrawFilterShapeHelper } from '../toolbar/spatial_filter/display_draw_helper'; import { ShapeFilter } from '../../../../../src/plugins/data/common'; import { DashboardProps } from '../map_page/map_page'; import { MapsServiceErrorMsg } from './maps_messages'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; layers: MapLayerSpecification[]; layersIndexPatterns: IndexPattern[]; setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; maplibreRef: React.MutableRefObject; mapState: MapState; mapConfig: ConfigSchema; isReadOnlyMode: boolean; dashboardProps?: DashboardProps; isUpdatingLayerRender: boolean; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; } export class MapsServiceError extends Error { constructor(message?: string) { super(message); this.name = 'MapsServiceError'; } } export const MapContainer = ({ setLayers, layers, layersIndexPatterns, setLayersIndexPatterns, maplibreRef, mapState, mapConfig, isReadOnlyMode, dashboardProps, isUpdatingLayerRender, setIsUpdatingLayerRender, addSpatialFilter, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); function onError(e: unknown) { if (e instanceof MapsServiceError) { services.toastNotifications.addWarning(MapsServiceErrorMsg); } } const mapContainer = useRef(null); const [mounted, setMounted] = useState(false); const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); const [selectedLayerConfig, setSelectedLayerConfig] = useState< MapLayerSpecification | undefined >(); // start with display feature const [tooltipState, setTooltipState] = useState(TOOLTIP_STATE.DISPLAY_FEATURES); const [filterProperties, setFilterProperties] = useState({ mode: FILTER_DRAW_MODE.NONE, }); useEffect(() => { if (!mapContainer.current) return; const mbStyle = { version: 8 as 8, sources: {}, layers: [], glyphs: mapConfig.opensearchVectorTileGlyphsUrl, }; maplibreRef.current = new Maplibre({ container: mapContainer.current!, center: [MAP_INITIAL_STATE.lng, MAP_INITIAL_STATE.lat], zoom, style: mbStyle, }); const maplibreInstance = maplibreRef.current!; maplibreInstance.addControl(new NavigationControl({ showCompass: true }), 'top-right'); maplibreInstance.on('style.load', function () { setMounted(true); }); maplibreInstance.on('move', () => { return setZoom(Number(maplibreInstance.getZoom().toFixed(2))); }); // By default, Maplibre only auto resize map window when browser size changes, but in dashboard mode, we need // manually resize map window size when map panel size changes const mapContainerElement: HTMLElement | null = document.querySelector('.map-page'); let resizeChecker: ResizeChecker; if (mapContainerElement) { resizeChecker = new ResizeChecker(mapContainerElement); if (isReadOnlyMode) { resizeChecker.on( 'resize', throttle(() => { maplibreInstance?.resize(); }, 300) ); } } return () => { maplibreInstance.remove(); if (resizeChecker) { resizeChecker.destroy(); } }; }, []); // Handle map bounding box change, it should update the search if "request data around map extent" was enabled useEffect(() => { // 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(() => { renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); }, 200); if (maplibreRef.current) { maplibreRef.current.on('moveend', debouncedRenderLayers); } return () => { if (maplibreRef.current) { maplibreRef.current.off('moveend', debouncedRenderLayers); } }; }, [layers, mapState, services]); // Update data layers when state bar enable auto refresh useEffect(() => { let intervalId: NodeJS.Timeout | undefined; if (dashboardProps && dashboardProps.refreshConfig && !dashboardProps.refreshConfig.pause) { const { refreshConfig } = dashboardProps; intervalId = setInterval(() => { renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); }, refreshConfig.value); } return () => clearInterval(intervalId); }, [dashboardProps?.refreshConfig]); // Update data layers when global filter is updated useEffect(() => { if (!mapState?.spatialMetaFilters) { return; } renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); }, [mapState.spatialMetaFilters]); useEffect(() => { if (!mounted || layers.length <= 0) { return; } const orderLayersAfterRenderLoaded = () => orderLayers(layers, maplibreRef.current!); if (isUpdatingLayerRender || isReadOnlyMode) { if (selectedLayerConfig) { if (baseLayerTypeLookup[selectedLayerConfig.type]) { handleBaseLayerRender(selectedLayerConfig, maplibreRef, onError); } else { updateIndexPatterns(); handleDataLayerRender( selectedLayerConfig as DataLayerSpecification, mapState, services, maplibreRef ); } setSelectedLayerConfig(undefined); } else { renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); renderBaseLayers(layers, maplibreRef, onError); // Because of async layer rendering, layers order is not guaranteed, so we need to order layers // after all layers are rendered. maplibreRef.current!.once('idle', orderLayersAfterRenderLoaded); } setIsUpdatingLayerRender(false); } return () => { maplibreRef.current!.off('idle', orderLayersAfterRenderLoaded); }; }, [ layers, mounted, dashboardProps?.query, dashboardProps?.timeRange, dashboardProps?.filters, mapState, isReadOnlyMode, ]); useEffect(() => { const currentTooltipState: TOOLTIP_STATE = filterProperties?.mode === FILTER_DRAW_MODE.NONE ? TOOLTIP_STATE.DISPLAY_FEATURES : TOOLTIP_STATE.FILTER_DRAW_SHAPE; setTooltipState(currentTooltipState); }, [filterProperties]); const updateIndexPatterns = async () => { if (!selectedLayerConfig) { return; } if (baseLayerTypeLookup[selectedLayerConfig.type]) { return; } const findIndexPattern = layersIndexPatterns.find( // @ts-ignore (indexPattern) => indexPattern.id === selectedLayerConfig.source.indexPatternId ); if (!findIndexPattern) { const newIndexPattern = await services.data.indexPatterns.get( // @ts-ignore selectedLayerConfig.source.indexPatternId ); const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; setLayersIndexPatterns(cloneLayersIndexPatterns); } }; return (
{mounted && maplibreRef.current && } {mounted && maplibreRef.current && ( setFilterProperties({ mode: FILTER_DRAW_MODE.NONE })} /> )} {mounted && ( )} {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && maplibreRef.current && ( )} {mounted && maplibreRef.current && tooltipState === TOOLTIP_STATE.FILTER_DRAW_SHAPE && ( )}
{!isReadOnlyMode && mounted && ( )}
); };