diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 1d536281..310ecd91 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -48,6 +48,7 @@ import { } from '../../model/layerRenderController'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; +import {moveLayers, removeLayers, updateLayerVisibility} from "../../model/map/layer_operations"; interface MaplibreRef { current: Maplibre | null; @@ -241,7 +242,8 @@ export const LayerControlPanel = memo( const currentMaplibreLayerId = layers[prevIndex].id; const beforeMaplibreLayerId = beforeMaplibreLayerID(prevIndex, newIndex); - LayerActions.move(maplibreRef, currentMaplibreLayerId, beforeMaplibreLayerId); + + moveLayers(maplibreRef.current!, currentMaplibreLayerId, beforeMaplibreLayerId); // update map layers const layersClone = [...layers]; @@ -288,7 +290,7 @@ export const LayerControlPanel = memo( layer.visibility = LAYER_VISIBILITY.VISIBLE; setLayerVisibility(new Map(layerVisibility.set(layer.id, true))); } - layersFunctionMap[layer.type]?.hide(maplibreRef, layer); + updateLayerVisibility(maplibreRef.current!, layer.id, layer.visibility); }; const onDeleteLayerIconClick = (layer: MapLayerSpecification) => { @@ -298,7 +300,7 @@ export const LayerControlPanel = memo( const onDeleteLayerConfirm = () => { if (selectedDeleteLayer) { - layersFunctionMap[selectedDeleteLayer.type]?.remove(maplibreRef, selectedDeleteLayer); + removeLayers(maplibreRef.current!, selectedDeleteLayer.id, true); removeLayer(selectedDeleteLayer.id); setIsDeleteLayerModalVisible(false); setSelectedDeleteLayer(undefined); diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts index ca879121..871904f0 100644 --- a/public/model/OSMLayerFunctions.ts +++ b/public/model/OSMLayerFunctions.ts @@ -1,6 +1,7 @@ import { Map as Maplibre, LayerSpecification } from 'maplibre-gl'; import { OSMLayerSpecification } from './mapLayerType'; -import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; +import { getMaplibreBeforeLayerId } from './layersFunctions'; +import { getLayers, hasLayer } from './map/layer_operations'; interface MaplibreRef { current: Maplibre | null; @@ -17,36 +18,27 @@ const fetchStyleLayers = (url: string) => { }); }; -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 - ); + getLayers(maplibreRef.current!, layerConfig.id).forEach((mbLayer) => { + 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); - } + handleStyleLayers(layerConfig, maplibreRef); }; const addNewLayer = ( @@ -55,16 +47,16 @@ const addNewLayer = ( beforeLayerId: string | undefined ) => { if (maplibreRef.current) { - const layerSource = layerConfig?.source; - const layerStyle = layerConfig?.style; + const { source, style } = layerConfig; maplibreRef.current.addSource(layerConfig.id, { type: 'vector', - url: layerSource?.dataURL, + url: source?.dataURL, }); - fetchStyleLayers(layerStyle?.styleURL).then((styleLayers: LayerSpecification[]) => { + fetchStyleLayers(style?.styleURL).then((styleLayers: LayerSpecification[]) => { const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); styleLayers.forEach((styleLayer) => { styleLayer.id = styleLayer.id + '_' + layerConfig.id; + // TODO: Add comments on why we skip background type if (styleLayer.type !== 'background') { styleLayer.source = layerConfig.id; } @@ -98,26 +90,8 @@ export const OSMLayerFunctions = { ) => { // 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); - } - }); + return hasLayer(maplibreRef.current!, layerConfig.id) + ? updateLayerConfig(layerConfig, maplibreRef) + : addNewLayer(layerConfig, maplibreRef, beforeLayerId); }, }; diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index b8f43bd9..2c675434 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -1,6 +1,7 @@ import { Map as Maplibre, AttributionControl, RasterSourceSpecification } from 'maplibre-gl'; import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; -import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; +import { getMaplibreBeforeLayerId } from './layersFunctions'; +import { hasLayer, removeLayers } from './map/layer_operations'; interface MaplibreRef { current: Maplibre | null; @@ -97,26 +98,11 @@ export const CustomLayerFunctions = { layerConfig: CustomLayerSpecification, beforeLayerId: string | undefined ) => { - if (layerExistInMbSource(layerConfig.id, maplibreRef)) { - updateLayerConfig(layerConfig, maplibreRef); - } else { - addNewLayer(layerConfig, maplibreRef, beforeLayerId); - } + return hasLayer(maplibreRef.current!, layerConfig.id) + ? updateLayerConfig(layerConfig, maplibreRef) + : 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); - } - }); + removeLayers(maplibreRef.current!, layerConfig.id, true); }, }; diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index 0dfce730..d803572c 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -7,14 +7,17 @@ import { Map as Maplibre } from 'maplibre-gl'; import { parse } from 'wellknown'; import { DocumentLayerSpecification } from './mapLayerType'; import { convertGeoPointToGeoJSON, isGeoJSON } from '../utils/geo_formater'; -import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; +import { getMaplibreBeforeLayerId } from './layersFunctions'; import { addCircleLayer, addLineLayer, addPolygonLayer, + hasLayer, + removeLayers, updateCircleLayer, updateLineLayer, updatePolygonLayer, + updateLayerVisibility, } from './map/layer_operations'; interface MaplibreRef { @@ -175,7 +178,7 @@ const addNewLayer = ( } }; -const updateLayerConfig = ( +const updateLayer = ( layerConfig: DocumentLayerSpecification, maplibreRef: MaplibreRef, data: any @@ -188,21 +191,21 @@ const updateLayerConfig = ( dataSource.setData(getLayerSource(data, layerConfig)); } updateCircleLayer(maplibreInstance, { - fillColor: layerConfig.style?.fillColor, + fillColor: layerConfig.style.fillColor, maxZoom: layerConfig.zoomRange[1], minZoom: layerConfig.zoomRange[0], opacity: layerConfig.opacity, - outlineColor: layerConfig.style?.borderColor, + outlineColor: layerConfig.style.borderColor, radius: layerConfig.style?.markerSize, sourceId: layerConfig.id, visibility: layerConfig.visibility, - width: layerConfig.style?.borderThickness, + width: layerConfig.style.borderThickness, }); const geoFieldType = getGeoFieldType(layerConfig); if (geoFieldType === 'geo_shape') { updateLineLayer(maplibreInstance, { - width: layerConfig.style?.borderThickness, - color: layerConfig.style?.fillColor, + width: layerConfig.style.borderThickness, + color: layerConfig.style.fillColor, maxZoom: layerConfig.zoomRange[1], minZoom: layerConfig.zoomRange[0], opacity: layerConfig.opacity, @@ -210,13 +213,13 @@ const updateLayerConfig = ( visibility: layerConfig.visibility, }); updatePolygonLayer(maplibreInstance, { - width: layerConfig.style?.borderThickness, - fillColor: layerConfig.style?.fillColor, + width: layerConfig.style.borderThickness, + fillColor: layerConfig.style.fillColor, maxZoom: layerConfig.zoomRange[1], minZoom: layerConfig.zoomRange[0], opacity: layerConfig.opacity, sourceId: layerConfig.id, - outlineColor: layerConfig.style?.borderColor, + outlineColor: layerConfig.style.borderColor, visibility: layerConfig.visibility, }); } @@ -230,26 +233,8 @@ export const DocumentLayerFunctions = { 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); - } - }); + return hasLayer(maplibreRef.current!, layerConfig.id) + ? updateLayer(layerConfig, maplibreRef, data) + : addNewLayer(layerConfig, maplibreRef, data, beforeLayerId); }, }; diff --git a/public/model/layersFunctions.ts b/public/model/layersFunctions.ts index 68634816..71625459 100644 --- a/public/model/layersFunctions.ts +++ b/public/model/layersFunctions.ts @@ -13,6 +13,7 @@ import { OSMLayerFunctions } from './OSMLayerFunctions'; import { DocumentLayerFunctions } from './documentLayerFunctions'; import { MapLayerSpecification } from './mapLayerType'; import { CustomLayerFunctions } from './customLayerFunctions'; +import { getLayers } from './map/layer_operations'; interface MaplibreRef { current: Maplibre | null; @@ -22,37 +23,6 @@ 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, @@ -65,16 +35,12 @@ export const layersTypeNameMap: { [key: string]: string } = { [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); + const currentLoadedMbLayers = getLayers(maplibreRef.current!); if (beforeLayerId) { const beforeMbLayer = currentLoadedMbLayers.find((mbLayer) => mbLayer.id.includes(beforeLayerId) @@ -84,16 +50,6 @@ export const getMaplibreBeforeLayerId = ( 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, diff --git a/public/model/map/__mocks__/map.ts b/public/model/map/__mocks__/map.ts index cf89df85..1f662def 100644 --- a/public/model/map/__mocks__/map.ts +++ b/public/model/map/__mocks__/map.ts @@ -6,15 +6,50 @@ import { LayerSpecification } from 'maplibre-gl'; import { MockLayer } from './layer'; +export type Source = any; + export class MockMaplibreMap { - public _layers: MockLayer[]; + private _styles: { + layers: MockLayer[]; + sources: Map; + }; + + constructor(layers: MockLayer[]) { + this._styles = { + layers: new Array(...layers), + sources: new Map(), + }; + } + + public addSource(sourceId: string, source: Source) { + this._styles.sources.set(sourceId, source); + } - constructor() { - this._layers = new Array(); + public getSource(sourceId: string): string | undefined { + return this._styles.sources.get(sourceId); + } + + public removeSource(sourceId: string) { + this._styles.sources.delete(sourceId); + } + + public getLayers(): MockLayer[] { + return [...this._styles.layers]; } getLayer(id: string): MockLayer[] { - return this._layers.filter((layer) => (layer.getProperty('id') as string).includes(id)); + return this._styles.layers.filter((layer) => (layer.getProperty('id') as string).includes(id)); + } + + getStyle(): { layers: LayerSpecification[] } { + const layerSpecs: LayerSpecification[] = this._styles.layers.map((layer) => { + return { + id: String(layer.getProperty('id')), + } as LayerSpecification; + }); + return { + layers: layerSpecs, + }; } public setLayerZoomRange(layerId: string, minZoom: number, maxZoom: number) { @@ -43,12 +78,35 @@ export class MockMaplibreMap { layer.setProperty(key, layerSpec[key]); }); if (!beforeId) { - this._layers.push(layer); + this._styles.layers.push(layer); return; } - const beforeLayerIndex = this._layers.findIndex((l) => { + const beforeLayerIndex = this._styles.layers.findIndex((l) => { return l.getProperty('id') === beforeId; }); - this._layers.splice(beforeLayerIndex, 0, layer); + this._styles.layers.splice(beforeLayerIndex, 0, layer); + } + + private move(fromIndex: number, toIndex: number) { + const element = this._styles.layers[fromIndex]; + this._styles.layers.splice(fromIndex, 1); + this._styles.layers.splice(toIndex, 0, element); + } + + moveLayer(layerId: string, beforeId?: string) { + if (layerId === beforeId) { + return; + } + const fromIndex: number = this._styles.layers.indexOf(this.getLayer(layerId)[0]); + const toIndex: number = beforeId + ? this._styles.layers.indexOf(this.getLayer(beforeId)[0]) + : this._styles.layers.length; + this.move(fromIndex, toIndex); + } + + removeLayer(layerId: string) { + this._styles.layers = this.getLayers().filter( + (layer) => !(layer.getProperty('id') as string).includes(layerId) + ); } } diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 779fdb4f..14ffc02e 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -6,16 +6,19 @@ import { addCircleLayer, addLineLayer, addPolygonLayer, + getLayers, + hasLayer, moveLayers, removeLayers, updateCircleLayer, updateLineLayer, - updatePolygonLayer, + updatePolygonLayer, updateLayerVisibility, } from './layer_operations'; import { Map as Maplibre } from 'maplibre-gl'; import { MockMaplibreMap } from './__mocks__/map'; +import { MockLayer } from './__mocks__/layer'; describe('Circle layer', () => { it('add new circle layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedLayerId: string = sourceId + '-circle'; expect(mockMap.getLayer(expectedLayerId).length).toBe(0); @@ -51,7 +54,7 @@ describe('Circle layer', () => { }); it('update circle layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; // add layer first @@ -99,7 +102,7 @@ describe('Circle layer', () => { describe('Line layer', () => { it('add new Line layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedLayerId: string = sourceId + '-line'; expect(mockMap.getLayer(expectedLayerId).length).toBe(0); @@ -129,7 +132,7 @@ describe('Line layer', () => { }); it('update line layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; // add layer first @@ -171,7 +174,7 @@ describe('Line layer', () => { describe('Polygon layer', () => { it('add new polygon layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedFillLayerId = sourceId + '-fill'; const expectedOutlineLayerId = expectedFillLayerId + '-outline'; @@ -218,7 +221,7 @@ describe('Polygon layer', () => { }); it('update polygon layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedFillLayerId = sourceId + '-fill'; @@ -277,3 +280,95 @@ describe('Polygon layer', () => { expect(outlineLayer.getProperty('line-width')).toBe(7); }); }); + +describe('get layer', () => { + it('should get layer successfully', function () { + const mockLayer: MockLayer = new MockLayer('layer-1'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); + const actualLayers = getLayers((mockMap as unknown) as Maplibre, 'layer-1'); + expect(actualLayers.length).toBe(1); + expect(actualLayers[0].id).toBe(mockLayer.getProperty('id')); + }); + + it('should confirm no layer exists', function () { + const mockLayer: MockLayer = new MockLayer('layer-1'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); + expect(hasLayer((mockMap as unknown) as Maplibre, 'layer-2')).toBe(false); + }); + + it('should confirm layer exists', function () { + const mockLayer: MockLayer = new MockLayer('layer-1'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); + expect(hasLayer((mockMap as unknown) as Maplibre, 'layer-1')).toBe(true); + }); +}); + +describe('move layer', () => { + it('should move to top', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + const mockLayer3: MockLayer = new MockLayer('layer-2'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + moveLayers((mockMap as unknown) as Maplibre, 'layer-1'); + const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); + expect(reorderedLayer).toEqual(['layer-2', 'layer-1', 'layer-11']); + }); + it('should move before middle layer', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-2'); + const mockLayer3: MockLayer = new MockLayer('layer-3'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + moveLayers((mockMap as unknown) as Maplibre, 'layer-1', 'layer-2'); + const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); + expect(reorderedLayer).toEqual(['layer-2', 'layer-1', 'layer-3']); + }); + it('should not move if no layer is matched', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-2'); + const mockLayer3: MockLayer = new MockLayer('layer-3'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + moveLayers((mockMap as unknown) as Maplibre, 'layer-4', 'layer-2'); + const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); + expect(reorderedLayer).toEqual(['layer-1', 'layer-2', 'layer-3']); + }); +}); + +describe('delete layer', function () { + it('should delete layer without source', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + const mockLayer3: MockLayer = new MockLayer('layer-2'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + mockMap.addSource('layer-1', 'geojson'); + removeLayers((mockMap as unknown) as Maplibre, 'layer-1'); + expect(mockMap.getLayers().length).toBe(1); + expect(mockMap.getSource('layer-1')).toBeDefined(); + expect(mockMap.getLayers()[0].getProperty('id')).toBe('layer-2'); + }); + it('should delete layer with source', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + const mockLayer3: MockLayer = new MockLayer('layer-2'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + mockMap.addSource('layer-2', 'geojson'); + removeLayers((mockMap as unknown) as Maplibre, 'layer-2', true); + expect(mockMap.getLayers().length).toBe(2); + expect(mockMap.getSource('layer-2')).toBeUndefined(); + expect( + mockMap.getLayers().filter((layer) => String(layer?.getProperty('id')) === 'layer-2') + ).toEqual([]); + }); +}); + +describe('update visibility', function () { + it('should update visibility for given layer', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + mockLayer1.setProperty('visibility', 'none'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2]); + updateLayerVisibility((mockMap as unknown) as Maplibre, 'layer-1', 'visible'); + expect(mockMap.getLayers().map((layer) => String(layer.getProperty('visibility')))).toEqual( + Array(2).fill('visible') + ); + }); +}); diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 948f06c1..7c3cfd99 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -2,7 +2,51 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { Map as Maplibre } from 'maplibre-gl'; +import { LayerSpecification, Map as Maplibre } from 'maplibre-gl'; + +export const getLayers = (map: Maplibre, dashboardMapsLayerId?: string): LayerSpecification[] => { + const layers: LayerSpecification[] = map.getStyle().layers; + return dashboardMapsLayerId + ? layers.filter((layer) => layer.id.includes(dashboardMapsLayerId)) + : layers; +}; + +export const hasLayer = (map: Maplibre, dashboardMapsLayerId: string): boolean => { + const maplibreMapLayers = getLayers(map); + for (const layer of maplibreMapLayers) { + if (layer.id.includes(dashboardMapsLayerId)) { + return true; + } + } + return false; +}; + +export const moveLayers = (map: Maplibre, sourceId: string, beforeId?: string) => { + const sourceLayers = getLayers(map, sourceId); + if (!sourceLayers.length) { + return; + } + const beforeLayers = beforeId ? getLayers(map, beforeId) : []; + const topOfBeforeLayer = beforeLayers.length ? beforeLayers[0].id : undefined; + sourceLayers.forEach((layer) => map?.moveLayer(layer.id, topOfBeforeLayer)); + return; +}; + +export const removeLayers = (map: Maplibre, layerId: string, removeSource?: boolean) => { + getLayers(map, layerId).forEach((layer) => { + map.removeLayer(layer.id); + }); + // client might remove source if it is not required anymore. + if (removeSource && map.getSource(layerId)) { + map.removeSource(layerId); + } +}; + +export const updateLayerVisibility = (map: Maplibre, layerId: string, visibility: string) => { + getLayers(map, layerId).forEach((layer) => { + map.setLayoutProperty(layer.id, 'visibility', visibility); + }); +}; export interface LineLayerSpecification { sourceId: string;