diff --git a/docs/data/bioentities.md b/docs/plugins/data/bioentities.md similarity index 100% rename from docs/data/bioentities.md rename to docs/plugins/data/bioentities.md diff --git a/docs/plugins/errors.md b/docs/plugins/errors.md index fca95f8b1558a7e48087d53b5a8b736cc549eea8..fd7d29c75311eb1ca2028e60e570e57e7955e371 100644 --- a/docs/plugins/errors.md +++ b/docs/plugins/errors.md @@ -17,3 +17,9 @@ ## Project Errors - **Project does not exist**: This error occurs when the project data is not available. + +## Zoom errors + +- **Provided zoom value exeeds max zoom of ...**: This error occurs when `zoom` param of `setZoom` exeeds max zoom value of the selected map + +- **Provided zoom value exceeds min zoom of ...**: This error occurs when `zoom` param of `setZoom` exceeds min zoom value of the selected map diff --git a/docs/plugins/map/position.md b/docs/plugins/map/position.md new file mode 100644 index 0000000000000000000000000000000000000000..a08872c6e6c00f67b041ef4b97641ef3ff84580e --- /dev/null +++ b/docs/plugins/map/position.md @@ -0,0 +1,63 @@ +### Map positon + +With use of the methods below plugins can access and modify user position data. + +#### Get zoom + +To get current zoom value, plugins can use the `window.minerva.map.getZoom()` method, which returns current zoom value as a number. + +**Example:** + +```ts +const currentZoom = window.minerva.map.getZoom(); +console.log(currentZoom); // 5 +``` + +#### Set zoom + +To modify current zoom value, plugins can use the `window.minerva.map.setZoom(zoom)` method. This function accepts non-negative number as an argument and returns nothing. If argument is invalid, `setZoom` method throws an error. + +**Valid example:** + +```ts +window.minerva.map.setZoom(7.54); +console.log(window.minerva.map.getZoom()); // 7.54 +``` + +**Invalid example:** + +```ts +window.minerva.map.setZoom(-14); +// Uncaught ZodError: [...] +``` + +#### Get center + +User position is defined as center coordinate. It's value is defined as x/y/z points of current viewport center translated to map position. Plugins can access center value and modify it. + +To get current position value, plugins can use the `window.minerva.map.getCenter()` method which returns current position value as an object containing `x`, `y` and `z` fields. All of them are non-negative numbers but `z` is an optional field and it defines current zoom value. If argument is invalid, `getCenter` method throws an error. + +**Valid example:** + +```ts +const currentCenter = window.minerva.map.getCenter(); +console.log(currentCenter); // {x: 13256, y: 8118, z: 5} +``` + +#### Set center + +To modify position center value plugins can use `window.minerva.map.setCenter(positionObject)` which accepts single object as an argument and returns nothing. This object should contain `x`, `y` fields and `z` optionally. All of them are non-negative numbers. If argument is invalid, `setCenter` method throws an error. + +**Valid example:** + +```ts +window.minerva.map.setCenter({ x: 13256, y: 8118, z: 5 }); +console.log(window.minerva.map.getCenter()); // {x: 13256, y: 8118, z: 5} +``` + +**Invalid example:** + +```ts +window.minerva.map.setCenter({ x: 13256, y: 8118, z: -5 }); +// Uncaught ZodError: [...] +``` diff --git a/docs/plugins/overview-images.md b/docs/plugins/overview-images.md new file mode 100644 index 0000000000000000000000000000000000000000..b425d071b1aa1cdbf327a383e28b986e51a1eb91 --- /dev/null +++ b/docs/plugins/overview-images.md @@ -0,0 +1,46 @@ +### Overview images + +The methods contained within 'Overview images' are used to access data on Overview images and modify behavior of Overview images modal. + +Below is a description of the methods, as well as the types they return. description of the object types can be found in folder `/docs/types/`. + +**Available data access methods include:** + +- `getCurrentOverviewImage` + - gets currently selected overview image + - returns `OverviewImageView` or `undefined` +- `getOverviewImage` + - gets all loaded overview images + - returns array of `OverviewImageView` + +**Available data modify methods include:** + +##### `hideOverviewImageModal` + +- accepts no arguments +- hides overview image modal if opened +- returns nothing +- example: + ```ts + window.minerva.overviewImage.hideOverviewImageModal(); + ``` + +##### `selectOverviewImage` + +- accepts single argument of number representing id of one of loaded overview images +- selects overview image of provided id as current, if image does not exists throws an error +- returns nothing +- example: + ```ts + window.minerva.overviewImage.selectOverviewImage(42); + ``` + +##### `showOverviewImageModal` + +- accepts single argument of number representing id of one of loaded overview images +- selects overview image of provided id as current and opens overview image modal, if image does not exists throws an error +- returns nothing +- example: + ```ts + window.minerva.overviewImage.showOverviewImageModal(24); + ``` diff --git a/docs/types/OverviewImageView.md b/docs/types/OverviewImageView.md new file mode 100644 index 0000000000000000000000000000000000000000..33cf4cce60d81e907af4d6cd37132b2e1aff6347 --- /dev/null +++ b/docs/types/OverviewImageView.md @@ -0,0 +1,88 @@ +```json +{ + "type": "object", + "properties": { + "idObject": { + "type": "number" + }, + "filename": { + "type": "string" + }, + "width": { + "type": "number" + }, + "height": { + "type": "number" + }, + "links": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "idObject": { + "type": "number" + }, + "polygon": { + "type": "array", + "items": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + }, + "required": ["x", "y"], + "additionalProperties": false + } + }, + "imageLinkId": { + "type": "number" + }, + "type": { + "type": "string" + } + }, + "required": ["idObject", "polygon", "imageLinkId", "type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "idObject": { + "type": "number" + }, + "polygon": { + "type": "array", + "items": { + "$ref": "#/definitions/overviewImageView/properties/links/items/anyOf/0/properties/polygon/items" + } + }, + "zoomLevel": { + "type": "number" + }, + "modelPoint": { + "$ref": "#/definitions/overviewImageView/properties/links/items/anyOf/0/properties/polygon/items" + }, + "modelLinkId": { + "type": "number" + }, + "type": { + "type": "string" + } + }, + "required": ["idObject", "polygon", "zoomLevel", "modelPoint", "modelLinkId", "type"], + "additionalProperties": false + } + ] + } + } + }, + "required": ["idObject", "filename", "width", "height", "links"], + "additionalProperties": false +} +``` diff --git a/index.d.ts b/index.d.ts index 3cd0efd1a8805d41f089cca9aa3f9a7fd87171d8..fcac0d8b269f59bfa4610ff8051b1266636ba44f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,10 +5,13 @@ import { triggerSearch } from '@/services/pluginsManager/map/triggerSearch'; import { MinervaConfiguration } from '@/services/pluginsManager/pluginsManager'; import { MapInstance } from '@/types/map'; import { getModels } from '@/services/pluginsManager/map/models/getModels'; -import { OpenMapArgs, openMap } from '@/services/pluginsManager/map/openMap'; +import { openMap } from '@/services/pluginsManager/map/openMap'; +import { getCenter } from '@/services/pluginsManager/map/position/getCenter'; +import { setCenter } from '@/services/pluginsManager/map/position/setCenter'; import { triggerSearch } from '@/services/pluginsManager/map/triggerSearch'; +import { getZoom } from '@/services/pluginsManager/map/zoom/getZoom'; +import { setZoom } from '@/services/pluginsManager/map/zoom/setZoom'; import { MinervaConfiguration } from '@/services/pluginsManager/pluginsManager'; -import { MapModel } from '@/types/models'; import { getDisease } from '@/services/pluginsManager/project/data/getDisease'; import { getName } from '@/services/pluginsManager/project/data/getName'; import { getOrganism } from '@/services/pluginsManager/project/data/getOrganism'; @@ -49,6 +52,17 @@ declare global { fitBounds: typeof fitBounds; openMap: typeof openMap; triggerSearch: typeof triggerSearch; + getZoom: typeof getZoom; + setZoom: typeof setZoom; + getCenter: typeof getCenter; + setCenter: typeof setCenter; + }; + overviewImage: { + getCurrentOverviewImage: typeof getCurrentOverviewImage; + getOverviewImages: typeof getOverviewImages; + hideOverviewImageModal: typeof hideOverviewImageModal; + selectOverviewImage: typeof selectOverviewImage; + showOverviewImageModal: typeof showOverviewImageModal; }; project: { data: { diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx index d944235a29d04b7a1c376869c37cdc29e502ed2d..4fae37530de53dd219cd0d5118bbe12aded2d3ae 100644 --- a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx +++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx @@ -1,14 +1,14 @@ /* eslint-disable no-magic-numbers */ +import { MODELS_MOCK } from '@/redux/compartmentPathways/compartmentPathways.mock'; +import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; +import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; +import { StoreType } from '@/redux/store'; +import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { InitialStoreState, getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; -import { StoreType } from '@/redux/store'; -import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; import { act, render, screen, within } from '@testing-library/react'; -import { MODELS_MOCK } from '@/redux/compartmentPathways/compartmentPathways.mock'; -import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; -import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { MapNavigation } from './MapNavigation.component'; const MAIN_MAP_ID = 5053; diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts index 55c6230d3e238a7a7595957d217c1025aafaaa65..609cc0f5ceaf38a4ee64f1433e553e11ea9bf7ce 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts @@ -12,11 +12,11 @@ import { } from '@/redux/map/map.fixtures'; import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock'; import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock'; +import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { OverviewImageLink } from '@/types/models'; import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { renderHook } from '@testing-library/react'; -import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { FIRST_ARRAY_ELEMENT, NOOP, @@ -313,7 +313,6 @@ describe('useOverviewImageLinkActions - hook', () => { jest.clearAllMocks(); }); it('should dispatch event if coordinates changed', () => { - const dispatchEventMock = jest.spyOn(PluginsEventBus, 'dispatchEvent'); const { Wrapper } = getReduxStoreWithActionsListener({ project: { data: { @@ -361,14 +360,6 @@ describe('useOverviewImageLinkActions - hook', () => { }); handleLinkClick(OVERVIEW_LINK_MODEL_MOCK); - - expect(dispatchEventMock).toHaveBeenCalledTimes(2); - expect(dispatchEventMock).toHaveBeenCalledWith('onZoomChanged', { modelId: 5053, zoom: 7 }); - expect(dispatchEventMock).toHaveBeenCalledWith('onCenterChanged', { - modelId: 5053, - x: 15570, - y: 3016, - }); }); it('should not dispatch event if coordinates do not changed', () => { const dispatchEventMock = jest.spyOn(PluginsEventBus, 'dispatchEvent'); diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts index 8a8689aa8029ca6877508bd0c00bea85cc350571..5059ea594c02245a181a02f3e0fe8e00407c82db 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.ts @@ -1,13 +1,13 @@ import { NOOP } from '@/constants/common'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { mapDataLastPositionSelector, mapOpenedMapsSelector } from '@/redux/map/map.selectors'; +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 { currentModelIdSelector, modelsDataSelector } from '@/redux/models/models.selectors'; import { projectOverviewImagesSelector } from '@/redux/project/project.selectors'; -import { MapModel, OverviewImageLink, OverviewImageLinkModel } from '@/types/models'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; +import { MapModel, OverviewImageLink, OverviewImageLinkModel } from '@/types/models'; import { OverviewImageLinkImageHandler, OverviewImageLinkModelHandler, @@ -23,7 +23,6 @@ export const useOverviewImageLinkActions = (): UseOverviewImageLinkActionsResult const models = useAppSelector(modelsDataSelector); const overviewImages = useAppSelector(projectOverviewImagesSelector); const currentMapModelId = useAppSelector(currentModelIdSelector); - const mapLastPosition = useAppSelector(mapDataLastPositionSelector); const checkIfImageIsAvailable = (imageId: number): boolean => overviewImages.some(image => image.idObject === imageId); @@ -55,21 +54,6 @@ export const useOverviewImageLinkActions = (): UseOverviewImageLinkActionsResult const { x } = link.modelPoint; const { y } = link.modelPoint; - if (mapLastPosition.z !== zoom) { - PluginsEventBus.dispatchEvent('onZoomChanged', { - modelId: currentMapModelId, - zoom, - }); - } - - if (mapLastPosition.x !== x || mapLastPosition.y !== y) { - PluginsEventBus.dispatchEvent('onCenterChanged', { - modelId: currentMapModelId, - x, - y, - }); - } - dispatch( setMapPosition({ x, diff --git a/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.ts b/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.ts index 209499399e2e28b88e311ca935895b0a1304e076..e80346b5d069b42e41ffaa1d690248fa91ad6497 100644 --- a/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.ts +++ b/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.ts @@ -1,14 +1,12 @@ +import { DEFAULT_ZOOM } from '@/constants/map'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { setMapPosition, varyPositionZoom } from '@/redux/map/map.slice'; +import { currentModelIdSelector, modelByIdSelector } from '@/redux/models/models.selectors'; import { SetBoundsResult, useSetBounds } from '@/utils/map/useSetBounds'; import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { currentModelIdSelector, modelByIdSelector } from '@/redux/models/models.selectors'; -import { DEFAULT_ZOOM } from '@/constants/map'; -import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; -import { mapDataLastPositionSelector } from '@/redux/map/map.selectors'; -import { useVisibleBioEntitiesPolygonCoordinates } from './useVisibleBioEntitiesPolygonCoordinates'; import { MAP_ZOOM_IN_DELTA, MAP_ZOOM_OUT_DELTA } from '../MappAdditionalActions.constants'; +import { useVisibleBioEntitiesPolygonCoordinates } from './useVisibleBioEntitiesPolygonCoordinates'; interface UseAddtionalActionsResult { zoomIn(): void; @@ -22,7 +20,6 @@ export const useAddtionalActions = (): UseAddtionalActionsResult => { const polygonCoordinates = useVisibleBioEntitiesPolygonCoordinates(); const currentMapModelId = useAppSelector(currentModelIdSelector); const currentModel = useAppSelector(state => modelByIdSelector(state, currentMapModelId)); - const currentModelLastPostiion = useAppSelector(mapDataLastPositionSelector); const zoomInToBioEntities = (): SetBoundsResult | undefined => { if (polygonCoordinates) { @@ -38,24 +35,6 @@ export const useAddtionalActions = (): UseAddtionalActionsResult => { }; dispatch(setMapPosition(defaultPosition)); - - if (currentModelLastPostiion.z !== defaultPosition.z) { - PluginsEventBus.dispatchEvent('onZoomChanged', { - modelId: currentMapModelId, - zoom: defaultPosition.z, - }); - } - - if ( - currentModelLastPostiion.x !== defaultPosition.x || - currentModelLastPostiion.y !== defaultPosition.y - ) { - PluginsEventBus.dispatchEvent('onCenterChanged', { - modelId: currentMapModelId, - x: defaultPosition.x, - y: defaultPosition.y, - }); - } } return undefined; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.ts index 174d0a138eab987b1fb7b3019f34b8659a6efea4..ffa0b76e1564bd0fbcd0c638744ef529a650a27b 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapView.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapView.ts @@ -56,9 +56,11 @@ export const useOlMapView = ({ mapInstance }: UseOlMapViewInput): MapConfig['vie center: [center.x, center.y], zoom: mapInitialPosition.z, showFullExtent: OPTIONS.showFullExtent, + maxZoom: mapSize.maxZoom, + minZoom: mapSize.minZoom, extent, }), - [mapInitialPosition.z, center, extent], + [mapInitialPosition.z, mapSize.maxZoom, mapSize.minZoom, center, extent], ); const view = useMemo(() => new View(viewConfig), [viewConfig]); diff --git a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.test.ts b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.test.ts index bc9228f024b01e1369e832ae32a93dcbf16c1b3d..295b91d1aad607f60565dbb88c33f6bc043659a0 100644 --- a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.test.ts @@ -16,8 +16,6 @@ const getEvent = (targetValues: ObjectEvent['target']['values_']): ObjectEvent = /* eslint-disable no-magic-numbers */ describe('onMapPositionChange - util', () => { - const MAP_ID = 52; - const LAST_ZOOM = 4; const cases: [MapSize, ObjectEvent['target']['values_'], Point][] = [ [ { @@ -65,7 +63,7 @@ describe('onMapPositionChange - util', () => { const dispatch = result.current; const event = getEvent(targetValues); - onMapPositionChange(mapSize, dispatch, MAP_ID, LAST_ZOOM)(event); + onMapPositionChange(mapSize, dispatch)(event); const { position } = mapDataSelector(store.getState()); expect(position.last).toMatchObject(lastPosition); diff --git a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts index d49f6f3edfd3d4a108619cf890fbc45a90a68d56..7102fec7fdecfd1f771295030dd82e30c42bc767 100644 --- a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts +++ b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts @@ -1,33 +1,19 @@ import { setMapPosition } from '@/redux/map/map.slice'; import { MapSize } from '@/redux/map/map.types'; import { AppDispatch } from '@/redux/store'; -import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { latLngToPoint } from '@/utils/map/latLngToPoint'; import { toLonLat } from 'ol/proj'; import { ObjectEvent } from 'openlayers'; /* prettier-ignore */ export const onMapPositionChange = - (mapSize: MapSize, dispatch: AppDispatch, modelId: number, mapLastZoomValue: number | undefined) => + (mapSize: MapSize, dispatch: AppDispatch) => (e: ObjectEvent): void => { // eslint-disable-next-line no-underscore-dangle const { center, zoom } = e.target.values_; const [lng, lat] = toLonLat(center); const { x, y } = latLngToPoint([lat, lng], mapSize, { rounded: true }); - if (mapLastZoomValue !== zoom) { - PluginsEventBus.dispatchEvent('onZoomChanged', { - modelId, - zoom, - }); - } - - PluginsEventBus.dispatchEvent('onCenterChanged', { - modelId, - x, - y - }); - dispatch( setMapPosition({ x, diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts index 7ca778b5965bafa6ac38cae39e887afbb5cc6c21..72016ec314a2fcfbdd4c31adf3b79416cc99fcb1 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts @@ -1,6 +1,6 @@ import { OPTIONS } from '@/constants/map'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { mapDataLastZoomValue, mapDataSizeSelector } from '@/redux/map/map.selectors'; +import { mapDataSizeSelector } from '@/redux/map/map.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { MapInstance } from '@/types/map'; import { View } from 'ol'; @@ -23,7 +23,6 @@ interface UseOlMapListenersInput { export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput): void => { const mapSize = useSelector(mapDataSizeSelector); const modelId = useSelector(currentModelIdSelector); - const mapLastZoomValue = useSelector(mapDataLastZoomValue); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); const dispatch = useAppDispatch(); @@ -37,7 +36,7 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) ); const handleChangeCenter = useDebouncedCallback( - onMapPositionChange(mapSize, dispatch, modelId, mapLastZoomValue), + onMapPositionChange(mapSize, dispatch), OPTIONS.queryPersistTime, { leading: false }, ); diff --git a/src/constants/errors.ts b/src/constants/errors.ts index 887f64f35d00d60ce428da9f5b4239c0d8c6cbaa..cba5c0d0c5954b0a36f8b40c6188afbb20117dd1 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -1 +1,12 @@ export const DEFAULT_ERROR: Error = { message: '', name: '' }; + +export const OVERVIEW_IMAGE_ERRORS = { + IMAGE_ID_IS_INVALID: "Image id is invalid. There's no such image in overview images list", +}; + +export const ZOOM_ERRORS = { + ZOOM_VALUE_TOO_HIGH: (maxZoom: number): string => + `Provided zoom value exeeds max zoom of ${maxZoom}`, + ZOOM_VALUE_TOO_LOW: (minZoom: number): string => + `Provided zoom value exceeds min zoom of ${minZoom}`, +}; diff --git a/src/models/pointSchema.ts b/src/models/pointSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b097ef8235bf4e2a0a27698337828153a62c553 --- /dev/null +++ b/src/models/pointSchema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const zPointSchema = z.number().nonnegative('z should be non negative').optional(); + +export const pointSchema = z.object({ + x: z.number().nonnegative('x should be non negative'), + y: z.number().nonnegative('y should be non negative'), + z: zPointSchema, +}); diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts index 362745fc00327c7fff640f6d74c388b85400bb07..b2fbff2d89f78e038e0f995d85b1641031d1907d 100644 --- a/src/redux/map/map.reducers.ts +++ b/src/redux/map/map.reducers.ts @@ -1,6 +1,6 @@ import { DEFAULT_ZOOM } from '@/constants/map'; -import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { getPointMerged } from '../../utils/object/getPointMerged'; import { initMapBackground, @@ -16,6 +16,7 @@ import { SetActiveMapAction, SetBackgroundAction, SetLastPositionZoomAction, + SetLastPositionZoomWithDeltaAction, SetMapDataAction, SetMapPositionDataAction, } from './map.types'; @@ -32,19 +33,38 @@ export const setMapDataReducer = (state: MapState, action: SetMapDataAction): vo export const setMapPositionReducer = (state: MapState, action: SetMapPositionDataAction): void => { const position = action.payload || {}; const statePosition = state.data.position; + const lastPosition = statePosition.last; + const lastZoom = lastPosition.z; + const finalPosition = getPointMerged(position || {}, lastPosition); + const { modelId } = state.data; + + if (lastPosition?.x !== finalPosition.x || lastPosition?.y !== finalPosition.y) { + PluginsEventBus.dispatchEvent('onCenterChanged', { + modelId, + x: finalPosition.x, + y: finalPosition.y, + }); + } + + if (position?.z && lastZoom && lastZoom !== position?.z) { + PluginsEventBus.dispatchEvent('onZoomChanged', { + modelId, + zoom: position?.z, + }); + } state.data = { ...state.data, position: { - initial: getPointMerged(position || {}, statePosition.initial), - last: getPointMerged(position || {}, statePosition.last), + initial: finalPosition, + last: finalPosition, }, }; }; export const varyPositionZoomReducer = ( state: MapState, - action: SetLastPositionZoomAction, + action: SetLastPositionZoomWithDeltaAction, ): void => { const { minZoom, maxZoom } = state.data.size; const { delta } = action.payload; @@ -63,6 +83,23 @@ export const varyPositionZoomReducer = ( state.data.position.initial.z = newZLimited; }; +export const setLastPositionZoomReducer = ( + state: MapState, + action: SetLastPositionZoomAction, +): void => { + const { zoom } = action.payload; + + if (state.data.position.last.z !== zoom) { + PluginsEventBus.dispatchEvent('onZoomChanged', { + modelId: state.data.modelId, + zoom, + }); + } + + state.data.position.last.z = zoom; + state.data.position.initial.z = zoom; +}; + const updateLastPositionOfCurrentlyActiveMap = (state: MapState): void => { const currentMapId = state.data.modelId; const currentOpenedMap = state.openedMaps.find(openedMap => openedMap.modelId === currentMapId); diff --git a/src/redux/map/map.slice.ts b/src/redux/map/map.slice.ts index 8fc687b9fb29301fc18cc15579ca40b6caef893f..02e8a878292906e9894c26fe8f841dd5219fbbc8 100644 --- a/src/redux/map/map.slice.ts +++ b/src/redux/map/map.slice.ts @@ -9,6 +9,7 @@ import { initOpenedMapsReducer, openMapAndSetActiveReducer, setActiveMapReducer, + setLastPositionZoomReducer, setMapBackgroundReducer, setMapDataReducer, setMapPositionReducer, @@ -27,6 +28,7 @@ const mapSlice = createSlice({ setMapPosition: setMapPositionReducer, varyPositionZoom: varyPositionZoomReducer, setMapBackground: setMapBackgroundReducer, + setLastPositionZoom: setLastPositionZoomReducer, }, extraReducers: builder => { initMapPositionReducers(builder); @@ -45,6 +47,7 @@ export const { setMapPosition, setMapBackground, varyPositionZoom, + setLastPositionZoom, } = mapSlice.actions; export default mapSlice.reducer; diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts index b11c5cfefe794fb850de5629934e181c4f94877d..727df76f71b29da80f9690edcc3e9e86c9888d12 100644 --- a/src/redux/map/map.types.ts +++ b/src/redux/map/map.types.ts @@ -88,10 +88,17 @@ export type GetUpdatedMapDataResult = Pick< export type SetMapPositionDataAction = PayloadAction<Point>; -export type SetLastPositionZoomActionPayload = { +export type SetLastPositionZoomWithDeltaActionPayload = { delta: number; }; +export type SetLastPositionZoomWithDeltaAction = + PayloadAction<SetLastPositionZoomWithDeltaActionPayload>; + +export type SetLastPositionZoomActionPayload = { + zoom: number; +}; + export type SetLastPositionZoomAction = PayloadAction<SetLastPositionZoomActionPayload>; export type InitMapDataActionPayload = { diff --git a/src/services/pluginsManager/map/position/getCenter.test.ts b/src/services/pluginsManager/map/position/getCenter.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9d9dd545bda215f262f039e2d9c5f2ae7349ab5 --- /dev/null +++ b/src/services/pluginsManager/map/position/getCenter.test.ts @@ -0,0 +1,39 @@ +import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; +import { RootState, store } from '@/redux/store'; +import { getCenter } from './getCenter'; + +jest.mock('../../../../redux/store'); + +describe('getCenter - plugin method', () => { + const getStateSpy = jest.spyOn(store, 'getState'); + + getStateSpy.mockImplementation( + () => + ({ + map: { + data: { + ...initialMapDataFixture, + position: { + ...initialMapDataFixture.position, + last: { + x: 2137, + y: 420, + z: 1.488, + }, + }, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + }) as RootState, + ); + + it('should return last position from Redux', () => { + expect(getCenter()).toStrictEqual({ + x: 2137, + y: 420, + z: 1.488, + }); + }); +}); diff --git a/src/services/pluginsManager/map/position/getCenter.ts b/src/services/pluginsManager/map/position/getCenter.ts new file mode 100644 index 0000000000000000000000000000000000000000..ccf28579481881132d6677d2a982a465321ddac0 --- /dev/null +++ b/src/services/pluginsManager/map/position/getCenter.ts @@ -0,0 +1,10 @@ +import { mapDataLastPositionSelector } from '@/redux/map/map.selectors'; +import { store } from '@/redux/store'; +import { Point } from '@/types/map'; + +export const getCenter = (): Point => { + const { getState } = store; + const lastPosition = mapDataLastPositionSelector(getState()); + + return lastPosition; +}; diff --git a/src/services/pluginsManager/map/position/setCenter.test.ts b/src/services/pluginsManager/map/position/setCenter.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef26567b20576c97982dd6385a71829b534d4249 --- /dev/null +++ b/src/services/pluginsManager/map/position/setCenter.test.ts @@ -0,0 +1,55 @@ +import { setMapPosition } from '@/redux/map/map.slice'; +import { store } from '@/redux/store'; +import { Point } from '@/types/map'; +import { ZodError } from 'zod'; +import { setCenter } from './setCenter'; + +jest.mock('../../../../redux/store'); + +describe('setCenter - plugin method', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + describe('when position is invalid', () => { + const invalidPositions = [ + { + x: -1, + y: 1, + z: 1, + }, + { + x: 1, + y: -1, + z: 1, + }, + { + x: 1, + y: 1, + z: -1, + }, + { + y: 1, + }, + { + x: 1, + }, + ] as Point[]; + + it.each(invalidPositions)('should throw error', position => { + expect(() => setCenter(position)).toThrow(ZodError); + }); + }); + + describe('when position is valid', () => { + const position: Point = { + x: 500, + y: 200, + z: 2, + }; + + it('should set map position', () => { + setCenter(position); + + expect(dispatchSpy).toHaveBeenCalledWith(setMapPosition(position)); + }); + }); +}); diff --git a/src/services/pluginsManager/map/position/setCenter.ts b/src/services/pluginsManager/map/position/setCenter.ts new file mode 100644 index 0000000000000000000000000000000000000000..a32717df9b4897e0136f3d461d26779a9e5480ab --- /dev/null +++ b/src/services/pluginsManager/map/position/setCenter.ts @@ -0,0 +1,11 @@ +import { pointSchema } from '@/models/pointSchema'; +import { setMapPosition } from '@/redux/map/map.slice'; +import { store } from '@/redux/store'; +import { Point } from '@/types/map'; + +export const setCenter = (position: Point): void => { + const { dispatch } = store; + pointSchema.parse(position); + + dispatch(setMapPosition(position)); +}; diff --git a/src/services/pluginsManager/map/zoom/getZoom.test.ts b/src/services/pluginsManager/map/zoom/getZoom.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7808d53163d34fa802f14c8e090119590c3c11f --- /dev/null +++ b/src/services/pluginsManager/map/zoom/getZoom.test.ts @@ -0,0 +1,69 @@ +/* eslint-disable no-magic-numbers */ +import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; +import { RootState, store } from '@/redux/store'; +import { getZoom } from './getZoom'; + +jest.mock('../../../../redux/store'); + +describe('getZoom - plugin method', () => { + const getStateSpy = jest.spyOn(store, 'getState'); + + describe('when last position zoom is present', () => { + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + map: { + data: { + ...initialMapDataFixture, + position: { + ...initialMapDataFixture.position, + last: { + x: 2137, + y: 420, + z: 1.488, + }, + }, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + }) as RootState, + ); + }); + + it('should return last position from Redux', () => { + expect(getZoom()).toEqual(1.488); + }); + }); + + describe('when last position zoom is NOT present', () => { + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + map: { + data: { + ...initialMapDataFixture, + position: { + ...initialMapDataFixture.position, + last: { + x: 2137, + y: 420, + }, + }, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + }) as RootState, + ); + }); + + it('should return undefined', () => { + expect(getZoom()).toBeUndefined(); + }); + }); +}); diff --git a/src/services/pluginsManager/map/zoom/getZoom.ts b/src/services/pluginsManager/map/zoom/getZoom.ts new file mode 100644 index 0000000000000000000000000000000000000000..fee865408304b40d1f3ea5d614d39c2315b32123 --- /dev/null +++ b/src/services/pluginsManager/map/zoom/getZoom.ts @@ -0,0 +1,9 @@ +import { mapDataLastPositionSelector } from '@/redux/map/map.selectors'; +import { store } from '@/redux/store'; + +export const getZoom = (): number | undefined => { + const { getState } = store; + const lastPosition = mapDataLastPositionSelector(getState()); + + return lastPosition?.z; +}; diff --git a/src/services/pluginsManager/map/zoom/setZoom.test.ts b/src/services/pluginsManager/map/zoom/setZoom.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..55502f302a70e74e6e7b7983e1bc12601e1071d9 --- /dev/null +++ b/src/services/pluginsManager/map/zoom/setZoom.test.ts @@ -0,0 +1,77 @@ +/* eslint-disable no-magic-numbers */ +import { ZOOM_ERRORS } from '@/constants/errors'; +import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; +import { setLastPositionZoom } from '@/redux/map/map.slice'; +import { RootState, store } from '@/redux/store'; +import { ZodError } from 'zod'; +import { setZoom } from './setZoom'; + +jest.mock('../../../../redux/store'); + +describe('setZoom - plugin method', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + const getStateSpy = jest.spyOn(store, 'getState'); + + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + map: { + data: { + ...initialMapDataFixture, + position: { + ...initialMapDataFixture.position, + last: { + x: 2137, + y: 420, + z: 1.488, + }, + }, + size: { + ...initialMapDataFixture.size, + minZoom: 2, + maxZoom: 8, + }, + }, + loading: 'succeeded', + error: { message: '', name: '' }, + openedMaps: openedMapsThreeSubmapsFixture, + }, + }) as RootState, + ); + }); + + describe('when zoom value type is invalid', () => { + const invalidZoom = [-1, -123, '-123'] as number[]; + + it.each(invalidZoom)('should throw error', zoom => { + expect(() => setZoom(zoom)).toThrow(ZodError); + }); + }); + + describe('when zoom value value exeeds max zoom', () => { + const invalidZoom = [444, 21, 9] as number[]; + + it.each(invalidZoom)('should throw error', zoom => { + expect(() => setZoom(zoom)).toThrow(ZOOM_ERRORS.ZOOM_VALUE_TOO_HIGH(8)); + }); + }); + + describe('when zoom value value exeeds min zoom', () => { + const invalidZoom = [1, 0] as number[]; + + it.each(invalidZoom)('should throw error', zoom => { + expect(() => setZoom(zoom)).toThrow(ZOOM_ERRORS.ZOOM_VALUE_TOO_LOW(2)); + }); + }); + + describe('when zoom is valid', () => { + const zoom = 2; + + it('should set map zoom', () => { + setZoom(zoom); + + expect(dispatchSpy).toHaveBeenCalledWith(setLastPositionZoom({ zoom })); + }); + }); +}); diff --git a/src/services/pluginsManager/map/zoom/setZoom.ts b/src/services/pluginsManager/map/zoom/setZoom.ts new file mode 100644 index 0000000000000000000000000000000000000000..f161d1c0588e5010c701d4e64ed2e6a7e83f7f5a --- /dev/null +++ b/src/services/pluginsManager/map/zoom/setZoom.ts @@ -0,0 +1,21 @@ +import { ZOOM_ERRORS } from '@/constants/errors'; +import { zPointSchema } from '@/models/pointSchema'; +import { mapDataSizeSelector } from '@/redux/map/map.selectors'; +import { setLastPositionZoom } from '@/redux/map/map.slice'; +import { store } from '@/redux/store'; + +export const setZoom = (zoom: number): void => { + const { dispatch, getState } = store; + const { minZoom, maxZoom } = mapDataSizeSelector(getState()); + zPointSchema.parse(zoom); + + if (zoom < minZoom) { + throw Error(ZOOM_ERRORS.ZOOM_VALUE_TOO_LOW(minZoom)); + } + + if (zoom > maxZoom) { + throw Error(ZOOM_ERRORS.ZOOM_VALUE_TOO_HIGH(maxZoom)); + } + + dispatch(setLastPositionZoom({ zoom })); +}; diff --git a/src/services/pluginsManager/overviewImage/getCurrentOverviewImage.ts b/src/services/pluginsManager/overviewImage/getCurrentOverviewImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9d54c9aad16354024f9efbf5e9f487e182926eb --- /dev/null +++ b/src/services/pluginsManager/overviewImage/getCurrentOverviewImage.ts @@ -0,0 +1,10 @@ +import { currentOverviewImageSelector } from '@/redux/project/project.selectors'; +import { store } from '@/redux/store'; +import { OverviewImageView } from '@/types/models'; + +export const getCurrentOverviewImage = (): OverviewImageView | undefined => { + const { getState } = store; + const overviewImage = currentOverviewImageSelector(getState()); + + return overviewImage; +}; diff --git a/src/services/pluginsManager/overviewImage/getOverviewImages.ts b/src/services/pluginsManager/overviewImage/getOverviewImages.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e2e19c189033e08f86b97d4a0b983d6cca30cb3 --- /dev/null +++ b/src/services/pluginsManager/overviewImage/getOverviewImages.ts @@ -0,0 +1,10 @@ +import { projectOverviewImagesSelector } from '@/redux/project/project.selectors'; +import { store } from '@/redux/store'; +import { OverviewImageView } from '@/types/models'; + +export const getOverviewImages = (): OverviewImageView[] => { + const { getState } = store; + const overviewImages = projectOverviewImagesSelector(getState()); + + return overviewImages; +}; diff --git a/src/services/pluginsManager/overviewImage/hideOverviewImageModal.test.ts b/src/services/pluginsManager/overviewImage/hideOverviewImageModal.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..25eb34afffd19c5809906d2adc013892927dc3e7 --- /dev/null +++ b/src/services/pluginsManager/overviewImage/hideOverviewImageModal.test.ts @@ -0,0 +1,60 @@ +import { closeModal } from '@/redux/modal/modal.slice'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +import { RootState, store } from '@/redux/store'; +import { hideOverviewImageModal } from './hideOverviewImageModal'; + +jest.mock('../../../redux/store'); + +describe('hideOverviewImageModal - util', () => { + const getStateSpy = jest.spyOn(store, 'getState'); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('when opened modal is overview image', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + ...INITIAL_STORE_STATE_MOCK, + modal: { + ...INITIAL_STORE_STATE_MOCK.modal, + modalName: 'overview-images', + }, + }) as RootState, + ); + }); + + it('should close modal', () => { + hideOverviewImageModal(); + + expect(dispatchSpy).toHaveBeenCalledWith(closeModal()); + }); + }); + + describe('when opened modal is NOT overview image', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + ...INITIAL_STORE_STATE_MOCK, + modal: { + ...INITIAL_STORE_STATE_MOCK.modal, + modalName: 'login', + }, + }) as RootState, + ); + }); + + it('should not close modal', () => { + hideOverviewImageModal(); + + expect(dispatchSpy).not.toHaveBeenCalledWith(closeModal()); + }); + }); +}); diff --git a/src/services/pluginsManager/overviewImage/hideOverviewImageModal.ts b/src/services/pluginsManager/overviewImage/hideOverviewImageModal.ts new file mode 100644 index 0000000000000000000000000000000000000000..47652cd524a0fb8a0851ad730d280085d3105559 --- /dev/null +++ b/src/services/pluginsManager/overviewImage/hideOverviewImageModal.ts @@ -0,0 +1,14 @@ +import { modalSelector } from '@/redux/modal/modal.selector'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { store } from '@/redux/store'; + +export const hideOverviewImageModal = (): void => { + const { getState, dispatch } = store; + const { modalName } = modalSelector(getState()); + + if (modalName !== 'overview-images') { + return; + } + + dispatch(closeModal()); +}; diff --git a/src/services/pluginsManager/overviewImage/selectOverviewImage.test.ts b/src/services/pluginsManager/overviewImage/selectOverviewImage.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..500bfae94ebce30954ce2b772a67a4c12b224917 --- /dev/null +++ b/src/services/pluginsManager/overviewImage/selectOverviewImage.test.ts @@ -0,0 +1,63 @@ +import { OVERVIEW_IMAGE_ERRORS } from '@/constants/errors'; +import { setOverviewImageId } from '@/redux/modal/modal.slice'; +import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +import { RootState, store } from '@/redux/store'; +import { selectOverviewImage } from './selectOverviewImage'; + +jest.mock('../../../redux/store'); + +describe('selectOverviewImage - plugin method', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + const getStateSpy = jest.spyOn(store, 'getState'); + + describe('when image id is valid', () => { + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + ...INITIAL_STORE_STATE_MOCK, + project: { + ...INITIAL_STORE_STATE_MOCK.project, + data: { + ...INITIAL_STORE_STATE_MOCK.project.data, + overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], + }, + }, + }) as RootState, + ); + }); + + it('should dispatch action set overview image', () => { + selectOverviewImage(PROJECT_OVERVIEW_IMAGE_MOCK.idObject); + + expect(dispatchSpy).toHaveBeenCalledWith( + setOverviewImageId(PROJECT_OVERVIEW_IMAGE_MOCK.idObject), + ); + }); + }); + + describe('when image id is NOT valid', () => { + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + ...INITIAL_STORE_STATE_MOCK, + project: { + ...INITIAL_STORE_STATE_MOCK.project, + data: { + ...INITIAL_STORE_STATE_MOCK.project.data, + overviewImageViews: [], + }, + }, + }) as RootState, + ); + }); + + it('should throw error', () => { + expect(() => selectOverviewImage(PROJECT_OVERVIEW_IMAGE_MOCK.idObject)).toThrow( + OVERVIEW_IMAGE_ERRORS.IMAGE_ID_IS_INVALID, + ); + }); + }); +}); diff --git a/src/services/pluginsManager/overviewImage/selectOverviewImage.ts b/src/services/pluginsManager/overviewImage/selectOverviewImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..71ef40f1ff80adc6892ac6621a62562750cd3be4 --- /dev/null +++ b/src/services/pluginsManager/overviewImage/selectOverviewImage.ts @@ -0,0 +1,17 @@ +import { OVERVIEW_IMAGE_ERRORS } from '@/constants/errors'; +import { setOverviewImageId } from '@/redux/modal/modal.slice'; +import { projectOverviewImagesSelector } from '@/redux/project/project.selectors'; +import { store } from '@/redux/store'; + +export const selectOverviewImage = (imageId: number): void => { + const { dispatch, getState } = store; + const overviewImages = projectOverviewImagesSelector(getState()); + const foundOverviewImage = overviewImages.find(o => o.idObject === imageId); + const isImageIdValid = Boolean(foundOverviewImage); + + if (!isImageIdValid) { + throw new Error(OVERVIEW_IMAGE_ERRORS.IMAGE_ID_IS_INVALID); + } + + dispatch(setOverviewImageId(imageId)); +}; diff --git a/src/services/pluginsManager/overviewImage/showOverviewImageModal.test.ts b/src/services/pluginsManager/overviewImage/showOverviewImageModal.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..f829470b22eb062f67a5e17a6641ffdbde72cbe7 --- /dev/null +++ b/src/services/pluginsManager/overviewImage/showOverviewImageModal.test.ts @@ -0,0 +1,105 @@ +import { OVERVIEW_IMAGE_ERRORS } from '@/constants/errors'; +import { openOverviewImagesModalById } from '@/redux/modal/modal.slice'; +import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +import { RootState, store } from '@/redux/store'; +import { showOverviewImageModal } from './showOverviewImageModal'; + +jest.mock('../../../redux/store'); + +describe('showOverviewImageModal - plugin method', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + const getStateSpy = jest.spyOn(store, 'getState'); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('when image id is not provided', () => { + const defaultImageId = 23332; + + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + ...INITIAL_STORE_STATE_MOCK, + project: { + ...INITIAL_STORE_STATE_MOCK.project, + data: { + ...INITIAL_STORE_STATE_MOCK.project.data, + overviewImageViews: [ + PROJECT_OVERVIEW_IMAGE_MOCK, + { + ...PROJECT_OVERVIEW_IMAGE_MOCK, + idObject: defaultImageId, + }, + ], + topOverviewImage: { + ...PROJECT_OVERVIEW_IMAGE_MOCK, + idObject: defaultImageId, + }, + }, + }, + }) as RootState, + ); + }); + + it('should dispatch action set overview image with defaultImageId', () => { + showOverviewImageModal(); + + expect(dispatchSpy).toHaveBeenCalledWith(openOverviewImagesModalById(defaultImageId)); + }); + }); + + describe('when image id is valid', () => { + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + ...INITIAL_STORE_STATE_MOCK, + project: { + ...INITIAL_STORE_STATE_MOCK.project, + data: { + ...INITIAL_STORE_STATE_MOCK.project.data, + overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + }, + }) as RootState, + ); + }); + + it('should dispatch action set overview image', () => { + showOverviewImageModal(PROJECT_OVERVIEW_IMAGE_MOCK.idObject); + + expect(dispatchSpy).toHaveBeenCalledWith( + openOverviewImagesModalById(PROJECT_OVERVIEW_IMAGE_MOCK.idObject), + ); + }); + }); + + describe('when image id is NOT valid', () => { + beforeEach(() => { + getStateSpy.mockImplementation( + () => + ({ + ...INITIAL_STORE_STATE_MOCK, + project: { + ...INITIAL_STORE_STATE_MOCK.project, + data: { + ...INITIAL_STORE_STATE_MOCK.project.data, + overviewImageViews: [], + topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK, + }, + }, + }) as RootState, + ); + }); + + it('should throw error', () => { + expect(() => showOverviewImageModal(PROJECT_OVERVIEW_IMAGE_MOCK.idObject)).toThrow( + OVERVIEW_IMAGE_ERRORS.IMAGE_ID_IS_INVALID, + ); + }); + }); +}); diff --git a/src/services/pluginsManager/overviewImage/showOverviewImageModal.ts b/src/services/pluginsManager/overviewImage/showOverviewImageModal.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac4351069bceb73d3a136dfe8a11d357ee7ba165 --- /dev/null +++ b/src/services/pluginsManager/overviewImage/showOverviewImageModal.ts @@ -0,0 +1,23 @@ +import { OVERVIEW_IMAGE_ERRORS } from '@/constants/errors'; +import { openOverviewImagesModalById } from '@/redux/modal/modal.slice'; +import { + projectDefaultOverviewImageIdSelector, + projectOverviewImagesSelector, +} from '@/redux/project/project.selectors'; +import { store } from '@/redux/store'; + +export const showOverviewImageModal = (imageId?: number): void => { + const { dispatch, getState } = store; + const overviewImages = projectOverviewImagesSelector(getState()); + const defaultImageId = projectDefaultOverviewImageIdSelector(getState()); + const selectedImageId = imageId || defaultImageId; + + const foundOverviewImage = overviewImages.find(o => o.idObject === selectedImageId); + const isImageIdValid = Boolean(foundOverviewImage); + + if (!isImageIdValid) { + throw new Error(OVERVIEW_IMAGE_ERRORS.IMAGE_ID_IS_INVALID); + } + + dispatch(openOverviewImagesModalById(selectedImageId)); +}; diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts index c0b7bd2b474b3c61cce3aa8fdfcd2de6d3cf2e54..f432527ab6e8e1f97755f1864745abb1819a687a 100644 --- a/src/services/pluginsManager/pluginsManager.ts +++ b/src/services/pluginsManager/pluginsManager.ts @@ -2,18 +2,27 @@ import { PLUGINS_CONTENT_ELEMENT_ATTR_NAME, PLUGINS_CONTENT_ELEMENT_ID } from '@ import { registerPlugin } from '@/redux/plugins/plugins.thunks'; import { store } from '@/redux/store'; import md5 from 'crypto-js/md5'; +import { bioEntitiesMethods } from './bioEntities'; import { getModels } from './map/models/getModels'; import { openMap } from './map/openMap'; -import { bioEntitiesMethods } from './bioEntities'; +import { getCenter } from './map/position/getCenter'; +import { setCenter } from './map/position/setCenter'; import { triggerSearch } from './map/triggerSearch'; +import { getZoom } from './map/zoom/getZoom'; +import { setZoom } from './map/zoom/setZoom'; +import { getCurrentOverviewImage } from './overviewImage/getCurrentOverviewImage'; +import { getOverviewImages } from './overviewImage/getOverviewImages'; +import { hideOverviewImageModal } from './overviewImage/hideOverviewImageModal'; +import { selectOverviewImage } from './overviewImage/selectOverviewImage'; +import { showOverviewImageModal } from './overviewImage/showOverviewImageModal'; import { PluginsEventBus } from './pluginsEventBus'; -import { getProjectId } from './project/data/getProjectId'; -import { getName } from './project/data/getName'; -import { getVersion } from './project/data/getVersion'; -import { getDisease } from './project/data/getDisease'; -import { getOrganism } from './project/data/getOrganism'; import type { PluginsManagerType } from './pluginsManager.types'; import { configurationMapper } from './pluginsManager.utils'; +import { getDisease } from './project/data/getDisease'; +import { getName } from './project/data/getName'; +import { getOrganism } from './project/data/getOrganism'; +import { getProjectId } from './project/data/getProjectId'; +import { getVersion } from './project/data/getVersion'; import { getBounds } from './map/data/getBounds'; import { fitBounds } from './map/fitBounds'; @@ -45,6 +54,17 @@ export const PluginsManager: PluginsManagerType = { fitBounds, openMap, triggerSearch, + getZoom, + setZoom, + getCenter, + setCenter, + }, + overviewImage: { + getCurrentOverviewImage, + getOverviewImages, + hideOverviewImageModal, + selectOverviewImage, + showOverviewImageModal, }, project: { data: { diff --git a/src/types/error.ts b/src/types/error.ts new file mode 100644 index 0000000000000000000000000000000000000000..48e6926ad764eeb1355d602ced1e5d149bcea3dc --- /dev/null +++ b/src/types/error.ts @@ -0,0 +1,3 @@ +export interface PluginError { + message: string; +}