diff --git a/custom_import_map/.eslintrc b/custom_import_map/.eslintrc deleted file mode 100644 index 70b6ed10..00000000 --- a/custom_import_map/.eslintrc +++ /dev/null @@ -1,4 +0,0 @@ ---- -extends: - - "@elastic/eslint-config-kibana" - - "plugin:@elastic/eui/recommended" diff --git a/custom_import_map/.eslintrc.js b/custom_import_map/.eslintrc.js new file mode 100644 index 00000000..b16a8b23 --- /dev/null +++ b/custom_import_map/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + '@osd/eslint/require-license-header': 'off', + }, +}; diff --git a/custom_import_map/.i18nrc.json b/custom_import_map/.i18nrc.json new file mode 100644 index 00000000..26b748a1 --- /dev/null +++ b/custom_import_map/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "customImportMap", + "paths": { + "customImportMap": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/custom_import_map/common/constants/shared.ts b/custom_import_map/common/constants/shared.ts index 3290bd78..7d1ed801 100644 --- a/custom_import_map/common/constants/shared.ts +++ b/custom_import_map/common/constants/shared.ts @@ -12,3 +12,5 @@ export const MAX_FILE_PAYLOAD_SIZE_IN_MB = 25; export const MAX_FILE_PAYLOAD_SIZE = fromMBtoBytes(MAX_FILE_PAYLOAD_SIZE_IN_MB); export const PLUGIN_ID = 'customImportMap'; export const PLUGIN_NAME = 'customImportMap'; +export const PLUGIN_NAVIGATION_BAR_TILE = 'Maps'; +export const PLUGIN_NAVIGATION_BAR_ID = 'maps-dashboards'; diff --git a/custom_import_map/common/index.ts b/custom_import_map/common/index.ts index 10feca0e..580a9e35 100644 --- a/custom_import_map/common/index.ts +++ b/custom_import_map/common/index.ts @@ -9,6 +9,7 @@ import { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, + PLUGIN_NAVIGATION_BAR_ID, PLUGIN_NAME, } from './constants/shared'; @@ -18,5 +19,103 @@ export { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, + PLUGIN_NAVIGATION_BAR_ID, PLUGIN_NAME, }; + +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'; +export const MAP_DEFAULT_MIN_ZOOM = 0; +export const MAP_DEFAULT_MAX_ZOOM = 22; +export const MAP_REFERENCE_LAYER_DEFAULT_OPACITY = 100; +export const MAP_DATA_LAYER_DEFAULT_OPACITY = 70; +export const MAP_LAYER_DEFAULT_MIN_OPACITY = 0; +export const MAP_LAYER_DEFAULT_MAX_OPACITY = 100; +export const MAP_LAYER_DEFAULT_OPACITY_STEP = 1; +export const MAP_LAYER_DEFAULT_BORDER_THICKNESS = 1; +export const DOCUMENTS_DEFAULT_REQUEST_NUMBER = 1000; +export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; +export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; +export const DOCUMENTS_DEFAULT_MARKER_SIZE = 5; +export const LAYER_PANEL_SHOW_LAYER_ICON = 'eye'; +export const LAYER_PANEL_HIDE_LAYER_ICON = 'eyeClosed'; +export const MAX_LAYER_NAME_LIMIT = 35; +export const MAP_LAYER_DEFAULT_NAME = 'Default map'; +export const NEW_MAP_LAYER_DEFAULT_PREFIX = 'New layer'; + +// Starting position [lng, lat] and zoom +export const MAP_INITIAL_STATE = { + lng: 0, + lat: 0, + zoom: 1, +}; + +export const APP_PATH = { + LANDING_PAGE_PATH: '/', + CREATE_MAP: '/create', + EDIT_MAP: '/:id', +}; + +export enum DASHBOARDS_MAPS_LAYER_NAME { + OPENSEARCH_MAP = 'OpenSearch Map', + DOCUMENTS = 'Documents', + CUSTOM_MAP = 'Custom Map', +} + +export enum DASHBOARDS_MAPS_LAYER_TYPE { + OPENSEARCH_MAP = 'opensearch_vector_tile_map', + DOCUMENTS = 'documents', + CUSTOM_MAP = 'custom_map', +} + +export enum DASHBOARDS_MAPS_LAYER_ICON { + OPENSEARCH_MAP = 'globe', + DOCUMENTS = 'document', + CUSTOM_MAP = 'globe', +} + +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 custom map source', +} + +export const DOCUMENTS = { + name: DASHBOARDS_MAPS_LAYER_NAME.DOCUMENTS, + type: DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS, + icon: DASHBOARDS_MAPS_LAYER_ICON.DOCUMENTS, + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.DOCUMENTS, +}; + +export const OPENSEARCH_MAP_LAYER = { + name: DASHBOARDS_MAPS_LAYER_NAME.OPENSEARCH_MAP, + type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, + icon: DASHBOARDS_MAPS_LAYER_ICON.OPENSEARCH_MAP, + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.OPENSEARCH_MAP, +}; + +export const CUSTOM_MAP = { + name: DASHBOARDS_MAPS_LAYER_NAME.CUSTOM_MAP, + type: DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP, + icon: DASHBOARDS_MAPS_LAYER_ICON.CUSTOM_MAP, + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.CUSTOM_MAP, +}; + +export interface Layer { + name: DASHBOARDS_MAPS_LAYER_NAME; + type: DASHBOARDS_MAPS_LAYER_TYPE; + icon: DASHBOARDS_MAPS_LAYER_ICON; + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION; +} + +export const LAYER_VISIBILITY = { + NONE: 'none', + VISIBLE: 'visible', +}; + +export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: 'globe', + [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: 'document', + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: 'globe', +}; diff --git a/custom_import_map/common/map_saved_object_attributes.ts b/custom_import_map/common/map_saved_object_attributes.ts new file mode 100644 index 00000000..3d071ebb --- /dev/null +++ b/custom_import_map/common/map_saved_object_attributes.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from 'opensearch-dashboards/server'; + +export interface MapSavedObjectAttributes extends SavedObjectAttributes { + /** Title of the map */ + title: string; + /** Description of the map */ + description?: string; + /** State of the map, which could include current zoom level, lat, lng etc. */ + mapState?: string; + /** Maps-dashboards layers of the map */ + layerList?: string; + /** UI state of the map */ + uiState?: string; + /** Version is used to track version differences in saved object mapping */ + version: number; + /** SearchSourceFields is used to reference other saved objects */ + searchSourceFields?: { + index?: string; + }; +} diff --git a/custom_import_map/opensearch_dashboards.json b/custom_import_map/opensearch_dashboards.json index ffd9ab96..0228511a 100644 --- a/custom_import_map/opensearch_dashboards.json +++ b/custom_import_map/opensearch_dashboards.json @@ -1,9 +1,9 @@ { "id": "customImportMap", "version": "3.0.0.0", - "opensearchDashboardsVersion": "2.4.1", + "opensearchDashboardsVersion": "3.0.0", "server": true, "ui": true, - "requiredPlugins": ["regionMap", "opensearchDashboardsReact"], + "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "navigation", "savedObjects", "data"], "optionalPlugins": [] } diff --git a/custom_import_map/package.json b/custom_import_map/package.json index bc3f8c69..6de5dbd4 100644 --- a/custom_import_map/package.json +++ b/custom_import_map/package.json @@ -22,6 +22,9 @@ "dependencies": { "@opensearch-dashboards-test/opensearch-dashboards-test-library": "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main", "@cypress/skip-test": "^2.6.1", - "cypress-file-upload": "^5.0.8" + "cypress-file-upload": "^5.0.8", + "maplibre-gl": "^2.4.0", + "uuid": "3.3.2", + "prettier": "^2.1.1" } } diff --git a/custom_import_map/public/_variables.scss b/custom_import_map/public/_variables.scss new file mode 100644 index 00000000..98f04c0a --- /dev/null +++ b/custom_import_map/public/_variables.scss @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +$mapHeaderOffset: 154px; diff --git a/custom_import_map/public/application.tsx b/custom_import_map/public/application.tsx new file mode 100644 index 00000000..aab8c3ea --- /dev/null +++ b/custom_import_map/public/application.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters } from '../../../src/core/public'; +import { MapServices } from './types'; +import { MapsDashboardsApp } from './components/app'; +import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; + +export const renderApp = ({ element }: AppMountParameters, services: MapServices) => { + ReactDOM.render( + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/custom_import_map/public/components/add_layer_panel/add_layer_panel.scss b/custom_import_map/public/components/add_layer_panel/add_layer_panel.scss new file mode 100644 index 00000000..0209fa59 --- /dev/null +++ b/custom_import_map/public/components/add_layer_panel/add_layer_panel.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.addLayer__button { + padding: $euiSizeM $euiSizeM; +} + +.addLayerDialog__description { + width: 272px; +} diff --git a/custom_import_map/public/components/add_layer_panel/add_layer_panel.tsx b/custom_import_map/public/components/add_layer_panel/add_layer_panel.tsx new file mode 100644 index 00000000..ca773228 --- /dev/null +++ b/custom_import_map/public/components/add_layer_panel/add_layer_panel.tsx @@ -0,0 +1,153 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiHorizontalRule, + EuiTitle, + EuiButton, + EuiIcon, + EuiKeyPadMenuItem, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import './add_layer_panel.scss'; +import { + DOCUMENTS, + OPENSEARCH_MAP_LAYER, + CUSTOM_MAP, + Layer, + NEW_MAP_LAYER_DEFAULT_PREFIX, +} from '../../../common'; +import { getLayerConfigMap } from '../../utils/getIntialConfig'; + +interface Props { + setIsLayerConfigVisible: Function; + setSelectedLayerConfig: Function; + IsLayerConfigVisible: boolean; + addLayer: Function; + setIsNewLayer: Function; + newLayerIndex: number; +} + +export const AddLayerPanel = ({ + setIsLayerConfigVisible, + setSelectedLayerConfig, + IsLayerConfigVisible, + addLayer, + setIsNewLayer, + newLayerIndex, +}: Props) => { + const [isAddNewLayerModalVisible, setIsAddNewLayerModalVisible] = useState(false); + const [highlightItem, setHighlightItem] = useState(null); + + function onClickAddNewLayer(layerType: string) { + const initLayerConfig = getLayerConfigMap()[layerType]; + initLayerConfig.name = NEW_MAP_LAYER_DEFAULT_PREFIX + ' ' + newLayerIndex; + setSelectedLayerConfig(initLayerConfig); + setIsAddNewLayerModalVisible(false); + setIsLayerConfigVisible(true); + setIsNewLayer(true); + addLayer(initLayerConfig); + } + + const dataLayers = [DOCUMENTS]; + const dataLayerItems = Object.values(dataLayers).map((layerItem, index) => { + return ( + onClickAddNewLayer(layerItem.type)} + onFocus={() => setHighlightItem(layerItem)} + onMouseEnter={() => setHighlightItem(layerItem)} + onMouseLeave={() => setHighlightItem(null)} + onBlur={() => setHighlightItem(null)} + > + + + ); + }); + + const referenceLayers = [OPENSEARCH_MAP_LAYER, CUSTOM_MAP]; + const referenceLayersItems = Object.values(referenceLayers).map((layerItem, index) => { + return ( + onClickAddNewLayer(layerItem.type)} + onFocus={() => setHighlightItem(layerItem)} + onMouseEnter={() => setHighlightItem(layerItem)} + onMouseLeave={() => setHighlightItem(null)} + onBlur={() => setHighlightItem(null)} + > + + + ); + }); + + const closeModal = () => setIsAddNewLayerModalVisible(false); + const showModal = () => setIsAddNewLayerModalVisible(true); + + return ( +
+ + + Add layer + + + {isAddNewLayerModalVisible && ( + + + +

Add layer

+
+
+ + + + +
Data layer
+
+ + {dataLayerItems} + + + +
Reference layer
+
+ + {referenceLayersItems} +
+ + +
{highlightItem?.name ? highlightItem.name : 'Select a layer type'}
+
+ + + {highlightItem?.description + ? highlightItem.description + : 'Start creating your map by selecting a layer type.'} + +
+
+
+
+ )} +
+ ); +}; diff --git a/custom_import_map/public/components/add_layer_panel/index.ts b/custom_import_map/public/components/add_layer_panel/index.ts new file mode 100644 index 00000000..d892657c --- /dev/null +++ b/custom_import_map/public/components/add_layer_panel/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { AddLayerPanel } from './add_layer_panel'; diff --git a/custom_import_map/public/components/app.tsx b/custom_import_map/public/components/app.tsx new file mode 100644 index 00000000..207a6906 --- /dev/null +++ b/custom_import_map/public/components/app.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Router, Route, Switch } from 'react-router-dom'; +import { I18nProvider } from '@osd/i18n/react'; +import { MapsList } from './maps_list'; +import { MapPage } from './map_page'; +import { APP_PATH } from '../../common'; +import { useOpenSearchDashboards } from '../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../types'; + +export const MapsDashboardsApp = () => { + const { + services: { appBasePath }, + } = useOpenSearchDashboards(); + // Render the application DOM. + return ( + + +
+ + } /> + } /> + +
+
+
+ ); +}; diff --git a/custom_import_map/public/components/layer_config/base_map_layer_config_panel.tsx b/custom_import_map/public/components/layer_config/base_map_layer_config_panel.tsx new file mode 100644 index 00000000..8b983623 --- /dev/null +++ b/custom_import_map/public/components/layer_config/base_map_layer_config_panel.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { LayerBasicSettings } from './layer_basic_settings'; + +interface Props { + selectedLayerConfig: MapLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + isLayerExists: Function; +} + +export const BaseMapLayerConfigPanel = (props: Props) => { + const tabs = [ + { + id: 'settings--id', + name: 'Settings', + content: ( + + + + + ), + }, + ]; + return ; +}; diff --git a/custom_import_map/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx b/custom_import_map/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx new file mode 100644 index 00000000..5c6d1673 --- /dev/null +++ b/custom_import_map/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/custom_import_map/public/components/layer_config/custom_map_config/custom_map_source.tsx b/custom_import_map/public/components/layer_config/custom_map_config/custom_map_source.tsx new file mode 100644 index 00000000..22ec2d71 --- /dev/null +++ b/custom_import_map/public/components/layer_config/custom_map_config/custom_map_source.tsx @@ -0,0 +1,297 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { EuiSpacer, EuiPanel, EuiForm, EuiFieldText, EuiSelect, EuiFormRow } from '@elastic/eui'; +import { CustomLayerSpecification } from '../../../model/mapLayerType'; + +interface Props { + selectedLayerConfig: CustomLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; +} + +export const CustomMapSource = ({ + selectedLayerConfig, + setSelectedLayerConfig, + setIsUpdateDisabled, +}: Props) => { + const customMapTypeOptions = [ + { value: 'tms', text: 'Tile Map Service (TMS)' }, + { value: 'wms', text: 'Web Map Service (WMS)' }, + ]; + + const [customMapURL, setCustomMapURL] = useState(''); + const [customMapAttribution, setCustomMapAttribution] = useState(''); + const [customType, setCustomType] = useState(customMapTypeOptions[1].value); + const [WMSLayers, setWMSLayers] = useState(''); + const [WMSVersion, setWMSVersion] = useState(''); + const [WMSFormat, setWMSFormat] = useState(''); + const [WMSStyles, setWMSStyles] = useState(''); + // CRS: Coordinate reference systems in WMS + const [WMSCoordinateSystem, setWMSCoordinateSystem] = useState(''); + const [WMSBbox, setWMSBbox] = useState(''); + + const onChangeCustomMapURL = (e: any) => { + setCustomMapURL(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + url: e.target.value, + }, + }); + }; + + const onChangeCustomMapAttribution = (e: any) => { + setCustomMapAttribution(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + attribution: e.target.value, + }, + }); + }; + + const onChangeCustomType = (e: any) => { + setCustomType(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + customType: e.target.value, + }, + }); + }; + + const onChangeWMSLayers = (e: any) => { + setWMSLayers(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + layers: e.target.value, + }, + }); + }; + + const onChangeWMSVersion = (e: any) => { + setWMSVersion(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + version: e.target.value, + }, + }); + }; + + const onChangeWMSFormat = (e: any) => { + setWMSFormat(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + format: e.target.value, + }, + }); + }; + + const onChangeWMSStyles = (e: any) => { + setWMSStyles(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + styles: e.target.value, + }, + }); + }; + + const onChangeWMSCoordinateSystem = (e: any) => { + setWMSCoordinateSystem(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + crs: e.target.value, + }, + }); + }; + + const onChangeWMSBbox = (e: any) => { + setWMSBbox(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + bbox: e.target.value, + }, + }); + }; + + const isInvalidURL = (url: string): boolean => { + if (url === '') return false; + try { + new URL(url); + return false; + } catch (e) { + return true; + } + }; + + useEffect(() => { + setCustomMapURL(selectedLayerConfig.source.url); + setCustomType(selectedLayerConfig.source.customType); + setCustomMapAttribution(selectedLayerConfig.source.attribution); + if (selectedLayerConfig.source.customType === 'wms') { + setWMSLayers(selectedLayerConfig.source.layers); + setWMSVersion(selectedLayerConfig.source.version); + setWMSFormat(selectedLayerConfig.source.format); + setWMSStyles(selectedLayerConfig.source.styles); + setWMSCoordinateSystem(selectedLayerConfig.source.crs); + setWMSBbox(selectedLayerConfig.source.bbox); + } + }, [selectedLayerConfig]); + + useEffect(() => { + setCustomMapAttribution(selectedLayerConfig.source.attribution); + }, [selectedLayerConfig.source.attribution]); + + useEffect(() => { + if (customType === 'wms') { + setIsUpdateDisabled( + customMapURL === '' || + WMSLayers === '' || + WMSVersion === '' || + WMSFormat === '' || + isInvalidURL(customMapURL) + ); + } else { + setIsUpdateDisabled(customMapURL === '' || isInvalidURL(customMapURL)); + } + }, [WMSFormat, WMSLayers, WMSVersion, customMapURL, customType, setIsUpdateDisabled]); + + return ( +
+ + + + + + + + {selectedLayerConfig.source.customType === 'tms' && ( + + + + + + + + + + )} + {selectedLayerConfig.source.customType === 'wms' && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + +
+ ); +}; diff --git a/custom_import_map/public/components/layer_config/documents_config/document_layer_config_panel.tsx b/custom_import_map/public/components/layer_config/documents_config/document_layer_config_panel.tsx new file mode 100644 index 00000000..86854147 --- /dev/null +++ b/custom_import_map/public/components/layer_config/documents_config/document_layer_config_panel.tsx @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import { DocumentLayerSpecification } from '../../../model/mapLayerType'; +import { LayerBasicSettings } from '../layer_basic_settings'; +import { DocumentLayerSource } from './document_layer_source'; +import { DocumentLayerStyle } from './document_layer_style'; + +interface Props { + selectedLayerConfig: DocumentLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + layersIndexPatterns: IndexPattern[]; + isLayerExists: Function; +} + +export const DocumentLayerConfigPanel = (props: Props) => { + const { selectedLayerConfig } = props; + + const checkKeys = [ + 'name', + { + key: 'source', + children: ['indexPatternId', 'geoFieldName'], + }, + ]; + const setIsUpdateDisabled = (isUpdateDisabled: boolean) => { + const check = (obj: any, keys: any) => { + return keys.some((key: any) => { + if (typeof key === 'string') { + return !obj[key]; + } else { + return !obj[key.key] || check(obj[key.key], key.children); + } + }); + }; + props.setIsUpdateDisabled(check(selectedLayerConfig, checkKeys) || isUpdateDisabled); + }; + + const newProps = { + ...props, + setIsUpdateDisabled, + }; + + const tabs = [ + { + id: 'data-source--id', + name: 'Data', + content: ( + + + + + ), + }, + { + id: 'style--id', + name: 'Style', + content: ( + + + + + ), + }, + { + id: 'settings--id', + name: 'Settings', + content: ( + + + + + ), + }, + ]; + return ; +}; diff --git a/custom_import_map/public/components/layer_config/documents_config/document_layer_source.tsx b/custom_import_map/public/components/layer_config/documents_config/document_layer_source.tsx new file mode 100644 index 00000000..ba29eff2 --- /dev/null +++ b/custom_import_map/public/components/layer_config/documents_config/document_layer_source.tsx @@ -0,0 +1,344 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiComboBox, + EuiFlexItem, + EuiFormLabel, + EuiFlexGrid, + EuiFieldNumber, + EuiFormErrorText, + EuiCollapsibleNavGroup, + EuiSpacer, + EuiPanel, + EuiForm, + EuiCheckbox, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import _, { Dictionary } from 'lodash'; +import { Filter, IndexPattern, IndexPatternField } from '../../../../../../src/plugins/data/public'; +import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../../types'; +import { DocumentLayerSpecification } from '../../../model/mapLayerType'; + +interface Props { + setSelectedLayerConfig: Function; + selectedLayerConfig: DocumentLayerSpecification; + setIsUpdateDisabled: Function; + layersIndexPatterns: IndexPattern[]; +} + +export const DocumentLayerSource = ({ + setSelectedLayerConfig, + selectedLayerConfig, + setIsUpdateDisabled, + layersIndexPatterns, +}: Props) => { + const { + services: { + savedObjects: { client: savedObjectsClient }, + data: { + ui: { IndexPatternSelect, SearchBar }, + indexPatterns, + }, + }, + } = useOpenSearchDashboards(); + const [indexPattern, setIndexPattern] = useState(); + const [geoFields, setGeoFields] = useState(); + const [selectedField, setSelectedField] = useState(); + const [documentRequestNumber, setDocumentRequestNumber] = useState( + selectedLayerConfig.source.documentRequestNumber + ); + const [hasInvalidRequestNumber, setHasInvalidRequestNumber] = useState(false); + const [showTooltips, setShowTooltips] = useState( + selectedLayerConfig.source.showTooltips + ); + const [selectedTooltipFields, setSelectedTooltipFields] = useState( + selectedLayerConfig.source.tooltipFields + ); + + const errorsMap = { + datasource: ['Required'], + geoFields: ['Required'], + }; + + useEffect(() => { + const disableUpdate = + !indexPattern || !selectedField || documentRequestNumber < 1 || documentRequestNumber > 10000; + setIsUpdateDisabled(disableUpdate); + }, [setIsUpdateDisabled, indexPattern, selectedField, documentRequestNumber]); + + const formatFieldToComboBox = (field?: IndexPatternField | null) => { + if (!field) return []; + return formatFieldsToComboBox([field]); + }; + + const formatFieldsToComboBox = (fields?: IndexPatternField[]) => { + if (!fields) return []; + + return fields?.map((field) => { + return { + label: field.displayName || field.name, + }; + }); + }; + + const tooltipFieldsOptions = () => { + const fieldList = indexPattern?.fields; + if (!fieldList) return []; + const fieldTypeMap: Dictionary = _.groupBy( + fieldList, + (field) => field.type + ); + + const fieldOptions: Array<{ label: string; options: Array<{ label: string }> }> = []; + let fieldsOfSameType: Array<{ label: string }> = []; + + Object.entries(fieldTypeMap).forEach(([fieldType, fieldEntries]) => { + for (const field of fieldEntries) { + fieldsOfSameType.push({ label: `${field.displayName || field.name}` }); + } + fieldOptions.push({ + label: `${fieldType}`, + options: fieldsOfSameType, + }); + fieldsOfSameType = []; + }); + return fieldOptions; + }; + + const formatTooltipFieldsToComboBox = (fields: string[]) => { + if (!fields) return []; + + return fields?.map((field) => { + return { + label: field, + }; + }); + }; + + const onDocumentRequestNumberChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const selectedNumber = parseInt(value, 10) || 1; + setDocumentRequestNumber(selectedNumber); + const source = { ...selectedLayerConfig.source, documentRequestNumber: selectedNumber }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + + const onTooltipSelectionChange = (options: any[]) => { + const tooltipSelection: string[] = []; + for (const option of options) { + tooltipSelection.push(option.label); + } + setSelectedTooltipFields(tooltipSelection); + const source = { ...selectedLayerConfig.source, tooltipFields: tooltipSelection }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + + const onFiltersUpdated = useCallback( + (filters: Filter[]) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { ...selectedLayerConfig.source, filters }, + }); + }, + [selectedLayerConfig] + ); + + useEffect(() => { + const selectIndexPattern = async () => { + if (selectedLayerConfig.source.indexPatternId) { + const selectedIndexPattern = layersIndexPatterns.find( + (ip) => ip.id === selectedLayerConfig.source.indexPatternId + ); + setIndexPattern(selectedIndexPattern); + } + }; + selectIndexPattern(); + }, [indexPatterns]); + + // Update the fields list every time the index pattern is modified. + useEffect(() => { + const acceptedFieldTypes = ['geo_point', 'geo_shape']; + const fields = indexPattern?.fields.filter( + (field) => acceptedFieldTypes.indexOf(field.type) !== -1 + ); + setGeoFields(fields); + fields?.filter((field) => field.displayName === selectedLayerConfig.source.geoFieldName); + const savedField = fields?.find( + (field) => field.name === selectedLayerConfig.source.geoFieldName + ); + setSelectedField(savedField); + if (selectedLayerConfig.source.indexPatternId === indexPattern?.id) { + setSelectedTooltipFields(selectedLayerConfig.source.tooltipFields); + } else { + setSelectedTooltipFields([]); + } + }, [indexPattern]); + + useEffect(() => { + const setLayerSource = () => { + if (!indexPattern || !selectedField) return; + const source = { + ...selectedLayerConfig.source, + indexPatternRefName: indexPattern?.title, + indexPatternId: indexPattern?.id, + geoFieldName: selectedField?.displayName, + geoFieldType: selectedField?.type, + }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + setLayerSource(); + }, [selectedField]); + + useEffect(() => { + setHasInvalidRequestNumber(documentRequestNumber < 1 || documentRequestNumber > 10000); + }, [documentRequestNumber]); + + const onShowTooltipsChange = (event: { target: { checked: React.SetStateAction } }) => { + setShowTooltips(event.target.checked); + const source = { ...selectedLayerConfig.source, showTooltips: event.target.checked }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + + const shouldTooltipSectionOpen = () => { + return ( + selectedLayerConfig.source.showTooltips && + selectedLayerConfig.source.tooltipFields?.length > 0 + ); + }; + + return ( +
+ + + + + + + { + const newIndexPattern = await indexPatterns.get(newIndexPatternId); + setIndexPattern(newIndexPattern); + }} + isClearable={false} + /> + + + + + { + const field = indexPattern?.getFieldByName(option[0].label); + setSelectedField(field || null); + }} + sortMatchesBy="startsWith" + placeholder={i18n.translate('documentLayer.selectDataFieldPlaceholder', { + defaultMessage: 'Select data field', + })} + /> + + + + Number of documents + + + {hasInvalidRequestNumber && ( + + + + )} + + + + + + + + 0} + > + + + + + + + + + + + + Tooltip Fields + + + + + + +
+ ); +}; diff --git a/custom_import_map/public/components/layer_config/documents_config/document_layer_style.tsx b/custom_import_map/public/components/layer_config/documents_config/document_layer_style.tsx new file mode 100644 index 00000000..d5b3d361 --- /dev/null +++ b/custom_import_map/public/components/layer_config/documents_config/document_layer_style.tsx @@ -0,0 +1,234 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiColorPicker, + useColorPickerState, + EuiFieldNumber, + EuiFormLabel, + EuiFormErrorText, + EuiFlexItem, + EuiSpacer, + EuiButtonGroup, + EuiPanel, + EuiTitle, + EuiFormRow, + EuiForm, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { DocumentLayerSpecification } from '../../../model/mapLayerType'; + +interface Props { + selectedLayerConfig: DocumentLayerSpecification; + setSelectedLayerConfig: Function; +} + +export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig }: Props) => { + const [fillColor, setFillColor] = useColorPickerState(selectedLayerConfig?.style?.fillColor); + const [borderColor, setBorderColor] = useColorPickerState( + selectedLayerConfig?.style?.borderColor + ); + const [borderThickness, setBorderThickness] = useState( + selectedLayerConfig?.style?.borderThickness + ); + const [markerSize, setMarkerSize] = useState(selectedLayerConfig?.style?.markerSize); + const [hasInvalidThickness, setHasInvalidThickness] = useState(false); + const [hasInvalidSize, setHasInvalidSize] = useState(false); + const geoTypeToggleButtonGroupPrefix = 'geoTypeToggleButtonGroup'; + const [toggleGeoTypeIdSelected, setToggleGeoTypeIdSelected] = useState( + `${geoTypeToggleButtonGroupPrefix}__Point` + ); + + useEffect(() => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + fillColor, + }, + }); + }, [fillColor]); + + useEffect(() => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + borderColor, + }, + }); + }, [borderColor]); + + const onBorderThicknessChange = (e: any) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + borderThickness: Number(e.target.value), + }, + }); + setBorderThickness(Number(e.target.value)); + }; + + const onMarkerSizeChange = (e: any) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + markerSize: Number(e.target.value), + }, + }); + setMarkerSize(Number(e.target.value)); + }; + + useEffect(() => { + if (borderThickness < 0 || borderThickness > 100) { + setHasInvalidThickness(true); + } else { + setHasInvalidThickness(false); + } + }, [borderThickness]); + + useEffect(() => { + if (markerSize < 0 || markerSize > 100) { + setHasInvalidSize(true); + } else { + setHasInvalidSize(false); + } + }, [markerSize]); + + const toggleButtonsGeoType = [ + { + id: `${geoTypeToggleButtonGroupPrefix}__Point`, + label: 'Points', + }, + { + id: `${geoTypeToggleButtonGroupPrefix}__Line`, + label: 'Lines', + }, + { + id: `${geoTypeToggleButtonGroupPrefix}__Polygon`, + label: 'Polygons', + }, + ]; + + const onChangeGeoTypeSelected = (optionId: string) => { + setToggleGeoTypeIdSelected(optionId); + }; + + interface ColorPickerProps { + color: string; + setColor: Function; + label: string; + } + + const ColorPicker = ({ color, setColor, label }: ColorPickerProps) => { + return ( + + {}} + fullWidth={true} + /> + + ); + }; + + interface WidthSelectorProps { + size: number; + onWidthChange: Function; + label: string; + hasInvalid: boolean; + } + + const WidthSelector = ({ label, onWidthChange, size, hasInvalid }: WidthSelectorProps) => { + return ( + + + px} + fullWidth={true} + /> + {hasInvalid && ( + + + + )} + + + ); + }; + + return ( + + +

Layer style

+
+ + onChangeGeoTypeSelected(id)} + buttonSize="compressed" + /> + + + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Point` && ( + + + + + + + )} + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Line` && ( + + + + + )} + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Polygon` && ( + + + + + + )} + +
+ ); +}; diff --git a/custom_import_map/public/components/layer_config/index.ts b/custom_import_map/public/components/layer_config/index.ts new file mode 100644 index 00000000..4d043e76 --- /dev/null +++ b/custom_import_map/public/components/layer_config/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { LayerConfigPanel } from './layer_config_panel'; +export { BaseMapLayerConfigPanel } from './base_map_layer_config_panel'; diff --git a/custom_import_map/public/components/layer_config/layer_basic_settings.tsx b/custom_import_map/public/components/layer_config/layer_basic_settings.tsx new file mode 100644 index 00000000..67fbfb56 --- /dev/null +++ b/custom_import_map/public/components/layer_config/layer_basic_settings.tsx @@ -0,0 +1,153 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { + EuiDualRange, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiTitle, + EuiSpacer, + EuiRange, + EuiPanel, + EuiFormLabel, + EuiTextArea, +} from '@elastic/eui'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { + MAP_DEFAULT_MIN_ZOOM, + MAP_DEFAULT_MAX_ZOOM, + MAP_LAYER_DEFAULT_MIN_OPACITY, + MAP_LAYER_DEFAULT_MAX_OPACITY, + MAP_LAYER_DEFAULT_OPACITY_STEP, + MAX_LAYER_NAME_LIMIT, +} from '../../../common'; +import { layersTypeNameMap } from '../../model/layersFunctions'; + +interface Props { + selectedLayerConfig: MapLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + isLayerExists: Function; +} + +export const LayerBasicSettings = ({ + selectedLayerConfig, + setSelectedLayerConfig, + setIsUpdateDisabled, + isLayerExists, +}: Props) => { + const [invalid, setInvalid] = useState(selectedLayerConfig.name.length === 0); + const [errors, setErrors] = useState([]); + + const validateName = (name: string) => { + if (name?.length === 0) { + setInvalid(true); + setErrors(['Name cannot be empty']); + return; + } + if (MAX_LAYER_NAME_LIMIT < name?.length) { + setInvalid(true); + setErrors(['Name should be less than ' + MAX_LAYER_NAME_LIMIT + ' characters']); + return; + } + if (isLayerExists(name)) { + setInvalid(true); + setErrors(['Name already exists']); + return; + } + setInvalid(false); + return; + }; + + const { name } = selectedLayerConfig; + + useEffect(() => { + const disableUpdate = !name || invalid; + setIsUpdateDisabled(disableUpdate); + }, [setIsUpdateDisabled, name, invalid]); + + const commonUpdate = (key: string, value: any) => { + const newLayerConfig = { ...selectedLayerConfig, [key]: value }; + setSelectedLayerConfig(newLayerConfig); + }; + const onZoomChange = (value: number[]) => { + commonUpdate('zoomRange', value); + }; + + const onOpacityChange = (e: any) => { + commonUpdate('opacity', Number(e.target.value)); + }; + + const onNameChange = (e: any) => { + const layerName = String(e.target.value); + validateName(layerName); + commonUpdate('name', layerName); + }; + + const onDescriptionChange = (e: any) => { + commonUpdate('description', String(e.target.value)); + }; + + return ( + + +

Layer settings

+
+ + + + + + + + + + + + + + + + + + %} + /> + + +
+ ); +}; diff --git a/custom_import_map/public/components/layer_config/layer_config_panel.tsx b/custom_import_map/public/components/layer_config/layer_config_panel.tsx new file mode 100644 index 00000000..e9a15d7f --- /dev/null +++ b/custom_import_map/public/components/layer_config/layer_config_panel.tsx @@ -0,0 +1,175 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { cloneDeep, isEqual } from 'lodash'; + +import { + EuiButton, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlexItem, + EuiButtonEmpty, + EuiFlexGroup, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiIcon, +} from '@elastic/eui'; + +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { BaseMapLayerConfigPanel } from './index'; +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; + selectedLayerConfig: MapLayerSpecification; + setSelectedLayerConfig: Function; + updateLayer: Function; + removeLayer: Function; + isNewLayer: boolean; + setIsNewLayer: Function; + layersIndexPatterns: IndexPattern[]; + updateIndexPatterns: Function; + isLayerExists: Function; +} + +export const LayerConfigPanel = ({ + closeLayerConfigPanel, + selectedLayerConfig, + setSelectedLayerConfig, + updateLayer, + removeLayer, + isNewLayer, + setIsNewLayer, + layersIndexPatterns, + updateIndexPatterns, + isLayerExists, +}: Props) => { + const [isUpdateDisabled, setIsUpdateDisabled] = useState(false); + const [originLayerConfig, setOriginLayerConfig] = useState(null); + const [warnModalVisible, setWarnModalVisible] = useState(false); + + useEffect(() => { + setOriginLayerConfig(cloneDeep(selectedLayerConfig)); + }, []); + + const discardChanges = () => { + closeLayerConfigPanel(false); + setSelectedLayerConfig(undefined); + }; + const onClose = () => { + if (isEqual(originLayerConfig, selectedLayerConfig)) { + discardChanges(); + } else { + setWarnModalVisible(true); + } + if (isNewLayer) { + removeLayer(selectedLayerConfig.id); + setIsNewLayer(false); + } + }; + const onUpdate = () => { + updateLayer(); + closeLayerConfigPanel(false); + if (isNewLayer) { + setIsNewLayer(false); + } + }; + + const closeModal = () => { + setWarnModalVisible(false); + }; + + return ( + + + + + + + + {selectedLayerConfig.name} + + + + + + + {selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP && ( + + )} + {selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS && ( + + )} + {selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP && ( + + )} + + + + + + + + Discard + + + + + Update + + + + + {warnModalVisible && ( + + + Unsaved changes + + +

Do you want to discard the changes?

+
+ + Cancel + + Discard + + +
+ )} +
+ ); +}; diff --git a/custom_import_map/public/components/layer_control_panel/index.ts b/custom_import_map/public/components/layer_control_panel/index.ts new file mode 100644 index 00000000..30cd7379 --- /dev/null +++ b/custom_import_map/public/components/layer_control_panel/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { LayerControlPanel } from './layer_control_panel'; diff --git a/custom_import_map/public/components/layer_control_panel/layer_control_panel.scss b/custom_import_map/public/components/layer_control_panel/layer_control_panel.scss new file mode 100644 index 00000000..b3ad8c0c --- /dev/null +++ b/custom_import_map/public/components/layer_control_panel/layer_control_panel.scss @@ -0,0 +1,32 @@ +.layerControlPanel--show { + pointer-events: auto; + width: $euiSizeL * 11; + + .layerControlPanel__title { + padding: $euiSizeM $euiSizeM + } + + .layerControlPanel__selected { + background-color: $euiColorLightShade; + } + + .layerControlPanel__layerFunctionButton { + height: $euiSizeL; + width: $euiSizeL; + } + + .euiListGroupItem__label { + width: $euiSizeL * 6; + } +} + +.layerControlPanel--hide { + pointer-events: auto; + + .layerControlPanel__visButton { + background-color: $euiColorEmptyShade; + color: $euiTextColor; + border-color: $euiColorLightShade; + } +} + diff --git a/custom_import_map/public/components/layer_control_panel/layer_control_panel.tsx b/custom_import_map/public/components/layer_control_panel/layer_control_panel.tsx new file mode 100644 index 00000000..14a7d9f2 --- /dev/null +++ b/custom_import_map/public/components/layer_control_panel/layer_control_panel.tsx @@ -0,0 +1,462 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo, useEffect, useState } from 'react'; +import { + EuiPanel, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiListGroupItem, + EuiButtonEmpty, + EuiHorizontalRule, + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiConfirmModal, + DropResult, +} from '@elastic/eui'; +import { I18nProvider } from '@osd/i18n/react'; +import { Map as Maplibre } from 'maplibre-gl'; +import './layer_control_panel.scss'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { AddLayerPanel } from '../add_layer_panel'; +import { LayerConfigPanel } from '../layer_config'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { + LAYER_VISIBILITY, + DASHBOARDS_MAPS_LAYER_TYPE, + LAYER_ICON_TYPE_MAP, + LAYER_PANEL_SHOW_LAYER_ICON, + LAYER_PANEL_HIDE_LAYER_ICON, +} from '../../../common'; +import { + LayerActions, + layersFunctionMap, + referenceLayerTypeLookup, +} from '../../model/layersFunctions'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; +import { + handleReferenceLayerRender, + handleDataLayerRender, +} from '../../model/layerRenderController'; +import { MapState } from '../../model/mapState'; + +interface MaplibreRef { + current: Maplibre | null; +} + +interface Props { + maplibreRef: MaplibreRef; + setLayers: (layers: MapLayerSpecification[]) => void; + layers: MapLayerSpecification[]; + layersIndexPatterns: IndexPattern[]; + setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; + mapState: MapState; +} + +export const LayerControlPanel = memo( + ({ + maplibreRef, + setLayers, + layers, + layersIndexPatterns, + setLayersIndexPatterns, + mapState, + }: Props) => { + const { services } = useOpenSearchDashboards(); + const { + data: { indexPatterns }, + } = services; + + const [isLayerConfigVisible, setIsLayerConfigVisible] = useState(false); + const [isLayerControlVisible, setIsLayerControlVisible] = useState(true); + const [selectedLayerConfig, setSelectedLayerConfig] = useState< + MapLayerSpecification | undefined + >(); + const [initialLayersLoaded, setInitialLayersLoaded] = useState(false); + const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(false); + const [isNewLayer, setIsNewLayer] = useState(false); + const [isDeleteLayerModalVisible, setIsDeleteLayerModalVisible] = useState(false); + const [selectedDeleteLayer, setSelectedDeleteLayer] = useState< + MapLayerSpecification | undefined + >(); + + useEffect(() => { + if (!isUpdatingLayerRender && initialLayersLoaded) { + return; + } + if (layers.length <= 0) { + return; + } + + if (initialLayersLoaded) { + if (!selectedLayerConfig) { + return; + } + if ( + selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP || + selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP + ) { + handleReferenceLayerRender(selectedLayerConfig, maplibreRef, undefined); + } else { + updateIndexPatterns(); + handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef, undefined); + } + setSelectedLayerConfig(undefined); + } else { + layers.forEach((layer) => { + const beforeLayerId = getMapBeforeLayerId(layer); + if (referenceLayerTypeLookup[layer.type]) { + handleReferenceLayerRender(layer, maplibreRef, beforeLayerId); + } else { + handleDataLayerRender(layer, mapState, services, maplibreRef, beforeLayerId); + } + }); + setInitialLayersLoaded(true); + } + setIsUpdatingLayerRender(false); + }, [layers]); + + // Get layer id from layers that is above the selected layer + function getMapBeforeLayerId(selectedLayer: MapLayerSpecification): string | undefined { + const selectedLayerIndex = layers.findIndex((layer) => layer.id === selectedLayer.id); + const beforeLayers = layers.slice(selectedLayerIndex + 1); + if (beforeLayers.length === 0) { + return undefined; + } + return beforeLayers[0]?.id; + } + + const closeLayerConfigPanel = () => { + setIsLayerConfigVisible(false); + setTimeout(() => { + maplibreRef.current?.resize(); + }, 0); + }; + + const newLayerIndex = () => { + return layers?.length + 1; + }; + + const addLayer = (layer: MapLayerSpecification) => { + setLayers([...layers, layer]); + }; + + const updateLayer = () => { + if (!selectedLayerConfig) { + return; + } + const layersClone = [...layers]; + const index = layersClone.findIndex((layer) => layer.id === selectedLayerConfig.id); + if (index <= -1) { + layersClone.push(selectedLayerConfig); + } else { + layersClone[index] = { + ...layersClone[index], + ...selectedLayerConfig, + }; + } + setLayers(layersClone); + setIsUpdatingLayerRender(true); + }; + + const removeLayer = (layerId: string) => { + const layersClone = [...layers]; + const index = layersClone.findIndex((layer) => layer.id === layerId); + if (index > -1) { + layersClone.splice(index, 1); + setLayers(layersClone); + } + }; + + const onClickLayerName = (layer: MapLayerSpecification) => { + setSelectedLayerConfig(layer); + setIsLayerConfigVisible(true); + }; + const isLayerExists = (name: string) => { + return layers.findIndex((layer) => layer.name === name) > -1; + }; + + const [layerVisibility, setLayerVisibility] = useState(new Map([])); + layers.forEach((layer) => { + layerVisibility.set(layer.id, layer.visibility === LAYER_VISIBILITY.VISIBLE); + }); + + const beforeMaplibreLayerID = (source: number, destination: number) => { + if (source > destination) { + // if layer is moved below, move current layer below given destination + return layers[destination].id; + } + const beforeIndex = destination + 1; // if layer is moved up, move current layer above destination + if (beforeIndex < layers.length) { + return layers[beforeIndex].id; + } + return undefined; + }; + + const onDragEnd = (dropResult: DropResult) => { + if (!dropResult) { + return; + } + if (dropResult.source && dropResult.destination) { + // we display list in reverse order + const prevIndex = getLayerIndex(dropResult.source.index); + const newIndex = getLayerIndex(dropResult.destination.index); + + const currentMaplibreLayerId = layers[prevIndex].id; + const beforeMaplibreLayerId = beforeMaplibreLayerID(prevIndex, newIndex); + LayerActions.move(maplibreRef, currentMaplibreLayerId, beforeMaplibreLayerId); + + // update map layers + const layersClone = [...layers]; + const oldLayer = layersClone[prevIndex]; + layersClone.splice(prevIndex, 1); + layersClone.splice(newIndex, 0, oldLayer); + setLayers(layersClone); + } + }; + + const getLayerIndex = (reversedIndex: number) => { + return layers.length - reversedIndex - 1; + }; + + const getReverseLayers = () => { + const layersClone = [...layers]; + return layersClone.reverse(); + }; + + const updateIndexPatterns = async () => { + if (!selectedLayerConfig) { + return; + } + if (referenceLayerTypeLookup[selectedLayerConfig.type]) { + return; + } + const findIndexPattern = layersIndexPatterns.find( + // @ts-ignore + (indexPattern) => indexPattern.id === selectedLayerConfig.source.indexPatternId + ); + if (!findIndexPattern) { + // @ts-ignore + const newIndexPattern = await indexPatterns.get(selectedLayerConfig.source.indexPatternId); + const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; + setLayersIndexPatterns(cloneLayersIndexPatterns); + } + }; + + const onLayerVisibilityChange = (layer: MapLayerSpecification) => { + if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { + layer.visibility = LAYER_VISIBILITY.NONE; + setLayerVisibility(new Map(layerVisibility.set(layer.id, false))); + } else { + layer.visibility = LAYER_VISIBILITY.VISIBLE; + setLayerVisibility(new Map(layerVisibility.set(layer.id, true))); + } + layersFunctionMap[layer.type]?.hide(maplibreRef, layer); + }; + + const onDeleteLayerIconClick = (layer: MapLayerSpecification) => { + setSelectedDeleteLayer(layer); + setIsDeleteLayerModalVisible(true); + }; + + const onDeleteLayerConfirm = () => { + if (selectedDeleteLayer) { + layersFunctionMap[selectedDeleteLayer.type]?.remove(maplibreRef, selectedDeleteLayer); + removeLayer(selectedDeleteLayer.id); + setIsDeleteLayerModalVisible(false); + setSelectedDeleteLayer(undefined); + } + }; + + const onCancelDeleteLayer = () => { + setIsDeleteLayerModalVisible(false); + setSelectedDeleteLayer(undefined); + }; + + let deleteLayerModal; + if (isDeleteLayerModalVisible) { + deleteLayerModal = ( + +

+ Do you want to delete layer {selectedDeleteLayer?.name}? +

+
+ ); + } + + if (isLayerControlVisible) { + return ( + + + + + + +

Layer

+
+
+ + setIsLayerControlVisible((visible) => !visible)} + aria-label="Hide layer control" + color="text" + className="layerControlPanel__visButton" + title="Collapse layers panel" + /> + +
+ + + + {getReverseLayers().map((layer, index) => { + const isLayerSelected = + isLayerConfigVisible && + selectedLayerConfig && + selectedLayerConfig.id === layer.id; + return ( + + {(provided) => ( +
+ + + onClickLayerName(layer)} + /> + + + + onLayerVisibilityChange(layer)} + aria-label="Hide or show layer" + color="text" + title={ + layerVisibility.get(layer.id) ? 'Hide layer' : 'Show layer' + } + /> + + + onDeleteLayerIconClick(layer)} + aria-label="Delete layer" + color={layer.id === selectedLayerConfig?.id ? 'text' : 'danger'} + title="Delete layer" + disabled={layer.id === selectedLayerConfig?.id} + /> + + + + + + + +
+ )} +
+ ); + })} +
+
+ {isLayerConfigVisible && selectedLayerConfig && ( + + )} + + {deleteLayerModal} +
+
+
+ ); + } + + return ( + + setIsLayerControlVisible((visible) => !visible)} + aria-label="Show layer control" + title="Expand layers panel" + /> + + ); + } +); diff --git a/custom_import_map/public/components/map_container/index.ts b/custom_import_map/public/components/map_container/index.ts new file mode 100644 index 00000000..b0716344 --- /dev/null +++ b/custom_import_map/public/components/map_container/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapContainer } from './map_container'; diff --git a/custom_import_map/public/components/map_container/map_container.scss b/custom_import_map/public/components/map_container/map_container.scss new file mode 100644 index 00000000..7453ca94 --- /dev/null +++ b/custom_import_map/public/components/map_container/map_container.scss @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +@import "maplibre-gl/dist/maplibre-gl.css"; +@import "../../variables"; + +/* stylelint-disable no-empty-source */ +.map-container { + width: 100%; + min-height: calc(100vh - #{$mapHeaderOffset}); +} + +.maplibregl-ctrl-top-left { + left: $euiSizeS; + top: $euiSizeS; +} + +.layerControlPanel-container { + z-index: 1; + position: absolute; + margin-left: $euiSizeS; + margin-top: $euiSizeS; +} + +.zoombar { + z-index: 1; + position: absolute; + bottom: $euiSizeM; + right: $euiSizeS; +} diff --git a/custom_import_map/public/components/map_container/map_container.tsx b/custom_import_map/public/components/map_container/map_container.tsx new file mode 100644 index 00000000..d2e71027 --- /dev/null +++ b/custom_import_map/public/components/map_container/map_container.tsx @@ -0,0 +1,153 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { LngLat, Map as Maplibre, MapMouseEvent, NavigationControl, Popup } from 'maplibre-gl'; +import { LayerControlPanel } from '../layer_control_panel'; +import './map_container.scss'; +import { MAP_INITIAL_STATE, MAP_GLYPHS } 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'; + +interface MapContainerProps { + setLayers: (layers: MapLayerSpecification[]) => void; + layers: MapLayerSpecification[]; + layersIndexPatterns: IndexPattern[]; + setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; + maplibreRef: React.MutableRefObject; + mapState: MapState; +} + +export const MapContainer = ({ + setLayers, + layers, + layersIndexPatterns, + setLayersIndexPatterns, + maplibreRef, + mapState, +}: MapContainerProps) => { + const mapContainer = useRef(null); + const [mounted, setMounted] = useState(false); + const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); + const [coordinates, setCoordinates] = useState(); + + useEffect(() => { + if (!mapContainer.current) return; + const mbStyle = { + version: 8 as 8, + sources: {}, + layers: [], + glyphs: MAP_GLYPHS, + }; + + 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))); + }); + }, []); + + 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) { + // remove previous popup + clickPopup?.remove(); + + const features = maplibreRef.current?.queryRenderedFeatures(e.point); + if (features && maplibreRef.current) { + clickPopup = createPopup({ features, layers: tooltipEnabledLayers }); + clickPopup + ?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat) + .addTo(maplibreRef.current); + } + } + + function onMouseMoveMap(e: MapMouseEvent) { + setCoordinates(e.lngLat.wrap()); + + // remove previous popup + hoverPopup?.remove(); + + const features = maplibreRef.current?.queryRenderedFeatures(e.point); + if (features && maplibreRef.current) { + hoverPopup = createPopup({ + features, + layers: tooltipEnabledLayers, + showCloseButton: false, + showPagination: false, + showLayerSelection: false, + }); + hoverPopup + ?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat) + .addTo(maplibreRef.current); + } + } + + if (maplibreRef.current) { + const map = maplibreRef.current; + map.on('click', onClickMap); + // reset cursor to default when user is no longer hovering over a clickable feature + map.on('mouseleave', () => { + map.getCanvas().style.cursor = ''; + hoverPopup?.remove(); + }); + map.on('mouseenter', () => { + map.getCanvas().style.cursor = 'pointer'; + }); + // add tooltip when users mouse move over a point + map.on('mousemove', onMouseMoveMap); + } + + return () => { + if (maplibreRef.current) { + maplibreRef.current.off('click', onClickMap); + maplibreRef.current.off('mousemove', onMouseMoveMap); + } + }; + }, [layers]); + + return ( +
+ + + {coordinates && + `lat: ${coordinates.lat.toFixed(4)}, lon: ${coordinates.lng.toFixed(4)}, `} + zoom: {zoom} + + +
+ {mounted && ( + + )} +
+
+
+ ); +}; diff --git a/custom_import_map/public/components/map_page/index.ts b/custom_import_map/public/components/map_page/index.ts new file mode 100644 index 00000000..a79e0689 --- /dev/null +++ b/custom_import_map/public/components/map_page/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapPage } from './map_page'; diff --git a/custom_import_map/public/components/map_page/map_page.tsx b/custom_import_map/public/components/map_page/map_page.tsx new file mode 100644 index 00000000..d8dc8d3d --- /dev/null +++ b/custom_import_map/public/components/map_page/map_page.tsx @@ -0,0 +1,86 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { SimpleSavedObject } from 'opensearch-dashboards/public'; +import { Map as Maplibre } from 'maplibre-gl'; +import { MapContainer } from '../map_container'; +import { MapTopNavMenu } from '../map_top_nav'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { MapServices } from '../../types'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { + DASHBOARDS_MAPS_LAYER_TYPE, + MAP_LAYER_DEFAULT_NAME, + OPENSEARCH_MAP_LAYER, +} from '../../../common'; +import { getLayerConfigMap, getInitialMapState } from '../../utils/getIntialConfig'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { MapState } from '../../model/mapState'; + +export const MapPage = () => { + const { services } = useOpenSearchDashboards(); + const { + savedObjects: { client: savedObjectsClient }, + } = services; + const [layers, setLayers] = useState([]); + const { id: mapIdFromUrl } = useParams<{ id: string }>(); + const [savedMapObject, setSavedMapObject] = + useState | null>(); + const [layersIndexPatterns, setLayersIndexPatterns] = useState([]); + const maplibreRef = useRef(null); + const [mapState, setMapState] = useState(getInitialMapState()); + + useEffect(() => { + if (mapIdFromUrl) { + savedObjectsClient.get('map', mapIdFromUrl).then((res) => { + setSavedMapObject(res); + const layerList: MapLayerSpecification[] = JSON.parse(res.attributes.layerList as string); + const savedMapState: MapState = JSON.parse(res.attributes.mapState as string); + setMapState(savedMapState); + setLayers(layerList); + const savedIndexPatterns: IndexPattern[] = []; + layerList.forEach(async (layer: MapLayerSpecification) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const indexPatternId = layer.source.indexPatternId; + const indexPattern = await services.data.indexPatterns.get(indexPatternId); + savedIndexPatterns.push(indexPattern); + } + }); + setLayersIndexPatterns(savedIndexPatterns); + }); + } else { + const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap()[ + OPENSEARCH_MAP_LAYER.type + ]; + initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; + setLayers([initialDefaultLayer]); + } + }, []); + + return ( +
+ + +
+ ); +}; diff --git a/custom_import_map/public/components/map_top_nav/get_top_nav_config.tsx b/custom_import_map/public/components/map_top_nav/get_top_nav_config.tsx new file mode 100644 index 00000000..4269c356 --- /dev/null +++ b/custom_import_map/public/components/map_top_nav/get_top_nav_config.tsx @@ -0,0 +1,133 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; +import { + OnSaveProps, + SavedObjectSaveModalOrigin, + showSaveModal, + checkForDuplicateTitle, +} from '../../../../../src/plugins/saved_objects/public'; +import { MapServices } from '../../types'; +import { MapState } from '../../model/mapState'; + +const SAVED_OBJECT_TYPE = 'map'; + +interface GetTopNavConfigParams { + mapIdFromUrl: string; + layers: any; + title: string; + description: string; + setTitle: (title: string) => void; + setDescription: (description: string) => void; + mapState: MapState; +} + +export const getTopNavConfig = ( + { + notifications: { toasts }, + i18n: { Context: I18nContext }, + savedObjects: { client: savedObjectsClient }, + history, + overlays, + }: MapServices, + { + mapIdFromUrl, + layers, + title, + description, + setTitle, + setDescription, + mapState, + }: GetTopNavConfigParams +) => { + const topNavConfig: TopNavMenuData[] = [ + { + iconType: 'save', + emphasize: true, + id: 'save', + label: i18n.translate('maps.topNav.saveMapButtonLabel', { + defaultMessage: `Save`, + }), + run: (_anchorElement) => { + const onModalSave = async ({ newTitle, newDescription, onTitleDuplicate }: OnSaveProps) => { + let newlySavedMap; + const saveAttributes = { + title: newTitle, + description: newDescription, + layerList: JSON.stringify(layers), + mapState: JSON.stringify(mapState), + }; + try { + await checkForDuplicateTitle( + { + title: newTitle, + lastSavedTitle: title, + copyOnSave: false, + getDisplayName: () => SAVED_OBJECT_TYPE, + getOpenSearchType: () => SAVED_OBJECT_TYPE, + }, + false, + onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } catch (_error) { + return {}; + } + if (mapIdFromUrl) { + // edit existing map + newlySavedMap = await savedObjectsClient.update( + SAVED_OBJECT_TYPE, + mapIdFromUrl, + saveAttributes + ); + } else { + // save new map + newlySavedMap = await savedObjectsClient.create(SAVED_OBJECT_TYPE, saveAttributes); + } + const id = newlySavedMap.id; + if (id) { + history.push({ + ...history.location, + pathname: `${id}`, + }); + setTitle(newTitle); + setDescription(newDescription); + toasts.addSuccess({ + title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', { + defaultMessage: `Saved ${newTitle}`, + values: { + visTitle: newTitle, + }, + }), + }); + } + return { id }; + }; + + const documentInfo = { + title, + description, + }; + + const saveModal = ( + {}} + /> + ); + showSaveModal(saveModal, I18nContext); + }, + }, + ]; + return topNavConfig; +}; diff --git a/custom_import_map/public/components/map_top_nav/index.ts b/custom_import_map/public/components/map_top_nav/index.ts new file mode 100644 index 00000000..c732aa00 --- /dev/null +++ b/custom_import_map/public/components/map_top_nav/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapTopNavMenu } from './top_nav_menu'; diff --git a/custom_import_map/public/components/map_top_nav/top_nav_menu.tsx b/custom_import_map/public/components/map_top_nav/top_nav_menu.tsx new file mode 100644 index 00000000..15b2fbe3 --- /dev/null +++ b/custom_import_map/public/components/map_top_nav/top_nav_menu.tsx @@ -0,0 +1,139 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { SimpleSavedObject } from 'opensearch-dashboards/public'; +import { IndexPattern, Query, TimeRange } from '../../../../../src/plugins/data/public'; +import { DASHBOARDS_MAPS_LAYER_TYPE, PLUGIN_NAVIGATION_BAR_ID } from '../../../common'; +import { getTopNavConfig } from './get_top_nav_config'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { getSavedMapBreadcrumbs } from '../../utils/breadcrumbs'; +import { handleDataLayerRender } from '../../model/layerRenderController'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { MapState } from '../../model/mapState'; + +interface MapTopNavMenuProps { + mapIdFromUrl: string; + layers: MapLayerSpecification[]; + savedMapObject: SimpleSavedObject | null | undefined; + layersIndexPatterns: IndexPattern[]; + maplibreRef: any; + mapState: MapState; + setMapState: (mapState: MapState) => void; +} + +export const MapTopNavMenu = ({ + mapIdFromUrl, + savedMapObject, + layers, + layersIndexPatterns, + maplibreRef, + mapState, + setMapState, +}: MapTopNavMenuProps) => { + const { services } = useOpenSearchDashboards(); + const { + setHeaderActionMenu, + navigation: { + ui: { TopNavMenu }, + }, + chrome, + application: { navigateToApp }, + } = services; + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [queryConfig, setQueryConfig] = useState({ query: '', language: 'kuery' }); + const [refreshIntervalValue, setRefreshIntervalValue] = useState(60000); + const [isRefreshPaused, setIsRefreshPaused] = useState(false); + const changeTitle = useCallback( + (newTitle: string) => { + chrome.setBreadcrumbs(getSavedMapBreadcrumbs(newTitle, navigateToApp)); + chrome.docTitle.change(newTitle); + }, + [chrome, navigateToApp] + ); + + useEffect(() => { + if (savedMapObject) { + setTitle(savedMapObject.attributes.title); + setDescription(savedMapObject.attributes.description!); + } + }, [savedMapObject, mapIdFromUrl]); + + useEffect(() => { + changeTitle(title || 'Create'); + }, [title, changeTitle]); + + const refreshDataLayerRender = () => { + layers.forEach((layer: MapLayerSpecification) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + return; + } + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); + }); + }; + + const handleQuerySubmit = ({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => { + if (query) { + setMapState({ ...mapState, query }); + } + if (dateRange) { + setMapState({ ...mapState, timeRange: dateRange }); + } + }; + + useEffect(() => { + setDateFrom(mapState.timeRange.from); + setDateTo(mapState.timeRange.to); + setQueryConfig(mapState.query); + setIsRefreshPaused(mapState.refreshInterval.pause); + setRefreshIntervalValue(mapState.refreshInterval.value); + refreshDataLayerRender(); + }, [mapState]); + + const onRefreshChange = useCallback( + ({ isPaused, refreshInterval }: { isPaused: boolean; refreshInterval: number }) => { + setIsRefreshPaused(isPaused); + setRefreshIntervalValue(refreshInterval); + }, + [] + ); + + return ( + + ); +}; diff --git a/custom_import_map/public/components/maps_list/index.ts b/custom_import_map/public/components/maps_list/index.ts new file mode 100644 index 00000000..11ecaf3e --- /dev/null +++ b/custom_import_map/public/components/maps_list/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapsList } from './maps_list'; diff --git a/custom_import_map/public/components/maps_list/maps_list.tsx b/custom_import_map/public/components/maps_list/maps_list.tsx new file mode 100644 index 00000000..3dbc5c9d --- /dev/null +++ b/custom_import_map/public/components/maps_list/maps_list.tsx @@ -0,0 +1,151 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React, { useCallback, useEffect } from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiLink, + EuiButton, + EuiPageHeader, +} from '@elastic/eui'; +import { + TableListView, + useOpenSearchDashboards, +} from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { MapServices } from '../../types'; +import { getMapsLandingBreadcrumbs } from '../../utils/breadcrumbs'; +import { APP_PATH, PLUGIN_NAVIGATION_BAR_ID } from '../../../common'; + +export const MapsList = () => { + const { + services: { + notifications: { toasts }, + savedObjects: { client: savedObjectsClient }, + application: { navigateToApp }, + chrome: { docTitle, setBreadcrumbs }, + }, + } = useOpenSearchDashboards(); + + useEffect(() => { + setBreadcrumbs(getMapsLandingBreadcrumbs(navigateToApp)); + docTitle.change(i18n.translate('maps.listing.pageTitle', { defaultMessage: 'Maps' })); + }, [docTitle, navigateToApp, setBreadcrumbs]); + + const navigateToSavedMapPage = (id: string) => { + navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: `/${id}` }); + }; + + const tableColumns = [ + { + field: 'attributes.title', + name: i18n.translate('maps.listing.table.titleColumnName', { + defaultMessage: 'Title', + }), + sortable: true, + render: (title: string, record: any) => ( + navigateToSavedMapPage(record.id)}>{title} + ), + }, + { + field: 'attributes.description', + name: i18n.translate('maps.listing.table.descriptionColumnName', { + defaultMessage: 'Description', + }), + sortable: true, + }, + { + field: 'updated_at', + name: i18n.translate('maps.listing.table.updatedTimeColumnName', { + defaultMessage: 'Last updated', + }), + sortable: true, + }, + ]; + + const navigateToCreateMapPage = () => { + navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: APP_PATH.CREATE_MAP }); + }; + + const fetchMaps = useCallback(async (): Promise<{ + total: number; + hits: object[]; + }> => { + const res = await savedObjectsClient.find({ + type: 'map', + fields: ['description', 'title'], + }); + return { + total: res.total, + hits: res.savedObjects, + }; + }, [savedObjectsClient]); + + const deleteMaps = useCallback( + async (selectedItems: object[]) => { + await Promise.all( + selectedItems.map((item: any) => savedObjectsClient.delete(item.type, item.id)) + ).catch((error) => { + toasts.addError(error, { + title: i18n.translate('map.mapListingDeleteErrorTitle', { + defaultMessage: 'Error deleting map', + }), + }); + }); + }, + [savedObjectsClient, toasts] + ); + + const noMapItem = ( + + Create map + , + ]} + /> + ); + + // Render the map list DOM. + return ( + + <> + + + + + + + + + + ); +}; diff --git a/custom_import_map/public/components/tooltip/create_tooltip.tsx b/custom_import_map/public/components/tooltip/create_tooltip.tsx new file mode 100644 index 00000000..e77bd0ca --- /dev/null +++ b/custom_import_map/public/components/tooltip/create_tooltip.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Popup, MapGeoJSONFeature } from 'maplibre-gl'; + +import { MapLayerSpecification, DocumentLayerSpecification } from '../../model/mapLayerType'; +import { FeatureGroupItem, TooltipContainer } from './tooltipContainer'; + +type Options = { + features: MapGeoJSONFeature[]; + layers: DocumentLayerSpecification[]; + showCloseButton?: boolean; + showPagination?: boolean; + showLayerSelection?: boolean; +}; + +export function isTooltipEnabledLayer( + layer: MapLayerSpecification +): layer is DocumentLayerSpecification { + return layer.type !== 'opensearch_vector_tile_map' && layer.source.showTooltips === true; +} + +export function groupFeaturesByLayers( + features: MapGeoJSONFeature[], + layers: DocumentLayerSpecification[] +) { + const featureGroups: FeatureGroupItem[] = []; + if (layers.length > 0) { + layers.forEach((layer) => { + const layerFeatures = features.filter((f) => f.layer.source === layer.id); + if (layerFeatures.length > 0) { + featureGroups.push({ features: layerFeatures, layer }); + } + }); + } + return featureGroups; +} + +export function getPopupLngLat(geometry: GeoJSON.Geometry) { + // geometry.coordinates is different for different geometry.type, here we use the geometry.coordinates + // of a Point as the position of the popup. For other types, such as Polygon, MultiPolygon, etc, + // use mouse position should be better + if (geometry.type === 'Point') { + return [geometry.coordinates[0], geometry.coordinates[1]] as [number, number]; + } else { + return null; + } +} + +export function createPopup({ + features, + layers, + showCloseButton = true, + showPagination = true, + showLayerSelection = true, +}: Options) { + const popup = new Popup({ + closeButton: false, + closeOnClick: false, + maxWidth: 'max-content', + }); + + const featureGroup = groupFeaturesByLayers(features, layers); + + // Don't show popup if no feature + if (featureGroup.length === 0) { + return null; + } + + const div = document.createElement('div'); + ReactDOM.render( + , + div + ); + + return popup.setDOMContent(div); +} diff --git a/custom_import_map/public/components/tooltip/tooltipContainer.tsx b/custom_import_map/public/components/tooltip/tooltipContainer.tsx new file mode 100644 index 00000000..236546ba --- /dev/null +++ b/custom_import_map/public/components/tooltip/tooltipContainer.tsx @@ -0,0 +1,96 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, useState } from 'react'; + +import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiText, EuiHorizontalRule } from '@elastic/eui'; +import { MapGeoJSONFeature } from 'maplibre-gl'; +import { TooltipHeaderContent } from './tooltipHeaderContent'; +import { ALL_LAYERS, PageData, TableData, TooltipTable } from './tooltipTable'; +import { DocumentLayerSpecification } from '../../model/mapLayerType'; + +export type FeatureGroupItem = { + features: MapGeoJSONFeature[]; + layer: DocumentLayerSpecification; +}; + +interface TooltipProps { + featureGroup: FeatureGroupItem[]; + onClose: () => void; + showCloseButton?: boolean; + showPagination?: boolean; + showLayerSelection?: boolean; +} + +function featureToTableRow(properties: Record) { + const row: PageData = []; + for (const [k, v] of Object.entries(properties)) { + row.push({ + key: k, + value: `${v}`, + }); + } + return row; +} + +function toTable(featureGroupItem: FeatureGroupItem) { + const table: TableData = []; + for (const feature of featureGroupItem.features) { + if (feature?.properties) { + table.push(featureToTableRow(feature.properties)); + } + } + return { table, layer: featureGroupItem.layer.name }; +} + +function createTableData(featureGroups: FeatureGroupItem[]) { + return featureGroups.map(toTable); +} + +export function TooltipContainer({ + featureGroup, + onClose, + showCloseButton = true, + showPagination = true, + showLayerSelection = true, +}: TooltipProps) { + const [selectedLayerIndexes, setSelectedLayerIndexes] = useState([0]); + const tables = useMemo(() => createTableData(featureGroup), [featureGroup]); + + const title = useMemo(() => { + if (selectedLayerIndexes.includes(ALL_LAYERS)) { + return 'All layers'; + } + if (selectedLayerIndexes.length === 1) { + return tables[selectedLayerIndexes[0]].layer; + } + return ''; + }, [selectedLayerIndexes, tables]); + + return ( + + + + + + + + + + + + + + ); +} diff --git a/custom_import_map/public/components/tooltip/tooltipHeaderContent.tsx b/custom_import_map/public/components/tooltip/tooltipHeaderContent.tsx new file mode 100644 index 00000000..7373b38f --- /dev/null +++ b/custom_import_map/public/components/tooltip/tooltipHeaderContent.tsx @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React from 'react'; + +interface Props { + title: string; + showCloseButton: boolean; + onClose: Function; +} + +const TooltipHeaderContent = (props: Props) => { + return ( + + + +

+ {props.title} +

+
+
+ {props.showCloseButton && ( + + { + return props.onClose(); + }} + iconType="cross" + aria-label={i18n.translate('maps.tooltip.closeLabel', { + defaultMessage: 'Close tooltip', + })} + data-test-subj="featureTooltipCloseButton" + /> + + )} +
+ ); +}; + +export { TooltipHeaderContent }; diff --git a/custom_import_map/public/components/tooltip/tooltipTable.tsx b/custom_import_map/public/components/tooltip/tooltipTable.tsx new file mode 100644 index 00000000..4ca7a571 --- /dev/null +++ b/custom_import_map/public/components/tooltip/tooltipTable.tsx @@ -0,0 +1,200 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiBasicTable, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, + EuiPagination, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useState, Fragment, useCallback, useEffect, useMemo } from 'react'; + +export type RowData = { + key: string; + value: string; +}; +export type PageData = RowData[]; +export type TableData = PageData[]; +type Table = { table: TableData; layer: string }; + +export const ALL_LAYERS = -1; + +interface Props { + tables: Table[]; + onLayerChange?: (layerIndexes: number[]) => void; + showPagination?: boolean; + showLayerSelection?: boolean; +} + +function mergeTables(tables: Table[], selectedIndex: number[]) { + const merged: TableData = []; + const allSelected = selectedIndex.includes(ALL_LAYERS); + + if (!allSelected) { + for (const index of selectedIndex) { + merged.push(...tables[index].table); + } + return merged; + } + for (let i = 0; i < tables.length; i++) { + const features: PageData[] = []; + tables[i].table.map((feature) => { + // Add layer name to every feature as first field + features.push( + [ + { + key: 'Layer name', + value: tables[i].layer, + }, + ].concat(feature.slice(0)) + ); + }); + merged.push(...features); + } + return merged; +} + +const TooltipTable = ({ + tables, + onLayerChange, + showPagination = true, + showLayerSelection = true, +}: Props) => { + const [selectedLayers, setSelectedLayers] = useState[]>([ + { + label: tables[0]?.layer ?? '', + value: 0, + key: '0', + }, + ]); + const [activePage, setActivePage] = useState(0); + const columns = [ + { + field: 'key', + name: 'Field Name', + width: '25%', + truncateText: false, + }, + { + field: 'value', + name: 'Field Value', + width: '75%', + truncateText: true, + }, + ]; + + useEffect(() => { + // When selected layer changed, reset the active page to the first page + setActivePage(0); + }, [selectedLayers]); + + const getRowProps = (item) => { + const { id } = item; + return { + 'data-test-subj': `row-${id}`, + className: 'customRowClass', + }; + }; + + const handleLayerChange = useCallback( + (layerSelections: EuiComboBoxOptionOption[]) => { + if (tables.length === 0) { + return; + } + + const selections = layerSelections; + + setSelectedLayers(selections); + if (onLayerChange) { + onLayerChange(selections.map((s) => s.value ?? 0)); + } + }, + [tables] + ); + + const options = useMemo(() => { + const layerOptions = []; + if (tables.length > 1) { + layerOptions.push({ label: 'All layers', value: ALL_LAYERS, key: '-1' }); + } + tables.forEach(({ layer }, i) => { + layerOptions.push({ label: layer, value: i, key: `${i}` }); + }); + return layerOptions; + }, [tables]); + + const tableItems = useMemo( + () => + mergeTables( + tables, + selectedLayers.map((l) => l.value ?? 0) + ), + [tables, selectedLayers] + ); + const pageItems = tableItems[activePage]; + + const getCellProps = (item, column) => { + const { id } = item; + const { field } = column; + return { + 'data-test-subj': `cell-${id}-${field}`, + className: 'customCellClass', + textOnly: true, + }; + }; + + return ( + + + + + + + + + {showLayerSelection && options?.length > 1 && ( + + + placeholder="Select a layer" + selectedOptions={selectedLayers} + singleSelection={{ asPlainText: true }} + options={options} + onChange={handleLayerChange} + isClearable={false} + /> + + )} + + {showPagination ? ( + + ) : ( + + {1} of {tableItems.length} + + )} + + + + ); +}; + +export { TooltipTable }; diff --git a/custom_import_map/public/index.scss b/custom_import_map/public/index.scss new file mode 100644 index 00000000..a850c169 --- /dev/null +++ b/custom_import_map/public/index.scss @@ -0,0 +1,4 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/custom_import_map/public/index.ts b/custom_import_map/public/index.ts index 6fe617d6..c0461f64 100644 --- a/custom_import_map/public/index.ts +++ b/custom_import_map/public/index.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import './index.scss'; + import { CustomImportMapPlugin } from './plugin'; // This exports static code and TypeScript types, diff --git a/custom_import_map/public/model/OSMLayerFunctions.ts b/custom_import_map/public/model/OSMLayerFunctions.ts new file mode 100644 index 00000000..ca879121 --- /dev/null +++ b/custom_import_map/public/model/OSMLayerFunctions.ts @@ -0,0 +1,123 @@ +import { Map as Maplibre, LayerSpecification } from 'maplibre-gl'; +import { OSMLayerSpecification } from './mapLayerType'; +import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} + +// Fetch style layers from OpenSearch vector tile service +const fetchStyleLayers = (url: string) => { + return fetch(url) + .then((res) => res.json()) + .then((json) => json.layers) + .catch((error) => { + // eslint-disable-next-line no-console + console.log('error', error); + }); +}; + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +const handleStyleLayers = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.setLayerZoomRange( + mbLayer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + // TODO: figure out error reason + if (mbLayer.type === 'symbol') { + return; + } + maplibreRef.current?.setPaintProperty( + mbLayer.id, + `${mbLayer.type}-opacity`, + layerConfig.opacity / 100 + ); + } + }); +}; + +const updateLayerConfig = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { + if (maplibreRef.current) { + handleStyleLayers(layerConfig, maplibreRef); + } +}; + +const addNewLayer = ( + layerConfig: OSMLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + if (maplibreRef.current) { + const layerSource = layerConfig?.source; + const layerStyle = layerConfig?.style; + maplibreRef.current.addSource(layerConfig.id, { + type: 'vector', + url: layerSource?.dataURL, + }); + fetchStyleLayers(layerStyle?.styleURL).then((styleLayers: LayerSpecification[]) => { + const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); + styleLayers.forEach((styleLayer) => { + styleLayer.id = styleLayer.id + '_' + layerConfig.id; + if (styleLayer.type !== 'background') { + styleLayer.source = layerConfig.id; + } + maplibreRef.current?.addLayer(styleLayer, beforeMbLayerId); + maplibreRef.current?.setLayoutProperty(styleLayer.id, 'visibility', layerConfig.visibility); + maplibreRef.current?.setLayerZoomRange( + styleLayer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + // TODO: figure out error reason + if (styleLayer.type === 'symbol') { + return; + } + maplibreRef.current?.setPaintProperty( + styleLayer.id, + `${styleLayer.type}-opacity`, + layerConfig.opacity / 100 + ); + }); + }); + } +}; + +// Functions for OpenSearch maps vector tile layer +export const OSMLayerFunctions = { + render: ( + maplibreRef: MaplibreRef, + layerConfig: OSMLayerSpecification, + beforeLayerId: string | undefined + ) => { + // If layer already exist in maplibre source, update layer config + // else add new layer. + if (layerExistInMbSource(layerConfig.id, 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/custom_import_map/public/model/customLayerFunctions.ts b/custom_import_map/public/model/customLayerFunctions.ts new file mode 100644 index 00000000..66647317 --- /dev/null +++ b/custom_import_map/public/model/customLayerFunctions.ts @@ -0,0 +1,117 @@ +import { Map as Maplibre, AttributionControl, RasterSourceSpecification } from 'maplibre-gl'; +import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; +import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +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(); + } + }); + } + const tilesURL = getCustomMapURL(layerConfig); + if (rasterLayerSource.tiles![0] !== tilesURL) { + 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 tilesURL = getCustomMapURL(layerConfig); + const layerSource = layerConfig?.source; + maplibreInstance.addSource(layerConfig.id, { + type: 'raster', + tiles: [tilesURL], + tileSize: 256, + attribution: layerSource?.attribution, + }); + const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); + maplibreInstance.addLayer( + { + id: layerConfig.id, + type: 'raster', + source: layerConfig.id, + }, + beforeMbLayerId + ); + } +}; + +const getCustomMapURL = (layerConfig: CustomLayerSpecification) => { + const layerSource = layerConfig?.source; + if (layerSource?.customType === 'tms') { + return layerSource?.url; + } else if (layerSource?.customType === 'wms') { + const referenceSystemName = layerSource.version === '1.3.0' ? 'crs' : 'srs'; + return `${layerSource?.url}?service=WMS&version=${layerSource.version}&request=GetMap&format=${layerSource.format}&transparent=true&layers=${layerSource?.layers}&styles=${layerSource.styles}&${referenceSystemName}=${layerSource.crs}&width=256&height=256&bbox={bbox-epsg-3857}`; + } else { + return ''; + } +}; + +export const CustomLayerFunctions = { + render: ( + maplibreRef: MaplibreRef, + layerConfig: CustomLayerSpecification, + beforeLayerId: string | undefined + ) => { + if (layerExistInMbSource(layerConfig.id, 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/custom_import_map/public/model/documentLayerFunctions.ts b/custom_import_map/public/model/documentLayerFunctions.ts new file mode 100644 index 00000000..c39dce4c --- /dev/null +++ b/custom_import_map/public/model/documentLayerFunctions.ts @@ -0,0 +1,379 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre, Popup, MapGeoJSONFeature } from 'maplibre-gl'; +import { createPopup, getPopupLngLat } from '../components/tooltip/create_tooltip'; +import { DocumentLayerSpecification } from './mapLayerType'; +import { convertGeoPointToGeoJSON, isGeoJSON } from '../utils/geo_formater'; +import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} +// https://opensearch.org/docs/1.3/opensearch/supported-field-types/geo-shape +const openSearchGeoJSONMap = new Map([ + ['point', 'Point'], + ['linestring', 'LineString'], + ['polygon', 'Polygon'], + ['multipoint', 'MultiPoint'], + ['multilinestring', 'MultiLineString'], + ['multipolygon', 'MultiPolygon'], + ['geometrycollection', 'GeometryCollection'], +]); + +const GeoJSONMaplibreMap = new Map([ + ['Point', 'circle'], + ['LineString', 'line'], + ['Polygon', 'fill'], +]); + +const getFieldValue = (data: any, name: string) => { + if (!name) { + return null; + } + const keys = name.split('.'); + return keys.reduce((pre, cur) => { + return pre?.[cur]; + }, data); +}; + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +const getGeoFieldType = (layerConfig: DocumentLayerSpecification) => { + return layerConfig?.source?.geoFieldType; +}; + +const getGeoFieldName = (layerConfig: DocumentLayerSpecification) => { + return layerConfig?.source?.geoFieldName; +}; + +const buildGeometry = (fieldType: string, location: any) => { + if (isGeoJSON(location)) { + return { + type: openSearchGeoJSONMap.get(location.type?.toLowerCase()), + coordinates: location.coordinates, + }; + } + if (fieldType === 'geo_point') { + // convert other supported formats to GeoJSON + return convertGeoPointToGeoJSON(location); + } + // We don't support non-geo-json format for geo_shape yet + return undefined; +}; + +const buildProperties = (document: any, fields: string[]) => { + const property: { [name: string]: any } = {}; + if (!fields) { + return property; + } + fields.forEach((field) => { + const fieldValue = getFieldValue(document._source, field); + if (fieldValue) { + property[field] = fieldValue; + } + }); + return property; +}; + +const getLayerSource = (data: any, layerConfig: DocumentLayerSpecification) => { + const geoFieldName = getGeoFieldName(layerConfig); + const geoFieldType = getGeoFieldType(layerConfig); + const featureList: any = []; + data.forEach((item: any) => { + const geoFieldValue = getFieldValue(item._source, geoFieldName); + const geometry = buildGeometry(geoFieldType, geoFieldValue); + if (geometry) { + const feature = { + geometry, + properties: buildProperties(item, layerConfig.source.tooltipFields), + }; + featureList.push(feature); + } + }); + return { + type: 'FeatureCollection', + features: featureList, + }; +}; + +const addNewLayer = ( + layerConfig: DocumentLayerSpecification, + maplibreRef: MaplibreRef, + data: any, + beforeLayerId: string | undefined +) => { + const maplibreInstance = maplibreRef.current; + const mbLayerBeforeId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); + const addGeoPointLayer = () => { + maplibreInstance?.addLayer( + { + id: layerConfig.id, + type: 'circle', + source: layerConfig.id, + paint: { + 'circle-radius': layerConfig.style?.markerSize, + 'circle-color': layerConfig.style?.fillColor, + 'circle-opacity': layerConfig.opacity / 100, + 'circle-stroke-width': layerConfig.style?.borderThickness, + 'circle-stroke-color': layerConfig.style?.borderColor, + }, + }, + mbLayerBeforeId + ); + maplibreInstance?.setLayoutProperty(layerConfig.id, 'visibility', layerConfig.visibility); + }; + + const addGeoShapeLayer = (source: any) => { + source.features.map((feature: any, index: number) => { + const mbType = GeoJSONMaplibreMap.get(feature.geometry.type); + const mbLayerId = `${layerConfig.id}-${index}`; + if (mbType === 'circle') { + maplibreInstance?.addLayer( + { + id: mbLayerId, + type: 'circle', + source: layerConfig.id, + filter: ['==', '$type', 'Point'], + paint: { + 'circle-radius': layerConfig.style?.markerSize, + 'circle-color': layerConfig.style?.fillColor, + 'circle-opacity': layerConfig.opacity / 100, + 'circle-stroke-width': layerConfig.style?.borderThickness, + 'circle-stroke-color': layerConfig.style?.borderColor, + }, + }, + mbLayerBeforeId + ); + maplibreInstance?.setLayoutProperty(mbLayerId, 'visibility', layerConfig.visibility); + } else if (mbType === 'line') { + maplibreInstance?.addLayer( + { + id: mbLayerId, + type: 'line', + source: layerConfig.id, + filter: ['==', '$type', 'LineString'], + paint: { + 'line-color': layerConfig.style?.fillColor, + 'line-opacity': layerConfig.opacity / 100, + 'line-width': layerConfig.style?.borderThickness, + }, + }, + mbLayerBeforeId + ); + maplibreInstance?.setLayoutProperty(mbLayerId, 'visibility', layerConfig.visibility); + } else if (mbType === 'fill') { + const polygonBorderLayerId = `${mbLayerId}-border`; + maplibreInstance?.addLayer( + { + id: mbLayerId, + type: 'fill', + source: layerConfig.id, + filter: ['==', '$type', 'Polygon'], + paint: { + 'fill-color': layerConfig.style?.fillColor, + 'fill-opacity': layerConfig.opacity / 100, + 'fill-outline-color': layerConfig.style?.borderColor, + }, + }, + mbLayerBeforeId + ); + maplibreInstance?.setLayoutProperty(mbLayerId, 'visibility', layerConfig.visibility); + // Add boarder for polygon + maplibreInstance?.addLayer( + { + id: polygonBorderLayerId, + type: 'line', + source: layerConfig.id, + filter: ['==', '$type', 'Polygon'], + paint: { + 'line-color': layerConfig.style?.borderColor, + 'line-opacity': layerConfig.opacity / 100, + 'line-width': layerConfig.style?.borderThickness, + }, + }, + mbLayerBeforeId + ); + maplibreInstance?.setLayoutProperty( + polygonBorderLayerId, + 'visibility', + layerConfig.visibility + ); + } + }); + }; + if (maplibreInstance) { + const source = getLayerSource(data, layerConfig); + maplibreInstance.addSource(layerConfig.id, { + type: 'geojson', + data: source, + }); + const geoFieldType = getGeoFieldType(layerConfig); + if (geoFieldType === 'geo_point') { + addGeoPointLayer(); + } else { + addGeoShapeLayer(source); + } + } +}; + +const updateLayerConfig = ( + layerConfig: DocumentLayerSpecification, + maplibreRef: MaplibreRef, + data: any +) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const dataSource = maplibreInstance?.getSource(layerConfig.id); + if (dataSource) { + // @ts-ignore + dataSource.setData(getLayerSource(data, layerConfig)); + } + const geoFieldType = getGeoFieldType(layerConfig); + if (geoFieldType === 'geo_point') { + maplibreInstance?.setLayerZoomRange( + layerConfig.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + maplibreInstance?.setPaintProperty( + layerConfig.id, + 'circle-opacity', + layerConfig.opacity / 100 + ); + maplibreInstance?.setPaintProperty( + layerConfig.id, + 'circle-color', + layerConfig.style?.fillColor + ); + maplibreInstance?.setPaintProperty( + layerConfig.id, + 'circle-stroke-color', + layerConfig.style?.borderColor + ); + maplibreInstance?.setPaintProperty( + layerConfig.id, + 'circle-stroke-width', + layerConfig.style?.borderThickness + ); + maplibreInstance?.setPaintProperty( + layerConfig.id, + 'circle-radius', + layerConfig.style?.markerSize + ); + } else { + getCurrentStyleLayers(maplibreRef).forEach((layer) => { + if (layer.id.includes(layerConfig.id)) { + maplibreInstance.setLayerZoomRange( + layer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + if (layer.type === 'circle') { + maplibreInstance?.setPaintProperty( + layer.id, + 'circle-opacity', + layerConfig.opacity / 100 + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'circle-color', + layerConfig.style?.fillColor + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'circle-stroke-color', + layerConfig.style?.borderColor + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'circle-stroke-width', + layerConfig.style?.borderThickness + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'circle-radius', + layerConfig.style?.markerSize + ); + } else if (layer.type === 'line') { + if (layer.id.includes('border')) { + maplibreInstance?.setPaintProperty( + layer.id, + 'line-color', + layerConfig.style?.borderColor + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'line-width', + layerConfig.style?.borderThickness + ); + } else { + maplibreInstance?.setPaintProperty( + layer.id, + 'line-opacity', + layerConfig.opacity / 100 + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'line-color', + layerConfig.style?.fillColor + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'line-width', + layerConfig.style?.borderThickness + ); + } + } else if (layer.type === 'fill') { + maplibreInstance?.setPaintProperty(layer.id, 'fill-opacity', layerConfig.opacity / 100); + maplibreInstance?.setPaintProperty( + layer.id, + 'fill-color', + layerConfig.style?.fillColor + ); + maplibreInstance?.setPaintProperty( + layer.id, + 'fill-outline-color', + layerConfig.style?.borderColor + ); + } + } + }); + } + } +}; + +export const DocumentLayerFunctions = { + render: ( + maplibreRef: MaplibreRef, + layerConfig: DocumentLayerSpecification, + data: any, + beforeLayerId: string | undefined + ) => { + if (layerExistInMbSource(layerConfig.id, maplibreRef)) { + updateLayerConfig(layerConfig, maplibreRef, data); + } else { + addNewLayer(layerConfig, maplibreRef, data, beforeLayerId); + } + }, + remove: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((layer: { id: any }) => { + if (layer.id.includes(layerConfig.id)) { + maplibreRef.current?.removeLayer(layer.id); + } + }); + }, + hide: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((layer) => { + if (layer.id.includes(layerConfig.id)) { + maplibreRef.current?.setLayoutProperty(layer.id, 'visibility', layerConfig.visibility); + } + }); + }, +}; diff --git a/custom_import_map/public/model/layerRenderController.ts b/custom_import_map/public/model/layerRenderController.ts new file mode 100644 index 00000000..5e710d17 --- /dev/null +++ b/custom_import_map/public/model/layerRenderController.ts @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre } from 'maplibre-gl'; +import { MapLayerSpecification } from './mapLayerType'; +import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; +import { + buildOpenSearchQuery, + getTime, + IOpenSearchDashboardsSearchResponse, + isCompleteResponse, +} from '../../../../src/plugins/data/common'; +import { layersFunctionMap } from './layersFunctions'; +import { MapServices } from '../types'; +import { MapState } from './mapState'; + +interface MaplibreRef { + current: Maplibre | null; +} + +export const prepareDataLayerSource = ( + layer: MapLayerSpecification, + mapState: MapState, + { data, notifications }: MapServices +): Promise => { + return new Promise(async (resolve, reject) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const sourceConfig = layer.source; + const indexPattern = await data.indexPatterns.get(sourceConfig.indexPatternId); + const indexPatternRefName = sourceConfig?.indexPatternRefName; + const geoField = sourceConfig.geoFieldName; + const sourceFields: string[] = [geoField]; + if (sourceConfig.showTooltips && sourceConfig.tooltipFields.length > 0) { + sourceFields.push(...sourceConfig.tooltipFields); + } + let buildQuery; + if (indexPattern) { + const timeFilters = getTime(indexPattern, mapState.timeRange); + buildQuery = buildOpenSearchQuery( + indexPattern, + [], + [ + ...(layer.source.filters ? layer.source.filters : []), + ...(timeFilters ? [timeFilters] : []), + ] + ); + } + const request = { + params: { + index: indexPatternRefName, + size: layer.source.documentRequestNumber, + body: { + _source: sourceFields, + query: buildQuery, + }, + }, + }; + + const search$ = data.search.search(request).subscribe({ + next: (response: IOpenSearchDashboardsSearchResponse) => { + if (isCompleteResponse(response)) { + const dataSource: object = response.rawResponse.hits.hits; + search$.unsubscribe(); + resolve({ dataSource, layer }); + } else { + notifications.toasts.addWarning('An error has occurred when query dataSource'); + search$.unsubscribe(); + reject(); + } + }, + error: (e: Error) => { + data.search.showError(e); + }, + }); + } + }); +}; + +export const handleDataLayerRender = ( + mapLayer: MapLayerSpecification, + mapState: MapState, + services: MapServices, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + return prepareDataLayerSource(mapLayer, mapState, services).then((result) => { + const { layer, dataSource } = result; + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); + } + }); +}; + +export const handleReferenceLayerRender = ( + layer: MapLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + layersFunctionMap[layer.type].render(maplibreRef, layer, beforeLayerId); +}; diff --git a/custom_import_map/public/model/layersFunctions.ts b/custom_import_map/public/model/layersFunctions.ts new file mode 100644 index 00000000..68634816 --- /dev/null +++ b/custom_import_map/public/model/layersFunctions.ts @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre } from 'maplibre-gl'; +import { + DASHBOARDS_MAPS_LAYER_ICON, + DASHBOARDS_MAPS_LAYER_NAME, + DASHBOARDS_MAPS_LAYER_TYPE, +} from '../../common'; +import { OSMLayerFunctions } from './OSMLayerFunctions'; +import { DocumentLayerFunctions } from './documentLayerFunctions'; +import { MapLayerSpecification } from './mapLayerType'; +import { CustomLayerFunctions } from './customLayerFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} + +interface MaplibreRef { + current: Maplibre | null; +} + +const getAllMaplibreLayersIncludesId = (maplibreRef: MaplibreRef, layerId?: string) => { + if (!layerId && !maplibreRef) { + return []; + } + return ( + maplibreRef.current + ?.getStyle() + .layers.filter((layer) => layer.id?.includes(String(layerId)) === true) || [] + ); +}; + +export const LayerActions = { + move: (maplibreRef: MaplibreRef, sourceId: string, beforeId?: string) => { + const sourceMaplibreLayers = getAllMaplibreLayersIncludesId(maplibreRef, sourceId); + if (!sourceMaplibreLayers) { + return; + } + const beforeMaplibreLayers = getAllMaplibreLayersIncludesId(maplibreRef, beforeId); + if (!beforeMaplibreLayers || beforeMaplibreLayers.length < 1) { + // move to top + sourceMaplibreLayers.forEach((layer) => maplibreRef.current?.moveLayer(layer.id)); + return; + } + const topOfBeforeLayer = beforeMaplibreLayers[0]; + sourceMaplibreLayers.forEach((layer) => + maplibreRef.current?.moveLayer(layer.id, topOfBeforeLayer.id) + ); + return; + }, +}; + +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) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +export const getMaplibreBeforeLayerId = ( + selectedLayer: MapLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +): string | undefined => { + const currentLoadedMbLayers = getCurrentStyleLayers(maplibreRef); + if (beforeLayerId) { + const beforeMbLayer = currentLoadedMbLayers.find((mbLayer) => + mbLayer.id.includes(beforeLayerId) + ); + return beforeMbLayer?.id; + } + return undefined; +}; + +export const layerExistInMbSource = (layerConfigId: string, maplibreRef: MaplibreRef) => { + const layers = getCurrentStyleLayers(maplibreRef); + for (const layer in layers) { + if (layers[layer].id.includes(layerConfigId)) { + return true; + } + } + return false; +}; + +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, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_ICON.CUSTOM_MAP, +}; + +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/custom_import_map/public/model/mapLayerType.ts b/custom_import_map/public/model/mapLayerType.ts new file mode 100644 index 00000000..e792c2f4 --- /dev/null +++ b/custom_import_map/public/model/mapLayerType.ts @@ -0,0 +1,92 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Filter } from '../../../../src/plugins/data/public'; + +/* eslint @typescript-eslint/consistent-type-definitions: ["error", "type"] */ +export type MapLayerSpecification = + | OSMLayerSpecification + | DocumentLayerSpecification + | CustomLayerSpecification; + +export type OSMLayerSpecification = { + name: string; + id: string; + type: 'opensearch_vector_tile_map'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + dataURL: string; + }; + style: { + styleURL: string; + }; +}; + +export type DocumentLayerSpecification = { + name: string; + id: string; + type: 'documents'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + indexPatternRefName: string; + indexPatternId: string; + geoFieldType: 'geo_point' | 'geo_shape'; + geoFieldName: string; + documentRequestNumber: number; + showTooltips: boolean; + tooltipFields: string[]; + filters: Filter[]; + }; + style: { + fillColor: string; + borderColor: string; + borderThickness: number; + markerSize: number; + }; +}; + +export type CustomLayerSpecification = CustomTMSLayerSpecification | CustomWMSLayerSpecification; + +export type CustomTMSLayerSpecification = { + name: string; + id: string; + type: 'custom_map'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + url: string; + customType: 'tms'; + attribution: string; + }; +}; + +export type CustomWMSLayerSpecification = { + name: string; + id: string; + type: 'custom_map'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + url: string; + customType: 'wms'; + attribution: string; + layers: string; + styles: string; + version: string; + format: string; + crs: string; + bbox: string; + }; +}; diff --git a/custom_import_map/public/model/mapState.ts b/custom_import_map/public/model/mapState.ts new file mode 100644 index 00000000..4d4ceddc --- /dev/null +++ b/custom_import_map/public/model/mapState.ts @@ -0,0 +1,10 @@ +import { Query, TimeRange } from '../../../../src/plugins/data/common'; + +export interface MapState { + timeRange: TimeRange; + query: Query; + refreshInterval: { + pause: boolean; + value: number; + }; +} diff --git a/custom_import_map/public/plugin.tsx b/custom_import_map/public/plugin.tsx index da0b291d..3fd3d89b 100644 --- a/custom_import_map/public/plugin.tsx +++ b/custom_import_map/public/plugin.tsx @@ -3,14 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; import { i18n } from '@osd/i18n'; -import { CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import React from 'react'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin, +} from '../../../src/core/public'; import { - CustomImportMapPluginSetup, - CustomImportMapPluginStart, - AppPluginSetupDependencies, + AppPluginStartDependencies, + MapServices, CustomImportMapPluginSetup, CustomImportMapPluginStart, } from './types'; +import {PLUGIN_NAME, PLUGIN_NAVIGATION_BAR_ID, PLUGIN_NAVIGATION_BAR_TILE} from '../common/constants/shared'; + +import { AppPluginSetupDependencies } from './types'; import { RegionMapVisualizationDependencies } from '../../../src/plugins/region_map/public'; import { VectorUploadOptions } from './components/vector_upload_options'; @@ -20,6 +28,39 @@ export class CustomImportMapPlugin core: CoreSetup, { regionMap }: AppPluginSetupDependencies ): CustomImportMapPluginSetup { + // Register an application into the side navigation menu + core.application.register({ + id: PLUGIN_NAVIGATION_BAR_ID, + title: PLUGIN_NAVIGATION_BAR_TILE, + category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in opensearch_dashboards.json + const [coreStart, depsStart] = await core.getStartServices(); + const { navigation, data } = depsStart as AppPluginStartDependencies; + + // make sure the index pattern list is up to date + data.indexPatterns.clearCache(); + // make sure a default index pattern exists + // if not, the page will be redirected to management and maps won't be rendered + await data.indexPatterns.ensureDefaultIndexPattern(); + + const services: MapServices = { + ...coreStart, + setHeaderActionMenu: params.setHeaderActionMenu, + appBasePath: params.history, + element: params.element, + navigation, + toastNotifications: coreStart.notifications.toasts, + history: params.history, + data, + }; + // Render the application + return renderApp(params, services); + }, + }); + regionMap.addOptionTab({ name: 'controls', title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.controlTabs.controlsTitle', { @@ -29,7 +70,16 @@ export class CustomImportMapPlugin }); // Return methods that should be available to other plugins - return {}; + return { + getGreeting() { + return i18n.translate('mapsDashboards.greetingText', { + defaultMessage: 'Hello from {name}!', + values: { + name: PLUGIN_NAME, + }, + }); + }, + }; } public start(core: CoreStart): CustomImportMapPluginStart { diff --git a/custom_import_map/public/types.ts b/custom_import_map/public/types.ts index 791b872f..d34f7273 100644 --- a/custom_import_map/public/types.ts +++ b/custom_import_map/public/types.ts @@ -3,11 +3,37 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + AppMountParameters, + CoreStart, + SavedObjectsClient, + ToastsStart, +} from 'opensearch-dashboards/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; + import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; + savedObjects: SavedObjectsClient; + data: DataPublicPluginStart; +} + +export interface MapServices extends CoreStart { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + appBasePath: AppMountParameters['history']; + element: AppMountParameters['element']; + navigation: NavigationPublicPluginStart; + toastNotifications: ToastsStart; + history: AppMountParameters['history']; + data: DataPublicPluginStart; +} + // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CustomImportMapPluginSetup {} +export interface CustomImportMapPluginSetup { + getGreeting: () => string; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomImportMapPluginStart {} diff --git a/custom_import_map/public/utils/breadcrumbs.ts b/custom_import_map/public/utils/breadcrumbs.ts new file mode 100644 index 00000000..daa69ea2 --- /dev/null +++ b/custom_import_map/public/utils/breadcrumbs.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import {PLUGIN_NAVIGATION_BAR_ID} from '../../common'; + +export function getMapsLandingBreadcrumbs(navigateToApp: any) { + return [ + { + text: i18n.translate('maps.listing.breadcrumb', { + defaultMessage: 'Maps', + }), + onClick: () => navigateToApp(PLUGIN_NAVIGATION_BAR_ID), + }, + ]; +} + +export function getCreateBreadcrumbs(navigateToApp: any) { + return [ + ...getMapsLandingBreadcrumbs(navigateToApp), + { + text: i18n.translate('maps.create.breadcrumb', { + defaultMessage: 'Create', + }), + }, + ]; +} + +export function getSavedMapBreadcrumbs(text: string, navigateToApp: any) { + return [ + ...getMapsLandingBreadcrumbs(navigateToApp), + { + text, + }, + ]; +} diff --git a/custom_import_map/public/utils/geo_formater.ts b/custom_import_map/public/utils/geo_formater.ts new file mode 100644 index 00000000..2af1933c --- /dev/null +++ b/custom_import_map/public/utils/geo_formater.ts @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const geoJSONTypes: string[] = [ + 'point', + 'linestring', + 'polygon', + 'multipoint', + 'multilinestring', + 'multipolygon', + 'geometrycollection', +]; + +export function isGeoJSON(value: { type: any; coordinates: any }) { + if (!value) return false; + if (!value.type || !value.coordinates) { + return false; + } + const geoJSONType = value.type; + if (geoJSONTypes.includes(geoJSONType.toLowerCase())) { + return true; + } + return false; +} + +function buildGeoJSONOfTypePoint(lon: number, lat: number) { + return { + type: 'Point', + coordinates: [lon, lat], + }; +} + +export function convertGeoPointToGeoJSON(location: any) { + // An object with 'lat' and 'lon' properties + if (location?.lat && location?.lon) { + return buildGeoJSONOfTypePoint(location?.lon, location?.lat); + } + // Geopoint as an array && support either (lon/lat) or (lon/lat/z) + if (Array.isArray(location) && (location.length === 2 || location.length === 3)) { + return buildGeoJSONOfTypePoint(location[0], location[1]); + } + + if (typeof location !== 'string') { + return undefined; + } + // Geopoint as a string && support either (lat,lon) or (lat, lon, z) + const values = location.trim().split(','); + if (values && (values.length === 2 || values.length === 3)) { + return buildGeoJSONOfTypePoint(parseFloat(values[1].trim()), parseFloat(values[0].trim())); + } + // TODO Geopoint as geohash & WKT Format + return undefined; +} diff --git a/custom_import_map/public/utils/getIntialConfig.ts b/custom_import_map/public/utils/getIntialConfig.ts new file mode 100644 index 00000000..6a05c892 --- /dev/null +++ b/custom_import_map/public/utils/getIntialConfig.ts @@ -0,0 +1,117 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { v4 as uuidv4 } from 'uuid'; +import { + DOCUMENTS, + DOCUMENTS_DEFAULT_MARKER_SIZE, + DOCUMENTS_DEFAULT_REQUEST_NUMBER, + DOCUMENTS_DEFAULT_SHOW_TOOLTIPS, + DOCUMENTS_DEFAULT_TOOLTIPS, + LAYER_VISIBILITY, + MAP_DATA_LAYER_DEFAULT_OPACITY, + MAP_DEFAULT_MAX_ZOOM, + MAP_DEFAULT_MIN_ZOOM, + MAP_LAYER_DEFAULT_BORDER_THICKNESS, + MAP_REFERENCE_LAYER_DEFAULT_OPACITY, + MAP_VECTOR_TILE_BASIC_STYLE, + MAP_VECTOR_TILE_DATA_SOURCE, + OPENSEARCH_MAP_LAYER, + CUSTOM_MAP, +} from '../../common'; +import { MapState } from '../model/mapState'; + +export const getLayerConfigMap = () => ({ + [OPENSEARCH_MAP_LAYER.type]: { + name: '', + description: '', + type: OPENSEARCH_MAP_LAYER.type, + id: uuidv4(), + zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM], + opacity: MAP_REFERENCE_LAYER_DEFAULT_OPACITY, + visibility: LAYER_VISIBILITY.VISIBLE, + source: { + dataURL: MAP_VECTOR_TILE_DATA_SOURCE, + }, + style: { + styleURL: MAP_VECTOR_TILE_BASIC_STYLE, + }, + }, + [DOCUMENTS.type]: { + name: '', + description: '', + type: DOCUMENTS.type, + id: uuidv4(), + zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM], + opacity: MAP_DATA_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, + }, + }, + [CUSTOM_MAP.type]: { + name: '', + description: '', + type: CUSTOM_MAP.type, + id: uuidv4(), + zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM], + opacity: MAP_REFERENCE_LAYER_DEFAULT_OPACITY, + visibility: LAYER_VISIBILITY.VISIBLE, + source: { + url: '', + customType: 'wms', + attribution: '', + layers: '', + styles: '', + version: '', + format: '', + crs: '', + bbox: '', + }, + }, +}); + +const getInitialColor = () => { + const colorCode = (Math.random() * 0xfffff * 1000000).toString(16); + return '#' + colorCode.slice(0, 6); +}; + +export const getStyleColor = () => { + const initialColor = getInitialColor(); + return { + fillColor: initialColor, + borderColor: initialColor, + }; +}; + +export const getInitialMapState = (): MapState => { + const timeRange = { + from: 'now-15m', + to: 'now', + }; + const query = { + query: '', + language: 'kuery', + }; + const refreshInterval = { + pause: true, + value: 12000, + }; + return { + timeRange, + query, + refreshInterval, + }; +}; diff --git a/custom_import_map/server/plugin.ts b/custom_import_map/server/plugin.ts index d92f3650..0be30228 100644 --- a/custom_import_map/server/plugin.ts +++ b/custom_import_map/server/plugin.ts @@ -16,6 +16,8 @@ import { CustomImportMapPluginSetup, CustomImportMapPluginStart } from './types' import { createGeospatialCluster } from './clusters'; import { GeospatialService, OpensearchService } from './services'; import { geospatial, opensearch } from '../server/routes'; +import { mapSavedObjectsType } from './saved_objects'; +import { capabilitiesProvider } from './saved_objects/capabilities_provider'; export class CustomImportMapPlugin implements Plugin { @@ -29,6 +31,7 @@ export class CustomImportMapPlugin public async setup(core: CoreSetup) { this.logger.debug('customImportMap: Setup'); + // @ts-ignore const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); const geospatialClient = createGeospatialCluster(core, globalConfig); @@ -41,6 +44,12 @@ export class CustomImportMapPlugin geospatial(geospatialService, router); opensearch(opensearchService, router); + // Register saved object types + core.savedObjects.registerType(mapSavedObjectsType); + + // Register capabilities + core.capabilities.registerProvider(capabilitiesProvider); + return {}; } diff --git a/custom_import_map/server/saved_objects/capabilities_provider.ts b/custom_import_map/server/saved_objects/capabilities_provider.ts new file mode 100644 index 00000000..dde86a4f --- /dev/null +++ b/custom_import_map/server/saved_objects/capabilities_provider.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const capabilitiesProvider = () => ({ + map: { + // TODO: investigate which capabilities we need to provide + // createNew: true, + // createShortUrl: true, + // delete: true, + show: true, + // showWriteControls: true, + // save: true, + // saveQuery: true, + }, +}); diff --git a/custom_import_map/server/saved_objects/index.ts b/custom_import_map/server/saved_objects/index.ts new file mode 100644 index 00000000..416ff0ab --- /dev/null +++ b/custom_import_map/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { mapSavedObjectsType } from './map_saved_object'; diff --git a/custom_import_map/server/saved_objects/map_saved_object.ts b/custom_import_map/server/saved_objects/map_saved_object.ts new file mode 100644 index 00000000..ca38099a --- /dev/null +++ b/custom_import_map/server/saved_objects/map_saved_object.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType } from 'opensearch-dashboards/server'; + +export const mapSavedObjectsType: SavedObjectsType = { + name: 'map', + hidden: false, + namespaceType: 'agnostic', + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/maps-dashboards#/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'map.show', + }; + }, + getEditUrl(obj) { + return `/management/opensearch-dashboards/objects/map/${encodeURIComponent(obj.id)}`; + }, + }, + mappings: { + properties: { + title: { type: 'text' }, + description: { type: 'text' }, + layerList: { type: 'text', index: false }, + uiState: { type: 'text', index: false }, + mapState: { type: 'text', index: false }, + version: { type: 'integer' }, + // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow + // When we save a saved object, the saved object plugin will extract the search source into two parts + // Some information will be put into kibanaSavedObjectMeta while others will be created as a reference object and pushed to the reference array + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false } }, + }, + }, + }, + migrations: {}, +}; diff --git a/custom_import_map/server/services/index.js b/custom_import_map/server/services/index.ts similarity index 100% rename from custom_import_map/server/services/index.js rename to custom_import_map/server/services/index.ts diff --git a/custom_import_map/translations/ja-JP.json b/custom_import_map/translations/ja-JP.json new file mode 100644 index 00000000..d00fb9fb --- /dev/null +++ b/custom_import_map/translations/ja-JP.json @@ -0,0 +1,81 @@ +{ + "formats": { + "number": { + "currency": { + "style": "currency" + }, + "percent": { + "style": "percent" + } + }, + "date": { + "short": { + "month": "numeric", + "day": "numeric", + "year": "2-digit" + }, + "medium": { + "month": "short", + "day": "numeric", + "year": "numeric" + }, + "long": { + "month": "long", + "day": "numeric", + "year": "numeric" + }, + "full": { + "weekday": "long", + "month": "long", + "day": "numeric", + "year": "numeric" + } + }, + "time": { + "short": { + "hour": "numeric", + "minute": "numeric" + }, + "medium": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric" + }, + "long": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + }, + "full": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + } + }, + "relative": { + "years": { + "units": "year" + }, + "months": { + "units": "month" + }, + "days": { + "units": "day" + }, + "hours": { + "units": "hour" + }, + "minutes": { + "units": "minute" + }, + "seconds": { + "units": "second" + } + } + }, + "messages": { + "mapsDashboards.buttonText": "Translate me to Japanese" + } +} diff --git a/custom_import_map/yarn.lock b/custom_import_map/yarn.lock new file mode 100644 index 00000000..a1ab629b --- /dev/null +++ b/custom_import_map/yarn.lock @@ -0,0 +1,1369 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cypress/request@^2.88.10": + version "2.88.10" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" + integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + http-signature "~1.3.6" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^8.3.2" + +"@cypress/skip-test@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@cypress/skip-test/-/skip-test-2.6.1.tgz#44a4bc4c2b2e369a7661177c9b38e50d417a36ea" + integrity sha512-X+ibefBiuOmC5gKG91wRIT0/OqXeETYvu7zXktjZ3yLeO186Y8ia0K7/gQUpAwuUi28DuqMd1+7tBQVtPkzbPA== + +"@cypress/xvfb@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + +"@mapbox/geojson-rewind@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz#591a5d71a9cd1da1a0bf3420b3bea31b0fc7946a" + integrity sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA== + dependencies: + get-stream "^6.0.1" + minimist "^1.2.6" + +"@mapbox/jsonlint-lines-primitives@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" + integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== + +"@mapbox/mapbox-gl-supported@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz#c15367178d8bfe4765e6b47b542fe821ce259c7b" + integrity sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ== + +"@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" + integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== + +"@mapbox/tiny-sdf@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz#cdba698d3d65087643130f9af43a2b622ce0b372" + integrity sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw== + +"@mapbox/unitbezier@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz#d32deb66c7177e9e9dfc3bbd697083e2e657ff01" + integrity sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw== + +"@mapbox/vector-tile@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz#d3a74c90402d06e89ec66de49ec817ff53409666" + integrity sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw== + dependencies: + "@mapbox/point-geometry" "~0.1.0" + +"@mapbox/whoots-js@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" + integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== + +"@opensearch-dashboards-test/opensearch-dashboards-test-library@git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main": + version "1.0.4" + resolved "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#9c0adaa6d492accd9f737aa905b2a2ca3be6840f" + +"@types/geojson@*", "@types/geojson@^7946.0.10": + version "7946.0.10" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== + +"@types/mapbox__point-geometry@*", "@types/mapbox__point-geometry@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz#488a9b76e8457d6792ea2504cdd4ecdd9860a27e" + integrity sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA== + +"@types/mapbox__vector-tile@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.0.tgz#8fa1379dbaead1e1b639b8d96cfd174404c379d6" + integrity sha512-kDwVreQO5V4c8yAxzZVQLE5tyWF+IPToAanloQaSnwfXmIcJ7cyOrv8z4Ft4y7PsLYmhWXmON8MBV8RX0Rgr8g== + dependencies: + "@types/geojson" "*" + "@types/mapbox__point-geometry" "*" + "@types/pbf" "*" + +"@types/node@*": + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + +"@types/node@^14.14.31": + version "14.18.36" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.36.tgz#c414052cb9d43fab67d679d5f3c641be911f5835" + integrity sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ== + +"@types/pbf@*", "@types/pbf@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" + integrity sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ== + +"@types/sinonjs__fake-timers@8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" + integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== + +"@types/sizzle@^2.3.2": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +blob-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +cachedir@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" + integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-more-types@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== + +ci-info@^3.2.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" + integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-table3@~0.6.1: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.16: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csscolorparser@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" + integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== + +cypress-file-upload@^5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" + integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== + +cypress-multi-reporters@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.2.tgz#129dfeffa00d4deca3e9f58d84570b9962c28c2b" + integrity sha512-lvwGwHqZG5CwGxBJ6UJXWaxlWGkJgxBjP0h+IVLrrwRlJpT4coSwwt+UzMdeqEMrzT4IDfhbtmUNOiDleisOYA== + dependencies: + debug "^4.1.1" + lodash "^4.17.15" + +cypress@9.5.4: + version "9.5.4" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.4.tgz#49d9272f62eba12f2314faf29c2a865610e87550" + integrity sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q== + dependencies: + "@cypress/request" "^2.88.10" + "@cypress/xvfb" "^1.2.4" + "@types/node" "^14.14.31" + "@types/sinonjs__fake-timers" "8.1.1" + "@types/sizzle" "^2.3.2" + arch "^2.2.0" + blob-util "^2.0.2" + bluebird "^3.7.2" + buffer "^5.6.0" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-cursor "^3.1.0" + cli-table3 "~0.6.1" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.10.4" + debug "^4.3.2" + enquirer "^2.3.6" + eventemitter2 "^6.4.3" + execa "4.1.0" + executable "^4.1.1" + extract-zip "2.0.1" + figures "^3.2.0" + fs-extra "^9.1.0" + getos "^3.2.1" + is-ci "^3.0.0" + is-installed-globally "~0.4.0" + lazy-ass "^1.6.0" + listr2 "^3.8.3" + lodash "^4.17.21" + log-symbols "^4.0.0" + minimist "^1.2.6" + ospath "^1.2.2" + pretty-bytes "^5.6.0" + proxy-from-env "1.0.0" + request-progress "^3.0.0" + semver "^7.3.2" + supports-color "^8.1.1" + tmp "~0.2.1" + untildify "^4.0.0" + yauzl "^2.10.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +dayjs@^1.10.4: + version "1.11.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" + integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +earcut@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" + integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +eventemitter2@^6.4.3: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + +execa@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +executable@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== + dependencies: + pify "^2.2.0" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +geojson-vt@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" + integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== + +get-stream@^5.0.0, get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +gl-matrix@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.3.tgz#fc1191e8320009fd4d20e9339595c6041ddc22c9" + integrity sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA== + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +http-signature@~1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" + integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== + dependencies: + assert-plus "^1.0.0" + jsprim "^2.0.2" + sshpk "^1.14.1" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +ieee754@^1.1.12, ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +is-ci@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== + dependencies: + ci-info "^3.2.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +kdbush@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" + integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +lazy-ass@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== + +listr2@^3.8.3: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" + +lodash.once@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +maplibre-gl@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-2.4.0.tgz#2b53dbf526626bf4ee92ad4f33f13ef09e5af182" + integrity sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA== + dependencies: + "@mapbox/geojson-rewind" "^0.5.2" + "@mapbox/jsonlint-lines-primitives" "^2.0.2" + "@mapbox/mapbox-gl-supported" "^2.0.1" + "@mapbox/point-geometry" "^0.1.0" + "@mapbox/tiny-sdf" "^2.0.5" + "@mapbox/unitbezier" "^0.0.1" + "@mapbox/vector-tile" "^1.3.1" + "@mapbox/whoots-js" "^3.1.0" + "@types/geojson" "^7946.0.10" + "@types/mapbox__point-geometry" "^0.1.2" + "@types/mapbox__vector-tile" "^1.3.0" + "@types/pbf" "^3.0.2" + csscolorparser "~1.0.3" + earcut "^2.2.4" + geojson-vt "^3.2.1" + gl-matrix "^3.4.3" + global-prefix "^3.0.0" + murmurhash-js "^1.0.0" + pbf "^3.2.1" + potpack "^1.0.2" + quickselect "^2.0.0" + supercluster "^7.1.5" + tinyqueue "^2.0.3" + vt-pbf "^3.1.3" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +murmurhash-js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" + integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== + +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +pbf@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" + integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== + dependencies: + ieee754 "^1.1.12" + resolve-protobuf-schema "^2.1.0" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +potpack@^1.0.2: + version "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.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" + integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== + +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +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" + integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== + +proxy-from-env@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== + +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +quickselect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" + integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== + +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== + dependencies: + throttleit "^1.0.0" + +resolve-protobuf-schema@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" + integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== + dependencies: + protocol-buffers-schema "^3.3.1" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rxjs@^7.5.1: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.3.2: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +sshpk@^1.14.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supercluster@^7.1.5: + version "7.1.5" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3" + integrity sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg== + dependencies: + kdbush "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tinyqueue@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" + integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== + +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tslib@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + +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== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vt-pbf@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" + integrity sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA== + dependencies: + "@mapbox/point-geometry" "0.1.0" + "@mapbox/vector-tile" "^1.3.1" + pbf "^3.2.1" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0"