From 43bce44e6742d83d3a80e47541f45f4516adb573 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Tue, 17 Dec 2024 11:08:21 +0100
Subject: [PATCH 01/22] feat(vector-map): implement adding a new layer

---
 .../LayerFactoryModal.component.tsx           | 93 +++++++++++++++++++
 .../Modal/LayerFactoryModal/index.ts          |  1 +
 .../FunctionalArea/Modal/Modal.component.tsx  |  6 ++
 .../ModalLayout/ModalLayout.component.tsx     |  1 +
 .../LayersDrawer/LayersDrawer.component.tsx   | 13 ++-
 .../utils/shapes/layer/Layer.test.ts          |  3 -
 src/models/layerTextSchema.ts                 |  4 -
 src/redux/apiPath.ts                          |  1 +
 src/redux/layers/layers.thunks.ts             | 21 ++++-
 src/redux/layers/layers.types.ts              |  7 ++
 src/redux/modal/modal.reducers.ts             |  6 ++
 src/redux/modal/modal.slice.ts                |  3 +
 src/shared/Switch/Switch.component.tsx        |  3 +
 src/types/modal.ts                            |  3 +-
 14 files changed, 155 insertions(+), 10 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
 create mode 100644 src/components/FunctionalArea/Modal/LayerFactoryModal/index.ts

diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
new file mode 100644
index 00000000..410706e7
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
@@ -0,0 +1,93 @@
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { Button } from '@/shared/Button';
+import { Input } from '@/shared/Input';
+import React, { useState } from 'react';
+
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { currentModelIdSelector } from '@/redux/models/models.selectors';
+import { closeModal } from '@/redux/modal/modal.slice';
+import { showToast } from '@/utils/showToast';
+import { Switch } from '@/shared/Switch';
+import { LayerStoreInterface } from '@/redux/layers/layers.types';
+import { addLayerForModel, getLayersForModel } from '@/redux/layers/layers.thunks';
+import { SerializedError } from '@reduxjs/toolkit';
+
+export const LayerFactoryModal: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const currentModelId = useAppSelector(currentModelIdSelector);
+
+  const [data, setData] = useState<LayerStoreInterface>({
+    name: '',
+    visible: false,
+    locked: false,
+    modelId: currentModelId,
+  });
+
+  const handleChange = (value: string | boolean, key: string): void => {
+    setData(prevData => ({ ...prevData, [key]: value }));
+  };
+
+  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
+    try {
+      event.preventDefault();
+      await dispatch(addLayerForModel(data)).unwrap();
+      dispatch(closeModal());
+      dispatch(getLayersForModel(currentModelId));
+      showToast({
+        type: 'success',
+        message: 'A new layer has been successfully added',
+      });
+    } catch (error) {
+      const typedError = error as SerializedError;
+      showToast({
+        type: 'error',
+        message: typedError.message || 'An error occurred while adding a new layer',
+      });
+    }
+  };
+
+  return (
+    <div className="w-[400px] border border-t-[#E1E0E6] bg-white p-[24px]">
+      <form onSubmit={handleSubmit}>
+        <label className="mb-6 block text-sm font-semibold" htmlFor="name">
+          Name:
+          <Input
+            type="text"
+            id="name"
+            placeholder="Layer name here..."
+            value={data.name}
+            onChange={event => {
+              handleChange(event.target.value, 'name');
+            }}
+            className="mt-2.5 text-sm font-medium text-font-400"
+          />
+        </label>
+        <label
+          htmlFor="visible"
+          className="mb-6 flex items-center justify-between text-sm font-semibold"
+        >
+          Visible:
+          <Switch
+            id="visible"
+            isChecked={data.visible}
+            onToggle={value => handleChange(value, 'visible')}
+          />
+        </label>
+        <label
+          htmlFor="locked"
+          className="mb-6 flex items-center justify-between text-sm font-semibold"
+        >
+          Locked:
+          <Switch
+            id="locked"
+            isChecked={data.locked}
+            onToggle={value => handleChange(value, 'locked')}
+          />
+        </label>
+        <Button type="submit" className="w-full justify-center text-base font-medium">
+          Submit
+        </Button>
+      </form>
+    </div>
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/index.ts b/src/components/FunctionalArea/Modal/LayerFactoryModal/index.ts
new file mode 100644
index 00000000..5e9a93d6
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/index.ts
@@ -0,0 +1 @@
+export { LayerFactoryModal } from './LayerFactoryModal.component';
diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx
index 2768dfa3..8419791d 100644
--- a/src/components/FunctionalArea/Modal/Modal.component.tsx
+++ b/src/components/FunctionalArea/Modal/Modal.component.tsx
@@ -12,6 +12,7 @@ import { ModalLayout } from './ModalLayout';
 import { OverviewImagesModal } from './OverviewImagesModal';
 import { PublicationsModal } from './PublicationsModal';
 import { LoggedInMenuModal } from './LoggedInMenuModal';
+import { LayerFactoryModal } from './LayerFactoryModal';
 
 const MolArtModal = dynamic(
   () => import('./MolArtModal/MolArtModal.component').then(mod => mod.MolArtModal),
@@ -79,6 +80,11 @@ export const Modal = (): React.ReactNode => {
           <AddCommentModal />
         </ModalLayout>
       )}
+      {isOpen && modalName === 'layer-factory' && (
+        <ModalLayout>
+          <LayerFactoryModal />
+        </ModalLayout>
+      )}
     </>
   );
 };
diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
index b202afcd..493f2bc7 100644
--- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
+++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
@@ -33,6 +33,7 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
             modalName === 'terms-of-service' && 'h-auto w-[400px]',
             modalName === 'add-comment' && 'h-auto w-[400px]',
             modalName === 'error-report' && 'h-auto w-[800px]',
+            modalName === 'layer-factory' && 'h-auto w-[400px]',
             ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]',
           )}
         >
diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
index e5f914e8..0387ccc8 100644
--- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
+++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
@@ -8,6 +8,8 @@ import {
 import { Switch } from '@/shared/Switch';
 import { setLayerVisibility } from '@/redux/layers/layers.slice';
 import { currentModelIdSelector } from '@/redux/models/models.selectors';
+import { Button } from '@/shared/Button';
+import { openLayerFactoryModal } from '@/redux/modal/modal.slice';
 
 export const LayersDrawer = (): JSX.Element => {
   const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector);
@@ -15,12 +17,21 @@ export const LayersDrawer = (): JSX.Element => {
   const currentModelId = useAppSelector(currentModelIdSelector);
   const dispatch = useAppDispatch();
 
+  const addNewLayer = (): void => {
+    dispatch(openLayerFactoryModal());
+  };
+
   return (
     <div data-testid="layers-drawer" className="h-full max-h-full">
       <DrawerHeading title="Layers" />
       <div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto px-6">
+        <div className="flex justify-start pt-2">
+          <Button icon="plus" isIcon isFrontIcon onClick={addNewLayer}>
+            Add new layer
+          </Button>
+        </div>
         {layersForCurrentModel.map(layer => (
-          <div key={layer.details.id} className="flex items-center justify-between border-b p-4">
+          <div key={layer.details.id} className="flex items-center justify-between border-b py-4">
             <h1>{layer.details.name}</h1>
             <Switch
               isChecked={layersVisibilityForCurrentModel[layer.details.id]}
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
index 46baee43..4fd1b57f 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
@@ -44,10 +44,7 @@ describe('Layer', () => {
           width: 100,
           height: 100,
           fontSize: 12,
-          size: 12312,
           notes: 'XYZ',
-          glyph: null,
-          elementId: '34',
           verticalAlign: 'MIDDLE',
           horizontalAlign: 'CENTER',
           backgroundColor: WHITE_COLOR,
diff --git a/src/models/layerTextSchema.ts b/src/models/layerTextSchema.ts
index 3ad77ed0..6858da82 100644
--- a/src/models/layerTextSchema.ts
+++ b/src/models/layerTextSchema.ts
@@ -1,6 +1,5 @@
 import { z } from 'zod';
 import { colorSchema } from '@/models/colorSchema';
-import { glyphSchema } from '@/models/glyphSchema';
 
 export const layerTextSchema = z.object({
   id: z.number(),
@@ -10,10 +9,7 @@ export const layerTextSchema = z.object({
   width: z.number(),
   height: z.number(),
   fontSize: z.number(),
-  size: z.number(),
   notes: z.string(),
-  glyph: glyphSchema.nullable(),
-  elementId: z.string(),
   verticalAlign: z.string(),
   horizontalAlign: z.string(),
   backgroundColor: colorSchema,
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 7daee91c..8f767881 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -58,6 +58,7 @@ export const apiPath = {
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/ovals/`,
   getLayerLines: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`,
+  storeLayer: (modelId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/`,
   getGlyphImage: (glyphId: number): string =>
     `projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`,
   getNewReactionsForModel: (modelId: number): string =>
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index f1594875..2d2699c3 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -8,7 +8,7 @@ import { getError } from '@/utils/error-report/getError';
 import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
 import { layerSchema } from '@/models/layerSchema';
 import { LAYERS_FETCHING_ERROR_PREFIX } from '@/redux/layers/layers.constants';
-import { LayersVisibilitiesState } from '@/redux/layers/layers.types';
+import { LayerStoreInterface, LayersVisibilitiesState } from '@/redux/layers/layers.types';
 import { layerTextSchema } from '@/models/layerTextSchema';
 import { layerRectSchema } from '@/models/layerRectSchema';
 import { pageableSchema } from '@/models/pageableSchema';
@@ -64,3 +64,22 @@ export const getLayersForModel = createAsyncThunk<
     return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX }));
   }
 });
+
+export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterface, ThunkConfig>(
+  'vectorMap/addLayer',
+  async ({ name, visible, locked, modelId }) => {
+    try {
+      const { data } = await axiosInstanceNewAPI.post<Layer>(apiPath.storeLayer(modelId), {
+        name,
+        visible,
+        locked,
+      });
+
+      const isDataValid = validateDataUsingZodSchema(data, layerSchema);
+
+      return isDataValid ? data : null;
+    } catch (error) {
+      return Promise.reject(getError({ error }));
+    }
+  },
+);
diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts
index 701a499a..4c1f283f 100644
--- a/src/redux/layers/layers.types.ts
+++ b/src/redux/layers/layers.types.ts
@@ -1,6 +1,13 @@
 import { KeyedFetchDataState } from '@/types/fetchDataState';
 import { Layer, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models';
 
+export interface LayerStoreInterface {
+  name: string;
+  visible: boolean;
+  locked: boolean;
+  modelId: number;
+}
+
 export type LayerState = {
   details: Layer;
   texts: LayerText[];
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index a3704696..78a651f2 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -124,3 +124,9 @@ export const openToSModalReducer = (state: ModalState): void => {
   state.modalName = 'terms-of-service';
   state.modalTitle = 'Terms of service!';
 };
+
+export const openLayerFactoryModalReducer = (state: ModalState): void => {
+  state.isOpen = true;
+  state.modalName = 'layer-factory';
+  state.modalTitle = 'Add new layer';
+};
diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts
index bb145852..8ed04215 100644
--- a/src/redux/modal/modal.slice.ts
+++ b/src/redux/modal/modal.slice.ts
@@ -16,6 +16,7 @@ import {
   openSelectProjectModalReducer,
   openLicenseModalReducer,
   openToSModalReducer,
+  openLayerFactoryModalReducer,
 } from './modal.reducers';
 
 const modalSlice = createSlice({
@@ -37,6 +38,7 @@ const modalSlice = createSlice({
     openSelectProjectModal: openSelectProjectModalReducer,
     openLicenseModal: openLicenseModalReducer,
     openToSModal: openToSModalReducer,
+    openLayerFactoryModal: openLayerFactoryModalReducer,
   },
 });
 
@@ -56,6 +58,7 @@ export const {
   openSelectProjectModal,
   openLicenseModal,
   openToSModal,
+  openLayerFactoryModal,
 } = modalSlice.actions;
 
 export default modalSlice.reducer;
diff --git a/src/shared/Switch/Switch.component.tsx b/src/shared/Switch/Switch.component.tsx
index 355e84b9..a3d9fafb 100644
--- a/src/shared/Switch/Switch.component.tsx
+++ b/src/shared/Switch/Switch.component.tsx
@@ -7,6 +7,7 @@ export interface SwitchProps {
   variantStyles?: VariantStyle;
   isChecked?: boolean;
   onToggle?: (checked: boolean) => void;
+  id?: string;
 }
 
 const variants = {
@@ -32,6 +33,7 @@ export const Switch = ({
   variantStyles = 'primary',
   isChecked = false,
   onToggle,
+  id,
 }: SwitchProps): JSX.Element => {
   const [checked, setChecked] = useState(isChecked);
 
@@ -49,6 +51,7 @@ export const Switch = ({
 
   return (
     <button
+      id={id}
       type="button"
       className={twMerge(
         'relative inline-flex h-5 w-10 cursor-pointer rounded-full transition-colors duration-300 ease-in-out',
diff --git a/src/types/modal.ts b/src/types/modal.ts
index eaf3a498..861bb295 100644
--- a/src/types/modal.ts
+++ b/src/types/modal.ts
@@ -11,4 +11,5 @@ export type ModalName =
   | 'access-denied'
   | 'select-project'
   | 'terms-of-service'
-  | 'logged-in-menu';
+  | 'logged-in-menu'
+  | 'layer-factory';
-- 
GitLab


From 1f929ceb4f76ab3730a7db4cbddf0338229b94a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Tue, 17 Dec 2024 13:11:23 +0100
Subject: [PATCH 02/22] feat(vector-map): implement editing an existing layer

---
 .../EditOverlayModal.component.test.tsx       |  7 ++
 .../hooks/useEditOverlay.test.ts              |  5 ++
 .../LayerFactoryModal.component.tsx           | 68 ++++++++++++++++---
 .../LayerFactoryModal.styles.css              | 12 ++++
 .../ElementLink.component.test.tsx            |  4 +-
 .../ChemicalsList.component.test.tsx          |  2 +-
 .../DrugsList/DrugsList.component.test.tsx    |  2 +-
 .../LayersDrawer/LayersDrawer.component.tsx   |  9 ++-
 src/redux/apiPath.ts                          |  4 ++
 src/redux/layers/layers.thunks.ts             | 41 ++++++++++-
 src/redux/layers/layers.types.ts              |  8 +++
 src/redux/modal/modal.constants.ts            |  1 +
 src/redux/modal/modal.mock.ts                 |  1 +
 src/redux/modal/modal.reducers.ts             | 12 +++-
 src/redux/modal/modal.selector.ts             |  5 ++
 src/redux/modal/modal.types.ts                |  5 ++
 .../LoadingIndicator.component.tsx            | 26 ++++---
 17 files changed, 184 insertions(+), 28 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.styles.css

diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx
index b8015a4b..f8072beb 100644
--- a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx
+++ b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx
@@ -47,6 +47,7 @@ describe('EditOverlayModal - component', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
@@ -65,6 +66,7 @@ describe('EditOverlayModal - component', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
@@ -95,6 +97,7 @@ describe('EditOverlayModal - component', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -130,6 +133,7 @@ describe('EditOverlayModal - component', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -166,6 +170,7 @@ describe('EditOverlayModal - component', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -201,6 +206,7 @@ describe('EditOverlayModal - component', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -230,6 +236,7 @@ describe('EditOverlayModal - component', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts
index 172a1026..16f43071 100644
--- a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts
+++ b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts
@@ -24,6 +24,7 @@ describe('useEditOverlay', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
@@ -60,6 +61,7 @@ describe('useEditOverlay', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
@@ -99,6 +101,7 @@ describe('useEditOverlay', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
@@ -134,6 +137,7 @@ describe('useEditOverlay', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
@@ -170,6 +174,7 @@ describe('useEditOverlay', () => {
         molArtState: {},
         overviewImagesState: {},
         errorReportState: {},
+        layerFactoryState: { id: undefined },
       },
     });
 
diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
index 410706e7..a5165763 100644
--- a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
@@ -1,20 +1,30 @@
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { Button } from '@/shared/Button';
 import { Input } from '@/shared/Input';
-import React, { useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { currentModelIdSelector } from '@/redux/models/models.selectors';
 import { closeModal } from '@/redux/modal/modal.slice';
 import { showToast } from '@/utils/showToast';
 import { Switch } from '@/shared/Switch';
-import { LayerStoreInterface } from '@/redux/layers/layers.types';
-import { addLayerForModel, getLayersForModel } from '@/redux/layers/layers.thunks';
+import { LayerStoreInterface, LayerUpdateInterface } from '@/redux/layers/layers.types';
+import {
+  addLayerForModel,
+  getLayer,
+  getLayersForModel,
+  updateLayer,
+} from '@/redux/layers/layers.thunks';
 import { SerializedError } from '@reduxjs/toolkit';
+import { layerFactoryStateSelector } from '@/redux/modal/modal.selector';
+import './LayerFactoryModal.styles.css';
+import { LoadingIndicator } from '@/shared/LoadingIndicator';
 
 export const LayerFactoryModal: React.FC = () => {
   const dispatch = useAppDispatch();
   const currentModelId = useAppSelector(currentModelIdSelector);
+  const layerFactoryState = useAppSelector(layerFactoryStateSelector);
+  const [loaded, setLoaded] = useState<boolean>(false);
 
   const [data, setData] = useState<LayerStoreInterface>({
     name: '',
@@ -23,6 +33,29 @@ export const LayerFactoryModal: React.FC = () => {
     modelId: currentModelId,
   });
 
+  const fetchData = useMemo(() => {
+    return async (layerId: number): Promise<void> => {
+      const layer = await dispatch(getLayer({ modelId: currentModelId, layerId })).unwrap();
+      if (layer) {
+        setData({
+          name: layer.name,
+          visible: layer.visible,
+          locked: layer.locked,
+          modelId: currentModelId,
+        });
+      }
+      setLoaded(true);
+    };
+  }, [currentModelId, dispatch]);
+
+  useEffect(() => {
+    if (layerFactoryState.id) {
+      fetchData(layerFactoryState.id);
+    } else {
+      setLoaded(true);
+    }
+  }, [fetchData, layerFactoryState.id]);
+
   const handleChange = (value: string | boolean, key: string): void => {
     setData(prevData => ({ ...prevData, [key]: value }));
   };
@@ -30,13 +63,25 @@ export const LayerFactoryModal: React.FC = () => {
   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
     try {
       event.preventDefault();
-      await dispatch(addLayerForModel(data)).unwrap();
+      if (layerFactoryState.id) {
+        const payload = {
+          ...data,
+          layerId: layerFactoryState.id,
+        } as LayerUpdateInterface;
+        await dispatch(updateLayer(payload)).unwrap();
+        showToast({
+          type: 'success',
+          message: 'The layer has been successfully updated',
+        });
+      } else {
+        await dispatch(addLayerForModel(data)).unwrap();
+        showToast({
+          type: 'success',
+          message: 'A new layer has been successfully added',
+        });
+      }
       dispatch(closeModal());
       dispatch(getLayersForModel(currentModelId));
-      showToast({
-        type: 'success',
-        message: 'A new layer has been successfully added',
-      });
     } catch (error) {
       const typedError = error as SerializedError;
       showToast({
@@ -47,7 +92,12 @@ export const LayerFactoryModal: React.FC = () => {
   };
 
   return (
-    <div className="w-[400px] border border-t-[#E1E0E6] bg-white p-[24px]">
+    <div className="relative w-[400px] border border-t-[#E1E0E6] bg-white p-[24px]">
+      {!loaded && (
+        <div className="c-layer-factory-loader">
+          <LoadingIndicator width={44} height={44} />
+        </div>
+      )}
       <form onSubmit={handleSubmit}>
         <label className="mb-6 block text-sm font-semibold" htmlFor="name">
           Name:
diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.styles.css b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.styles.css
new file mode 100644
index 00000000..9178a2af
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.styles.css
@@ -0,0 +1,12 @@
+.c-layer-factory-loader {
+  width: 100%;
+  height: 100%;
+  margin-left: -24px;
+  margin-top: -24px;
+  background: #f9f9f980;
+  z-index: 1;
+  position: absolute;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx
index 9ed58cb2..38e85800 100644
--- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx
@@ -54,7 +54,7 @@ describe('ElementLink - component', () => {
     });
 
     it('should show loading indicator', () => {
-      const loadingIndicator = screen.getByAltText('spinner icon');
+      const loadingIndicator = screen.getByTestId('loading-indicator');
 
       expect(loadingIndicator).toBeInTheDocument();
     });
@@ -75,7 +75,7 @@ describe('ElementLink - component', () => {
     });
 
     it('should not show loading indicator', async () => {
-      const loadingIndicator = screen.getByAltText('spinner icon');
+      const loadingIndicator = screen.getByTestId('loading-indicator');
 
       await waitFor(() => {
         expect(loadingIndicator).not.toBeInTheDocument();
diff --git a/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.test.tsx
index 03adc8fd..c40c8c87 100644
--- a/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.test.tsx
+++ b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.test.tsx
@@ -47,7 +47,7 @@ describe('ChemicalsList - component', () => {
     });
 
     it('should show loading indicator', () => {
-      expect(screen.getByAltText('spinner icon')).toBeInTheDocument();
+      expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
     });
   });
 
diff --git a/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.test.tsx
index 0ec503c4..b04ddbab 100644
--- a/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.test.tsx
+++ b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.test.tsx
@@ -47,7 +47,7 @@ describe('DrugsList - component', () => {
     });
 
     it('should show loading indicator', () => {
-      expect(screen.getByAltText('spinner icon')).toBeInTheDocument();
+      expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
     });
   });
 
diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
index 0387ccc8..4c38de5e 100644
--- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
+++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
@@ -21,6 +21,10 @@ export const LayersDrawer = (): JSX.Element => {
     dispatch(openLayerFactoryModal());
   };
 
+  const editLayer = (layerId: number): void => {
+    dispatch(openLayerFactoryModal(layerId));
+  };
+
   return (
     <div data-testid="layers-drawer" className="h-full max-h-full">
       <DrawerHeading title="Layers" />
@@ -32,7 +36,10 @@ export const LayersDrawer = (): JSX.Element => {
         </div>
         {layersForCurrentModel.map(layer => (
           <div key={layer.details.id} className="flex items-center justify-between border-b py-4">
-            <h1>{layer.details.name}</h1>
+            <div className="flex items-center gap-3">
+              <Button onClick={() => editLayer(layer.details.id)}>Edit</Button>
+              <h1>{layer.details.name}</h1>
+            </div>
             <Switch
               isChecked={layersVisibilityForCurrentModel[layer.details.id]}
               onToggle={value =>
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 8f767881..96388b3f 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -59,6 +59,10 @@ export const apiPath = {
   getLayerLines: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`,
   storeLayer: (modelId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/`,
+  updateLayer: (modelId: number, layerId: number): string =>
+    `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
+  getLayer: (modelId: number, layerId: number): string =>
+    `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
   getGlyphImage: (glyphId: number): string =>
     `projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`,
   getNewReactionsForModel: (modelId: number): string =>
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index 2d2699c3..346ef51e 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -8,13 +8,33 @@ import { getError } from '@/utils/error-report/getError';
 import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
 import { layerSchema } from '@/models/layerSchema';
 import { LAYERS_FETCHING_ERROR_PREFIX } from '@/redux/layers/layers.constants';
-import { LayerStoreInterface, LayersVisibilitiesState } from '@/redux/layers/layers.types';
+import {
+  LayerStoreInterface,
+  LayersVisibilitiesState,
+  LayerUpdateInterface,
+} from '@/redux/layers/layers.types';
 import { layerTextSchema } from '@/models/layerTextSchema';
 import { layerRectSchema } from '@/models/layerRectSchema';
 import { pageableSchema } from '@/models/pageableSchema';
 import { layerOvalSchema } from '@/models/layerOvalSchema';
 import { layerLineSchema } from '@/models/layerLineSchema';
 
+export const getLayer = createAsyncThunk<
+  Layer | null,
+  { modelId: number; layerId: number },
+  ThunkConfig
+>('vectorMap/getLayer', async ({ modelId, layerId }) => {
+  try {
+    const { data } = await axiosInstanceNewAPI.get<Layer>(apiPath.getLayer(modelId, layerId));
+
+    const isDataValid = validateDataUsingZodSchema(data, layerSchema);
+
+    return isDataValid ? data : null;
+  } catch (error) {
+    return Promise.reject(getError({ error }));
+  }
+});
+
 export const getLayersForModel = createAsyncThunk<
   LayersVisibilitiesState | undefined,
   number,
@@ -83,3 +103,22 @@ export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterfa
     }
   },
 );
+
+export const updateLayer = createAsyncThunk<Layer | null, LayerUpdateInterface, ThunkConfig>(
+  'vectorMap/updateLayer',
+  async ({ name, visible, locked, modelId, layerId }) => {
+    try {
+      const { data } = await axiosInstanceNewAPI.put<Layer>(apiPath.updateLayer(modelId, layerId), {
+        name,
+        visible,
+        locked,
+      });
+
+      const isDataValid = validateDataUsingZodSchema(data, layerSchema);
+
+      return isDataValid ? data : null;
+    } catch (error) {
+      return Promise.reject(getError({ error }));
+    }
+  },
+);
diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts
index 4c1f283f..a5c1b30e 100644
--- a/src/redux/layers/layers.types.ts
+++ b/src/redux/layers/layers.types.ts
@@ -8,6 +8,14 @@ export interface LayerStoreInterface {
   modelId: number;
 }
 
+export interface LayerUpdateInterface {
+  layerId: number;
+  name: string;
+  visible: boolean;
+  locked: boolean;
+  modelId: number;
+}
+
 export type LayerState = {
   details: Layer;
   texts: LayerText[];
diff --git a/src/redux/modal/modal.constants.ts b/src/redux/modal/modal.constants.ts
index d7dea45c..1184d4ed 100644
--- a/src/redux/modal/modal.constants.ts
+++ b/src/redux/modal/modal.constants.ts
@@ -13,4 +13,5 @@ export const MODAL_INITIAL_STATE: ModalState = {
   },
   editOverlayState: null,
   errorReportState: {},
+  layerFactoryState: { id: undefined },
 };
diff --git a/src/redux/modal/modal.mock.ts b/src/redux/modal/modal.mock.ts
index cde5fab5..1a7a519f 100644
--- a/src/redux/modal/modal.mock.ts
+++ b/src/redux/modal/modal.mock.ts
@@ -13,4 +13,5 @@ export const MODAL_INITIAL_STATE_MOCK: ModalState = {
   },
   editOverlayState: null,
   errorReportState: {},
+  layerFactoryState: { id: undefined },
 };
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index 78a651f2..3371ed3c 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -125,8 +125,16 @@ export const openToSModalReducer = (state: ModalState): void => {
   state.modalTitle = 'Terms of service!';
 };
 
-export const openLayerFactoryModalReducer = (state: ModalState): void => {
+export const openLayerFactoryModalReducer = (
+  state: ModalState,
+  action: PayloadAction<number | undefined>,
+): void => {
+  state.layerFactoryState = { id: action.payload };
   state.isOpen = true;
   state.modalName = 'layer-factory';
-  state.modalTitle = 'Add new layer';
+  if (action.payload) {
+    state.modalTitle = 'Edit layer';
+  } else {
+    state.modalTitle = 'Add new layer';
+  }
 };
diff --git a/src/redux/modal/modal.selector.ts b/src/redux/modal/modal.selector.ts
index 654dfb7a..7f7c4441 100644
--- a/src/redux/modal/modal.selector.ts
+++ b/src/redux/modal/modal.selector.ts
@@ -21,6 +21,11 @@ export const currentEditedOverlaySelector = createSelector(
   modal => modal.editOverlayState,
 );
 
+export const layerFactoryStateSelector = createSelector(
+  modalSelector,
+  modal => modal.layerFactoryState,
+);
+
 export const currentErrorDataSelector = createSelector(
   modalSelector,
   modal => modal?.errorReportState.errorData || undefined,
diff --git a/src/redux/modal/modal.types.ts b/src/redux/modal/modal.types.ts
index ea772096..1b544f52 100644
--- a/src/redux/modal/modal.types.ts
+++ b/src/redux/modal/modal.types.ts
@@ -17,6 +17,10 @@ export type ErrorRepostState = {
 
 export type EditOverlayState = MapOverlay | null;
 
+export type LayerFactoryState = {
+  id: number | undefined;
+};
+
 export interface ModalState {
   isOpen: boolean;
   modalName: ModalName;
@@ -25,6 +29,7 @@ export interface ModalState {
   molArtState: MolArtModalState;
   errorReportState: ErrorRepostState;
   editOverlayState: EditOverlayState;
+  layerFactoryState: LayerFactoryState;
 }
 
 export type OpenEditOverlayModalPayload = MapOverlay;
diff --git a/src/shared/LoadingIndicator/LoadingIndicator.component.tsx b/src/shared/LoadingIndicator/LoadingIndicator.component.tsx
index 39a2a413..e63f0267 100644
--- a/src/shared/LoadingIndicator/LoadingIndicator.component.tsx
+++ b/src/shared/LoadingIndicator/LoadingIndicator.component.tsx
@@ -1,6 +1,3 @@
-import Image from 'next/image';
-import spinnerIcon from '@/assets/vectors/icons/spinner.svg';
-
 type LoadingIndicatorProps = {
   height?: number;
   width?: number;
@@ -13,12 +10,19 @@ export const LoadingIndicator = ({
   height = DEFAULT_HEIGHT,
   width = DEFAULT_WIDTH,
 }: LoadingIndicatorProps): JSX.Element => (
-  <Image
-    src={spinnerIcon}
-    alt="spinner icon"
-    height={height}
-    width={width}
-    className="animate-spin"
-    data-testid="loading-indicator"
-  />
+  <div style={{ width, height }} className="animate-spin" data-testid="loading-indicator">
+    <svg width={width} height={height} viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
+      <circle
+        cx="25"
+        cy="25"
+        r="20"
+        fill="none"
+        stroke="currentColor"
+        strokeWidth="4"
+        strokeDasharray="90, 150"
+        strokeDashoffset="0"
+        strokeLinecap="round"
+      />
+    </svg>
+  </div>
 );
-- 
GitLab


From 9989610faf52020b9f68efc0c6cd1cfb0804c5e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Wed, 18 Dec 2024 09:58:50 +0100
Subject: [PATCH 03/22] feat(vector-map): implement removing an existing layer

---
 pages/_document.tsx                           |  1 +
 .../Modal/QuestionModal/Question.styles.css   | 27 ++++++
 .../QuestionModal/QustionModal.component.tsx  | 56 ++++++++++++
 .../LayersDrawer/LayersDrawer.component.tsx   | 87 +++++++++++++++----
 src/redux/apiPath.ts                          |  2 +
 src/redux/layers/layers.thunks.ts             | 13 +++
 src/shared/Button/Button.component.tsx        |  7 +-
 src/shared/Icon/Icon.component.tsx            |  2 +
 src/shared/Icon/Icons/QuestionIcon.tsx        | 23 +++++
 src/types/iconTypes.ts                        |  3 +-
 10 files changed, 202 insertions(+), 19 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/QuestionModal/Question.styles.css
 create mode 100644 src/components/FunctionalArea/Modal/QuestionModal/QustionModal.component.tsx
 create mode 100644 src/shared/Icon/Icons/QuestionIcon.tsx

diff --git a/pages/_document.tsx b/pages/_document.tsx
index 94c6212c..c0d2fd72 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -8,6 +8,7 @@ const Document = (): React.ReactNode => (
     </Head>
     <body>
       <Main />
+      <div id="modal-root" />
       <NextScript />
       <Script src="./config.js" strategy="beforeInteractive" />
     </body>
diff --git a/src/components/FunctionalArea/Modal/QuestionModal/Question.styles.css b/src/components/FunctionalArea/Modal/QuestionModal/Question.styles.css
new file mode 100644
index 00000000..9d2ec888
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/QuestionModal/Question.styles.css
@@ -0,0 +1,27 @@
+.c-question-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 11;
+}
+
+.c-question-modal {
+  width: 400px;
+  height: auto;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  gap: 2rem;
+  background-color: #fff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+  text-align: center;
+}
diff --git a/src/components/FunctionalArea/Modal/QuestionModal/QustionModal.component.tsx b/src/components/FunctionalArea/Modal/QuestionModal/QustionModal.component.tsx
new file mode 100644
index 00000000..3f7f3db7
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/QuestionModal/QustionModal.component.tsx
@@ -0,0 +1,56 @@
+import React, { ReactPortal } from 'react';
+import ReactDOM from 'react-dom';
+import { Button } from '@/shared/Button';
+import './Question.styles.css';
+import { QuestionIcon } from '@/shared/Icon/Icons/QuestionIcon';
+
+type QuestionModalProps = {
+  isOpen: boolean;
+  onClose: () => void;
+  onConfirm: () => void;
+  question: string;
+};
+
+const QuestionModal = ({
+  isOpen,
+  onClose,
+  onConfirm,
+  question,
+}: QuestionModalProps): null | ReactPortal => {
+  if (!isOpen) return null;
+
+  const domElement = document.getElementById('modal-root');
+
+  if (!domElement) {
+    return null;
+  }
+
+  return ReactDOM.createPortal(
+    <div className="c-question-overlay">
+      <div className="c-question-modal">
+        <QuestionIcon size={94} />
+        <h1 className="text-center text-2xl font-semibold">{question}</h1>
+        <div className="flex w-full justify-center gap-10">
+          <Button
+            type="submit"
+            className="w-[100px] justify-center text-base font-medium"
+            variantStyles="remove"
+            onClick={onClose}
+          >
+            No
+          </Button>
+          <Button
+            type="submit"
+            className="w-[100px] justify-center text-base font-medium"
+            onClick={onConfirm}
+          >
+            Yes
+          </Button>
+        </div>
+      </div>
+    </div>,
+    domElement,
+  );
+};
+
+export default QuestionModal;
diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
index 4c38de5e..b431590c 100644
--- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
+++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx
@@ -10,23 +10,66 @@ import { setLayerVisibility } from '@/redux/layers/layers.slice';
 import { currentModelIdSelector } from '@/redux/models/models.selectors';
 import { Button } from '@/shared/Button';
 import { openLayerFactoryModal } from '@/redux/modal/modal.slice';
+import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component';
+import { useState } from 'react';
+import { getLayersForModel, removeLayer } from '@/redux/layers/layers.thunks';
+import { showToast } from '@/utils/showToast';
+import { SerializedError } from '@reduxjs/toolkit';
 
 export const LayersDrawer = (): JSX.Element => {
   const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector);
   const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector);
   const currentModelId = useAppSelector(currentModelIdSelector);
   const dispatch = useAppDispatch();
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [layerId, setLayerId] = useState<number | null>(null);
 
   const addNewLayer = (): void => {
     dispatch(openLayerFactoryModal());
   };
 
-  const editLayer = (layerId: number): void => {
-    dispatch(openLayerFactoryModal(layerId));
+  const editLayer = (layerIdToEdit: number): void => {
+    dispatch(openLayerFactoryModal(layerIdToEdit));
+  };
+
+  const rejectRemove = (): void => {
+    setIsModalOpen(false);
+  };
+
+  const confirmRemove = async (): Promise<void> => {
+    if (!layerId) {
+      return;
+    }
+    try {
+      await dispatch(removeLayer({ modelId: currentModelId, layerId })).unwrap();
+      showToast({
+        type: 'success',
+        message: 'The layer has been successfully removed',
+      });
+      setIsModalOpen(false);
+      dispatch(getLayersForModel(currentModelId));
+    } catch (error) {
+      const typedError = error as SerializedError;
+      showToast({
+        type: 'error',
+        message: typedError.message || 'An error occurred while removing the layer',
+      });
+    }
+  };
+
+  const onRemoveLayer = (layerIdToRemove: number): void => {
+    setLayerId(layerIdToRemove);
+    setIsModalOpen(true);
   };
 
   return (
     <div data-testid="layers-drawer" className="h-full max-h-full">
+      <QuestionModal
+        isOpen={isModalOpen}
+        onClose={rejectRemove}
+        onConfirm={confirmRemove}
+        question="Are you sure you want to remove the layer?"
+      />
       <DrawerHeading title="Layers" />
       <div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto px-6">
         <div className="flex justify-start pt-2">
@@ -35,23 +78,33 @@ export const LayersDrawer = (): JSX.Element => {
           </Button>
         </div>
         {layersForCurrentModel.map(layer => (
-          <div key={layer.details.id} className="flex items-center justify-between border-b py-4">
-            <div className="flex items-center gap-3">
+          <div
+            key={layer.details.id}
+            className="flex items-center justify-between gap-3 border-b py-4"
+          >
+            <h1 className="truncate">{layer.details.name}</h1>
+            <div className="flex items-center gap-2">
+              <Switch
+                isChecked={layersVisibilityForCurrentModel[layer.details.id]}
+                onToggle={value =>
+                  dispatch(
+                    setLayerVisibility({
+                      modelId: currentModelId,
+                      visible: value,
+                      layerId: layer.details.id,
+                    }),
+                  )
+                }
+              />
               <Button onClick={() => editLayer(layer.details.id)}>Edit</Button>
-              <h1>{layer.details.name}</h1>
+              <Button
+                onClick={() => onRemoveLayer(layer.details.id)}
+                color="error"
+                variantStyles="remove"
+              >
+                Remove
+              </Button>
             </div>
-            <Switch
-              isChecked={layersVisibilityForCurrentModel[layer.details.id]}
-              onToggle={value =>
-                dispatch(
-                  setLayerVisibility({
-                    modelId: currentModelId,
-                    visible: value,
-                    layerId: layer.details.id,
-                  }),
-                )
-              }
-            />
           </div>
         ))}
       </div>
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 96388b3f..6a080e33 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -61,6 +61,8 @@ export const apiPath = {
   storeLayer: (modelId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/`,
   updateLayer: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
+  removeLayer: (modelId: number, layerId: number): string =>
+    `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
   getLayer: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
   getGlyphImage: (glyphId: number): string =>
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index 346ef51e..c8ee4fd5 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -122,3 +122,16 @@ export const updateLayer = createAsyncThunk<Layer | null, LayerUpdateInterface,
     }
   },
 );
+
+export const removeLayer = createAsyncThunk<
+  void,
+  { modelId: number; layerId: number },
+  ThunkConfig
+  // eslint-disable-next-line consistent-return
+>('vectorMap/removeLayer', async ({ modelId, layerId }) => {
+  try {
+    await axiosInstanceNewAPI.delete<void>(apiPath.removeLayer(modelId, layerId));
+  } catch (error) {
+    return Promise.reject(getError({ error }));
+  }
+});
diff --git a/src/shared/Button/Button.component.tsx b/src/shared/Button/Button.component.tsx
index a7f831e7..72833322 100644
--- a/src/shared/Button/Button.component.tsx
+++ b/src/shared/Button/Button.component.tsx
@@ -4,7 +4,7 @@ import { twMerge } from 'tailwind-merge';
 import type { ButtonHTMLAttributes } from 'react';
 import type { IconTypes } from '@/types/iconTypes';
 
-type VariantStyle = 'primary' | 'secondary' | 'ghost' | 'quiet';
+type VariantStyle = 'primary' | 'secondary' | 'ghost' | 'quiet' | 'remove';
 
 export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
   variantStyles?: VariantStyle;
@@ -34,6 +34,11 @@ const variants = {
       'text-font-500 bg-white-pearl hover:bg-greyscale-500 active:bg-greyscale-600 disabled:text-font-400 disabled:bg-white-pearl',
     icon: 'fill-font-500 group-disabled:fill-font-400',
   },
+  remove: {
+    button:
+      'text-white-pearl bg-red-500 hover:bg-red-600 active:bg-red-700 disabled:bg-greyscale-700',
+    icon: 'fill-white-pearl',
+  },
 } as const;
 
 export const Button = ({
diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx
index 63456954..f22e882b 100644
--- a/src/shared/Icon/Icon.component.tsx
+++ b/src/shared/Icon/Icon.component.tsx
@@ -8,6 +8,7 @@ import { CloseIcon } from '@/shared/Icon/Icons/CloseIcon';
 import { DotsIcon } from '@/shared/Icon/Icons/DotsIcon';
 import { ExportIcon } from '@/shared/Icon/Icons/ExportIcon';
 import { LayersIcon } from '@/shared/Icon/Icons/LayersIcon';
+import { QuestionIcon } from '@/shared/Icon/Icons/QuestionIcon';
 import { InfoIcon } from '@/shared/Icon/Icons/InfoIcon';
 import { LegendIcon } from '@/shared/Icon/Icons/LegendIcon';
 import { PageIcon } from '@/shared/Icon/Icons/PageIcon';
@@ -43,6 +44,7 @@ const icons: Record<IconTypes, IconComponentType> = {
   admin: AdminIcon,
   export: ExportIcon,
   layers: LayersIcon,
+  question: QuestionIcon,
   info: InfoIcon,
   download: DownloadIcon,
   legend: LegendIcon,
diff --git a/src/shared/Icon/Icons/QuestionIcon.tsx b/src/shared/Icon/Icons/QuestionIcon.tsx
new file mode 100644
index 00000000..9deb2242
--- /dev/null
+++ b/src/shared/Icon/Icons/QuestionIcon.tsx
@@ -0,0 +1,23 @@
+/* eslint-disable no-magic-numbers */
+interface QuestionIconProps {
+  className?: string;
+  size?: number;
+}
+
+export const QuestionIcon = ({ className, size = 20 }: QuestionIconProps): JSX.Element => (
+  <svg
+    width={size}
+    height={size}
+    viewBox="0 0 100 100"
+    fill="none"
+    className={className}
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <circle cx="50" cy="50" r="44" stroke="black" strokeWidth="2" fill="none" />
+    <path
+      d="M50 80a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm1-20H47c0-6.6 3.6-8.8 6.6-10.9 3.4-2.3 5.4-4.5 5.4-8.1 0-5.5-4.5-10-10-10s-10 4.5-10 10H33c0-9.4 7.6-17 17-17s17 7.6 17 17c0 5.5-3.3 8.6-6.9 11.1-2.6 1.7-4.1 3.3-4.1 6.9z"
+      fill="black"
+      strokeWidth={1}
+    />
+  </svg>
+);
diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts
index 5000d642..76818394 100644
--- a/src/types/iconTypes.ts
+++ b/src/types/iconTypes.ts
@@ -23,6 +23,7 @@ export type IconTypes =
   | 'clear'
   | 'user'
   | 'manage-user'
-  | 'download';
+  | 'download'
+  | 'question';
 
 export type IconComponentType = ({ className }: { className: string }) => JSX.Element;
-- 
GitLab


From 3aef53f4de020d0be3433be63cc27f3f1afe9694 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Wed, 18 Dec 2024 10:38:31 +0100
Subject: [PATCH 04/22] feat(vector-map): add tests for layers crud

---
 .../LayerFactoryModal.component.test.tsx      | 88 ++++++++++++++++++
 .../LayerFactoryModal.component.tsx           | 10 ++-
 .../QuestionModal.component.test.tsx          | 58 ++++++++++++
 src/models/fixtures/layerFixture.ts           |  9 ++
 src/redux/layers/layers.thunks.test.ts        | 90 ++++++++++++++++++-
 src/shared/Switch/Switch.component.tsx        |  6 +-
 6 files changed, 257 insertions(+), 4 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx
 create mode 100644 src/components/FunctionalArea/Modal/QuestionModal/QuestionModal.component.test.tsx
 create mode 100644 src/models/fixtures/layerFixture.ts

diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx
new file mode 100644
index 00000000..037943f2
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx
@@ -0,0 +1,88 @@
+/* eslint-disable no-magic-numbers */
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { StoreType } from '@/redux/store';
+import {
+  InitialStoreState,
+  getReduxWrapperWithStore,
+} from '@/utils/testing/getReduxWrapperWithStore';
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import { overlaysFixture } from '@/models/fixtures/overlaysFixture';
+import { apiPath } from '@/redux/apiPath';
+import { HttpStatusCode } from 'axios';
+import { layerFixture } from '@/models/fixtures/layerFixture';
+import { layersFixture } from '@/models/fixtures/layersFixture';
+import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture';
+import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture';
+import { layerOvalsFixture } from '@/models/fixtures/layerOvalsFixture';
+import { layerLinesFixture } from '@/models/fixtures/layerLinesFixture';
+import { act } from 'react-dom/test-utils';
+import { LayerFactoryModal } from './LayerFactoryModal.component';
+
+const mockedAxiosNewClient = mockNetworkNewAPIResponse();
+
+const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
+
+  return (
+    render(
+      <Wrapper>
+        <LayerFactoryModal />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
+
+describe('LayerFactoryModal - component', () => {
+  it('should render LayerFactoryModal component', () => {
+    renderComponent();
+
+    const name = screen.getByTestId('layer-factory-name');
+    const visible = screen.getByTestId('layer-factory-visible');
+    const locked = screen.getByTestId('layer-factory-locked');
+    expect(name).toBeInTheDocument();
+    expect(visible).toBeInTheDocument();
+    expect(locked).toBeInTheDocument();
+  });
+
+  it('should handles input change correctly', () => {
+    renderComponent();
+
+    const nameInput: HTMLInputElement = screen.getByTestId('layer-factory-name');
+
+    fireEvent.change(nameInput, { target: { value: 'test layer' } });
+
+    expect(nameInput.value).toBe('test layer');
+  });
+
+  it('should fetch layers when the form is  successfully submitted', async () => {
+    mockedAxiosNewClient.onPost(apiPath.storeLayer(0)).reply(HttpStatusCode.Ok, layerFixture);
+    mockedAxiosNewClient.onGet(apiPath.getLayers(0)).reply(HttpStatusCode.Ok, layersFixture);
+    mockedAxiosNewClient
+      .onGet(apiPath.getLayerTexts(0, layersFixture.content[0].id))
+      .reply(HttpStatusCode.Ok, layerTextsFixture);
+    mockedAxiosNewClient
+      .onGet(apiPath.getLayerRects(0, layersFixture.content[0].id))
+      .reply(HttpStatusCode.Ok, layerRectsFixture);
+    mockedAxiosNewClient
+      .onGet(apiPath.getLayerOvals(0, layersFixture.content[0].id))
+      .reply(HttpStatusCode.Ok, layerOvalsFixture);
+    mockedAxiosNewClient
+      .onGet(apiPath.getLayerLines(0, layersFixture.content[0].id))
+      .reply(HttpStatusCode.Ok, layerLinesFixture);
+
+    const { store } = renderComponent();
+    const nameInput: HTMLInputElement = screen.getByTestId('layer-factory-name');
+    const submitButton = screen.getByTestId('submit');
+
+    fireEvent.change(nameInput, { target: { value: 'test layer' } });
+    act(() => {
+      submitButton.click();
+    });
+    await waitFor(() => {
+      expect(store.getState().layers[0].loading).toBe('succeeded');
+    });
+  });
+});
diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
index a5165763..21835162 100644
--- a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
@@ -83,6 +83,7 @@ export const LayerFactoryModal: React.FC = () => {
       dispatch(closeModal());
       dispatch(getLayersForModel(currentModelId));
     } catch (error) {
+      console.log('error', error);
       const typedError = error as SerializedError;
       showToast({
         type: 'error',
@@ -104,6 +105,7 @@ export const LayerFactoryModal: React.FC = () => {
           <Input
             type="text"
             id="name"
+            data-testid="layer-factory-name"
             placeholder="Layer name here..."
             value={data.name}
             onChange={event => {
@@ -119,6 +121,7 @@ export const LayerFactoryModal: React.FC = () => {
           Visible:
           <Switch
             id="visible"
+            data-testid="layer-factory-visible"
             isChecked={data.visible}
             onToggle={value => handleChange(value, 'visible')}
           />
@@ -130,11 +133,16 @@ export const LayerFactoryModal: React.FC = () => {
           Locked:
           <Switch
             id="locked"
+            data-testid="layer-factory-locked"
             isChecked={data.locked}
             onToggle={value => handleChange(value, 'locked')}
           />
         </label>
-        <Button type="submit" className="w-full justify-center text-base font-medium">
+        <Button
+          type="submit"
+          className="w-full justify-center text-base font-medium"
+          data-testid="submit"
+        >
           Submit
         </Button>
       </form>
diff --git a/src/components/FunctionalArea/Modal/QuestionModal/QuestionModal.component.test.tsx b/src/components/FunctionalArea/Modal/QuestionModal/QuestionModal.component.test.tsx
new file mode 100644
index 00000000..556cf577
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/QuestionModal/QuestionModal.component.test.tsx
@@ -0,0 +1,58 @@
+/* eslint-disable no-magic-numbers */
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import QuestionModal from './QustionModal.component';
+
+beforeEach(() => {
+  const modalRoot = document.createElement('div');
+  modalRoot.setAttribute('id', 'modal-root');
+  document.body.appendChild(modalRoot);
+});
+
+afterEach(() => {
+  const modalRoot = document.getElementById('modal-root');
+  if (modalRoot) {
+    document.body.removeChild(modalRoot);
+  }
+});
+
+describe('QuestionModal', () => {
+  const defaultProps = {
+    isOpen: true,
+    onClose: jest.fn(),
+    onConfirm: jest.fn(),
+    question: 'Are you sure?',
+  };
+
+  it('should not render when isOpen is false', () => {
+    render(<QuestionModal {...defaultProps} isOpen={false} />);
+    const modalContent = screen.queryByText(defaultProps.question);
+    expect(modalContent).not.toBeInTheDocument();
+  });
+
+  it('should render the question when isOpen is true', () => {
+    render(<QuestionModal {...defaultProps} />);
+    expect(screen.getByText('Are you sure?')).toBeInTheDocument();
+  });
+
+  it('should call onClose when "No" button is clicked', () => {
+    render(<QuestionModal {...defaultProps} />);
+    const noButton = screen.getByText('No');
+    fireEvent.click(noButton);
+    expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
+  });
+
+  it('should call onConfirm when "Yes" button is clicked', () => {
+    render(<QuestionModal {...defaultProps} />);
+    const yesButton = screen.getByText('Yes');
+    fireEvent.click(yesButton);
+    expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1);
+  });
+
+  it('should render inside the modal-root portal', () => {
+    render(<QuestionModal {...defaultProps} />);
+    const modalRoot = document.getElementById('modal-root');
+    expect(modalRoot).toContainElement(screen.getByText('Are you sure?'));
+  });
+});
diff --git a/src/models/fixtures/layerFixture.ts b/src/models/fixtures/layerFixture.ts
new file mode 100644
index 00000000..9f9d4843
--- /dev/null
+++ b/src/models/fixtures/layerFixture.ts
@@ -0,0 +1,9 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { layerSchema } from '@/models/layerSchema';
+
+export const layerFixture = createFixture(layerSchema, {
+  seed: ZOD_SEED,
+  array: { min: 1, max: 1 },
+});
diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts
index 234d9950..c8b9b2bc 100644
--- a/src/redux/layers/layers.thunks.test.ts
+++ b/src/redux/layers/layers.thunks.test.ts
@@ -7,12 +7,19 @@ import {
 import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
 import { HttpStatusCode } from 'axios';
 import { LayersState } from '@/redux/layers/layers.types';
-import { getLayersForModel } from '@/redux/layers/layers.thunks';
+import {
+  addLayerForModel,
+  getLayer,
+  getLayersForModel,
+  removeLayer,
+  updateLayer,
+} from '@/redux/layers/layers.thunks';
 import { layersFixture } from '@/models/fixtures/layersFixture';
 import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture';
 import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture';
 import { layerOvalsFixture } from '@/models/fixtures/layerOvalsFixture';
 import { layerLinesFixture } from '@/models/fixtures/layerLinesFixture';
+import { layerFixture } from '@/models/fixtures/layerFixture';
 import layersReducer from './layers.slice';
 
 const mockedAxiosClient = mockNetworkNewAPIResponse();
@@ -65,4 +72,85 @@ describe('layers thunks', () => {
       expect(payload).toEqual(undefined);
     });
   });
+
+  describe('getLayer', () => {
+    it('should return a layer when data is valid', async () => {
+      mockedAxiosClient.onGet(apiPath.getLayer(1, 2)).reply(HttpStatusCode.Ok, layerFixture);
+
+      const { payload } = await store.dispatch(getLayer({ modelId: 1, layerId: 2 }));
+      expect(payload).toEqual(layerFixture);
+    });
+
+    it('should return null when data is invalid', async () => {
+      mockedAxiosClient.onGet(apiPath.getLayer(1, 2)).reply(HttpStatusCode.Ok, { invalid: 'data' });
+
+      const { payload } = await store.dispatch(getLayer({ modelId: 1, layerId: 2 }));
+      expect(payload).toBeNull();
+    });
+  });
+
+  describe('addLayerForModel', () => {
+    it('should add a layer when data is valid', async () => {
+      mockedAxiosClient.onPost(apiPath.storeLayer(1)).reply(HttpStatusCode.Created, layerFixture);
+
+      const { payload } = await store.dispatch(
+        addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }),
+      );
+      expect(payload).toEqual(layerFixture);
+    });
+
+    it('should return null when response data is invalid', async () => {
+      mockedAxiosClient
+        .onPost(apiPath.storeLayer(1))
+        .reply(HttpStatusCode.Created, { invalid: 'data' });
+
+      const { payload } = await store.dispatch(
+        addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }),
+      );
+      expect(payload).toBeNull();
+    });
+  });
+
+  describe('updateLayer', () => {
+    it('should update a layer successfully', async () => {
+      mockedAxiosClient.onPut(apiPath.updateLayer(1, 2)).reply(HttpStatusCode.Ok, layerFixture);
+
+      const { payload } = await store.dispatch(
+        updateLayer({
+          name: 'Updated Layer',
+          visible: false,
+          locked: true,
+          modelId: 1,
+          layerId: 2,
+        }),
+      );
+      expect(payload).toEqual(layerFixture);
+    });
+
+    it('should return null for invalid data', async () => {
+      mockedAxiosClient
+        .onPut(apiPath.updateLayer(1, 2))
+        .reply(HttpStatusCode.Ok, { invalid: 'data' });
+
+      const { payload } = await store.dispatch(
+        updateLayer({
+          name: 'Updated Layer',
+          visible: false,
+          locked: true,
+          modelId: 1,
+          layerId: 2,
+        }),
+      );
+      expect(payload).toBeNull();
+    });
+  });
+
+  describe('removeLayer', () => {
+    it('should successfully remove a layer', async () => {
+      mockedAxiosClient.onDelete(apiPath.removeLayer(1, 2)).reply(HttpStatusCode.NoContent);
+
+      const result = await store.dispatch(removeLayer({ modelId: 1, layerId: 2 }));
+      expect(result.meta.requestStatus).toBe('fulfilled');
+    });
+  });
 });
diff --git a/src/shared/Switch/Switch.component.tsx b/src/shared/Switch/Switch.component.tsx
index a3d9fafb..b519870e 100644
--- a/src/shared/Switch/Switch.component.tsx
+++ b/src/shared/Switch/Switch.component.tsx
@@ -1,9 +1,9 @@
 import { twMerge } from 'tailwind-merge';
-import { useEffect, useState } from 'react';
+import { type ButtonHTMLAttributes, useEffect, useState } from 'react';
 
 type VariantStyle = 'primary' | 'secondary' | 'ghost' | 'quiet';
 
-export interface SwitchProps {
+export interface SwitchProps extends ButtonHTMLAttributes<HTMLButtonElement> {
   variantStyles?: VariantStyle;
   isChecked?: boolean;
   onToggle?: (checked: boolean) => void;
@@ -34,6 +34,7 @@ export const Switch = ({
   isChecked = false,
   onToggle,
   id,
+  ...props
 }: SwitchProps): JSX.Element => {
   const [checked, setChecked] = useState(isChecked);
 
@@ -59,6 +60,7 @@ export const Switch = ({
         checked ? 'bg-primary-600' : '',
       )}
       onClick={handleToggle}
+      {...props}
     >
       <span
         className={twMerge(
-- 
GitLab


From 8d5c50e26f08a0a093398508fefd406bb60a5da2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Wed, 18 Dec 2024 11:14:57 +0100
Subject: [PATCH 05/22] feat(vector-map): implement image objects for layers

---
 .../LayerFactoryModal.component.test.tsx      |  5 +++-
 .../LayerFactoryModal.component.tsx           |  1 -
 .../useOlMapAdditionalLayers.ts               |  1 +
 .../utils/shapes/layer/Layer.test.ts          | 11 +++++++
 .../utils/shapes/layer/Layer.ts               | 29 ++++++++++++++++++-
 src/models/fixtures/layerImagesFixture.ts     | 10 +++++++
 src/models/layerImageSchema.ts                | 11 +++++++
 src/redux/apiPath.ts                          |  2 ++
 src/redux/layers/layers.reducers.test.ts      |  9 ++++++
 src/redux/layers/layers.thunks.test.ts        |  5 ++++
 src/redux/layers/layers.thunks.ts             | 19 +++++++-----
 src/redux/layers/layers.types.ts              |  3 +-
 src/types/models.ts                           |  2 ++
 13 files changed, 97 insertions(+), 11 deletions(-)
 create mode 100644 src/models/fixtures/layerImagesFixture.ts
 create mode 100644 src/models/layerImageSchema.ts

diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx
index 037943f2..91d4fb0e 100644
--- a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.test.tsx
@@ -6,7 +6,6 @@ import {
   getReduxWrapperWithStore,
 } from '@/utils/testing/getReduxWrapperWithStore';
 import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
-import { overlaysFixture } from '@/models/fixtures/overlaysFixture';
 import { apiPath } from '@/redux/apiPath';
 import { HttpStatusCode } from 'axios';
 import { layerFixture } from '@/models/fixtures/layerFixture';
@@ -16,6 +15,7 @@ import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture';
 import { layerOvalsFixture } from '@/models/fixtures/layerOvalsFixture';
 import { layerLinesFixture } from '@/models/fixtures/layerLinesFixture';
 import { act } from 'react-dom/test-utils';
+import { layerImagesFixture } from '@/models/fixtures/layerImagesFixture';
 import { LayerFactoryModal } from './LayerFactoryModal.component';
 
 const mockedAxiosNewClient = mockNetworkNewAPIResponse();
@@ -72,6 +72,9 @@ describe('LayerFactoryModal - component', () => {
     mockedAxiosNewClient
       .onGet(apiPath.getLayerLines(0, layersFixture.content[0].id))
       .reply(HttpStatusCode.Ok, layerLinesFixture);
+    mockedAxiosNewClient
+      .onGet(apiPath.getLayerImages(0, layersFixture.content[0].id))
+      .reply(HttpStatusCode.Ok, layerImagesFixture);
 
     const { store } = renderComponent();
     const nameInput: HTMLInputElement = screen.getByTestId('layer-factory-name');
diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
index 21835162..8d925951 100644
--- a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx
@@ -83,7 +83,6 @@ export const LayerFactoryModal: React.FC = () => {
       dispatch(closeModal());
       dispatch(getLayersForModel(currentModelId));
     } catch (error) {
-      console.log('error', error);
       const typedError = error as SerializedError;
       showToast({
         type: 'error',
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
index f0dfaa9f..6b841f53 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
@@ -54,6 +54,7 @@ export const useOlMapAdditionalLayers = (
         rects: layer.rects,
         ovals: layer.ovals,
         lines: layer.lines,
+        images: layer.images,
         visible: layer.details.visible,
         layerId: layer.details.id,
         lineTypes,
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
index 4fd1b57f..eeabb89c 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
@@ -113,6 +113,17 @@ describe('Layer', () => {
           lineType: 'SOLID',
         },
       ],
+      images: [
+        {
+          id: 1,
+          glyph: 1,
+          x: 1,
+          y: 1,
+          width: 1,
+          height: 1,
+          z: 1,
+        },
+      ],
       visible: true,
       layerId: 23,
       pointToProjection: jest.fn(point => [point.x, point.y]),
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
index 0b8d6403..ef12c427 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
@@ -1,5 +1,5 @@
 /* eslint-disable no-magic-numbers */
-import { LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models';
+import { LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models';
 import { MapInstance } from '@/types/map';
 import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
 import { Feature } from 'ol';
@@ -26,12 +26,14 @@ import {
 } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
 import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle';
 import { Stroke } from 'ol/style';
+import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph';
 
 export interface LayerProps {
   texts: Array<LayerText>;
   rects: Array<LayerRect>;
   ovals: Array<LayerOval>;
   lines: Array<LayerLine>;
+  images: Array<LayerImage>;
   visible: boolean;
   layerId: number;
   lineTypes: LineTypeDict;
@@ -49,6 +51,8 @@ export default class Layer {
 
   lines: Array<LayerLine>;
 
+  images: Array<LayerImage>;
+
   lineTypes: LineTypeDict;
 
   arrowTypes: ArrowTypeDict;
@@ -59,6 +63,8 @@ export default class Layer {
 
   ovalFeatures: Array<Feature<Polygon>>;
 
+  imageFeatures: Array<Feature<Polygon>>;
+
   lineFeatures: Array<Feature<LineString>>;
 
   arrowFeatures: Array<Feature<MultiPolygon>>;
@@ -80,6 +86,7 @@ export default class Layer {
     rects,
     ovals,
     lines,
+    images,
     visible,
     layerId,
     lineTypes,
@@ -91,6 +98,7 @@ export default class Layer {
     this.rects = rects;
     this.ovals = ovals;
     this.lines = lines;
+    this.images = images;
     this.lineTypes = lineTypes;
     this.arrowTypes = arrowTypes;
     this.pointToProjection = pointToProjection;
@@ -98,6 +106,7 @@ export default class Layer {
     this.textFeatures = this.getTextsFeatures();
     this.rectFeatures = this.getRectsFeatures();
     this.ovalFeatures = this.getOvalsFeatures();
+    this.imageFeatures = this.getImagesFeatures();
     const { linesFeatures, arrowsFeatures } = this.getLinesFeatures();
     this.lineFeatures = linesFeatures;
     this.arrowFeatures = arrowsFeatures;
@@ -108,6 +117,7 @@ export default class Layer {
         ...this.ovalFeatures,
         ...this.lineFeatures,
         ...this.arrowFeatures,
+        ...this.imageFeatures,
       ],
     });
     this.vectorLayer = new VectorLayer({
@@ -293,6 +303,23 @@ export default class Layer {
     return { linesFeatures, arrowsFeatures };
   };
 
+  private getImagesFeatures = (): Array<Feature<Polygon>> => {
+    return this.images.map(image => {
+      const glyph = new Glyph({
+        elementId: image.id,
+        glyphId: image.glyph,
+        x: image.x,
+        y: image.y,
+        width: image.width,
+        height: image.height,
+        zIndex: image.z,
+        pointToProjection: this.pointToProjection,
+        mapInstance: this.mapInstance,
+      });
+      return glyph.feature;
+    });
+  };
+
   protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void {
     const styles: Array<Style> = [];
     const maxZoom = this.mapInstance?.getView().get('originalMaxZoom');
diff --git a/src/models/fixtures/layerImagesFixture.ts b/src/models/fixtures/layerImagesFixture.ts
new file mode 100644
index 00000000..382b5f92
--- /dev/null
+++ b/src/models/fixtures/layerImagesFixture.ts
@@ -0,0 +1,10 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { pageableSchema } from '@/models/pageableSchema';
+import { layerImageSchema } from '@/models/layerImageSchema';
+
+export const layerImagesFixture = createFixture(pageableSchema(layerImageSchema), {
+  seed: ZOD_SEED,
+  array: { min: 3, max: 3 },
+});
diff --git a/src/models/layerImageSchema.ts b/src/models/layerImageSchema.ts
new file mode 100644
index 00000000..61a6df2d
--- /dev/null
+++ b/src/models/layerImageSchema.ts
@@ -0,0 +1,11 @@
+import { z } from 'zod';
+
+export const layerImageSchema = z.object({
+  id: z.number(),
+  x: z.number(),
+  y: z.number(),
+  z: z.number(),
+  width: z.number(),
+  height: z.number(),
+  glyph: z.number(),
+});
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 6a080e33..cd1b447c 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -58,6 +58,8 @@ export const apiPath = {
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/ovals/`,
   getLayerLines: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`,
+  getLayerImages: (modelId: number, layerId: number): string =>
+    `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/`,
   storeLayer: (modelId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/`,
   updateLayer: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts
index 158ed1b0..938b7f3e 100644
--- a/src/redux/layers/layers.reducers.test.ts
+++ b/src/redux/layers/layers.reducers.test.ts
@@ -14,6 +14,7 @@ import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture';
 import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture';
 import { layerOvalsFixture } from '@/models/fixtures/layerOvalsFixture';
 import { layerLinesFixture } from '@/models/fixtures/layerLinesFixture';
+import { layerImagesFixture } from '@/models/fixtures/layerImagesFixture';
 import { LayersState } from './layers.types';
 import layersReducer from './layers.slice';
 
@@ -47,6 +48,9 @@ describe('layers reducer', () => {
     mockedAxiosClient
       .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id))
       .reply(HttpStatusCode.Ok, layerLinesFixture);
+    mockedAxiosClient
+      .onGet(apiPath.getLayerImages(1, layersFixture.content[0].id))
+      .reply(HttpStatusCode.Ok, layerImagesFixture);
 
     const { type } = await store.dispatch(getLayersForModel(1));
     const { data, loading, error } = store.getState().layers[1];
@@ -61,6 +65,7 @@ describe('layers reducer', () => {
           rects: layerRectsFixture.content,
           ovals: layerOvalsFixture.content,
           lines: layerLinesFixture.content,
+          images: layerImagesFixture.content,
         },
       ],
       layersVisibility: {
@@ -98,6 +103,9 @@ describe('layers reducer', () => {
     mockedAxiosClient
       .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id))
       .reply(HttpStatusCode.Ok, layerLinesFixture);
+    mockedAxiosClient
+      .onGet(apiPath.getLayerImages(1, layersFixture.content[0].id))
+      .reply(HttpStatusCode.Ok, layerImagesFixture);
 
     const layersPromise = store.dispatch(getLayersForModel(1));
 
@@ -116,6 +124,7 @@ describe('layers reducer', () => {
             rects: layerRectsFixture.content,
             ovals: layerOvalsFixture.content,
             lines: layerLinesFixture.content,
+            images: layerImagesFixture.content,
           },
         ],
         layersVisibility: {
diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts
index c8b9b2bc..975e8ec9 100644
--- a/src/redux/layers/layers.thunks.test.ts
+++ b/src/redux/layers/layers.thunks.test.ts
@@ -20,6 +20,7 @@ import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture';
 import { layerOvalsFixture } from '@/models/fixtures/layerOvalsFixture';
 import { layerLinesFixture } from '@/models/fixtures/layerLinesFixture';
 import { layerFixture } from '@/models/fixtures/layerFixture';
+import { layerImagesFixture } from '@/models/fixtures/layerImagesFixture';
 import layersReducer from './layers.slice';
 
 const mockedAxiosClient = mockNetworkNewAPIResponse();
@@ -45,6 +46,9 @@ describe('layers thunks', () => {
       mockedAxiosClient
         .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id))
         .reply(HttpStatusCode.Ok, layerLinesFixture);
+      mockedAxiosClient
+        .onGet(apiPath.getLayerImages(1, layersFixture.content[0].id))
+        .reply(HttpStatusCode.Ok, layerImagesFixture);
 
       const { payload } = await store.dispatch(getLayersForModel(1));
       expect(payload).toEqual({
@@ -55,6 +59,7 @@ describe('layers thunks', () => {
             rects: layerRectsFixture.content,
             ovals: layerOvalsFixture.content,
             lines: layerLinesFixture.content,
+            images: layerImagesFixture.content,
           },
         ],
         layersVisibility: {
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index c8ee4fd5..1fd5d6d4 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -18,6 +18,7 @@ import { layerRectSchema } from '@/models/layerRectSchema';
 import { pageableSchema } from '@/models/pageableSchema';
 import { layerOvalSchema } from '@/models/layerOvalSchema';
 import { layerLineSchema } from '@/models/layerLineSchema';
+import { layerImageSchema } from '@/models/layerImageSchema';
 
 export const getLayer = createAsyncThunk<
   Layer | null,
@@ -48,12 +49,14 @@ export const getLayersForModel = createAsyncThunk<
     }
     let layers = await Promise.all(
       data.content.map(async (layer: Layer) => {
-        const [textsResponse, rectsResponse, ovalsResponse, linesResponse] = await Promise.all([
-          axiosInstanceNewAPI.get(apiPath.getLayerTexts(modelId, layer.id)),
-          axiosInstanceNewAPI.get(apiPath.getLayerRects(modelId, layer.id)),
-          axiosInstanceNewAPI.get(apiPath.getLayerOvals(modelId, layer.id)),
-          axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)),
-        ]);
+        const [textsResponse, rectsResponse, ovalsResponse, linesResponse, imagesResponse] =
+          await Promise.all([
+            axiosInstanceNewAPI.get(apiPath.getLayerTexts(modelId, layer.id)),
+            axiosInstanceNewAPI.get(apiPath.getLayerRects(modelId, layer.id)),
+            axiosInstanceNewAPI.get(apiPath.getLayerOvals(modelId, layer.id)),
+            axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)),
+            axiosInstanceNewAPI.get(apiPath.getLayerImages(modelId, layer.id)),
+          ]);
 
         return {
           details: layer,
@@ -61,6 +64,7 @@ export const getLayersForModel = createAsyncThunk<
           rects: rectsResponse.data.content,
           ovals: ovalsResponse.data.content,
           lines: linesResponse.data.content,
+          images: imagesResponse.data.content,
         };
       }),
     );
@@ -69,7 +73,8 @@ export const getLayersForModel = createAsyncThunk<
         z.array(layerTextSchema).safeParse(layer.texts).success &&
         z.array(layerRectSchema).safeParse(layer.rects).success &&
         z.array(layerOvalSchema).safeParse(layer.ovals).success &&
-        z.array(layerLineSchema).safeParse(layer.lines).success
+        z.array(layerLineSchema).safeParse(layer.lines).success &&
+        z.array(layerImageSchema).safeParse(layer.images).success
       );
     });
     const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => {
diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts
index a5c1b30e..f2283106 100644
--- a/src/redux/layers/layers.types.ts
+++ b/src/redux/layers/layers.types.ts
@@ -1,5 +1,5 @@
 import { KeyedFetchDataState } from '@/types/fetchDataState';
-import { Layer, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models';
+import { Layer, LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models';
 
 export interface LayerStoreInterface {
   name: string;
@@ -22,6 +22,7 @@ export type LayerState = {
   rects: LayerRect[];
   ovals: LayerOval[];
   lines: LayerLine[];
+  images: LayerImage[];
 };
 
 export type LayerVisibilityState = {
diff --git a/src/types/models.ts b/src/types/models.ts
index a7f9735a..91a9a887 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -84,6 +84,7 @@ import { reactionProduct } from '@/models/reactionProduct';
 import { operatorSchema } from '@/models/operatorSchema';
 import { modificationResiduesSchema } from '@/models/modificationResiduesSchema';
 import { segmentSchema } from '@/models/segmentSchema';
+import { layerImageSchema } from '@/models/layerImageSchema';
 
 export type Project = z.infer<typeof projectSchema>;
 export type OverviewImageView = z.infer<typeof overviewImageView>;
@@ -102,6 +103,7 @@ export type LayerText = z.infer<typeof layerTextSchema>;
 export type LayerRect = z.infer<typeof layerRectSchema>;
 export type LayerOval = z.infer<typeof layerOvalSchema>;
 export type LayerLine = z.infer<typeof layerLineSchema>;
+export type LayerImage = z.infer<typeof layerImageSchema>;
 export type Arrow = z.infer<typeof arrowSchema>;
 const modelElementsSchema = pageableSchema(modelElementSchema);
 export type ModelElements = z.infer<typeof modelElementsSchema>;
-- 
GitLab


From 35249e07845833ac58cf1f6f131b5f736e65b1f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Thu, 19 Dec 2024 16:16:43 +0100
Subject: [PATCH 06/22] feat(vector-map): implement adding layer image objects

---
 next.config.js                                |   2 +-
 .../ImagePreview/ImagePreview.component.tsx   |  57 ++++++
 .../EditOverlayModal.component.test.tsx       |   7 +
 .../hooks/useEditOverlay.test.ts              |   5 +
 .../LayerImageGlyphSelector.component.tsx     |  82 +++++++++
 .../LayerImageGlyphSelector.styles.css        |  14 ++
 ...LayerImageObjectFactoryModal.component.tsx | 162 ++++++++++++++++++
 .../LayerImageObjectFactoryModal.styles.css   |  12 ++
 .../LayerImageObjectFactoryModal/index.ts     |   1 +
 .../FunctionalArea/Modal/Modal.component.tsx  |   6 +
 .../ModalLayout/ModalLayout.component.tsx     |   1 +
 src/components/Map/Map.component.tsx          |   8 +-
 .../MapActiveLayerSelector.component.tsx      |  53 ++++++
 .../useOlMapAdditionalLayers.ts               |  28 ++-
 .../utils/shapes/elements/Glyph.test.ts       |   5 -
 .../utils/shapes/elements/Glyph.ts            |  57 +++---
 .../utils/shapes/layer/Layer.ts               |   3 +
 .../shapes/layer/getDrawImageInteraction.ts   |  65 +++++++
 src/models/fixtures/glyphsFixture.ts          |  10 ++
 src/models/layerImageSchema.ts                |   2 +-
 src/redux/apiPath.ts                          |   4 +
 src/redux/glyphs/glyphs.constants.ts          |   1 +
 src/redux/glyphs/glyphs.mock.ts               |   8 +
 src/redux/glyphs/glyphs.reducers.test.ts      |  72 ++++++++
 src/redux/glyphs/glyphs.reducers.ts           |  16 ++
 src/redux/glyphs/glyphs.selectors.ts          |   6 +
 src/redux/glyphs/glyphs.slice.ts              |  15 ++
 src/redux/glyphs/glyphs.thunks.test.ts        |  38 ++++
 src/redux/glyphs/glyphs.thunks.ts             |  43 +++++
 src/redux/glyphs/glyphs.types.ts              |   4 +
 src/redux/layers/layers.mock.ts               |  14 +-
 src/redux/layers/layers.reducers.test.ts      |   8 +-
 src/redux/layers/layers.reducers.ts           |  12 ++
 src/redux/layers/layers.selectors.ts          |  25 +++
 src/redux/layers/layers.slice.ts              |   4 +-
 src/redux/layers/layers.thunks.test.ts        |   1 +
 src/redux/layers/layers.thunks.ts             |  48 +++++-
 src/redux/layers/layers.types.ts              |   1 +
 src/redux/modal/modal.constants.ts            |   1 +
 src/redux/modal/modal.mock.ts                 |   1 +
 src/redux/modal/modal.reducers.ts             |  10 ++
 src/redux/modal/modal.selector.ts             |   5 +
 src/redux/modal/modal.slice.ts                |   3 +
 src/redux/modal/modal.types.ts                |  10 ++
 src/redux/root/init.thunks.ts                 |   2 +
 src/redux/root/root.fixtures.ts               |   2 +
 src/redux/store.ts                            |   2 +
 src/shared/Input/Input.component.tsx          |  26 +--
 src/shared/Select/Select.component.tsx        |   6 +-
 src/types/modal.ts                            |   3 +-
 src/types/models.ts                           |   2 +
 51 files changed, 913 insertions(+), 60 deletions(-)
 create mode 100644 src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts
 create mode 100644 src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
 create mode 100644 src/models/fixtures/glyphsFixture.ts
 create mode 100644 src/redux/glyphs/glyphs.constants.ts
 create mode 100644 src/redux/glyphs/glyphs.mock.ts
 create mode 100644 src/redux/glyphs/glyphs.reducers.test.ts
 create mode 100644 src/redux/glyphs/glyphs.reducers.ts
 create mode 100644 src/redux/glyphs/glyphs.selectors.ts
 create mode 100644 src/redux/glyphs/glyphs.slice.ts
 create mode 100644 src/redux/glyphs/glyphs.thunks.test.ts
 create mode 100644 src/redux/glyphs/glyphs.thunks.ts
 create mode 100644 src/redux/glyphs/glyphs.types.ts

diff --git a/next.config.js b/next.config.js
index 46540574..c882ad1b 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,6 @@
 /** @type {import("next").NextConfig} */
 const nextConfig = {
-  reactStrictMode: true,
+  reactStrictMode: false,
   basePath: process.env.APP_PREFIX ? process.env.APP_PREFIX + '/index.html' : '',
   assetPrefix: process.env.APP_PREFIX ? process.env.APP_PREFIX : '',
   productionBrowserSourceMaps: true,
diff --git a/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx b/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx
new file mode 100644
index 00000000..d5b13b0a
--- /dev/null
+++ b/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx
@@ -0,0 +1,57 @@
+import React, { useEffect, useMemo, useState } from 'react';
+
+interface FileInterface {
+  url: string;
+}
+
+interface ImagePreviewProps {
+  imageFile?: File | FileInterface | null;
+}
+
+const ImagePreview: React.FC<ImagePreviewProps> = ({ imageFile }) => {
+  const [imageSrc, setImageSrc] = useState<string | null>(null);
+
+  const previewImage = (file: File): void => {
+    const reader = new FileReader();
+    reader.onload = (event): void => {
+      if (event.target?.result && typeof event.target.result === 'string') {
+        setImageSrc(event.target.result);
+      }
+    };
+    reader.readAsDataURL(file);
+  };
+
+  const setImageFile = useMemo(() => {
+    return (): void => {
+      if (imageFile) {
+        if (imageFile instanceof File) {
+          previewImage(imageFile);
+        } else if ('url' in imageFile) {
+          setImageSrc(imageFile.url);
+        }
+      } else {
+        setImageSrc(null);
+      }
+    };
+  }, [imageFile]);
+
+  useEffect(() => {
+    setImageFile();
+  }, [imageFile, setImageFile]);
+
+  return (
+    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
+      {imageSrc ? (
+        <img
+          src={imageSrc}
+          alt="Preview"
+          style={{ maxHeight: '350px', borderRadius: '8px', objectFit: 'cover' }}
+        />
+      ) : (
+        <div>No Data Available</div>
+      )}
+    </div>
+  );
+};
+
+export default ImagePreview;
diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx
index f8072beb..f9c7b302 100644
--- a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx
+++ b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx
@@ -48,6 +48,7 @@ describe('EditOverlayModal - component', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
@@ -67,6 +68,7 @@ describe('EditOverlayModal - component', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
@@ -98,6 +100,7 @@ describe('EditOverlayModal - component', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -134,6 +137,7 @@ describe('EditOverlayModal - component', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -171,6 +175,7 @@ describe('EditOverlayModal - component', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -207,6 +212,7 @@ describe('EditOverlayModal - component', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
       overlays: OVERLAYS_INITIAL_STATE_MOCK,
     });
@@ -237,6 +243,7 @@ describe('EditOverlayModal - component', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts
index 16f43071..0079d31f 100644
--- a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts
+++ b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts
@@ -25,6 +25,7 @@ describe('useEditOverlay', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
@@ -62,6 +63,7 @@ describe('useEditOverlay', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
@@ -102,6 +104,7 @@ describe('useEditOverlay', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
@@ -138,6 +141,7 @@ describe('useEditOverlay', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
@@ -175,6 +179,7 @@ describe('useEditOverlay', () => {
         overviewImagesState: {},
         errorReportState: {},
         layerFactoryState: { id: undefined },
+        layerImageObjectFactoryState: undefined,
       },
     });
 
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx
new file mode 100644
index 00000000..f5ae36b6
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx
@@ -0,0 +1,82 @@
+import React, { ReactElement, useEffect, useState } from 'react';
+import Autosuggest from 'react-autosuggest';
+import { Glyph } from '@/types/models';
+import './LayerImageGlyphSelector.styles.css';
+
+interface LayerImageGlyphSelectorProps {
+  glyphs: Glyph[];
+  selectedGlyph: number | null;
+  onGlyphSelect: (glyphId: number) => void;
+}
+
+const LayerImageGlyphSelector: React.FC<LayerImageGlyphSelectorProps> = ({
+  glyphs,
+  selectedGlyph,
+  onGlyphSelect,
+}) => {
+  const [searchValue, setSearchValue] = useState('');
+  const [suggestions, setSuggestions] = useState<Glyph[]>([]);
+
+  useEffect(() => {
+    if (selectedGlyph) {
+      setSearchValue(String(selectedGlyph));
+    } else {
+      setSearchValue('');
+    }
+  }, [selectedGlyph]);
+
+  const getSuggestions = (inputValue: string): Glyph[] => {
+    if (!inputValue) {
+      return glyphs;
+    }
+    const input = inputValue.trim().toLowerCase();
+    return glyphs.filter(glyph => String(glyph.file).toLowerCase().includes(input));
+  };
+
+  const getSuggestionValue = (suggestion: Glyph): string => String(suggestion.file);
+
+  const renderSuggestion = (suggestion: Glyph): ReactElement => (
+    <div className="cursor-pointer p-2">{suggestion.file}</div>
+  );
+
+  const onChange = (event: React.FormEvent, { newValue }: { newValue: string }): void => {
+    setSearchValue(newValue);
+  };
+
+  const onSuggestionsFetchRequested = ({ value }: { value: string }): void => {
+    setSuggestions(getSuggestions(value));
+  };
+
+  const onSuggestionsClearRequested = (): void => {
+    setSuggestions([]);
+  };
+
+  const onSuggestionSelected = (
+    event: React.FormEvent,
+    { suggestion }: { suggestion: Glyph },
+  ): void => {
+    onGlyphSelect(suggestion.id);
+    setSearchValue(String(suggestion.file));
+  };
+
+  const inputProps = {
+    placeholder: 'Select glyph...',
+    value: searchValue,
+    onChange,
+  };
+
+  return (
+    <Autosuggest
+      suggestions={suggestions}
+      onSuggestionsFetchRequested={onSuggestionsFetchRequested}
+      onSuggestionsClearRequested={onSuggestionsClearRequested}
+      shouldRenderSuggestions={() => true}
+      getSuggestionValue={getSuggestionValue}
+      renderSuggestion={renderSuggestion}
+      onSuggestionSelected={onSuggestionSelected}
+      inputProps={inputProps}
+    />
+  );
+};
+
+export default LayerImageGlyphSelector;
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css
new file mode 100644
index 00000000..f1d46937
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css
@@ -0,0 +1,14 @@
+.react-autosuggest__suggestions-container {
+  position: absolute;
+  z-index: 1000;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.react-autosuggest__input {
+  width: 100%;
+  height: 40px;
+  padding: 10px;
+  border: 1px solid #ccc;
+  border-radius: 4px 0 0 4px;
+}
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
new file mode 100644
index 00000000..10539f95
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
@@ -0,0 +1,162 @@
+/* eslint-disable no-magic-numbers */
+import React, { useState, useRef } from 'react';
+import { glyphsDataSelector } from '@/redux/glyphs/glyphs.selectors';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { layerImageObjectFactoryStateSelector } from '@/redux/modal/modal.selector';
+import { Button } from '@/shared/Button';
+import { BASE_NEW_API_URL } from '@/constants';
+import { apiPath } from '@/redux/apiPath';
+import { Input } from '@/shared/Input';
+import Image from 'next/image';
+import LayerImageGlyphSelector from '@/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component';
+import { Glyph } from '@/types/models';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { currentModelIdSelector } from '@/redux/models/models.selectors';
+import { highestZIndexSelector, layersActiveLayerSelector } from '@/redux/layers/layers.selectors';
+import { addLayerImageObject, getLayersForModel } from '@/redux/layers/layers.thunks';
+import { addGlyph } from '@/redux/glyphs/glyphs.thunks';
+import { SerializedError } from '@reduxjs/toolkit';
+import { showToast } from '@/utils/showToast';
+import { closeModal } from '@/redux/modal/modal.slice';
+import { LoadingIndicator } from '@/shared/LoadingIndicator';
+import './LayerImageObjectFactoryModal.styles.css';
+
+export const LayerImageObjectFactoryModal: React.FC = () => {
+  const glyphs: Glyph[] = useAppSelector(glyphsDataSelector);
+  const currentModelId = useAppSelector(currentModelIdSelector);
+  const activeLayer = useAppSelector(layersActiveLayerSelector);
+  const layerImageObjectFactoryState = useAppSelector(layerImageObjectFactoryStateSelector);
+  const dispatch = useAppDispatch();
+  const fileInputRef = useRef<HTMLInputElement>(null);
+  const highestZIndex = useAppSelector(highestZIndexSelector);
+
+  const [selectedGlyph, setSelectedGlyph] = useState<number | null>(null);
+  const [file, setFile] = useState<File | null>(null);
+  const [isSending, setIsSending] = useState<boolean>(false);
+  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
+
+  const handleGlyphChange = (glyphId: number | null): void => {
+    setSelectedGlyph(glyphId);
+    if (!glyphId) {
+      return;
+    }
+    setFile(null);
+    setPreviewUrl(`${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyphId)}`);
+
+    if (fileInputRef.current) {
+      fileInputRef.current.value = '';
+    }
+  };
+
+  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
+    const uploadedFile = e.target.files?.[0] || null;
+
+    setFile(uploadedFile);
+    if (!uploadedFile) {
+      return;
+    }
+
+    setSelectedGlyph(null);
+    if (uploadedFile) {
+      const url = URL.createObjectURL(uploadedFile);
+      setPreviewUrl(url);
+    } else {
+      setPreviewUrl(null);
+    }
+  };
+
+  const handleSubmit = async (): Promise<void> => {
+    if (!layerImageObjectFactoryState || !activeLayer) {
+      return;
+    }
+    setIsSending(true);
+    try {
+      let glyphId = selectedGlyph;
+      if (file) {
+        const data = await dispatch(addGlyph(file)).unwrap();
+        if (!data) {
+          return;
+        }
+        glyphId = data.id;
+      }
+      await dispatch(
+        addLayerImageObject({
+          modelId: currentModelId,
+          layerId: activeLayer,
+          x: layerImageObjectFactoryState.x,
+          y: layerImageObjectFactoryState.y,
+          z: highestZIndex + 1,
+          width: layerImageObjectFactoryState.width,
+          height: layerImageObjectFactoryState.height,
+          glyph: glyphId,
+        }),
+      ).unwrap();
+      showToast({
+        type: 'success',
+        message: 'A new image object has been successfully added',
+      });
+      dispatch(closeModal());
+      dispatch(getLayersForModel(currentModelId));
+    } catch (error) {
+      const typedError = error as SerializedError;
+      showToast({
+        type: 'error',
+        message: typedError.message || 'An error occurred while adding a new image object',
+      });
+    } finally {
+      setIsSending(false);
+    }
+  };
+
+  return (
+    <div className="relative w-[800px] border border-t-[#E1E0E6] bg-white p-[24px]">
+      {isSending && (
+        <div className="c-layer-image-object-factory-loader">
+          <LoadingIndicator width={44} height={44} />
+        </div>
+      )}
+      <div className="grid grid-cols-2 gap-2">
+        <div className="mb-4 flex flex-col gap-2">
+          <span>Glyph:</span>
+          <LayerImageGlyphSelector
+            selectedGlyph={selectedGlyph}
+            glyphs={glyphs}
+            onGlyphSelect={handleGlyphChange}
+          />
+        </div>
+        <div className="mb-4 flex flex-col gap-2">
+          <span>File:</span>
+          <Input
+            ref={fileInputRef}
+            type="file"
+            accept="image/*"
+            onChange={handleFileChange}
+            className="w-full border border-[#ccc] bg-white p-2"
+          />
+        </div>
+      </div>
+
+      <div className="relative mb-4 flex h-[350px] w-full items-center justify-center overflow-hidden rounded border">
+        {previewUrl ? (
+          <Image
+            src={previewUrl}
+            alt="image preview"
+            layout="fill"
+            objectFit="contain"
+            className="rounded"
+          />
+        ) : (
+          <div className="text-gray-500">No Image</div>
+        )}
+      </div>
+
+      <Button
+        type="button"
+        onClick={handleSubmit}
+        className="w-full justify-center text-base font-medium"
+      >
+        Submit
+      </Button>
+    </div>
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css
new file mode 100644
index 00000000..db49e443
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css
@@ -0,0 +1,12 @@
+.c-layer-image-object-factory-loader {
+  width: 100%;
+  height: 100%;
+  margin-left: -24px;
+  margin-top: -24px;
+  background: #f9f9f980;
+  z-index: 1;
+  position: absolute;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts
new file mode 100644
index 00000000..11947806
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts
@@ -0,0 +1 @@
+export { LayerImageObjectFactoryModal } from './LayerImageObjectFactoryModal.component';
diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx
index 8419791d..feec78d9 100644
--- a/src/components/FunctionalArea/Modal/Modal.component.tsx
+++ b/src/components/FunctionalArea/Modal/Modal.component.tsx
@@ -5,6 +5,7 @@ import { AccessDeniedModal } from '@/components/FunctionalArea/Modal/AccessDenie
 import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component';
 import { LicenseModal } from '@/components/FunctionalArea/Modal/LicenseModal';
 import { ToSModal } from '@/components/FunctionalArea/Modal/ToSModal/ToSModal.component';
+import { LayerImageObjectFactoryModal } from '@/components/FunctionalArea/Modal/LayerImageObjectFactoryModal';
 import { EditOverlayModal } from './EditOverlayModal';
 import { LoginModal } from './LoginModal';
 import { ErrorReportModal } from './ErrorReportModal';
@@ -85,6 +86,11 @@ export const Modal = (): React.ReactNode => {
           <LayerFactoryModal />
         </ModalLayout>
       )}
+      {isOpen && modalName === 'layer-image-object-factory' && (
+        <ModalLayout>
+          <LayerImageObjectFactoryModal />
+        </ModalLayout>
+      )}
     </>
   );
 };
diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
index 493f2bc7..64816f0b 100644
--- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
+++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
@@ -34,6 +34,7 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
             modalName === 'add-comment' && 'h-auto w-[400px]',
             modalName === 'error-report' && 'h-auto w-[800px]',
             modalName === 'layer-factory' && 'h-auto w-[400px]',
+            modalName === 'layer-image-object-factory' && 'h-auto w-[800px]',
             ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]',
           )}
         >
diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx
index 02ee36d8..65392d69 100644
--- a/src/components/Map/Map.component.tsx
+++ b/src/components/Map/Map.component.tsx
@@ -4,6 +4,7 @@ import { Legend } from '@/components/Map/Legend';
 import { MapViewer } from '@/components/Map/MapViewer';
 import { MapLoader } from '@/components/Map/MapLoader/MapLoader.component';
 import { MapVectorBackgroundSelector } from '@/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component';
+import { MapActiveLayerSelector } from '@/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { vectorRenderingSelector } from '@/redux/models/models.selectors';
 import { MapAdditionalLogos } from '@/components/Map/MapAdditionalLogos';
@@ -20,7 +21,12 @@ export const Map = (): JSX.Element => {
     >
       <MapViewer />
       {!vectorRendering && <MapAdditionalOptions />}
-      {vectorRendering && <MapVectorBackgroundSelector />}
+      {vectorRendering && (
+        <>
+          <MapVectorBackgroundSelector />
+          <MapActiveLayerSelector />
+        </>
+      )}
       <Drawer />
       <PluginsDrawer />
       <Legend />
diff --git a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
new file mode 100644
index 00000000..d4fd65a1
--- /dev/null
+++ b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
@@ -0,0 +1,53 @@
+/* eslint-disable no-magic-numbers */
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { twMerge } from 'tailwind-merge';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { Select } from '@/shared/Select';
+import {
+  layersActiveLayerSelector,
+  layersForCurrentModelSelector,
+  layersVisibilityForCurrentModelSelector,
+} from '@/redux/layers/layers.selectors';
+import { useEffect, useMemo } from 'react';
+import { setActiveLayer } from '@/redux/layers/layers.slice';
+import { currentModelIdSelector } from '@/redux/models/models.selectors';
+
+export const MapActiveLayerSelector = (): JSX.Element => {
+  const dispatch = useAppDispatch();
+  const layers = useAppSelector(layersForCurrentModelSelector);
+  const layersVisibility = useAppSelector(layersVisibilityForCurrentModelSelector);
+  const currentModelId = useAppSelector(currentModelIdSelector);
+  const activeLayer = useAppSelector(layersActiveLayerSelector);
+
+  const handleChange = (activeLayerId: number): void => {
+    dispatch(setActiveLayer({ modelId: currentModelId, layerId: activeLayerId }));
+  };
+  const options: Array<{ id: number; name: string }> = useMemo(() => {
+    return layers
+      .filter(layer => layersVisibility[layer.details.id])
+      .map(layer => {
+        return {
+          id: layer.details.id,
+          name: layer.details.name,
+        };
+      });
+  }, [layers, layersVisibility]);
+
+  useEffect(() => {
+    const selectedOption = options.find(option => option.id === activeLayer) || null;
+    if (selectedOption) {
+      return;
+    }
+    if (options.length === 0 && currentModelId) {
+      dispatch(setActiveLayer({ modelId: currentModelId, layerId: null }));
+    } else {
+      dispatch(setActiveLayer({ modelId: currentModelId, layerId: options[0].id }));
+    }
+  }, [activeLayer, currentModelId, dispatch, options]);
+
+  return (
+    <div className={twMerge('absolute right-[140px] top-[calc(64px+40px+24px)] z-10 flex')}>
+      <Select options={options} selectedId={activeLayer} onChange={handleChange} width={140} />
+    </div>
+  );
+};
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
index 6b841f53..31c1a087 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
@@ -4,10 +4,11 @@ import VectorLayer from 'ol/layer/Vector';
 import VectorSource from 'ol/source/Vector';
 import { useEffect, useMemo } from 'react';
 import { useSelector } from 'react-redux';
-import { currentModelIdSelector } from '@/redux/models/models.selectors';
+import { currentModelIdSelector, vectorRenderingSelector } from '@/redux/models/models.selectors';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { getLayersForModel } from '@/redux/layers/layers.thunks';
 import {
+  layersActiveLayerSelector,
   layersForCurrentModelSelector,
   layersLoadingSelector,
   layersVisibilityForCurrentModelSelector,
@@ -19,6 +20,8 @@ import Polygon from 'ol/geom/Polygon';
 import Layer from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer';
 import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { mapDataSizeSelector } from '@/redux/map/map.selectors';
+import getDrawImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction';
 
 export const useOlMapAdditionalLayers = (
   mapInstance: MapInstance,
@@ -28,16 +31,26 @@ export const useOlMapAdditionalLayers = (
   >
 > => {
   const dispatch = useAppDispatch();
+  const mapSize = useSelector(mapDataSizeSelector);
   const currentModelId = useSelector(currentModelIdSelector);
 
   const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector);
   const layersLoading = useAppSelector(layersLoadingSelector);
   const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector);
+  const activeLayer = useAppSelector(layersActiveLayerSelector);
+  const vectorRendering = useAppSelector(vectorRenderingSelector);
 
   const lineTypes = useSelector(lineTypesSelector);
   const arrowTypes = useSelector(arrowTypesSelector);
   const pointToProjection = usePointToProjection();
 
+  const drawImageInteraction = useMemo(() => {
+    if (!mapSize || !dispatch) {
+      return null;
+    }
+    return getDrawImageInteraction(mapSize, dispatch);
+  }, [mapSize, dispatch]);
+
   useEffect(() => {
     if (!currentModelId) {
       return;
@@ -64,7 +77,7 @@ export const useOlMapAdditionalLayers = (
       });
       return additionalLayer.vectorLayer;
     });
-  }, [arrowTypes, lineTypes, mapInstance, layersForCurrentModel, pointToProjection]);
+  }, [layersForCurrentModel, lineTypes, arrowTypes, mapInstance, pointToProjection]);
 
   useEffect(() => {
     vectorLayers.forEach(layer => {
@@ -75,5 +88,16 @@ export const useOlMapAdditionalLayers = (
     });
   }, [layersVisibilityForCurrentModel, vectorLayers]);
 
+  useEffect(() => {
+    if (!drawImageInteraction) {
+      return;
+    }
+    mapInstance?.removeInteraction(drawImageInteraction);
+    if (!activeLayer || !vectorRendering) {
+      return;
+    }
+    mapInstance?.addInteraction(drawImageInteraction);
+  }, [activeLayer, currentModelId, drawImageInteraction, mapInstance, vectorRendering]);
+
   return vectorLayers;
 };
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
index f2fc4d4d..6ac8c5d2 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
@@ -59,13 +59,8 @@ describe('Glyph', () => {
   });
 
   it('should scale image based on map resolution', () => {
-    const getImageScale = glyph.feature.get('getImageScale');
     const getAnchorAndCoords = glyph.feature.get('getAnchorAndCoords');
     if (mapInstance) {
-      const resolution = mapInstance
-        .getView()
-        .getResolutionForZoom(mapInstance.getView().getMaxZoom());
-      expect(getImageScale(resolution)).toBe(1);
       expect(getAnchorAndCoords()).toEqual({ anchor: [0, 0], coords: [0, 0] });
     }
   });
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
index 41cc21a6..ff5a7887 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
@@ -1,7 +1,7 @@
 /* eslint-disable no-magic-numbers */
 import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
 import { Feature } from 'ol';
-import Style from 'ol/style/Style';
+import { Style, Text } from 'ol/style';
 import Icon from 'ol/style/Icon';
 import { FeatureLike } from 'ol/Feature';
 import { MapInstance } from '@/types/map';
@@ -13,10 +13,12 @@ import { Coordinate } from 'ol/coordinate';
 import { FEATURE_TYPE } from '@/constants/features';
 import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle';
 import { WHITE_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
+import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill';
+import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle';
 
 export type GlyphProps = {
   elementId: number;
-  glyphId: number;
+  glyphId: number | null;
   x: number;
   y: number;
   width: number;
@@ -31,6 +33,8 @@ export default class Glyph {
 
   style: Style = new Style({});
 
+  noGlyphStyle: Style;
+
   imageScale: number = 1;
 
   polygonStyle: Style;
@@ -49,6 +53,8 @@ export default class Glyph {
 
   pixelRatio: number = 1;
 
+  minResolution: number;
+
   pointToProjection: UsePointToProjectionResult;
 
   constructor({
@@ -71,10 +77,10 @@ export default class Glyph {
     const point2 = this.pointToProjection({ x: this.width, y: this.height });
     this.widthOnMap = Math.abs(point2[0] - point1[0]);
     this.heightOnMap = Math.abs(point2[1] - point1[1]);
-    const minResolution = mapInstance?.getView().getMinResolution();
-    if (minResolution) {
-      this.pixelRatio = this.widthOnMap / minResolution / this.width;
-    }
+
+    const maxZoom = mapInstance?.getView().get('originalMaxZoom');
+    this.minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom) || 1;
+    this.pixelRatio = this.widthOnMap / this.minResolution / this.width;
     const polygon = new Polygon([
       [
         pointToProjection({ x, y }),
@@ -84,23 +90,33 @@ export default class Glyph {
         pointToProjection({ x, y }),
       ],
     ]);
+
     this.polygonStyle = getStyle({
       geometry: polygon,
       zIndex,
       borderColor: { ...WHITE_COLOR, alpha: 0 },
       fillColor: { ...WHITE_COLOR, alpha: 0 },
     });
+
+    this.noGlyphStyle = getStyle({
+      geometry: polygon,
+      zIndex,
+      fillColor: '#E7E7E7',
+    });
+    this.noGlyphStyle.setText(
+      new Text({
+        text: 'No image',
+        font: '12pt Arial',
+        fill: getFill({ color: '#000' }),
+        overflow: true,
+      }),
+    );
+
     this.feature = new Feature({
       geometry: polygon,
       id: elementId,
       type: FEATURE_TYPE.GLYPH,
       zIndex,
-      getImageScale: (resolution: number): number => {
-        if (mapInstance) {
-          return mapInstance.getView().getMinResolution() / resolution;
-        }
-        return 1;
-      },
       getAnchorAndCoords: (): { anchor: Array<number>; coords: Coordinate } => {
         const center = mapInstance?.getView().getCenter();
         let anchorX = 0;
@@ -115,6 +131,10 @@ export default class Glyph {
       },
     });
 
+    this.feature.setStyle(this.getStyle.bind(this));
+    if (!glyphId) {
+      return;
+    }
     const img = new Image();
     img.onload = (): void => {
       const imageWidth = img.naturalWidth;
@@ -128,30 +148,27 @@ export default class Glyph {
         }),
         zIndex,
       });
-      this.feature.setStyle(this.getStyle.bind(this));
     };
     img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyphId)}`;
   }
 
   protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void {
-    const getImageScale = feature.get('getImageScale');
+    const scale = this.minResolution / resolution;
     const getAnchorAndCoords = feature.get('getAnchorAndCoords');
-    let imageScale = 1;
     let anchor = [0, 0];
     let coords = this.pointToProjection({ x: this.x, y: this.y });
-    if (getImageScale instanceof Function) {
-      imageScale = getImageScale(resolution);
-    }
+
     if (getAnchorAndCoords instanceof Function) {
       const anchorAndCoords = getAnchorAndCoords();
       anchor = anchorAndCoords.anchor;
       coords = anchorAndCoords.coords;
     }
     if (this.style.getImage()) {
-      this.style.getImage()?.setScale(imageScale * this.pixelRatio * this.imageScale);
+      this.style.getImage()?.setScale(scale * this.pixelRatio * this.imageScale);
       (this.style.getImage() as Icon).setAnchor(anchor);
       this.style.setGeometry(new Point(coords));
+      return [this.style, this.polygonStyle];
     }
-    return [this.style, this.polygonStyle];
+    return getScaledElementStyle(this.noGlyphStyle, undefined, scale);
   }
 }
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
index ef12c427..50a2b285 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
@@ -123,7 +123,10 @@ export default class Layer {
     this.vectorLayer = new VectorLayer({
       source: this.vectorSource,
       visible,
+      updateWhileAnimating: true,
+      updateWhileInteracting: true,
     });
+
     this.vectorLayer.set('id', layerId);
   }
 
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
new file mode 100644
index 00000000..a4f044ee
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
@@ -0,0 +1,65 @@
+/* eslint-disable no-magic-numbers */
+import Draw from 'ol/interaction/Draw';
+import SimpleGeometry from 'ol/geom/SimpleGeometry';
+import Polygon from 'ol/geom/Polygon';
+import { toLonLat } from 'ol/proj';
+import { latLngToPoint } from '@/utils/map/latLngToPoint';
+import { MapSize } from '@/redux/map/map.types';
+import { AppDispatch } from '@/redux/store';
+import { Coordinate } from 'ol/coordinate';
+import { openLayerImageObjectFactoryModal } from '@/redux/modal/modal.slice';
+
+export default function getDrawImageInteraction(mapSize: MapSize, dispatch: AppDispatch): Draw {
+  const drawImageInteraction = new Draw({
+    type: 'Circle',
+    geometryFunction: (coordinates, geometry): SimpleGeometry => {
+      const newGeometry = geometry || new Polygon([]);
+      if (!Array.isArray(coordinates) || coordinates.length < 2) {
+        return geometry;
+      }
+      const start = coordinates[0] as Coordinate;
+      const end = coordinates[1] as Coordinate;
+
+      const minX = Math.min(start[0], end[0]);
+      const minY = Math.min(start[1], end[1]);
+      const maxX = Math.max(start[0], end[0]);
+      const maxY = Math.max(start[1], end[1]);
+
+      const coords: Array<Coordinate> = [
+        [minX, minY],
+        [maxX, minY],
+        [maxX, maxY],
+        [minX, maxY],
+        [minX, minY],
+      ];
+
+      newGeometry.setCoordinates([coords]);
+
+      return newGeometry;
+    },
+  });
+
+  drawImageInteraction.on('drawend', event => {
+    const geometry = event.feature.getGeometry() as Polygon;
+    const extent = geometry.getExtent();
+
+    const [startLng, startLat] = toLonLat([extent[0], extent[3]]);
+    const startPoint = latLngToPoint([startLat, startLng], mapSize);
+    const [endLng, endLat] = toLonLat([extent[2], extent[1]]);
+    const endPoint = latLngToPoint([endLat, endLng], mapSize);
+
+    const width = Math.abs(endPoint.x - startPoint.x);
+    const height = Math.abs(endPoint.y - startPoint.y);
+
+    dispatch(
+      openLayerImageObjectFactoryModal({
+        x: startPoint.x,
+        y: startPoint.y,
+        width,
+        height,
+      }),
+    );
+  });
+
+  return drawImageInteraction;
+}
diff --git a/src/models/fixtures/glyphsFixture.ts b/src/models/fixtures/glyphsFixture.ts
new file mode 100644
index 00000000..489f5015
--- /dev/null
+++ b/src/models/fixtures/glyphsFixture.ts
@@ -0,0 +1,10 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { pageableSchema } from '@/models/pageableSchema';
+import { glyphSchema } from '@/models/glyphSchema';
+
+export const glyphsFixture = createFixture(pageableSchema(glyphSchema), {
+  seed: ZOD_SEED,
+  array: { min: 3, max: 3 },
+});
diff --git a/src/models/layerImageSchema.ts b/src/models/layerImageSchema.ts
index 61a6df2d..8547b313 100644
--- a/src/models/layerImageSchema.ts
+++ b/src/models/layerImageSchema.ts
@@ -7,5 +7,5 @@ export const layerImageSchema = z.object({
   z: z.number(),
   width: z.number(),
   height: z.number(),
-  glyph: z.number(),
+  glyph: z.number().nullable(),
 });
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index cd1b447c..1def76d9 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -65,10 +65,14 @@ export const apiPath = {
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
   removeLayer: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
+  addLayerImageObject: (modelId: number, layerId: number): string =>
+    `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/`,
   getLayer: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
   getGlyphImage: (glyphId: number): string =>
     `projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`,
+  getGlyphs: (): string => `projects/${PROJECT_ID}/glyphs/`,
+  addGlyph: (): string => `projects/${PROJECT_ID}/glyphs/`,
   getNewReactionsForModel: (modelId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/?size=10000`,
   getNewReaction: (modelId: number, reactionId: number): string =>
diff --git a/src/redux/glyphs/glyphs.constants.ts b/src/redux/glyphs/glyphs.constants.ts
new file mode 100644
index 00000000..e70a9a58
--- /dev/null
+++ b/src/redux/glyphs/glyphs.constants.ts
@@ -0,0 +1 @@
+export const GLYPHS_FETCHING_ERROR_PREFIX = 'Failed to fetch glyphs';
diff --git a/src/redux/glyphs/glyphs.mock.ts b/src/redux/glyphs/glyphs.mock.ts
new file mode 100644
index 00000000..4e04e5d3
--- /dev/null
+++ b/src/redux/glyphs/glyphs.mock.ts
@@ -0,0 +1,8 @@
+import { DEFAULT_ERROR } from '@/constants/errors';
+import { GlyphsState } from '@/redux/glyphs/glyphs.types';
+
+export const GLYPHS_STATE_INITIAL_MOCK: GlyphsState = {
+  data: [],
+  loading: 'idle',
+  error: DEFAULT_ERROR,
+};
diff --git a/src/redux/glyphs/glyphs.reducers.test.ts b/src/redux/glyphs/glyphs.reducers.test.ts
new file mode 100644
index 00000000..6a4264b5
--- /dev/null
+++ b/src/redux/glyphs/glyphs.reducers.test.ts
@@ -0,0 +1,72 @@
+import { apiPath } from '@/redux/apiPath';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { unwrapResult } from '@reduxjs/toolkit';
+import { GLYPHS_STATE_INITIAL_MOCK } from '@/redux/glyphs/glyphs.mock';
+import { GlyphsState } from '@/redux/glyphs/glyphs.types';
+import { getGlyphs } from '@/redux/glyphs/glyphs.thunks';
+import { glyphsFixture } from '@/models/fixtures/glyphsFixture';
+import glyphsReducer from './glyphs.slice';
+
+const mockedAxiosClient = mockNetworkNewAPIResponse();
+
+const INITIAL_STATE: GlyphsState = GLYPHS_STATE_INITIAL_MOCK;
+
+describe('glyphs reducer', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<GlyphsState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('glyphs', glyphsReducer);
+  });
+
+  it('should match initial state', () => {
+    const action = { type: 'unknown' };
+
+    expect(glyphsReducer(undefined, action)).toEqual(INITIAL_STATE);
+  });
+
+  it('should update store after successful getGlyphs query', async () => {
+    mockedAxiosClient.onGet(apiPath.getGlyphs()).reply(HttpStatusCode.Ok, glyphsFixture);
+
+    const { type } = await store.dispatch(getGlyphs());
+    const { data, loading, error } = store.getState().glyphs;
+    expect(type).toBe('getGlyphs/fulfilled');
+    expect(loading).toEqual('succeeded');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual(glyphsFixture.content);
+  });
+
+  it('should update store after failed getGlyphs query', async () => {
+    mockedAxiosClient.onGet(apiPath.getGlyphs()).reply(HttpStatusCode.NotFound, []);
+
+    const action = await store.dispatch(getGlyphs());
+    const { data, loading, error } = store.getState().glyphs;
+
+    expect(action.type).toBe('getGlyphs/rejected');
+    expect(() => unwrapResult(action)).toThrow(
+      "Failed to fetch glyphs: The page you're looking for doesn't exist. Please verify the URL and try again.",
+    );
+    expect(loading).toEqual('failed');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual([]);
+  });
+
+  it('should update store on loading getGlyphs query', async () => {
+    mockedAxiosClient.onGet(apiPath.getGlyphs()).reply(HttpStatusCode.Ok, glyphsFixture);
+
+    const glyphsPromise = store.dispatch(getGlyphs());
+
+    const { data, loading } = store.getState().glyphs;
+    expect(data).toEqual([]);
+    expect(loading).toEqual('pending');
+
+    glyphsPromise.then(() => {
+      const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().glyphs;
+      expect(dataPromiseFulfilled).toEqual(glyphsFixture.content);
+      expect(promiseFulfilled).toEqual('succeeded');
+    });
+  });
+});
diff --git a/src/redux/glyphs/glyphs.reducers.ts b/src/redux/glyphs/glyphs.reducers.ts
new file mode 100644
index 00000000..30db8147
--- /dev/null
+++ b/src/redux/glyphs/glyphs.reducers.ts
@@ -0,0 +1,16 @@
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import { getGlyphs } from '@/redux/glyphs/glyphs.thunks';
+import { GlyphsState } from '@/redux/glyphs/glyphs.types';
+
+export const getGlyphsReducer = (builder: ActionReducerMapBuilder<GlyphsState>): void => {
+  builder.addCase(getGlyphs.pending, state => {
+    state.loading = 'pending';
+  });
+  builder.addCase(getGlyphs.fulfilled, (state, action) => {
+    state.data = action.payload || {};
+    state.loading = 'succeeded';
+  });
+  builder.addCase(getGlyphs.rejected, state => {
+    state.loading = 'failed';
+  });
+};
diff --git a/src/redux/glyphs/glyphs.selectors.ts b/src/redux/glyphs/glyphs.selectors.ts
new file mode 100644
index 00000000..6f99b412
--- /dev/null
+++ b/src/redux/glyphs/glyphs.selectors.ts
@@ -0,0 +1,6 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { rootSelector } from '@/redux/root/root.selectors';
+
+export const glyphsSelector = createSelector(rootSelector, state => state.glyphs);
+
+export const glyphsDataSelector = createSelector(glyphsSelector, state => state.data);
diff --git a/src/redux/glyphs/glyphs.slice.ts b/src/redux/glyphs/glyphs.slice.ts
new file mode 100644
index 00000000..ff81f3e4
--- /dev/null
+++ b/src/redux/glyphs/glyphs.slice.ts
@@ -0,0 +1,15 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+import { GLYPHS_STATE_INITIAL_MOCK } from '@/redux/glyphs/glyphs.mock';
+import { getGlyphsReducer } from '@/redux/glyphs/glyphs.reducers';
+
+export const glyphsSlice = createSlice({
+  name: 'glyphs',
+  initialState: GLYPHS_STATE_INITIAL_MOCK,
+  reducers: {},
+  extraReducers: builder => {
+    getGlyphsReducer(builder);
+  },
+});
+
+export default glyphsSlice.reducer;
diff --git a/src/redux/glyphs/glyphs.thunks.test.ts b/src/redux/glyphs/glyphs.thunks.test.ts
new file mode 100644
index 00000000..abd3a86d
--- /dev/null
+++ b/src/redux/glyphs/glyphs.thunks.test.ts
@@ -0,0 +1,38 @@
+import { apiPath } from '@/redux/apiPath';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { GlyphsState } from '@/redux/glyphs/glyphs.types';
+import { getGlyphs } from '@/redux/glyphs/glyphs.thunks';
+import { glyphsFixture } from '@/models/fixtures/glyphsFixture';
+import glyphsReducer from './glyphs.slice';
+
+const mockedAxiosClient = mockNetworkNewAPIResponse();
+
+describe('glyphs thunks', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<GlyphsState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('glyphs', glyphsReducer);
+  });
+
+  describe('getGlyphs', () => {
+    it('should return data when data response from API is valid', async () => {
+      mockedAxiosClient.onGet(apiPath.getGlyphs()).reply(HttpStatusCode.Ok, glyphsFixture);
+
+      const { payload } = await store.dispatch(getGlyphs());
+      expect(payload).toEqual(glyphsFixture.content);
+    });
+
+    it('should return empty object when data response from API is not valid ', async () => {
+      mockedAxiosClient
+        .onGet(apiPath.getGlyphs())
+        .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' });
+
+      const { payload } = await store.dispatch(getGlyphs());
+      expect(payload).toEqual([]);
+    });
+  });
+});
diff --git a/src/redux/glyphs/glyphs.thunks.ts b/src/redux/glyphs/glyphs.thunks.ts
new file mode 100644
index 00000000..2529ba48
--- /dev/null
+++ b/src/redux/glyphs/glyphs.thunks.ts
@@ -0,0 +1,43 @@
+import { apiPath } from '@/redux/apiPath';
+import { Glyph, PageOf } from '@/types/models';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { ThunkConfig } from '@/types/store';
+import { getError } from '@/utils/error-report/getError';
+import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
+import { glyphSchema } from '@/models/glyphSchema';
+import { GLYPHS_FETCHING_ERROR_PREFIX } from '@/redux/glyphs/glyphs.constants';
+import { pageableSchema } from '@/models/pageableSchema';
+
+export const getGlyphs = createAsyncThunk<Glyph[], void, ThunkConfig>('getGlyphs', async () => {
+  try {
+    const { data } = await axiosInstanceNewAPI.get<PageOf<Glyph>>(apiPath.getGlyphs());
+    const isDataValid = validateDataUsingZodSchema(data, pageableSchema(glyphSchema));
+    if (!isDataValid) {
+      return [];
+    }
+    return data.content;
+  } catch (error) {
+    return Promise.reject(getError({ error, prefix: GLYPHS_FETCHING_ERROR_PREFIX }));
+  }
+});
+
+export const addGlyph = createAsyncThunk<
+  Glyph | undefined,
+  File,
+  ThunkConfig
+  // eslint-disable-next-line consistent-return
+>('addGlyph', async file => {
+  try {
+    const formData = new FormData();
+    formData.append('file', file);
+    const { data } = await axiosInstanceNewAPI.post<Glyph>(apiPath.addGlyph(), formData);
+    const isDataValid = validateDataUsingZodSchema(data, glyphSchema);
+    if (!isDataValid) {
+      return undefined;
+    }
+    return data;
+  } catch (error) {
+    return Promise.reject(getError({ error }));
+  }
+});
diff --git a/src/redux/glyphs/glyphs.types.ts b/src/redux/glyphs/glyphs.types.ts
new file mode 100644
index 00000000..acf2e905
--- /dev/null
+++ b/src/redux/glyphs/glyphs.types.ts
@@ -0,0 +1,4 @@
+import { FetchDataState } from '@/types/fetchDataState';
+import { Glyph } from '@/types/models';
+
+export type GlyphsState = FetchDataState<Glyph[], []>;
diff --git a/src/redux/layers/layers.mock.ts b/src/redux/layers/layers.mock.ts
index 38e72675..729624ff 100644
--- a/src/redux/layers/layers.mock.ts
+++ b/src/redux/layers/layers.mock.ts
@@ -4,16 +4,16 @@ import { FetchDataState } from '@/types/fetchDataState';
 
 export const LAYERS_STATE_INITIAL_MOCK: LayersState = {};
 
+export const LAYER_STATE_DEFAULT_DATA = {
+  layers: [],
+  layersVisibility: {},
+  activeLayer: null,
+};
+
 export const LAYERS_STATE_INITIAL_LAYER_MOCK: FetchDataState<LayersVisibilitiesState> = {
   data: {
-    layers: [],
-    layersVisibility: {},
+    ...LAYER_STATE_DEFAULT_DATA,
   },
   loading: 'idle',
   error: DEFAULT_ERROR,
 };
-
-export const LAYER_STATE_DEFAULT_DATA = {
-  layers: [],
-  layersVisibility: {},
-};
diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts
index 938b7f3e..f68bdc4a 100644
--- a/src/redux/layers/layers.reducers.test.ts
+++ b/src/redux/layers/layers.reducers.test.ts
@@ -58,6 +58,7 @@ describe('layers reducer', () => {
     expect(loading).toEqual('succeeded');
     expect(error).toEqual({ message: '', name: '' });
     expect(data).toEqual({
+      activeLayer: null,
       layers: [
         {
           details: layersFixture.content[0],
@@ -86,7 +87,11 @@ describe('layers reducer', () => {
     );
     expect(loading).toEqual('failed');
     expect(error).toEqual({ message: '', name: '' });
-    expect(data).toEqual({ layers: [], layersVisibility: {} });
+    expect(data).toEqual({
+      activeLayer: null,
+      layers: [],
+      layersVisibility: {},
+    });
   });
 
   it('should update store on loading getLayers query', async () => {
@@ -117,6 +122,7 @@ describe('layers reducer', () => {
       const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().layers[1];
 
       expect(dataPromiseFulfilled).toEqual({
+        activeLayer: null,
         layers: [
           {
             details: layersFixture.content[0],
diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts
index b0e601d5..70bdf8d5 100644
--- a/src/redux/layers/layers.reducers.ts
+++ b/src/redux/layers/layers.reducers.ts
@@ -50,3 +50,15 @@ export const setLayerVisibilityReducer = (
     data.layersVisibility[layerId] = visible;
   }
 };
+
+export const setActiveLayerReducer = (
+  state: LayersState,
+  action: PayloadAction<{ modelId: number; layerId: number | null }>,
+): void => {
+  const { modelId, layerId } = action.payload;
+  const { data } = state[modelId];
+  if (!data) {
+    return;
+  }
+  data.activeLayer = layerId;
+};
diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts
index 4d9e3f6d..4392df6e 100644
--- a/src/redux/layers/layers.selectors.ts
+++ b/src/redux/layers/layers.selectors.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-magic-numbers */
 import { createSelector } from '@reduxjs/toolkit';
 import { rootSelector } from '@/redux/root/root.selectors';
 import { currentModelIdSelector } from '@/redux/models/models.selectors';
@@ -10,6 +11,11 @@ export const layersStateForCurrentModelSelector = createSelector(
   (state, currentModelId) => state[currentModelId],
 );
 
+export const layersActiveLayerSelector = createSelector(
+  layersStateForCurrentModelSelector,
+  state => state?.data?.activeLayer || null,
+);
+
 export const layersLoadingSelector = createSelector(
   layersStateForCurrentModelSelector,
   state => state?.loading,
@@ -24,3 +30,22 @@ export const layersForCurrentModelSelector = createSelector(
   layersStateForCurrentModelSelector,
   state => state?.data?.layers || [],
 );
+
+export const highestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => {
+  if (!layers || layers.length === 0) return 0;
+
+  const getMaxZFromItems = <T extends { z?: number }>(items: T[] = []): number =>
+    items.length > 0 ? Math.max(...items.map(item => item.z || 0)) : 0;
+
+  return layers.reduce((maxZ, layer) => {
+    const textsMaxZ = getMaxZFromItems(layer.texts);
+    const rectsMaxZ = getMaxZFromItems(layer.rects);
+    const ovalsMaxZ = getMaxZFromItems(layer.ovals);
+    const linesMaxZ = getMaxZFromItems(layer.lines);
+    const imagesMaxZ = getMaxZFromItems(layer.images);
+
+    const layerMaxZ = Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ);
+
+    return Math.max(maxZ, layerMaxZ);
+  }, 0);
+});
diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts
index 7c07cdc0..d35f1399 100644
--- a/src/redux/layers/layers.slice.ts
+++ b/src/redux/layers/layers.slice.ts
@@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
 import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock';
 import {
   getLayersForModelReducer,
+  setActiveLayerReducer,
   setLayerVisibilityReducer,
 } from '@/redux/layers/layers.reducers';
 
@@ -10,12 +11,13 @@ export const layersSlice = createSlice({
   initialState: LAYERS_STATE_INITIAL_MOCK,
   reducers: {
     setLayerVisibility: setLayerVisibilityReducer,
+    setActiveLayer: setActiveLayerReducer,
   },
   extraReducers: builder => {
     getLayersForModelReducer(builder);
   },
 });
 
-export const { setLayerVisibility } = layersSlice.actions;
+export const { setLayerVisibility, setActiveLayer } = layersSlice.actions;
 
 export default layersSlice.reducer;
diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts
index 975e8ec9..03ba8d80 100644
--- a/src/redux/layers/layers.thunks.test.ts
+++ b/src/redux/layers/layers.thunks.test.ts
@@ -52,6 +52,7 @@ describe('layers thunks', () => {
 
       const { payload } = await store.dispatch(getLayersForModel(1));
       expect(payload).toEqual({
+        activeLayer: null,
         layers: [
           {
             details: layersFixture.content[0],
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index 1fd5d6d4..44f642a2 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -1,4 +1,5 @@
-import { z } from 'zod';
+/* eslint-disable no-magic-numbers */
+import { z as zod } from 'zod';
 import { apiPath } from '@/redux/apiPath';
 import { Layer, Layers } from '@/types/models';
 import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
@@ -70,20 +71,26 @@ export const getLayersForModel = createAsyncThunk<
     );
     layers = layers.filter(layer => {
       return (
-        z.array(layerTextSchema).safeParse(layer.texts).success &&
-        z.array(layerRectSchema).safeParse(layer.rects).success &&
-        z.array(layerOvalSchema).safeParse(layer.ovals).success &&
-        z.array(layerLineSchema).safeParse(layer.lines).success &&
-        z.array(layerImageSchema).safeParse(layer.images).success
+        zod.array(layerTextSchema).safeParse(layer.texts).success &&
+        zod.array(layerRectSchema).safeParse(layer.rects).success &&
+        zod.array(layerOvalSchema).safeParse(layer.ovals).success &&
+        zod.array(layerLineSchema).safeParse(layer.lines).success &&
+        zod.array(layerImageSchema).safeParse(layer.images).success
       );
     });
     const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => {
       acc[layer.details.id] = layer.details.visible;
       return acc;
     }, {});
+    let activeLayer = null;
+    const activeLayers = layers.filter(layer => layer.details.visible);
+    if (activeLayers.length) {
+      activeLayer = activeLayers[0].details.id;
+    }
     return {
       layers,
       layersVisibility,
+      activeLayer,
     };
   } catch (error) {
     return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX }));
@@ -140,3 +147,32 @@ export const removeLayer = createAsyncThunk<
     return Promise.reject(getError({ error }));
   }
 });
+
+export const addLayerImageObject = createAsyncThunk<
+  void,
+  {
+    modelId: number;
+    layerId: number;
+    x: number;
+    y: number;
+    z: number;
+    width: number;
+    height: number;
+    glyph: number | null;
+  },
+  ThunkConfig
+  // eslint-disable-next-line consistent-return
+>('vectorMap/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => {
+  try {
+    await axiosInstanceNewAPI.post<void>(apiPath.addLayerImageObject(modelId, layerId), {
+      x,
+      y,
+      z,
+      width,
+      height,
+      glyph,
+    });
+  } catch (error) {
+    return Promise.reject(getError({ error }));
+  }
+});
diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts
index f2283106..049828d8 100644
--- a/src/redux/layers/layers.types.ts
+++ b/src/redux/layers/layers.types.ts
@@ -32,6 +32,7 @@ export type LayerVisibilityState = {
 export type LayersVisibilitiesState = {
   layersVisibility: LayerVisibilityState;
   layers: LayerState[];
+  activeLayer: number | null;
 };
 
 export type LayersState = KeyedFetchDataState<LayersVisibilitiesState>;
diff --git a/src/redux/modal/modal.constants.ts b/src/redux/modal/modal.constants.ts
index 1184d4ed..1b755f3b 100644
--- a/src/redux/modal/modal.constants.ts
+++ b/src/redux/modal/modal.constants.ts
@@ -14,4 +14,5 @@ export const MODAL_INITIAL_STATE: ModalState = {
   editOverlayState: null,
   errorReportState: {},
   layerFactoryState: { id: undefined },
+  layerImageObjectFactoryState: undefined,
 };
diff --git a/src/redux/modal/modal.mock.ts b/src/redux/modal/modal.mock.ts
index 1a7a519f..40464dd3 100644
--- a/src/redux/modal/modal.mock.ts
+++ b/src/redux/modal/modal.mock.ts
@@ -14,4 +14,5 @@ export const MODAL_INITIAL_STATE_MOCK: ModalState = {
   editOverlayState: null,
   errorReportState: {},
   layerFactoryState: { id: undefined },
+  layerImageObjectFactoryState: undefined,
 };
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index 3371ed3c..b59bf868 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -138,3 +138,13 @@ export const openLayerFactoryModalReducer = (
     state.modalTitle = 'Add new layer';
   }
 };
+
+export const openLayerImageObjectFactoryModalReducer = (
+  state: ModalState,
+  action: PayloadAction<{ x: number; y: number; width: number; height: number }>,
+): void => {
+  state.layerImageObjectFactoryState = action.payload;
+  state.isOpen = true;
+  state.modalName = 'layer-image-object-factory';
+  state.modalTitle = 'Select glyph or upload file';
+};
diff --git a/src/redux/modal/modal.selector.ts b/src/redux/modal/modal.selector.ts
index 7f7c4441..132472b6 100644
--- a/src/redux/modal/modal.selector.ts
+++ b/src/redux/modal/modal.selector.ts
@@ -30,3 +30,8 @@ export const currentErrorDataSelector = createSelector(
   modalSelector,
   modal => modal?.errorReportState.errorData || undefined,
 );
+
+export const layerImageObjectFactoryStateSelector = createSelector(
+  modalSelector,
+  modal => modal.layerImageObjectFactoryState,
+);
diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts
index 8ed04215..a9baf72a 100644
--- a/src/redux/modal/modal.slice.ts
+++ b/src/redux/modal/modal.slice.ts
@@ -17,6 +17,7 @@ import {
   openLicenseModalReducer,
   openToSModalReducer,
   openLayerFactoryModalReducer,
+  openLayerImageObjectFactoryModalReducer,
 } from './modal.reducers';
 
 const modalSlice = createSlice({
@@ -39,6 +40,7 @@ const modalSlice = createSlice({
     openLicenseModal: openLicenseModalReducer,
     openToSModal: openToSModalReducer,
     openLayerFactoryModal: openLayerFactoryModalReducer,
+    openLayerImageObjectFactoryModal: openLayerImageObjectFactoryModalReducer,
   },
 });
 
@@ -59,6 +61,7 @@ export const {
   openLicenseModal,
   openToSModal,
   openLayerFactoryModal,
+  openLayerImageObjectFactoryModal,
 } = modalSlice.actions;
 
 export default modalSlice.reducer;
diff --git a/src/redux/modal/modal.types.ts b/src/redux/modal/modal.types.ts
index 1b544f52..3b22209e 100644
--- a/src/redux/modal/modal.types.ts
+++ b/src/redux/modal/modal.types.ts
@@ -21,6 +21,15 @@ export type LayerFactoryState = {
   id: number | undefined;
 };
 
+export type LayerImageObjectFactoryState =
+  | {
+      x: number;
+      y: number;
+      width: number;
+      height: number;
+    }
+  | undefined;
+
 export interface ModalState {
   isOpen: boolean;
   modalName: ModalName;
@@ -30,6 +39,7 @@ export interface ModalState {
   errorReportState: ErrorRepostState;
   editOverlayState: EditOverlayState;
   layerFactoryState: LayerFactoryState;
+  layerImageObjectFactoryState: LayerImageObjectFactoryState;
 }
 
 export type OpenEditOverlayModalPayload = MapOverlay;
diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts
index b5a5b4e0..ba7f3059 100644
--- a/src/redux/root/init.thunks.ts
+++ b/src/redux/root/init.thunks.ts
@@ -23,6 +23,7 @@ import {
   USER_ACCEPTED_MATOMO_COOKIES_COOKIE_NAME,
 } from '@/components/FunctionalArea/CookieBanner/CookieBanner.constants';
 import { injectMatomoTracking } from '@/utils/injectMatomoTracking';
+import { getGlyphs } from '@/redux/glyphs/glyphs.thunks';
 import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks';
 import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks';
 import {
@@ -67,6 +68,7 @@ export const fetchInitialAppData = createAsyncThunk<
     dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)),
     dispatch(getModels()),
     dispatch(getShapes()),
+    dispatch(getGlyphs()),
     dispatch(getLineTypes()),
     dispatch(getArrowTypes()),
   ]);
diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts
index 007c2b35..e2724495 100644
--- a/src/redux/root/root.fixtures.ts
+++ b/src/redux/root/root.fixtures.ts
@@ -7,6 +7,7 @@ import { SHAPES_STATE_INITIAL_MOCK } from '@/redux/shapes/shapes.mock';
 import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock';
 import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock';
 import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock';
+import { GLYPHS_STATE_INITIAL_MOCK } from '@/redux/glyphs/glyphs.mock';
 import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock';
 import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock';
 import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock';
@@ -41,6 +42,7 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
   search: SEARCH_STATE_INITIAL_MOCK,
   project: PROJECT_STATE_INITIAL_MOCK,
   shapes: SHAPES_STATE_INITIAL_MOCK,
+  glyphs: GLYPHS_STATE_INITIAL_MOCK,
   projects: PROJECTS_STATE_INITIAL_MOCK,
   drugs: DRUGS_INITIAL_STATE_MOCK,
   chemicals: CHEMICALS_INITIAL_STATE_MOCK,
diff --git a/src/redux/store.ts b/src/redux/store.ts
index a5d31bb5..031b51be 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -11,6 +11,7 @@ import mapReducer from '@/redux/map/map.slice';
 import modalReducer from '@/redux/modal/modal.slice';
 import modelsReducer from '@/redux/models/models.slice';
 import shapesReducer from '@/redux/shapes/shapes.slice';
+import glyphsReducer from '@/redux/glyphs/glyphs.slice';
 import modelElementsReducer from '@/redux/modelElements/modelElements.slice';
 import layersReducer from '@/redux/layers/layers.slice';
 import oauthReducer from '@/redux/oauth/oauth.slice';
@@ -64,6 +65,7 @@ export const reducers = {
   overlays: overlaysReducer,
   models: modelsReducer,
   shapes: shapesReducer,
+  glyphs: glyphsReducer,
   modelElements: modelElementsReducer,
   layers: layersReducer,
   reactions: reactionsReducer,
diff --git a/src/shared/Input/Input.component.tsx b/src/shared/Input/Input.component.tsx
index 00f3e924..96675fdf 100644
--- a/src/shared/Input/Input.component.tsx
+++ b/src/shared/Input/Input.component.tsx
@@ -1,4 +1,4 @@
-import React, { InputHTMLAttributes } from 'react';
+import React, { InputHTMLAttributes, forwardRef } from 'react';
 import { twMerge } from 'tailwind-merge';
 
 type StyleVariant = 'primary' | 'primaryWithoutFull';
@@ -8,6 +8,7 @@ type InputProps = {
   className?: string;
   styleVariant?: StyleVariant;
   sizeVariant?: SizeVariant;
+  ref?: React.Ref<HTMLInputElement>;
 } & InputHTMLAttributes<HTMLInputElement>;
 
 const styleVariants = {
@@ -22,14 +23,17 @@ const sizeVariants = {
   medium: 'rounded-lg h-12 text-sm',
 } as const;
 
-export const Input = ({
-  className = '',
-  sizeVariant = 'small',
-  styleVariant = 'primary',
-  ...props
-}: InputProps): React.ReactNode => (
-  <input
-    {...props}
-    className={twMerge(styleVariants[styleVariant], sizeVariants[sizeVariant], className)}
-  />
+export const Input = forwardRef<HTMLInputElement, InputProps>(
+  (
+    { className = '', sizeVariant = 'small', styleVariant = 'primary', ...props }: InputProps,
+    ref,
+  ): React.ReactNode => (
+    <input
+      ref={ref}
+      {...props}
+      className={twMerge(styleVariants[styleVariant], sizeVariants[sizeVariant], className)}
+    />
+  ),
 );
+
+Input.displayName = 'Input';
diff --git a/src/shared/Select/Select.component.tsx b/src/shared/Select/Select.component.tsx
index aa3ab6d5..f0107571 100644
--- a/src/shared/Select/Select.component.tsx
+++ b/src/shared/Select/Select.component.tsx
@@ -5,7 +5,7 @@ import { Icon } from '@/shared/Icon';
 
 type SelectProps = {
   options: Array<{ id: number; name: string }>;
-  selectedId: number;
+  selectedId: number | null;
   onChange: (selectedId: number) => void;
   width?: string | number;
 };
@@ -16,7 +16,7 @@ export const Select = ({
   onChange,
   width = '100%',
 }: SelectProps): React.JSX.Element => {
-  const selectedOption = options.find(option => option.id === selectedId);
+  const selectedOption = options.find(option => option.id === selectedId) || null;
 
   const {
     isOpen,
@@ -63,7 +63,7 @@ export const Select = ({
       </div>
       <ul
         className={twMerge(
-          'absolute z-10 overflow-auto rounded-b bg-white shadow-lg',
+          'absolute z-20 overflow-auto rounded-b bg-white shadow-lg',
           !isOpen && 'hidden',
         )}
         style={widthStyle}
diff --git a/src/types/modal.ts b/src/types/modal.ts
index 861bb295..edf1c858 100644
--- a/src/types/modal.ts
+++ b/src/types/modal.ts
@@ -12,4 +12,5 @@ export type ModalName =
   | 'select-project'
   | 'terms-of-service'
   | 'logged-in-menu'
-  | 'layer-factory';
+  | 'layer-factory'
+  | 'layer-image-object-factory';
diff --git a/src/types/models.ts b/src/types/models.ts
index 91a9a887..0c00639e 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -85,6 +85,7 @@ import { operatorSchema } from '@/models/operatorSchema';
 import { modificationResiduesSchema } from '@/models/modificationResiduesSchema';
 import { segmentSchema } from '@/models/segmentSchema';
 import { layerImageSchema } from '@/models/layerImageSchema';
+import { glyphSchema } from '@/models/glyphSchema';
 
 export type Project = z.infer<typeof projectSchema>;
 export type OverviewImageView = z.infer<typeof overviewImageView>;
@@ -105,6 +106,7 @@ export type LayerOval = z.infer<typeof layerOvalSchema>;
 export type LayerLine = z.infer<typeof layerLineSchema>;
 export type LayerImage = z.infer<typeof layerImageSchema>;
 export type Arrow = z.infer<typeof arrowSchema>;
+export type Glyph = z.infer<typeof glyphSchema>;
 const modelElementsSchema = pageableSchema(modelElementSchema);
 export type ModelElements = z.infer<typeof modelElementsSchema>;
 export type ModelElement = z.infer<typeof modelElementSchema>;
-- 
GitLab


From c617996bfd3b93c7d7bc1c05500b4eb77a0bd217 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Fri, 20 Dec 2024 09:56:10 +0100
Subject: [PATCH 07/22] fix(glyphs): correct scaling glyph image

---
 .../MapViewerVector/utils/shapes/elements/Glyph.ts     | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
index ff5a7887..c7844ae3 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
@@ -139,7 +139,15 @@ export default class Glyph {
     img.onload = (): void => {
       const imageWidth = img.naturalWidth;
       const imageHeight = img.naturalHeight;
-      this.imageScale = width / imageWidth;
+      const heightScale = height / imageHeight;
+      const widthScale = width / imageWidth;
+      if (heightScale < widthScale) {
+        this.imageScale = heightScale;
+        this.widthOnMap = (this.heightOnMap * imageWidth) / imageHeight;
+      } else {
+        this.imageScale = widthScale;
+        this.heightOnMap = (this.widthOnMap * imageHeight) / imageWidth;
+      }
       this.style = new Style({
         image: new Icon({
           anchor: [0, 0],
-- 
GitLab


From 377d704bc358c5b4e7a5eb29d564182347bda831 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Fri, 20 Dec 2024 11:54:57 +0100
Subject: [PATCH 08/22] feat(image-layer): add drawing image object

---
 next.config.js                                |  2 +-
 .../AppWrapper/AppWrapper.component.tsx       |  2 +
 ...LayerImageObjectFactoryModal.component.tsx | 27 ++++++-
 .../useOlMapAdditionalLayers.ts               | 19 ++++-
 .../utils/shapes/layer/Layer.test.ts          |  9 +--
 .../utils/shapes/layer/Layer.ts               | 78 ++++++++-----------
 src/components/SPA/MinervaSPA.component.tsx   |  4 +-
 src/models/fixtures/layerImagesFixture.ts     |  2 +-
 src/redux/layers/layers.reducers.test.ts      |  4 +-
 src/redux/layers/layers.reducers.ts           | 17 ++++
 src/redux/layers/layers.selectors.ts          |  2 +-
 src/redux/layers/layers.slice.ts              |  4 +-
 src/redux/layers/layers.thunks.test.ts        |  2 +-
 src/redux/layers/layers.thunks.ts             | 32 ++++----
 src/redux/layers/layers.types.ts              |  2 +-
 src/redux/modal/modal.reducers.ts             |  7 +-
 src/utils/array/arrayToKeyValue.ts            | 12 +++
 17 files changed, 144 insertions(+), 81 deletions(-)
 create mode 100644 src/utils/array/arrayToKeyValue.ts

diff --git a/next.config.js b/next.config.js
index c882ad1b..46540574 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,6 @@
 /** @type {import("next").NextConfig} */
 const nextConfig = {
-  reactStrictMode: false,
+  reactStrictMode: true,
   basePath: process.env.APP_PREFIX ? process.env.APP_PREFIX + '/index.html' : '',
   assetPrefix: process.env.APP_PREFIX ? process.env.APP_PREFIX : '',
   productionBrowserSourceMaps: true,
diff --git a/src/components/AppWrapper/AppWrapper.component.tsx b/src/components/AppWrapper/AppWrapper.component.tsx
index 2bae3997..39a65352 100644
--- a/src/components/AppWrapper/AppWrapper.component.tsx
+++ b/src/components/AppWrapper/AppWrapper.component.tsx
@@ -3,6 +3,7 @@ import { MapInstanceProvider } from '@/utils/context/mapInstanceContext';
 import { ReactNode } from 'react';
 import { Provider } from 'react-redux';
 import { Toaster } from 'sonner';
+import { Modal } from '@/components/FunctionalArea/Modal';
 
 interface AppWrapperProps {
   children: ReactNode;
@@ -13,6 +14,7 @@ export const AppWrapper = ({ children }: AppWrapperProps): JSX.Element => {
     <MapInstanceProvider>
       <Provider store={store}>
         <>
+          <Modal />
           <Toaster
             position="top-center"
             visibleToasts={1}
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
index 10539f95..5144ee8c 100644
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
@@ -13,13 +13,15 @@ import { Glyph } from '@/types/models';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { currentModelIdSelector } from '@/redux/models/models.selectors';
 import { highestZIndexSelector, layersActiveLayerSelector } from '@/redux/layers/layers.selectors';
-import { addLayerImageObject, getLayersForModel } from '@/redux/layers/layers.thunks';
+import { addLayerImageObject } from '@/redux/layers/layers.thunks';
 import { addGlyph } from '@/redux/glyphs/glyphs.thunks';
 import { SerializedError } from '@reduxjs/toolkit';
 import { showToast } from '@/utils/showToast';
 import { closeModal } from '@/redux/modal/modal.slice';
 import { LoadingIndicator } from '@/shared/LoadingIndicator';
 import './LayerImageObjectFactoryModal.styles.css';
+import { useMapInstance } from '@/utils/context/mapInstanceContext';
+import { layerAddImage } from '@/redux/layers/layers.slice';
 
 export const LayerImageObjectFactoryModal: React.FC = () => {
   const glyphs: Glyph[] = useAppSelector(glyphsDataSelector);
@@ -29,6 +31,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
   const dispatch = useAppDispatch();
   const fileInputRef = useRef<HTMLInputElement>(null);
   const highestZIndex = useAppSelector(highestZIndexSelector);
+  const { mapInstance } = useMapInstance();
 
   const [selectedGlyph, setSelectedGlyph] = useState<number | null>(null);
   const [file, setFile] = useState<File | null>(null);
@@ -79,7 +82,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
         }
         glyphId = data.id;
       }
-      await dispatch(
+      const imageData = await dispatch(
         addLayerImageObject({
           modelId: currentModelId,
           layerId: activeLayer,
@@ -91,12 +94,30 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
           glyph: glyphId,
         }),
       ).unwrap();
+      if (!imageData) {
+        showToast({
+          type: 'error',
+          message: 'Error during adding layer image object',
+        });
+        return;
+      }
+      dispatch(
+        layerAddImage({ modelId: currentModelId, layerId: activeLayer, layerImage: imageData }),
+      );
+      mapInstance?.getAllLayers().forEach(layer => {
+        if (layer.get('id') === activeLayer && layer.get('drawImage')) {
+          const drawImage = layer.get('drawImage');
+          if (drawImage instanceof Function) {
+            drawImage(imageData);
+          }
+        }
+      });
+
       showToast({
         type: 'success',
         message: 'A new image object has been successfully added',
       });
       dispatch(closeModal());
-      dispatch(getLayersForModel(currentModelId));
     } catch (error) {
       const typedError = error as SerializedError;
       showToast({
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
index 31c1a087..1563b536 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
@@ -2,7 +2,7 @@
 import { Feature } from 'ol';
 import VectorLayer from 'ol/layer/Vector';
 import VectorSource from 'ol/source/Vector';
-import { useEffect, useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
 import { useSelector } from 'react-redux';
 import { currentModelIdSelector, vectorRenderingSelector } from '@/redux/models/models.selectors';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
@@ -22,6 +22,7 @@ import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.sel
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { mapDataSizeSelector } from '@/redux/map/map.selectors';
 import getDrawImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction';
+import { LayerState } from '@/redux/layers/layers.types';
 
 export const useOlMapAdditionalLayers = (
   mapInstance: MapInstance,
@@ -40,6 +41,9 @@ export const useOlMapAdditionalLayers = (
   const activeLayer = useAppSelector(layersActiveLayerSelector);
   const vectorRendering = useAppSelector(vectorRenderingSelector);
 
+  const [layersState, setLayersState] = useState<Array<LayerState>>([]);
+  const [layersLoadingState, setLayersLoadingState] = useState(false);
+
   const lineTypes = useSelector(lineTypesSelector);
   const arrowTypes = useSelector(arrowTypesSelector);
   const pointToProjection = usePointToProjection();
@@ -61,7 +65,7 @@ export const useOlMapAdditionalLayers = (
   }, [currentModelId, dispatch, layersLoading]);
 
   const vectorLayers = useMemo(() => {
-    return layersForCurrentModel.map(layer => {
+    return layersState.map(layer => {
       const additionalLayer = new Layer({
         texts: layer.texts,
         rects: layer.rects,
@@ -77,7 +81,16 @@ export const useOlMapAdditionalLayers = (
       });
       return additionalLayer.vectorLayer;
     });
-  }, [layersForCurrentModel, lineTypes, arrowTypes, mapInstance, pointToProjection]);
+  }, [layersState, lineTypes, arrowTypes, mapInstance, pointToProjection]);
+
+  useEffect(() => {
+    if (layersLoading === 'pending') {
+      setLayersLoadingState(true);
+    } else if (layersLoading === 'succeeded' && layersLoadingState) {
+      setLayersLoadingState(false);
+      setLayersState(layersForCurrentModel);
+    }
+  }, [layersForCurrentModel, layersLoading, layersLoadingState]);
 
   useEffect(() => {
     vectorLayers.forEach(layer => {
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
index eeabb89c..1cdd1aef 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
@@ -113,8 +113,8 @@ describe('Layer', () => {
           lineType: 'SOLID',
         },
       ],
-      images: [
-        {
+      images: {
+        1: {
           id: 1,
           glyph: 1,
           x: 1,
@@ -123,7 +123,7 @@ describe('Layer', () => {
           height: 1,
           z: 1,
         },
-      ],
+      },
       visible: true,
       layerId: 23,
       pointToProjection: jest.fn(point => [point.x, point.y]),
@@ -144,9 +144,6 @@ describe('Layer', () => {
   it('should initialize a Layer class', () => {
     const layer = new Layer(props);
 
-    expect(layer.textFeatures.length).toBe(1);
-    expect(layer.rectFeatures.length).toBe(1);
-    expect(layer.ovalFeatures.length).toBe(1);
     expect(layer.vectorSource).toBeInstanceOf(VectorSource);
     expect(layer.vectorLayer).toBeInstanceOf(VectorLayer);
   });
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
index 50a2b285..e48faf93 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
@@ -33,7 +33,7 @@ export interface LayerProps {
   rects: Array<LayerRect>;
   ovals: Array<LayerOval>;
   lines: Array<LayerLine>;
-  images: Array<LayerImage>;
+  images: { [key: string]: LayerImage };
   visible: boolean;
   layerId: number;
   lineTypes: LineTypeDict;
@@ -51,24 +51,12 @@ export default class Layer {
 
   lines: Array<LayerLine>;
 
-  images: Array<LayerImage>;
+  images: { [key: string]: LayerImage };
 
   lineTypes: LineTypeDict;
 
   arrowTypes: ArrowTypeDict;
 
-  textFeatures: Array<Feature<Point>>;
-
-  rectFeatures: Array<Feature<Polygon>>;
-
-  ovalFeatures: Array<Feature<Polygon>>;
-
-  imageFeatures: Array<Feature<Polygon>>;
-
-  lineFeatures: Array<Feature<LineString>>;
-
-  arrowFeatures: Array<Feature<MultiPolygon>>;
-
   pointToProjection: UsePointToProjectionResult;
 
   mapInstance: MapInstance;
@@ -94,6 +82,8 @@ export default class Layer {
     mapInstance,
     pointToProjection,
   }: LayerProps) {
+    this.vectorSource = new VectorSource({});
+
     this.texts = texts;
     this.rects = rects;
     this.ovals = ovals;
@@ -103,23 +93,16 @@ export default class Layer {
     this.arrowTypes = arrowTypes;
     this.pointToProjection = pointToProjection;
     this.mapInstance = mapInstance;
-    this.textFeatures = this.getTextsFeatures();
-    this.rectFeatures = this.getRectsFeatures();
-    this.ovalFeatures = this.getOvalsFeatures();
-    this.imageFeatures = this.getImagesFeatures();
+
+    this.vectorSource.addFeatures(this.getTextsFeatures());
+    this.vectorSource.addFeatures(this.getRectsFeatures());
+    this.vectorSource.addFeatures(this.getOvalsFeatures());
+    this.drawImages();
+
     const { linesFeatures, arrowsFeatures } = this.getLinesFeatures();
-    this.lineFeatures = linesFeatures;
-    this.arrowFeatures = arrowsFeatures;
-    this.vectorSource = new VectorSource({
-      features: [
-        ...this.textFeatures,
-        ...this.rectFeatures,
-        ...this.ovalFeatures,
-        ...this.lineFeatures,
-        ...this.arrowFeatures,
-        ...this.imageFeatures,
-      ],
-    });
+    this.vectorSource.addFeatures(linesFeatures);
+    this.vectorSource.addFeatures(arrowsFeatures);
+
     this.vectorLayer = new VectorLayer({
       source: this.vectorSource,
       visible,
@@ -128,6 +111,7 @@ export default class Layer {
     });
 
     this.vectorLayer.set('id', layerId);
+    this.vectorLayer.set('drawImage', this.drawImage.bind(this));
   }
 
   private getTextsFeatures = (): Array<Feature<Point>> => {
@@ -306,22 +290,26 @@ export default class Layer {
     return { linesFeatures, arrowsFeatures };
   };
 
-  private getImagesFeatures = (): Array<Feature<Polygon>> => {
-    return this.images.map(image => {
-      const glyph = new Glyph({
-        elementId: image.id,
-        glyphId: image.glyph,
-        x: image.x,
-        y: image.y,
-        width: image.width,
-        height: image.height,
-        zIndex: image.z,
-        pointToProjection: this.pointToProjection,
-        mapInstance: this.mapInstance,
-      });
-      return glyph.feature;
+  private drawImages(): void {
+    Object.values(this.images).forEach(image => {
+      this.drawImage(image);
     });
-  };
+  }
+
+  private drawImage(image: LayerImage): void {
+    const glyph = new Glyph({
+      elementId: image.id,
+      glyphId: image.glyph,
+      x: image.x,
+      y: image.y,
+      width: image.width,
+      height: image.height,
+      zIndex: image.z,
+      pointToProjection: this.pointToProjection,
+      mapInstance: this.mapInstance,
+    });
+    this.vectorSource.addFeature(glyph.feature);
+  }
 
   protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void {
     const styles: Array<Style> = [];
diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx
index 5762a59b..2e9751e3 100644
--- a/src/components/SPA/MinervaSPA.component.tsx
+++ b/src/components/SPA/MinervaSPA.component.tsx
@@ -6,7 +6,7 @@ import { twMerge } from 'tailwind-merge';
 import { useEffect } from 'react';
 import { PluginsManager } from '@/services/pluginsManager';
 import { useInitializeStore } from '../../utils/initialize/useInitializeStore';
-import { Modal } from '../FunctionalArea/Modal';
+// import { Modal } from '../FunctionalArea/Modal';
 import { ContextMenu } from '../FunctionalArea/ContextMenu';
 import { CookieBanner } from '../FunctionalArea/CookieBanner';
 
@@ -24,7 +24,7 @@ export const MinervaSPA = (): JSX.Element => {
     <div className={twMerge('relative', manrope.variable)}>
       <FunctionalArea />
       <Map />
-      <Modal />
+      {/* <Modal /> */}
       <ContextMenu />
       <CookieBanner />
     </div>
diff --git a/src/models/fixtures/layerImagesFixture.ts b/src/models/fixtures/layerImagesFixture.ts
index 382b5f92..77c2d027 100644
--- a/src/models/fixtures/layerImagesFixture.ts
+++ b/src/models/fixtures/layerImagesFixture.ts
@@ -6,5 +6,5 @@ import { layerImageSchema } from '@/models/layerImageSchema';
 
 export const layerImagesFixture = createFixture(pageableSchema(layerImageSchema), {
   seed: ZOD_SEED,
-  array: { min: 3, max: 3 },
+  array: { min: 1, max: 1 },
 });
diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts
index f68bdc4a..20d1cdbd 100644
--- a/src/redux/layers/layers.reducers.test.ts
+++ b/src/redux/layers/layers.reducers.test.ts
@@ -66,7 +66,7 @@ describe('layers reducer', () => {
           rects: layerRectsFixture.content,
           ovals: layerOvalsFixture.content,
           lines: layerLinesFixture.content,
-          images: layerImagesFixture.content,
+          images: { [layerImagesFixture.content[0].id]: layerImagesFixture.content[0] },
         },
       ],
       layersVisibility: {
@@ -130,7 +130,7 @@ describe('layers reducer', () => {
             rects: layerRectsFixture.content,
             ovals: layerOvalsFixture.content,
             lines: layerLinesFixture.content,
-            images: layerImagesFixture.content,
+            images: { [layerImagesFixture.content[0].id]: layerImagesFixture.content[0] },
           },
         ],
         layersVisibility: {
diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts
index 70bdf8d5..9e41f8de 100644
--- a/src/redux/layers/layers.reducers.ts
+++ b/src/redux/layers/layers.reducers.ts
@@ -7,6 +7,7 @@ import {
   LAYERS_STATE_INITIAL_LAYER_MOCK,
 } from '@/redux/layers/layers.mock';
 import { DEFAULT_ERROR } from '@/constants/errors';
+import { LayerImage } from '@/types/models';
 
 export const getLayersForModelReducer = (builder: ActionReducerMapBuilder<LayersState>): void => {
   builder.addCase(getLayersForModel.pending, (state, action) => {
@@ -62,3 +63,19 @@ export const setActiveLayerReducer = (
   }
   data.activeLayer = layerId;
 };
+
+export const layerAddImageReducer = (
+  state: LayersState,
+  action: PayloadAction<{ modelId: number; layerId: number; layerImage: LayerImage }>,
+): void => {
+  const { modelId, layerId, layerImage } = action.payload;
+  const { data } = state[modelId];
+  if (!data) {
+    return;
+  }
+  const layer = data.layers.find(layerState => layerState.details.id === layerId);
+  if (!layer) {
+    return;
+  }
+  layer.images[layerImage.id] = layerImage;
+};
diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts
index 4392df6e..aa71d5e4 100644
--- a/src/redux/layers/layers.selectors.ts
+++ b/src/redux/layers/layers.selectors.ts
@@ -42,7 +42,7 @@ export const highestZIndexSelector = createSelector(layersForCurrentModelSelecto
     const rectsMaxZ = getMaxZFromItems(layer.rects);
     const ovalsMaxZ = getMaxZFromItems(layer.ovals);
     const linesMaxZ = getMaxZFromItems(layer.lines);
-    const imagesMaxZ = getMaxZFromItems(layer.images);
+    const imagesMaxZ = getMaxZFromItems(Object.values(layer.images));
 
     const layerMaxZ = Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ);
 
diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts
index d35f1399..9f78f0dd 100644
--- a/src/redux/layers/layers.slice.ts
+++ b/src/redux/layers/layers.slice.ts
@@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
 import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock';
 import {
   getLayersForModelReducer,
+  layerAddImageReducer,
   setActiveLayerReducer,
   setLayerVisibilityReducer,
 } from '@/redux/layers/layers.reducers';
@@ -12,12 +13,13 @@ export const layersSlice = createSlice({
   reducers: {
     setLayerVisibility: setLayerVisibilityReducer,
     setActiveLayer: setActiveLayerReducer,
+    layerAddImage: layerAddImageReducer,
   },
   extraReducers: builder => {
     getLayersForModelReducer(builder);
   },
 });
 
-export const { setLayerVisibility, setActiveLayer } = layersSlice.actions;
+export const { setLayerVisibility, setActiveLayer, layerAddImage } = layersSlice.actions;
 
 export default layersSlice.reducer;
diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts
index 03ba8d80..218d9ce5 100644
--- a/src/redux/layers/layers.thunks.test.ts
+++ b/src/redux/layers/layers.thunks.test.ts
@@ -60,7 +60,7 @@ describe('layers thunks', () => {
             rects: layerRectsFixture.content,
             ovals: layerOvalsFixture.content,
             lines: layerLinesFixture.content,
-            images: layerImagesFixture.content,
+            images: { [layerImagesFixture.content[0].id]: layerImagesFixture.content[0] },
           },
         ],
         layersVisibility: {
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index 44f642a2..5a2ec2d4 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -1,7 +1,7 @@
 /* eslint-disable no-magic-numbers */
 import { z as zod } from 'zod';
 import { apiPath } from '@/redux/apiPath';
-import { Layer, Layers } from '@/types/models';
+import { Layer, LayerImage, Layers } from '@/types/models';
 import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
 import { createAsyncThunk } from '@reduxjs/toolkit';
 import { ThunkConfig } from '@/types/store';
@@ -20,6 +20,7 @@ import { pageableSchema } from '@/models/pageableSchema';
 import { layerOvalSchema } from '@/models/layerOvalSchema';
 import { layerLineSchema } from '@/models/layerLineSchema';
 import { layerImageSchema } from '@/models/layerImageSchema';
+import arrayToKeyValue from '@/utils/array/arrayToKeyValue';
 
 export const getLayer = createAsyncThunk<
   Layer | null,
@@ -58,14 +59,13 @@ export const getLayersForModel = createAsyncThunk<
             axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)),
             axiosInstanceNewAPI.get(apiPath.getLayerImages(modelId, layer.id)),
           ]);
-
         return {
           details: layer,
           texts: textsResponse.data.content,
           rects: rectsResponse.data.content,
           ovals: ovalsResponse.data.content,
           lines: linesResponse.data.content,
-          images: imagesResponse.data.content,
+          images: arrayToKeyValue(imagesResponse.data.content, 'id'),
         };
       }),
     );
@@ -75,7 +75,7 @@ export const getLayersForModel = createAsyncThunk<
         zod.array(layerRectSchema).safeParse(layer.rects).success &&
         zod.array(layerOvalSchema).safeParse(layer.ovals).success &&
         zod.array(layerLineSchema).safeParse(layer.lines).success &&
-        zod.array(layerImageSchema).safeParse(layer.images).success
+        zod.array(layerImageSchema).safeParse(Object.values(layer.images)).success
       );
     });
     const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => {
@@ -149,7 +149,7 @@ export const removeLayer = createAsyncThunk<
 });
 
 export const addLayerImageObject = createAsyncThunk<
-  void,
+  LayerImage | null,
   {
     modelId: number;
     layerId: number;
@@ -164,14 +164,20 @@ export const addLayerImageObject = createAsyncThunk<
   // eslint-disable-next-line consistent-return
 >('vectorMap/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => {
   try {
-    await axiosInstanceNewAPI.post<void>(apiPath.addLayerImageObject(modelId, layerId), {
-      x,
-      y,
-      z,
-      width,
-      height,
-      glyph,
-    });
+    const { data } = await axiosInstanceNewAPI.post<LayerImage>(
+      apiPath.addLayerImageObject(modelId, layerId),
+      {
+        x,
+        y,
+        z,
+        width,
+        height,
+        glyph,
+      },
+    );
+    const isDataValid = validateDataUsingZodSchema(data, layerImageSchema);
+
+    return isDataValid ? data : null;
   } catch (error) {
     return Promise.reject(getError({ error }));
   }
diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts
index 049828d8..27dc36fc 100644
--- a/src/redux/layers/layers.types.ts
+++ b/src/redux/layers/layers.types.ts
@@ -22,7 +22,7 @@ export type LayerState = {
   rects: LayerRect[];
   ovals: LayerOval[];
   lines: LayerLine[];
-  images: LayerImage[];
+  images: { [key: string]: LayerImage };
 };
 
 export type LayerVisibilityState = {
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index b59bf868..f678ea91 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -141,7 +141,12 @@ export const openLayerFactoryModalReducer = (
 
 export const openLayerImageObjectFactoryModalReducer = (
   state: ModalState,
-  action: PayloadAction<{ x: number; y: number; width: number; height: number }>,
+  action: PayloadAction<{
+    x: number;
+    y: number;
+    width: number;
+    height: number;
+  }>,
 ): void => {
   state.layerImageObjectFactoryState = action.payload;
   state.isOpen = true;
diff --git a/src/utils/array/arrayToKeyValue.ts b/src/utils/array/arrayToKeyValue.ts
new file mode 100644
index 00000000..52c7cf2e
--- /dev/null
+++ b/src/utils/array/arrayToKeyValue.ts
@@ -0,0 +1,12 @@
+export default function arrayToKeyValue<T extends Record<string, never>, K extends keyof T>(
+  array: T[],
+  key: K,
+): Record<T[K] & PropertyKey, T> {
+  return array.reduce(
+    (accumulator, currentItem) => {
+      accumulator[currentItem[key] as T[K] & PropertyKey] = currentItem;
+      return accumulator;
+    },
+    {} as Record<T[K] & PropertyKey, T>,
+  );
+}
-- 
GitLab


From a3cf7e480f8e5f7e59c30f8abbd3a7b42d70b654 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Fri, 20 Dec 2024 13:44:44 +0100
Subject: [PATCH 09/22] feat(image-layer): add toggle drawing image action

---
 src/components/Map/Map.component.tsx          |  2 ++
 .../MapActiveLayerSelector.component.tsx      |  2 +-
 .../MapDrawActions.component.tsx              | 27 +++++++++++++++
 .../MapDrawActionsButton.component.tsx        | 33 +++++++++++++++++++
 .../MapVectorBackgroundSelector.component.tsx |  2 +-
 .../useOlMapAdditionalLayers.ts               | 14 ++++++--
 .../shapes/layer/getDrawImageInteraction.ts   |  1 +
 .../mapEditTools/mapEditTools.constants.ts    |  3 ++
 src/redux/mapEditTools/mapEditTools.mock.ts   |  5 +++
 .../mapEditTools/mapEditTools.reducers.ts     | 15 +++++++++
 .../mapEditTools/mapEditTools.selectors.ts    | 10 ++++++
 src/redux/mapEditTools/mapEditTools.slice.ts  | 15 +++++++++
 src/redux/mapEditTools/mapEditTools.types.ts  |  5 +++
 src/redux/root/root.fixtures.ts               |  2 ++
 src/redux/store.ts                            |  2 ++
 src/shared/Icon/Icon.component.tsx            |  2 ++
 src/shared/Icon/Icons/ImageIcon.tsx           | 25 ++++++++++++++
 src/types/iconTypes.ts                        |  3 +-
 18 files changed, 163 insertions(+), 5 deletions(-)
 create mode 100644 src/components/Map/MapDrawActions/MapDrawActions.component.tsx
 create mode 100644 src/components/Map/MapDrawActions/MapDrawActionsButton.component.tsx
 create mode 100644 src/redux/mapEditTools/mapEditTools.constants.ts
 create mode 100644 src/redux/mapEditTools/mapEditTools.mock.ts
 create mode 100644 src/redux/mapEditTools/mapEditTools.reducers.ts
 create mode 100644 src/redux/mapEditTools/mapEditTools.selectors.ts
 create mode 100644 src/redux/mapEditTools/mapEditTools.slice.ts
 create mode 100644 src/redux/mapEditTools/mapEditTools.types.ts
 create mode 100644 src/shared/Icon/Icons/ImageIcon.tsx

diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx
index 65392d69..3209e8ac 100644
--- a/src/components/Map/Map.component.tsx
+++ b/src/components/Map/Map.component.tsx
@@ -8,6 +8,7 @@ import { MapActiveLayerSelector } from '@/components/Map/MapActiveLayerSelector/
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { vectorRenderingSelector } from '@/redux/models/models.selectors';
 import { MapAdditionalLogos } from '@/components/Map/MapAdditionalLogos';
+import { MapDrawActions } from '@/components/Map/MapDrawActions/MapDrawActions.component';
 import { MapAdditionalActions } from './MapAdditionalActions';
 import { MapAdditionalOptions } from './MapAdditionalOptions';
 import { PluginsDrawer } from './PluginsDrawer';
@@ -25,6 +26,7 @@ export const Map = (): JSX.Element => {
         <>
           <MapVectorBackgroundSelector />
           <MapActiveLayerSelector />
+          <MapDrawActions />
         </>
       )}
       <Drawer />
diff --git a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
index d4fd65a1..0f6a8810 100644
--- a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
+++ b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
@@ -46,7 +46,7 @@ export const MapActiveLayerSelector = (): JSX.Element => {
   }, [activeLayer, currentModelId, dispatch, options]);
 
   return (
-    <div className={twMerge('absolute right-[140px] top-[calc(64px+40px+24px)] z-10 flex')}>
+    <div className={twMerge('absolute right-6 top-[calc(64px+40px+84px)] z-10 flex')}>
       <Select options={options} selectedId={activeLayer} onChange={handleChange} width={140} />
     </div>
   );
diff --git a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
new file mode 100644
index 00000000..3d02bee2
--- /dev/null
+++ b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
@@ -0,0 +1,27 @@
+/* eslint-disable no-magic-numbers */
+import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
+import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component';
+
+export const MapDrawActions = (): React.JSX.Element => {
+  const activeAction = useAppSelector(mapEditToolsActiveActionSelector);
+  const dispatch = useAppDispatch();
+
+  const toggleMapEditAction = (action: keyof typeof MAP_EDIT_ACTIONS): void => {
+    dispatch(mapEditToolsSetActiveAction(action));
+  };
+
+  return (
+    <div className="absolute right-6 top-[calc(64px+40px+144px)] z-10 flex flex-col gap-4">
+      <MapDrawActionsButton
+        isActive={activeAction === MAP_EDIT_ACTIONS.DRAW_IMAGE}
+        toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)}
+        icon="image"
+        title="Draw image"
+      />
+    </div>
+  );
+};
diff --git a/src/components/Map/MapDrawActions/MapDrawActionsButton.component.tsx b/src/components/Map/MapDrawActions/MapDrawActionsButton.component.tsx
new file mode 100644
index 00000000..eacefa31
--- /dev/null
+++ b/src/components/Map/MapDrawActions/MapDrawActionsButton.component.tsx
@@ -0,0 +1,33 @@
+/* eslint-disable no-magic-numbers */
+import { Icon } from '@/shared/Icon';
+import type { IconTypes } from '@/types/iconTypes';
+
+type MapDrawActionsButtonProps = {
+  isActive: boolean;
+  toggleMapEditAction: () => void;
+  icon: IconTypes;
+  title?: string;
+};
+
+export const MapDrawActionsButton = ({
+  isActive,
+  toggleMapEditAction,
+  icon,
+  title = '',
+}: MapDrawActionsButtonProps): React.JSX.Element => {
+  return (
+    <button
+      type="button"
+      className={`flex h-12 w-12 items-center justify-center rounded-full ${
+        isActive ? 'bg-primary-100' : 'bg-white drop-shadow-primary'
+      }`}
+      onClick={() => toggleMapEditAction()}
+      title={title}
+    >
+      <Icon
+        className={`h-[28px] w-[28px] ${isActive ? 'text-primary-500' : 'text-black'}`}
+        name={icon}
+      />
+    </button>
+  );
+};
diff --git a/src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx b/src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx
index 32af5af4..c4e2cafa 100644
--- a/src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx
+++ b/src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx
@@ -20,7 +20,7 @@ export const MapVectorBackgroundSelector = (): JSX.Element => {
         options={MAP_BACKGROUND_TYPES}
         selectedId={backgroundType}
         onChange={handleChange}
-        width={100}
+        width={140}
       />
     </div>
   );
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
index 1563b536..2cdceec6 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
@@ -23,6 +23,8 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { mapDataSizeSelector } from '@/redux/map/map.selectors';
 import getDrawImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction';
 import { LayerState } from '@/redux/layers/layers.types';
+import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors';
+import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
 
 export const useOlMapAdditionalLayers = (
   mapInstance: MapInstance,
@@ -31,6 +33,7 @@ export const useOlMapAdditionalLayers = (
     VectorSource<Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>
   >
 > => {
+  const activeAction = useAppSelector(mapEditToolsActiveActionSelector);
   const dispatch = useAppDispatch();
   const mapSize = useSelector(mapDataSizeSelector);
   const currentModelId = useSelector(currentModelIdSelector);
@@ -106,11 +109,18 @@ export const useOlMapAdditionalLayers = (
       return;
     }
     mapInstance?.removeInteraction(drawImageInteraction);
-    if (!activeLayer || !vectorRendering) {
+    if (!activeLayer || !vectorRendering || activeAction !== MAP_EDIT_ACTIONS.DRAW_IMAGE) {
       return;
     }
     mapInstance?.addInteraction(drawImageInteraction);
-  }, [activeLayer, currentModelId, drawImageInteraction, mapInstance, vectorRendering]);
+  }, [
+    activeAction,
+    activeLayer,
+    currentModelId,
+    drawImageInteraction,
+    mapInstance,
+    vectorRendering,
+  ]);
 
   return vectorLayers;
 };
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
index a4f044ee..2b8d42c4 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
@@ -12,6 +12,7 @@ import { openLayerImageObjectFactoryModal } from '@/redux/modal/modal.slice';
 export default function getDrawImageInteraction(mapSize: MapSize, dispatch: AppDispatch): Draw {
   const drawImageInteraction = new Draw({
     type: 'Circle',
+    freehand: true,
     geometryFunction: (coordinates, geometry): SimpleGeometry => {
       const newGeometry = geometry || new Polygon([]);
       if (!Array.isArray(coordinates) || coordinates.length < 2) {
diff --git a/src/redux/mapEditTools/mapEditTools.constants.ts b/src/redux/mapEditTools/mapEditTools.constants.ts
new file mode 100644
index 00000000..3f54d2b0
--- /dev/null
+++ b/src/redux/mapEditTools/mapEditTools.constants.ts
@@ -0,0 +1,3 @@
+export const MAP_EDIT_ACTIONS = {
+  DRAW_IMAGE: 'DRAW_IMAGE',
+} as const;
diff --git a/src/redux/mapEditTools/mapEditTools.mock.ts b/src/redux/mapEditTools/mapEditTools.mock.ts
new file mode 100644
index 00000000..81dd0812
--- /dev/null
+++ b/src/redux/mapEditTools/mapEditTools.mock.ts
@@ -0,0 +1,5 @@
+import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types';
+
+export const MAP_EDIT_TOOLS_STATE_INITIAL_MOCK: MapEditToolsState = {
+  activeAction: null,
+};
diff --git a/src/redux/mapEditTools/mapEditTools.reducers.ts b/src/redux/mapEditTools/mapEditTools.reducers.ts
new file mode 100644
index 00000000..2b83e994
--- /dev/null
+++ b/src/redux/mapEditTools/mapEditTools.reducers.ts
@@ -0,0 +1,15 @@
+/* eslint-disable no-magic-numbers */
+import { PayloadAction } from '@reduxjs/toolkit';
+import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
+import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types';
+
+export const mapEditToolsSetActiveActionReducer = (
+  state: MapEditToolsState,
+  action: PayloadAction<keyof typeof MAP_EDIT_ACTIONS>,
+): void => {
+  if (state.activeAction !== action.payload) {
+    state.activeAction = action.payload;
+  } else {
+    state.activeAction = null;
+  }
+};
diff --git a/src/redux/mapEditTools/mapEditTools.selectors.ts b/src/redux/mapEditTools/mapEditTools.selectors.ts
new file mode 100644
index 00000000..545d0413
--- /dev/null
+++ b/src/redux/mapEditTools/mapEditTools.selectors.ts
@@ -0,0 +1,10 @@
+/* eslint-disable no-magic-numbers */
+import { createSelector } from '@reduxjs/toolkit';
+import { rootSelector } from '@/redux/root/root.selectors';
+
+export const mapEditToolsSelector = createSelector(rootSelector, state => state.mapEditTools);
+
+export const mapEditToolsActiveActionSelector = createSelector(
+  mapEditToolsSelector,
+  state => state.activeAction,
+);
diff --git a/src/redux/mapEditTools/mapEditTools.slice.ts b/src/redux/mapEditTools/mapEditTools.slice.ts
new file mode 100644
index 00000000..bea57d9c
--- /dev/null
+++ b/src/redux/mapEditTools/mapEditTools.slice.ts
@@ -0,0 +1,15 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock';
+import { mapEditToolsSetActiveActionReducer } from '@/redux/mapEditTools/mapEditTools.reducers';
+
+export const layersSlice = createSlice({
+  name: 'layers',
+  initialState: MAP_EDIT_TOOLS_STATE_INITIAL_MOCK,
+  reducers: {
+    mapEditToolsSetActiveAction: mapEditToolsSetActiveActionReducer,
+  },
+});
+
+export const { mapEditToolsSetActiveAction } = layersSlice.actions;
+
+export default layersSlice.reducer;
diff --git a/src/redux/mapEditTools/mapEditTools.types.ts b/src/redux/mapEditTools/mapEditTools.types.ts
new file mode 100644
index 00000000..8a000d1d
--- /dev/null
+++ b/src/redux/mapEditTools/mapEditTools.types.ts
@@ -0,0 +1,5 @@
+import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
+
+export type MapEditToolsState = {
+  activeAction: keyof typeof MAP_EDIT_ACTIONS | null;
+};
diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts
index e2724495..c90d96c3 100644
--- a/src/redux/root/root.fixtures.ts
+++ b/src/redux/root/root.fixtures.ts
@@ -8,6 +8,7 @@ import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelEl
 import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock';
 import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock';
 import { GLYPHS_STATE_INITIAL_MOCK } from '@/redux/glyphs/glyphs.mock';
+import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock';
 import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock';
 import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock';
 import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock';
@@ -73,4 +74,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
   markers: MARKERS_INITIAL_STATE_MOCK,
   entityNumber: ENTITY_NUMBER_INITIAL_STATE_MOCK,
   comment: COMMENT_INITIAL_STATE_MOCK,
+  mapEditTools: MAP_EDIT_TOOLS_STATE_INITIAL_MOCK,
 };
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 031b51be..0e3a85b9 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -23,6 +23,7 @@ import reactionsReducer from '@/redux/reactions/reactions.slice';
 import newReactionsReducer from '@/redux/newReactions/newReactions.slice';
 import searchReducer from '@/redux/search/search.slice';
 import userReducer from '@/redux/user/user.slice';
+import mapEditToolsReducer from '@/redux/mapEditTools/mapEditTools.slice';
 import {
   autocompleteChemicalReducer,
   autocompleteDrugReducer,
@@ -73,6 +74,7 @@ export const reducers = {
   contextMenu: contextMenuReducer,
   cookieBanner: cookieBannerReducer,
   user: userReducer,
+  mapEditTools: mapEditToolsReducer,
   configuration: configurationReducer,
   constant: constantReducer,
   overlayBioEntity: overlayBioEntityReducer,
diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx
index f22e882b..784cfb0e 100644
--- a/src/shared/Icon/Icon.component.tsx
+++ b/src/shared/Icon/Icon.component.tsx
@@ -18,6 +18,7 @@ import { PlusIcon } from '@/shared/Icon/Icons/PlusIcon';
 
 import type { IconComponentType, IconTypes } from '@/types/iconTypes';
 import { DownloadIcon } from '@/shared/Icon/Icons/DownloadIcon';
+import { ImageIcon } from '@/shared/Icon/Icons/ImageIcon';
 import { LocationIcon } from './Icons/LocationIcon';
 import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn';
 import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut';
@@ -59,6 +60,7 @@ const icons: Record<IconTypes, IconComponentType> = {
   clear: ClearIcon,
   user: UserIcon,
   'manage-user': ManageUserIcon,
+  image: ImageIcon,
 } as const;
 
 export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => {
diff --git a/src/shared/Icon/Icons/ImageIcon.tsx b/src/shared/Icon/Icons/ImageIcon.tsx
new file mode 100644
index 00000000..1c616b7f
--- /dev/null
+++ b/src/shared/Icon/Icons/ImageIcon.tsx
@@ -0,0 +1,25 @@
+interface ImageIconProps {
+  className?: string;
+}
+
+export const ImageIcon = ({ className }: ImageIconProps): JSX.Element => (
+  <svg
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    fill="none"
+    className={className}
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="1.5" />
+    <circle cx="8" cy="8" r="1.5" stroke="currentColor" strokeWidth="1.5" fill="none" />
+    <path
+      d="M4 18L9 13L12 16L16 12L20 18H4Z"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      fill="none"
+    />
+  </svg>
+);
diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts
index 76818394..0ef11e99 100644
--- a/src/types/iconTypes.ts
+++ b/src/types/iconTypes.ts
@@ -24,6 +24,7 @@ export type IconTypes =
   | 'user'
   | 'manage-user'
   | 'download'
-  | 'question';
+  | 'question'
+  | 'image';
 
 export type IconComponentType = ({ className }: { className: string }) => JSX.Element;
-- 
GitLab


From 7e162b10891eedf12c229940b41eb0e01507b6c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Fri, 20 Dec 2024 14:20:35 +0100
Subject: [PATCH 10/22] feat(autocomplete): add universal autocomplete
 component

---
 package-lock.json                             | 573 ++++++++++++++++--
 package.json                                  |   1 +
 .../LayerImageGlyphSelector.component.tsx     |  82 ---
 .../LayerImageGlyphSelector.styles.css        |  14 -
 ...LayerImageObjectFactoryModal.component.tsx |  14 +-
 .../Autocomplete/Autocomplete.component.tsx   |  46 ++
 .../Autocomplete/Autocomplete.styles.css      |   7 +
 src/shared/Autocomplete/index.ts              |   1 +
 8 files changed, 570 insertions(+), 168 deletions(-)
 delete mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx
 delete mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css
 create mode 100644 src/shared/Autocomplete/Autocomplete.component.tsx
 create mode 100644 src/shared/Autocomplete/Autocomplete.styles.css
 create mode 100644 src/shared/Autocomplete/index.ts

diff --git a/package-lock.json b/package-lock.json
index 93aa9055..6f42bd09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,7 @@
         "react-dom": "18.2.0",
         "react-dropzone": "14.2.3",
         "react-redux": "8.1.3",
+        "react-select": "5.9.0",
         "sonner": "1.4.3",
         "tailwind-merge": "1.14.0",
         "tailwindcss": "3.4.13",
@@ -136,7 +137,6 @@
       "version": "7.23.5",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
       "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
-      "dev": true,
       "dependencies": {
         "@babel/highlight": "^7.23.4",
         "chalk": "^2.4.2"
@@ -149,7 +149,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -161,7 +160,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -175,7 +173,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "dependencies": {
         "color-name": "1.1.3"
       }
@@ -183,14 +180,12 @@
     "node_modules/@babel/code-frame/node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
     },
     "node_modules/@babel/code-frame/node_modules/has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -199,7 +194,6 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
       "dependencies": {
         "has-flag": "^3.0.0"
       },
@@ -360,7 +354,6 @@
       "version": "7.22.15",
       "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
       "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
-      "dev": true,
       "dependencies": {
         "@babel/types": "^7.22.15"
       },
@@ -424,7 +417,6 @@
       "version": "7.23.4",
       "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
       "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
-      "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
@@ -433,7 +425,6 @@
       "version": "7.22.20",
       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
       "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
-      "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
@@ -465,7 +456,6 @@
       "version": "7.23.4",
       "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
       "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
-      "dev": true,
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
@@ -479,7 +469,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "dependencies": {
         "color-convert": "^1.9.0"
       },
@@ -491,7 +480,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -505,7 +493,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "dependencies": {
         "color-name": "1.1.3"
       }
@@ -513,14 +500,12 @@
     "node_modules/@babel/highlight/node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
     },
     "node_modules/@babel/highlight/node_modules/has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -529,7 +514,6 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
       "dependencies": {
         "has-flag": "^3.0.0"
       },
@@ -785,7 +769,6 @@
       "version": "7.23.6",
       "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
       "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
-      "dev": true,
       "dependencies": {
         "@babel/helper-string-parser": "^7.23.4",
         "@babel/helper-validator-identifier": "^7.22.20",
@@ -1217,6 +1200,133 @@
         "ms": "^2.1.1"
       }
     },
+    "node_modules/@emotion/babel-plugin": {
+      "version": "11.13.5",
+      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+      "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/runtime": "^7.18.3",
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/serialize": "^1.3.3",
+        "babel-plugin-macros": "^3.1.0",
+        "convert-source-map": "^1.5.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-root": "^1.1.0",
+        "source-map": "^0.5.7",
+        "stylis": "4.2.0"
+      }
+    },
+    "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+    },
+    "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@emotion/babel-plugin/node_modules/source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/@emotion/cache": {
+      "version": "11.14.0",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+      "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+      "dependencies": {
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/sheet": "^1.4.0",
+        "@emotion/utils": "^1.4.2",
+        "@emotion/weak-memoize": "^0.4.0",
+        "stylis": "4.2.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
+    },
+    "node_modules/@emotion/memoize": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+      "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
+    },
+    "node_modules/@emotion/react": {
+      "version": "11.14.0",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+      "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.13.5",
+        "@emotion/cache": "^11.14.0",
+        "@emotion/serialize": "^1.3.3",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+        "@emotion/utils": "^1.4.2",
+        "@emotion/weak-memoize": "^0.4.0",
+        "hoist-non-react-statics": "^3.3.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@emotion/serialize": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+      "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+      "dependencies": {
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/unitless": "^0.10.0",
+        "@emotion/utils": "^1.4.2",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/@emotion/sheet": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+      "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+      "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
+    },
+    "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+      "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@emotion/utils": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+      "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
+    },
+    "node_modules/@emotion/weak-memoize": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+      "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
+    },
     "node_modules/@eslint-community/eslint-utils": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -1289,6 +1399,28 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+      "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.8"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.6.12",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
+      "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
+      "dependencies": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.8"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.8",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+      "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
+    },
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.11.13",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -2435,6 +2567,11 @@
       "resolved": "https://registry.npmjs.org/@types/openlayers/-/openlayers-4.6.23.tgz",
       "integrity": "sha512-7jCPIa4D4LV03Rttae1AEqvkIN0+nc6Snz4IgA/IjsJD5O3ONxpscqIOdp1qAGuAsikR/ZC9vrPF9np8JRc6ig=="
     },
+    "node_modules/@types/parse-json": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+      "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
+    },
     "node_modules/@types/prop-types": {
       "version": "15.7.11",
       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
@@ -2484,6 +2621,14 @@
         "redux": "^4.0.0"
       }
     },
+    "node_modules/@types/react-transition-group": {
+      "version": "4.4.12",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+      "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+      "peerDependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/redux-mock-store": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.6.tgz",
@@ -3383,6 +3528,43 @@
         "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
       }
     },
+    "node_modules/babel-plugin-macros": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+      "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "cosmiconfig": "^7.0.0",
+        "resolve": "^1.19.0"
+      },
+      "engines": {
+        "node": ">=10",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/babel-plugin-macros/node_modules/cosmiconfig": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+      "dependencies": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.2.1",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.10.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/babel-plugin-macros/node_modules/yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/babel-preset-current-node-syntax": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
@@ -5044,6 +5226,15 @@
       "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
       "dev": true
     },
+    "node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "node_modules/domexception": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
@@ -5211,7 +5402,6 @@
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
       "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
       "dependencies": {
         "is-arrayish": "^0.2.1"
       }
@@ -5363,7 +5553,6 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -6569,8 +6758,7 @@
     "node_modules/find-root": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
-      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
-      "dev": true
+      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
     },
     "node_modules/find-up": {
       "version": "4.1.0",
@@ -7553,8 +7741,7 @@
     "node_modules/is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "dev": true
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
     },
     "node_modules/is-async-function": {
       "version": "2.0.0",
@@ -9292,8 +9479,7 @@
     "node_modules/json-parse-even-better-errors": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
-      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
-      "dev": true
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
     },
     "node_modules/json-schema": {
       "version": "0.4.0",
@@ -10294,6 +10480,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
+    },
     "node_modules/meow": {
       "version": "8.1.2",
       "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz",
@@ -10984,7 +11175,6 @@
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
       "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "dev": true,
       "dependencies": {
         "@babel/code-frame": "^7.0.0",
         "error-ex": "^1.3.1",
@@ -11811,6 +12001,26 @@
         }
       }
     },
+    "node_modules/react-select": {
+      "version": "5.9.0",
+      "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz",
+      "integrity": "sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.0",
+        "@emotion/cache": "^11.4.0",
+        "@emotion/react": "^11.8.1",
+        "@floating-ui/dom": "^1.0.1",
+        "@types/react-transition-group": "^4.4.0",
+        "memoize-one": "^6.0.0",
+        "prop-types": "^15.6.0",
+        "react-transition-group": "^4.3.0",
+        "use-isomorphic-layout-effect": "^1.2.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/react-themeable": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz",
@@ -11827,6 +12037,21 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -12908,6 +13133,11 @@
         }
       }
     },
+    "node_modules/stylis": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+      "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+    },
     "node_modules/sucrase": {
       "version": "3.35.0",
       "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -13173,7 +13403,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
       "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "dev": true,
       "engines": {
         "node": ">=4"
       }
@@ -13568,6 +13797,19 @@
         "react": ">=16.8.0"
       }
     },
+    "node_modules/use-isomorphic-layout-effect": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz",
+      "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -14177,7 +14419,6 @@
       "version": "7.23.5",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
       "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
-      "dev": true,
       "requires": {
         "@babel/highlight": "^7.23.4",
         "chalk": "^2.4.2"
@@ -14187,7 +14428,6 @@
           "version": "3.2.1",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
           "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
           "requires": {
             "color-convert": "^1.9.0"
           }
@@ -14196,7 +14436,6 @@
           "version": "2.4.2",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
           "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "dev": true,
           "requires": {
             "ansi-styles": "^3.2.1",
             "escape-string-regexp": "^1.0.5",
@@ -14207,7 +14446,6 @@
           "version": "1.9.3",
           "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
           "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-          "dev": true,
           "requires": {
             "color-name": "1.1.3"
           }
@@ -14215,20 +14453,17 @@
         "color-name": {
           "version": "1.1.3",
           "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-          "dev": true
+          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
         },
         "has-flag": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-          "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-          "dev": true
+          "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
         },
         "supports-color": {
           "version": "5.5.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
           "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
           }
@@ -14355,7 +14590,6 @@
       "version": "7.22.15",
       "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
       "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.22.15"
       }
@@ -14400,14 +14634,12 @@
     "@babel/helper-string-parser": {
       "version": "7.23.4",
       "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
-      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
-      "dev": true
+      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ=="
     },
     "@babel/helper-validator-identifier": {
       "version": "7.22.20",
       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
-      "dev": true
+      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
     },
     "@babel/helper-validator-option": {
       "version": "7.23.5",
@@ -14430,7 +14662,6 @@
       "version": "7.23.4",
       "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
       "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
-      "dev": true,
       "requires": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
@@ -14441,7 +14672,6 @@
           "version": "3.2.1",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
           "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
           "requires": {
             "color-convert": "^1.9.0"
           }
@@ -14450,7 +14680,6 @@
           "version": "2.4.2",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
           "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "dev": true,
           "requires": {
             "ansi-styles": "^3.2.1",
             "escape-string-regexp": "^1.0.5",
@@ -14461,7 +14690,6 @@
           "version": "1.9.3",
           "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
           "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-          "dev": true,
           "requires": {
             "color-name": "1.1.3"
           }
@@ -14469,20 +14697,17 @@
         "color-name": {
           "version": "1.1.3",
           "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-          "dev": true
+          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
         },
         "has-flag": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-          "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-          "dev": true
+          "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
         },
         "supports-color": {
           "version": "5.5.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
           "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
           }
@@ -14670,7 +14895,6 @@
       "version": "7.23.6",
       "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
       "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
-      "dev": true,
       "requires": {
         "@babel/helper-string-parser": "^7.23.4",
         "@babel/helper-validator-identifier": "^7.22.20",
@@ -15016,6 +15240,116 @@
         }
       }
     },
+    "@emotion/babel-plugin": {
+      "version": "11.13.5",
+      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+      "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+      "requires": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/runtime": "^7.18.3",
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/serialize": "^1.3.3",
+        "babel-plugin-macros": "^3.1.0",
+        "convert-source-map": "^1.5.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-root": "^1.1.0",
+        "source-map": "^0.5.7",
+        "stylis": "4.2.0"
+      },
+      "dependencies": {
+        "convert-source-map": {
+          "version": "1.9.0",
+          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+          "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+        },
+        "escape-string-regexp": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+          "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="
+        }
+      }
+    },
+    "@emotion/cache": {
+      "version": "11.14.0",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+      "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+      "requires": {
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/sheet": "^1.4.0",
+        "@emotion/utils": "^1.4.2",
+        "@emotion/weak-memoize": "^0.4.0",
+        "stylis": "4.2.0"
+      }
+    },
+    "@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
+    },
+    "@emotion/memoize": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+      "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
+    },
+    "@emotion/react": {
+      "version": "11.14.0",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+      "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.13.5",
+        "@emotion/cache": "^11.14.0",
+        "@emotion/serialize": "^1.3.3",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+        "@emotion/utils": "^1.4.2",
+        "@emotion/weak-memoize": "^0.4.0",
+        "hoist-non-react-statics": "^3.3.1"
+      }
+    },
+    "@emotion/serialize": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+      "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+      "requires": {
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/unitless": "^0.10.0",
+        "@emotion/utils": "^1.4.2",
+        "csstype": "^3.0.2"
+      }
+    },
+    "@emotion/sheet": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+      "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
+    },
+    "@emotion/unitless": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+      "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
+    },
+    "@emotion/use-insertion-effect-with-fallbacks": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+      "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+      "requires": {}
+    },
+    "@emotion/utils": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+      "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
+    },
+    "@emotion/weak-memoize": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+      "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
+    },
     "@eslint-community/eslint-utils": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -15068,6 +15402,28 @@
       "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
       "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A=="
     },
+    "@floating-ui/core": {
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+      "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+      "requires": {
+        "@floating-ui/utils": "^0.2.8"
+      }
+    },
+    "@floating-ui/dom": {
+      "version": "1.6.12",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
+      "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
+      "requires": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.8"
+      }
+    },
+    "@floating-ui/utils": {
+      "version": "0.2.8",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+      "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
+    },
     "@humanwhocodes/config-array": {
       "version": "0.11.13",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -15925,6 +16281,11 @@
       "resolved": "https://registry.npmjs.org/@types/openlayers/-/openlayers-4.6.23.tgz",
       "integrity": "sha512-7jCPIa4D4LV03Rttae1AEqvkIN0+nc6Snz4IgA/IjsJD5O3ONxpscqIOdp1qAGuAsikR/ZC9vrPF9np8JRc6ig=="
     },
+    "@types/parse-json": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+      "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
+    },
     "@types/prop-types": {
       "version": "15.7.11",
       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
@@ -15974,6 +16335,12 @@
         "redux": "^4.0.0"
       }
     },
+    "@types/react-transition-group": {
+      "version": "4.4.12",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+      "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+      "requires": {}
+    },
     "@types/redux-mock-store": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.6.tgz",
@@ -16606,6 +16973,35 @@
         "@types/babel__traverse": "^7.0.6"
       }
     },
+    "babel-plugin-macros": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+      "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+      "requires": {
+        "@babel/runtime": "^7.12.5",
+        "cosmiconfig": "^7.0.0",
+        "resolve": "^1.19.0"
+      },
+      "dependencies": {
+        "cosmiconfig": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+          "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+          "requires": {
+            "@types/parse-json": "^4.0.0",
+            "import-fresh": "^3.2.1",
+            "parse-json": "^5.0.0",
+            "path-type": "^4.0.0",
+            "yaml": "^1.10.0"
+          }
+        },
+        "yaml": {
+          "version": "1.10.2",
+          "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+          "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
+        }
+      }
+    },
     "babel-preset-current-node-syntax": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
@@ -17840,6 +18236,15 @@
       "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
       "dev": true
     },
+    "dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "requires": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "domexception": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
@@ -17972,7 +18377,6 @@
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
       "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
       "requires": {
         "is-arrayish": "^0.2.1"
       }
@@ -18102,8 +18506,7 @@
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
     },
     "escodegen": {
       "version": "2.1.0",
@@ -18933,8 +19336,7 @@
     "find-root": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
-      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
-      "dev": true
+      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
     },
     "find-up": {
       "version": "4.1.0",
@@ -19631,8 +20033,7 @@
     "is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "dev": true
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
     },
     "is-async-function": {
       "version": "2.0.0",
@@ -20872,8 +21273,7 @@
     "json-parse-even-better-errors": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
-      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
-      "dev": true
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
     },
     "json-schema": {
       "version": "0.4.0",
@@ -21615,6 +22015,11 @@
       "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
       "dev": true
     },
+    "memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
+    },
     "meow": {
       "version": "8.1.2",
       "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz",
@@ -22111,7 +22516,6 @@
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
       "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
         "error-ex": "^1.3.1",
@@ -22602,6 +23006,22 @@
         "use-sync-external-store": "^1.0.0"
       }
     },
+    "react-select": {
+      "version": "5.9.0",
+      "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz",
+      "integrity": "sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==",
+      "requires": {
+        "@babel/runtime": "^7.12.0",
+        "@emotion/cache": "^11.4.0",
+        "@emotion/react": "^11.8.1",
+        "@floating-ui/dom": "^1.0.1",
+        "@types/react-transition-group": "^4.4.0",
+        "memoize-one": "^6.0.0",
+        "prop-types": "^15.6.0",
+        "react-transition-group": "^4.3.0",
+        "use-isomorphic-layout-effect": "^1.2.0"
+      }
+    },
     "react-themeable": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz",
@@ -22617,6 +23037,17 @@
         }
       }
     },
+    "react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "requires": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      }
+    },
     "read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -23430,6 +23861,11 @@
         "client-only": "0.0.1"
       }
     },
+    "stylis": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+      "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+    },
     "sucrase": {
       "version": "3.35.0",
       "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -23629,8 +24065,7 @@
     "to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "dev": true
+      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
     },
     "to-regex-range": {
       "version": "5.0.1",
@@ -23903,6 +24338,12 @@
       "integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
       "requires": {}
     },
+    "use-isomorphic-layout-effect": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz",
+      "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==",
+      "requires": {}
+    },
     "use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
diff --git a/package.json b/package.json
index 213f3cd7..52f25ca9 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
     "react-dom": "18.2.0",
     "react-dropzone": "14.2.3",
     "react-redux": "8.1.3",
+    "react-select": "5.9.0",
     "sonner": "1.4.3",
     "tailwind-merge": "1.14.0",
     "tailwindcss": "3.4.13",
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx
deleted file mode 100644
index f5ae36b6..00000000
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React, { ReactElement, useEffect, useState } from 'react';
-import Autosuggest from 'react-autosuggest';
-import { Glyph } from '@/types/models';
-import './LayerImageGlyphSelector.styles.css';
-
-interface LayerImageGlyphSelectorProps {
-  glyphs: Glyph[];
-  selectedGlyph: number | null;
-  onGlyphSelect: (glyphId: number) => void;
-}
-
-const LayerImageGlyphSelector: React.FC<LayerImageGlyphSelectorProps> = ({
-  glyphs,
-  selectedGlyph,
-  onGlyphSelect,
-}) => {
-  const [searchValue, setSearchValue] = useState('');
-  const [suggestions, setSuggestions] = useState<Glyph[]>([]);
-
-  useEffect(() => {
-    if (selectedGlyph) {
-      setSearchValue(String(selectedGlyph));
-    } else {
-      setSearchValue('');
-    }
-  }, [selectedGlyph]);
-
-  const getSuggestions = (inputValue: string): Glyph[] => {
-    if (!inputValue) {
-      return glyphs;
-    }
-    const input = inputValue.trim().toLowerCase();
-    return glyphs.filter(glyph => String(glyph.file).toLowerCase().includes(input));
-  };
-
-  const getSuggestionValue = (suggestion: Glyph): string => String(suggestion.file);
-
-  const renderSuggestion = (suggestion: Glyph): ReactElement => (
-    <div className="cursor-pointer p-2">{suggestion.file}</div>
-  );
-
-  const onChange = (event: React.FormEvent, { newValue }: { newValue: string }): void => {
-    setSearchValue(newValue);
-  };
-
-  const onSuggestionsFetchRequested = ({ value }: { value: string }): void => {
-    setSuggestions(getSuggestions(value));
-  };
-
-  const onSuggestionsClearRequested = (): void => {
-    setSuggestions([]);
-  };
-
-  const onSuggestionSelected = (
-    event: React.FormEvent,
-    { suggestion }: { suggestion: Glyph },
-  ): void => {
-    onGlyphSelect(suggestion.id);
-    setSearchValue(String(suggestion.file));
-  };
-
-  const inputProps = {
-    placeholder: 'Select glyph...',
-    value: searchValue,
-    onChange,
-  };
-
-  return (
-    <Autosuggest
-      suggestions={suggestions}
-      onSuggestionsFetchRequested={onSuggestionsFetchRequested}
-      onSuggestionsClearRequested={onSuggestionsClearRequested}
-      shouldRenderSuggestions={() => true}
-      getSuggestionValue={getSuggestionValue}
-      renderSuggestion={renderSuggestion}
-      onSuggestionSelected={onSuggestionSelected}
-      inputProps={inputProps}
-    />
-  );
-};
-
-export default LayerImageGlyphSelector;
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css
deleted file mode 100644
index f1d46937..00000000
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.styles.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.react-autosuggest__suggestions-container {
-  position: absolute;
-  z-index: 1000;
-  max-height: 400px;
-  overflow-y: auto;
-}
-
-.react-autosuggest__input {
-  width: 100%;
-  height: 40px;
-  padding: 10px;
-  border: 1px solid #ccc;
-  border-radius: 4px 0 0 4px;
-}
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
index 5144ee8c..4bf27452 100644
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
@@ -8,7 +8,6 @@ import { BASE_NEW_API_URL } from '@/constants';
 import { apiPath } from '@/redux/apiPath';
 import { Input } from '@/shared/Input';
 import Image from 'next/image';
-import LayerImageGlyphSelector from '@/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageGlyphSelector.component';
 import { Glyph } from '@/types/models';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { currentModelIdSelector } from '@/redux/models/models.selectors';
@@ -22,6 +21,7 @@ import { LoadingIndicator } from '@/shared/LoadingIndicator';
 import './LayerImageObjectFactoryModal.styles.css';
 import { useMapInstance } from '@/utils/context/mapInstanceContext';
 import { layerAddImage } from '@/redux/layers/layers.slice';
+import { Autocomplete } from '@/shared/Autocomplete';
 
 export const LayerImageObjectFactoryModal: React.FC = () => {
   const glyphs: Glyph[] = useAppSelector(glyphsDataSelector);
@@ -38,7 +38,8 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
   const [isSending, setIsSending] = useState<boolean>(false);
   const [previewUrl, setPreviewUrl] = useState<string | null>(null);
 
-  const handleGlyphChange = (glyphId: number | null): void => {
+  const handleGlyphChange = (glyph: Glyph | null): void => {
+    const glyphId = glyph?.id || null;
     setSelectedGlyph(glyphId);
     if (!glyphId) {
       return;
@@ -139,10 +140,11 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
       <div className="grid grid-cols-2 gap-2">
         <div className="mb-4 flex flex-col gap-2">
           <span>Glyph:</span>
-          <LayerImageGlyphSelector
-            selectedGlyph={selectedGlyph}
-            glyphs={glyphs}
-            onGlyphSelect={handleGlyphChange}
+          <Autocomplete<Glyph>
+            options={glyphs}
+            valueKey="id"
+            labelKey="file"
+            onChange={handleGlyphChange}
           />
         </div>
         <div className="mb-4 flex flex-col gap-2">
diff --git a/src/shared/Autocomplete/Autocomplete.component.tsx b/src/shared/Autocomplete/Autocomplete.component.tsx
new file mode 100644
index 00000000..0b4cf3be
--- /dev/null
+++ b/src/shared/Autocomplete/Autocomplete.component.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import Select, { SingleValue } from 'react-select';
+import './Autocomplete.styles.css';
+
+type AutocompleteProps<T> = {
+  options: Array<T>;
+  valueKey?: keyof T;
+  labelKey?: keyof T;
+  placeholder?: string;
+  onChange: (value: T | null) => void;
+};
+
+type OptionType<T> = {
+  value: T[keyof T];
+  label: string;
+  originalOption: T;
+};
+
+export const Autocomplete = <T,>({
+  options,
+  valueKey = 'value' as keyof T,
+  labelKey = 'label' as keyof T,
+  placeholder = 'Select...',
+  onChange,
+}: AutocompleteProps<T>): React.JSX.Element => {
+  const formattedOptions = options.map(option => ({
+    value: option[valueKey],
+    label: option[labelKey] as string,
+    originalOption: option,
+  }));
+
+  const handleChange = (selectedOption: SingleValue<OptionType<T>>): void => {
+    onChange(selectedOption ? selectedOption.originalOption : null);
+  };
+
+  return (
+    <Select
+      options={formattedOptions}
+      onChange={handleChange}
+      placeholder={placeholder}
+      classNamePrefix="react-select"
+    />
+  );
+};
+
+Autocomplete.displayName = 'Autocomplete';
diff --git a/src/shared/Autocomplete/Autocomplete.styles.css b/src/shared/Autocomplete/Autocomplete.styles.css
new file mode 100644
index 00000000..49f417b1
--- /dev/null
+++ b/src/shared/Autocomplete/Autocomplete.styles.css
@@ -0,0 +1,7 @@
+.react-select__control {
+  height: 40px;
+}
+
+.react-select__menu {
+  margin: 0 !important;
+}
diff --git a/src/shared/Autocomplete/index.ts b/src/shared/Autocomplete/index.ts
new file mode 100644
index 00000000..f78074f3
--- /dev/null
+++ b/src/shared/Autocomplete/index.ts
@@ -0,0 +1 @@
+export { Autocomplete } from './Autocomplete.component';
-- 
GitLab


From 4c977837f3bb95cc821ec35ff3ed5e84efd21c6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Mon, 23 Dec 2024 09:58:00 +0100
Subject: [PATCH 11/22] fix(vector-map): correct conditions for map actions

---
 ...LayerImageObjectFactoryModal.component.tsx |  6 ++---
 src/components/Map/Map.component.tsx          | 24 +++++++++++++------
 .../MapActiveLayerSelector.component.tsx      |  2 +-
 src/models/glyphSchema.ts                     |  1 +
 4 files changed, 22 insertions(+), 11 deletions(-)

diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
index 4bf27452..5c054fb2 100644
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
@@ -143,7 +143,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
           <Autocomplete<Glyph>
             options={glyphs}
             valueKey="id"
-            labelKey="file"
+            labelKey="filename"
             onChange={handleGlyphChange}
           />
         </div>
@@ -164,8 +164,8 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
           <Image
             src={previewUrl}
             alt="image preview"
-            layout="fill"
-            objectFit="contain"
+            fill
+            style={{ objectFit: 'contain' }}
             className="rounded"
           />
         ) : (
diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx
index 3209e8ac..1ae88fa6 100644
--- a/src/components/Map/Map.component.tsx
+++ b/src/components/Map/Map.component.tsx
@@ -9,12 +9,26 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { vectorRenderingSelector } from '@/redux/models/models.selectors';
 import { MapAdditionalLogos } from '@/components/Map/MapAdditionalLogos';
 import { MapDrawActions } from '@/components/Map/MapDrawActions/MapDrawActions.component';
+import {
+  layersActiveLayerSelector,
+  layersForCurrentModelSelector,
+  layersVisibilityForCurrentModelSelector,
+} from '@/redux/layers/layers.selectors';
+import { useMemo } from 'react';
 import { MapAdditionalActions } from './MapAdditionalActions';
 import { MapAdditionalOptions } from './MapAdditionalOptions';
 import { PluginsDrawer } from './PluginsDrawer';
 
 export const Map = (): JSX.Element => {
   const vectorRendering = useAppSelector(vectorRenderingSelector);
+  const activeLayer = useAppSelector(layersActiveLayerSelector);
+  const layers = useAppSelector(layersForCurrentModelSelector);
+  const layersVisibility = useAppSelector(layersVisibilityForCurrentModelSelector);
+
+  const visibleLayersLength: number = useMemo(() => {
+    return layers.filter(layer => layersVisibility[layer.details.id]).length;
+  }, [layers, layersVisibility]);
+
   return (
     <div
       className="relative z-0 h-screen w-full overflow-hidden bg-black"
@@ -22,13 +36,9 @@ export const Map = (): JSX.Element => {
     >
       <MapViewer />
       {!vectorRendering && <MapAdditionalOptions />}
-      {vectorRendering && (
-        <>
-          <MapVectorBackgroundSelector />
-          <MapActiveLayerSelector />
-          <MapDrawActions />
-        </>
-      )}
+      {vectorRendering && <MapVectorBackgroundSelector />}
+      {vectorRendering && visibleLayersLength && <MapActiveLayerSelector />}
+      {vectorRendering && activeLayer && <MapDrawActions />}
       <Drawer />
       <PluginsDrawer />
       <Legend />
diff --git a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
index 0f6a8810..cd81e3df 100644
--- a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
+++ b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
@@ -46,7 +46,7 @@ export const MapActiveLayerSelector = (): JSX.Element => {
   }, [activeLayer, currentModelId, dispatch, options]);
 
   return (
-    <div className={twMerge('absolute right-6 top-[calc(64px+40px+84px)] z-10 flex')}>
+    <div className={twMerge('z-11 absolute right-6 top-[calc(64px+40px+84px)] flex')}>
       <Select options={options} selectedId={activeLayer} onChange={handleChange} width={140} />
     </div>
   );
diff --git a/src/models/glyphSchema.ts b/src/models/glyphSchema.ts
index eedb213a..319fd589 100644
--- a/src/models/glyphSchema.ts
+++ b/src/models/glyphSchema.ts
@@ -3,4 +3,5 @@ import { z } from 'zod';
 export const glyphSchema = z.object({
   id: z.number(),
   file: z.number(),
+  filename: z.string().optional().nullable(),
 });
-- 
GitLab


From 001d93a8f0f5572cf24bb0da522352debcedfae8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Mon, 23 Dec 2024 10:46:23 +0100
Subject: [PATCH 12/22] feat(image-preview): add test for ImagePreview
 component

---
 .../Image/ImagePreview/ImagePreview.test.tsx  | 47 +++++++++++++++++++
 1 file changed, 47 insertions(+)
 create mode 100644 src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx

diff --git a/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx b/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx
new file mode 100644
index 00000000..1baa4a67
--- /dev/null
+++ b/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import ImagePreview from './ImagePreview.component';
+
+const createMockFile = (name: string, type: string, content: string): File => {
+  return new File([content], name, { type });
+};
+
+describe('ImagePreview Component', () => {
+  it('should display "No Data Available" when no imageFile is provided', () => {
+    render(<ImagePreview />);
+    expect(screen.getByText(/No Data Available/i)).toBeInTheDocument();
+  });
+
+  it('should display the image when a File is provided', async () => {
+    const mockFile = createMockFile('test-image.jpg', 'image/jpeg', 'dummy content');
+
+    render(<ImagePreview imageFile={mockFile} />);
+
+    const imgElement = await waitFor(() => screen.getByRole('img', { name: 'Preview' }));
+
+    expect(imgElement).toBeInTheDocument();
+    expect(imgElement).toHaveAttribute('src');
+    expect(imgElement).toHaveAttribute('alt', 'Preview');
+  });
+
+  it('should display the image when a FileInterface object with a URL is provided', () => {
+    const mockFileInterface = { url: 'https://example.com/image.png' };
+
+    render(<ImagePreview imageFile={mockFileInterface} />);
+
+    const image = screen.getByAltText(/Preview/i);
+    expect(image).toHaveAttribute('src', 'https://example.com/image.png');
+  });
+
+  it('should update the image when imageFile changes', () => {
+    const { rerender } = render(<ImagePreview imageFile={null} />);
+
+    expect(screen.getByText(/No Data Available/i)).toBeInTheDocument();
+
+    const mockFileInterface = { url: 'https://example.com/image.png' };
+    rerender(<ImagePreview imageFile={mockFileInterface} />);
+
+    const image = screen.getByAltText(/Preview/i);
+    expect(image).toHaveAttribute('src', 'https://example.com/image.png');
+  });
+});
-- 
GitLab


From 1b49e54ee4e852ff92964fe24ecc2d0e80963743 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Tue, 7 Jan 2025 08:40:31 +0100
Subject: [PATCH 13/22] feat(layer-image): add test for
 LayerImageObjectFactoryModal

---
 .../ImagePreview/ImagePreview.component.tsx   |  57 -------
 .../Image/ImagePreview/ImagePreview.test.tsx  |  47 ------
 ...ImageObjectFactoryModal.component.test.tsx | 141 ++++++++++++++++++
 ...LayerImageObjectFactoryModal.component.tsx |   3 +-
 src/components/Map/Map.component.tsx          |   2 +-
 .../MapActiveLayerSelector.component.tsx      |   2 +-
 src/models/fixtures/layerImageFixture.ts      |   9 ++
 .../Autocomplete/Autocomplete.component.tsx   |  14 +-
 8 files changed, 162 insertions(+), 113 deletions(-)
 delete mode 100644 src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx
 delete mode 100644 src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx
 create mode 100644 src/models/fixtures/layerImageFixture.ts

diff --git a/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx b/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx
deleted file mode 100644
index d5b13b0a..00000000
--- a/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.component.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { useEffect, useMemo, useState } from 'react';
-
-interface FileInterface {
-  url: string;
-}
-
-interface ImagePreviewProps {
-  imageFile?: File | FileInterface | null;
-}
-
-const ImagePreview: React.FC<ImagePreviewProps> = ({ imageFile }) => {
-  const [imageSrc, setImageSrc] = useState<string | null>(null);
-
-  const previewImage = (file: File): void => {
-    const reader = new FileReader();
-    reader.onload = (event): void => {
-      if (event.target?.result && typeof event.target.result === 'string') {
-        setImageSrc(event.target.result);
-      }
-    };
-    reader.readAsDataURL(file);
-  };
-
-  const setImageFile = useMemo(() => {
-    return (): void => {
-      if (imageFile) {
-        if (imageFile instanceof File) {
-          previewImage(imageFile);
-        } else if ('url' in imageFile) {
-          setImageSrc(imageFile.url);
-        }
-      } else {
-        setImageSrc(null);
-      }
-    };
-  }, [imageFile]);
-
-  useEffect(() => {
-    setImageFile();
-  }, [imageFile, setImageFile]);
-
-  return (
-    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
-      {imageSrc ? (
-        <img
-          src={imageSrc}
-          alt="Preview"
-          style={{ maxHeight: '350px', borderRadius: '8px', objectFit: 'cover' }}
-        />
-      ) : (
-        <div>No Data Available</div>
-      )}
-    </div>
-  );
-};
-
-export default ImagePreview;
diff --git a/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx b/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx
deleted file mode 100644
index 1baa4a67..00000000
--- a/src/components/FunctionalArea/Image/ImagePreview/ImagePreview.test.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import ImagePreview from './ImagePreview.component';
-
-const createMockFile = (name: string, type: string, content: string): File => {
-  return new File([content], name, { type });
-};
-
-describe('ImagePreview Component', () => {
-  it('should display "No Data Available" when no imageFile is provided', () => {
-    render(<ImagePreview />);
-    expect(screen.getByText(/No Data Available/i)).toBeInTheDocument();
-  });
-
-  it('should display the image when a File is provided', async () => {
-    const mockFile = createMockFile('test-image.jpg', 'image/jpeg', 'dummy content');
-
-    render(<ImagePreview imageFile={mockFile} />);
-
-    const imgElement = await waitFor(() => screen.getByRole('img', { name: 'Preview' }));
-
-    expect(imgElement).toBeInTheDocument();
-    expect(imgElement).toHaveAttribute('src');
-    expect(imgElement).toHaveAttribute('alt', 'Preview');
-  });
-
-  it('should display the image when a FileInterface object with a URL is provided', () => {
-    const mockFileInterface = { url: 'https://example.com/image.png' };
-
-    render(<ImagePreview imageFile={mockFileInterface} />);
-
-    const image = screen.getByAltText(/Preview/i);
-    expect(image).toHaveAttribute('src', 'https://example.com/image.png');
-  });
-
-  it('should update the image when imageFile changes', () => {
-    const { rerender } = render(<ImagePreview imageFile={null} />);
-
-    expect(screen.getByText(/No Data Available/i)).toBeInTheDocument();
-
-    const mockFileInterface = { url: 'https://example.com/image.png' };
-    rerender(<ImagePreview imageFile={mockFileInterface} />);
-
-    const image = screen.getByAltText(/Preview/i);
-    expect(image).toHaveAttribute('src', 'https://example.com/image.png');
-  });
-});
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx
new file mode 100644
index 00000000..5806621f
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx
@@ -0,0 +1,141 @@
+/* eslint-disable no-magic-numbers */
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
+import { StoreType } from '@/redux/store';
+import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
+import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
+import { GLYPHS_STATE_INITIAL_MOCK } from '@/redux/glyphs/glyphs.mock';
+import { apiPath } from '@/redux/apiPath';
+import { HttpStatusCode } from 'axios';
+import { layerImageFixture } from '@/models/fixtures/layerImageFixture';
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import {
+  LAYER_STATE_DEFAULT_DATA,
+  LAYERS_STATE_INITIAL_LAYER_MOCK,
+} from '@/redux/layers/layers.mock';
+import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock';
+import { overlayFixture } from '@/models/fixtures/overlaysFixture';
+import { showToast } from '@/utils/showToast';
+import { LayerImageObjectFactoryModal } from './LayerImageObjectFactoryModal.component';
+
+const mockedAxiosNewClient = mockNetworkNewAPIResponse();
+
+const glyph = { id: 1, file: 23, filename: 'Glyph1.png' };
+
+jest.mock('../../../../utils/showToast');
+
+const renderComponent = (): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore({
+    ...INITIAL_STORE_STATE_MOCK,
+    glyphs: {
+      ...GLYPHS_STATE_INITIAL_MOCK,
+      data: [glyph],
+    },
+    layers: {
+      0: {
+        ...LAYERS_STATE_INITIAL_LAYER_MOCK,
+        data: {
+          ...LAYER_STATE_DEFAULT_DATA,
+          activeLayer: 1,
+        },
+      },
+    },
+    modal: {
+      isOpen: true,
+      modalTitle: overlayFixture.name,
+      modalName: 'edit-overlay',
+      editOverlayState: overlayFixture,
+      molArtState: {},
+      overviewImagesState: {},
+      errorReportState: {},
+      layerFactoryState: { id: undefined },
+      layerImageObjectFactoryState: {
+        x: 1,
+        y: 1,
+        width: 1,
+        height: 1,
+      },
+    },
+    models: {
+      ...MODELS_DATA_MOCK_WITH_MAIN_MAP,
+    },
+  });
+
+  return {
+    store,
+    ...render(
+      <Wrapper>
+        <LayerImageObjectFactoryModal />
+      </Wrapper>,
+    ),
+  };
+};
+
+describe('LayerImageObjectFactoryModal - component', () => {
+  it('should render LayerImageObjectFactoryModal component with initial state', () => {
+    renderComponent();
+
+    expect(screen.getByText(/Glyph:/i)).toBeInTheDocument();
+    expect(screen.getByText(/File:/i)).toBeInTheDocument();
+    expect(screen.getByText(/Submit/i)).toBeInTheDocument();
+    expect(screen.getByText(/No Image/i)).toBeInTheDocument();
+  });
+
+  it('should display a list of glyphs in the dropdown', async () => {
+    renderComponent();
+
+    const dropdown = screen.getByTestId('autocomplete');
+    if (!dropdown.firstChild) {
+      throw new Error('Dropdown does not have a firstChild');
+    }
+    fireEvent.keyDown(dropdown.firstChild, { key: 'ArrowDown' });
+    await waitFor(() => expect(screen.getByText(glyph.filename)).toBeInTheDocument());
+    fireEvent.click(screen.getByText(glyph.filename));
+  });
+
+  it('should update the selected glyph on dropdown change', async () => {
+    renderComponent();
+
+    const dropdown = screen.getByTestId('autocomplete');
+    if (!dropdown.firstChild) {
+      throw new Error('Dropdown does not have a firstChild');
+    }
+    fireEvent.keyDown(dropdown.firstChild, { key: 'ArrowDown' });
+    await waitFor(() => expect(screen.getByText(glyph.filename)).toBeInTheDocument());
+    fireEvent.click(screen.getByText(glyph.filename));
+
+    await waitFor(() => {
+      const imgPreview: HTMLImageElement = screen.getByTestId('layer-image-preview');
+      const decodedSrc = decodeURIComponent(imgPreview.src);
+      expect(decodedSrc).toContain(`glyphs/${glyph.id}/fileContent`);
+    });
+  });
+
+  it('should handle form submission correctly', async () => {
+    mockedAxiosNewClient
+      .onPost(apiPath.addLayerImageObject(0, 1))
+      .reply(HttpStatusCode.Ok, layerImageFixture);
+    renderComponent();
+
+    const submitButton = screen.getByText(/Submit/i);
+
+    await act(async () => {
+      fireEvent.click(submitButton);
+    });
+
+    expect(showToast).toHaveBeenCalledWith({
+      message: 'A new image object has been successfully added',
+      type: 'success',
+    });
+  });
+
+  it('should display "No Image" when there is no image file', () => {
+    const { store } = renderComponent();
+
+    store.dispatch({
+      type: 'glyphs/clearGlyphData',
+    });
+
+    expect(screen.getByText(/No Image/i)).toBeInTheDocument();
+  });
+});
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
index 5c054fb2..b750d061 100644
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
@@ -113,7 +113,6 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
           }
         }
       });
-
       showToast({
         type: 'success',
         message: 'A new image object has been successfully added',
@@ -154,6 +153,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
             type="file"
             accept="image/*"
             onChange={handleFileChange}
+            data-testid="image-file-input"
             className="w-full border border-[#ccc] bg-white p-2"
           />
         </div>
@@ -167,6 +167,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
             fill
             style={{ objectFit: 'contain' }}
             className="rounded"
+            data-testid="layer-image-preview"
           />
         ) : (
           <div className="text-gray-500">No Image</div>
diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx
index 1ae88fa6..e055ad95 100644
--- a/src/components/Map/Map.component.tsx
+++ b/src/components/Map/Map.component.tsx
@@ -38,7 +38,7 @@ export const Map = (): JSX.Element => {
       {!vectorRendering && <MapAdditionalOptions />}
       {vectorRendering && <MapVectorBackgroundSelector />}
       {vectorRendering && visibleLayersLength && <MapActiveLayerSelector />}
-      {vectorRendering && activeLayer && <MapDrawActions />}
+      {vectorRendering && activeLayer && visibleLayersLength && <MapDrawActions />}
       <Drawer />
       <PluginsDrawer />
       <Legend />
diff --git a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
index cd81e3df..c07d33c2 100644
--- a/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
+++ b/src/components/Map/MapActiveLayerSelector/MapActiveLayerSelector.component.tsx
@@ -46,7 +46,7 @@ export const MapActiveLayerSelector = (): JSX.Element => {
   }, [activeLayer, currentModelId, dispatch, options]);
 
   return (
-    <div className={twMerge('z-11 absolute right-6 top-[calc(64px+40px+84px)] flex')}>
+    <div className={twMerge('absolute right-6 top-[calc(64px+40px+84px)] flex')}>
       <Select options={options} selectedId={activeLayer} onChange={handleChange} width={140} />
     </div>
   );
diff --git a/src/models/fixtures/layerImageFixture.ts b/src/models/fixtures/layerImageFixture.ts
new file mode 100644
index 00000000..d386d544
--- /dev/null
+++ b/src/models/fixtures/layerImageFixture.ts
@@ -0,0 +1,9 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { layerImageSchema } from '@/models/layerImageSchema';
+
+export const layerImageFixture = createFixture(layerImageSchema, {
+  seed: ZOD_SEED,
+  array: { min: 1, max: 1 },
+});
diff --git a/src/shared/Autocomplete/Autocomplete.component.tsx b/src/shared/Autocomplete/Autocomplete.component.tsx
index 0b4cf3be..232dca19 100644
--- a/src/shared/Autocomplete/Autocomplete.component.tsx
+++ b/src/shared/Autocomplete/Autocomplete.component.tsx
@@ -34,12 +34,14 @@ export const Autocomplete = <T,>({
   };
 
   return (
-    <Select
-      options={formattedOptions}
-      onChange={handleChange}
-      placeholder={placeholder}
-      classNamePrefix="react-select"
-    />
+    <div data-testid="autocomplete">
+      <Select
+        options={formattedOptions}
+        onChange={handleChange}
+        placeholder={placeholder}
+        classNamePrefix="react-select"
+      />
+    </div>
   );
 };
 
-- 
GitLab


From c5261685f3d9c7ef80d2beb5b4a1d1d3b3813a8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Tue, 7 Jan 2025 09:00:28 +0100
Subject: [PATCH 14/22] feat(array-utils): add test for arrayToKeyValue

---
 src/components/SPA/MinervaSPA.component.tsx |  1 -
 src/redux/layers/layers.thunks.ts           |  2 +-
 src/utils/array/arrayToKeyValue.test.ts     | 46 +++++++++++++++++++++
 src/utils/array/arrayToKeyValue.ts          |  2 +-
 4 files changed, 48 insertions(+), 3 deletions(-)
 create mode 100644 src/utils/array/arrayToKeyValue.test.ts

diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx
index 2e9751e3..f086919b 100644
--- a/src/components/SPA/MinervaSPA.component.tsx
+++ b/src/components/SPA/MinervaSPA.component.tsx
@@ -24,7 +24,6 @@ export const MinervaSPA = (): JSX.Element => {
     <div className={twMerge('relative', manrope.variable)}>
       <FunctionalArea />
       <Map />
-      {/* <Modal /> */}
       <ContextMenu />
       <CookieBanner />
     </div>
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index 5a2ec2d4..f3f78c43 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -65,7 +65,7 @@ export const getLayersForModel = createAsyncThunk<
           rects: rectsResponse.data.content,
           ovals: ovalsResponse.data.content,
           lines: linesResponse.data.content,
-          images: arrayToKeyValue(imagesResponse.data.content, 'id'),
+          images: arrayToKeyValue(imagesResponse.data.content as Array<LayerImage>, 'id'),
         };
       }),
     );
diff --git a/src/utils/array/arrayToKeyValue.test.ts b/src/utils/array/arrayToKeyValue.test.ts
new file mode 100644
index 00000000..347d3f98
--- /dev/null
+++ b/src/utils/array/arrayToKeyValue.test.ts
@@ -0,0 +1,46 @@
+/* eslint-disable no-magic-numbers */
+import arrayToKeyValue from './arrayToKeyValue';
+
+describe('arrayToKeyValue', () => {
+  interface Person {
+    id: number;
+    name: string;
+    age: number;
+    isActive: boolean;
+  }
+
+  const people: Person[] = [
+    { id: 1, name: 'John', age: 30, isActive: true },
+    { id: 2, name: 'Anna', age: 25, isActive: false },
+    { id: 3, name: 'Peter', age: 28, isActive: true },
+  ];
+
+  it('create dict with key "id" and value "name"', () => {
+    const result = arrayToKeyValue(people, 'id');
+    expect(result).toEqual({
+      1: people[0],
+      2: people[1],
+      3: people[2],
+    });
+  });
+
+  it('create dict with key "name" and value "age"', () => {
+    const result = arrayToKeyValue(people, 'name');
+    expect(result).toEqual({
+      John: people[0],
+      Anna: people[1],
+      Peter: people[2],
+    });
+  });
+
+  it('handles duplicate keys, overwriting previous values', () => {
+    const duplicateData = [
+      { id: 1, name: 'John', age: 30, isActive: true },
+      { id: 1, name: 'Anna', age: 25, isActive: false },
+    ];
+    const result = arrayToKeyValue(duplicateData, 'id');
+    expect(result).toEqual({
+      1: duplicateData[1],
+    });
+  });
+});
diff --git a/src/utils/array/arrayToKeyValue.ts b/src/utils/array/arrayToKeyValue.ts
index 52c7cf2e..1b04bab5 100644
--- a/src/utils/array/arrayToKeyValue.ts
+++ b/src/utils/array/arrayToKeyValue.ts
@@ -1,4 +1,4 @@
-export default function arrayToKeyValue<T extends Record<string, never>, K extends keyof T>(
+export default function arrayToKeyValue<T, K extends keyof T>(
   array: T[],
   key: K,
 ): Record<T[K] & PropertyKey, T> {
-- 
GitLab


From b70371a2464f0f3ee795e2040a7f0aba31e782d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Tue, 7 Jan 2025 09:27:46 +0100
Subject: [PATCH 15/22] feat(autocomplete): add test for autocomplete

---
 .../Autocomplete.component.test.tsx           | 60 +++++++++++++++++++
 1 file changed, 60 insertions(+)
 create mode 100644 src/shared/Autocomplete/Autocomplete.component.test.tsx

diff --git a/src/shared/Autocomplete/Autocomplete.component.test.tsx b/src/shared/Autocomplete/Autocomplete.component.test.tsx
new file mode 100644
index 00000000..65662aed
--- /dev/null
+++ b/src/shared/Autocomplete/Autocomplete.component.test.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { Autocomplete } from './Autocomplete.component';
+
+interface Option {
+  id: number;
+  name: string;
+}
+
+describe('Autocomplete', () => {
+  const options: Option[] = [
+    { id: 1, name: 'Option 1' },
+    { id: 2, name: 'Option 2' },
+    { id: 3, name: 'Option 3' },
+  ];
+
+  it('renders the component with placeholder', () => {
+    render(
+      <Autocomplete
+        options={options}
+        valueKey="id"
+        labelKey="name"
+        placeholder="Select an option"
+        onChange={() => {}}
+      />,
+    );
+
+    const placeholder = screen.getByText('Select an option');
+    expect(placeholder).toBeInTheDocument();
+  });
+
+  it('displays options and handles selection', () => {
+    const handleChange = jest.fn();
+
+    render(
+      <Autocomplete
+        options={options}
+        valueKey="id"
+        labelKey="name"
+        placeholder="Select an option"
+        onChange={handleChange}
+      />,
+    );
+
+    const dropdown = screen.getByTestId('autocomplete');
+    if (!dropdown.firstChild) {
+      throw new Error('Dropdown does not have a firstChild');
+    }
+    fireEvent.keyDown(dropdown.firstChild, { key: 'ArrowDown' });
+
+    const option1 = screen.getByText('Option 1');
+    const option2 = screen.getByText('Option 2');
+    expect(option1).toBeInTheDocument();
+    expect(option2).toBeInTheDocument();
+
+    fireEvent.click(option1);
+
+    expect(handleChange).toHaveBeenCalledWith({ id: 1, name: 'Option 1' });
+  });
+});
-- 
GitLab


From 4636c288b3e72597430ebdf6713db1dfd59776f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Tue, 7 Jan 2025 10:13:28 +0100
Subject: [PATCH 16/22] feat(draw-image): add test for getDrawImageInteraction

---
 .../layer/getDrawImageInteraction.test.ts     | 66 +++++++++++++++++++
 1 file changed, 66 insertions(+)
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.test.ts

diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.test.ts
new file mode 100644
index 00000000..0687acb6
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.test.ts
@@ -0,0 +1,66 @@
+/* eslint-disable no-magic-numbers */
+import Draw from 'ol/interaction/Draw';
+import { latLngToPoint } from '@/utils/map/latLngToPoint';
+import modalReducer, { openLayerImageObjectFactoryModal } from '@/redux/modal/modal.slice';
+import { MapSize } from '@/redux/map/map.types';
+import {
+  createStoreInstanceUsingSliceReducer,
+  ToolkitStoreWithSingleSlice,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { ModalState } from '@/redux/modal/modal.types';
+import { DEFAULT_TILE_SIZE } from '@/constants/map';
+import { Map } from 'ol';
+import getDrawImageInteraction from './getDrawImageInteraction';
+
+jest.mock('../../../../../../../utils/map/latLngToPoint', () => ({
+  latLngToPoint: jest.fn(latLng => ({ x: latLng[0], y: latLng[1] })),
+}));
+
+describe('getDrawImageInteraction', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<ModalState>;
+  const mockDispatch = jest.fn(() => {});
+
+  let mapSize: MapSize;
+
+  beforeEach(() => {
+    mapSize = {
+      width: 800,
+      height: 600,
+      minZoom: 1,
+      maxZoom: 9,
+      tileSize: DEFAULT_TILE_SIZE,
+    };
+    store = createStoreInstanceUsingSliceReducer('modal', modalReducer);
+    store.dispatch = mockDispatch;
+  });
+
+  it('returns a Draw interaction', () => {
+    const drawInteraction = getDrawImageInteraction(mapSize, store.dispatch);
+    expect(drawInteraction).toBeInstanceOf(Draw);
+  });
+
+  it('dispatches openLayerImageObjectFactoryModal on drawend', () => {
+    const dummyElement = document.createElement('div');
+    const mapInstance = new Map({ target: dummyElement });
+    const drawInteraction = getDrawImageInteraction(mapSize, store.dispatch);
+    mapInstance.addInteraction(drawInteraction);
+    drawInteraction.appendCoordinates([
+      [0, 0],
+      [10, 10],
+    ]);
+
+    drawInteraction.finishDrawing();
+
+    expect(latLngToPoint).toHaveBeenCalledTimes(4);
+    expect(store.dispatch).toHaveBeenCalledWith(
+      openLayerImageObjectFactoryModal(
+        expect.objectContaining({
+          x: expect.any(Number),
+          y: expect.any(Number),
+          width: expect.any(Number),
+          height: expect.any(Number),
+        }),
+      ),
+    );
+  });
+});
-- 
GitLab


From 6a1f2a4a19287382211578481a1259eb0bef51eb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Tue, 7 Jan 2025 10:38:07 +0100
Subject: [PATCH 17/22] feat(map-draw-actions): add test for MapDrawActions

---
 .../MapDrawActions.component.test.tsx         | 51 +++++++++++++++++++
 1 file changed, 51 insertions(+)
 create mode 100644 src/components/Map/MapDrawActions/MapDrawActions.component.test.tsx

diff --git a/src/components/Map/MapDrawActions/MapDrawActions.component.test.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.test.tsx
new file mode 100644
index 00000000..dd9443f3
--- /dev/null
+++ b/src/components/Map/MapDrawActions/MapDrawActions.component.test.tsx
@@ -0,0 +1,51 @@
+import { MapDrawActions } from '@/components/Map/MapDrawActions/MapDrawActions.component';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
+import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice';
+import {
+  getReduxWrapperWithStore,
+  InitialStoreState,
+} from '@/utils/testing/getReduxWrapperWithStore';
+import { StoreType } from '@/redux/store';
+import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+
+jest.mock('../../../redux/hooks/useAppDispatch', () => ({
+  useAppDispatch: jest.fn(),
+}));
+
+const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
+
+  return (
+    render(
+      <Wrapper>
+        <MapDrawActions />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
+
+describe('MapDrawActions', () => {
+  const mockDispatch = jest.fn(() => {});
+
+  beforeEach(() => {
+    (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch);
+  });
+
+  it('renders the MapDrawActionsButton and toggles action on click', () => {
+    renderComponent({
+      mapEditTools: MAP_EDIT_TOOLS_STATE_INITIAL_MOCK,
+    });
+    const button = screen.getByRole('button', { name: /draw image/i });
+    expect(button).toBeInTheDocument();
+    fireEvent.click(button);
+
+    expect(mockDispatch).toHaveBeenCalledWith(
+      mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.DRAW_IMAGE),
+    );
+  });
+});
-- 
GitLab


From eb6d75505e64339d863a4f02df62990ce8648986 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Wed, 8 Jan 2025 15:02:09 +0100
Subject: [PATCH 18/22] feat(vector-map): add transform for layer image objects

---
 package-lock.json                             | 424 ++++++++++++++++++
 package.json                                  |   2 +
 .../MapDrawActions.component.tsx              |   6 +
 .../MapViewerVector/MapViewerVector.types.ts  |   7 +
 .../mouseLeftClick/onMapLeftClick.test.ts     |  15 +-
 .../mouseLeftClick/onMapLeftClick.ts          |   4 +-
 .../useOlMapAdditionalLayers.ts               |  39 +-
 .../reactionsLayer/processModelElements.ts    |   1 +
 .../shapes/coords/getBoundingBoxFromExtent.ts |  23 +
 .../utils/shapes/elements/Glyph.test.ts       |   8 +
 .../utils/shapes/elements/Glyph.ts            | 167 +++++--
 .../utils/shapes/layer/Layer.test.ts          |   8 +
 .../utils/shapes/layer/Layer.ts               |  31 +-
 .../shapes/layer/getDrawImageInteraction.ts   |  20 +-
 .../layer/getTransformImageInteraction.ts     |  59 +++
 src/redux/apiPath.ts                          |   2 +
 src/redux/layers/layers.reducers.ts           |  16 +
 src/redux/layers/layers.slice.ts              |   5 +-
 src/redux/layers/layers.thunks.ts             |  41 ++
 .../mapEditTools/mapEditTools.constants.ts    |   1 +
 src/shared/Icon/Icon.component.tsx            |   2 +
 src/shared/Icon/Icons/ResizeImageIcon.tsx     |  19 +
 src/types/iconTypes.ts                        |   3 +-
 23 files changed, 828 insertions(+), 75 deletions(-)
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.ts
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
 create mode 100644 src/shared/Icon/Icons/ResizeImageIcon.tsx

diff --git a/package-lock.json b/package-lock.json
index 6f42bd09..b626982e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
         "molart": "1.15.0",
         "next": "13.4.19",
         "ol": "10.2.0",
+        "ol-ext": "4.0.24",
         "polished": "4.3.1",
         "postcss": "8.4.29",
         "query-string": "7.1.3",
@@ -56,6 +57,7 @@
         "@types/crypto-js": "4.2.2",
         "@types/is-uuid": "1.0.2",
         "@types/jest": "29.5.11",
+        "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.6.1",
         "@types/react-autosuggest": "^10.1.11",
         "@types/react-redux": "7.1.33",
         "@types/redux-mock-store": "1.0.6",
@@ -2562,6 +2564,16 @@
       "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
       "dev": true
     },
+    "node_modules/@types/ol-ext": {
+      "name": "@siedlerchr/types-ol-ext",
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@siedlerchr/types-ol-ext/-/types-ol-ext-3.6.1.tgz",
+      "integrity": "sha512-CSqgXdS1d028TvnVXf2/dgnU66lRGHM9+R5E8lL2ilmm6ktHVZtsIhRR8rWSXu4Ybt1rGdMbwbDxofIbilWIyQ==",
+      "dev": true,
+      "peerDependencies": {
+        "jspdf": "^2.5.2"
+      }
+    },
     "node_modules/@types/openlayers": {
       "version": "4.6.23",
       "resolved": "https://registry.npmjs.org/@types/openlayers/-/openlayers-4.6.23.tgz",
@@ -2577,6 +2589,14 @@
       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
       "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
     },
+    "node_modules/@types/raf": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+      "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "node_modules/@types/rbush": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.3.tgz",
@@ -3317,6 +3337,19 @@
         "node": ">= 4.0.0"
       }
     },
+    "node_modules/atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true,
+      "peer": true,
+      "bin": {
+        "atob": "bin/atob.js"
+      },
+      "engines": {
+        "node": ">= 4.5.0"
+      }
+    },
     "node_modules/attr-accept": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
@@ -3609,6 +3642,17 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "node_modules/base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3729,6 +3773,19 @@
         "node-int64": "^0.4.0"
       }
     },
+    "node_modules/btoa": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+      "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+      "dev": true,
+      "peer": true,
+      "bin": {
+        "btoa": "bin/btoa.js"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
     "node_modules/buffer": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -3892,6 +3949,35 @@
         }
       ]
     },
+    "node_modules/canvg": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
+      "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@types/raf": "^3.4.0",
+        "core-js": "^3.8.3",
+        "raf": "^3.4.1",
+        "regenerator-runtime": "^0.13.7",
+        "rgbcolor": "^1.0.1",
+        "stackblur-canvas": "^2.0.0",
+        "svg-pathdata": "^6.0.3"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/canvg/node_modules/regenerator-runtime": {
+      "version": "0.13.11",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "node_modules/caseless": {
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@@ -4519,6 +4605,19 @@
       "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
       "dev": true
     },
+    "node_modules/core-js": {
+      "version": "3.40.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
+      "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "peer": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -4610,6 +4709,17 @@
       "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
       "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
     },
+    "node_modules/css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/css.escape": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -5248,6 +5358,14 @@
         "node": ">=12"
       }
     },
+    "node_modules/dompurify": {
+      "version": "2.5.8",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
+      "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "node_modules/dot-prop": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
@@ -6684,6 +6802,13 @@
         "pend": "~1.2.0"
       }
     },
+    "node_modules/fflate": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+      "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+      "dev": true,
+      "peer": true
+    },
     "node_modules/figures": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -7399,6 +7524,21 @@
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "node_modules/html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/http-proxy-agent": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
@@ -9552,6 +9692,25 @@
         "node": "*"
       }
     },
+    "node_modules/jspdf": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
+      "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "atob": "^2.1.2",
+        "btoa": "^1.2.1",
+        "fflate": "^0.8.1"
+      },
+      "optionalDependencies": {
+        "canvg": "^3.0.6",
+        "core-js": "^3.6.0",
+        "dompurify": "^2.5.4",
+        "html2canvas": "^1.0.0-rc.5"
+      }
+    },
     "node_modules/jsprim": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -11001,6 +11160,14 @@
         "url": "https://opencollective.com/openlayers"
       }
     },
+    "node_modules/ol-ext": {
+      "version": "4.0.24",
+      "resolved": "https://registry.npmjs.org/ol-ext/-/ol-ext-4.0.24.tgz",
+      "integrity": "sha512-VEf1+mjvbe35mMsszVsugqcvWfeGcU8TwS+GgXm3nGYqiHR7CckX2DWmM9B94QCDnrJWKKXBicfInbkoe2xT7w==",
+      "peerDependencies": {
+        "ol": ">= 5.3.0"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -11837,6 +12004,17 @@
       "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
       "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
     },
+    "node_modules/raf": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+      "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "performance-now": "^2.1.0"
+      }
+    },
     "node_modules/randexp": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz",
@@ -12408,6 +12586,17 @@
       "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
       "dev": true
     },
+    "node_modules/rgbcolor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+      "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 0.8.15"
+      }
+    },
     "node_modules/rimraf": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -12848,6 +13037,17 @@
         "node": ">=8"
       }
     },
+    "node_modules/stackblur-canvas": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+      "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=0.1.14"
+      }
+    },
     "node_modules/stop-iteration-iterator": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@@ -13224,6 +13424,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/svg-pathdata": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+      "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/symbol-tree": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -13333,6 +13544,17 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -13823,6 +14045,17 @@
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
+    "node_modules/utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "node_modules/uuid": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@@ -16276,6 +16509,13 @@
       "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
       "dev": true
     },
+    "@types/ol-ext": {
+      "version": "npm:@siedlerchr/types-ol-ext@3.6.1",
+      "resolved": "https://registry.npmjs.org/@siedlerchr/types-ol-ext/-/types-ol-ext-3.6.1.tgz",
+      "integrity": "sha512-CSqgXdS1d028TvnVXf2/dgnU66lRGHM9+R5E8lL2ilmm6ktHVZtsIhRR8rWSXu4Ybt1rGdMbwbDxofIbilWIyQ==",
+      "dev": true,
+      "requires": {}
+    },
     "@types/openlayers": {
       "version": "4.6.23",
       "resolved": "https://registry.npmjs.org/@types/openlayers/-/openlayers-4.6.23.tgz",
@@ -16291,6 +16531,14 @@
       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
       "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
     },
+    "@types/raf": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+      "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "@types/rbush": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.3.tgz",
@@ -16824,6 +17072,13 @@
       "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
       "dev": true
     },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true,
+      "peer": true
+    },
     "attr-accept": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
@@ -17037,6 +17292,14 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -17117,6 +17380,13 @@
         "node-int64": "^0.4.0"
       }
     },
+    "btoa": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+      "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+      "dev": true,
+      "peer": true
+    },
     "buffer": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -17218,6 +17488,34 @@
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz",
       "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw=="
     },
+    "canvg": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
+      "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "@babel/runtime": "^7.12.5",
+        "@types/raf": "^3.4.0",
+        "core-js": "^3.8.3",
+        "raf": "^3.4.1",
+        "regenerator-runtime": "^0.13.7",
+        "rgbcolor": "^1.0.1",
+        "stackblur-canvas": "^2.0.0",
+        "svg-pathdata": "^6.0.3"
+      },
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.13.11",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+          "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+          "dev": true,
+          "optional": true,
+          "peer": true
+        }
+      }
+    },
     "caseless": {
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@@ -17692,6 +17990,14 @@
       "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
       "dev": true
     },
+    "core-js": {
+      "version": "3.40.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
+      "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -17752,6 +18058,17 @@
       "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
       "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
     },
+    "css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
     "css.escape": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -18254,6 +18571,14 @@
         "webidl-conversions": "^7.0.0"
       }
     },
+    "dompurify": {
+      "version": "2.5.8",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
+      "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "dot-prop": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
@@ -19280,6 +19605,13 @@
         "pend": "~1.2.0"
       }
     },
+    "fflate": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+      "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+      "dev": true,
+      "peer": true
+    },
     "figures": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -19792,6 +20124,18 @@
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      }
+    },
     "http-proxy-agent": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
@@ -21332,6 +21676,23 @@
         "through": ">=2.2.7 <3"
       }
     },
+    "jspdf": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
+      "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
+      "dev": true,
+      "peer": true,
+      "requires": {
+        "@babel/runtime": "^7.23.2",
+        "atob": "^2.1.2",
+        "btoa": "^1.2.1",
+        "canvg": "^3.0.6",
+        "core-js": "^3.6.0",
+        "dompurify": "^2.5.4",
+        "fflate": "^0.8.1",
+        "html2canvas": "^1.0.0-rc.5"
+      }
+    },
     "jsprim": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -22385,6 +22746,12 @@
         "rbush": "^4.0.0"
       }
     },
+    "ol-ext": {
+      "version": "4.0.24",
+      "resolved": "https://registry.npmjs.org/ol-ext/-/ol-ext-4.0.24.tgz",
+      "integrity": "sha512-VEf1+mjvbe35mMsszVsugqcvWfeGcU8TwS+GgXm3nGYqiHR7CckX2DWmM9B94QCDnrJWKKXBicfInbkoe2xT7w==",
+      "requires": {}
+    },
     "once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -22905,6 +23272,17 @@
       "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
       "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
     },
+    "raf": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+      "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "performance-now": "^2.1.0"
+      }
+    },
     "randexp": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz",
@@ -23329,6 +23707,14 @@
       "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
       "dev": true
     },
+    "rgbcolor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+      "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "rimraf": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -23658,6 +24044,14 @@
         }
       }
     },
+    "stackblur-canvas": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+      "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "stop-iteration-iterator": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@@ -23923,6 +24317,14 @@
       "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
       "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
     },
+    "svg-pathdata": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+      "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
     "symbol-tree": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -24005,6 +24407,17 @@
       "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==",
       "dev": true
     },
+    "text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
     "text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -24355,6 +24768,17 @@
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
     },
+    "utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "requires": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "uuid": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
diff --git a/package.json b/package.json
index 52f25ca9..1d5e4494 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
     "molart": "1.15.0",
     "next": "13.4.19",
     "ol": "10.2.0",
+    "ol-ext": "4.0.24",
     "polished": "4.3.1",
     "postcss": "8.4.29",
     "query-string": "7.1.3",
@@ -62,6 +63,7 @@
     "zod-to-json-schema": "3.22.4"
   },
   "devDependencies": {
+    "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.6.1",
     "@commitlint/cli": "17.8.1",
     "@commitlint/config-conventional": "17.8.1",
     "@testing-library/jest-dom": "6.1.6",
diff --git a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
index 3d02bee2..69110b76 100644
--- a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
+++ b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
@@ -22,6 +22,12 @@ export const MapDrawActions = (): React.JSX.Element => {
         icon="image"
         title="Draw image"
       />
+      <MapDrawActionsButton
+        isActive={activeAction === MAP_EDIT_ACTIONS.TRANSFORM_IMAGE}
+        toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.TRANSFORM_IMAGE)}
+        icon="resize-image"
+        title="Transform image"
+      />
     </div>
   );
 };
diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts
index 63fddd60..709336b1 100644
--- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts
@@ -15,3 +15,10 @@ export type ScaleFunction = (resolution: number) => number;
 export type OverlayBioEntityGroupedElementsType = {
   [id: string]: Array<OverlayBioEntityRender & { amount: number }>;
 };
+
+export type BoundingBox = {
+  x: number;
+  y: number;
+  width: number;
+  height: number;
+};
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts
index fb720a27..11211b8b 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts
@@ -7,10 +7,11 @@ import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/
 import Map from 'ol/Map';
 import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick';
 import { Comment } from '@/types/models';
-import { Layer } from 'ol/layer';
 import SimpleGeometry from 'ol/geom/SimpleGeometry';
 import { Feature } from 'ol';
 import { FEATURE_TYPE } from '@/constants/features';
+import VectorLayer from 'ol/layer/Vector';
+import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
 import * as leftClickHandleAlias from './leftClickHandleAlias';
 import * as clickHandleReaction from '../clickHandleReaction';
 
@@ -33,6 +34,8 @@ describe('onMapLeftClick', () => {
   const isResultDrawerOpen = true;
   const comments: Array<Comment> = [];
   let mapInstance: Map;
+  const vectorLayer = new VectorLayer({});
+  vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE);
   const event = { coordinate: [100, 50], pixel: [200, 100] };
   const mapSize = {
     width: 90,
@@ -51,11 +54,7 @@ describe('onMapLeftClick', () => {
   it('dispatches updateLastClick and resets data if no feature at pixel', async () => {
     const dispatch = jest.fn();
     jest.spyOn(mapInstance, 'forEachFeatureAtPixel').mockImplementation((_, callback) => {
-      callback(
-        new Feature({ zIndex: 1 }),
-        null as unknown as Layer,
-        null as unknown as SimpleGeometry,
-      );
+      callback(new Feature({ zIndex: 1 }), vectorLayer, null as unknown as SimpleGeometry);
     });
     await onMapLeftClick(
       mapSize,
@@ -80,7 +79,7 @@ describe('onMapLeftClick', () => {
     }));
     const feature = new Feature({ id: 1, type: FEATURE_TYPE.ALIAS, zIndex: 1 });
     jest.spyOn(mapInstance, 'forEachFeatureAtPixel').mockImplementation((_, callback) => {
-      callback(feature, null as unknown as Layer, null as unknown as SimpleGeometry);
+      callback(feature, vectorLayer, null as unknown as SimpleGeometry);
     });
     (handleFeaturesClick as jest.Mock).mockReturnValue({ shouldBlockCoordSearch: false });
 
@@ -104,7 +103,7 @@ describe('onMapLeftClick', () => {
     }));
     const feature = new Feature({ id: 1, type: FEATURE_TYPE.REACTION, zIndex: 1 });
     jest.spyOn(mapInstance, 'forEachFeatureAtPixel').mockImplementation((_, callback) => {
-      callback(feature, null as unknown as Layer, null as unknown as SimpleGeometry);
+      callback(feature, vectorLayer, null as unknown as SimpleGeometry);
     });
     (handleFeaturesClick as jest.Mock).mockReturnValue({ shouldBlockCoordSearch: false });
 
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts
index 3a33837b..d9a8031c 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts
@@ -15,6 +15,7 @@ import { resetReactionsData } from '@/redux/reactions/reactions.slice';
 import { handleDataReset } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset';
 import { FEATURE_TYPE } from '@/constants/features';
 import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction';
+import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
 
 function isFeatureFilledCompartment(feature: FeatureLike): boolean {
   return feature.get('type') === FEATURE_TYPE.COMPARTMENT && feature.get('filled');
@@ -50,9 +51,10 @@ export const onMapLeftClick =
       let featureAtPixel: FeatureLike | undefined;
       mapInstance.forEachFeatureAtPixel(
         pixel,
-        feature => {
+        (feature, layer) => {
           const featureZIndex = feature.get('zIndex');
           if (
+            layer && layer.get('type') === VECTOR_MAP_LAYER_TYPE &&
             (isFeatureFilledCompartment(feature) || isFeatureNotCompartment(feature)) &&
             (featureZIndex === undefined || featureZIndex >= 0)
           ) {
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
index 2cdceec6..026c093e 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts
@@ -1,5 +1,5 @@
 /* eslint-disable no-magic-numbers */
-import { Feature } from 'ol';
+import { Collection, Feature } from 'ol';
 import VectorLayer from 'ol/layer/Vector';
 import VectorSource from 'ol/source/Vector';
 import { useEffect, useMemo, useState } from 'react';
@@ -15,7 +15,7 @@ import {
 } from '@/redux/layers/layers.selectors';
 import { usePointToProjection } from '@/utils/map/usePointToProjection';
 import { MapInstance } from '@/types/map';
-import { LineString, MultiPolygon, Point } from 'ol/geom';
+import { Geometry, LineString, MultiPolygon, Point } from 'ol/geom';
 import Polygon from 'ol/geom/Polygon';
 import Layer from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer';
 import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors';
@@ -25,6 +25,7 @@ import getDrawImageInteraction from '@/components/Map/MapViewer/MapViewerVector/
 import { LayerState } from '@/redux/layers/layers.types';
 import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors';
 import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
+import getTransformImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction';
 
 export const useOlMapAdditionalLayers = (
   mapInstance: MapInstance,
@@ -80,11 +81,12 @@ export const useOlMapAdditionalLayers = (
         lineTypes,
         arrowTypes,
         mapInstance,
+        mapSize,
         pointToProjection,
       });
       return additionalLayer.vectorLayer;
     });
-  }, [layersState, lineTypes, arrowTypes, mapInstance, pointToProjection]);
+  }, [layersState, lineTypes, arrowTypes, mapInstance, mapSize, pointToProjection]);
 
   useEffect(() => {
     if (layersLoading === 'pending') {
@@ -95,6 +97,24 @@ export const useOlMapAdditionalLayers = (
     }
   }, [layersForCurrentModel, layersLoading, layersLoadingState]);
 
+  const transformInteraction = useMemo(() => {
+    if (!dispatch || !currentModelId || !activeLayer) {
+      return null;
+    }
+    let imagesFeatures: Collection<Feature<Geometry>> = new Collection();
+    const vectorLayer = vectorLayers.find(layer => layer.get('id') === activeLayer);
+    if (vectorLayer) {
+      imagesFeatures = new Collection(vectorLayer.get('imagesFeatures'));
+    }
+    return getTransformImageInteraction(
+      dispatch,
+      mapSize,
+      currentModelId,
+      activeLayer,
+      imagesFeatures,
+    );
+  }, [dispatch, mapSize, currentModelId, activeLayer, vectorLayers]);
+
   useEffect(() => {
     vectorLayers.forEach(layer => {
       const layerId = layer.get('id');
@@ -104,6 +124,19 @@ export const useOlMapAdditionalLayers = (
     });
   }, [layersVisibilityForCurrentModel, vectorLayers]);
 
+  useEffect(() => {
+    if (!transformInteraction) {
+      return () => {};
+    }
+    if (!activeLayer || !vectorRendering || activeAction !== MAP_EDIT_ACTIONS.TRANSFORM_IMAGE) {
+      return () => {};
+    }
+    mapInstance?.addInteraction(transformInteraction);
+    return () => {
+      mapInstance?.removeInteraction(transformInteraction);
+    };
+  }, [activeAction, activeLayer, mapInstance, transformInteraction, vectorRendering]);
+
   useEffect(() => {
     if (!drawImageInteraction) {
       return;
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts
index 2f5b97ba..60746292 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts
@@ -46,6 +46,7 @@ export default function processModelElements(
         zIndex: element.z,
         pointToProjection,
         mapInstance,
+        mapSize,
       });
       validElements.push(glyph);
       return;
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.ts
new file mode 100644
index 00000000..efab1e2d
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.ts
@@ -0,0 +1,23 @@
+/* eslint-disable no-magic-numbers */
+import { MapSize } from '@/redux/map/map.types';
+import { toLonLat } from 'ol/proj';
+import { latLngToPoint } from '@/utils/map/latLngToPoint';
+import { Extent } from 'ol/extent';
+import { BoundingBox } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
+
+export default function getBoundingBoxFromExtent(extent: Extent, mapSize: MapSize): BoundingBox {
+  const [startLng, startLat] = toLonLat([extent[0], extent[3]]);
+  const startPoint = latLngToPoint([startLat, startLng], mapSize);
+  const [endLng, endLat] = toLonLat([extent[2], extent[1]]);
+  const endPoint = latLngToPoint([endLat, endLng], mapSize);
+
+  const width = Math.abs(endPoint.x - startPoint.x);
+  const height = Math.abs(endPoint.y - startPoint.y);
+
+  return {
+    width,
+    height,
+    x: startPoint.x,
+    y: startPoint.y,
+  };
+}
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
index 6ac8c5d2..bef1e037 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
@@ -13,6 +13,13 @@ describe('Glyph', () => {
   let glyph: Glyph;
   let mapInstance: MapInstance;
   let pointToProjectionMock: jest.MockedFunction<UsePointToProjectionResult>;
+  const mapSize = {
+    width: 90,
+    height: 90,
+    tileSize: 256,
+    minZoom: 2,
+    maxZoom: 9,
+  };
 
   beforeEach(() => {
     const dummyElement = document.createElement('div');
@@ -37,6 +44,7 @@ describe('Glyph', () => {
       zIndex: 1,
       pointToProjection: pointToProjectionMock,
       mapInstance,
+      mapSize,
     };
     glyph = new Glyph(props);
   });
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
index c7844ae3..53f71dc9 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
@@ -15,6 +15,9 @@ import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/st
 import { WHITE_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
 import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill';
 import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle';
+import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent';
+import { MapSize } from '@/redux/map/map.types';
+import { LayerImage } from '@/types/models';
 
 export type GlyphProps = {
   elementId: number;
@@ -26,6 +29,7 @@ export type GlyphProps = {
   zIndex: number;
   pointToProjection: UsePointToProjectionResult;
   mapInstance: MapInstance;
+  mapSize: MapSize;
 };
 
 export default class Glyph {
@@ -39,6 +43,10 @@ export default class Glyph {
 
   polygonStyle: Style;
 
+  polygon: Polygon = new Polygon([]);
+
+  elementId: number;
+
   width: number;
 
   height: number;
@@ -47,6 +55,10 @@ export default class Glyph {
 
   y: number;
 
+  zIndex: number;
+
+  glyphId: number | null;
+
   widthOnMap: number;
 
   heightOnMap: number;
@@ -55,8 +67,20 @@ export default class Glyph {
 
   minResolution: number;
 
+  imageWidth: number = 1;
+
+  imageHeight: number = 1;
+
+  imageWidthOnMap: number = 1;
+
+  imageHeightOnMap: number = 1;
+
+  mapInstance: MapInstance;
+
   pointToProjection: UsePointToProjectionResult;
 
+  mapSize: MapSize;
+
   constructor({
     elementId,
     glyphId,
@@ -67,11 +91,17 @@ export default class Glyph {
     zIndex,
     pointToProjection,
     mapInstance,
+    mapSize,
   }: GlyphProps) {
+    this.elementId = elementId;
     this.width = width;
     this.height = height;
+    this.mapSize = mapSize;
+    this.glyphId = glyphId;
     this.x = x;
     this.y = y;
+    this.zIndex = zIndex;
+    this.mapInstance = mapInstance;
     this.pointToProjection = pointToProjection;
     const point1 = this.pointToProjection({ x: 0, y: 0 });
     const point2 = this.pointToProjection({ x: this.width, y: this.height });
@@ -81,26 +111,19 @@ export default class Glyph {
     const maxZoom = mapInstance?.getView().get('originalMaxZoom');
     this.minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom) || 1;
     this.pixelRatio = this.widthOnMap / this.minResolution / this.width;
-    const polygon = new Polygon([
-      [
-        pointToProjection({ x, y }),
-        pointToProjection({ x: x + width, y }),
-        pointToProjection({ x: x + width, y: y + height }),
-        pointToProjection({ x, y: y + height }),
-        pointToProjection({ x, y }),
-      ],
-    ]);
+
+    this.drawPolygon();
 
     this.polygonStyle = getStyle({
-      geometry: polygon,
-      zIndex,
+      geometry: this.polygon,
+      zIndex: this.zIndex,
       borderColor: { ...WHITE_COLOR, alpha: 0 },
       fillColor: { ...WHITE_COLOR, alpha: 0 },
     });
 
     this.noGlyphStyle = getStyle({
-      geometry: polygon,
-      zIndex,
+      geometry: this.polygon,
+      zIndex: this.zIndex,
       fillColor: '#E7E7E7',
     });
     this.noGlyphStyle.setText(
@@ -113,61 +136,127 @@ export default class Glyph {
     );
 
     this.feature = new Feature({
-      geometry: polygon,
-      id: elementId,
+      geometry: this.polygon,
+      id: this.elementId,
       type: FEATURE_TYPE.GLYPH,
-      zIndex,
-      getAnchorAndCoords: (): { anchor: Array<number>; coords: Coordinate } => {
-        const center = mapInstance?.getView().getCenter();
+      zIndex: this.zIndex,
+      getAnchorAndCoords: (coords: Coordinate): { anchor: Array<number>; coords: Coordinate } => {
+        const center = this.mapInstance?.getView().getCenter();
         let anchorX = 0;
         let anchorY = 0;
         if (center) {
-          anchorX =
-            (center[0] - this.pointToProjection({ x: this.x, y: this.y })[0]) / this.widthOnMap;
-          anchorY =
-            -(center[1] - this.pointToProjection({ x: this.x, y: this.y })[1]) / this.heightOnMap;
+          anchorX = (center[0] - coords[0]) / this.widthOnMap;
+          anchorY = -(center[1] - coords[1]) / this.heightOnMap;
         }
         return { anchor: [anchorX, anchorY], coords: center || [0, 0] };
       },
     });
 
+    this.feature.set('setCoordinates', this.setCoordinates.bind(this));
+    this.feature.set('getGlyphData', this.getGlyphData.bind(this));
+    this.feature.set('reset', this.reset.bind(this));
     this.feature.setStyle(this.getStyle.bind(this));
-    if (!glyphId) {
+
+    if (!this.glyphId) {
       return;
     }
     const img = new Image();
     img.onload = (): void => {
-      const imageWidth = img.naturalWidth;
-      const imageHeight = img.naturalHeight;
-      const heightScale = height / imageHeight;
-      const widthScale = width / imageWidth;
-      if (heightScale < widthScale) {
-        this.imageScale = heightScale;
-        this.widthOnMap = (this.heightOnMap * imageWidth) / imageHeight;
-      } else {
-        this.imageScale = widthScale;
-        this.heightOnMap = (this.widthOnMap * imageHeight) / imageWidth;
-      }
+      this.imageWidth = img.naturalWidth;
+      this.imageHeight = img.naturalHeight;
+      const imagePoint1 = this.pointToProjection({ x: 0, y: 0 });
+      const imagePoint2 = this.pointToProjection({ x: this.imageWidth, y: this.imageHeight });
+      this.imageWidthOnMap = Math.abs(imagePoint2[0] - imagePoint1[0]);
+      this.imageHeightOnMap = Math.abs(imagePoint2[1] - imagePoint1[1]);
+      this.setImageScaleAndDimensions(this.heightOnMap, this.widthOnMap);
       this.style = new Style({
         image: new Icon({
           anchor: [0, 0],
           img,
-          size: [imageWidth, imageHeight],
+          size: [this.imageWidth, this.imageHeight],
         }),
-        zIndex,
+        zIndex: this.zIndex,
       });
     };
-    img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyphId)}`;
+    img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(this.glyphId)}`;
+  }
+
+  private drawPolygon(): void {
+    this.polygon = new Polygon([
+      [
+        this.pointToProjection({ x: this.x, y: this.y }),
+        this.pointToProjection({ x: this.x + this.width, y: this.y }),
+        this.pointToProjection({ x: this.x + this.width, y: this.y + this.height }),
+        this.pointToProjection({ x: this.x, y: this.y + this.height }),
+        this.pointToProjection({ x: this.x, y: this.y }),
+      ],
+    ]);
+  }
+
+  private reset(): void {
+    this.drawPolygon();
+    this.polygonStyle.setGeometry(this.polygon);
+    this.feature.setGeometry(this.polygon);
+  }
+
+  protected setImageScaleAndDimensions(height: number, width: number): void {
+    this.widthOnMap = width;
+    this.heightOnMap = height;
+    const heightScale = height / this.imageHeightOnMap;
+    const widthScale = width / this.imageWidthOnMap;
+    if (heightScale < widthScale) {
+      this.imageScale = heightScale;
+      this.widthOnMap = (this.heightOnMap * this.imageWidth) / this.imageHeight;
+    } else {
+      this.imageScale = widthScale;
+      this.heightOnMap = (this.widthOnMap * this.imageHeight) / this.imageWidth;
+    }
+  }
+
+  private setCoordinates(coords: Coordinate[][]): void {
+    const geometry = this.polygonStyle.getGeometry();
+    if (geometry && geometry instanceof Polygon) {
+      geometry.setCoordinates(coords);
+      const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), this.mapSize);
+      this.x = boundingBox.x;
+      this.y = boundingBox.y;
+      this.width = boundingBox.width;
+      this.height = boundingBox.height;
+    }
+  }
+
+  private getGlyphData(): LayerImage {
+    return {
+      id: this.elementId,
+      x: this.x,
+      y: this.y,
+      width: this.width,
+      height: this.height,
+      glyph: this.glyphId,
+      z: this.zIndex,
+    };
   }
 
   protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void {
     const scale = this.minResolution / resolution;
     const getAnchorAndCoords = feature.get('getAnchorAndCoords');
     let anchor = [0, 0];
-    let coords = this.pointToProjection({ x: this.x, y: this.y });
+    let coords = [this.x, this.y];
 
+    const geometry = feature.getGeometry();
+    if (geometry && geometry instanceof Polygon) {
+      const polygonExtent = geometry.getExtent();
+      if (polygonExtent) {
+        coords = [polygonExtent[0], polygonExtent[3]];
+        const width = Math.abs(polygonExtent[0] - polygonExtent[2]);
+        const height = Math.abs(polygonExtent[1] - polygonExtent[3]);
+        this.setImageScaleAndDimensions(height, width);
+      }
+    } else {
+      return [];
+    }
     if (getAnchorAndCoords instanceof Function) {
-      const anchorAndCoords = getAnchorAndCoords();
+      const anchorAndCoords = getAnchorAndCoords(coords);
       anchor = anchorAndCoords.anchor;
       coords = anchorAndCoords.coords;
     }
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
index 1cdd1aef..9da93414 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts
@@ -23,6 +23,13 @@ jest.mock('../style/rgbToHex');
 
 describe('Layer', () => {
   let props: LayerProps;
+  const mapSize = {
+    width: 90,
+    height: 90,
+    tileSize: 256,
+    minZoom: 2,
+    maxZoom: 9,
+  };
 
   beforeEach(() => {
     const dummyElement = document.createElement('div');
@@ -128,6 +135,7 @@ describe('Layer', () => {
       layerId: 23,
       pointToProjection: jest.fn(point => [point.x, point.y]),
       mapInstance,
+      mapSize,
       lineTypes: {},
       arrowTypes: {},
     };
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
index e48faf93..65c6f613 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
@@ -27,6 +27,7 @@ import {
 import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle';
 import { Stroke } from 'ol/style';
 import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph';
+import { MapSize } from '@/redux/map/map.types';
 
 export interface LayerProps {
   texts: Array<LayerText>;
@@ -39,10 +40,13 @@ export interface LayerProps {
   lineTypes: LineTypeDict;
   arrowTypes: ArrowTypeDict;
   mapInstance: MapInstance;
+  mapSize: MapSize;
   pointToProjection: UsePointToProjectionResult;
 }
 
 export default class Layer {
+  layerId: number;
+
   texts: Array<LayerText>;
 
   rects: Array<LayerRect>;
@@ -61,6 +65,8 @@ export default class Layer {
 
   mapInstance: MapInstance;
 
+  mapSize: MapSize;
+
   vectorSource: VectorSource<
     Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>
   >;
@@ -80,6 +86,7 @@ export default class Layer {
     lineTypes,
     arrowTypes,
     mapInstance,
+    mapSize,
     pointToProjection,
   }: LayerProps) {
     this.vectorSource = new VectorSource({});
@@ -93,11 +100,14 @@ export default class Layer {
     this.arrowTypes = arrowTypes;
     this.pointToProjection = pointToProjection;
     this.mapInstance = mapInstance;
+    this.mapSize = mapSize;
+    this.layerId = layerId;
 
     this.vectorSource.addFeatures(this.getTextsFeatures());
     this.vectorSource.addFeatures(this.getRectsFeatures());
     this.vectorSource.addFeatures(this.getOvalsFeatures());
-    this.drawImages();
+    const imagesFeatures = this.getImagesFeatures();
+    this.vectorSource.addFeatures(imagesFeatures);
 
     const { linesFeatures, arrowsFeatures } = this.getLinesFeatures();
     this.vectorSource.addFeatures(linesFeatures);
@@ -111,6 +121,7 @@ export default class Layer {
     });
 
     this.vectorLayer.set('id', layerId);
+    this.vectorLayer.set('imagesFeatures', imagesFeatures);
     this.vectorLayer.set('drawImage', this.drawImage.bind(this));
   }
 
@@ -290,13 +301,22 @@ export default class Layer {
     return { linesFeatures, arrowsFeatures };
   };
 
-  private drawImages(): void {
-    Object.values(this.images).forEach(image => {
-      this.drawImage(image);
+  private getImagesFeatures(): Feature<Polygon>[] {
+    return Object.values(this.images).map(image => {
+      return this.getGlyphFeature(image);
     });
   }
 
   private drawImage(image: LayerImage): void {
+    const glyphFeature = this.getGlyphFeature(image);
+    const imagesFeatures = this.vectorLayer.get('imagesFeatures');
+    if (imagesFeatures && Array.isArray(imagesFeatures)) {
+      imagesFeatures.push(glyphFeature);
+    }
+    this.vectorSource.addFeature(glyphFeature);
+  }
+
+  private getGlyphFeature(image: LayerImage): Feature<Polygon> {
     const glyph = new Glyph({
       elementId: image.id,
       glyphId: image.glyph,
@@ -307,8 +327,9 @@ export default class Layer {
       zIndex: image.z,
       pointToProjection: this.pointToProjection,
       mapInstance: this.mapInstance,
+      mapSize: this.mapSize,
     });
-    this.vectorSource.addFeature(glyph.feature);
+    return glyph.feature;
   }
 
   protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void {
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
index 2b8d42c4..92580108 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts
@@ -2,12 +2,11 @@
 import Draw from 'ol/interaction/Draw';
 import SimpleGeometry from 'ol/geom/SimpleGeometry';
 import Polygon from 'ol/geom/Polygon';
-import { toLonLat } from 'ol/proj';
-import { latLngToPoint } from '@/utils/map/latLngToPoint';
 import { MapSize } from '@/redux/map/map.types';
 import { AppDispatch } from '@/redux/store';
 import { Coordinate } from 'ol/coordinate';
 import { openLayerImageObjectFactoryModal } from '@/redux/modal/modal.slice';
+import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent';
 
 export default function getDrawImageInteraction(mapSize: MapSize, dispatch: AppDispatch): Draw {
   const drawImageInteraction = new Draw({
@@ -44,22 +43,9 @@ export default function getDrawImageInteraction(mapSize: MapSize, dispatch: AppD
     const geometry = event.feature.getGeometry() as Polygon;
     const extent = geometry.getExtent();
 
-    const [startLng, startLat] = toLonLat([extent[0], extent[3]]);
-    const startPoint = latLngToPoint([startLat, startLng], mapSize);
-    const [endLng, endLat] = toLonLat([extent[2], extent[1]]);
-    const endPoint = latLngToPoint([endLat, endLng], mapSize);
+    const boundingBox = getBoundingBoxFromExtent(extent, mapSize);
 
-    const width = Math.abs(endPoint.x - startPoint.x);
-    const height = Math.abs(endPoint.y - startPoint.y);
-
-    dispatch(
-      openLayerImageObjectFactoryModal({
-        x: startPoint.x,
-        y: startPoint.y,
-        width,
-        height,
-      }),
-    );
+    dispatch(openLayerImageObjectFactoryModal(boundingBox));
   });
 
   return drawImageInteraction;
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
new file mode 100644
index 00000000..752eb905
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
@@ -0,0 +1,59 @@
+/* eslint-disable no-magic-numbers */
+import Polygon from 'ol/geom/Polygon';
+import { AppDispatch } from '@/redux/store';
+import Transform from 'ol-ext/interaction/Transform';
+import { Geometry } from 'ol/geom';
+import { Collection, Feature } from 'ol';
+import BaseEvent from 'ol/events/Event';
+import { updateLayerImageObject } from '@/redux/layers/layers.thunks';
+import { layerUpdateImage } from '@/redux/layers/layers.slice';
+import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent';
+import { MapSize } from '@/redux/map/map.types';
+
+export default function getTransformImageInteraction(
+  dispatch: AppDispatch,
+  mapSize: MapSize,
+  modelId: number,
+  activeLayer: number,
+  featuresCollection: Collection<Feature<Geometry>>,
+): Transform {
+  const transform = new Transform({
+    features: featuresCollection,
+    scale: true,
+    rotate: false,
+    stretch: false,
+    keepRectangle: true,
+    translate: true,
+  });
+
+  transform.on(['scaleend', 'translateend'], async (event: BaseEvent | Event): Promise<void> => {
+    const transformEvent = event as unknown as { feature: Feature };
+    const { feature } = transformEvent;
+    const setCoordinates = feature.get('setCoordinates');
+    const getGlyphData = feature.get('getGlyphData');
+    const reset = feature.get('reset');
+    const geometry = feature.getGeometry();
+    if (geometry && getGlyphData instanceof Function) {
+      const glyphData = getGlyphData();
+      try {
+        const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), mapSize);
+        const layerImage = await dispatch(
+          updateLayerImageObject({ modelId, layerId: activeLayer, ...glyphData, ...boundingBox }),
+        ).unwrap();
+        if (layerImage) {
+          dispatch(layerUpdateImage({ modelId, layerId: activeLayer, layerImage }));
+        }
+        if (geometry instanceof Polygon && setCoordinates instanceof Function) {
+          setCoordinates(geometry.getCoordinates());
+          geometry.changed();
+        }
+      } catch {
+        if (reset instanceof Function) {
+          reset();
+        }
+      }
+    }
+  });
+
+  return transform;
+}
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 1def76d9..13db481d 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -67,6 +67,8 @@ export const apiPath = {
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
   addLayerImageObject: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/`,
+  updateLayerImageObject: (modelId: number, layerId: number, imageId: number): string =>
+    `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/${imageId}`,
   getLayer: (modelId: number, layerId: number): string =>
     `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`,
   getGlyphImage: (glyphId: number): string =>
diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts
index 9e41f8de..f5718ad4 100644
--- a/src/redux/layers/layers.reducers.ts
+++ b/src/redux/layers/layers.reducers.ts
@@ -79,3 +79,19 @@ export const layerAddImageReducer = (
   }
   layer.images[layerImage.id] = layerImage;
 };
+
+export const layerUpdateImageReducer = (
+  state: LayersState,
+  action: PayloadAction<{ modelId: number; layerId: number; layerImage: LayerImage }>,
+): void => {
+  const { modelId, layerId, layerImage } = action.payload;
+  const { data } = state[modelId];
+  if (!data) {
+    return;
+  }
+  const layer = data.layers.find(layerState => layerState.details.id === layerId);
+  if (!layer) {
+    return;
+  }
+  layer.images[layerImage.id] = layerImage;
+};
diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts
index 9f78f0dd..9e4ea3cd 100644
--- a/src/redux/layers/layers.slice.ts
+++ b/src/redux/layers/layers.slice.ts
@@ -3,6 +3,7 @@ import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock';
 import {
   getLayersForModelReducer,
   layerAddImageReducer,
+  layerUpdateImageReducer,
   setActiveLayerReducer,
   setLayerVisibilityReducer,
 } from '@/redux/layers/layers.reducers';
@@ -14,12 +15,14 @@ export const layersSlice = createSlice({
     setLayerVisibility: setLayerVisibilityReducer,
     setActiveLayer: setActiveLayerReducer,
     layerAddImage: layerAddImageReducer,
+    layerUpdateImage: layerUpdateImageReducer,
   },
   extraReducers: builder => {
     getLayersForModelReducer(builder);
   },
 });
 
-export const { setLayerVisibility, setActiveLayer, layerAddImage } = layersSlice.actions;
+export const { setLayerVisibility, setActiveLayer, layerAddImage, layerUpdateImage } =
+  layersSlice.actions;
 
 export default layersSlice.reducer;
diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts
index f3f78c43..3de3990d 100644
--- a/src/redux/layers/layers.thunks.ts
+++ b/src/redux/layers/layers.thunks.ts
@@ -182,3 +182,44 @@ export const addLayerImageObject = createAsyncThunk<
     return Promise.reject(getError({ error }));
   }
 });
+
+export const updateLayerImageObject = createAsyncThunk<
+  LayerImage | null,
+  {
+    modelId: number;
+    layerId: number;
+    id: number;
+    x: number;
+    y: number;
+    z: number;
+    width: number;
+    height: number;
+    glyph: number | null;
+  },
+  ThunkConfig
+  // eslint-disable-next-line consistent-return
+>(
+  'vectorMap/updateLayerImageObject',
+  async ({ modelId, layerId, id, x, y, z, width, height, glyph }) => {
+    try {
+      const { data } = await axiosInstanceNewAPI.put<LayerImage>(
+        apiPath.updateLayerImageObject(modelId, layerId, id),
+        {
+          x,
+          y,
+          z,
+          width,
+          height,
+          glyph,
+        },
+      );
+      const isDataValid = validateDataUsingZodSchema(data, layerImageSchema);
+      if (isDataValid) {
+        return data;
+      }
+      return null;
+    } catch (error) {
+      return Promise.reject(getError({ error }));
+    }
+  },
+);
diff --git a/src/redux/mapEditTools/mapEditTools.constants.ts b/src/redux/mapEditTools/mapEditTools.constants.ts
index 3f54d2b0..2524c492 100644
--- a/src/redux/mapEditTools/mapEditTools.constants.ts
+++ b/src/redux/mapEditTools/mapEditTools.constants.ts
@@ -1,3 +1,4 @@
 export const MAP_EDIT_ACTIONS = {
   DRAW_IMAGE: 'DRAW_IMAGE',
+  TRANSFORM_IMAGE: 'TRANSFORM_IMAGE',
 } as const;
diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx
index 784cfb0e..4d888fc2 100644
--- a/src/shared/Icon/Icon.component.tsx
+++ b/src/shared/Icon/Icon.component.tsx
@@ -19,6 +19,7 @@ import { PlusIcon } from '@/shared/Icon/Icons/PlusIcon';
 import type { IconComponentType, IconTypes } from '@/types/iconTypes';
 import { DownloadIcon } from '@/shared/Icon/Icons/DownloadIcon';
 import { ImageIcon } from '@/shared/Icon/Icons/ImageIcon';
+import { ResizeImageIcon } from '@/shared/Icon/Icons/ResizeImageIcon';
 import { LocationIcon } from './Icons/LocationIcon';
 import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn';
 import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut';
@@ -61,6 +62,7 @@ const icons: Record<IconTypes, IconComponentType> = {
   user: UserIcon,
   'manage-user': ManageUserIcon,
   image: ImageIcon,
+  'resize-image': ResizeImageIcon,
 } as const;
 
 export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => {
diff --git a/src/shared/Icon/Icons/ResizeImageIcon.tsx b/src/shared/Icon/Icons/ResizeImageIcon.tsx
new file mode 100644
index 00000000..71a0c324
--- /dev/null
+++ b/src/shared/Icon/Icons/ResizeImageIcon.tsx
@@ -0,0 +1,19 @@
+interface ResizeImageIconProps {
+  className?: string;
+}
+
+export const ResizeImageIcon = ({ className }: ResizeImageIconProps): JSX.Element => (
+  <svg
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    fill="none"
+    className={className}
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <rect x="6" y="2" width="17" height="17" stroke="currentColor" strokeWidth="1.5" fill="none" />
+    <rect x="1" y="14" width="9" height="9" stroke="currentColor" strokeWidth="1.5" fill="white" />
+    <line x1="10" y1="14" x2="18" y2="7" stroke="currentColor" strokeWidth="1.5" />
+    <polygon points="12,5 20,5 20,13" fill="currentColor" strokeWidth="1.5" />
+  </svg>
+);
diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts
index 0ef11e99..469b7699 100644
--- a/src/types/iconTypes.ts
+++ b/src/types/iconTypes.ts
@@ -25,6 +25,7 @@ export type IconTypes =
   | 'manage-user'
   | 'download'
   | 'question'
-  | 'image';
+  | 'image'
+  | 'resize-image';
 
 export type IconComponentType = ({ className }: { className: string }) => JSX.Element;
-- 
GitLab


From a0ad825f9cddb6964c36943f489f5c96dd0a0919 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Thu, 9 Jan 2025 09:12:39 +0100
Subject: [PATCH 19/22] feat(vector-map): add test for getBoundBoxFromExtent

---
 .../coords/getBoundingBoxFromExtent.test.ts   | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.test.ts

diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.test.ts
new file mode 100644
index 00000000..532c485d
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.test.ts
@@ -0,0 +1,26 @@
+/* eslint-disable no-magic-numbers */
+import { Extent } from 'ol/extent';
+import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent';
+
+describe('getBoundingBoxFromExtent', () => {
+  it('should return a bounding box for extent', () => {
+    const extent: Extent = [0, 195700, 195700, 0];
+    const mapSize = {
+      width: 512,
+      height: 512,
+      tileSize: 256,
+      minZoom: 2,
+      maxZoom: 4,
+    };
+
+    const result = getBoundingBoxFromExtent(extent, mapSize);
+
+    expect(result).toHaveProperty('x', 1024);
+    expect(result).toHaveProperty('y', 1024);
+    expect(result).toHaveProperty('width');
+    expect(result).toHaveProperty('height');
+
+    expect(result.width).toBeCloseTo(10);
+    expect(result.height).toBeCloseTo(10);
+  });
+});
-- 
GitLab


From 6f18fefb1f5af75234c49677f693ed582c1fb0e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Thu, 9 Jan 2025 10:08:39 +0100
Subject: [PATCH 20/22] feat(vector-map): add test for
 getTransformImageInteraction

---
 .../getTransformImageInteraction.test.ts      | 53 +++++++++++++++++++
 1 file changed, 53 insertions(+)
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.test.ts

diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.test.ts
new file mode 100644
index 00000000..113870e2
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.test.ts
@@ -0,0 +1,53 @@
+/* eslint-disable no-magic-numbers */
+import modalReducer from '@/redux/modal/modal.slice';
+import { MapSize } from '@/redux/map/map.types';
+import {
+  createStoreInstanceUsingSliceReducer,
+  ToolkitStoreWithSingleSlice,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { ModalState } from '@/redux/modal/modal.types';
+import { DEFAULT_TILE_SIZE } from '@/constants/map';
+import { Collection, Feature } from 'ol';
+import getTransformImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction';
+import Transform from 'ol-ext/interaction/Transform';
+import { Geometry } from 'ol/geom';
+
+jest.mock('../../../../../../../utils/map/latLngToPoint', () => ({
+  latLngToPoint: jest.fn(latLng => ({ x: latLng[0], y: latLng[1] })),
+}));
+
+describe('getTransformImageInteraction', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<ModalState>;
+  let modelIdMock: number;
+  let layerIdMock: number;
+  let featuresCollectionMock: Collection<Feature<Geometry>>;
+  const mockDispatch = jest.fn(() => {});
+
+  let mapSize: MapSize;
+
+  beforeEach(() => {
+    mapSize = {
+      width: 800,
+      height: 600,
+      minZoom: 1,
+      maxZoom: 9,
+      tileSize: DEFAULT_TILE_SIZE,
+    };
+    store = createStoreInstanceUsingSliceReducer('modal', modalReducer);
+    store.dispatch = mockDispatch;
+    modelIdMock = 1;
+    layerIdMock = 1;
+    featuresCollectionMock = new Collection<Feature<Geometry>>();
+  });
+
+  it('returns a Transform interaction', () => {
+    const transformInteraction = getTransformImageInteraction(
+      store.dispatch,
+      mapSize,
+      modelIdMock,
+      layerIdMock,
+      featuresCollectionMock,
+    );
+    expect(transformInteraction).toBeInstanceOf(Transform);
+  });
+});
-- 
GitLab


From 19df32ddbc992dab7c50a906e324dc9501314dac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Thu, 9 Jan 2025 14:52:37 +0100
Subject: [PATCH 21/22] feat(layer-image): add glyph editing for layer image
 objects

---
 .../LayerImageObjectFactoryModal/index.ts     |   1 -
 ...eObjectEditFactoryModal.component.test.tsx | 173 ++++++++++++++++++
 ...rImageObjectEditFactoryModal.component.tsx |  98 ++++++++++
 ...ImageObjectFactoryModal.component.test.tsx |   0
 ...LayerImageObjectFactoryModal.component.tsx | 108 ++---------
 .../LayerImageObjectForm.component.tsx        | 121 ++++++++++++
 .../LayerImageObjectForm.styles.css}          |   0
 .../Modal/LayerImageObjectModal/index.ts      |   2 +
 .../FunctionalArea/Modal/Modal.component.tsx  |  10 +-
 .../ModalLayout/ModalLayout.component.tsx     |   3 +-
 .../MapDrawActions.component.tsx              |   7 +-
 .../MapDrawEditActions.component.tsx          |  50 +++++
 .../utils/shapes/elements/Glyph.ts            |  45 +++--
 .../layer/getTransformImageInteraction.ts     |  15 ++
 src/redux/mapEditTools/mapEditTools.mock.ts   |   1 +
 .../mapEditTools/mapEditTools.reducers.ts     |   8 +
 .../mapEditTools/mapEditTools.selectors.ts    |   5 +
 src/redux/mapEditTools/mapEditTools.slice.ts  |   8 +-
 src/redux/mapEditTools/mapEditTools.types.ts  |   2 +
 src/redux/modal/modal.reducers.ts             |   6 +
 src/redux/modal/modal.slice.ts                |   3 +
 .../Autocomplete/Autocomplete.component.tsx   |  15 +-
 src/shared/Icon/Icon.component.tsx            |   6 +
 src/shared/Icon/Icons/EditImageIcon.tsx       |  29 +++
 src/shared/Icon/Icons/PencilIcon.tsx          |  30 +++
 src/shared/Icon/Icons/TrashIcon.tsx           |  30 +++
 src/types/iconTypes.ts                        |   5 +-
 src/types/modal.ts                            |   3 +-
 28 files changed, 656 insertions(+), 128 deletions(-)
 delete mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx
 rename src/components/FunctionalArea/Modal/{LayerImageObjectFactoryModal => LayerImageObjectModal}/LayerImageObjectFactoryModal.component.test.tsx (100%)
 rename src/components/FunctionalArea/Modal/{LayerImageObjectFactoryModal => LayerImageObjectModal}/LayerImageObjectFactoryModal.component.tsx (51%)
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx
 rename src/components/FunctionalArea/Modal/{LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css => LayerImageObjectModal/LayerImageObjectForm.styles.css} (100%)
 create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts
 create mode 100644 src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx
 create mode 100644 src/shared/Icon/Icons/EditImageIcon.tsx
 create mode 100644 src/shared/Icon/Icons/PencilIcon.tsx
 create mode 100644 src/shared/Icon/Icons/TrashIcon.tsx

diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts
deleted file mode 100644
index 11947806..00000000
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { LayerImageObjectFactoryModal } from './LayerImageObjectFactoryModal.component';
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx
new file mode 100644
index 00000000..8095d60e
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx
@@ -0,0 +1,173 @@
+/* eslint-disable no-magic-numbers */
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
+import { StoreType } from '@/redux/store';
+import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
+import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
+import { GLYPHS_STATE_INITIAL_MOCK } from '@/redux/glyphs/glyphs.mock';
+import { apiPath } from '@/redux/apiPath';
+import { HttpStatusCode } from 'axios';
+import { layerImageFixture } from '@/models/fixtures/layerImageFixture';
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import {
+  LAYER_STATE_DEFAULT_DATA,
+  LAYERS_STATE_INITIAL_LAYER_MOCK,
+} from '@/redux/layers/layers.mock';
+import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock';
+import { overlayFixture } from '@/models/fixtures/overlaysFixture';
+import { showToast } from '@/utils/showToast';
+import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types';
+import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock';
+import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
+import { Feature } from 'ol';
+import Polygon from 'ol/geom/Polygon';
+import { LayerImageObjectEditFactoryModal } from './LayerImageObjectEditFactoryModal.component';
+
+const mockedAxiosNewClient = mockNetworkNewAPIResponse();
+
+const glyph = { id: 1, file: 23, filename: 'Glyph1.png' };
+
+jest.mock('../../../../utils/showToast');
+
+const renderComponent = (
+  initialMapEditToolsState: MapEditToolsState = MAP_EDIT_TOOLS_STATE_INITIAL_MOCK,
+): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore({
+    ...INITIAL_STORE_STATE_MOCK,
+    glyphs: {
+      ...GLYPHS_STATE_INITIAL_MOCK,
+      data: [glyph],
+    },
+    layers: {
+      0: {
+        ...LAYERS_STATE_INITIAL_LAYER_MOCK,
+        data: {
+          ...LAYER_STATE_DEFAULT_DATA,
+          activeLayer: 1,
+        },
+      },
+    },
+    modal: {
+      isOpen: true,
+      modalTitle: overlayFixture.name,
+      modalName: 'edit-overlay',
+      editOverlayState: overlayFixture,
+      molArtState: {},
+      overviewImagesState: {},
+      errorReportState: {},
+      layerFactoryState: { id: undefined },
+      layerImageObjectFactoryState: {
+        x: 1,
+        y: 1,
+        width: 1,
+        height: 1,
+      },
+    },
+    models: {
+      ...MODELS_DATA_MOCK_WITH_MAIN_MAP,
+    },
+    mapEditTools: initialMapEditToolsState,
+  });
+  return {
+    store,
+    ...render(
+      <Wrapper>
+        <LayerImageObjectEditFactoryModal />
+      </Wrapper>,
+    ),
+  };
+};
+
+describe('LayerImageObjectEditFactoryModal - component', () => {
+  it('should render LayerImageObjectEditFactoryModal component with initial state', () => {
+    renderComponent();
+
+    expect(screen.getByText(/Glyph:/i)).toBeInTheDocument();
+    expect(screen.getByText(/File:/i)).toBeInTheDocument();
+    expect(screen.getByText(/Submit/i)).toBeInTheDocument();
+    expect(screen.getByText(/No Image/i)).toBeInTheDocument();
+  });
+
+  it('should display a list of glyphs in the dropdown', async () => {
+    renderComponent();
+
+    const dropdown = screen.getByTestId('autocomplete');
+    if (!dropdown.firstChild) {
+      throw new Error('Dropdown does not have a firstChild');
+    }
+    fireEvent.keyDown(dropdown.firstChild, { key: 'ArrowDown' });
+    await waitFor(() => expect(screen.getByText(glyph.filename)).toBeInTheDocument());
+    fireEvent.click(screen.getByText(glyph.filename));
+  });
+
+  it('should update the selected glyph on dropdown change', async () => {
+    renderComponent();
+
+    const dropdown = screen.getByTestId('autocomplete');
+    if (!dropdown.firstChild) {
+      throw new Error('Dropdown does not have a firstChild');
+    }
+    fireEvent.keyDown(dropdown.firstChild, { key: 'ArrowDown' });
+    await waitFor(() => expect(screen.getByText(glyph.filename)).toBeInTheDocument());
+    fireEvent.click(screen.getByText(glyph.filename));
+
+    await waitFor(() => {
+      const imgPreview: HTMLImageElement = screen.getByTestId('layer-image-preview');
+      const decodedSrc = decodeURIComponent(imgPreview.src);
+      expect(decodedSrc).toContain(`glyphs/${glyph.id}/fileContent`);
+    });
+  });
+
+  it('should handle form submission correctly', async () => {
+    mockedAxiosNewClient
+      .onPut(apiPath.updateLayerImageObject(0, 1, 1))
+      .reply(HttpStatusCode.Ok, layerImageFixture);
+    const geometry = new Polygon([
+      [
+        [10, 10],
+        [10, 10],
+      ],
+    ]);
+    const layerObjectFeature = new Feature({ geometry });
+    const glyphData = {
+      id: 1,
+      x: 1,
+      y: 1,
+      width: 1,
+      height: 1,
+      glyph: 1,
+      z: 1,
+    };
+    const getGlyphDataMock = jest.fn(() => glyphData);
+    jest.spyOn(layerObjectFeature, 'get').mockImplementation(key => {
+      if (key === 'setGlyph') return (): void => {};
+      if (key === 'getGlyphData') return getGlyphDataMock;
+      return undefined;
+    });
+    renderComponent({
+      activeAction: MAP_EDIT_ACTIONS.TRANSFORM_IMAGE,
+      layerImageObject: glyphData,
+    });
+
+    const submitButton = screen.getByText(/Submit/i);
+
+    await act(async () => {
+      fireEvent.click(submitButton);
+    });
+
+    expect(showToast).toHaveBeenCalledWith({
+      message: 'The layer image object has been successfully updated',
+      type: 'success',
+    });
+  });
+
+  it('should display "No Image" when there is no image file', () => {
+    const { store } = renderComponent();
+
+    store.dispatch({
+      type: 'glyphs/clearGlyphData',
+    });
+
+    expect(screen.getByText(/No Image/i)).toBeInTheDocument();
+  });
+});
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx
new file mode 100644
index 00000000..589b3a14
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx
@@ -0,0 +1,98 @@
+/* eslint-disable no-magic-numbers */
+import React, { useState } from 'react';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+
+import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors';
+import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component';
+import { currentModelIdSelector } from '@/redux/models/models.selectors';
+import { layersActiveLayerSelector } from '@/redux/layers/layers.selectors';
+import { addGlyph } from '@/redux/glyphs/glyphs.thunks';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { updateLayerImageObject } from '@/redux/layers/layers.thunks';
+import { layerUpdateImage } from '@/redux/layers/layers.slice';
+import { showToast } from '@/utils/showToast';
+import { closeModal } from '@/redux/modal/modal.slice';
+import { SerializedError } from '@reduxjs/toolkit';
+import { useMapInstance } from '@/utils/context/mapInstanceContext';
+import VectorSource from 'ol/source/Vector';
+
+export const LayerImageObjectEditFactoryModal: React.FC = () => {
+  const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector);
+  const { mapInstance } = useMapInstance();
+
+  const currentModelId = useAppSelector(currentModelIdSelector);
+  const activeLayer = useAppSelector(layersActiveLayerSelector);
+  const dispatch = useAppDispatch();
+
+  const [selectedGlyph, setSelectedGlyph] = useState<number | null>(
+    layerImageObject?.glyph || null,
+  );
+  const [file, setFile] = useState<File | null>(null);
+  const [isSending, setIsSending] = useState<boolean>(false);
+
+  const handleSubmit = async (): Promise<void> => {
+    if (!layerImageObject || !activeLayer) {
+      return;
+    }
+    setIsSending(true);
+
+    try {
+      let glyphId = selectedGlyph;
+      if (file) {
+        const data = await dispatch(addGlyph(file)).unwrap();
+        if (!data) {
+          return;
+        }
+        glyphId = data.id;
+      }
+      const layerImage = await dispatch(
+        updateLayerImageObject({
+          modelId: currentModelId,
+          layerId: activeLayer,
+          ...layerImageObject,
+          glyph: glyphId,
+        }),
+      ).unwrap();
+      if (layerImage) {
+        dispatch(layerUpdateImage({ modelId: currentModelId, layerId: activeLayer, layerImage }));
+        mapInstance?.getAllLayers().forEach(layer => {
+          if (layer.get('id') === activeLayer) {
+            const source = layer.getSource();
+            if (source instanceof VectorSource) {
+              const feature = source.getFeatureById(layerImage.id);
+              const setGlyph = feature?.get('setGlyph');
+              if (setGlyph && setGlyph instanceof Function) {
+                setGlyph(layerImage.glyph);
+                feature.changed();
+              }
+            }
+          }
+        });
+      }
+      showToast({
+        type: 'success',
+        message: 'The layer image object has been successfully updated',
+      });
+      dispatch(closeModal());
+    } catch (error) {
+      const typedError = error as SerializedError;
+      showToast({
+        type: 'error',
+        message: typedError.message || 'An error occurred while adding a new image object',
+      });
+    } finally {
+      setIsSending(false);
+    }
+  };
+
+  return (
+    <LayerImageObjectForm
+      file={file}
+      selectedGlyph={selectedGlyph}
+      isSending={isSending}
+      onSubmit={handleSubmit}
+      setFile={setFile}
+      setSelectedGlyph={setSelectedGlyph}
+    />
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx
similarity index 100%
rename from src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx
rename to src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx
similarity index 51%
rename from src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
rename to src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx
index b750d061..98bc5fc5 100644
--- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx
@@ -1,14 +1,7 @@
 /* eslint-disable no-magic-numbers */
-import React, { useState, useRef } from 'react';
-import { glyphsDataSelector } from '@/redux/glyphs/glyphs.selectors';
+import React, { useState } from 'react';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { layerImageObjectFactoryStateSelector } from '@/redux/modal/modal.selector';
-import { Button } from '@/shared/Button';
-import { BASE_NEW_API_URL } from '@/constants';
-import { apiPath } from '@/redux/apiPath';
-import { Input } from '@/shared/Input';
-import Image from 'next/image';
-import { Glyph } from '@/types/models';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { currentModelIdSelector } from '@/redux/models/models.selectors';
 import { highestZIndexSelector, layersActiveLayerSelector } from '@/redux/layers/layers.selectors';
@@ -17,57 +10,22 @@ import { addGlyph } from '@/redux/glyphs/glyphs.thunks';
 import { SerializedError } from '@reduxjs/toolkit';
 import { showToast } from '@/utils/showToast';
 import { closeModal } from '@/redux/modal/modal.slice';
-import { LoadingIndicator } from '@/shared/LoadingIndicator';
-import './LayerImageObjectFactoryModal.styles.css';
+import './LayerImageObjectForm.styles.css';
 import { useMapInstance } from '@/utils/context/mapInstanceContext';
 import { layerAddImage } from '@/redux/layers/layers.slice';
-import { Autocomplete } from '@/shared/Autocomplete';
+import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component';
 
 export const LayerImageObjectFactoryModal: React.FC = () => {
-  const glyphs: Glyph[] = useAppSelector(glyphsDataSelector);
   const currentModelId = useAppSelector(currentModelIdSelector);
   const activeLayer = useAppSelector(layersActiveLayerSelector);
   const layerImageObjectFactoryState = useAppSelector(layerImageObjectFactoryStateSelector);
   const dispatch = useAppDispatch();
-  const fileInputRef = useRef<HTMLInputElement>(null);
   const highestZIndex = useAppSelector(highestZIndexSelector);
   const { mapInstance } = useMapInstance();
 
   const [selectedGlyph, setSelectedGlyph] = useState<number | null>(null);
   const [file, setFile] = useState<File | null>(null);
   const [isSending, setIsSending] = useState<boolean>(false);
-  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
-
-  const handleGlyphChange = (glyph: Glyph | null): void => {
-    const glyphId = glyph?.id || null;
-    setSelectedGlyph(glyphId);
-    if (!glyphId) {
-      return;
-    }
-    setFile(null);
-    setPreviewUrl(`${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyphId)}`);
-
-    if (fileInputRef.current) {
-      fileInputRef.current.value = '';
-    }
-  };
-
-  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
-    const uploadedFile = e.target.files?.[0] || null;
-
-    setFile(uploadedFile);
-    if (!uploadedFile) {
-      return;
-    }
-
-    setSelectedGlyph(null);
-    if (uploadedFile) {
-      const url = URL.createObjectURL(uploadedFile);
-      setPreviewUrl(url);
-    } else {
-      setPreviewUrl(null);
-    }
-  };
 
   const handleSubmit = async (): Promise<void> => {
     if (!layerImageObjectFactoryState || !activeLayer) {
@@ -130,57 +88,13 @@ export const LayerImageObjectFactoryModal: React.FC = () => {
   };
 
   return (
-    <div className="relative w-[800px] border border-t-[#E1E0E6] bg-white p-[24px]">
-      {isSending && (
-        <div className="c-layer-image-object-factory-loader">
-          <LoadingIndicator width={44} height={44} />
-        </div>
-      )}
-      <div className="grid grid-cols-2 gap-2">
-        <div className="mb-4 flex flex-col gap-2">
-          <span>Glyph:</span>
-          <Autocomplete<Glyph>
-            options={glyphs}
-            valueKey="id"
-            labelKey="filename"
-            onChange={handleGlyphChange}
-          />
-        </div>
-        <div className="mb-4 flex flex-col gap-2">
-          <span>File:</span>
-          <Input
-            ref={fileInputRef}
-            type="file"
-            accept="image/*"
-            onChange={handleFileChange}
-            data-testid="image-file-input"
-            className="w-full border border-[#ccc] bg-white p-2"
-          />
-        </div>
-      </div>
-
-      <div className="relative mb-4 flex h-[350px] w-full items-center justify-center overflow-hidden rounded border">
-        {previewUrl ? (
-          <Image
-            src={previewUrl}
-            alt="image preview"
-            fill
-            style={{ objectFit: 'contain' }}
-            className="rounded"
-            data-testid="layer-image-preview"
-          />
-        ) : (
-          <div className="text-gray-500">No Image</div>
-        )}
-      </div>
-
-      <Button
-        type="button"
-        onClick={handleSubmit}
-        className="w-full justify-center text-base font-medium"
-      >
-        Submit
-      </Button>
-    </div>
+    <LayerImageObjectForm
+      file={file}
+      selectedGlyph={selectedGlyph}
+      isSending={isSending}
+      onSubmit={handleSubmit}
+      setFile={setFile}
+      setSelectedGlyph={setSelectedGlyph}
+    />
   );
 };
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx
new file mode 100644
index 00000000..58a6efac
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx
@@ -0,0 +1,121 @@
+/* eslint-disable no-magic-numbers */
+import React, { useRef, useMemo } from 'react';
+import { glyphsDataSelector } from '@/redux/glyphs/glyphs.selectors';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { Button } from '@/shared/Button';
+import { BASE_NEW_API_URL } from '@/constants';
+import { apiPath } from '@/redux/apiPath';
+import { Input } from '@/shared/Input';
+import Image from 'next/image';
+import { Glyph } from '@/types/models';
+import { LoadingIndicator } from '@/shared/LoadingIndicator';
+import './LayerImageObjectForm.styles.css';
+import { Autocomplete } from '@/shared/Autocomplete';
+
+type LayerImageObjectFormProps = {
+  onSubmit: () => void;
+  isSending: boolean;
+  selectedGlyph: number | null;
+  setSelectedGlyph: (glyphId: number | null) => void;
+  file: File | null;
+  setFile: (file: File | null) => void;
+};
+
+export const LayerImageObjectForm = ({
+  onSubmit,
+  isSending,
+  selectedGlyph,
+  setSelectedGlyph,
+  file,
+  setFile,
+}: LayerImageObjectFormProps): React.JSX.Element => {
+  const glyphs: Glyph[] = useAppSelector(glyphsDataSelector);
+  const fileInputRef = useRef<HTMLInputElement>(null);
+  const initialSelectedGlyph = glyphs.find(glyph => glyph.id === selectedGlyph);
+
+  const previewUrl: string | null = useMemo(() => {
+    if (selectedGlyph) {
+      return `${BASE_NEW_API_URL}${apiPath.getGlyphImage(selectedGlyph)}`;
+    }
+    if (file) {
+      return URL.createObjectURL(file);
+    }
+    return null;
+  }, [file, selectedGlyph]);
+
+  const handleGlyphChange = (glyph: Glyph | null): void => {
+    const glyphId = glyph?.id || null;
+    setSelectedGlyph(glyphId);
+    if (!glyphId) {
+      return;
+    }
+    if (fileInputRef.current) {
+      fileInputRef.current.value = '';
+    }
+  };
+
+  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
+    const uploadedFile = e.target.files?.[0] || null;
+    setFile(uploadedFile);
+    if (!uploadedFile) {
+      return;
+    }
+    setSelectedGlyph(null);
+  };
+
+  return (
+    <div className="relative w-[800px] border border-t-[#E1E0E6] bg-white p-[24px]">
+      {isSending && (
+        <div className="c-layer-image-object-factory-loader">
+          <LoadingIndicator width={44} height={44} />
+        </div>
+      )}
+      <div className="grid grid-cols-2 gap-2">
+        <div className="mb-4 flex flex-col gap-2">
+          <span>Glyph:</span>
+          <Autocomplete<Glyph>
+            options={glyphs}
+            initialValue={initialSelectedGlyph}
+            valueKey="id"
+            labelKey="filename"
+            onChange={handleGlyphChange}
+          />
+        </div>
+        <div className="mb-4 flex flex-col gap-2">
+          <span>File:</span>
+          <Input
+            ref={fileInputRef}
+            type="file"
+            accept="image/*"
+            onChange={handleFileChange}
+            data-testid="image-file-input"
+            className="w-full border border-[#ccc] bg-white p-2"
+          />
+        </div>
+      </div>
+
+      <div className="relative mb-4 flex h-[350px] w-full items-center justify-center overflow-hidden rounded border">
+        {previewUrl ? (
+          <Image
+            src={previewUrl}
+            alt="image preview"
+            fill
+            style={{ objectFit: 'contain' }}
+            className="rounded"
+            data-testid="layer-image-preview"
+          />
+        ) : (
+          <div className="text-gray-500">No Image</div>
+        )}
+      </div>
+
+      <Button
+        type="button"
+        onClick={onSubmit}
+        className="w-full justify-center text-base font-medium"
+      >
+        Submit
+      </Button>
+    </div>
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.styles.css
similarity index 100%
rename from src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css
rename to src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.styles.css
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts b/src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts
new file mode 100644
index 00000000..7a355330
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts
@@ -0,0 +1,2 @@
+export { LayerImageObjectFactoryModal } from './LayerImageObjectFactoryModal.component';
+export { LayerImageObjectEditFactoryModal } from './LayerImageObjectEditFactoryModal.component';
diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx
index feec78d9..79a6ac8d 100644
--- a/src/components/FunctionalArea/Modal/Modal.component.tsx
+++ b/src/components/FunctionalArea/Modal/Modal.component.tsx
@@ -5,7 +5,10 @@ import { AccessDeniedModal } from '@/components/FunctionalArea/Modal/AccessDenie
 import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component';
 import { LicenseModal } from '@/components/FunctionalArea/Modal/LicenseModal';
 import { ToSModal } from '@/components/FunctionalArea/Modal/ToSModal/ToSModal.component';
-import { LayerImageObjectFactoryModal } from '@/components/FunctionalArea/Modal/LayerImageObjectFactoryModal';
+import {
+  LayerImageObjectEditFactoryModal,
+  LayerImageObjectFactoryModal,
+} from '@/components/FunctionalArea/Modal/LayerImageObjectModal';
 import { EditOverlayModal } from './EditOverlayModal';
 import { LoginModal } from './LoginModal';
 import { ErrorReportModal } from './ErrorReportModal';
@@ -91,6 +94,11 @@ export const Modal = (): React.ReactNode => {
           <LayerImageObjectFactoryModal />
         </ModalLayout>
       )}
+      {isOpen && modalName === 'layer-image-object-edit-factory' && (
+        <ModalLayout>
+          <LayerImageObjectEditFactoryModal />
+        </ModalLayout>
+      )}
     </>
   );
 };
diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
index 64816f0b..56e3afbc 100644
--- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
+++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
@@ -34,7 +34,8 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
             modalName === 'add-comment' && 'h-auto w-[400px]',
             modalName === 'error-report' && 'h-auto w-[800px]',
             modalName === 'layer-factory' && 'h-auto w-[400px]',
-            modalName === 'layer-image-object-factory' && 'h-auto w-[800px]',
+            ['layer-image-object-factory', 'layer-image-object-edit-factory'].includes(modalName) &&
+              'h-auto w-[800px]',
             ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]',
           )}
         >
diff --git a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
index 69110b76..a4a02240 100644
--- a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
+++ b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx
@@ -5,6 +5,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component';
+import { MapDrawEditActionsComponent } from '@/components/Map/MapDrawActions/MapDrawEditActions.component';
 
 export const MapDrawActions = (): React.JSX.Element => {
   const activeAction = useAppSelector(mapEditToolsActiveActionSelector);
@@ -15,18 +16,16 @@ export const MapDrawActions = (): React.JSX.Element => {
   };
 
   return (
-    <div className="absolute right-6 top-[calc(64px+40px+144px)] z-10 flex flex-col gap-4">
+    <div className="absolute right-6 top-[calc(64px+40px+144px)] z-10 flex flex-col items-end gap-4">
       <MapDrawActionsButton
         isActive={activeAction === MAP_EDIT_ACTIONS.DRAW_IMAGE}
         toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)}
         icon="image"
         title="Draw image"
       />
-      <MapDrawActionsButton
+      <MapDrawEditActionsComponent
         isActive={activeAction === MAP_EDIT_ACTIONS.TRANSFORM_IMAGE}
         toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.TRANSFORM_IMAGE)}
-        icon="resize-image"
-        title="Transform image"
       />
     </div>
   );
diff --git a/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx
new file mode 100644
index 00000000..153e93ad
--- /dev/null
+++ b/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx
@@ -0,0 +1,50 @@
+/* eslint-disable no-magic-numbers */
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component';
+import { openLayerImageObjectEditFactoryModal } from '@/redux/modal/modal.slice';
+
+type MapDrawEditActionsComponentProps = {
+  toggleMapEditAction: () => void;
+  isActive: boolean;
+};
+
+export const MapDrawEditActionsComponent = ({
+  toggleMapEditAction,
+  isActive,
+}: MapDrawEditActionsComponentProps): React.JSX.Element => {
+  const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector);
+  const dispatch = useAppDispatch();
+
+  const editMapObject = (): void => {
+    dispatch(openLayerImageObjectEditFactoryModal());
+  };
+
+  return (
+    <div className="flex flex-row-reverse gap-4">
+      <MapDrawActionsButton
+        isActive={isActive}
+        toggleMapEditAction={toggleMapEditAction}
+        icon="pencil"
+        title="Edit image"
+      />
+      {layerImageObject && (
+        <>
+          <MapDrawActionsButton
+            isActive={false}
+            toggleMapEditAction={() => editMapObject()}
+            icon="edit-image"
+            title="Edit image"
+          />
+          <MapDrawActionsButton
+            isActive={false}
+            toggleMapEditAction={() => {}}
+            icon="trash"
+            title="Remove image"
+          />
+        </>
+      )}
+    </div>
+  );
+};
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
index 53f71dc9..f2046aeb 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
@@ -155,30 +155,14 @@ export default class Glyph {
     this.feature.set('setCoordinates', this.setCoordinates.bind(this));
     this.feature.set('getGlyphData', this.getGlyphData.bind(this));
     this.feature.set('reset', this.reset.bind(this));
+    this.feature.set('setGlyph', this.setGlyph.bind(this));
+    this.feature.setId(this.elementId);
     this.feature.setStyle(this.getStyle.bind(this));
 
     if (!this.glyphId) {
       return;
     }
-    const img = new Image();
-    img.onload = (): void => {
-      this.imageWidth = img.naturalWidth;
-      this.imageHeight = img.naturalHeight;
-      const imagePoint1 = this.pointToProjection({ x: 0, y: 0 });
-      const imagePoint2 = this.pointToProjection({ x: this.imageWidth, y: this.imageHeight });
-      this.imageWidthOnMap = Math.abs(imagePoint2[0] - imagePoint1[0]);
-      this.imageHeightOnMap = Math.abs(imagePoint2[1] - imagePoint1[1]);
-      this.setImageScaleAndDimensions(this.heightOnMap, this.widthOnMap);
-      this.style = new Style({
-        image: new Icon({
-          anchor: [0, 0],
-          img,
-          size: [this.imageWidth, this.imageHeight],
-        }),
-        zIndex: this.zIndex,
-      });
-    };
-    img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(this.glyphId)}`;
+    this.setGlyph(this.glyphId);
   }
 
   private drawPolygon(): void {
@@ -225,6 +209,29 @@ export default class Glyph {
     }
   }
 
+  private setGlyph(glyph: number): void {
+    const img = new Image();
+    img.onload = (): void => {
+      this.imageWidth = img.naturalWidth;
+      this.imageHeight = img.naturalHeight;
+      const imagePoint1 = this.pointToProjection({ x: 0, y: 0 });
+      const imagePoint2 = this.pointToProjection({ x: this.imageWidth, y: this.imageHeight });
+      this.imageWidthOnMap = Math.abs(imagePoint2[0] - imagePoint1[0]);
+      this.imageHeightOnMap = Math.abs(imagePoint2[1] - imagePoint1[1]);
+      this.setImageScaleAndDimensions(this.heightOnMap, this.widthOnMap);
+      this.style = new Style({
+        image: new Icon({
+          anchor: [0, 0],
+          img,
+          size: [this.imageWidth, this.imageHeight],
+        }),
+        zIndex: this.zIndex,
+      });
+    };
+    img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyph)}`;
+    this.glyphId = glyph;
+  }
+
   private getGlyphData(): LayerImage {
     return {
       id: this.elementId,
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
index 752eb905..9e3f8142 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
@@ -9,6 +9,7 @@ import { updateLayerImageObject } from '@/redux/layers/layers.thunks';
 import { layerUpdateImage } from '@/redux/layers/layers.slice';
 import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent';
 import { MapSize } from '@/redux/map/map.types';
+import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice';
 
 export default function getTransformImageInteraction(
   dispatch: AppDispatch,
@@ -26,6 +27,20 @@ export default function getTransformImageInteraction(
     translate: true,
   });
 
+  transform.on('select', event => {
+    const transformEvent = event as unknown as { feature: Feature };
+    const { feature } = transformEvent;
+    if (!feature) {
+      dispatch(mapEditToolsSetLayerObject(null));
+      return;
+    }
+    const getGlyphData = feature.get('getGlyphData');
+    if (getGlyphData && getGlyphData instanceof Function) {
+      const glyphData = getGlyphData();
+      dispatch(mapEditToolsSetLayerObject(glyphData));
+    }
+  });
+
   transform.on(['scaleend', 'translateend'], async (event: BaseEvent | Event): Promise<void> => {
     const transformEvent = event as unknown as { feature: Feature };
     const { feature } = transformEvent;
diff --git a/src/redux/mapEditTools/mapEditTools.mock.ts b/src/redux/mapEditTools/mapEditTools.mock.ts
index 81dd0812..d6fe529c 100644
--- a/src/redux/mapEditTools/mapEditTools.mock.ts
+++ b/src/redux/mapEditTools/mapEditTools.mock.ts
@@ -2,4 +2,5 @@ import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types';
 
 export const MAP_EDIT_TOOLS_STATE_INITIAL_MOCK: MapEditToolsState = {
   activeAction: null,
+  layerImageObject: null,
 };
diff --git a/src/redux/mapEditTools/mapEditTools.reducers.ts b/src/redux/mapEditTools/mapEditTools.reducers.ts
index 2b83e994..d498472a 100644
--- a/src/redux/mapEditTools/mapEditTools.reducers.ts
+++ b/src/redux/mapEditTools/mapEditTools.reducers.ts
@@ -2,6 +2,7 @@
 import { PayloadAction } from '@reduxjs/toolkit';
 import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
 import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types';
+import { LayerImage } from '@/types/models';
 
 export const mapEditToolsSetActiveActionReducer = (
   state: MapEditToolsState,
@@ -13,3 +14,10 @@ export const mapEditToolsSetActiveActionReducer = (
     state.activeAction = null;
   }
 };
+
+export const mapEditToolsSetLayerObjectReducer = (
+  state: MapEditToolsState,
+  action: PayloadAction<LayerImage | null>,
+): void => {
+  state.layerImageObject = action.payload;
+};
diff --git a/src/redux/mapEditTools/mapEditTools.selectors.ts b/src/redux/mapEditTools/mapEditTools.selectors.ts
index 545d0413..d8c86f7e 100644
--- a/src/redux/mapEditTools/mapEditTools.selectors.ts
+++ b/src/redux/mapEditTools/mapEditTools.selectors.ts
@@ -8,3 +8,8 @@ export const mapEditToolsActiveActionSelector = createSelector(
   mapEditToolsSelector,
   state => state.activeAction,
 );
+
+export const mapEditToolsLayerImageObjectSelector = createSelector(
+  mapEditToolsSelector,
+  state => state.layerImageObject,
+);
diff --git a/src/redux/mapEditTools/mapEditTools.slice.ts b/src/redux/mapEditTools/mapEditTools.slice.ts
index bea57d9c..2db0001c 100644
--- a/src/redux/mapEditTools/mapEditTools.slice.ts
+++ b/src/redux/mapEditTools/mapEditTools.slice.ts
@@ -1,15 +1,19 @@
 import { createSlice } from '@reduxjs/toolkit';
 import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock';
-import { mapEditToolsSetActiveActionReducer } from '@/redux/mapEditTools/mapEditTools.reducers';
+import {
+  mapEditToolsSetActiveActionReducer,
+  mapEditToolsSetLayerObjectReducer,
+} from '@/redux/mapEditTools/mapEditTools.reducers';
 
 export const layersSlice = createSlice({
   name: 'layers',
   initialState: MAP_EDIT_TOOLS_STATE_INITIAL_MOCK,
   reducers: {
     mapEditToolsSetActiveAction: mapEditToolsSetActiveActionReducer,
+    mapEditToolsSetLayerObject: mapEditToolsSetLayerObjectReducer,
   },
 });
 
-export const { mapEditToolsSetActiveAction } = layersSlice.actions;
+export const { mapEditToolsSetActiveAction, mapEditToolsSetLayerObject } = layersSlice.actions;
 
 export default layersSlice.reducer;
diff --git a/src/redux/mapEditTools/mapEditTools.types.ts b/src/redux/mapEditTools/mapEditTools.types.ts
index 8a000d1d..e141e6b8 100644
--- a/src/redux/mapEditTools/mapEditTools.types.ts
+++ b/src/redux/mapEditTools/mapEditTools.types.ts
@@ -1,5 +1,7 @@
 import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants';
+import { LayerImage } from '@/types/models';
 
 export type MapEditToolsState = {
   activeAction: keyof typeof MAP_EDIT_ACTIONS | null;
+  layerImageObject: LayerImage | null;
 };
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index f678ea91..a7e1c774 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -153,3 +153,9 @@ export const openLayerImageObjectFactoryModalReducer = (
   state.modalName = 'layer-image-object-factory';
   state.modalTitle = 'Select glyph or upload file';
 };
+
+export const openLayerImageObjectEditFactoryModalReducer = (state: ModalState): void => {
+  state.isOpen = true;
+  state.modalName = 'layer-image-object-edit-factory';
+  state.modalTitle = 'Edit layer image object';
+};
diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts
index a9baf72a..82da41c6 100644
--- a/src/redux/modal/modal.slice.ts
+++ b/src/redux/modal/modal.slice.ts
@@ -18,6 +18,7 @@ import {
   openToSModalReducer,
   openLayerFactoryModalReducer,
   openLayerImageObjectFactoryModalReducer,
+  openLayerImageObjectEditFactoryModalReducer,
 } from './modal.reducers';
 
 const modalSlice = createSlice({
@@ -41,6 +42,7 @@ const modalSlice = createSlice({
     openToSModal: openToSModalReducer,
     openLayerFactoryModal: openLayerFactoryModalReducer,
     openLayerImageObjectFactoryModal: openLayerImageObjectFactoryModalReducer,
+    openLayerImageObjectEditFactoryModal: openLayerImageObjectEditFactoryModalReducer,
   },
 });
 
@@ -62,6 +64,7 @@ export const {
   openToSModal,
   openLayerFactoryModal,
   openLayerImageObjectFactoryModal,
+  openLayerImageObjectEditFactoryModal,
 } = modalSlice.actions;
 
 export default modalSlice.reducer;
diff --git a/src/shared/Autocomplete/Autocomplete.component.tsx b/src/shared/Autocomplete/Autocomplete.component.tsx
index 232dca19..aa602259 100644
--- a/src/shared/Autocomplete/Autocomplete.component.tsx
+++ b/src/shared/Autocomplete/Autocomplete.component.tsx
@@ -8,6 +8,7 @@ type AutocompleteProps<T> = {
   labelKey?: keyof T;
   placeholder?: string;
   onChange: (value: T | null) => void;
+  initialValue?: T | null;
 };
 
 type OptionType<T> = {
@@ -22,6 +23,7 @@ export const Autocomplete = <T,>({
   labelKey = 'label' as keyof T,
   placeholder = 'Select...',
   onChange,
+  initialValue = null,
 }: AutocompleteProps<T>): React.JSX.Element => {
   const formattedOptions = options.map(option => ({
     value: option[valueKey],
@@ -29,13 +31,24 @@ export const Autocomplete = <T,>({
     originalOption: option,
   }));
 
+  const initialFormattedValue = React.useMemo(() => {
+    if (!initialValue) {
+      return null;
+    }
+    return (
+      formattedOptions.find(option => option.originalOption[valueKey] === initialValue[valueKey]) ||
+      null
+    );
+  }, [initialValue, valueKey, labelKey, formattedOptions]);
+
   const handleChange = (selectedOption: SingleValue<OptionType<T>>): void => {
     onChange(selectedOption ? selectedOption.originalOption : null);
   };
 
   return (
     <div data-testid="autocomplete">
-      <Select
+      <Select<OptionType<T>>
+        value={initialFormattedValue}
         options={formattedOptions}
         onChange={handleChange}
         placeholder={placeholder}
diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx
index 4d888fc2..e9cf537c 100644
--- a/src/shared/Icon/Icon.component.tsx
+++ b/src/shared/Icon/Icon.component.tsx
@@ -20,6 +20,9 @@ import type { IconComponentType, IconTypes } from '@/types/iconTypes';
 import { DownloadIcon } from '@/shared/Icon/Icons/DownloadIcon';
 import { ImageIcon } from '@/shared/Icon/Icons/ImageIcon';
 import { ResizeImageIcon } from '@/shared/Icon/Icons/ResizeImageIcon';
+import { PencilIcon } from '@/shared/Icon/Icons/PencilIcon';
+import { EditImageIcon } from '@/shared/Icon/Icons/EditImageIcon';
+import { TrashIcon } from '@/shared/Icon/Icons/TrashIcon';
 import { LocationIcon } from './Icons/LocationIcon';
 import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn';
 import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut';
@@ -63,6 +66,9 @@ const icons: Record<IconTypes, IconComponentType> = {
   'manage-user': ManageUserIcon,
   image: ImageIcon,
   'resize-image': ResizeImageIcon,
+  'edit-image': EditImageIcon,
+  trash: TrashIcon,
+  pencil: PencilIcon,
 } as const;
 
 export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => {
diff --git a/src/shared/Icon/Icons/EditImageIcon.tsx b/src/shared/Icon/Icons/EditImageIcon.tsx
new file mode 100644
index 00000000..7cf80469
--- /dev/null
+++ b/src/shared/Icon/Icons/EditImageIcon.tsx
@@ -0,0 +1,29 @@
+interface EditImageIconProps {
+  className?: string;
+}
+
+export const EditImageIcon = ({ className }: EditImageIconProps): JSX.Element => (
+  <svg
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    fill="none"
+    className={className}
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <path
+      d="M21.2 6.4L12 15.6C11.2 16.4 8.8 16.8 8.3 16.3C7.8 15.8 8.2 13.4 9 12.6L18.4 3.2C18.6 3 18.8 2.8 19.1 2.7C19.4 2.5 19.7 2.4 20.1 2.4C20.4 2.4 20.7 2.5 21 2.7C21.3 2.9 21.5 3.1 21.7 3.4C21.9 3.7 22 4 22 4.4C22 4.7 21.9 5 21.7 5.3C21.5 5.6 21.3 5.8 21.2 6L21.2 6.4Z"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="square"
+      strokeLinejoin="miter"
+    />
+    <path
+      d="M11 4H6C5 4 4.2 4.4 3.6 5C3 5.6 2.6 6.4 2.6 8V18C2.6 19 3 19.8 3.6 20.4C4.2 21 5 21.4 6 21.4H17C18.8 21.4 19.6 20.4 19.6 18V13"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="square"
+      strokeLinejoin="miter"
+    />
+  </svg>
+);
diff --git a/src/shared/Icon/Icons/PencilIcon.tsx b/src/shared/Icon/Icons/PencilIcon.tsx
new file mode 100644
index 00000000..7d86605f
--- /dev/null
+++ b/src/shared/Icon/Icons/PencilIcon.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+interface PencilIconProps {
+  className?: string;
+}
+
+export const PencilIcon = ({ className }: PencilIconProps): JSX.Element => (
+  <svg
+    className={className}
+    fill="currentColor"
+    height="24"
+    width="24"
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 306.637 306.637"
+    aria-hidden="true"
+    role="img"
+    strokeWidth="2"
+  >
+    <g>
+      <path
+        d="M12.809,238.52L0,306.637l68.118-12.809l184.277-184.277l-55.309-55.309L12.809,238.52z M60.79,279.943l-41.992,7.896
+          l7.896-41.992L197.086,75.455l34.096,34.096L60.79,279.943z"
+      />
+      <path
+        d="M251.329,0l-41.507,41.507l55.308,55.308l41.507-41.507L251.329,0z M231.035,41.507l20.294-20.294l34.095,34.095
+          L265.13,75.602L231.035,41.507z"
+      />
+    </g>
+  </svg>
+);
diff --git a/src/shared/Icon/Icons/TrashIcon.tsx b/src/shared/Icon/Icons/TrashIcon.tsx
new file mode 100644
index 00000000..cf0b83b7
--- /dev/null
+++ b/src/shared/Icon/Icons/TrashIcon.tsx
@@ -0,0 +1,30 @@
+interface TrashIconProps {
+  className?: string;
+}
+
+export const TrashIcon = ({ className }: TrashIconProps): JSX.Element => (
+  <svg
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    fill="none"
+    className={className}
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <path d="M7 6H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
+    <path d="M9 4H15V6H9V4Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
+    <rect
+      x="6"
+      y="6"
+      width="12"
+      height="14"
+      rx="1"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      fill="none"
+    />
+    <path d="M9 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
+    <path d="M12 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
+    <path d="M15 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
+  </svg>
+);
diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts
index 469b7699..dc1ee2b1 100644
--- a/src/types/iconTypes.ts
+++ b/src/types/iconTypes.ts
@@ -26,6 +26,9 @@ export type IconTypes =
   | 'download'
   | 'question'
   | 'image'
-  | 'resize-image';
+  | 'resize-image'
+  | 'edit-image'
+  | 'trash'
+  | 'pencil';
 
 export type IconComponentType = ({ className }: { className: string }) => JSX.Element;
diff --git a/src/types/modal.ts b/src/types/modal.ts
index edf1c858..88d3dcd3 100644
--- a/src/types/modal.ts
+++ b/src/types/modal.ts
@@ -13,4 +13,5 @@ export type ModalName =
   | 'terms-of-service'
   | 'logged-in-menu'
   | 'layer-factory'
-  | 'layer-image-object-factory';
+  | 'layer-image-object-factory'
+  | 'layer-image-object-edit-factory';
-- 
GitLab


From 023cc449ae8dbf42a25bebba63df9854afa17fd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Thu, 9 Jan 2025 15:51:05 +0100
Subject: [PATCH 22/22] feat(layer-image): add zIndex editing for layer image
 objects

---
 ...eObjectEditFactoryModal.component.test.tsx |  2 +-
 ...rImageObjectEditFactoryModal.component.tsx | 18 ++------
 .../MapDrawEditActions.component.tsx          | 41 +++++++++++++++++++
 .../reactionsLayer/processModelElements.ts    |  2 +-
 .../reactionsLayer/useOlMapReactionsLayer.ts  |  2 +-
 .../shapes/elements/{ => Glyph}/Glyph.test.ts |  2 +-
 .../shapes/elements/{ => Glyph}/Glyph.ts      | 41 ++++++++++++++-----
 .../shapes/elements/Glyph/updateGlyph.ts      | 23 +++++++++++
 .../utils/shapes/layer/Layer.ts               |  2 +-
 .../layer/getTransformImageInteraction.ts     |  7 ++--
 src/shared/Icon/Icon.component.tsx            |  4 ++
 src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx | 38 +++++++++++++++++
 src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx   | 38 +++++++++++++++++
 src/types/iconTypes.ts                        |  4 +-
 14 files changed, 191 insertions(+), 33 deletions(-)
 rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/{ => Glyph}/Glyph.test.ts (98%)
 rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/{ => Glyph}/Glyph.ts (90%)
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/updateGlyph.ts
 create mode 100644 src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx
 create mode 100644 src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx

diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx
index 8095d60e..87a2adf0 100644
--- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx
@@ -140,7 +140,7 @@ describe('LayerImageObjectEditFactoryModal - component', () => {
     };
     const getGlyphDataMock = jest.fn(() => glyphData);
     jest.spyOn(layerObjectFeature, 'get').mockImplementation(key => {
-      if (key === 'setGlyph') return (): void => {};
+      if (key === 'update') return (): void => {};
       if (key === 'getGlyphData') return getGlyphDataMock;
       return undefined;
     });
diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx
index 589b3a14..1af8c76a 100644
--- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx
@@ -14,7 +14,8 @@ import { showToast } from '@/utils/showToast';
 import { closeModal } from '@/redux/modal/modal.slice';
 import { SerializedError } from '@reduxjs/toolkit';
 import { useMapInstance } from '@/utils/context/mapInstanceContext';
-import VectorSource from 'ol/source/Vector';
+import updateGlyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/updateGlyph';
+import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice';
 
 export const LayerImageObjectEditFactoryModal: React.FC = () => {
   const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector);
@@ -55,19 +56,8 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => {
       ).unwrap();
       if (layerImage) {
         dispatch(layerUpdateImage({ modelId: currentModelId, layerId: activeLayer, layerImage }));
-        mapInstance?.getAllLayers().forEach(layer => {
-          if (layer.get('id') === activeLayer) {
-            const source = layer.getSource();
-            if (source instanceof VectorSource) {
-              const feature = source.getFeatureById(layerImage.id);
-              const setGlyph = feature?.get('setGlyph');
-              if (setGlyph && setGlyph instanceof Function) {
-                setGlyph(layerImage.glyph);
-                feature.changed();
-              }
-            }
-          }
-        });
+        dispatch(mapEditToolsSetLayerObject(layerImage));
+        updateGlyph(mapInstance, activeLayer, layerImage);
       }
       showToast({
         type: 'success',
diff --git a/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx
index 153e93ad..6c30f1c1 100644
--- a/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx
+++ b/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx
@@ -4,6 +4,13 @@ import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEd
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component';
 import { openLayerImageObjectEditFactoryModal } from '@/redux/modal/modal.slice';
+import { updateLayerImageObject } from '@/redux/layers/layers.thunks';
+import { mapModelIdSelector } from '@/redux/map/map.selectors';
+import { layersActiveLayerSelector } from '@/redux/layers/layers.selectors';
+import { layerUpdateImage } from '@/redux/layers/layers.slice';
+import { useMapInstance } from '@/utils/context/mapInstanceContext';
+import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice';
+import updateGlyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/updateGlyph';
 
 type MapDrawEditActionsComponentProps = {
   toggleMapEditAction: () => void;
@@ -14,13 +21,35 @@ export const MapDrawEditActionsComponent = ({
   toggleMapEditAction,
   isActive,
 }: MapDrawEditActionsComponentProps): React.JSX.Element => {
+  const currentModelId = useAppSelector(mapModelIdSelector);
+  const activeLayer = useAppSelector(layersActiveLayerSelector);
   const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector);
   const dispatch = useAppDispatch();
+  const { mapInstance } = useMapInstance();
 
   const editMapObject = (): void => {
     dispatch(openLayerImageObjectEditFactoryModal());
   };
 
+  const updateZIndex = async (value: number): Promise<void> => {
+    if (!activeLayer || !layerImageObject) {
+      return;
+    }
+    const layerImage = await dispatch(
+      updateLayerImageObject({
+        modelId: currentModelId,
+        layerId: activeLayer,
+        ...layerImageObject,
+        z: layerImageObject.z + value,
+      }),
+    ).unwrap();
+    if (layerImage) {
+      dispatch(layerUpdateImage({ modelId: currentModelId, layerId: activeLayer, layerImage }));
+      dispatch(mapEditToolsSetLayerObject(layerImage));
+      updateGlyph(mapInstance, activeLayer, layerImage);
+    }
+  };
+
   return (
     <div className="flex flex-row-reverse gap-4">
       <MapDrawActionsButton
@@ -43,6 +72,18 @@ export const MapDrawEditActionsComponent = ({
             icon="trash"
             title="Remove image"
           />
+          <MapDrawActionsButton
+            isActive={false}
+            toggleMapEditAction={() => updateZIndex(1)}
+            icon="arrow-double-up"
+            title="Remove image"
+          />
+          <MapDrawActionsButton
+            isActive={false}
+            toggleMapEditAction={() => updateZIndex(-1)}
+            icon="arrow-double-down"
+            title="Remove image"
+          />
         </>
       )}
     </div>
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts
index 60746292..d832a64b 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts
@@ -3,7 +3,7 @@ import MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/
 import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle';
 import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare';
 import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway';
-import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph';
+import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph';
 import {
   HorizontalAlign,
   VerticalAlign,
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts
index bcf5cac4..fd83ac6f 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts
@@ -21,7 +21,7 @@ import { getModelElementsForModel } from '@/redux/modelElements/modelElements.th
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare';
 import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle';
-import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph';
+import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph';
 import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway';
 import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction';
 import {
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph.test.ts
similarity index 98%
rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph.test.ts
index bef1e037..ff2a0ac2 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph.test.ts
@@ -4,7 +4,7 @@ import { Style } from 'ol/style';
 import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
 import Glyph, {
   GlyphProps,
-} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph';
+} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph';
 import { MapInstance } from '@/types/map';
 import Polygon from 'ol/geom/Polygon';
 
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph.ts
similarity index 90%
rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph.ts
index f2046aeb..a0676acd 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph.ts
@@ -154,15 +154,12 @@ export default class Glyph {
 
     this.feature.set('setCoordinates', this.setCoordinates.bind(this));
     this.feature.set('getGlyphData', this.getGlyphData.bind(this));
-    this.feature.set('reset', this.reset.bind(this));
-    this.feature.set('setGlyph', this.setGlyph.bind(this));
+    this.feature.set('refreshPolygon', this.refreshPolygon.bind(this));
+    this.feature.set('update', this.update.bind(this));
     this.feature.setId(this.elementId);
     this.feature.setStyle(this.getStyle.bind(this));
 
-    if (!this.glyphId) {
-      return;
-    }
-    this.setGlyph(this.glyphId);
+    this.drawImage();
   }
 
   private drawPolygon(): void {
@@ -177,12 +174,34 @@ export default class Glyph {
     ]);
   }
 
-  private reset(): void {
+  private refreshPolygon(): void {
     this.drawPolygon();
     this.polygonStyle.setGeometry(this.polygon);
     this.feature.setGeometry(this.polygon);
   }
 
+  private refreshZIndex(): void {
+    this.polygonStyle.setZIndex(this.zIndex);
+    this.noGlyphStyle.setZIndex(this.zIndex);
+    this.style.setZIndex(this.zIndex);
+    this.feature.changed();
+  }
+
+  private update(imageObject: LayerImage): void {
+    this.elementId = imageObject.id;
+    this.x = imageObject.x;
+    this.y = imageObject.y;
+    this.zIndex = imageObject.z;
+    this.width = imageObject.width;
+    this.height = imageObject.height;
+    this.glyphId = imageObject.glyph;
+
+    this.refreshPolygon();
+    this.refreshZIndex();
+    this.drawImage();
+    this.feature.changed();
+  }
+
   protected setImageScaleAndDimensions(height: number, width: number): void {
     this.widthOnMap = width;
     this.heightOnMap = height;
@@ -209,7 +228,10 @@ export default class Glyph {
     }
   }
 
-  private setGlyph(glyph: number): void {
+  private drawImage(): void {
+    if (!this.glyphId) {
+      return;
+    }
     const img = new Image();
     img.onload = (): void => {
       this.imageWidth = img.naturalWidth;
@@ -228,8 +250,7 @@ export default class Glyph {
         zIndex: this.zIndex,
       });
     };
-    img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyph)}`;
-    this.glyphId = glyph;
+    img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(this.glyphId)}`;
   }
 
   private getGlyphData(): LayerImage {
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/updateGlyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/updateGlyph.ts
new file mode 100644
index 00000000..05418973
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/updateGlyph.ts
@@ -0,0 +1,23 @@
+import VectorSource from 'ol/source/Vector';
+import { LayerImage } from '@/types/models';
+import { MapInstance } from '@/types/map';
+
+export default function updateGlyph(
+  mapInstance: MapInstance,
+  layerId: number,
+  layerImage: LayerImage,
+): void {
+  mapInstance?.getAllLayers().forEach(layer => {
+    if (layer.get('id') === layerId) {
+      const source = layer.getSource();
+      if (source instanceof VectorSource) {
+        const feature = source.getFeatureById(layerImage.id);
+        const update = feature?.get('update');
+        if (update && update instanceof Function) {
+          update(layerImage);
+          feature.changed();
+        }
+      }
+    }
+  });
+}
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
index 65c6f613..84c3f317 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts
@@ -26,7 +26,7 @@ import {
 } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
 import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle';
 import { Stroke } from 'ol/style';
-import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph';
+import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph/Glyph';
 import { MapSize } from '@/redux/map/map.types';
 
 export interface LayerProps {
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
index 9e3f8142..e4e0dd53 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts
@@ -46,7 +46,7 @@ export default function getTransformImageInteraction(
     const { feature } = transformEvent;
     const setCoordinates = feature.get('setCoordinates');
     const getGlyphData = feature.get('getGlyphData');
-    const reset = feature.get('reset');
+    const refreshPolygon = feature.get('refreshPolygon');
     const geometry = feature.getGeometry();
     if (geometry && getGlyphData instanceof Function) {
       const glyphData = getGlyphData();
@@ -57,14 +57,15 @@ export default function getTransformImageInteraction(
         ).unwrap();
         if (layerImage) {
           dispatch(layerUpdateImage({ modelId, layerId: activeLayer, layerImage }));
+          dispatch(mapEditToolsSetLayerObject(layerImage));
         }
         if (geometry instanceof Polygon && setCoordinates instanceof Function) {
           setCoordinates(geometry.getCoordinates());
           geometry.changed();
         }
       } catch {
-        if (reset instanceof Function) {
-          reset();
+        if (refreshPolygon instanceof Function) {
+          refreshPolygon();
         }
       }
     }
diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx
index e9cf537c..44cade94 100644
--- a/src/shared/Icon/Icon.component.tsx
+++ b/src/shared/Icon/Icon.component.tsx
@@ -23,6 +23,8 @@ import { ResizeImageIcon } from '@/shared/Icon/Icons/ResizeImageIcon';
 import { PencilIcon } from '@/shared/Icon/Icons/PencilIcon';
 import { EditImageIcon } from '@/shared/Icon/Icons/EditImageIcon';
 import { TrashIcon } from '@/shared/Icon/Icons/TrashIcon';
+import { ArrowDoubleUpIcon } from '@/shared/Icon/Icons/ArrowDoubleUpIcon';
+import { ArrowDoubleDownIcon } from '@/shared/Icon/Icons/ArrowDoubleDownIcon';
 import { LocationIcon } from './Icons/LocationIcon';
 import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn';
 import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut';
@@ -69,6 +71,8 @@ const icons: Record<IconTypes, IconComponentType> = {
   'edit-image': EditImageIcon,
   trash: TrashIcon,
   pencil: PencilIcon,
+  'arrow-double-up': ArrowDoubleUpIcon,
+  'arrow-double-down': ArrowDoubleDownIcon,
 } as const;
 
 export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => {
diff --git a/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx
new file mode 100644
index 00000000..d7ab4e38
--- /dev/null
+++ b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx
@@ -0,0 +1,38 @@
+interface ArrowDoubleDownIconProps {
+  className?: string;
+}
+
+export const ArrowDoubleDownIcon = ({ className }: ArrowDoubleDownIconProps): JSX.Element => (
+  <svg
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    fill="none"
+    className={className}
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <path
+      d="M8 8L8 18M8 18L5 15M8 18L11 15"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+    />
+    <path
+      d="M16 8L16 18M16 18L13 15M16 18L19 15"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+    />
+    <line
+      x1="4"
+      y1="6"
+      x2="20"
+      y2="6"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="round"
+    />
+  </svg>
+);
diff --git a/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx
new file mode 100644
index 00000000..ed51a602
--- /dev/null
+++ b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx
@@ -0,0 +1,38 @@
+interface ArrowDoubleUpIconProps {
+  className?: string;
+}
+
+export const ArrowDoubleUpIcon = ({ className }: ArrowDoubleUpIconProps): JSX.Element => (
+  <svg
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    fill="none"
+    className={className}
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <path
+      d="M8 16L8 6M8 6L5 9M8 6L11 9"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+    />
+    <path
+      d="M16 16L16 6M16 6L13 9M16 6L19 9"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+    />
+    <line
+      x1="4"
+      y1="18"
+      x2="20"
+      y2="18"
+      stroke="currentColor"
+      strokeWidth="1.5"
+      strokeLinecap="round"
+    />
+  </svg>
+);
diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts
index dc1ee2b1..a631d12a 100644
--- a/src/types/iconTypes.ts
+++ b/src/types/iconTypes.ts
@@ -29,6 +29,8 @@ export type IconTypes =
   | 'resize-image'
   | 'edit-image'
   | 'trash'
-  | 'pencil';
+  | 'pencil'
+  | 'arrow-double-up'
+  | 'arrow-double-down';
 
 export type IconComponentType = ({ className }: { className: string }) => JSX.Element;
-- 
GitLab