From 6c6b503546b085e7caa9c33dc32480e89f1db44b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Fri, 8 Dec 2023 11:38:28 +0100
Subject: [PATCH] feat: add render layer of overview image

---
 .../FunctionalArea/Modal/Modal.component.tsx  |  4 +-
 .../OverviewImageModal.types.ts               |  9 ++++
 .../OverviewImagesModal.component.tsx         | 24 +++++++++-
 .../utils/useOverviewImage.ts                 | 24 ++++++++++
 .../utils/useOverviewImageSize.ts             | 48 +++++++++++++++++++
 .../utils/useOverviewImageUrl.ts              | 13 +++++
 .../MapAdditionalOptions.component.tsx        |  7 ++-
 src/constants/project.ts                      |  9 ++++
 src/models/project.ts                         |  1 +
 src/redux/map/map.constants.ts                |  2 +
 src/redux/modal/modal.constants.ts            |  4 ++
 src/redux/modal/modal.mock.ts                 |  4 ++
 src/redux/modal/modal.reducers.ts             |  8 +++-
 src/redux/modal/modal.selector.ts             |  6 +++
 src/redux/modal/modal.slice.ts                |  6 +--
 src/redux/modal/modal.types.ts                |  5 ++
 src/redux/project/project.selectors.ts        | 22 +++++++++
 yarn.lock                                     |  6 +--
 18 files changed, 190 insertions(+), 12 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts
 create mode 100644 src/constants/project.ts

diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx
index 1a0f2f38..4ca533ed 100644
--- a/src/components/FunctionalArea/Modal/Modal.component.tsx
+++ b/src/components/FunctionalArea/Modal/Modal.component.tsx
@@ -24,14 +24,14 @@ export const Modal = (): React.ReactNode => {
       role={MODAL_ROLE}
     >
       <div className="flex h-full w-full items-center justify-center">
-        <div className="flex flex-col overflow-hidden	rounded-lg	">
+        <div className="flex h-5/6 w-10/12	flex-col	overflow-hidden rounded-lg">
           <div className="flex items-center justify-between bg-white p-[24px] text-xl">
             <div>{modalTitle}</div>
             <button type="button" onClick={handleCloseModal} aria-label="close button">
               <Icon name="close" className="fill-font-500" />
             </button>
           </div>
-          <div>{isOpen && modalName === 'overview-images' && <OverviewImagesModal />}</div>
+          {isOpen && modalName === 'overview-images' && <OverviewImagesModal />}
         </div>
       </div>
     </div>
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts
new file mode 100644
index 00000000..ec227bb3
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts
@@ -0,0 +1,9 @@
+export interface OverviewImageSize {
+  width: number;
+  height: number;
+}
+
+export interface ImageContainerSize {
+  width: number;
+  height: number;
+}
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
index 29c97f24..927b3805 100644
--- a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
@@ -1,5 +1,27 @@
+/* eslint-disable @next/next/no-img-element */
 import * as React from 'react';
+import { useCallback, useState } from 'react';
+import { useOverviewImage } from './utils/useOverviewImage';
 
 export const OverviewImagesModal: React.FC = () => {
-  return <div className="h-[200px] w-[500px] bg-white " />;
+  const [containerRect, setContainerRect] = useState<DOMRect>();
+  const { imageUrl, size } = useOverviewImage({ containerRect });
+  const { width, height } = size;
+
+  const handleRect = useCallback((node: HTMLDivElement | null) => {
+    if (!node) {
+      return;
+    }
+
+    setContainerRect(node.getBoundingClientRect());
+  }, []);
+
+  return (
+    <div className="flex h-full w-full items-center justify-center bg-white" ref={handleRect}>
+      <div className="relative" style={{ width, height }}>
+        <img alt="overview" className="block h-full w-full" src={imageUrl} />
+        {/* TODO: interactions - clickable elements (in next task) */}
+      </div>
+    </div>
+  );
 };
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts
new file mode 100644
index 00000000..db956acd
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts
@@ -0,0 +1,24 @@
+import { OverviewImageSize } from '../OverviewImageModal.types';
+import { useOverviewImageSize } from './useOverviewImageSize';
+import { useOverviewImageUrl } from './useOverviewImageUrl';
+
+interface UseOverviewImageArgs {
+  containerRect?: DOMRect;
+}
+
+interface UseOverviewImageResults {
+  imageUrl: string;
+  size: OverviewImageSize;
+}
+
+export const useOverviewImage = ({
+  containerRect,
+}: UseOverviewImageArgs): UseOverviewImageResults => {
+  const imageUrl = useOverviewImageUrl();
+  const size = useOverviewImageSize({ containerRect });
+
+  return {
+    size,
+    imageUrl,
+  };
+};
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts
new file mode 100644
index 00000000..96015735
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts
@@ -0,0 +1,48 @@
+import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project';
+import { currentOverviewImageSelector } from '@/redux/project/project.selectors';
+import { useSelector } from 'react-redux';
+import { ImageContainerSize, OverviewImageSize } from '../OverviewImageModal.types';
+
+interface UseOverviewImageArgs {
+  containerRect?: DOMRect;
+}
+
+interface UseOverviewImageResult extends OverviewImageSize {
+  sizeFactor: number;
+}
+
+const getFinalImageSize = (
+  containerSize: ImageContainerSize,
+  maxImageSize: OverviewImageSize,
+): UseOverviewImageResult => {
+  const maxHeight = Math.min(containerSize.height, maxImageSize.height);
+  const maxWidth = Math.min(containerSize.width, maxImageSize.width);
+
+  const heightSizeFactor = maxHeight / maxImageSize.height;
+  const widthSizeFactor = maxWidth / maxImageSize.width;
+  const sizeFactor = Math.min(heightSizeFactor, widthSizeFactor);
+
+  const width = maxImageSize.width * sizeFactor;
+  const height = maxImageSize.height * sizeFactor;
+
+  return {
+    height,
+    width,
+    sizeFactor,
+  };
+};
+
+export const useOverviewImageSize = ({
+  containerRect,
+}: UseOverviewImageArgs): UseOverviewImageResult => {
+  const currentImage = useSelector(currentOverviewImageSelector);
+
+  if (!currentImage || !containerRect) return DEFAULT_OVERVIEW_IMAGE_SIZE;
+
+  const maxImageSize = {
+    width: currentImage.width,
+    height: currentImage.height,
+  };
+
+  return getFinalImageSize(containerRect, maxImageSize);
+};
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts
new file mode 100644
index 00000000..af9e09fe
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts
@@ -0,0 +1,13 @@
+import { BASE_MAP_IMAGES_URL } from '@/constants';
+import { currentOverviewImageSelector } from '@/redux/project/project.selectors';
+import { useSelector } from 'react-redux';
+
+export const useOverviewImageUrl = (): string => {
+  const currentImage = useSelector(currentOverviewImageSelector);
+
+  if (!currentImage) {
+    return '';
+  }
+
+  return `${BASE_MAP_IMAGES_URL}/map_images/${currentImage.filename}`;
+};
diff --git a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx
index 1435e558..84dec36a 100644
--- a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx
+++ b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx
@@ -1,6 +1,8 @@
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
-import { openOverviewImagesModal } from '@/redux/modal/modal.slice';
+import { openOverviewImagesModalById } from '@/redux/modal/modal.slice';
+import { projectDefaultOverviewImageIdSelector } from '@/redux/project/project.selectors';
 import { Button } from '@/shared/Button';
+import { useSelector } from 'react-redux';
 import { twMerge } from 'tailwind-merge';
 import { BackgroundSelector } from './BackgroundsSelector';
 
@@ -8,9 +10,10 @@ import { BackgroundSelector } from './BackgroundsSelector';
 
 export const MapAdditionalOptions = (): JSX.Element => {
   const dispatch = useAppDispatch();
+  const defaultOverviewImageId = useSelector(projectDefaultOverviewImageIdSelector);
 
   const handleBrowseOverviewImagesClick = (): void => {
-    dispatch(openOverviewImagesModal());
+    dispatch(openOverviewImagesModalById(defaultOverviewImageId));
   };
 
   return (
diff --git a/src/constants/project.ts b/src/constants/project.ts
new file mode 100644
index 00000000..04fad94b
--- /dev/null
+++ b/src/constants/project.ts
@@ -0,0 +1,9 @@
+export const DEFAULT_OVERVIEW_IMAGE_WIDTH = 800;
+
+export const DEFAULT_OVERVIEW_IMAGE_HEIGHT = 500;
+
+export const DEFAULT_OVERVIEW_IMAGE_SIZE = {
+  width: DEFAULT_OVERVIEW_IMAGE_WIDTH,
+  height: DEFAULT_OVERVIEW_IMAGE_HEIGHT,
+  sizeFactor: 0,
+};
diff --git a/src/models/project.ts b/src/models/project.ts
index 9f763751..844fe680 100644
--- a/src/models/project.ts
+++ b/src/models/project.ts
@@ -20,4 +20,5 @@ export const projectSchema = z.object({
   creationDate: z.string(),
   mapCanvasType: z.string(),
   overviewImageViews: z.array(overviewImageView),
+  topOverviewImage: overviewImageView,
 });
diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts
index 333a2062..e02cb78f 100644
--- a/src/redux/map/map.constants.ts
+++ b/src/redux/map/map.constants.ts
@@ -14,6 +14,8 @@ export const MODEL_ID_DEFAULT: number = 0;
 
 export const BACKGROUND_ID_DEFAULT: number = 0;
 
+export const OVERVIEW_IMAGE_ID_DEFAULT: number = 0;
+
 export const MAP_DATA_INITIAL_STATE: MapData = {
   projectId: PROJECT_ID,
   meshId: '',
diff --git a/src/redux/modal/modal.constants.ts b/src/redux/modal/modal.constants.ts
index 90ee066d..2266911d 100644
--- a/src/redux/modal/modal.constants.ts
+++ b/src/redux/modal/modal.constants.ts
@@ -1,7 +1,11 @@
+import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants';
 import { ModalState } from './modal.types';
 
 export const MODAL_INITIAL_STATE: ModalState = {
   isOpen: false,
   modalName: 'none',
   modalTitle: '',
+  overviewImagesState: {
+    imageId: OVERVIEW_IMAGE_ID_DEFAULT,
+  },
 };
diff --git a/src/redux/modal/modal.mock.ts b/src/redux/modal/modal.mock.ts
index f145fba0..2f202d4b 100644
--- a/src/redux/modal/modal.mock.ts
+++ b/src/redux/modal/modal.mock.ts
@@ -1,7 +1,11 @@
+import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants';
 import { ModalState } from './modal.types';
 
 export const MODAL_INITIAL_STATE_MOCK: ModalState = {
   isOpen: false,
   modalName: 'none',
   modalTitle: '',
+  overviewImagesState: {
+    imageId: OVERVIEW_IMAGE_ID_DEFAULT,
+  },
 };
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
index 17687aca..b5534465 100644
--- a/src/redux/modal/modal.reducers.ts
+++ b/src/redux/modal/modal.reducers.ts
@@ -12,8 +12,14 @@ export const closeModalReducer = (state: ModalState): void => {
   state.modalName = 'none';
 };
 
-export const openOverviewImagesModalReducer = (state: ModalState): void => {
+export const openOverviewImagesModalByIdReducer = (
+  state: ModalState,
+  action: PayloadAction<number>,
+): void => {
   state.isOpen = true;
   state.modalName = 'overview-images';
   state.modalTitle = 'Overview images';
+  state.overviewImagesState = {
+    imageId: action.payload,
+  };
 };
diff --git a/src/redux/modal/modal.selector.ts b/src/redux/modal/modal.selector.ts
index 6221d93c..c233d253 100644
--- a/src/redux/modal/modal.selector.ts
+++ b/src/redux/modal/modal.selector.ts
@@ -1,6 +1,12 @@
 import { createSelector } from '@reduxjs/toolkit';
+import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants';
 import { rootSelector } from '../root/root.selectors';
 
 export const modalSelector = createSelector(rootSelector, state => state.modal);
 
 export const isModalOpenSelector = createSelector(modalSelector, state => state.isOpen);
+
+export const currentOverviewImageId = createSelector(
+  modalSelector,
+  modal => modal?.overviewImagesState.imageId || OVERVIEW_IMAGE_ID_DEFAULT,
+);
diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts
index 9ba21287..a5ae1796 100644
--- a/src/redux/modal/modal.slice.ts
+++ b/src/redux/modal/modal.slice.ts
@@ -3,7 +3,7 @@ import { MODAL_INITIAL_STATE } from './modal.constants';
 import {
   closeModalReducer,
   openModalReducer,
-  openOverviewImagesModalReducer,
+  openOverviewImagesModalByIdReducer,
 } from './modal.reducers';
 
 const modalSlice = createSlice({
@@ -12,10 +12,10 @@ const modalSlice = createSlice({
   reducers: {
     openModal: openModalReducer,
     closeModal: closeModalReducer,
-    openOverviewImagesModal: openOverviewImagesModalReducer,
+    openOverviewImagesModalById: openOverviewImagesModalByIdReducer,
   },
 });
 
-export const { openModal, closeModal, openOverviewImagesModal } = modalSlice.actions;
+export const { openModal, closeModal, openOverviewImagesModalById } = modalSlice.actions;
 
 export default modalSlice.reducer;
diff --git a/src/redux/modal/modal.types.ts b/src/redux/modal/modal.types.ts
index 77b0e71f..cad6ec7d 100644
--- a/src/redux/modal/modal.types.ts
+++ b/src/redux/modal/modal.types.ts
@@ -1,7 +1,12 @@
 import { ModalName } from '@/types/modal';
 
+export type OverviewImagesModalState = {
+  imageId?: number;
+};
+
 export interface ModalState {
   isOpen: boolean;
   modalName: ModalName;
   modalTitle: string;
+  overviewImagesState: OverviewImagesModalState;
 }
diff --git a/src/redux/project/project.selectors.ts b/src/redux/project/project.selectors.ts
index 2725956a..970ef322 100644
--- a/src/redux/project/project.selectors.ts
+++ b/src/redux/project/project.selectors.ts
@@ -1,6 +1,28 @@
+import { OverviewImageView } from '@/types/models';
 import { createSelector } from '@reduxjs/toolkit';
+import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants';
+import { currentOverviewImageId } from '../modal/modal.selector';
 import { rootSelector } from '../root/root.selectors';
 
 export const projectSelector = createSelector(rootSelector, state => state.project);
 
 export const projectDataSelector = createSelector(projectSelector, project => project?.data);
+
+export const projectDefaultOverviewImageIdSelector = createSelector(
+  projectDataSelector,
+  projectData => projectData?.topOverviewImage.idObject || OVERVIEW_IMAGE_ID_DEFAULT,
+);
+
+export const currentOverviewImageSelector = createSelector(
+  projectDataSelector,
+  currentOverviewImageId,
+  (projectData, imageId): OverviewImageView | undefined =>
+    (projectData?.overviewImageViews || []).find(
+      overviewImage => overviewImage.idObject === imageId,
+    ),
+);
+
+export const projectDirectorySelector = createSelector(
+  projectDataSelector,
+  projectData => projectData?.directory,
+);
diff --git a/yarn.lock b/yarn.lock
index d403655e..3ba2ee3e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -847,9 +847,9 @@
   "resolved" "https://registry.npmjs.org/@next/font/-/font-13.5.4.tgz"
   "version" "13.5.4"
 
-"@next/swc-darwin-x64@13.4.19":
-  "integrity" "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw=="
-  "resolved" "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz"
+"@next/swc-darwin-arm64@13.4.19":
+  "integrity" "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ=="
+  "resolved" "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz"
   "version" "13.4.19"
 
 "@nodelib/fs.scandir@2.1.5":
-- 
GitLab