From cbdc5d730e33bb650597c7385fa205b2d190ee9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 09:52:54 +0100 Subject: [PATCH] feature(vector-map): implement context menu --- .../MapViewerVector.constants.ts | 2 + .../clickHandleReaction.test.ts} | 8 +- .../clickHandleReaction.ts} | 2 +- .../leftClickHandleAlias.test.ts | 4 +- .../mouseLeftClick}/leftClickHandleAlias.ts | 0 .../mouseLeftClick}/onMapLeftClick.test.ts | 16 ++-- .../mouseLeftClick}/onMapLeftClick.ts | 8 +- .../mouseRightClick/onMapRightClick.test.ts | 95 +++++++++++++++++++ .../mouseRightClick/onMapRightClick.ts | 56 +++++++++++ .../rightClickHandleAlias.test.ts | 28 ++++++ .../mouseRightClick/rightClickHandleAlias.ts | 16 ++++ .../listeners/useOlMapVectorListeners.test.ts | 4 +- .../listeners/useOlMapVectorListeners.ts | 46 ++++++++- .../reactionsLayer/useOlMapReactionsLayer.ts | 15 +-- .../utils/listeners/useOlMapListeners.ts | 4 +- src/models/fixtures/modelElementFixture.ts | 9 ++ src/models/modelElementSchema.ts | 4 +- src/redux/bioEntity/bioEntity.reducers.ts | 16 ++++ src/redux/bioEntity/bioEntity.slice.ts | 5 +- .../bioEntity/mapModelElementToBioEntity.ts | 51 ++++++++++ 20 files changed, 353 insertions(+), 36 deletions(-) rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick/leftClickHandleReaction.test.ts => mouseClick/clickHandleReaction.test.ts} (89%) rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick/leftClickHandleReaction.ts => mouseClick/clickHandleReaction.ts} (98%) rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/leftClickHandleAlias.test.ts (92%) rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/leftClickHandleAlias.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/onMapLeftClick.test.ts (87%) rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/onMapLeftClick.ts (87%) create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts create mode 100644 src/models/fixtures/modelElementFixture.ts create mode 100644 src/utils/bioEntity/mapModelElementToBioEntity.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts index 4ac8a6fb..9a036ac1 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts @@ -1,5 +1,7 @@ import { Color, ShapeRelAbs, ShapeRelAbsBezierPoint } from '@/types/models'; +export const VECTOR_MAP_LAYER_TYPE = 'vectorMapLayer'; + export const WHITE_COLOR: Color = { alpha: 255, rgb: 16777215, diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts similarity index 89% rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.test.ts rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts index 1bca3522..f6aab10a 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts @@ -1,5 +1,4 @@ /* eslint-disable no-magic-numbers */ -import { leftClickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction'; import { openReactionDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds'; @@ -10,12 +9,13 @@ import { apiPath } from '@/redux/apiPath'; import { HttpStatusCode } from 'axios'; import { bioEntityFixture } from '@/models/fixtures/bioEntityFixture'; import { FEATURE_TYPE } from '@/constants/features'; +import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction'; const mockedAxiosClient = mockNetworkNewAPIResponse(); jest.mock('../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds'); const eventBusDispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); -describe('leftClickHandleReaction', () => { +describe('clickHandleReaction', () => { let dispatch: jest.Mock; let modelId = 1; let reactionId = 1; @@ -54,7 +54,7 @@ describe('leftClickHandleReaction', () => { ) .reply(HttpStatusCode.Ok, bioEntityFixture); }); - await leftClickHandleReaction(dispatch, hasFitBounds)(feature, modelId); + await clickHandleReaction(dispatch, hasFitBounds)(feature, modelId); expect(dispatch).toHaveBeenCalledTimes(4); expect(dispatch).toHaveBeenCalledWith(openReactionDrawerById(reactionId)); expect(dispatch).toHaveBeenCalledWith(selectTab('')); @@ -68,7 +68,7 @@ describe('leftClickHandleReaction', () => { unwrap: jest.fn().mockResolvedValue(mockBioEntities), })); - await leftClickHandleReaction(dispatch, false)(feature, modelId); + await clickHandleReaction(dispatch, false)(feature, modelId); expect(searchFitBounds).not.toHaveBeenCalled(); }); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts similarity index 98% rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.ts rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts index 1481cee4..676d0b49 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts @@ -15,7 +15,7 @@ import { getBioEntitiesIdsFromReaction } from '@/components/Map/MapViewer/utils/ import { FEATURE_TYPE } from '@/constants/features'; /* prettier-ignore */ -export const leftClickHandleReaction = +export const clickHandleReaction = (dispatch: AppDispatch, hasFitBounds = false) => async (feature: FeatureLike, modelId: number): Promise<void> => { const id = feature.get('id'); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts similarity index 92% rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.test.ts rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts index 82b8dfc2..46ca1e40 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts @@ -1,12 +1,12 @@ /* eslint-disable no-magic-numbers */ -import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias'; +import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias'; import { openBioEntityDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds'; import { Feature } from 'ol'; import { FEATURE_TYPE } from '@/constants/features'; -jest.mock('../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds'); +jest.mock('../../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds'); const eventBusDispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent'); describe('leftClickHandleAlias', () => { diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.ts rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts similarity index 87% rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.test.ts rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts index e7fa9bc7..166b70df 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts @@ -5,16 +5,16 @@ import { resetReactionsData } from '@/redux/reactions/reactions.slice'; import { clearBioEntitiesData } from '@/redux/bioEntity/bioEntity.slice'; import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick'; import Map from 'ol/Map'; -import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick'; +import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick'; import { Comment } from '@/types/models'; import { Layer } from 'ol/layer'; import SimpleGeometry from 'ol/geom/SimpleGeometry'; import { Feature } from 'ol'; import { FEATURE_TYPE } from '@/constants/features'; import * as leftClickHandleAlias from './leftClickHandleAlias'; -import * as leftClickHandleReaction from './leftClickHandleReaction'; +import * as clickHandleReaction from '../clickHandleReaction'; -jest.mock('../../../utils/listeners/mapSingleClick/handleFeaturesClick', () => ({ +jest.mock('../../../../utils/listeners/mapSingleClick/handleFeaturesClick', () => ({ handleFeaturesClick: jest.fn(), })); jest.mock('./leftClickHandleAlias', () => ({ @@ -22,11 +22,11 @@ jest.mock('./leftClickHandleAlias', () => ({ ...jest.requireActual('./leftClickHandleAlias'), })); const leftClickHandleAliasSpy = jest.spyOn(leftClickHandleAlias, 'leftClickHandleAlias'); -jest.mock('./leftClickHandleReaction', () => ({ +jest.mock('../clickHandleReaction', () => ({ __esModule: true, - ...jest.requireActual('./leftClickHandleReaction'), + ...jest.requireActual('../clickHandleReaction'), })); -const leftClickHandleReactionSpy = jest.spyOn(leftClickHandleReaction, 'leftClickHandleReaction'); +const clickHandleReactionSpy = jest.spyOn(clickHandleReaction, 'clickHandleReaction'); describe('onMapLeftClick', () => { const modelId = 1; @@ -89,7 +89,7 @@ describe('onMapLeftClick', () => { expect(leftClickHandleAliasSpy).toHaveBeenCalledWith(dispatch); }); - it('calls leftClickHandleReaction if feature type is REACTION', async () => { + it('calls clickHandleReaction if feature type is REACTION', async () => { const mockBioEntities = [{ id: 1, name: 'BioEntity 1' }]; const dispatch = jest.fn(() => ({ unwrap: jest.fn().mockResolvedValue(mockBioEntities), @@ -108,6 +108,6 @@ describe('onMapLeftClick', () => { comments, )(event, mapInstance); - expect(leftClickHandleReactionSpy).toHaveBeenCalledWith(dispatch); + expect(clickHandleReactionSpy).toHaveBeenCalledWith(dispatch); }); }); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts similarity index 87% rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.ts rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts index aa481cd3..7b047dda 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts @@ -8,12 +8,12 @@ import { latLngToPoint } from '@/utils/map/latLngToPoint'; import { FeatureLike } from 'ol/Feature'; import { closeDrawer } from '@/redux/drawer/drawer.slice'; import { clearBioEntitiesData } from '@/redux/bioEntity/bioEntity.slice'; -import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias'; +import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias'; import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick'; import { resetReactionsData } from '@/redux/reactions/reactions.slice'; -import { leftClickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction'; import { handleDataReset } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset'; import { FEATURE_TYPE } from '@/constants/features'; +import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction'; /* prettier-ignore */ export const onMapLeftClick = @@ -25,7 +25,7 @@ export const onMapLeftClick = dispatch(updateLastClick({ coordinates: point, modelId })); let featureAtPixel: FeatureLike | undefined; - mapInstance.forEachFeatureAtPixel(pixel, (feature) => { + mapInstance.forEachFeatureAtPixel(pixel, (feature, ) => { if(feature.get('id') && [...Object.values(FEATURE_TYPE)].includes(feature.get('type'))) { featureAtPixel = feature; return true; @@ -55,6 +55,6 @@ export const onMapLeftClick = if(type === FEATURE_TYPE.ALIAS) { await leftClickHandleAlias(dispatch)(featureAtPixel, modelId); } else if (type === FEATURE_TYPE.REACTION) { - await leftClickHandleReaction(dispatch)(featureAtPixel, modelId); + await clickHandleReaction(dispatch)(featureAtPixel, modelId); } }; diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts new file mode 100644 index 00000000..a96f6601 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-magic-numbers */ +import { updateLastRightClick } from '@/redux/map/map.slice'; +import Map from 'ol/Map'; +import { onMapRightClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick'; +import { Layer } from 'ol/layer'; +import { openContextMenu } from '@/redux/contextMenu/contextMenu.slice'; +import { Source } from 'ol/source'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import { Feature } from 'ol'; +import { FEATURE_TYPE } from '@/constants/features'; +import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; +import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import * as rightClickHandleAlias from './rightClickHandleAlias'; +import * as clickHandleReaction from '../clickHandleReaction'; + +jest.mock('./rightClickHandleAlias', () => ({ + __esModule: true, + ...jest.requireActual('./rightClickHandleAlias'), +})); +const rightClickHandleAliasSpy = jest.spyOn(rightClickHandleAlias, 'rightClickHandleAlias'); +jest.mock('../clickHandleReaction', () => ({ + __esModule: true, + ...jest.requireActual('../clickHandleReaction'), +})); +const clickHandleReactionSpy = jest.spyOn(clickHandleReaction, 'clickHandleReaction'); + +describe('onMapRightClick', () => { + const modelId = 1; + let mapInstance: Map; + const event = { coordinate: [100, 50], pixel: [200, 100] }; + const mapSize = { + width: 90, + height: 90, + tileSize: 256, + minZoom: 2, + maxZoom: 9, + }; + let vectorLayer: VectorLayer; + let vectorSource: VectorSource; + + beforeEach(() => { + const dummyElement = document.createElement('div'); + mapInstance = new Map({ target: dummyElement }); + vectorSource = new VectorSource({}); + vectorLayer = new VectorLayer({ + source: vectorSource, + }); + vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE); + jest.clearAllMocks(); + }); + + it('calls rightClickHandleAlias if feature type is ALIAS', async () => { + const dispatch = jest.fn(); + const modelElement = modelElementsFixture.content[0]; + jest.spyOn(mapInstance, 'getAllLayers').mockImplementation((): Layer<Source>[] => { + return [vectorLayer]; + }); + jest.spyOn(vectorLayer, 'isVisible').mockImplementation((): boolean => { + return true; + }); + jest.spyOn(vectorLayer, 'getSource').mockImplementation((): VectorSource => { + return vectorSource; + }); + jest.spyOn(vectorSource, 'getClosestFeatureToCoordinate').mockImplementation((): Feature => { + return new Feature({ id: modelElement.id, type: FEATURE_TYPE.ALIAS }); + }); + await onMapRightClick(mapSize, modelId, dispatch, [modelElement])(event, mapInstance); + + expect(dispatch).toHaveBeenCalledWith(updateLastRightClick(expect.any(Object))); + expect(dispatch).toHaveBeenCalledWith(openContextMenu(event.pixel)); + expect(rightClickHandleAliasSpy).toHaveBeenCalledWith(dispatch); + }); + + it('calls rightClickHandleAlias if feature type is REACTION', async () => { + const dispatch = jest.fn(); + jest.spyOn(mapInstance, 'getAllLayers').mockImplementation((): Layer<Source>[] => { + return [vectorLayer]; + }); + jest.spyOn(vectorLayer, 'isVisible').mockImplementation((): boolean => { + return true; + }); + jest.spyOn(vectorLayer, 'getSource').mockImplementation((): VectorSource => { + return vectorSource; + }); + jest.spyOn(vectorSource, 'getClosestFeatureToCoordinate').mockImplementation((): Feature => { + return new Feature({ id: 1, type: FEATURE_TYPE.REACTION }); + }); + await onMapRightClick(mapSize, modelId, dispatch, [])(event, mapInstance); + + expect(dispatch).toHaveBeenCalledWith(updateLastRightClick(expect.any(Object))); + expect(dispatch).toHaveBeenCalledWith(openContextMenu(event.pixel)); + expect(clickHandleReactionSpy).toHaveBeenCalledWith(dispatch); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts new file mode 100644 index 00000000..c4673a83 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts @@ -0,0 +1,56 @@ +import { MapSize } from '@/redux/map/map.types'; +import { AppDispatch } from '@/redux/store'; +import { Feature, Map, MapBrowserEvent } from 'ol'; +import { updateLastRightClick } from '@/redux/map/map.slice'; +import { toLonLat } from 'ol/proj'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; +import { handleDataReset } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset'; +import { FEATURE_TYPE } from '@/constants/features'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import { openContextMenu } from '@/redux/contextMenu/contextMenu.slice'; +import { ModelElement } from '@/types/models'; +import { rightClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias'; +import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction'; +import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; + +/* prettier-ignore */ +export const onMapRightClick = + (mapSize: MapSize, modelId: number, dispatch: AppDispatch, modelElements: Array<ModelElement>) => + async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => { + + const [lng, lat] = toLonLat(coordinate); + const point = latLngToPoint([lat, lng], mapSize); + dispatch(updateLastRightClick({ coordinates: point, modelId })); + + let foundFeature: Feature | undefined; + mapInstance.getAllLayers().forEach(layer => { + if(layer.isVisible() && layer instanceof VectorLayer) { + if (layer.get('type') === VECTOR_MAP_LAYER_TYPE) { + const source = layer.getSource(); + if (source instanceof VectorSource) { + foundFeature = source.getClosestFeatureToCoordinate(coordinate, (feature) => { + return [FEATURE_TYPE.ALIAS, FEATURE_TYPE.REACTION].includes(feature.get('type')); + }); + } + } + } + }); + if(!foundFeature) { + return; + } + dispatch(handleDataReset); + dispatch(openContextMenu(pixel)); + + const type = foundFeature.get('type'); + const id = foundFeature.get('id'); + if(type === FEATURE_TYPE.ALIAS) { + const modelElement = modelElements.find(element => element.id === id); + if(!modelElement) { + return; + } + await rightClickHandleAlias(dispatch)(id, modelElement); + } else if (type === FEATURE_TYPE.REACTION) { + await clickHandleReaction(dispatch)(foundFeature, modelId); + } + }; diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts new file mode 100644 index 00000000..2b936519 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-magic-numbers */ +import { rightClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias'; +import { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { setBioEntityContents } from '@/redux/bioEntity/bioEntity.slice'; +import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; +import { setCurrentSelectedBioEntityId } from '@/redux/contextMenu/contextMenu.slice'; + +jest.mock('../../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds'); + +describe('rightClickHandleAlias', () => { + let dispatch: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + dispatch = jest.fn(); + }); + + it('dispatches setBioEntityContents, addNumbersToEntityNumberData and setCurrentSelectedBioEntityId', async () => { + await rightClickHandleAlias(dispatch)(modelElementFixture.id, modelElementFixture); + expect(dispatch).toHaveBeenCalledTimes(3); + + expect(dispatch).toHaveBeenCalledWith(setBioEntityContents(expect.any(Object))); + expect(dispatch).toHaveBeenCalledWith( + addNumbersToEntityNumberData([modelElementFixture.elementId]), + ); + expect(dispatch).toHaveBeenCalledWith(setCurrentSelectedBioEntityId(modelElementFixture.id)); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts new file mode 100644 index 00000000..4968eea6 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts @@ -0,0 +1,16 @@ +import { setCurrentSelectedBioEntityId } from '@/redux/contextMenu/contextMenu.slice'; +import { AppDispatch } from '@/redux/store'; +import { ModelElement } from '@/types/models'; +import { setBioEntityContents } from '@/redux/bioEntity/bioEntity.slice'; +import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; +import { mapModelElementToBioEntity } from '@/utils/bioEntity/mapModelElementToBioEntity'; + +/* prettier-ignore */ +export const rightClickHandleAlias = + (dispatch: AppDispatch) => + async (id: number, modelElement: ModelElement): Promise<void> => { + const bioEntity = mapModelElementToBioEntity(modelElement); + dispatch(setBioEntityContents({ bioEntity, perfect: true })); + dispatch(addNumbersToEntityNumberData([bioEntity.elementId])); + dispatch(setCurrentSelectedBioEntityId(id)); + }; diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts index 64691511..8b772615 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts @@ -4,9 +4,9 @@ import { View } from 'ol'; import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; import { useOlMapVectorListeners } from '@/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners'; -import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick'; +import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick'; -jest.mock('./mapLeftClick/onMapLeftClick', () => ({ +jest.mock('./mouseClick/mouseLeftClick/onMapLeftClick', () => ({ __esModule: true, onMapLeftClick: jest.fn(), })); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts index ab9dd6a3..2a0af8ea 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts @@ -5,22 +5,29 @@ import { mapDataSizeSelector } from '@/redux/map/map.selectors'; import { currentModelIdSelector, vectorRenderingSelector } from '@/redux/models/models.selectors'; import { MapInstance } from '@/types/map'; import { unByKey } from 'ol/Observable'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useDebouncedCallback } from 'use-debounce'; import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; -import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick'; +import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { Coordinate } from 'ol/coordinate'; +import { Pixel } from 'ol/pixel'; +import { onMapRightClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick'; +import { modelElementsSelector } from '@/redux/modelElements/modelElements.selector'; -interface UseOlMapListenersInput { +interface UseOlMapVectorListenersInput { mapInstance: MapInstance; } -export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapListenersInput): void => { +export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListenersInput): void => { const mapSize = useSelector(mapDataSizeSelector); const modelId = useSelector(currentModelIdSelector); const isResultDrawerOpen = useSelector(resultDrawerOpen); + const modelElements = useSelector(modelElementsSelector); const dispatch = useAppDispatch(); + const coordinate = useRef<Coordinate>([]); + const pixel = useRef<Pixel>([]); const comments = useSelector(allCommentsSelectorOfCurrentMap); const vectorRendering = useAppSelector(vectorRenderingSelector); @@ -31,6 +38,14 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapListenersInput) { leading: false }, ); + const handleRightClick = useDebouncedCallback( + onMapRightClick(mapSize, modelId, dispatch, modelElements?.content || []), + OPTIONS.clickPersistTime, + { + leading: false, + }, + ); + useEffect(() => { if (!mapInstance || !vectorRendering) { return; @@ -43,4 +58,27 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapListenersInput) // eslint-disable-next-line consistent-return return () => unByKey(key); }, [mapInstance, handleMapLeftClick, vectorRendering]); + + useEffect(() => { + if (!mapInstance || !vectorRendering) { + return; + } + + const rightClickEvent = (e: MouseEvent): Promise<void> | undefined => { + e.preventDefault(); + + coordinate.current = mapInstance.getEventCoordinate(e); + pixel.current = mapInstance.getEventPixel(e); + + return handleRightClick( + { coordinate: coordinate.current, pixel: pixel.current }, + mapInstance, + ); + }; + + mapInstance.getViewport().addEventListener('contextmenu', rightClickEvent); + + // eslint-disable-next-line consistent-return + return () => mapInstance.getViewport().removeEventListener('contextmenu', rightClickEvent); + }, [mapInstance, handleRightClick, vectorRendering]); }; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index e2d4fca4..00ff8cb8 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -28,6 +28,7 @@ import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction'; import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors'; import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -182,11 +183,11 @@ export const useOlMapReactionsLayer = ({ }); }, [features]); - return useMemo( - () => - new VectorLayer({ - source: vectorSource, - }), - [vectorSource], - ); + return useMemo(() => { + const vectorLayer = new VectorLayer({ + source: vectorSource, + }); + vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE); + return vectorLayer; + }, [vectorSource]); }; diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts index d10e31d5..e7df6b03 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts @@ -126,7 +126,7 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) }, [mapInstance, handleMapSingleClick, vectorRendering]); useEffect(() => { - if (!mapInstance) { + if (!mapInstance || vectorRendering) { return; } @@ -143,5 +143,5 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) // eslint-disable-next-line consistent-return return () => mapInstance.getViewport().removeEventListener('contextmenu', rightClickEvent); - }, [mapInstance, handleRightClick]); + }, [mapInstance, handleRightClick, vectorRendering]); }; diff --git a/src/models/fixtures/modelElementFixture.ts b/src/models/fixtures/modelElementFixture.ts new file mode 100644 index 00000000..0936dceb --- /dev/null +++ b/src/models/fixtures/modelElementFixture.ts @@ -0,0 +1,9 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { modelElementSchema } from '@/models/modelElementSchema'; + +export const modelElementFixture = createFixture(modelElementSchema, { + seed: ZOD_SEED, + array: { min: 3, max: 3 }, +}); diff --git a/src/models/modelElementSchema.ts b/src/models/modelElementSchema.ts index 98f28825..b95b6f6a 100644 --- a/src/models/modelElementSchema.ts +++ b/src/models/modelElementSchema.ts @@ -7,9 +7,11 @@ import { glyphSchema } from '@/models/glyphSchema'; export const modelElementSchema = z.object({ id: z.number(), - model: z.number().nullable(), + model: z.number(), glyph: glyphSchema.nullable(), submodel: submodelSchema.nullable(), + compartment: z.number().nullable(), + elementId: z.string(), x: z.number(), y: z.number(), z: z.number(), diff --git a/src/redux/bioEntity/bioEntity.reducers.ts b/src/redux/bioEntity/bioEntity.reducers.ts index 3f68a154..deadfae3 100644 --- a/src/redux/bioEntity/bioEntity.reducers.ts +++ b/src/redux/bioEntity/bioEntity.reducers.ts @@ -1,6 +1,7 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; import { getBioEntityById } from '@/redux/bioEntity/thunks/getBioEntity'; +import { BioEntityContent } from '@/types/models'; import { BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE } from './bioEntity.constants'; import { getBioEntity, getMultiBioEntity } from './bioEntity.thunks'; import { BioEntityContentsState } from './bioEntity.types'; @@ -120,3 +121,18 @@ export const toggleIsContentTabOpenedReducer = ( ): void => { state.isContentTabOpened = action.payload; }; + +export const setBioEntityContentsReducer = ( + state: BioEntityContentsState, + action: PayloadAction<BioEntityContent>, +): void => { + state.data = [ + { + data: [action.payload], + loading: 'succeeded', + error: DEFAULT_ERROR, + searchQueryElement: action.payload.bioEntity.id.toString(), + }, + ]; + state.loading = 'succeeded'; +}; diff --git a/src/redux/bioEntity/bioEntity.slice.ts b/src/redux/bioEntity/bioEntity.slice.ts index 728467a9..40f2bbb9 100644 --- a/src/redux/bioEntity/bioEntity.slice.ts +++ b/src/redux/bioEntity/bioEntity.slice.ts @@ -5,6 +5,7 @@ import { getBioEntityContentsReducer, getMultiBioEntityContentsReducer, getSubmapConnectionsBioEntityReducer, + setBioEntityContentsReducer, toggleIsContentTabOpenedReducer, } from './bioEntity.reducers'; @@ -14,6 +15,7 @@ export const bioEntityContentsSlice = createSlice({ reducers: { clearBioEntitiesData: clearBioEntitiesDataReducer, toggleIsContentTabOpened: toggleIsContentTabOpenedReducer, + setBioEntityContents: setBioEntityContentsReducer, }, extraReducers: builder => { getBioEntityContentsReducer(builder); @@ -22,6 +24,7 @@ export const bioEntityContentsSlice = createSlice({ }, }); -export const { clearBioEntitiesData, toggleIsContentTabOpened } = bioEntityContentsSlice.actions; +export const { clearBioEntitiesData, toggleIsContentTabOpened, setBioEntityContents } = + bioEntityContentsSlice.actions; export default bioEntityContentsSlice.reducer; diff --git a/src/utils/bioEntity/mapModelElementToBioEntity.ts b/src/utils/bioEntity/mapModelElementToBioEntity.ts new file mode 100644 index 00000000..e8be81f2 --- /dev/null +++ b/src/utils/bioEntity/mapModelElementToBioEntity.ts @@ -0,0 +1,51 @@ +import { BioEntity, ModelElement } from '@/types/models'; + +export function mapModelElementToBioEntity(modelElement: ModelElement): BioEntity { + return { + id: modelElement.id, + name: modelElement.name, + model: modelElement.model, + elementId: modelElement.elementId, + references: modelElement.references, + z: modelElement.z, + notes: modelElement.notes, + symbol: modelElement.symbol, + homodimer: modelElement.homodimer, + nameX: modelElement.nameX, + nameY: modelElement.nameY, + nameWidth: modelElement.nameWidth, + nameHeight: modelElement.nameHeight, + nameVerticalAlign: modelElement.nameVerticalAlign, + nameHorizontalAlign: modelElement.nameHorizontalAlign, + width: modelElement.width, + height: modelElement.height, + visibilityLevel: modelElement.visibilityLevel, + transparencyLevel: modelElement.transparencyLevel, + synonyms: modelElement.synonyms, + formerSymbols: modelElement.formerSymbols, + fullName: modelElement.fullName, + abbreviation: modelElement.abbreviation, + formula: modelElement.formula, + glyph: modelElement.glyph, + activity: modelElement.activity, + hypothetical: modelElement.hypothetical, + boundaryCondition: modelElement.boundaryCondition, + constant: modelElement.constant, + initialAmount: modelElement.initialAmount, + initialConcentration: modelElement.initialConcentration, + charge: modelElement.charge, + substanceUnits: modelElement.substanceUnits, + onlySubstanceUnits: modelElement.onlySubstanceUnits, + modificationResidues: modelElement.modificationResidues, + complex: modelElement.complex, + submodel: modelElement.submodel, + x: modelElement.x, + y: modelElement.y, + lineWidth: modelElement.lineWidth, + fontColor: modelElement.fontColor, + fontSize: modelElement.fontSize, + fillColor: modelElement.fillColor, + borderColor: modelElement.borderColor, + sboTerm: modelElement.sboTerm, + } as BioEntity; +} -- GitLab