diff --git a/maps_dashboards/common/index.ts b/maps_dashboards/common/index.ts index e131abe8..a45056be 100644 --- a/maps_dashboards/common/index.ts +++ b/maps_dashboards/common/index.ts @@ -6,7 +6,9 @@ export const PLUGIN_ID = 'maps-dashboards'; export const PLUGIN_NAME = 'Maps'; -export const MAP_VECTOR_TILE_URL = 'https://tiles.maps.opensearch.org/styles/basic.json'; +export const MAP_VECTOR_TILE_BASIC_STYLE = 'https://tiles.maps.opensearch.org/styles/basic.json'; +export const MAP_GLYPHS = 'https://tiles.maps.opensearch.org/fonts/{fontstack}/{range}.pbf'; +export const MAP_VECTOR_TILE_DATA_SOURCE = 'https://tiles.maps.opensearch.org/data/v1.json'; // Starting position [lng, lat] and zoom export const MAP_INITIAL_STATE = { diff --git a/maps_dashboards/package.json b/maps_dashboards/package.json index 44692ffa..8b56ebe9 100644 --- a/maps_dashboards/package.json +++ b/maps_dashboards/package.json @@ -8,6 +8,8 @@ "osd": "node ../../scripts/osd" }, "dependencies": { - "maplibre-gl": "^2.4.0" + "maplibre-gl": "^2.4.0", + "uuid": "3.3.2", + "prettier": "^2.1.1" } } diff --git a/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx b/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx index 45fa843f..6f88f464 100644 --- a/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx +++ b/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -11,27 +11,36 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiSpacer, - EuiTabbedContent, + EuiHorizontalRule, EuiTitle, EuiButton, EuiIcon, EuiKeyPadMenuItem, } from '@elastic/eui'; import './add_layer_panel.scss'; -import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; +import { v4 as uuidv4 } from 'uuid'; +import { DASHBOARDS_MAPS_LAYER_TYPE, LAYER_VISIBILITY } from '../../../common'; interface Props { setIsLayerConfigVisible: Function; setSelectedLayerConfig: Function; - addNewLayerToList: Function; } export const AddLayerPanel = ({ setIsLayerConfigVisible, setSelectedLayerConfig }: Props) => { const [isAddNewLayerModalVisible, setIsAddNewLayerModalVisible] = useState(false); function onClickAddNewLayer(layerItem: string) { - // Will add new layer logic + if (layerItem === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + setSelectedLayerConfig({ + iconType: 'visMapRegion', + name: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, + type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, + id: uuidv4(), + zoomRange: [0, 22], + opacity: 1, + visibility: LAYER_VISIBILITY.VISIBLE, + }); + } setIsAddNewLayerModalVisible(false); setIsLayerConfigVisible(true); } @@ -56,31 +65,6 @@ export const AddLayerPanel = ({ setIsLayerConfigVisible, setSelectedLayerConfig const closeModal = () => setIsAddNewLayerModalVisible(false); const showModal = () => setIsAddNewLayerModalVisible(true); - const tabs = [ - { - id: 'select-layer-id', - name: 'Select layer', - content: ( - - - {availableLayers} - - ), - }, - { - id: 'import-id', - name: 'Import', - content: ( - - - -

Import GeoJSON or JSON

-
-
- ), - }, - ]; - return (
@@ -96,12 +80,11 @@ export const AddLayerPanel = ({ setIsLayerConfigVisible, setSelectedLayerConfig - + + +

Reference layer

+
+ {availableLayers}
)} 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 f1802d4b..e879330a 100644 --- a/maps_dashboards/public/components/layer_config/layer_config_panel.tsx +++ b/maps_dashboards/public/components/layer_config/layer_config_panel.tsx @@ -23,7 +23,6 @@ interface Props { setIsLayerConfigVisible: Function; selectedLayerConfig: ILayerConfig; setSelectedLayerConfig: Function; - addNewLayerFunction: Function; updateLayer: Function; } @@ -39,7 +38,6 @@ export const LayerConfigPanel = ({ }; const onUpdate = () => { updateLayer(); - selectedLayerConfig.update?.(); onClose(); }; 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 3f1c71d9..f690c2b7 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 @@ -17,10 +17,16 @@ import { import { I18nProvider } from '@osd/i18n/react'; import { Map as Maplibre } from 'maplibre-gl'; import './layer_control_panel.scss'; +import { v4 as uuidv4 } from 'uuid'; import { AddLayerPanel } from '../add_layer_panel'; import { LayerConfigPanel } from '../layer_config'; import { ILayerConfig } from '../../model/ILayerConfig'; -import { DASHBOARDS_MAPS_LAYER_TYPE, MAP_VECTOR_TILE_URL, LAYER_VISIBILITY } from '../../../common'; +import { + DASHBOARDS_MAPS_LAYER_TYPE, + LAYER_VISIBILITY, + MAP_VECTOR_TILE_BASIC_STYLE, +} from '../../../common'; +import { layersFunctionMap } from '../../model/layersFunctions'; interface MaplibreRef { current: Maplibre | null; @@ -44,81 +50,45 @@ const LayerControlPanel = ({ maplibreRef }: Props) => { visibility: '', }); - const [layers, setLayers] = useState([ - { - iconType: 'visMapRegion', - id: 'example_id_1', - type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, - name: 'Base Map Layer', - zoomRange: [0, 12], - opacity: 1, - visibility: LAYER_VISIBILITY.VISIBLE, - update() { - const maplibreInstance = maplibreRef.current; - if (maplibreInstance) { - const baseMapJson = maplibreInstance.getStyle().layers; - if (baseMapJson) { - baseMapJson.forEach((mbLayer) => { - maplibreInstance.setLayerZoomRange(mbLayer.id, this.zoomRange[0], this.zoomRange[1]); - // it will catch error when update opacity in symbol type layer, need figure out later - if (mbLayer.type === 'symbol') { - return; - } - maplibreInstance.setPaintProperty( - mbLayer.id, - `${mbLayer.type}-opacity`, - this.opacity - ); - maplibreInstance.setLayoutProperty(mbLayer.id, 'visibility', this.visibility); - }); - } else { - maplibreInstance.setStyle(MAP_VECTOR_TILE_URL); - } - } - }, - hide() { - const maplibreInstance = maplibreRef.current; - if (maplibreInstance) { - const baseMapJson = maplibreInstance.getStyle().layers; - if (baseMapJson) { - baseMapJson.forEach((mbLayer) => { - maplibreInstance.setLayoutProperty(mbLayer.id, 'visibility', this.visibility); - }); - } - } - }, - remove() { - const maplibreInstance = maplibreRef.current; - if (maplibreInstance) { - const baseMapJson = maplibreInstance.getStyle().layers; - if (baseMapJson) { - baseMapJson.forEach((mbLayer) => { - maplibreInstance.removeLayer(mbLayer.id); - }); - } - } - }, + const initialLoadLayer: ILayerConfig = { + iconType: 'visMapRegion', + id: uuidv4(), + type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, + name: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, + zoomRange: [0, 22], + opacity: 1, + visibility: LAYER_VISIBILITY.VISIBLE, + layerSpec: { + OSMUrl: MAP_VECTOR_TILE_BASIC_STYLE, }, - ]); + }; + + const [layers, setLayers] = useState([initialLoadLayer]); useEffect(() => { - layers.forEach((layer) => { - layer.update?.(); + maplibreRef.current?.on('load', function () { + layers.forEach((layer) => { + layersFunctionMap[layer.type]?.initial(maplibreRef, layer); + }); }); - }, [layers]); + }, []); - const updateLayers = () => { + const updateLayer = () => { const layersClone = [...layers]; const index = layersClone.findIndex((layer) => layer.id === selectedLayerConfig.id); - if (index > -1) { + if (index <= -1) { + layersClone.push(selectedLayerConfig); + layersFunctionMap[selectedLayerConfig.type]?.addNewLayer(maplibreRef, selectedLayerConfig); + } else { layersClone[index] = { ...layersClone[index], ...selectedLayerConfig, }; - } else { - layersClone.push(selectedLayerConfig); } setLayers(layersClone); + setTimeout(function () { + layersFunctionMap[selectedLayerConfig.type]?.update(maplibreRef, selectedLayerConfig); + }, 50); }; const removeLayer = (index: number) => { @@ -188,7 +158,7 @@ const LayerControlPanel = ({ maplibreRef }: Props) => { { if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { @@ -196,7 +166,7 @@ const LayerControlPanel = ({ maplibreRef }: Props) => { } else { layer.visibility = LAYER_VISIBILITY.VISIBLE; } - layer.hide(index); + layersFunctionMap[layer.type]?.hide(maplibreRef, layer); }} aria-label="Hide or show layer" color="text" @@ -208,7 +178,7 @@ const LayerControlPanel = ({ maplibreRef }: Props) => { size="s" iconType="trash" onClick={() => { - layer.remove(index); + layersFunctionMap[layer.type]?.remove(maplibreRef, layer); removeLayer(index); }} aria-label="Delete layer" @@ -226,14 +196,13 @@ const LayerControlPanel = ({ maplibreRef }: Props) => { )} diff --git a/maps_dashboards/public/components/map_container/map_container.scss b/maps_dashboards/public/components/map_container/map_container.scss index e73cad5b..be555172 100644 --- a/maps_dashboards/public/components/map_container/map_container.scss +++ b/maps_dashboards/public/components/map_container/map_container.scss @@ -22,3 +22,10 @@ left: $euiSizeS; top: $euiSizeS; } + +.zoombar { + z-index: 1; + position: absolute; + bottom: $euiSizeM; + right: $euiSizeS; +} diff --git a/maps_dashboards/public/components/map_container/map_container.tsx b/maps_dashboards/public/components/map_container/map_container.tsx index 8d53d3e7..8615a91d 100644 --- a/maps_dashboards/public/components/map_container/map_container.tsx +++ b/maps_dashboards/public/components/map_container/map_container.tsx @@ -3,36 +3,63 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { EuiPanel } from '@elastic/eui'; import { Map as Maplibre, NavigationControl } from 'maplibre-gl'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; -import { MAP_VECTOR_TILE_URL, MAP_INITIAL_STATE } from '../../../common/index'; +import { MAP_INITIAL_STATE, MAP_GLYPHS, MAP_VECTOR_TILE_DATA_SOURCE } from '../../../common'; export const MapContainer = () => { const maplibreRef = useRef(null); const mapContainer = useRef(null); + const [mounted, setMounted] = useState(false); + const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); useEffect(() => { - const mapStyle = MAP_VECTOR_TILE_URL; - const initialState = MAP_INITIAL_STATE; + const mbStyle = { + version: 8 as 8, + sources: {}, + layers: [], + glyphs: MAP_GLYPHS, + }; - const maplibre = new Maplibre({ + maplibreRef.current = new Maplibre({ // @ts-ignore container: mapContainer.current, - center: [initialState.lng, initialState.lat], - zoom: initialState.zoom, + center: [MAP_INITIAL_STATE.lng, MAP_INITIAL_STATE.lat], + zoom, + style: mbStyle, }); - maplibre.setStyle(mapStyle); - maplibreRef.current = maplibre; - maplibre.addControl(new NavigationControl({ showCompass: false }), 'top-right'); + maplibreRef.current.addControl(new NavigationControl({ showCompass: false }), 'top-right'); + maplibreRef.current.on('style.load', function () { + // @ts-ignore + maplibreRef.current.addSource('openmaptiles', { + type: 'vector', + url: MAP_VECTOR_TILE_DATA_SOURCE, + }); + setMounted(true); + }); }, []); + useEffect(() => { + // @ts-ignore + maplibreRef.current.on('move', () => { + // @ts-ignore + return setZoom(maplibreRef.current.getZoom().toFixed(2)); + }); + }, [zoom]); + + useEffect(() => {}, []); + return (
+ +

Zoom: {zoom}

+
- + {mounted && }
diff --git a/maps_dashboards/public/model/ILayerConfig.ts b/maps_dashboards/public/model/ILayerConfig.ts index f9c54749..9a6dcd4e 100644 --- a/maps_dashboards/public/model/ILayerConfig.ts +++ b/maps_dashboards/public/model/ILayerConfig.ts @@ -11,7 +11,5 @@ export interface ILayerConfig { zoomRange: number[]; opacity: number; visibility: string; - update: any; - remove: any; - hide: any; + layerSpec?: any; } diff --git a/maps_dashboards/public/model/OSMLayerFunctions.ts b/maps_dashboards/public/model/OSMLayerFunctions.ts new file mode 100644 index 00000000..7cb565da --- /dev/null +++ b/maps_dashboards/public/model/OSMLayerFunctions.ts @@ -0,0 +1,123 @@ +import { Map as Maplibre, LayerSpecification } from 'maplibre-gl'; +import { ILayerConfig } from './ILayerConfig'; +import { MAP_VECTOR_TILE_BASIC_STYLE } from '../../common'; + +interface MaplibreRef { + current: Maplibre | null; +} + +// Functions for OpenSearch maps vector tile layer +export const OSMLayerFunctions = { + initial: (maplibreRef: MaplibreRef, layerConfig: ILayerConfig) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const renderStyleData = async () => { + try { + const response = await fetch(MAP_VECTOR_TILE_BASIC_STYLE); + const json = await response.json(); + const styleLayers: LayerSpecification[] = await json.layers; + styleLayers.forEach((styleLayer) => { + styleLayer.id = styleLayer.id + '_' + layerConfig.id; + // @ts-ignore + maplibreRef.current.addLayer(styleLayer); + }); + } catch (error) { + console.log('error', error); + } + }; + renderStyleData(); + + // @ts-ignore + setTimeout(function () { + const mbLayerJson: LayerSpecification[] = maplibreRef.current.getStyle().layers; + mbLayerJson.map((mbLayer) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreInstance.setLayerZoomRange( + mbLayer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + // TODO: figure out error reason + if (mbLayer.type === 'symbol') { + return; + } + maplibreInstance.setPaintProperty( + mbLayer.id, + `${mbLayer.type}-opacity`, + layerConfig.opacity + ); + } + }); + }, 50); + } + }, + update: (maplibreRef: MaplibreRef, layerConfig: ILayerConfig) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const mbLayerJson = maplibreInstance.getStyle().layers; + mbLayerJson.forEach((mbLayer: { id: any; type: string }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreInstance.setLayerZoomRange( + mbLayer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + if (mbLayer.type === 'symbol') { + return; + } + maplibreInstance.setPaintProperty( + mbLayer.id, + `${mbLayer.type}-opacity`, + layerConfig.opacity + ); + } + }); + } + }, + addNewLayer: (maplibreRef: MaplibreRef, layerConfig: ILayerConfig) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const renderStyleData = async () => { + try { + const response = await fetch(MAP_VECTOR_TILE_BASIC_STYLE); + const json = await response.json(); + const styleLayers: LayerSpecification[] = await json.layers; + styleLayers.forEach((styleLayer) => { + styleLayer.id = styleLayer.id + '_' + layerConfig.id; + // @ts-ignore + maplibreRef.current.addLayer(styleLayer); + }); + } catch (error) { + console.log('error', error); + } + }; + renderStyleData(); + } + }, + remove: (maplibreRef: MaplibreRef, layerConfig: ILayerConfig) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const mbLayerJson = maplibreInstance.getStyle().layers; + if (mbLayerJson) { + mbLayerJson.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreInstance.removeLayer(mbLayer.id); + } + }); + } + } + }, + hide: (maplibreRef: MaplibreRef, layerConfig: ILayerConfig) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const mbLayerJson = maplibreInstance.getStyle().layers; + if (mbLayerJson) { + mbLayerJson.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreInstance.setLayoutProperty(mbLayer.id, 'visibility', layerConfig.visibility); + } + }); + } + } + }, +}; diff --git a/maps_dashboards/public/model/layersFunctions.ts b/maps_dashboards/public/model/layersFunctions.ts new file mode 100644 index 00000000..eee50a51 --- /dev/null +++ b/maps_dashboards/public/model/layersFunctions.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; +import { OSMLayerFunctions } from './OSMLayerFunctions'; + +export const layersFunctionMap = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: OSMLayerFunctions, +}; diff --git a/maps_dashboards/yarn.lock b/maps_dashboards/yarn.lock index 4f6e9646..818b5a9f 100644 --- a/maps_dashboards/yarn.lock +++ b/maps_dashboards/yarn.lock @@ -183,6 +183,11 @@ potpack@^1.0.2: resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14" integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ== +prettier@^2.1.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + protocol-buffers-schema@^3.3.1: version "3.6.0" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" @@ -212,6 +217,11 @@ tinyqueue@^2.0.3: resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + vt-pbf@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac"