Vizzuality/landgriffon

View on GitHub
client/src/store/features/analysis/map.ts

Summary

Maintainability
A
25 mins
Test Coverage
import { createSlice } from '@reduxjs/toolkit';

import type { PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from 'store';
import type { Layer } from 'types';

const DEFAULT_LAYER_ATTRIBUTES = {
  order: 0,
  visible: false,
  active: false,
  opacity: 0.7,
  loading: false,
  isContextual: false,
};

const DEFAULT_DECKGL_PROPS = {
  data: [],
  wireframe: false,
  filled: true,
  stroked: true,
  extruded: false,
  highPrecision: 'auto',
  pickable: true,
  coverage: 0.9,
  lineWidthMinPixels: 2,
  opacity: DEFAULT_LAYER_ATTRIBUTES.opacity,
  visible: DEFAULT_LAYER_ATTRIBUTES.active,
};

interface DeckGLConstructorProps {
  id: Layer['id'];
  visible: boolean;
  opacity: number;
}

type TooltipData = {
  id: string;
  name: string;
  value?: number;
  unit?: string;
};

export type AnalysisMapState = {
  layers: Record<Layer['id'], Layer>;
  // Tooltip state
  tooltipData: TooltipData[];
  tooltipPosition: {
    x: number;
    y: number;
    viewport?: {
      width: number;
      height: number;
    };
  };
  // Deck.gl layer props by layer id
  layerDeckGLProps: Record<Layer['id'], Partial<DeckGLConstructorProps>>;
};

// Define the initial state using that type
export const initialState: AnalysisMapState = {
  layers: {
    impact: {
      ...DEFAULT_LAYER_ATTRIBUTES,
      id: 'impact',
      order: 0,
      active: true,
      visible: true,
    },
    material: {
      ...DEFAULT_LAYER_ATTRIBUTES,
      id: 'material',
      order: 1,
    },
  },
  tooltipData: [],
  tooltipPosition: {
    viewport: null,
    x: 0,
    y: 0,
  },
  layerDeckGLProps: {},
};

export const analysisMapSlice = createSlice({
  name: 'analysisMap',
  initialState,
  reducers: {
    setLayer: (
      state,
      action: PayloadAction<{
        id: Layer['id'];
        layer: Partial<Layer>;
      }>,
    ) => {
      const layers = {
        ...state.layers,
        [action.payload.id]: {
          ...DEFAULT_LAYER_ATTRIBUTES,
          id: action.payload.id,
          ...state.layers[action.payload.id],
          ...action.payload.layer,
        },
      };

      return {
        ...state,
        layerDeckGLProps: {
          ...state.layerDeckGLProps,
          [action.payload.id]: {
            ...DEFAULT_DECKGL_PROPS,
            id: action.payload.id,
            ...state.layerDeckGLProps[action.payload.id],
            visible: layers[action.payload.id].active && layers[action.payload.id].visible,
            opacity: layers[action.payload.id].opacity,
          },
        },
        layers,
      };
    },
    setLayerDeckGLProps: (
      state,
      action: PayloadAction<{
        id: Layer['id'];
        props: Partial<DeckGLConstructorProps>;
      }>,
    ) => {
      return {
        ...state,
        layerDeckGLProps: {
          ...state.layerDeckGLProps,
          [action.payload.id]: {
            ...DEFAULT_DECKGL_PROPS,
            ...state.layerDeckGLProps[action.payload.id],
            ...action.payload.props,
          },
        },
      };
    },
    setLayerOrder: (state: AnalysisMapState, action: PayloadAction<Layer['id'][]>) => {
      Object.values(state.layers).forEach((layer) => {
        layer.order = action.payload.indexOf(layer.id);
      });
      return state;
    },
    // Tooltip
    setTooltipData: (state, action: PayloadAction<TooltipData>) => {
      const exists = !!state.tooltipData.find(({ id }) => action.payload.id === id);
      // Remove tooltip is value is undefined but not zero
      if (exists && !action.payload.value && action.payload.value !== 0) {
        return {
          ...state,
          tooltipData: state.tooltipData.filter((data) => data.id !== action.payload.id),
        };
      }

      // If exists replace the info
      if (exists) {
        return {
          ...state,
          tooltipData: state.tooltipData.map((data) => {
            if (data.id === action.payload.id) {
              return { ...data, ...action.payload };
            }
            return data;
          }),
        };
      }

      // add data if doesn't exist
      return {
        ...state,
        tooltipData: [...state.tooltipData, action.payload],
      };
    },

    setTooltipPosition: (state, action: PayloadAction<AnalysisMapState['tooltipPosition']>) => ({
      ...state,
      tooltipPosition: action.payload,
    }),
  },
});

export const { setLayer, setLayerDeckGLProps, setTooltipData, setTooltipPosition, setLayerOrder } =
  analysisMapSlice.actions;

export const analysisMap = (state: RootState): AnalysisMapState => state['analysis/map'];

export default analysisMapSlice.reducer;