diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts index ec227bb337061f3d5bafca30f35a8f46253829c5..a09968c65631dad23f8b8df5fca373d2b35e42a2 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 ef0e608a1086bd4e72defc88889523994d704d0c..86f7e1f263692f8b6f48a87bdadc1e2044120f62 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 0000000000000000000000000000000000000000..66a82ad7c2cb69bd564bd488cc98e4af9cb58940 --- /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 f4d964d3095eb52532ff2ed7097a146cac1f6d3e..cd7468fd69680ce98645251e955ece392dc61994 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 db956acda5b183bcd854960c17242eb303fc3282..e09467b051efbdca9720a87f661027c85b7986b8 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 0000000000000000000000000000000000000000..fa6c658c192ae69a2196a5ca58b3ba2675aa0350 --- /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 0000000000000000000000000000000000000000..622f8aa83039824658accbbb9b4956577c5ef1dd --- /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 2832e68da8c053073a8d9bf713a99251c4e42a6a..48b643ceab96db55df795a156aa6880fa3ac3e53 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 482f0509d7bbdd3b3da128a3ce3638c818302d12..8bdf55b6e8614a029db153428d9f7defd248b114 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 973c26af876d5e91f9e0599ff1a21571d96579b5..d8772414c1d8377da3b1590922c5aac46a85e106 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 7c31c710645441466fa82e65a036c9065c84d511..6a36667a3f7fd50cd76c5bc209ef06add6a6312c 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 ae325fb9127f901719094ef2883ce029581aede1..b17f1432b0cad6c95e8877e6970d2c6de5d501fa 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 9ed2e9129f3906fbce9393b8525a1a289827377c..16f5e54210814eca8c851769a049622b2b4057c1 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 8083f40e940043bc116ec8cdcaca7f1bbeb233cc..8d0f4b3e54a016ff95899e88f9802a0d70ac9e4b 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 b5534465a8a26d95da67664a8425eeb9cc27840f..90759575d6c3ec73246a2d402d7ed5489875f3b7 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 a5ae179621048583f838a2427fe399b88c236913..3dbe1970fb135799870640bf00c38f3ab5c9a080 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 f83079f195cdeced1fdc30201e5fc87ea404fe77..0537282a1fe4fa95e2636cc35bf4c1d54064b0e6 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>;