From 7666e9c933cea83817a2a4130a9f66c710c3c958 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 28 Dec 2022 11:12:48 -0800 Subject: [PATCH] Support custom layer to Maps Signed-off-by: Junqiu Lei --- maps_dashboards/common/index.ts | 2 +- .../custom_map_config_panel.tsx | 47 +++++++ .../custom_map_config/custom_map_source.tsx | 119 ++++++++++++++++++ .../layer_config/layer_config_panel.tsx | 9 ++ .../layer_control_panel.tsx | 21 ++-- .../public/model/customLayerFunctions.ts | 115 +++++++++++++++++ .../public/model/layersFunctions.ts | 9 ++ maps_dashboards/public/model/mapLayerType.ts | 19 ++- .../public/utils/getIntialConfig.ts | 16 +-- 9 files changed, 335 insertions(+), 22 deletions(-) create mode 100644 maps_dashboards/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx create mode 100644 maps_dashboards/public/components/layer_config/custom_map_config/custom_map_source.tsx create mode 100644 maps_dashboards/public/model/customLayerFunctions.ts diff --git a/maps_dashboards/common/index.ts b/maps_dashboards/common/index.ts index 616d07be..d023b54c 100644 --- a/maps_dashboards/common/index.ts +++ b/maps_dashboards/common/index.ts @@ -61,7 +61,7 @@ export enum DASHBOARDS_MAPS_LAYER_ICON { export enum DASHBOARDS_MAPS_LAYER_DESCRIPTION { OPENSEARCH_MAP = 'Default basemaps from OpenSearch', DOCUMENTS = 'View points, lines and polygons on map', - CUSTOM_MAP = 'Configure Maps to use WMS map server', + CUSTOM_MAP = 'Configure Maps to use a third-party raster tile source.', } export const DOCUMENTS = { diff --git a/maps_dashboards/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx b/maps_dashboards/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx new file mode 100644 index 00000000..5c6d1673 --- /dev/null +++ b/maps_dashboards/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { CustomLayerSpecification } from '../../../model/mapLayerType'; +import { LayerBasicSettings } from '../layer_basic_settings'; +import { CustomMapSource } from './custom_map_source'; + +interface Props { + selectedLayerConfig: CustomLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + isLayerExists: Function; +} + +export const CustomMapConfigPanel = (props: Props) => { + const newProps = { + ...props, + }; + + const tabs = [ + { + id: 'custom-map-source--id', + name: 'Data', + content: ( + + + + + ), + }, + { + id: 'settings--id', + name: 'Settings', + content: ( + + + + + ), + }, + ]; + return ; +}; diff --git a/maps_dashboards/public/components/layer_config/custom_map_config/custom_map_source.tsx b/maps_dashboards/public/components/layer_config/custom_map_config/custom_map_source.tsx new file mode 100644 index 00000000..7d0fc3f6 --- /dev/null +++ b/maps_dashboards/public/components/layer_config/custom_map_config/custom_map_source.tsx @@ -0,0 +1,119 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiFlexItem, + EuiFormLabel, + EuiFlexGrid, + EuiSpacer, + EuiPanel, + EuiForm, + EuiFieldText, + EuiFormErrorText, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { CustomLayerSpecification } from '../../../model/mapLayerType'; + +interface Props { + selectedLayerConfig: CustomLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; +} + +export const CustomMapSource = ({ + selectedLayerConfig, + setSelectedLayerConfig, + setIsUpdateDisabled, +}: Props) => { + const [customMapURL, setCustomMapURL] = useState(''); + const [customMapAttribution, setCustomMapAttribution] = useState(''); + + const onChangeCustomMapURL = (e: any) => { + setCustomMapURL(e.target.value); + setIsUpdateDisabled(false); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + url: e.target.value, + }, + }); + }; + + const onChangeCustomMapAttribution = (e: any) => { + setCustomMapAttribution(e.target.value); + setIsUpdateDisabled(false); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + attribution: e.target.value, + }, + }); + }; + + const isInvalidURL = (url: string): boolean => { + try { + new URL(url); + return false; + } catch (e) { + return true; + } + }; + + useEffect(() => { + setCustomMapURL(selectedLayerConfig.source.url); + }, [selectedLayerConfig.source.url]); + + useEffect(() => { + setCustomMapAttribution(selectedLayerConfig.source.attribution); + }, [selectedLayerConfig.source.attribution]); + + useEffect(() => { + const disableUpdate = isInvalidURL(customMapURL); + setIsUpdateDisabled(disableUpdate); + }, [customMapURL, setIsUpdateDisabled]); + + return ( +
+ + + + + Raster Tile URL + + + {isInvalidURL(customMapURL) && ( + + + + )} + + + Raster Tile Attribution + + + + + + + +
+ ); +}; diff --git a/maps_dashboards/public/components/layer_config/layer_config_panel.tsx b/maps_dashboards/public/components/layer_config/layer_config_panel.tsx index eee3bef1..e9a15d7f 100644 --- a/maps_dashboards/public/components/layer_config/layer_config_panel.tsx +++ b/maps_dashboards/public/components/layer_config/layer_config_panel.tsx @@ -29,6 +29,7 @@ import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; import { DocumentLayerConfigPanel } from './documents_config/document_layer_config_panel'; import { layersTypeIconMap } from '../../model/layersFunctions'; import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { CustomMapConfigPanel } from './custom_map_config/custom_map_config_panel'; interface Props { closeLayerConfigPanel: Function; @@ -128,6 +129,14 @@ export const LayerConfigPanel = ({ isLayerExists={isLayerExists} /> )} + {selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP && ( + + )} diff --git a/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx b/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx index 10c719f8..05cf4d28 100644 --- a/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx +++ b/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx @@ -33,7 +33,11 @@ import { LAYER_PANEL_SHOW_LAYER_ICON, LAYER_PANEL_HIDE_LAYER_ICON, } from '../../../common'; -import { LayerActions, layersFunctionMap } from '../../model/layersFunctions'; +import { + LayerActions, + layersFunctionMap, + referenceLayerTypeLookup, +} from '../../model/layersFunctions'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; import { @@ -95,7 +99,10 @@ export const LayerControlPanel = memo( if (!selectedLayerConfig) { return; } - if (selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + if ( + selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP || + selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP + ) { handleReferenceLayerRender(selectedLayerConfig, maplibreRef, undefined); } else { updateIndexPatterns(); @@ -107,7 +114,7 @@ export const LayerControlPanel = memo( } else { layers.forEach((layer) => { const beforeLayerId = getMapBeforeLayerId(layer); - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + if (referenceLayerTypeLookup[layer.type]) { handleReferenceLayerRender(layer, maplibreRef, beforeLayerId); } else { handleDataLayerRender(layer, mapState, services, maplibreRef, beforeLayerId); @@ -231,10 +238,10 @@ export const LayerControlPanel = memo( if (!selectedLayerConfig) { return; } - if (selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { - return; - } - if (!selectedLayerConfig.source.indexPatternId) { + if ( + selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP || + selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP + ) { return; } const findIndexPattern = layersIndexPatterns.find( diff --git a/maps_dashboards/public/model/customLayerFunctions.ts b/maps_dashboards/public/model/customLayerFunctions.ts new file mode 100644 index 00000000..dbfd60fc --- /dev/null +++ b/maps_dashboards/public/model/customLayerFunctions.ts @@ -0,0 +1,115 @@ +import { + Map as Maplibre, + AttributionControl, + RasterSourceSpecification, +} from 'maplibre-gl'; +import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; + +interface MaplibreRef { + current: Maplibre | null; +} + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +const layerExistInMbSource = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { + const layers = getCurrentStyleLayers(maplibreRef); + for (const layer in layers) { + if (layers[layer].id.includes(layerConfig.id)) { + return true; + } + } + return false; +}; + +const updateLayerConfig = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const customLauer = maplibreInstance.getLayer(layerConfig.id); + if (customLauer) { + maplibreInstance.setPaintProperty( + layerConfig.id, + 'raster-opacity', + layerConfig.opacity / 100 + ); + maplibreInstance.setLayerZoomRange( + layerConfig.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + const rasterLayerSource = maplibreInstance.getSource( + layerConfig.id + )! as RasterSourceSpecification; + if (rasterLayerSource.attribution !== layerConfig.source?.attribution) { + rasterLayerSource.attribution = layerConfig?.source?.attribution; + maplibreInstance._controls.forEach((control) => { + if (control instanceof AttributionControl) { + control._updateAttributions(); + } + }); + } + if (rasterLayerSource.tiles![0] !== layerConfig.source?.url) { + rasterLayerSource.tiles = [layerConfig?.source?.url]; + maplibreInstance.style.sourceCaches[layerConfig.id].clearTiles(); + maplibreInstance.style.sourceCaches[layerConfig.id].update(maplibreInstance.transform); + maplibreInstance.triggerRepaint(); + } + } + } +}; + +const addNewLayer = ( + layerConfig: CustomLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const layerSource = layerConfig?.source; + maplibreInstance.addSource(layerConfig.id, { + type: 'raster', + tiles: [layerSource?.url], + tileSize: 256, + attribution: layerSource?.attribution, + }); + maplibreInstance.addLayer( + { + id: layerConfig.id, + type: 'raster', + source: layerConfig.id, + }, + beforeLayerId + ); + } +}; + +export const CustomLayerFunctions = { + render: ( + maplibreRef: MaplibreRef, + layerConfig: CustomLayerSpecification, + beforeLayerId: string | undefined + ) => { + if (layerExistInMbSource(layerConfig, maplibreRef)) { + updateLayerConfig(layerConfig, maplibreRef); + } else { + addNewLayer(layerConfig, maplibreRef, beforeLayerId); + } + }, + remove: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.removeLayer(mbLayer.id); + } + }); + }, + hide: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.setLayoutProperty(mbLayer.id, 'visibility', layerConfig.visibility); + } + }); + }, +}; diff --git a/maps_dashboards/public/model/layersFunctions.ts b/maps_dashboards/public/model/layersFunctions.ts index 24355ae4..a4311b6a 100644 --- a/maps_dashboards/public/model/layersFunctions.ts +++ b/maps_dashboards/public/model/layersFunctions.ts @@ -12,6 +12,7 @@ import { import { OSMLayerFunctions } from './OSMLayerFunctions'; import { DocumentLayerFunctions } from './documentLayerFunctions'; import { MapLayerSpecification } from './mapLayerType'; +import { CustomLayerFunctions } from './customLayerFunctions'; interface MaplibreRef { current: Maplibre | null; @@ -55,11 +56,13 @@ export const LayerActions = { export const layersFunctionMap: { [key: string]: any } = { [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: OSMLayerFunctions, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DocumentLayerFunctions, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: CustomLayerFunctions, }; export const layersTypeNameMap: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_NAME.OPENSEARCH_MAP, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_NAME.DOCUMENTS, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_NAME.CUSTOM_MAP, }; const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { @@ -84,3 +87,9 @@ export const layersTypeIconMap: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_ICON.OPENSEARCH_MAP, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_ICON.DOCUMENTS, }; + +export const referenceLayerTypeLookup = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: true, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: true, + [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: false, +}; diff --git a/maps_dashboards/public/model/mapLayerType.ts b/maps_dashboards/public/model/mapLayerType.ts index ec0e4de1..8484b26a 100644 --- a/maps_dashboards/public/model/mapLayerType.ts +++ b/maps_dashboards/public/model/mapLayerType.ts @@ -6,7 +6,10 @@ import { Filter } from '../../../../src/plugins/data/public'; /* eslint @typescript-eslint/consistent-type-definitions: ["error", "type"] */ -export type MapLayerSpecification = OSMLayerSpecification | DocumentLayerSpecification; +export type MapLayerSpecification = + | OSMLayerSpecification + | DocumentLayerSpecification + | CustomLayerSpecification; export type OSMLayerSpecification = { name: string; @@ -49,3 +52,17 @@ export type DocumentLayerSpecification = { markerSize: number; }; }; + +export type CustomLayerSpecification = { + name: string; + id: string; + type: 'custom_map'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + url: string; + attribution: string; + }; +}; diff --git a/maps_dashboards/public/utils/getIntialConfig.ts b/maps_dashboards/public/utils/getIntialConfig.ts index 114e20c9..919a6399 100644 --- a/maps_dashboards/public/utils/getIntialConfig.ts +++ b/maps_dashboards/public/utils/getIntialConfig.ts @@ -61,27 +61,17 @@ export const getLayerConfigMap = () => ({ markerSize: DOCUMENTS_DEFAULT_MARKER_SIZE, }, }, - //TODO: update custom layer config [CUSTOM_MAP.type]: { name: '', description: '', type: CUSTOM_MAP.type, id: uuidv4(), zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM], - opacity: MAP_DATA_LAYER_DEFAULT_OPACITY, + opacity: MAP_REFERENCE_LAYER_DEFAULT_OPACITY, visibility: LAYER_VISIBILITY.VISIBLE, source: { - indexPatternRefName: undefined, - geoFieldType: undefined, - geoFieldName: undefined, - documentRequestNumber: DOCUMENTS_DEFAULT_REQUEST_NUMBER, - tooltipFields: DOCUMENTS_DEFAULT_TOOLTIPS, - showTooltips: DOCUMENTS_DEFAULT_SHOW_TOOLTIPS, - }, - style: { - ...getStyleColor(), - borderThickness: MAP_LAYER_DEFAULT_BORDER_THICKNESS, - markerSize: DOCUMENTS_DEFAULT_MARKER_SIZE, + url: '', + attribution: '', }, }, });