From 8aa25edd71a4423fc6c922cea9e98680b8e0051b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Mon, 11 Dec 2023 19:05:32 +0100
Subject: [PATCH] feat: add overview image interactive layer w/o tests

---
 .../OverviewImageModal.types.ts               | 19 +++++
 .../OverviewImagesModal.component.tsx         | 20 +++++-
 .../utils/getOverviewImageLinkSize.ts         | 24 +++++++
 .../utils/useOverviewImage.test.ts            |  5 +-
 .../utils/useOverviewImage.ts                 |  6 +-
 .../utils/useOverviewImageLinkActions.ts      | 72 +++++++++++++++++++
 .../utils/useOverviewImageLinkElements.tsx    | 28 ++++++++
 .../utils/config/useOlMapView.test.ts         |  8 +--
 .../utils/listeners/onMapPositionChange.ts    | 10 +--
 src/constants/common.ts                       |  2 +
 src/models/overviewImageLink.ts               | 33 ++++-----
 src/redux/map/map.reducers.ts                 | 25 ++++---
 src/redux/map/map.types.ts                    |  4 +-
 src/redux/map/middleware/map.middleware.ts    |  8 +--
 src/redux/modal/modal.reducers.ts             |  9 +++
 src/redux/modal/modal.slice.ts                |  5 +-
 src/types/models.ts                           |  8 +++
 17 files changed, 231 insertions(+), 55 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getOverviewImageLinkSize.ts
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkElements.tsx

diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts
index ec227bb3..a09968c6 100644
--- a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts
@@ -1,3 +1,5 @@
+import { OverviewImageLinkImage, OverviewImageLinkModel } from '@/types/models';
+
 export interface OverviewImageSize {
   width: number;
   height: number;
@@ -7,3 +9,20 @@ export interface ImageContainerSize {
   width: number;
   height: number;
 }
+
+export interface OverviewImageLinkConfigSize {
+  top: number;
+  left: number;
+  width: number;
+  height: number;
+}
+
+export interface OverviewImageLinkConfig {
+  idObject: number;
+  size: OverviewImageLinkConfigSize;
+  onClick(): void;
+}
+
+export type OverviewImageLinkImageHandler = (link: OverviewImageLinkImage) => void;
+
+export type OverviewImageLinkModelHandler = (link: OverviewImageLinkModel) => void;
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
index ef0e608a..86f7e1f2 100644
--- a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
@@ -5,7 +5,7 @@ import { useOverviewImage } from './utils/useOverviewImage';
 
 export const OverviewImagesModal: React.FC = () => {
   const [containerRect, setContainerRect] = useState<DOMRect>();
-  const { imageUrl, size } = useOverviewImage({ containerRect });
+  const { imageUrl, size, linkConfigs } = useOverviewImage({ containerRect });
   const { width, height } = size;
 
   const handleRect = useCallback((node: HTMLDivElement | null) => {
@@ -28,7 +28,23 @@ export const OverviewImagesModal: React.FC = () => {
     >
       <div className="relative" style={{ width, height }}>
         <img alt="overview" className="block h-full w-full" src={imageUrl} />
-        {/* TODO: interactions - clickable elements (in next task) */}
+        {linkConfigs.map(({ size: linkSize, onClick, idObject }) => (
+          // eslint-disable-next-line jsx-a11y/click-events-have-key-events
+          <div
+            role="button"
+            tabIndex={0}
+            key={idObject}
+            style={{
+              height: linkSize.height,
+              width: linkSize.width,
+              top: linkSize.top,
+              left: linkSize.left,
+              background: 'rgba(0,0,0,0.2)',
+              position: 'absolute',
+            }}
+            onClick={onClick}
+          />
+        ))}
       </div>
     </div>
   );
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getOverviewImageLinkSize.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getOverviewImageLinkSize.ts
new file mode 100644
index 00000000..66a82ad7
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getOverviewImageLinkSize.ts
@@ -0,0 +1,24 @@
+import { OverviewImageLink } from '@/types/models';
+import { OverviewImageLinkConfigSize } from '../OverviewImageModal.types';
+
+export const getOverviewImageLinkSize = (
+  { polygon }: OverviewImageLink,
+  {
+    sizeFactor,
+  }: {
+    sizeFactor: number;
+  },
+): OverviewImageLinkConfigSize => {
+  const polygonScaled = polygon.map(({ x, y }) => ({ x: x * sizeFactor, y: y * sizeFactor }));
+  const [pointTopLeft, , pointBottomRight] = polygonScaled;
+  const width = pointBottomRight.x - pointTopLeft.x;
+  const height = pointBottomRight.y - pointTopLeft.y;
+  const { x, y } = pointTopLeft;
+
+  return {
+    top: y,
+    left: x,
+    width,
+    height,
+  };
+};
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts
index f4d964d3..cd7468fd 100644
--- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts
@@ -35,6 +35,7 @@ describe('useOverviewImage - hook', () => {
       expect(result.current).toStrictEqual({
         imageUrl: '',
         size: DEFAULT_OVERVIEW_IMAGE_SIZE,
+        linkConfigs: [],
       });
     });
   });
@@ -65,7 +66,7 @@ describe('useOverviewImage - hook', () => {
     it('should return default size of image and valid imageUrl', () => {
       const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`;
 
-      expect(result.current).toStrictEqual({
+      expect(result.current).toMatchObject({
         imageUrl,
         size: DEFAULT_OVERVIEW_IMAGE_SIZE,
       });
@@ -107,7 +108,7 @@ describe('useOverviewImage - hook', () => {
     it('should return size of image and valid imageUrl', () => {
       const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`;
 
-      expect(result.current).toStrictEqual({
+      expect(result.current).toMatchObject({
         imageUrl,
         size: { height: 100, width: 100, sizeFactor: 0.2 },
       });
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts
index db956acd..e09467b0 100644
--- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts
@@ -1,4 +1,5 @@
-import { OverviewImageSize } from '../OverviewImageModal.types';
+import { OverviewImageLinkConfig, OverviewImageSize } from '../OverviewImageModal.types';
+import { useOverviewImageLinkConfigs } from './useOverviewImageLinkElements';
 import { useOverviewImageSize } from './useOverviewImageSize';
 import { useOverviewImageUrl } from './useOverviewImageUrl';
 
@@ -9,6 +10,7 @@ interface UseOverviewImageArgs {
 interface UseOverviewImageResults {
   imageUrl: string;
   size: OverviewImageSize;
+  linkConfigs: OverviewImageLinkConfig[];
 }
 
 export const useOverviewImage = ({
@@ -16,9 +18,11 @@ export const useOverviewImage = ({
 }: UseOverviewImageArgs): UseOverviewImageResults => {
   const imageUrl = useOverviewImageUrl();
   const size = useOverviewImageSize({ containerRect });
+  const linkConfigs = useOverviewImageLinkConfigs({ sizeFactor: size.sizeFactor });
 
   return {
     size,
     imageUrl,
+    linkConfigs,
   };
 };
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts
new file mode 100644
index 00000000..fa6c658c
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts
@@ -0,0 +1,72 @@
+import { NOOP } from '@/constants/common';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { mapOpenedMapsSelector } from '@/redux/map/map.selectors';
+import { openMapAndSetActive, setActiveMap, setMapPosition } from '@/redux/map/map.slice';
+import { closeModal, setOverviewImageId } from '@/redux/modal/modal.slice';
+import { modelsDataSelector } from '@/redux/models/models.selectors';
+import { MapModel, OverviewImageLink } from '@/types/models';
+import {
+  OverviewImageLinkImageHandler,
+  OverviewImageLinkModelHandler,
+} from '../OverviewImageModal.types';
+
+interface UseOverviewImageLinkActionsResult {
+  handleOnLinkClick(link: OverviewImageLink): void;
+}
+
+export const useOvervieImageLinkActions = (): UseOverviewImageLinkActionsResult => {
+  const dispatch = useAppDispatch();
+  const openedMaps = useAppSelector(mapOpenedMapsSelector);
+  const models = useAppSelector(modelsDataSelector);
+
+  const isMapAlreadyOpened = (modelId: number): boolean =>
+    openedMaps.some(map => map.modelId === modelId);
+
+  const getModelById = (modelId: number): MapModel | undefined =>
+    models.find(map => map.idObject === modelId);
+
+  const onSubmapClick: OverviewImageLinkModelHandler = link => {
+    const modelId = link.modelLinkId;
+    const model = getModelById(modelId);
+    const isMapOpened = isMapAlreadyOpened(modelId);
+    if (!model) {
+      return;
+    }
+
+    if (isMapOpened) {
+      dispatch(setActiveMap({ modelId }));
+    } else {
+      dispatch(openMapAndSetActive({ modelId, modelName: model.name }));
+    }
+
+    dispatch(
+      setMapPosition({
+        x: link.modelPoint.x,
+        y: link.modelPoint.y,
+        z: link.zoomLevel + model.minZoom,
+      }),
+    );
+    dispatch(closeModal());
+  };
+
+  const onImageClick: OverviewImageLinkImageHandler = link => {
+    dispatch(setOverviewImageId(link.imageLinkId));
+  };
+
+  const handleOnLinkClick: UseOverviewImageLinkActionsResult['handleOnLinkClick'] = link => {
+    if ('imageLinkId' in link) {
+      return onImageClick(link);
+    }
+
+    if ('modelLinkId' in link) {
+      return onSubmapClick(link);
+    }
+
+    return NOOP;
+  };
+
+  return {
+    handleOnLinkClick,
+  };
+};
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkElements.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkElements.tsx
new file mode 100644
index 00000000..622f8aa8
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkElements.tsx
@@ -0,0 +1,28 @@
+import { currentOverviewImageSelector } from '@/redux/project/project.selectors';
+import { useSelector } from 'react-redux';
+import { OverviewImageLinkConfig } from '../OverviewImageModal.types';
+import { getOverviewImageLinkSize } from './getOverviewImageLinkSize';
+import { useOvervieImageLinkActions } from './useOverviewImageLinkActions';
+
+interface UseOverviewImageLinksArgs {
+  sizeFactor: number;
+}
+
+export const useOverviewImageLinkConfigs = ({
+  sizeFactor,
+}: UseOverviewImageLinksArgs): OverviewImageLinkConfig[] => {
+  const { handleOnLinkClick } = useOvervieImageLinkActions();
+  const currentImage = useSelector(currentOverviewImageSelector);
+
+  if (!currentImage || !sizeFactor) return [];
+
+  const linkConfigs = currentImage.links.map(link => {
+    return {
+      idObject: link.idObject,
+      size: getOverviewImageLinkSize(link, { sizeFactor }),
+      onClick: () => handleOnLinkClick(link),
+    };
+  });
+
+  return linkConfigs;
+};
diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
index 2832e68d..48b643ce 100644
--- a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
+++ b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
@@ -53,12 +53,8 @@ describe('useOlMapView - util', () => {
     await act(() => {
       store.dispatch(
         setMapPosition({
-          position: {
-            initial: {
-              x: 0,
-              y: 0,
-            },
-          },
+          x: 0,
+          y: 0,
         }),
       );
     });
diff --git a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts
index 482f0509..8bdf55b6 100644
--- a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts
+++ b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts
@@ -16,13 +16,9 @@ export const onMapPositionChange =
 
       dispatch(
         setMapPosition({
-          position: {
-            last: {
-              x,
-              y,
-              z: Math.round(zoom),
-            }
-          }
+          x,
+          y,
+          z: Math.round(zoom),
         }),
       );
     };
diff --git a/src/constants/common.ts b/src/constants/common.ts
index 973c26af..d8772414 100644
--- a/src/constants/common.ts
+++ b/src/constants/common.ts
@@ -4,3 +4,5 @@ export const FIRST_ARRAY_ELEMENT = 0;
 
 export const ONE = 1;
 export const SECOND_ARRAY_ELEMENT = 1;
+
+export const NOOP = (): void => {};
diff --git a/src/models/overviewImageLink.ts b/src/models/overviewImageLink.ts
index 7c31c710..6a36667a 100644
--- a/src/models/overviewImageLink.ts
+++ b/src/models/overviewImageLink.ts
@@ -1,19 +1,20 @@
 import { z } from 'zod';
 import { positionSchema } from './positionSchema';
 
-export const overviewImageLink = z.union([
-  z.object({
-    idObject: z.number(),
-    polygon: z.array(positionSchema),
-    imageLinkId: z.number(),
-    type: z.string(),
-  }),
-  z.object({
-    idObject: z.number(),
-    polygon: z.array(positionSchema),
-    zoomLevel: z.number(),
-    modelPoint: positionSchema,
-    modelLinkId: z.number(),
-    type: z.string(),
-  }),
-]);
+export const overviewImageLinkImage = z.object({
+  idObject: z.number(),
+  polygon: z.array(positionSchema),
+  imageLinkId: z.number(),
+  type: z.string(),
+});
+
+export const overviewImageLinkModel = z.object({
+  idObject: z.number(),
+  polygon: z.array(positionSchema),
+  zoomLevel: z.number(),
+  modelPoint: positionSchema,
+  modelLinkId: z.number(),
+  type: z.string(),
+});
+
+export const overviewImageLink = z.union([overviewImageLinkImage, overviewImageLinkModel]);
diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts
index ae325fb9..b17f1432 100644
--- a/src/redux/map/map.reducers.ts
+++ b/src/redux/map/map.reducers.ts
@@ -1,5 +1,13 @@
-import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
 import { ZERO } from '@/constants/common';
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import { getPointMerged } from '../../utils/object/getPointMerged';
+import { MAIN_MAP } from './map.constants';
+import {
+  initMapBackground,
+  initMapPosition,
+  initMapSizeAndModelId,
+  initOpenedMaps,
+} from './map.thunks';
 import {
   CloseMapAction,
   MapState,
@@ -9,14 +17,6 @@ import {
   SetMapDataAction,
   SetMapPositionDataAction,
 } from './map.types';
-import { MAIN_MAP } from './map.constants';
-import { getPointMerged } from '../../utils/object/getPointMerged';
-import {
-  initMapBackground,
-  initMapPosition,
-  initMapSizeAndModelId,
-  initOpenedMaps,
-} from './map.thunks';
 
 export const setMapDataReducer = (state: MapState, action: SetMapDataAction): void => {
   const payload = action.payload || {};
@@ -28,15 +28,14 @@ export const setMapDataReducer = (state: MapState, action: SetMapDataAction): vo
 };
 
 export const setMapPositionReducer = (state: MapState, action: SetMapPositionDataAction): void => {
-  const payload = action.payload || {};
-  const payloadPosition = 'position' in payload ? payload.position : undefined;
+  const position = action.payload || {};
   const statePosition = state.data.position;
 
   state.data = {
     ...state.data,
     position: {
-      initial: getPointMerged(payloadPosition?.initial || {}, statePosition.initial),
-      last: getPointMerged(payloadPosition?.last || {}, statePosition.last),
+      initial: getPointMerged(position || {}, statePosition.initial),
+      last: getPointMerged(position || {}, statePosition.last),
     },
   };
 };
diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts
index 9ed2e912..16f5e542 100644
--- a/src/redux/map/map.types.ts
+++ b/src/redux/map/map.types.ts
@@ -80,9 +80,7 @@ export type GetUpdatedMapDataResult = Pick<
   'modelId' | 'backgroundId' | 'size' | 'position'
 >;
 
-export type SetMapPositionDataActionPayload = GetUpdatedMapDataResult | object;
-
-export type SetMapPositionDataAction = PayloadAction<SetMapPositionDataActionPayload>;
+export type SetMapPositionDataAction = PayloadAction<Point>;
 
 export type InitMapDataActionPayload = {
   data: GetUpdatedMapDataResult | object;
diff --git a/src/redux/map/middleware/map.middleware.ts b/src/redux/map/middleware/map.middleware.ts
index 8083f40e..8d0f4b3e 100644
--- a/src/redux/map/middleware/map.middleware.ts
+++ b/src/redux/map/middleware/map.middleware.ts
@@ -2,17 +2,17 @@ import { currentBackgroundSelector } from '@/redux/backgrounds/background.select
 import type { AppListenerEffectAPI, AppStartListening } from '@/redux/store';
 import { getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
 import { Action, createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';
+import { mapOpenedMapPositionByIdSelector } from '../map.selectors';
 import {
+  closeMapAndSetMainMapActive,
   openMapAndSetActive,
   setActiveMap,
+  setMapBackground,
   setMapData,
   setMapPosition,
-  closeMapAndSetMainMapActive,
-  setMapBackground,
 } from '../map.slice';
 import { checkIfIsMapUpdateActionValid } from './checkIfIsMapUpdateActionValid';
 import { getUpdatedModel } from './getUpdatedModel';
-import { mapOpenedMapPositionByIdSelector } from '../map.selectors';
 
 export const mapListenerMiddleware = createListenerMiddleware();
 
@@ -39,7 +39,7 @@ export const mapDataMiddlewareListener = async (
     background,
   });
   dispatch(setMapData(updatedMapData));
-  dispatch(setMapPosition(updatedMapData));
+  dispatch(setMapPosition(updatedMapData.position.initial));
 };
 
 startListening({
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index b5534465..90759575 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -23,3 +23,12 @@ export const openOverviewImagesModalByIdReducer = (
     imageId: action.payload,
   };
 };
+
+export const setOverviewImageIdReducer = (
+  state: ModalState,
+  action: PayloadAction<number>,
+): void => {
+  state.overviewImagesState = {
+    imageId: action.payload,
+  };
+};
diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts
index a5ae1796..3dbe1970 100644
--- a/src/redux/modal/modal.slice.ts
+++ b/src/redux/modal/modal.slice.ts
@@ -4,6 +4,7 @@ import {
   closeModalReducer,
   openModalReducer,
   openOverviewImagesModalByIdReducer,
+  setOverviewImageIdReducer,
 } from './modal.reducers';
 
 const modalSlice = createSlice({
@@ -13,9 +14,11 @@ const modalSlice = createSlice({
     openModal: openModalReducer,
     closeModal: closeModalReducer,
     openOverviewImagesModalById: openOverviewImagesModalByIdReducer,
+    setOverviewImageId: setOverviewImageIdReducer,
   },
 });
 
-export const { openModal, closeModal, openOverviewImagesModalById } = modalSlice.actions;
+export const { openModal, closeModal, openOverviewImagesModalById, setOverviewImageId } =
+  modalSlice.actions;
 
 export default modalSlice.reducer;
diff --git a/src/types/models.ts b/src/types/models.ts
index f83079f1..0537282a 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -9,6 +9,11 @@ import { mapBackground } from '@/models/mapBackground';
 import { mapOverlay } from '@/models/mapOverlay';
 import { mapModelSchema } from '@/models/modelSchema';
 import { organism } from '@/models/organism';
+import {
+  overviewImageLink,
+  overviewImageLinkImage,
+  overviewImageLinkModel,
+} from '@/models/overviewImageLink';
 import { overviewImageView } from '@/models/overviewImageView';
 import { projectSchema } from '@/models/project';
 import { reactionSchema } from '@/models/reaction';
@@ -19,6 +24,9 @@ import { z } from 'zod';
 
 export type Project = z.infer<typeof projectSchema>;
 export type OverviewImageView = z.infer<typeof overviewImageView>;
+export type OverviewImageLink = z.infer<typeof overviewImageLink>;
+export type OverviewImageLinkImage = z.infer<typeof overviewImageLinkImage>;
+export type OverviewImageLinkModel = z.infer<typeof overviewImageLinkModel>;
 export type MapModel = z.infer<typeof mapModelSchema>;
 export type MapOverlay = z.infer<typeof mapOverlay>;
 export type MapBackground = z.infer<typeof mapBackground>;
-- 
GitLab