x-pack/platform/plugins/shared/maps/public/actions/layer_actions.ts (846 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ import { AnyAction, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import type { Query } from '@kbn/es-query'; import { Adapters } from '@kbn/inspector-plugin/common/adapters'; import { MapStoreState } from '../reducers/store'; import { createLayerInstance, getEditState, getLayerById, getLayerDescriptor, getLayerList, getLayerListRaw, getMapColors, getMapReady, getSelectedLayerId, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; import { cancelRequest, getInspectorAdapters } from '../reducers/non_serializable_instances'; import { hideTOCDetails, setDrawMode, showTOCDetails, updateFlyout } from './ui_actions'; import { ADD_LAYER, ADD_WAITING_FOR_MAP_READY_LAYER, CLEAR_LAYER_PROP, CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, REMOVE_LAYER, REMOVE_TRACKED_LAYER_STATE, ROLLBACK_TO_TRACKED_LAYER_STATE, SET_JOINS, SET_LAYER_VISIBILITY, SET_SELECTED_LAYER, SET_WAITING_FOR_READY_HIDDEN_LAYERS, TRACK_CURRENT_LAYER_STATE, UPDATE_LAYER, UPDATE_LAYER_ORDER, UPDATE_LAYER_PROP, UPDATE_LAYER_STYLE, UPDATE_SOURCE_PROP, } from './map_action_constants'; import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_request_actions'; import { Attribution, JoinDescriptor, LayerDescriptor, StyleDescriptor, TileError, TileMetaFeature, VectorLayerDescriptor, VectorStyleDescriptor, } from '../../common/descriptor_types'; import { ILayer } from '../classes/layers/layer'; import { hasVectorLayerMethod } from '../classes/layers/vector_layer'; import { OnSourceChangeArgs } from '../classes/sources/source'; import { isESVectorTileSource } from '../classes/sources/es_source'; import { DRAW_MODE, LAYER_STYLE_TYPE, LAYER_TYPE, SCALING_TYPES, STYLE_TYPE, } from '../../common/constants'; import { IVectorStyle } from '../classes/styles/vector/vector_style'; import { notifyLicensedFeatureUsage } from '../licensed_features'; import { IESAggField } from '../classes/fields/agg'; import { IField } from '../classes/fields/field'; import type { IVectorSource } from '../classes/sources/vector_source'; import { getDrawMode, getOpenTOCDetails } from '../selectors/ui_selectors'; import { isLayerGroup, LayerGroup } from '../classes/layers/layer_group'; import { isSpatialJoin } from '../classes/joins/is_spatial_join'; export function trackCurrentLayerState(layerId: string) { return { type: TRACK_CURRENT_LAYER_STATE, layerId, }; } export function rollbackToTrackedLayerStateForSelectedLayer() { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layerId = getSelectedLayerId(getState()); await dispatch({ type: ROLLBACK_TO_TRACKED_LAYER_STATE, layerId, }); // Ensure updateStyleMeta is triggered // syncDataForLayer may not trigger endDataLoad if no re-fetch is required dispatch(updateStyleMeta(layerId)); dispatch(syncDataForLayerId(layerId, false)); }; } export function removeTrackedLayerStateForSelectedLayer() { return (dispatch: Dispatch, getState: () => MapStoreState) => { const layerId = getSelectedLayerId(getState()); dispatch({ type: REMOVE_TRACKED_LAYER_STATE, layerId, }); }; } export function replaceLayerList(newLayerList: LayerDescriptor[]) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const isMapReady = getMapReady(getState()); if (!isMapReady) { dispatch({ type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, }); } else { getLayerListRaw(getState()).forEach(({ id }) => { dispatch(removeLayerFromLayerList(id)); }); } newLayerList.forEach((layerDescriptor) => { dispatch(addLayer(layerDescriptor)); }); }; } export function updateLayerDescriptor(layerDescriptor: LayerDescriptor) { return { type: UPDATE_LAYER, layer: layerDescriptor, }; } export function cloneLayer(layerId: string) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layer = getLayerById(layerId, getState()); if (!layer) { return; } (await layer.cloneDescriptor()).forEach((layerDescriptor) => { dispatch(addLayer(layerDescriptor)); if (layer.getParent()) { dispatch(moveLayerToLeftOfTarget(layerDescriptor.id, layerId)); } }); }; } export function addLayer(layerDescriptor: LayerDescriptor) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const isMapReady = getMapReady(getState()); if (!isMapReady) { dispatch({ type: ADD_WAITING_FOR_MAP_READY_LAYER, layer: layerDescriptor, }); return; } dispatch({ type: ADD_LAYER, layer: layerDescriptor, }); dispatch(syncDataForLayerId(layerDescriptor.id, false)); const layer = createLayerInstance(layerDescriptor, []); // custom icons not needed, layer instance only used to get licensed features const features = await layer.getLicensedFeatures(); features.forEach(notifyLicensedFeatureUsage); }; } export function addLayerWithoutDataSync(layerDescriptor: LayerDescriptor) { return { type: ADD_LAYER, layer: layerDescriptor, }; } export function addPreviewLayers(layerDescriptors: LayerDescriptor[]) { return (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => { dispatch(removePreviewLayers()); layerDescriptors.forEach((layerDescriptor) => { dispatch(addLayer({ ...layerDescriptor, __isPreviewLayer: true })); // Auto open layer legend to increase legend discoverability if ( layerDescriptor.style && (hasByValueStyling(layerDescriptor.style) || layerDescriptor.style.type === LAYER_STYLE_TYPE.HEATMAP) ) { dispatch(showTOCDetails(layerDescriptor.id)); } }); }; } export function removePreviewLayers() { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { getLayerList(getState()).forEach((layer) => { if (layer.isPreviewLayer()) { if (isLayerGroup(layer)) { dispatch(ungroupLayer(layer.getId())); } dispatch(removeLayer(layer.getId())); } }); }; } export function promotePreviewLayers() { return (dispatch: Dispatch, getState: () => MapStoreState) => { getLayerList(getState()).forEach((layer) => { if (layer.isPreviewLayer()) { dispatch({ type: UPDATE_LAYER_PROP, id: layer.getId(), propName: '__isPreviewLayer', newValue: false, }); } }); }; } export function setLayerVisibility(layerId: string, makeVisible: boolean) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layer = getLayerById(layerId, getState()); if (!layer) { return; } if (isLayerGroup(layer)) { layer.getChildren().forEach((childLayer) => { dispatch(setLayerVisibility(childLayer.getId(), makeVisible)); }); } // If the layer visibility is already what we want it to be, do nothing if (layer.isVisible() === makeVisible) { return; } dispatch({ type: SET_LAYER_VISIBILITY, layerId, visibility: makeVisible, }); // if the current-state is invisible, we also want to sync data // e.g. if a layer was invisible at start-up, it won't have any data loaded if (makeVisible) { dispatch(syncDataForLayerId(layerId, false)); } }; } export function toggleLayerVisible(layerId: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layer = getLayerById(layerId, getState()); if (!layer) { return; } const makeVisible = !layer.isVisible(); dispatch(setLayerVisibility(layerId, makeVisible)); }; } export function hideAllLayers() { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { getLayerList(getState()).forEach((layer: ILayer, index: number) => { if (!layer.isBasemap(index)) { dispatch(setLayerVisibility(layer.getId(), false)); } }); }; } export function showAllLayers() { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { getLayerList(getState()).forEach((layer: ILayer, index: number) => { dispatch(setLayerVisibility(layer.getId(), true)); }); }; } export function showThisLayerOnly(layerId: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { getLayerList(getState()).forEach((layer: ILayer, index: number) => { if (layer.isBasemap(index) || layer.getId() === layerId) { return; } // hide all other layers dispatch(setLayerVisibility(layer.getId(), false)); }); // show target layer after hiding all other layers // since hiding layer group will hide its children const targetLayer = getLayerById(layerId, getState()); if (targetLayer) { dispatch(setLayerVisibility(layerId, true)); } }; } export function setSelectedLayer(layerId: string | null) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const oldSelectedLayer = getSelectedLayerId(getState()); if (oldSelectedLayer) { await dispatch(rollbackToTrackedLayerStateForSelectedLayer()); } if (layerId) { dispatch(trackCurrentLayerState(layerId)); // Reset draw mode only if setting a new selected layer if (getDrawMode(getState()) !== DRAW_MODE.NONE) { dispatch(setDrawMode(DRAW_MODE.NONE)); } } dispatch({ type: SET_SELECTED_LAYER, selectedLayerId: layerId, }); }; } export function setFirstPreviewLayerToSelectedLayer() { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const firstPreviewLayer = getLayerList(getState()).find((layer) => { return layer.isPreviewLayer(); }); if (firstPreviewLayer) { dispatch(setSelectedLayer(firstPreviewLayer.getId())); } }; } export function updateLayerOrder(newLayerOrder: number[]) { return { type: UPDATE_LAYER_ORDER, newLayerOrder, }; } function updateMetricsProp(layerId: string, value: unknown) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layer = getLayerById(layerId, getState()); const previousFields = layer && hasVectorLayerMethod(layer, 'getFields') ? await layer.getFields() : []; dispatch({ type: UPDATE_SOURCE_PROP, layerId, propName: 'metrics', value, }); await dispatch(updateStyleProperties(layerId, previousFields as IESAggField[])); }; } function updateSourcePropWithoutSync( layerId: string, propName: string, value: unknown, newLayerType?: LAYER_TYPE ) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { if (propName === 'metrics') { if (newLayerType) { throw new Error('May not change layer-type when modifying metrics source-property'); } return await dispatch(updateMetricsProp(layerId, value)); } dispatch({ type: UPDATE_SOURCE_PROP, layerId, propName, value, }); if (newLayerType) { dispatch(updateLayerType(layerId, newLayerType)); } if (propName === 'scalingType') { // get joins from layer descriptor instead of layer.getJoins() // 1) IVectorLayer implementations may return empty array when descriptor has joins // 2) getJoins returns instances and descriptors are needed. const layerDescriptor = getLayerDescriptor(getState(), layerId) as VectorLayerDescriptor; const joins = layerDescriptor.joins ? layerDescriptor.joins : []; if (value === SCALING_TYPES.CLUSTERS && joins.length) { // Blended scaling type does not support joins // It is not possible to display join metrics when showing clusters dispatch({ type: SET_JOINS, layerId, joins: [], }); await dispatch(updateStyleProperties(layerId)); } else if (value === SCALING_TYPES.MVT) { const filteredJoins = joins.filter((joinDescriptor) => { return !isSpatialJoin(joinDescriptor); }); // Maplibre feature-state join uses promoteId and there is a limit to one promoteId // Therefore, Vector tile scaling supports only one join dispatch({ type: SET_JOINS, layerId, joins: filteredJoins.length ? [filteredJoins[0]] : [], }); // update style props regardless of updating joins // Allow style to clean-up data driven style properties with join fields that do not support feature-state. await dispatch(updateStyleProperties(layerId)); } } }; } export function updateSourceProp( layerId: string, propName: string, value: unknown, newLayerType?: LAYER_TYPE ) { return async (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => { await dispatch(updateSourcePropWithoutSync(layerId, propName, value, newLayerType)); dispatch(syncDataForLayerId(layerId, false)); }; } export function updateSourceProps(layerId: string, sourcePropChanges: OnSourceChangeArgs[]) { return async (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => { // Using for loop to ensure update completes before starting next update for (let i = 0; i < sourcePropChanges.length; i++) { const { propName, value, newLayerType } = sourcePropChanges[i]; await dispatch(updateSourcePropWithoutSync(layerId, propName, value, newLayerType)); } dispatch(syncDataForLayerId(layerId, false)); }; } function updateLayerType(layerId: string, newLayerType: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layer = getLayerById(layerId, getState()); if (!layer || layer.getType() === newLayerType) { return; } dispatch(clearDataRequests(layer)); clearInspectorAdapters(layer, getInspectorAdapters(getState())); dispatch({ type: UPDATE_LAYER_PROP, id: layerId, propName: 'type', newValue: newLayerType, }); }; } export function updateLayerLabel(id: string, newLabel: string) { return { type: UPDATE_LAYER_PROP, id, propName: 'label', newValue: newLabel, }; } export function updateLayerLocale(id: string, locale: string) { return { type: UPDATE_LAYER_PROP, id, propName: 'locale', newValue: locale, }; } export function setLayerAttribution(id: string, attribution: Attribution) { return { type: UPDATE_LAYER_PROP, id, propName: 'attribution', newValue: attribution, }; } export function clearLayerAttribution(id: string) { return { type: CLEAR_LAYER_PROP, id, propName: 'attribution', }; } export function updateLayerMinZoom(id: string, minZoom: number) { return { type: UPDATE_LAYER_PROP, id, propName: 'minZoom', newValue: minZoom, }; } export function updateLayerMaxZoom(id: string, maxZoom: number) { return { type: UPDATE_LAYER_PROP, id, propName: 'maxZoom', newValue: maxZoom, }; } export function updateLayerAlpha(id: string, alpha: number) { return { type: UPDATE_LAYER_PROP, id, propName: 'alpha', newValue: alpha, }; } export function updateLabelsOnTop(id: string, areLabelsOnTop: boolean) { return { type: UPDATE_LAYER_PROP, id, propName: 'areLabelsOnTop', newValue: areLabelsOnTop, }; } export function updateFittableFlag(id: string, includeInFitToBounds: boolean) { return { type: UPDATE_LAYER_PROP, id, propName: 'includeInFitToBounds', newValue: includeInFitToBounds, }; } export function updateDisableTooltips(id: string, disableTooltips: boolean) { return { type: UPDATE_LAYER_PROP, id, propName: 'disableTooltips', newValue: disableTooltips, }; } export function setLayerQuery(id: string, query: Query) { return (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => { dispatch({ type: UPDATE_LAYER_PROP, id, propName: 'query', newValue: query, }); dispatch(syncDataForLayerId(id, false)); }; } export function setLayerParent(id: string, parent: string | undefined) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { dispatch({ type: UPDATE_LAYER_PROP, id, propName: 'parent', newValue: parent, }); if (parent) { // Open parent layer details. Without opening parent details, layer disappears from legend and this confuses users dispatch(showTOCDetails(parent)); } }; } export function removeSelectedLayer() { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const state = getState(); const layerId = getSelectedLayerId(state); if (layerId) { dispatch(removeLayer(layerId)); } }; } export function removeLayer(layerId: string) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const state = getState(); const selectedLayerId = getSelectedLayerId(state); if (layerId === selectedLayerId) { dispatch(updateFlyout(FLYOUT_STATE.NONE)); await dispatch(setSelectedLayer(null)); } dispatch(removeLayerFromLayerList(layerId)); }; } function removeLayerFromLayerList(layerId: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layerGettingRemoved = getLayerById(layerId, getState()); if (!layerGettingRemoved) { return; } layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); clearInspectorAdapters(layerGettingRemoved, getInspectorAdapters(getState())); dispatch({ type: REMOVE_LAYER, id: layerId, }); // Clean up draw state if needed const editState = getEditState(getState()); if (layerId === editState?.layerId) { dispatch(setDrawMode(DRAW_MODE.NONE)); } const openTOCDetails = getOpenTOCDetails(getState()); if (openTOCDetails.includes(layerId)) { dispatch(hideTOCDetails(layerId)); } if (isLayerGroup(layerGettingRemoved)) { layerGettingRemoved.getChildren().forEach((childLayer) => { dispatch(removeLayerFromLayerList(childLayer.getId())); }); } }; } function updateStyleProperties(layerId: string, previousFields?: IField[]) { return async ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const targetLayer: ILayer | undefined = getLayerById(layerId, getState()); if (!targetLayer) { return; } const style = targetLayer!.getCurrentStyle(); if (!style || style.getType() !== LAYER_STYLE_TYPE.VECTOR) { return; } if (!hasVectorLayerMethod(targetLayer, 'getFields')) { return; } const nextFields = await targetLayer.getFields(); // take into account all fields, since labels can be driven by any field (source or join) const { hasChanges, nextStyleDescriptor } = await ( style as IVectorStyle ).getDescriptorWithUpdatedStyleProps(nextFields, getMapColors(getState()), previousFields); if (hasChanges && nextStyleDescriptor) { dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); } }; } export function updateLayerStyle(layerId: string, styleDescriptor: StyleDescriptor) { return (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => { dispatch({ type: UPDATE_LAYER_STYLE, layerId, style: { ...styleDescriptor, }, }); // Auto open layer legend to increase legend discoverability if (hasByValueStyling(styleDescriptor)) { dispatch(showTOCDetails(layerId)); } // Ensure updateStyleMeta is triggered // syncDataForLayer may not trigger endDataLoad if no re-fetch is required dispatch(updateStyleMeta(layerId)); // Style update may require re-fetch, for example ES search may need to retrieve field used for dynamic styling dispatch(syncDataForLayerId(layerId, false)); }; } export function updateLayerStyleForSelectedLayer(styleDescriptor: StyleDescriptor) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const selectedLayerId = getSelectedLayerId(getState()); if (!selectedLayerId) { return; } dispatch(updateLayerStyle(selectedLayerId, styleDescriptor)); }; } export function setJoinsForLayer(layer: ILayer, joins: Array<Partial<JoinDescriptor>>) { return async (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => { const previousFields = hasVectorLayerMethod(layer, 'getFields') ? await layer.getFields() : []; dispatch({ type: SET_JOINS, layerId: layer.getId(), joins, }); await dispatch(updateStyleProperties(layer.getId(), previousFields)); dispatch(syncDataForLayerId(layer.getId(), false)); }; } export function setHiddenLayers(hiddenLayerIds: string[]) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const isMapReady = getMapReady(getState()); if (!isMapReady) { dispatch({ type: SET_WAITING_FOR_READY_HIDDEN_LAYERS, hiddenLayerIds }); } else { getLayerListRaw(getState()).forEach((layer) => dispatch(setLayerVisibility(layer.id, !hiddenLayerIds.includes(layer.id))) ); } }; } export function setTileState( layerId: string, areTilesLoaded: boolean, tileMetaFeatures?: TileMetaFeature[], tileErrors?: TileError[] ) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layer = getLayerById(layerId, getState()); if (!layer) { return; } dispatch({ type: UPDATE_LAYER_PROP, id: layerId, propName: '__areTilesLoaded', newValue: areTilesLoaded, }); dispatch({ type: UPDATE_LAYER_PROP, id: layerId, propName: '__tileErrors', newValue: tileErrors, }); if (!isLayerGroup(layer) && isESVectorTileSource(layer.getSource())) { getInspectorAdapters(getState()).vectorTiles.setTileResults( layerId, tileMetaFeatures, tileErrors ); } if (!tileMetaFeatures && !layer.getDescriptor().__tileMetaFeatures) { return; } dispatch({ type: UPDATE_LAYER_PROP, id: layerId, propName: '__tileMetaFeatures', newValue: tileMetaFeatures, }); dispatch(updateStyleMeta(layerId)); }; } function clearInspectorAdapters(layer: ILayer, adapters: Adapters) { if (isLayerGroup(layer)) { return; } if (adapters.vectorTiles) { adapters.vectorTiles.removeLayer(layer.getId()); } const source = layer.getSource(); if ('getInspectorRequestIds' in source) { (source as IVectorSource).getInspectorRequestIds().forEach((id) => { adapters.requests!.resetRequest(id); }); } if (adapters.requests && hasVectorLayerMethod(layer, 'getValidJoins')) { layer.getValidJoins().forEach((join) => { adapters.requests!.resetRequest(join.getRightJoinSource().getId()); }); } } function hasByValueStyling(styleDescriptor: StyleDescriptor) { return ( styleDescriptor.type === LAYER_STYLE_TYPE.VECTOR && Object.values((styleDescriptor as VectorStyleDescriptor).properties).some((styleProperty) => { return (styleProperty as { type?: STYLE_TYPE })?.type === STYLE_TYPE.DYNAMIC; }) ); } export function createLayerGroup(draggedLayerId: string, combineLayerId: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const group = LayerGroup.createDescriptor({}); const combineLayerDescriptor = getLayerDescriptor(getState(), combineLayerId); if (combineLayerDescriptor?.parent) { group.parent = combineLayerDescriptor.parent; } dispatch({ type: ADD_LAYER, layer: group, }); // Move group to left of combine-layer dispatch(moveLayerToLeftOfTarget(group.id, combineLayerId)); dispatch(showTOCDetails(group.id)); dispatch(setLayerParent(draggedLayerId, group.id)); dispatch(setLayerParent(combineLayerId, group.id)); // Move dragged-layer to left of combine-layer dispatch(moveLayerToLeftOfTarget(draggedLayerId, combineLayerId)); }; } export function ungroupLayer(layerId: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layer = getLayerList(getState()).find((findLayer) => findLayer.getId() === layerId); if (!layer || !isLayerGroup(layer)) { return; } layer.getChildren().forEach((childLayer) => { dispatch(setLayerParent(childLayer.getId(), layer.getParent())); }); }; } export function moveLayerToLeftOfTarget(moveLayerId: string, targetLayerId: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layers = getLayerList(getState()); const moveLayerIndex = layers.findIndex((layer) => layer.getId() === moveLayerId); const targetLayerIndex = layers.findIndex((layer) => layer.getId() === targetLayerId); if (moveLayerIndex === -1 || targetLayerIndex === -1) { return; } const moveLayer = layers[moveLayerIndex]; const newIndex = moveLayerIndex > targetLayerIndex ? // When layer is moved to the right, new left sibling index is to the left of destination targetLayerIndex + 1 : // When layer is moved to the left, new left sibling index is the destination index targetLayerIndex; const newOrder = []; for (let i = 0; i < layers.length; i++) { newOrder.push(i); } newOrder.splice(moveLayerIndex, 1); newOrder.splice(newIndex, 0, moveLayerIndex); dispatch(updateLayerOrder(newOrder)); if (isLayerGroup(moveLayer)) { moveLayer.getChildren().forEach((childLayer) => { dispatch(moveLayerToLeftOfTarget(childLayer.getId(), targetLayerId)); }); } }; } export function moveLayerToBottom(moveLayerId: string) { return ( dispatch: ThunkDispatch<MapStoreState, void, AnyAction>, getState: () => MapStoreState ) => { const layers = getLayerList(getState()); const moveLayerIndex = layers.findIndex((layer) => layer.getId() === moveLayerId); if (moveLayerIndex === -1) { return; } const moveLayer = layers[moveLayerIndex]; const newIndex = 0; const newOrder = []; for (let i = 0; i < layers.length; i++) { newOrder.push(i); } newOrder.splice(moveLayerIndex, 1); newOrder.splice(newIndex, 0, moveLayerIndex); dispatch(updateLayerOrder(newOrder)); if (isLayerGroup(moveLayer)) { (moveLayer as LayerGroup).getChildren().forEach((childLayer) => { dispatch(moveLayerToBottom(childLayer.getId())); }); } }; }