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 0000000000000000000000000000000000000000..ec227bb337061f3d5bafca30f35a8f46253829c5 --- /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.test.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b8c92be95826c1dd894928b759f8ea36dd29f218 --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.test.tsx @@ -0,0 +1,104 @@ +import { BASE_MAP_IMAGES_URL } from '@/constants'; +import { projectFixture } from '@/models/fixtures/projectFixture'; +import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock'; +import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock'; +import { StoreType } from '@/redux/store'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { render, screen } from '@testing-library/react'; +import { OverviewImagesModal } from './OverviewImagesModal.component'; + +jest.mock('./utils/useOverviewImageSize', () => ({ + __esModule: true, + useOverviewImageSize: jest.fn().mockImplementation(() => ({ + width: 200, + height: 300, + })), +})); + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <OverviewImagesModal /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('OverviewImagesModal - component', () => { + describe('when currentImage is NOT valid', () => { + beforeEach(() => { + renderComponent({ + project: { + data: { + ...projectFixture, + overviewImageViews: [], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: 0, + }, + }, + }); + }); + + it('should not render component', () => { + const element = screen.queryByTestId('overview-images-modal'); + expect(element).toBeNull(); + }); + }); + + describe('when currentImage is valid', () => { + beforeEach(() => { + renderComponent({ + project: { + data: { + ...projectFixture, + overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject, + }, + }, + }); + }); + + it('should render component', () => { + const element = screen.queryByTestId('overview-images-modal'); + expect(element).not.toBeNull(); + }); + + it('should render image with valid src', () => { + const imageElement = screen.getByAltText('overview'); + const result = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`; + expect(imageElement.getAttribute('src')).toBe(result); + }); + + it('should render image wrapper with valid size', () => { + const imageElement = screen.getByAltText('overview'); + const wrapperElement = imageElement.closest('div'); + const wrapperStyle = wrapperElement?.getAttribute('style'); + + expect(wrapperStyle).toBe('width: 200px; height: 300px;'); + }); + }); +}); diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx index 29c97f248fc1fbefe6fab9c85a89fee2c2461c0b..ef0e608a1086bd4e72defc88889523994d704d0c 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx @@ -1,5 +1,35 @@ +/* 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()); + }, []); + + if (!imageUrl) { + return null; + } + + return ( + <div + data-testid="overview-images-modal" + 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/getFinalImageSize.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getFinalImageSize.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4d85760078b81b965d113a2ed8ae537404622de --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getFinalImageSize.test.ts @@ -0,0 +1,33 @@ +import { getFinalImageSize } from './getFinalImageSize'; + +describe('getFinalImageSize - util', () => { + const cases = [ + [ + { width: 0, height: 0 }, + { width: 0, height: 0 }, + { width: 0, height: 0, sizeFactor: 0 }, + ], + [ + { width: 100, height: 100 }, + { width: 100, height: 100 }, + { width: 100, height: 100, sizeFactor: 1 }, + ], + [ + { width: 100, height: 100 }, + { width: 200, height: 250 }, + { width: 80, height: 100, sizeFactor: 0.4 }, + ], + [ + { width: 10, height: 40 }, + { width: 40, height: 60 }, + { width: 10, height: 15, sizeFactor: 0.25 }, + ], + ]; + + it.each(cases)( + 'should return valid size and size factor', + (containerSize, maxImageSize, finalSize) => { + expect(getFinalImageSize(containerSize, maxImageSize)).toStrictEqual(finalSize); + }, + ); +}); diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getFinalImageSize.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getFinalImageSize.ts new file mode 100644 index 0000000000000000000000000000000000000000..877bc9fd8d69e85f1e314f8b8f627d2b59647d9e --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/getFinalImageSize.ts @@ -0,0 +1,27 @@ +import { ZERO } from '@/constants/common'; +import { ImageContainerSize, OverviewImageSize } from '../OverviewImageModal.types'; + +interface GetFinalImageSizeResult extends OverviewImageSize { + sizeFactor: number; +} + +export const getFinalImageSize = ( + containerSize: ImageContainerSize, + maxImageSize: OverviewImageSize, +): GetFinalImageSizeResult => { + 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: height || ZERO, + width: width || ZERO, + sizeFactor: sizeFactor || ZERO, + }; +}; diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4d964d3095eb52532ff2ed7097a146cac1f6d3e --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.test.ts @@ -0,0 +1,116 @@ +import { BASE_MAP_IMAGES_URL } from '@/constants'; +import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project'; +import { projectFixture } from '@/models/fixtures/projectFixture'; +import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock'; +import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; +import { useOverviewImage } from './useOverviewImage'; + +describe('useOverviewImage - hook', () => { + describe('when image data is invalid', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: 0, + }, + }, + }); + + const { result } = renderHook(() => useOverviewImage({ containerRect: undefined }), { + wrapper: Wrapper, + }); + + it('should return default size of image and empty imageUrl', () => { + expect(result.current).toStrictEqual({ + imageUrl: '', + size: DEFAULT_OVERVIEW_IMAGE_SIZE, + }); + }); + }); + + describe('when containerReact is undefined', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject, + }, + }, + }); + + const { result } = renderHook(() => useOverviewImage({ containerRect: undefined }), { + wrapper: Wrapper, + }); + + 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({ + imageUrl, + size: DEFAULT_OVERVIEW_IMAGE_SIZE, + }); + }); + }); + + describe('when containerReact is valid', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [ + { + ...PROJECT_OVERVIEW_IMAGE_MOCK, + height: 500, + width: 500, + }, + ], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject, + }, + }, + }); + + const { result } = renderHook( + () => useOverviewImage({ containerRect: { width: 100, height: 200 } as DOMRect }), + { + wrapper: Wrapper, + }, + ); + + 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({ + 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 new file mode 100644 index 0000000000000000000000000000000000000000..db956acda5b183bcd854960c17242eb303fc3282 --- /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.test.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b5c8b6b555fc20703fdc2b0344fc1dbc3b37020f --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.test.tsx @@ -0,0 +1,104 @@ +/* eslint-disable no-magic-numbers */ +import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project'; +import { projectFixture } from '@/models/fixtures/projectFixture'; +import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock'; +import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; +import { useOverviewImageSize } from './useOverviewImageSize'; + +describe('useOverviewImageSize - hook', () => { + describe('when currentImage is not valid', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: 0, + }, + }, + }); + + const { result } = renderHook( + () => useOverviewImageSize({ containerRect: { width: 800, height: 600 } as DOMRect }), + { + wrapper: Wrapper, + }, + ); + + it('should return default value', () => { + expect(result.current).toStrictEqual(DEFAULT_OVERVIEW_IMAGE_SIZE); + }); + }); + + describe('when containerRect is not valid', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject, + }, + }, + }); + + const { result } = renderHook(() => useOverviewImageSize({ containerRect: undefined }), { + wrapper: Wrapper, + }); + + it('should return default value', () => { + expect(result.current).toStrictEqual(DEFAULT_OVERVIEW_IMAGE_SIZE); + }); + }); + + describe('when data is valid', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject, + }, + }, + }); + + const { result } = renderHook( + () => useOverviewImageSize({ containerRect: { width: 1600, height: 1000 } as DOMRect }), + { + wrapper: Wrapper, + }, + ); + + const { height, width, sizeFactor } = result.current; + + it('should return calculated height, width, sizeFactor', () => { + expect(height).toBeCloseTo(1000); + expect(width).toBeCloseTo(1429.7); + expect(sizeFactor).toBeCloseTo(0.247); + }); + }); +}); 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 0000000000000000000000000000000000000000..1623d5261593d12df401ca37a5b7b005a6d6720c --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts @@ -0,0 +1,28 @@ +import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project'; +import { currentOverviewImageSelector } from '@/redux/project/project.selectors'; +import { useSelector } from 'react-redux'; +import { OverviewImageSize } from '../OverviewImageModal.types'; +import { getFinalImageSize } from './getFinalImageSize'; + +interface UseOverviewImageArgs { + containerRect?: DOMRect; +} + +interface UseOverviewImageResult extends OverviewImageSize { + sizeFactor: number; +} + +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.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5e9beb81e816e0c24690dc03128b31d86f26c17 --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.test.ts @@ -0,0 +1,63 @@ +import { BASE_MAP_IMAGES_URL } from '@/constants'; +import { projectFixture } from '@/models/fixtures/projectFixture'; +import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock'; +import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; +import { useOverviewImageUrl } from './useOverviewImageUrl'; + +describe('useOverviewImageUrl - hook', () => { + describe('when currentImage data is valid', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: 0, + }, + }, + }); + + it('should return valid url', () => { + const { result } = renderHook(() => useOverviewImageUrl(), { wrapper: Wrapper }); + + expect(result.current).toBe(''); + }); + }); + + describe('when currentImage data is valid', () => { + const { Wrapper } = getReduxWrapperWithStore({ + project: { + data: { + ...projectFixture, + overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + }, + modal: { + ...MODAL_INITIAL_STATE_MOCK, + overviewImagesState: { + imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject, + }, + }, + }); + + it('should return valid url', () => { + const { result } = renderHook(() => useOverviewImageUrl(), { wrapper: Wrapper }); + + expect(result.current).toBe( + `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`, + ); + }); + }); +}); 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 0000000000000000000000000000000000000000..af9e09fe05b7d6ec6b6bab6fca489b438354066e --- /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 1435e5584ba55490c2b4d1c66d6460536f120db9..84dec36a881d484691c2845d80688ca9dba1056c 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 0000000000000000000000000000000000000000..85fd9c6855e6d6389eaf85271fbf56f1d2963189 --- /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: 1, +}; diff --git a/src/models/project.ts b/src/models/project.ts index 9f763751b2a179aac4d7cac803d565c3e1256481..844fe68047b7407e3a4e17152290f9778905ceb0 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 333a20627dc73fa4844b919b580d2231b1539c13..e02cb78f3982501deb21e653402729dbafdfafd9 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 90ee066d340c2a6dde4b423d6f844dbbed82bbcc..2266911df056dfdf677843aaba8a3e5609fd0bc5 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 f145fba0355bec8fd70ee9bfd73431adf8c48759..2f202d4b42c8c69314c144b93c21e4e6705fd1c1 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 17687aca85709fada1de1ad607ab063f17ac3f69..b5534465a8a26d95da67664a8425eeb9cc27840f 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 6221d93ca0f02dfebb5af2e2455ff31808c8a793..c233d253a934a2ef5be26cf88a3c6df7a9e8e61f 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 9ba21287e7cad925a28f88135950b5c8629d2143..a5ae179621048583f838a2427fe399b88c236913 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 77b0e71fabc18cd84b1df6145d393d4b134325c6..cad6ec7d02b8efeff5aabb75e944ef5f15b1b0c7 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.mock.ts b/src/redux/project/project.mock.ts index 036d26346ca92cb0a8108cb83b7c7584db660381..dada67e42131deef75d6f209e0f660a609931340 100644 --- a/src/redux/project/project.mock.ts +++ b/src/redux/project/project.mock.ts @@ -1,4 +1,5 @@ import { DEFAULT_ERROR } from '@/constants/errors'; +import { OverviewImageView } from '@/types/models'; import { ProjectState } from './project.types'; export const PROJECT_STATE_INITIAL_MOCK: ProjectState = { @@ -6,3 +7,165 @@ export const PROJECT_STATE_INITIAL_MOCK: ProjectState = { loading: 'idle', error: DEFAULT_ERROR, }; + +export const PROJECT_OVERVIEW_IMAGE_MOCK: OverviewImageView = { + idObject: 440, + filename: '9d4911bdeeea752f076e57a91d9b1f45/biolayout_main_root.png', + width: 5776, + height: 4040, + links: [ + { + idObject: 2062, + polygon: [ + { + x: 515, + y: 2187, + }, + { + x: 1073, + y: 2187, + }, + { + x: 1073, + y: 2520, + }, + { + x: 515, + y: 2520, + }, + ], + zoomLevel: 4, + modelPoint: { + x: 3473, + y: 5871, + }, + modelLinkId: 5053, + type: 'OverviewModelLink', + }, + { + idObject: 2063, + polygon: [ + { + x: 2410, + y: 1360, + }, + { + x: 2692, + y: 1360, + }, + { + x: 2692, + y: 1570, + }, + { + x: 2410, + y: 1570, + }, + ], + imageLinkId: 435, + type: 'OverviewImageLink', + }, + { + idObject: 2064, + polygon: [ + { + x: 2830, + y: 497, + }, + { + x: 3256, + y: 497, + }, + { + x: 3256, + y: 832, + }, + { + x: 2830, + y: 832, + }, + ], + zoomLevel: 5, + modelPoint: { + x: 8081, + y: 1240, + }, + modelLinkId: 5053, + type: 'OverviewModelLink', + }, + { + idObject: 2065, + polygon: [ + { + x: 3232, + y: 2259, + }, + { + x: 3520, + y: 2259, + }, + { + x: 3520, + y: 2456, + }, + { + x: 3232, + y: 2456, + }, + ], + imageLinkId: 433, + type: 'OverviewImageLink', + }, + { + idObject: 2066, + polygon: [ + { + x: 4205, + y: 761, + }, + { + x: 4625, + y: 761, + }, + { + x: 4625, + y: 1102, + }, + { + x: 4205, + y: 1102, + }, + ], + zoomLevel: 5, + modelPoint: { + x: 7488, + y: 11986, + }, + modelLinkId: 5053, + type: 'OverviewModelLink', + }, + { + idObject: 2067, + polygon: [ + { + x: 4960, + y: 1971, + }, + { + x: 5241, + y: 1971, + }, + { + x: 5241, + y: 2163, + }, + { + x: 4960, + y: 2163, + }, + ], + imageLinkId: 434, + type: 'OverviewImageLink', + }, + ], +}; diff --git a/src/redux/project/project.selectors.ts b/src/redux/project/project.selectors.ts index 2725956ae48088e4c4c8de5054b5425c3a73981d..970ef322a089277a96da126713a463695a483a04 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 d403655e3377f5c0dc897ba526a0756ced375814..3ba2ee3e331785c9cd9c4f37c51d09a81789e163 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":