From 9d863898ecac903d1d67049b457708fb54b0ed78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 12 Nov 2024 08:50:53 +0100 Subject: [PATCH 01/29] feature(vector-map): implement context menu --- .../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 | 94 +++++++++++++++++++ .../mouseRightClick/onMapRightClick.ts | 55 +++++++++++ .../rightClickHandleAlias.test.ts | 28 ++++++ .../mouseRightClick/rightClickHandleAlias.ts | 16 ++++ .../listeners/useOlMapVectorListeners.test.ts | 4 +- .../listeners/useOlMapVectorListeners.ts | 46 ++++++++- .../reactionsLayer/useOlMapReactionsLayer.ts | 14 +-- .../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 +- .../newReactions/newReactions.selectors.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/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..362df7ae --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts @@ -0,0 +1,94 @@ +/* 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 * 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', 'vectorMapLayer'); + 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..ae01e642 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts @@ -0,0 +1,55 @@ +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'; + +/* 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') === 'vectorMapLayer') { + 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..5fbc2ae0 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -182,11 +182,11 @@ export const useOlMapReactionsLayer = ({ }); }, [features]); - return useMemo( - () => - new VectorLayer({ - source: vectorSource, - }), - [vectorSource], - ); + return useMemo(() => { + const vectorLayer = new VectorLayer({ + source: vectorSource, + }); + vectorLayer.set('type', 'vectorMapLayer'); + 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/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index 4dc2babe..fd9978fd 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -7,3 +7,8 @@ export const newReactionsDataSelector = createSelector( newReactionsSelector, reactions => reactions.data || [], ); + +// export const newReactionsReactionByIdSelector = createSelector( +// newReactionsDataSelector, +// reactions => reactions.find(reaction => reaction.id === id) || [], +// ); 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 From d7503d3348a2858848d1f236079fe8e56079ca5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 12 Nov 2024 12:14:03 +0100 Subject: [PATCH 02/29] feat(vector-map): handle user action using only frontend data --- .../mouseClick/clickHandleReaction.test.ts | 15 +- .../mouseClick/clickHandleReaction.ts | 90 ++++---- .../getModelElementsIdsFromReaction.test.ts | 24 +++ .../getModelElementsIdsFromReaction.ts | 7 + .../mouseLeftClick/onMapLeftClick.test.ts | 6 + .../mouseLeftClick/onMapLeftClick.ts | 7 +- .../mouseRightClick/onMapRightClick.test.ts | 22 +- .../mouseRightClick/onMapRightClick.ts | 6 +- .../listeners/useOlMapVectorListeners.ts | 14 +- src/redux/bioEntity/bioEntity.reducers.ts | 22 +- src/redux/bioEntity/bioEntity.slice.ts | 10 +- .../entityNumber/entityNumber.reducers.ts | 1 - .../newReactions/newReactions.selectors.ts | 5 - src/redux/reactions/reactions.reducers.ts | 11 +- src/redux/reactions/reactions.slice.ts | 9 +- src/utils/bioEntity/mapReactionToBioEntity.ts | 19 ++ src/utils/reaction/ReactionTypeEnum.ts | 30 +++ .../reaction/mapNewReactionToReaction.test.ts | 193 ++++++++++++++++++ .../reaction/mapNewReactionToReaction.ts | 45 ++++ 19 files changed, 454 insertions(+), 82 deletions(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.ts create mode 100644 src/utils/bioEntity/mapReactionToBioEntity.ts create mode 100644 src/utils/reaction/ReactionTypeEnum.ts create mode 100644 src/utils/reaction/mapNewReactionToReaction.test.ts create mode 100644 src/utils/reaction/mapNewReactionToReaction.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts index f6aab10a..5282e246 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts @@ -2,14 +2,13 @@ import { openReactionDrawerById, 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 { reactionsFixture } from '@/models/fixtures/reactionFixture'; import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; 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'; +import { newReactionFixture } from '@/models/fixtures/newReactionFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); jest.mock('../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds'); @@ -20,7 +19,6 @@ describe('clickHandleReaction', () => { let modelId = 1; let reactionId = 1; const hasFitBounds = true; - const feature = new Feature({ type: FEATURE_TYPE.REACTION, id: 1 }); beforeEach(() => { jest.clearAllMocks(); @@ -54,8 +52,13 @@ describe('clickHandleReaction', () => { ) .reply(HttpStatusCode.Ok, bioEntityFixture); }); - await clickHandleReaction(dispatch, hasFitBounds)(feature, modelId); - expect(dispatch).toHaveBeenCalledTimes(4); + clickHandleReaction(dispatch, hasFitBounds)( + [], + [{ ...newReactionFixture, id: reactionId }], + reactionId, + modelId, + ); + expect(dispatch).toHaveBeenCalledTimes(5); expect(dispatch).toHaveBeenCalledWith(openReactionDrawerById(reactionId)); expect(dispatch).toHaveBeenCalledWith(selectTab('')); expect(eventBusDispatchEventSpy).toHaveBeenCalled(); @@ -68,7 +71,7 @@ describe('clickHandleReaction', () => { unwrap: jest.fn().mockResolvedValue(mockBioEntities), })); - await clickHandleReaction(dispatch, false)(feature, modelId); + clickHandleReaction(dispatch, false)([], [{ ...newReactionFixture, id: 1 }], 1, modelId); expect(searchFitBounds).not.toHaveBeenCalled(); }); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts index 676d0b49..392e4c3e 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts @@ -1,68 +1,56 @@ -import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { openReactionDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; -import { getReactionsByIds } from '@/redux/reactions/reactions.thunks'; import { AppDispatch } from '@/redux/store'; import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; -import { BioEntity } from '@/types/models'; -import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { apiPath } from '@/redux/apiPath'; -import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; -import { bioEntitySchema } from '@/models/bioEntitySchema'; -import { getMultiBioEntityByIds } from '@/redux/bioEntity/thunks/getMultiBioEntity'; -import { FeatureLike } from 'ol/Feature'; -import { getBioEntitiesIdsFromReaction } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/getBioEntitiesIdsFromReaction'; +import { BioEntity, ModelElement, NewReaction } from '@/types/models'; import { FEATURE_TYPE } from '@/constants/features'; +import { setMultipleBioEntityContents } from '@/redux/bioEntity/bioEntity.slice'; +import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; +import { setReactions } from '@/redux/reactions/reactions.slice'; +import mapNewReactionToReaction from '@/utils/reaction/mapNewReactionToReaction'; +import { mapReactionToBioEntity } from '@/utils/bioEntity/mapReactionToBioEntity'; +import getModelElementsIdsFromReaction from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction'; +import { mapModelElementToBioEntity } from '@/utils/bioEntity/mapModelElementToBioEntity'; /* prettier-ignore */ export const clickHandleReaction = (dispatch: AppDispatch, hasFitBounds = false) => - async (feature: FeatureLike, modelId: number): Promise<void> => { - const id = feature.get('id'); - const data = await dispatch(getReactionsByIds([id])); - const payload = data?.payload; - if (!data || !payload || typeof payload === 'string' || payload.data.length === SIZE_OF_EMPTY_ARRAY) { + ( modelElements: Array<ModelElement>, reactions: Array<NewReaction>, reactionId: number, modelId: number): void => { + + const reactionBioEntities: Array<BioEntity> = []; + const reaction = reactions.find(newReaction => newReaction.id === reactionId); + if(!reaction) { return; } + const modelElementsIds = getModelElementsIdsFromReaction(reaction); + modelElementsIds.forEach(modelElementId => { + const modelElement = modelElements.find(element => + element.id === modelElementId + ); + if(!modelElement) { + return; + } + reactionBioEntities.push(mapModelElementToBioEntity(modelElement)); + }); - const reaction = payload.data[FIRST_ARRAY_ELEMENT]; - - const bioEntitiesIds = getBioEntitiesIdsFromReaction(reaction); - - dispatch(openReactionDrawerById(reaction.id)); + dispatch(openReactionDrawerById(reactionId)); dispatch(selectTab('')); - const response = await axiosInstanceNewAPI.get<BioEntity>(apiPath.getReactionByIdInNewApi(reaction.id, reaction.modelId)); - const isDataValid = validateDataUsingZodSchema(response.data, bioEntitySchema); - - if (isDataValid) { - const reactionNewApi = response.data; - - const bioEntities = await dispatch( - getMultiBioEntityByIds({ - elementsToFetch: bioEntitiesIds.map((bioEntityId) => { - return { - elementId: parseInt(bioEntityId, 10), - modelId, - type: FEATURE_TYPE.ALIAS - }; - }) - }) - ).unwrap(); - - if (bioEntities) { - const result = bioEntities.map((bioEntity) => {return { bioEntity, perfect: true };}); - result.push({ bioEntity: reactionNewApi, perfect: true }); - PluginsEventBus.dispatchEvent('onSearch', { - type: 'reaction', - searchValues: [{ id, modelId, type: FEATURE_TYPE.REACTION }], - results: [result] - }); - - if (hasFitBounds) { - searchFitBounds(); - } - } + const bioEntityReaction = mapReactionToBioEntity(reaction); + dispatch(setMultipleBioEntityContents(reactionBioEntities)); + dispatch(addNumbersToEntityNumberData(reactionBioEntities.map(reactionBioEntity => reactionBioEntity.elementId))); + dispatch(setReactions([mapNewReactionToReaction(reaction)])); + + const result = reactionBioEntities.map((bioEntity) => {return { bioEntity, perfect: true };}); + result.push({ bioEntity: bioEntityReaction, perfect: true }); + PluginsEventBus.dispatchEvent('onSearch', { + type: 'reaction', + searchValues: [{ id: reactionId, modelId, type: FEATURE_TYPE.REACTION }], + results: [result] + }); + + if (hasFitBounds) { + searchFitBounds(); } }; diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.test.ts new file mode 100644 index 00000000..fec3d634 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.test.ts @@ -0,0 +1,24 @@ +/* eslint-disable no-magic-numbers */ +import { newReactionFixture } from '@/models/fixtures/newReactionFixture'; +import getModelElementsIdsFromReaction from './getModelElementsIdsFromReaction'; + +describe('getModelElementsIdsFromReaction', () => { + it('should return correct model elements ids from given reaction', () => { + const result = getModelElementsIdsFromReaction(newReactionFixture); + expect(result).toEqual([ + ...newReactionFixture.products.map(product => product.element), + ...newReactionFixture.reactants.map(reactant => reactant.element), + ...newReactionFixture.modifiers.map(modifier => modifier.element), + ]); + }); + + it('should return empty array', () => { + const result = getModelElementsIdsFromReaction({ + ...newReactionFixture, + products: [], + reactants: [], + modifiers: [], + }); + expect(result).toEqual([]); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.ts new file mode 100644 index 00000000..a3ed0a9f --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/getModelElementsIdsFromReaction.ts @@ -0,0 +1,7 @@ +import { NewReaction } from '@/types/models'; + +export default function getModelElementsIdsFromReaction(reaction: NewReaction): Array<number> { + return [...reaction.products, ...reaction.reactants, ...reaction.modifiers].map( + bioEntity => bioEntity.element, + ); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts index 166b70df..373c01d1 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts @@ -59,6 +59,8 @@ describe('onMapLeftClick', () => { dispatch, isResultDrawerOpen, comments, + [], + [], )(event, mapInstance); expect(dispatch).toHaveBeenCalledWith(updateLastClick(expect.any(Object))); @@ -84,6 +86,8 @@ describe('onMapLeftClick', () => { dispatch, isResultDrawerOpen, comments, + [], + [], )(event, mapInstance); expect(leftClickHandleAliasSpy).toHaveBeenCalledWith(dispatch); @@ -106,6 +110,8 @@ describe('onMapLeftClick', () => { dispatch, isResultDrawerOpen, comments, + [], + [], )(event, mapInstance); expect(clickHandleReactionSpy).toHaveBeenCalledWith(dispatch); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts index 7b047dda..7a2dd125 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts @@ -1,7 +1,7 @@ import { MapSize } from '@/redux/map/map.types'; import { AppDispatch } from '@/redux/store'; import { Map, MapBrowserEvent } from 'ol'; -import { Comment } from '@/types/models'; +import { Comment, ModelElement, NewReaction } from '@/types/models'; import { updateLastClick } from '@/redux/map/map.slice'; import { toLonLat } from 'ol/proj'; import { latLngToPoint } from '@/utils/map/latLngToPoint'; @@ -17,7 +17,7 @@ import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/ /* prettier-ignore */ export const onMapLeftClick = - (mapSize: MapSize, modelId: number, dispatch: AppDispatch, isResultDrawerOpen: boolean, comments: Comment[]) => + (mapSize: MapSize, modelId: number, dispatch: AppDispatch, isResultDrawerOpen: boolean, comments: Comment[], modelElements: Array<ModelElement>, reactions: Array<NewReaction>) => async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => { const [lng, lat] = toLonLat(coordinate); const point = latLngToPoint([lat, lng], mapSize); @@ -52,9 +52,10 @@ export const onMapLeftClick = dispatch(handleDataReset); const type = featureAtPixel.get('type'); + const id = featureAtPixel.get('id'); if(type === FEATURE_TYPE.ALIAS) { await leftClickHandleAlias(dispatch)(featureAtPixel, modelId); } else if (type === FEATURE_TYPE.REACTION) { - await clickHandleReaction(dispatch)(featureAtPixel, modelId); + clickHandleReaction(dispatch)(modelElements, reactions, id, 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 index 362df7ae..4502edd0 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts @@ -9,7 +9,8 @@ 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 { modelElementFixture } from '@/models/fixtures/modelElementFixture'; +import { newReactionFixture } from '@/models/fixtures/newReactionFixture'; import * as rightClickHandleAlias from './rightClickHandleAlias'; import * as clickHandleReaction from '../clickHandleReaction'; @@ -51,7 +52,6 @@ describe('onMapRightClick', () => { 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]; }); @@ -62,9 +62,15 @@ describe('onMapRightClick', () => { return vectorSource; }); jest.spyOn(vectorSource, 'getClosestFeatureToCoordinate').mockImplementation((): Feature => { - return new Feature({ id: modelElement.id, type: FEATURE_TYPE.ALIAS }); + return new Feature({ id: modelElementFixture.id, type: FEATURE_TYPE.ALIAS }); }); - await onMapRightClick(mapSize, modelId, dispatch, [modelElement])(event, mapInstance); + await onMapRightClick( + mapSize, + modelId, + dispatch, + [modelElementFixture], + [], + )(event, mapInstance); expect(dispatch).toHaveBeenCalledWith(updateLastRightClick(expect.any(Object))); expect(dispatch).toHaveBeenCalledWith(openContextMenu(event.pixel)); @@ -85,7 +91,13 @@ describe('onMapRightClick', () => { jest.spyOn(vectorSource, 'getClosestFeatureToCoordinate').mockImplementation((): Feature => { return new Feature({ id: 1, type: FEATURE_TYPE.REACTION }); }); - await onMapRightClick(mapSize, modelId, dispatch, [])(event, mapInstance); + await onMapRightClick( + mapSize, + modelId, + dispatch, + [], + [{ ...newReactionFixture, id: 1 }], + )(event, mapInstance); expect(dispatch).toHaveBeenCalledWith(updateLastRightClick(expect.any(Object))); expect(dispatch).toHaveBeenCalledWith(openContextMenu(event.pixel)); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts index ae01e642..cb141e34 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts @@ -9,13 +9,13 @@ 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 { ModelElement, NewReaction } from '@/types/models'; import { rightClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias'; import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction'; /* prettier-ignore */ export const onMapRightClick = - (mapSize: MapSize, modelId: number, dispatch: AppDispatch, modelElements: Array<ModelElement>) => + (mapSize: MapSize, modelId: number, dispatch: AppDispatch, modelElements: Array<ModelElement>, reactions: Array<NewReaction>) => async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => { const [lng, lat] = toLonLat(coordinate); @@ -50,6 +50,6 @@ export const onMapRightClick = } await rightClickHandleAlias(dispatch)(id, modelElement); } else if (type === FEATURE_TYPE.REACTION) { - await clickHandleReaction(dispatch)(foundFeature, modelId); + clickHandleReaction(dispatch)(modelElements, reactions, id, modelId); } }; diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts index 2a0af8ea..05993a81 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts @@ -15,6 +15,7 @@ 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'; +import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors'; interface UseOlMapVectorListenersInput { mapInstance: MapInstance; @@ -25,6 +26,7 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListeners const modelId = useSelector(currentModelIdSelector); const isResultDrawerOpen = useSelector(resultDrawerOpen); const modelElements = useSelector(modelElementsSelector); + const reactions = useSelector(newReactionsDataSelector); const dispatch = useAppDispatch(); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); @@ -33,13 +35,21 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListeners const vectorRendering = useAppSelector(vectorRenderingSelector); const handleMapLeftClick = useDebouncedCallback( - onMapLeftClick(mapSize, modelId, dispatch, isResultDrawerOpen, comments), + onMapLeftClick( + mapSize, + modelId, + dispatch, + isResultDrawerOpen, + comments, + modelElements?.content || [], + reactions, + ), OPTIONS.clickPersistTime, { leading: false }, ); const handleRightClick = useDebouncedCallback( - onMapRightClick(mapSize, modelId, dispatch, modelElements?.content || []), + onMapRightClick(mapSize, modelId, dispatch, modelElements?.content || [], reactions), OPTIONS.clickPersistTime, { leading: false, diff --git a/src/redux/bioEntity/bioEntity.reducers.ts b/src/redux/bioEntity/bioEntity.reducers.ts index deadfae3..f5f4f94c 100644 --- a/src/redux/bioEntity/bioEntity.reducers.ts +++ b/src/redux/bioEntity/bioEntity.reducers.ts @@ -1,7 +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, BioEntityContent } from '@/types/models'; import { BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE } from './bioEntity.constants'; import { getBioEntity, getMultiBioEntity } from './bioEntity.thunks'; import { BioEntityContentsState } from './bioEntity.types'; @@ -136,3 +136,23 @@ export const setBioEntityContentsReducer = ( ]; state.loading = 'succeeded'; }; + +export const setMultipleBioEntityContentsReducer = ( + state: BioEntityContentsState, + action: PayloadAction<Array<BioEntity>>, +): void => { + state.data = [ + { + data: action.payload.map(bioEntity => { + return { + bioEntity, + perfect: true, + }; + }), + loading: 'succeeded', + error: DEFAULT_ERROR, + searchQueryElement: 'asd', + }, + ]; + state.loading = 'succeeded'; +}; diff --git a/src/redux/bioEntity/bioEntity.slice.ts b/src/redux/bioEntity/bioEntity.slice.ts index 40f2bbb9..dbd2093c 100644 --- a/src/redux/bioEntity/bioEntity.slice.ts +++ b/src/redux/bioEntity/bioEntity.slice.ts @@ -6,6 +6,7 @@ import { getMultiBioEntityContentsReducer, getSubmapConnectionsBioEntityReducer, setBioEntityContentsReducer, + setMultipleBioEntityContentsReducer, toggleIsContentTabOpenedReducer, } from './bioEntity.reducers'; @@ -16,6 +17,7 @@ export const bioEntityContentsSlice = createSlice({ clearBioEntitiesData: clearBioEntitiesDataReducer, toggleIsContentTabOpened: toggleIsContentTabOpenedReducer, setBioEntityContents: setBioEntityContentsReducer, + setMultipleBioEntityContents: setMultipleBioEntityContentsReducer, }, extraReducers: builder => { getBioEntityContentsReducer(builder); @@ -24,7 +26,11 @@ export const bioEntityContentsSlice = createSlice({ }, }); -export const { clearBioEntitiesData, toggleIsContentTabOpened, setBioEntityContents } = - bioEntityContentsSlice.actions; +export const { + clearBioEntitiesData, + toggleIsContentTabOpened, + setBioEntityContents, + setMultipleBioEntityContents, +} = bioEntityContentsSlice.actions; export default bioEntityContentsSlice.reducer; diff --git a/src/redux/entityNumber/entityNumber.reducers.ts b/src/redux/entityNumber/entityNumber.reducers.ts index dd26edb9..15662170 100644 --- a/src/redux/entityNumber/entityNumber.reducers.ts +++ b/src/redux/entityNumber/entityNumber.reducers.ts @@ -20,7 +20,6 @@ export const addNumbersToEntityNumberDataReducer = ( const newEntityNumber: EntityNumber = Object.fromEntries( uniqueIds.map((id, index) => [id, lastNumber + index]), ); - state.data = { ...newEntityNumber, ...state.data, diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index fd9978fd..4dc2babe 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -7,8 +7,3 @@ export const newReactionsDataSelector = createSelector( newReactionsSelector, reactions => reactions.data || [], ); - -// export const newReactionsReactionByIdSelector = createSelector( -// newReactionsDataSelector, -// reactions => reactions.find(reaction => reaction.id === id) || [], -// ); diff --git a/src/redux/reactions/reactions.reducers.ts b/src/redux/reactions/reactions.reducers.ts index 2ef4a163..848b4ab7 100644 --- a/src/redux/reactions/reactions.reducers.ts +++ b/src/redux/reactions/reactions.reducers.ts @@ -1,4 +1,5 @@ -import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; +import { Reaction } from '@/types/models'; import { REACTIONS_INITIAL_STATE } from './reactions.constants'; import { getReactionsByIds } from './reactions.thunks'; import { ReactionsState } from './reactions.types'; @@ -26,3 +27,11 @@ export const resetReactionsDataReducer = (state: ReactionsState): void => { state.error = REACTIONS_INITIAL_STATE.error; state.loading = REACTIONS_INITIAL_STATE.loading; }; + +export const setReactionsReducer = ( + state: ReactionsState, + action: PayloadAction<Array<Reaction>>, +): void => { + state.data = action.payload; + state.loading = 'succeeded'; +}; diff --git a/src/redux/reactions/reactions.slice.ts b/src/redux/reactions/reactions.slice.ts index f97e9050..97cce842 100644 --- a/src/redux/reactions/reactions.slice.ts +++ b/src/redux/reactions/reactions.slice.ts @@ -1,18 +1,23 @@ import { createSlice } from '@reduxjs/toolkit'; import { REACTIONS_INITIAL_STATE } from './reactions.constants'; -import { getReactionsReducer, resetReactionsDataReducer } from './reactions.reducers'; +import { + getReactionsReducer, + resetReactionsDataReducer, + setReactionsReducer, +} from './reactions.reducers'; export const reactionsSlice = createSlice({ name: 'reactions', initialState: REACTIONS_INITIAL_STATE, reducers: { resetReactionsData: resetReactionsDataReducer, + setReactions: setReactionsReducer, }, extraReducers: builder => { getReactionsReducer(builder); }, }); -export const { resetReactionsData } = reactionsSlice.actions; +export const { resetReactionsData, setReactions } = reactionsSlice.actions; export default reactionsSlice.reducer; diff --git a/src/utils/bioEntity/mapReactionToBioEntity.ts b/src/utils/bioEntity/mapReactionToBioEntity.ts new file mode 100644 index 00000000..0aab81b7 --- /dev/null +++ b/src/utils/bioEntity/mapReactionToBioEntity.ts @@ -0,0 +1,19 @@ +import { BioEntity, NewReaction } from '@/types/models'; + +export function mapReactionToBioEntity(reaction: NewReaction): BioEntity { + return { + id: reaction.id, + name: reaction.name, + model: reaction.model, + elementId: reaction.elementId, + references: reaction.references, + z: reaction.z, + notes: reaction.notes, + symbol: reaction.symbol, + visibilityLevel: reaction.visibilityLevel, + synonyms: reaction.synonyms, + abbreviation: reaction.abbreviation, + formula: reaction.formula, + sboTerm: reaction.sboTerm, + } as BioEntity; +} diff --git a/src/utils/reaction/ReactionTypeEnum.ts b/src/utils/reaction/ReactionTypeEnum.ts new file mode 100644 index 00000000..a10ec320 --- /dev/null +++ b/src/utils/reaction/ReactionTypeEnum.ts @@ -0,0 +1,30 @@ +enum ReactionTypeEnum { + 'SBO:0000013' = 'Catalysis', + 'SBO:0000180' = 'Dissociation', + 'SBO:0000177' = 'Heterodimer association', + 'SBO:0000537' = 'Inhibition', + 'SBO:0000205' = 'Known Transition omitted', + 'SBO:0000594' = 'Modulation', + 'SBO:0000407' = 'Negative influence', + 'SBO:0000459' = 'Physical stimulation', + 'SBO:0000171' = 'Positive influence', + 'SBO:0000632' = 'Reduced modulation', + 'SBO:0000411' = 'Reduced physical stimulation', + 'SBO:0000533' = 'Reduced trigger', + 'SBO:0000176' = 'State transition', + 'SBO:0000183' = 'Transcription', + 'SBO:0000184' = 'Translation', + 'SBO:0000185' = 'Transport', + 'SBO:0000461' = 'Trigger', + 'SBO:0000178' = 'Truncation', + 'SBO:0000462' = 'Unknown catalysis', + 'SBO:0000536' = 'Unknown inhibition', + 'SBO:0000169' = 'Unknown negative influence', + 'SBO:0000172' = 'Unknown positive influence', + 'SBO:0000631' = 'Unknown reduced modulation', + 'SBO:0000170' = 'Unknown reduced physical stimulation', + 'SBO:0000534' = 'Unknown reduced trigger', + 'SBO:0000396' = 'Unknown transition', +} + +export default ReactionTypeEnum; diff --git a/src/utils/reaction/mapNewReactionToReaction.test.ts b/src/utils/reaction/mapNewReactionToReaction.test.ts new file mode 100644 index 00000000..23097b83 --- /dev/null +++ b/src/utils/reaction/mapNewReactionToReaction.test.ts @@ -0,0 +1,193 @@ +/* eslint-disable no-magic-numbers */ +import mapNewReactionToReaction from '@/utils/reaction/mapNewReactionToReaction'; + +describe('mapNewReactionToReaction', () => { + const newReaction = { + id: 31141, + notes: '', + idReaction: 're22', + name: '', + reversible: false, + symbol: null, + abbreviation: null, + formula: null, + mechanicalConfidenceScore: null, + lowerBound: null, + upperBound: null, + subsystem: null, + geneProteinReaction: null, + visibilityLevel: '', + z: 45, + synonyms: [], + model: 137, + kinetics: null, + line: { + id: 109668, + width: 1, + color: { + alpha: 255, + rgb: -16777216, + }, + z: 0, + segments: [ + { + x1: 149.31765717927775, + y1: 319.13724818355684, + x2: 142.5553586937381, + y2: 314.86275181644316, + }, + ], + startArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + endArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + lineType: 'SOLID', + }, + processCoordinates: null, + modifiers: [], + products: [ + { + id: 85169, + line: { + id: 109670, + width: 1, + color: { + alpha: 255, + rgb: -16777216, + }, + z: 0, + segments: [ + { + x1: 142.5553586937381, + y1: 314.86275181644316, + x2: 122.2063492063492, + y2: 302, + }, + ], + startArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + endArrow: { + arrowType: 'OPEN', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + lineType: 'SOLID', + }, + stoichiometry: null, + element: 58886, + }, + ], + reactants: [ + { + id: 85168, + line: { + id: 109669, + width: 1, + color: { + alpha: 255, + rgb: -16777216, + }, + z: 0, + segments: [ + { + x1: 169.66666666666666, + y1: 332, + x2: 149.31765717927775, + y2: 319.13724818355684, + }, + ], + startArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + endArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + lineType: 'SOLID', + }, + stoichiometry: null, + element: 58872, + }, + ], + operators: [], + elementId: 're22', + references: [], + sboTerm: 'SBO:0000171', + }; + const expectedReaction = { + centerPoint: { + x: 0, + y: 0, + }, + hierarchyVisibilityLevel: '', + id: 31141, + kineticLaw: null, + lines: [ + { + start: { + x: 149.31765717927775, + y: 319.13724818355684, + }, + end: { + x: 142.5553586937381, + y: 314.86275181644316, + }, + type: '', + }, + { + start: { + x: 142.5553586937381, + y: 314.86275181644316, + }, + end: { + x: 122.2063492063492, + y: 302, + }, + type: '', + }, + { + start: { + x: 169.66666666666666, + y: 332, + }, + end: { + x: 149.31765717927775, + y: 319.13724818355684, + }, + type: '', + }, + ], + modelId: 137, + modifiers: [], + name: '', + notes: '', + products: [], + reactants: [], + reactionId: 're22', + references: [], + type: 'Positive influence', + }; + + it('should return correct reaction object from new reaction object', () => { + const result = mapNewReactionToReaction(newReaction); + expect(result).toEqual(expectedReaction); + }); +}); diff --git a/src/utils/reaction/mapNewReactionToReaction.ts b/src/utils/reaction/mapNewReactionToReaction.ts new file mode 100644 index 00000000..a5fbd0e4 --- /dev/null +++ b/src/utils/reaction/mapNewReactionToReaction.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-magic-numbers */ +import { NewReaction, Reaction, ReactionLine } from '@/types/models'; +import ReactionTypeEnum from '@/utils/reaction/ReactionTypeEnum'; + +type ReactionTypeKey = keyof typeof ReactionTypeEnum; + +export default function mapNewReactionToReaction(newReaction: NewReaction): Reaction { + const lines: Array<ReactionLine> = []; + let start; + let end; + newReaction.line.segments.forEach(segment => { + start = { x: segment.x1, y: segment.y1 }; + end = { x: segment.x2, y: segment.y2 }; + lines.push({ start, end, type: '' }); + }); + [ + ...newReaction.products, + ...newReaction.reactants, + ...newReaction.modifiers, + ...newReaction.operators, + ].forEach(element => { + element.line.segments.forEach(segment => { + start = { x: segment.x1, y: segment.y1 }; + end = { x: segment.x2, y: segment.y2 }; + lines.push({ start, end, type: '' }); + }); + }); + + return { + centerPoint: { x: 0, y: 0 }, + hierarchyVisibilityLevel: '', + id: newReaction.id, + kineticLaw: null, + lines, + modelId: newReaction.model, + modifiers: [], + name: '', + notes: newReaction.notes, + products: [], + reactants: [], + reactionId: newReaction.idReaction, + references: newReaction.references, + type: ReactionTypeEnum[newReaction.sboTerm as ReactionTypeKey], + }; +} -- GitLab From 23f08c1e7345a5590de9d218aa6b6adf16271e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 12 Nov 2024 13:19:40 +0100 Subject: [PATCH 03/29] refactor(vector-map): move vector layer type name string to constant --- .../MapViewer/MapViewerVector/MapViewerVector.constants.ts | 2 ++ .../mouseClick/mouseRightClick/onMapRightClick.test.ts | 3 ++- .../listeners/mouseClick/mouseRightClick/onMapRightClick.ts | 3 ++- .../utils/config/reactionsLayer/useOlMapReactionsLayer.ts | 3 ++- src/redux/newReactions/newReactions.selectors.ts | 5 ----- 5 files changed, 8 insertions(+), 8 deletions(-) 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/mouseClick/mouseRightClick/onMapRightClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts index 362df7ae..a96f6601 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts @@ -10,6 +10,7 @@ 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'; @@ -45,7 +46,7 @@ describe('onMapRightClick', () => { vectorLayer = new VectorLayer({ source: vectorSource, }); - vectorLayer.set('type', 'vectorMapLayer'); + vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE); jest.clearAllMocks(); }); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts index ae01e642..c4673a83 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts @@ -12,6 +12,7 @@ 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 = @@ -25,7 +26,7 @@ export const onMapRightClick = let foundFeature: Feature | undefined; mapInstance.getAllLayers().forEach(layer => { if(layer.isVisible() && layer instanceof VectorLayer) { - if (layer.get('type') === 'vectorMapLayer') { + if (layer.get('type') === VECTOR_MAP_LAYER_TYPE) { const source = layer.getSource(); if (source instanceof VectorSource) { foundFeature = source.getClosestFeatureToCoordinate(coordinate, (feature) => { 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 5fbc2ae0..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, @@ -186,7 +187,7 @@ export const useOlMapReactionsLayer = ({ const vectorLayer = new VectorLayer({ source: vectorSource, }); - vectorLayer.set('type', 'vectorMapLayer'); + vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE); return vectorLayer; }, [vectorSource]); }; diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index fd9978fd..4dc2babe 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -7,8 +7,3 @@ export const newReactionsDataSelector = createSelector( newReactionsSelector, reactions => reactions.data || [], ); - -// export const newReactionsReactionByIdSelector = createSelector( -// newReactionsDataSelector, -// reactions => reactions.find(reaction => reaction.id === id) || [], -// ); -- GitLab From fd7315e2e83819974612f6a4facd044bf539a95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Wed, 13 Nov 2024 16:27:06 +0100 Subject: [PATCH 04/29] feat(vector-map): implement rectangle and sumbap-link overlays functionality --- .../MapViewerVector/MapViewerVector.types.ts | 5 ++ .../reactionsLayer/useOlMapReactionsLayer.ts | 42 ++++++++++-- .../utils/shapes/elements/MapElement.test.ts | 1 + .../utils/shapes/elements/MapElement.ts | 56 +++++++++++++++- .../calculateOverlayDimensions.test.ts | 47 +++++++++++++ .../overlay/calculateOverlayDimensions.ts | 20 ++++++ .../findMatchingSubmapLinkRectangle.test.ts | 66 +++++++++++++++++++ .../findMatchingSubmapLinkRectangle.ts | 19 ++++++ .../utils/shapes/overlay/getOverlays.test.ts | 60 +++++++++++++++++ .../utils/shapes/overlay/getOverlays.ts | 26 ++++++++ .../overlay/groupOverlayEntities.test.ts | 60 +++++++++++++++++ .../shapes/overlay/groupOverlayEntities.ts | 34 ++++++++++ .../processOverlayGroupedElements.test.ts | 55 ++++++++++++++++ .../overlay/processOverlayGroupedElements.ts | 35 ++++++++++ .../overlay/sortElementOverlayByColor.test.ts | 51 ++++++++++++++ .../overlay/sortElementOverlayByColor.ts | 20 ++++++ .../utils/shapes/style/getStyle.ts | 8 +-- .../createOverlayGeometryFeature.ts | 5 +- .../overlaysLayer/useGetOverlayColor.ts | 4 +- .../utils/config/useOlMapCommonLayers.test.ts | 7 -- .../utils/config/useOlMapCommonLayers.ts | 4 +- .../utils/config/useOlMapLayers.test.ts | 8 +++ .../MapViewer/utils/config/useOlMapLayers.ts | 4 +- 23 files changed, 613 insertions(+), 24 deletions(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts index a896f4be..e7ccb7c9 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts @@ -1,5 +1,6 @@ import View from 'ol/View'; import BaseLayer from 'ol/layer/Base'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; export type MapConfig = { view: View; @@ -8,3 +9,7 @@ export type MapConfig = { export type VerticalAlign = 'TOP' | 'MIDDLE' | 'BOTTOM'; export type HorizontalAlign = 'LEFT' | 'RIGHT' | 'CENTER' | 'END' | 'START'; + +export type OverlayBioEntityGroupedElementsType = { + [id: string]: Array<OverlayBioEntityRender & { amount: number }>; +}; 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 00ff8cb8..4e0b8bd0 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -29,6 +29,14 @@ import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/re 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'; +import { + getOverlayOrderSelector, + overlayBioEntitiesForCurrentModelSelector, +} from '@/redux/overlayBioEntity/overlayBioEntity.selector'; +import { groupBy } from '@/utils/array/groupBy'; +import { useGetOverlayColor } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import getOverlays from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -36,18 +44,28 @@ export const useOlMapReactionsLayer = ({ mapInstance: MapInstance; }): VectorLayer<VectorSource<Feature>> => { const dispatch = useAppDispatch(); + + const currentModelId = useSelector(currentModelIdSelector); const modelElements = useSelector(modelElementsSelector); const modelReactions = useSelector(newReactionsDataSelector); - const currentModelId = useSelector(currentModelIdSelector); + const shapes = useSelector(bioShapesSelector); + const lineTypes = useSelector(lineTypesSelector); + const arrowTypes = useSelector(arrowTypesSelector); + const overlaysOrder = useSelector(getOverlayOrderSelector); + const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); + + const pointToProjection = usePointToProjection(); + useEffect(() => { dispatch(getModelElements(currentModelId)); dispatch(getNewReactions(currentModelId)); }, [currentModelId, dispatch]); - const pointToProjection = usePointToProjection(); - const shapes = useSelector(bioShapesSelector); - const lineTypes = useSelector(lineTypesSelector); - const arrowTypes = useSelector(arrowTypesSelector); + const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); + const groupedOverlays = useMemo(() => { + const grouped = groupBy(bioEntities, bioEntity => bioEntity.id.toString()); + return getOverlays(grouped, getOverlayBioEntityColorByAvailableProperties); + }, [bioEntities, getOverlayBioEntityColorByAvailableProperties]); const reactions = useMemo(() => { return modelReactions.map(reaction => { @@ -164,12 +182,24 @@ export const useOlMapReactionsLayer = ({ modifications: element.modificationResidues, lineTypes, bioShapes: shapes, + overlays: groupedOverlays[element.id], + overlaysOrder, + getOverlayColor: getOverlayBioEntityColorByAvailableProperties, }), ); } }); return validElements; - }, [modelElements, shapes, pointToProjection, mapInstance, lineTypes]); + }, [ + modelElements, + shapes, + pointToProjection, + mapInstance, + lineTypes, + groupedOverlays, + overlaysOrder, + getOverlayBioEntityColorByAvailableProperties, + ]); const features = useMemo(() => { const reactionsFeatures = reactions.flat(); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts index 64c1f92d..71c5c3c2 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts @@ -60,6 +60,7 @@ describe('MapElement', () => { nameHorizontalAlign: 'CENTER', pointToProjection: jest.fn(), mapInstance, + getOverlayColor: (): string => '#ffffff', }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index dae35c65..94d5f96f 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -22,6 +22,11 @@ import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shape import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import { BioShapesDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { FEATURE_TYPE } from '@/constants/features'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { getPolygonLatitudeCoordinates } from '@/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates'; +import { ZERO } from '@/constants/common'; +import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; export type MapElementProps = { id: number; @@ -51,6 +56,9 @@ export type MapElementProps = { bioShapes?: BioShapesDict; lineTypes?: LineTypeDict; modifications?: Array<Modification>; + overlays?: Array<OverlayBioEntityRender>; + overlaysOrder?: Array<OverlayOrder>; + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; }; export default class MapElement extends BaseMultiPolygon { @@ -72,6 +80,12 @@ export default class MapElement extends BaseMultiPolygon { lineDash: Array<number> = []; + overlays: Array<OverlayBioEntityRender> = []; + + overlaysOrder: Array<OverlayOrder> = []; + + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + constructor({ id, shapes, @@ -100,6 +114,9 @@ export default class MapElement extends BaseMultiPolygon { bioShapes = {}, lineTypes = {}, modifications = [], + overlays = [], + overlaysOrder = [], + getOverlayColor, }: MapElementProps) { super({ type: FEATURE_TYPE.ALIAS, @@ -130,6 +147,9 @@ export default class MapElement extends BaseMultiPolygon { this.bioShapes = bioShapes; this.lineTypes = lineTypes; this.modifications = modifications; + this.overlays = overlays; + this.overlaysOrder = overlaysOrder; + this.getOverlayColor = getOverlayColor; this.createPolygons(); this.drawText(); this.drawMultiPolygonFeature(mapInstance); @@ -160,6 +180,7 @@ export default class MapElement extends BaseMultiPolygon { } this.drawElementPolygon(homodimerShift, homodimerOffset); } + this.drawOverlays(); } drawModification(modification: Modification, shapes: Array<Shape>): void { @@ -250,7 +271,7 @@ export default class MapElement extends BaseMultiPolygon { const elementStyle = getStyle({ geometry: elementPolygon, borderColor: this.borderColor, - fillColor: this.fillColor, + fillColor: this.overlays.length ? undefined : this.fillColor, lineWidth: this.lineWidth, lineDash: this.lineDash, zIndex: this.zIndex, @@ -260,4 +281,37 @@ export default class MapElement extends BaseMultiPolygon { this.styles.push(elementStyle); }); } + + drawOverlays(): void { + this.overlays.forEach(entity => { + if (entity.value === Infinity) { + return; + } + const { xMin, xMax } = getPolygonLatitudeCoordinates({ + width: entity.width, + nOverlays: this.overlaysOrder.length, + xMin: entity.x1, + overlayIndexBasedOnOrder: + this.overlaysOrder.find(({ id }) => id === entity.overlayId)?.index || ZERO, + }); + const color = this.getOverlayColor(entity); + const polygon = new Polygon([ + [ + this.pointToProjection({ x: xMin, y: entity.y1 }), + this.pointToProjection({ x: xMax, y: entity.y1 }), + this.pointToProjection({ x: xMax, y: entity.y2 }), + this.pointToProjection({ x: xMin, y: entity.y2 }), + ], + ]); + const style = getStyle({ + geometry: polygon, + borderColor: color, + fillColor: color, + zIndex: this.zIndex, + }); + this.polygons.push(polygon); + this.lineWidths.push(1); + this.styles.push(style); + }); + } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.test.ts new file mode 100644 index 00000000..e328d4e4 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.test.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import calculateOverlayDimensions from './calculateOverlayDimensions'; + +describe('calculateOverlayDimensions', () => { + it('should calculate overlay dimensions and update y1, y2, and height', () => { + const overlay: OverlayBioEntityRender & { amount: number } = { + amount: 3, + color: null, + height: 100, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }; + const entityOverlays: Array<OverlayBioEntityRender> = [ + { + color: null, + height: 100, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + const index = 1; + const result = calculateOverlayDimensions(overlay, index, 4, 100, entityOverlays); + + expect(result.height).toEqual(75); + expect(result.y1).toEqual(entityOverlays[index - 1].y1 + 75); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.ts new file mode 100644 index 00000000..7ac9819c --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; + +export default function calculateOverlayDimensions( + overlay: OverlayBioEntityRender & { amount: number }, + index: number, + totalAmount: number, + totalHeight: number, + entityOverlays: Array<OverlayBioEntityRender>, +): OverlayBioEntityRender { + const ratio = overlay.amount / totalAmount; + const overlayHeight = ratio * totalHeight; + const overlayEntity = { ...overlay, height: overlayHeight }; + if (index !== 0) { + overlayEntity.y2 = entityOverlays[index - 1].y1; + } + overlayEntity.y1 = overlayEntity.y2 + overlayHeight; + + return overlayEntity; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.test.ts new file mode 100644 index 00000000..e19c1bfc --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.test.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import findMatchingSubmapLinkRectangle from './findMatchingSubmapLinkRectangle'; + +describe('findMatchingSubmapLinkRectangle', () => { + const elements: Array<OverlayBioEntityRender & { amount: number }> = [ + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + it('should find a matching submap link rectangle by value or color', () => { + const overlayBioEntity: OverlayBioEntityRender = { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }; + + const result = findMatchingSubmapLinkRectangle(elements, overlayBioEntity); + expect(result).toEqual(elements[0]); + }); + + it('should return undefined if no matching element is found', () => { + const overlayBioEntity: OverlayBioEntityRender = { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.21, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }; + + const result = findMatchingSubmapLinkRectangle(elements, overlayBioEntity); + expect(result).toBeUndefined(); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.ts new file mode 100644 index 00000000..25305cc3 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.ts @@ -0,0 +1,19 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; + +export default function findMatchingSubmapLinkRectangle( + elements: Array<OverlayBioEntityRender & { amount: number }>, + overlayBioEntity: OverlayBioEntityRender, +): (OverlayBioEntityRender & { amount: number }) | undefined { + return elements.find(element => { + const hasAllRequiredValueProperties = element.value && overlayBioEntity.value; + const isValueEqual = hasAllRequiredValueProperties && element.value === overlayBioEntity.value; + + const hasAllRequiredColorProperties = element.color && overlayBioEntity.color; + const isColorEqual = + hasAllRequiredColorProperties && + element.color?.alpha === overlayBioEntity?.color?.alpha && + element.color?.rgb === overlayBioEntity?.color?.rgb; + + return Boolean(isValueEqual || isColorEqual); + }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.test.ts new file mode 100644 index 00000000..e9fef8cf --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.test.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import groupOverlayEntities from './groupOverlayEntities'; + +describe('groupOverlays', () => { + it('should group overlay entities correctly by overlayId, value, and color', () => { + const overlayBioEntities: Array<OverlayBioEntityRender> = [ + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '1', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '1', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 2, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + const result = groupOverlayEntities(overlayBioEntities); + expect(result['1'][0].amount).toBe(2); + expect(result['2'][0].amount).toBe(1); + expect(result['3']).toBeUndefined(); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.ts new file mode 100644 index 00000000..8cc7c004 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.ts @@ -0,0 +1,26 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import groupOverlayEntities from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities'; +import processOverlayGroupedElements from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements'; + +export default function getOverlays( + groupedOverlays: Record<string, Array<OverlayBioEntityRender>>, + getColor: GetOverlayBioEntityColorByAvailableProperties, +): Record<string, Array<OverlayBioEntityRender>> { + const resultEntityOverlays: Record<string, Array<OverlayBioEntityRender>> = {}; + + Object.entries(groupedOverlays).forEach(([key, overlayBioEntities]) => { + const entityOverlays: Array<OverlayBioEntityRender> = []; + if (!resultEntityOverlays[key]) { + resultEntityOverlays[key] = []; + } + + const groupedElements = groupOverlayEntities(overlayBioEntities); + + processOverlayGroupedElements(groupedElements, entityOverlays, getColor); + + resultEntityOverlays[key].push(...entityOverlays); + }); + + return resultEntityOverlays; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.test.ts new file mode 100644 index 00000000..1f58872b --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.test.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import groupOverlayEntities from './groupOverlayEntities'; + +describe('groupOverlayEntities', () => { + it('should group overlay entities correctly by overlayId, value, and color', () => { + const overlayBioEntities: Array<OverlayBioEntityRender> = [ + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '1', + modelId: 0, + overlayId: 0, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '2', + modelId: 0, + overlayId: 0, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + const result = groupOverlayEntities(overlayBioEntities); + + expect(result['0'][0].amount).toBe(2); + expect(result['1'][0].amount).toBe(1); + expect(result['2']).toBeUndefined(); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.ts new file mode 100644 index 00000000..984574cf --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.ts @@ -0,0 +1,34 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import findMatchingSubmapLinkRectangle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle'; +import { OverlayBioEntityGroupedElementsType } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +export default function groupOverlayEntities( + overlayBioEntities: Array<OverlayBioEntityRender>, +): OverlayBioEntityGroupedElementsType { + const groupedElements: OverlayBioEntityGroupedElementsType = {}; + + overlayBioEntities.forEach(overlayBioEntity => { + if (overlayBioEntity.type !== 'submap-link') { + return; + } + if (!groupedElements[overlayBioEntity.overlayId]) { + groupedElements[overlayBioEntity.overlayId] = []; + } + + const matchedElement = findMatchingSubmapLinkRectangle( + groupedElements[overlayBioEntity.overlayId], + overlayBioEntity, + ); + + if (!matchedElement) { + groupedElements[overlayBioEntity.overlayId].push({ + ...overlayBioEntity, + amount: 1, + }); + } else { + matchedElement.amount += 1; + } + }); + + return groupedElements; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.test.ts new file mode 100644 index 00000000..680002ea --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.test.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import processOverlayGroupedElements from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements'; +import { OverlayBioEntityGroupedElementsType } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +describe('processOverlayGroupedElements', () => { + it('should correctly process overlay grouped elements and add to entityOverlays', () => { + const groupedElements = { + '1': [ + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '5', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '5', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ], + } as OverlayBioEntityGroupedElementsType; + + const entityOverlays: Array<OverlayBioEntityRender> = []; + const getColor: GetOverlayBioEntityColorByAvailableProperties = jest.fn(() => 'color'); + + processOverlayGroupedElements(groupedElements, entityOverlays, getColor); + + expect(entityOverlays.length).toBe(2); + expect(entityOverlays[0].height).toEqual(25); + expect(entityOverlays[1].height).toEqual(25); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.ts new file mode 100644 index 00000000..7e2a9b43 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.ts @@ -0,0 +1,35 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import sortElementOverlayByColor from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor'; +import calculateOverlayDimensions from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions'; +import { OverlayBioEntityGroupedElementsType } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +export default function processOverlayGroupedElements( + groupedElements: OverlayBioEntityGroupedElementsType, + entityOverlays: Array<OverlayBioEntityRender>, + getColor: GetOverlayBioEntityColorByAvailableProperties, +): void { + Object.values(groupedElements).forEach(elementOverlay => { + const overlaysPerGroup: Array<OverlayBioEntityRender> = []; + sortElementOverlayByColor(elementOverlay, getColor); + + const totalHeight = elementOverlay[0].height; + const totalAmount = elementOverlay.reduce( + (accumulator: number, overlay) => accumulator + overlay.amount, + 0, + ); + + elementOverlay.forEach((overlay, index) => { + const overlayEntity = calculateOverlayDimensions( + overlay, + index, + totalAmount, + totalHeight, + overlaysPerGroup, + ); + overlaysPerGroup.push(overlayEntity); + }); + entityOverlays.push(...overlaysPerGroup); + }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.test.ts new file mode 100644 index 00000000..1f39cc2a --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.test.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import sortElementOverlayByColor from './sortElementOverlayByColor'; + +describe('sortElementOverlayByColor', () => { + it('should sort elements by color', () => { + const elementOverlay: Array<OverlayBioEntityRender & { amount: number }> = [ + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '5', + modelId: 0, + overlayId: 0, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '2', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + const getColor: GetOverlayBioEntityColorByAvailableProperties = jest.fn( + entity => `#A633C${entity.id}`, + ); + sortElementOverlayByColor(elementOverlay, getColor); + + expect(elementOverlay[0].id).toEqual('2'); + expect(elementOverlay[1].id).toEqual('5'); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.ts new file mode 100644 index 00000000..b185b637 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-magic-numbers */ +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; + +export default function sortElementOverlayByColor( + elementOverlay: Array<OverlayBioEntityRender & { amount: number }>, + getColor: GetOverlayBioEntityColorByAvailableProperties, +): void { + elementOverlay.sort((a, b) => { + const colorA = getColor(a); + const colorB = getColor(b); + if (colorA === colorB) { + return 0; + } + if (colorA < colorB) { + return -1; + } + return 1; + }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts index 6c98bf80..96083434 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts @@ -19,8 +19,8 @@ export default function getStyle({ zIndex = 1, }: { geometry?: Geometry; - borderColor?: Color; - fillColor?: Color; + borderColor?: Color | string; + fillColor?: Color | string; lineWidth?: number; lineDash?: Array<number>; zIndex?: number; @@ -28,11 +28,11 @@ export default function getStyle({ return new Style({ geometry, stroke: getStroke({ - color: rgbToHex(borderColor), + color: typeof borderColor === 'string' ? borderColor : rgbToHex(borderColor), width: lineWidth, lineDash, }), - fill: getFill({ color: rgbToHex(fillColor) }), + fill: getFill({ color: typeof fillColor === 'string' ? fillColor : rgbToHex(fillColor) }), zIndex, }); } diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts index e11025d8..a6ca120f 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts @@ -5,7 +5,10 @@ import { Fill, Stroke, Style } from 'ol/style'; import { createFeatureFromExtent } from './createFeatureFromExtent'; const getBioEntityOverlayFeatureStyle = (color: string): Style => - new Style({ fill: new Fill({ color }), stroke: new Stroke({ color: 'black', width: 1 }) }); + new Style({ + fill: new Fill({ color }), + stroke: new Stroke({ color: 'black', width: 1 }), + }); export const createOverlayGeometryFeature = ( [xMin, yMin, xMax, yMax]: number[], diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts index 46d71907..0ac575a4 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts @@ -14,7 +14,9 @@ import { getHexStringColorFromRGBIntWithAlpha } from '@/utils/convert/getHexStri import { getHexTricolorGradientColorWithAlpha } from '@/utils/convert/getHexTricolorGradientColorWithAlpha'; import { useCallback, useMemo } from 'react'; -type GetOverlayBioEntityColorByAvailableProperties = (entity: OverlayBioEntityRender) => string; +export type GetOverlayBioEntityColorByAvailableProperties = ( + entity: OverlayBioEntityRender, +) => string; type UseTriColorLerpReturn = { getOverlayBioEntityColorByAvailableProperties: GetOverlayBioEntityColorByAvailableProperties; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts index ded469b2..852f8093 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts @@ -88,11 +88,4 @@ describe('useOlMapCommonLayers - util', () => { expect(result[2]).toBeInstanceOf(VectorLayer); expect(result[2].getSourceState()).toBe('ready'); }); - - it('should return valid VectorLayer instance [4]', () => { - const result = getRenderedHookResults(); - - expect(result[3]).toBeInstanceOf(VectorLayer); - expect(result[3].getSourceState()).toBe('ready'); - }); }); diff --git a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts index bca964e7..bfec74c6 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts @@ -1,15 +1,13 @@ /* eslint-disable no-magic-numbers */ -import { useOlMapOverlaysLayer } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer'; import { useOlMapPinsLayer } from '@/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer'; import { useOlMapReactionsLayer } from '@/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer'; import { useOlMapCommentsLayer } from '@/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer'; import { MapConfig } from '../../MapViewer.types'; export const useOlMapCommonLayers = (): MapConfig['layers'] => { - const overlaysLayer = useOlMapOverlaysLayer(); const pinsLayer = useOlMapPinsLayer(); const reactionsLayer = useOlMapReactionsLayer(); const commentsLayer = useOlMapCommentsLayer(); - return [overlaysLayer, pinsLayer, reactionsLayer, commentsLayer]; + return [pinsLayer, reactionsLayer, commentsLayer]; }; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts index c0be0af8..ffcd6326 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts @@ -5,6 +5,7 @@ import { renderHook } from '@testing-library/react'; import BaseLayer from 'ol/layer/Base'; import TileLayer from 'ol/layer/Tile'; import React from 'react'; +import VectorLayer from 'ol/layer/Vector'; import { useOlMapLayers } from './useOlMapLayers'; const useRefValue = { @@ -74,4 +75,11 @@ describe('useOlMapLayers - util', () => { expect(result[0]).toBeInstanceOf(TileLayer); expect(result[0].getSourceState()).toBe('ready'); }); + + it('should return valid VectorLayer instance [2]', () => { + const result = getRenderedHookResults(); + + expect(result[1]).toBeInstanceOf(VectorLayer); + expect(result[1].getSourceState()).toBe('ready'); + }); }); diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts index f05f8c9d..10169c98 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts @@ -1,9 +1,11 @@ /* eslint-disable no-magic-numbers */ +import { useOlMapOverlaysLayer } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer'; import { MapConfig } from '../../MapViewer.types'; import { useOlMapTileLayer } from './useOlMapTileLayer'; export const useOlMapLayers = (): MapConfig['layers'] => { + const overlaysLayer = useOlMapOverlaysLayer(); const tileLayer = useOlMapTileLayer(); - return [tileLayer]; + return [tileLayer, overlaysLayer]; }; -- GitLab From c47e1c1da8c237894f5b7e456b1dd6f355923e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 08:50:42 +0100 Subject: [PATCH 05/29] feat(vector-map): implement line overlay functionality --- .../reactionsLayer/useOlMapReactionsLayer.ts | 35 +++++++-- .../utils/shapes/overlay/LineOverlay.test.ts | 60 +++++++++++++++ .../utils/shapes/overlay/LineOverlay.ts | 75 +++++++++++++++++++ 3 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.ts 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 4e0b8bd0..325d3097 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -37,6 +37,7 @@ import { groupBy } from '@/utils/array/groupBy'; import { useGetOverlayColor } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import getOverlays from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays'; +import LineOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -62,11 +63,33 @@ export const useOlMapReactionsLayer = ({ }, [currentModelId, dispatch]); const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); - const groupedOverlays = useMemo(() => { - const grouped = groupBy(bioEntities, bioEntity => bioEntity.id.toString()); + + const groupedElementsOverlays = useMemo(() => { + const elementsBioEntitesOverlay = bioEntities.filter(bioEntity => bioEntity.type !== 'line'); + const grouped = groupBy(elementsBioEntitesOverlay, bioEntity => bioEntity.id.toString()); return getOverlays(grouped, getOverlayBioEntityColorByAvailableProperties); }, [bioEntities, getOverlayBioEntityColorByAvailableProperties]); + const linesOverlays = useMemo(() => { + return bioEntities.filter(bioEntity => bioEntity.type === 'line'); + }, [bioEntities]); + + const linesOverlaysFeatures = useMemo(() => { + return linesOverlays.map(lineOverlay => { + return new LineOverlay({ + lineOverlay, + getOverlayColor: getOverlayBioEntityColorByAvailableProperties, + pointToProjection, + mapInstance, + }).lineFeature; + }); + }, [ + getOverlayBioEntityColorByAvailableProperties, + linesOverlays, + mapInstance, + pointToProjection, + ]); + const reactions = useMemo(() => { return modelReactions.map(reaction => { const reactionShapes = shapes && shapes[reaction.sboTerm]; @@ -182,7 +205,7 @@ export const useOlMapReactionsLayer = ({ modifications: element.modificationResidues, lineTypes, bioShapes: shapes, - overlays: groupedOverlays[element.id], + overlays: groupedElementsOverlays[element.id], overlaysOrder, getOverlayColor: getOverlayBioEntityColorByAvailableProperties, }), @@ -196,7 +219,7 @@ export const useOlMapReactionsLayer = ({ pointToProjection, mapInstance, lineTypes, - groupedOverlays, + groupedElementsOverlays, overlaysOrder, getOverlayBioEntityColorByAvailableProperties, ]); @@ -204,8 +227,8 @@ export const useOlMapReactionsLayer = ({ const features = useMemo(() => { const reactionsFeatures = reactions.flat(); const elementsFeatures = elements.map(element => element.feature); - return [...reactionsFeatures, ...elementsFeatures]; - }, [elements, reactions]); + return [...reactionsFeatures, ...elementsFeatures, ...linesOverlaysFeatures]; + }, [elements, linesOverlaysFeatures, reactions]); const vectorSource = useMemo(() => { return new VectorSource({ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.test.ts new file mode 100644 index 00000000..4efbc9d6 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.test.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-magic-numbers */ +import { Feature, Map } from 'ol'; +import { Stroke, Style } from 'ol/style'; +import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; +import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import View from 'ol/View'; +import LineOverlay, { + LineOverlayProps, +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay'; +import { Coordinate } from 'ol/coordinate'; + +jest.mock('../style/getStroke'); +jest.mock('../style/getFill'); +jest.mock('../style/rgbToHex'); + +describe('LineOverlay', () => { + let props: LineOverlayProps; + + beforeEach(() => { + const dummyElement = document.createElement('div'); + const mapInstance = new Map({ + target: dummyElement, + view: new View({ + zoom: 5, + minZoom: 3, + maxZoom: 7, + }), + }); + props = { + lineOverlay: { + color: null, + height: 2, + id: '3', + modelId: 1, + overlayId: 0, + type: 'line', + value: 0.43, + width: 2, + x1: 1, + x2: 3, + y1: 3, + y2: 1, + }, + getOverlayColor: (): string => '#AABB11', + pointToProjection: ({ x, y }: { x: number; y: number }): Coordinate => [x, y], + mapInstance, + }; + + (getStroke as jest.Mock).mockReturnValue(new Stroke()); + (getFill as jest.Mock).mockReturnValue(new Style()); + (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); + }); + + it('should initialize line overlay feature', () => { + const lineOverlay = new LineOverlay(props); + + expect(lineOverlay.lineFeature).toBeInstanceOf(Feature); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.ts new file mode 100644 index 00000000..b6d8eecb --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.ts @@ -0,0 +1,75 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { LineString } from 'ol/geom'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import Style from 'ol/style/Style'; +import { MapInstance } from '@/types/map'; + +export type LineOverlayProps = { + lineOverlay: OverlayBioEntityRender; + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; +}; + +export default class LineOverlay { + lineOverlay: OverlayBioEntityRender; + + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + + pointToProjection: UsePointToProjectionResult; + + mapInstance: MapInstance; + + lineFeature: Feature; + + constructor({ lineOverlay, getOverlayColor, pointToProjection, mapInstance }: LineOverlayProps) { + this.lineOverlay = lineOverlay; + this.getOverlayColor = getOverlayColor; + this.pointToProjection = pointToProjection; + this.mapInstance = mapInstance; + this.lineFeature = this.drawOverlay(); + } + + drawOverlay(): Feature { + const points = [ + this.pointToProjection({ x: this.lineOverlay.x1, y: this.lineOverlay.y1 }), + this.pointToProjection({ x: this.lineOverlay.x2, y: this.lineOverlay.y2 }), + ]; + const color = this.getOverlayColor(this.lineOverlay); + const lineString = new LineString(points); + const lineStyle = getStyle({ + geometry: lineString, + borderColor: color, + lineWidth: 6, + zIndex: 99999, + }); + const lineFeature = new Feature<LineString>({ + geometry: lineString, + style: lineStyle, + lineWidth: 6, + }); + lineFeature.setStyle(this.getStyle.bind(this)); + return lineFeature; + } + + protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); + const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); + const style = feature.get('style'); + if (!minResolution || !style) { + return []; + } + + const scale = minResolution / resolution; + const lineWidth = feature.get('lineWidth') * scale; + + if (style instanceof Style && style.getStroke()) { + style.getStroke()?.setWidth(lineWidth); + } + return style; + } +} -- GitLab From c797efb1435c289ff50899de26f1a014de6fd6ce 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:29:35 +0100 Subject: [PATCH 06/29] feat(vector-map): implement marker overlay functionality --- .../reactionsLayer/useOlMapReactionsLayer.ts | 29 ++++++- .../shapes/overlay/MarkerOverlay.test.ts | 61 ++++++++++++++ .../utils/shapes/overlay/MarkerOverlay.ts | 83 +++++++++++++++++++ .../overlaysLayer/useOverlayFeatures.ts | 1 + 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.ts 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 325d3097..0dca84c6 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -38,6 +38,9 @@ import { useGetOverlayColor } from '@/components/Map/MapViewer/utils/config/over import { useAppSelector } from '@/redux/hooks/useAppSelector'; import getOverlays from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays'; import LineOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay'; +import { markersSufraceOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors'; +import { parseSurfaceMarkersToBioEntityRender } from '@/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender'; +import MarkerOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -53,6 +56,9 @@ export const useOlMapReactionsLayer = ({ const lineTypes = useSelector(lineTypesSelector); const arrowTypes = useSelector(arrowTypesSelector); const overlaysOrder = useSelector(getOverlayOrderSelector); + const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); + const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); + const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); const pointToProjection = usePointToProjection(); @@ -90,6 +96,22 @@ export const useOlMapReactionsLayer = ({ pointToProjection, ]); + const markerOverlaysFeatures = useMemo(() => { + return markersRender.map(marker => { + return new MarkerOverlay({ + markerOverlay: marker, + getOverlayColor: getOverlayBioEntityColorByAvailableProperties, + pointToProjection, + mapInstance, + }).markerFeature; + }); + }, [ + getOverlayBioEntityColorByAvailableProperties, + mapInstance, + markersRender, + pointToProjection, + ]); + const reactions = useMemo(() => { return modelReactions.map(reaction => { const reactionShapes = shapes && shapes[reaction.sboTerm]; @@ -227,7 +249,12 @@ export const useOlMapReactionsLayer = ({ const features = useMemo(() => { const reactionsFeatures = reactions.flat(); const elementsFeatures = elements.map(element => element.feature); - return [...reactionsFeatures, ...elementsFeatures, ...linesOverlaysFeatures]; + return [ + ...reactionsFeatures, + ...elementsFeatures, + ...linesOverlaysFeatures, + ...markerOverlaysFeatures, + ]; }, [elements, linesOverlaysFeatures, reactions]); const vectorSource = useMemo(() => { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.test.ts new file mode 100644 index 00000000..4fe3c4aa --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.test.ts @@ -0,0 +1,61 @@ +/* eslint-disable no-magic-numbers */ +import { Feature, Map } from 'ol'; +import { Stroke, Style } from 'ol/style'; +import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; +import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import View from 'ol/View'; +import { Coordinate } from 'ol/coordinate'; +import MarkerOverlay, { + MarkerOverlayProps, +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay'; + +jest.mock('../style/getStroke'); +jest.mock('../style/getFill'); +jest.mock('../style/rgbToHex'); + +describe('MarkerOverlay', () => { + let props: MarkerOverlayProps; + + beforeEach(() => { + const dummyElement = document.createElement('div'); + const mapInstance = new Map({ + target: dummyElement, + view: new View({ + zoom: 5, + minZoom: 3, + maxZoom: 7, + }), + }); + props = { + markerOverlay: { + color: null, + height: 2, + hexColor: '#A756BA90', + id: '3', + modelId: 1, + overlayId: 0, + type: 'rectangle', + value: 0.43, + width: 2, + x1: 1, + x2: 3, + y1: 3, + y2: 1, + }, + getOverlayColor: (): string => '#AABB11', + pointToProjection: ({ x, y }: { x: number; y: number }): Coordinate => [x, y], + mapInstance, + }; + + (getStroke as jest.Mock).mockReturnValue(new Stroke()); + (getFill as jest.Mock).mockReturnValue(new Style()); + (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); + }); + + it('should initialize line overlay feature', () => { + const markerOverlay = new MarkerOverlay(props); + + expect(markerOverlay.markerFeature).toBeInstanceOf(Feature); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.ts new file mode 100644 index 00000000..a0336335 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.ts @@ -0,0 +1,83 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import Style from 'ol/style/Style'; +import { MapInstance } from '@/types/map'; +import Polygon from 'ol/geom/Polygon'; + +export type MarkerOverlayProps = { + markerOverlay: OverlayBioEntityRender; + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; +}; + +export default class MarkerOverlay { + markerOverlay: OverlayBioEntityRender; + + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + + pointToProjection: UsePointToProjectionResult; + + mapInstance: MapInstance; + + markerFeature: Feature; + + constructor({ + markerOverlay, + getOverlayColor, + pointToProjection, + mapInstance, + }: MarkerOverlayProps) { + this.markerOverlay = markerOverlay; + this.getOverlayColor = getOverlayColor; + this.pointToProjection = pointToProjection; + this.mapInstance = mapInstance; + this.markerFeature = this.drawOverlay(); + } + + drawOverlay(): Feature { + const color = this.getOverlayColor(this.markerOverlay); + const polygon = new Polygon([ + [ + this.pointToProjection({ x: this.markerOverlay.x1, y: this.markerOverlay.y1 }), + this.pointToProjection({ x: this.markerOverlay.x2, y: this.markerOverlay.y1 }), + this.pointToProjection({ x: this.markerOverlay.x2, y: this.markerOverlay.y2 }), + this.pointToProjection({ x: this.markerOverlay.x1, y: this.markerOverlay.y2 }), + ], + ]); + const style = getStyle({ + geometry: polygon, + fillColor: this.markerOverlay.hexColor || color, + lineWidth: 1, + zIndex: 99999, + }); + const markerFeature = new Feature<Polygon>({ + geometry: polygon, + style, + lineWidth: 1, + }); + markerFeature.setStyle(this.getStyle.bind(this)); + return markerFeature; + } + + protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); + const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); + const style = feature.get('style'); + if (!minResolution || !style) { + return []; + } + + const scale = minResolution / resolution; + const lineWidth = feature.get('lineWidth') * scale; + + if (style instanceof Style && style.getStroke()) { + style.getStroke()?.setWidth(lineWidth); + } + return style; + } +} diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts index 107add31..020e6ae9 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts @@ -22,6 +22,7 @@ export const useOverlayFeatures = (): Feature<Polygon>[] | Feature<SimpleGeometr const overlaysOrder = useAppSelector(getOverlayOrderSelector); const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); + const bioEntities = useBioEntitiesWithSubmapsLinks(); const markersFeatures = useMemo( -- GitLab From 5287f115f2878724db9abe28d4057c5af77ed7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 10:11:19 +0100 Subject: [PATCH 07/29] refactor(vector-map): rendering modifications after element rendering --- .../utils/shapes/elements/MapElement.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index 94d5f96f..f485c3fb 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -156,18 +156,6 @@ export default class MapElement extends BaseMultiPolygon { } protected createPolygons(): void { - this.modifications.forEach(modification => { - if (modification.state === null) { - return; - } - - const shapes = this.bioShapes[modification.sboTerm]; - if (!shapes) { - return; - } - this.drawModification(modification, shapes); - }); - if (this.lineType) { this.lineDash = this.lineTypes[this.lineType] || []; } @@ -181,6 +169,18 @@ export default class MapElement extends BaseMultiPolygon { this.drawElementPolygon(homodimerShift, homodimerOffset); } this.drawOverlays(); + + this.modifications.forEach(modification => { + if (modification.state === null) { + return; + } + + const shapes = this.bioShapes[modification.sboTerm]; + if (!shapes) { + return; + } + this.drawModification(modification, shapes); + }); } drawModification(modification: Modification, shapes: Array<Shape>): void { -- GitLab From 708f80ef4c566071a65daba4f63c030a90c1aed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 11:55:03 +0100 Subject: [PATCH 08/29] feat(vector-map): hide modifications when text is hidden --- .../utils/shapes/elements/BaseMultiPolygon.ts | 47 +++++++++++-------- .../utils/shapes/elements/Compartment.ts | 7 ++- .../shapes/elements/CompartmentPathway.ts | 4 +- .../utils/shapes/elements/MapElement.ts | 17 +++++-- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 84dfd159..f039c015 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -77,10 +77,6 @@ export default abstract class BaseMultiPolygon { styles: Array<Style> = []; - polygonsTexts: Array<string> = []; - - lineWidths: Array<number> = []; - feature: Feature = new Feature(); pointToProjection: UsePointToProjectionResult; @@ -142,6 +138,9 @@ export default abstract class BaseMultiPolygon { pointToProjection: this.pointToProjection, }); const textPolygon = new Polygon([[textCoords, textCoords]]); + textPolygon.set('type', 'text'); + textPolygon.set('text', this.text); + textPolygon.set('fontSize', this.fontSize); const textStyle = getTextStyle({ text: this.text, fontSize: this.fontSize, @@ -151,7 +150,6 @@ export default abstract class BaseMultiPolygon { }); textStyle.setGeometry(textPolygon); this.styles.push(textStyle); - this.polygonsTexts.push(this.text); this.polygons.push(textPolygon); } } @@ -177,29 +175,38 @@ export default abstract class BaseMultiPolygon { } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const styles: Array<Style> = []; const getTextScale = feature.get('getTextScale'); let textScale = 1; if (getTextScale instanceof Function) { textScale = getTextScale(resolution); } - let textIndex = 0; - let strokeIndex = 0; + + let type: string; + let fontSize: number; + let lineWidth: number; + let text: string; + this.styles.forEach(style => { - if (style.getText()) { - if (this.fontSize * textScale > 4) { - style.getText()?.setScale(textScale); - style.getText()?.setText(this.polygonsTexts[textIndex]); - textIndex += 1; - } else { - style.getText()?.setText(undefined); - } + if (style.getGeometry() instanceof Polygon) { + type = (style.getGeometry() as Polygon).get('type'); + text = (style.getGeometry() as Polygon).get('text'); + fontSize = (style.getGeometry() as Polygon).get('fontSize'); + lineWidth = (style.getGeometry() as Polygon).get('lineWidth'); + } + if (['modification', 'text'].includes(type) && textScale * fontSize <= 4) { + return; + } + + if (type === 'text') { + style.getText()?.setScale(textScale); + style.getText()?.setText(text); } - if (style.getStroke()) { - const lineWidth = this.lineWidths[strokeIndex] * textScale; - style.getStroke()?.setWidth(lineWidth); - strokeIndex += 1; + if (style.getStroke() && lineWidth) { + style.getStroke()?.setWidth(lineWidth * textScale); } + styles.push(style); }); - return this.styles; + return styles; } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index b1aca3d8..9adc9ece 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -108,6 +108,7 @@ export default abstract class Compartment extends BaseMultiPolygon { protected createPolygons(): void { const framePolygon = new Polygon([this.outerCoords, this.innerCoords]); + framePolygon.set('type', 'compartment'); this.styles.push( new Style({ geometry: framePolygon, @@ -118,6 +119,8 @@ export default abstract class Compartment extends BaseMultiPolygon { this.polygons.push(framePolygon); const outerPolygon = new Polygon([this.outerCoords]); + outerPolygon.set('type', 'compartment'); + outerPolygon.set('lineWidth', this.outerWidth); this.styles.push( new Style({ geometry: outerPolygon, @@ -125,10 +128,11 @@ export default abstract class Compartment extends BaseMultiPolygon { zIndex: this.zIndex, }), ); - this.lineWidths.push(this.outerWidth); this.polygons.push(outerPolygon); const innerPolygon = new Polygon([this.innerCoords]); + innerPolygon.set('type', 'compartment'); + innerPolygon.set('lineWidth', this.innerWidth); this.styles.push( new Style({ geometry: innerPolygon, @@ -137,7 +141,6 @@ export default abstract class Compartment extends BaseMultiPolygon { zIndex: this.zIndex, }), ); - this.lineWidths.push(this.innerWidth); this.polygons.push(innerPolygon); } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index 8adec3ec..cc1e287c 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -98,7 +98,9 @@ export default class CompartmentPathway extends BaseMultiPolygon { this.pointToProjection({ x: this.x, y: this.y + this.height }), ], ]); - this.lineWidths.push(this.outerWidth); + compartmentPolygon.set('type', 'compartment'); + compartmentPolygon.set('lineWidth', this.outerWidth); + this.styles.push( getStyle({ geometry: compartmentPolygon, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index f485c3fb..c8934cc2 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -194,13 +194,15 @@ export default class MapElement extends BaseMultiPolygon { pointToProjection: this.pointToProjection, mirror: modification.direction && modification.direction === 'RIGHT', }); + modificationPolygon.set('type', 'modification'); + modificationPolygon.set('fontSize', modification.fontSize); + modificationPolygon.set('lineWidth', 1); const modificationStyle = new Style({ geometry: modificationPolygon, stroke: getStroke({ color: rgbToHex(modification.borderColor) }), fill: getFill({ color: rgbToHex(modification.fillColor) }), zIndex: modification.z, }); - this.lineWidths.push(1); this.polygons.push(modificationPolygon); this.styles.push(modificationStyle); }); @@ -222,6 +224,9 @@ export default class MapElement extends BaseMultiPolygon { const modificationTextPolygon = new Polygon([ [modificationTextCoords, modificationTextCoords], ]); + modificationTextPolygon.set('type', 'text'); + modificationTextPolygon.set('text', modificationText); + modificationTextPolygon.set('fontSize', modification.fontSize); const modificationTextStyle = getTextStyle({ text: modificationText, fontSize: modification.fontSize, @@ -231,7 +236,6 @@ export default class MapElement extends BaseMultiPolygon { }); modificationTextStyle.setGeometry(modificationTextPolygon); this.styles.push(modificationTextStyle); - this.polygonsTexts.push(modificationText); this.polygons.push(modificationTextPolygon); } } @@ -246,6 +250,8 @@ export default class MapElement extends BaseMultiPolygon { height: this.height - homodimerOffset + 10, pointToProjection: this.pointToProjection, }); + activityBorderPolygon.set('type', 'activityBorder'); + activityBorderPolygon.set('lineWidth', 1); const activityBorderStyle = getStyle({ geometry: activityBorderPolygon, fillColor: { rgb: 0, alpha: 0 }, @@ -253,7 +259,6 @@ export default class MapElement extends BaseMultiPolygon { zIndex: this.zIndex, }); this.polygons.push(activityBorderPolygon); - this.lineWidths.push(1); this.styles.push(activityBorderStyle); }); } @@ -268,6 +273,8 @@ export default class MapElement extends BaseMultiPolygon { height: this.height - homodimerOffset, pointToProjection: this.pointToProjection, }); + elementPolygon.set('type', 'element'); + elementPolygon.set('lineWidth', this.lineWidth); const elementStyle = getStyle({ geometry: elementPolygon, borderColor: this.borderColor, @@ -277,7 +284,6 @@ export default class MapElement extends BaseMultiPolygon { zIndex: this.zIndex, }); this.polygons.push(elementPolygon); - this.lineWidths.push(this.lineWidth); this.styles.push(elementStyle); }); } @@ -303,6 +309,8 @@ export default class MapElement extends BaseMultiPolygon { this.pointToProjection({ x: xMin, y: entity.y2 }), ], ]); + polygon.set('type', 'overlay'); + polygon.set('lineWidth', 1); const style = getStyle({ geometry: polygon, borderColor: color, @@ -310,7 +318,6 @@ export default class MapElement extends BaseMultiPolygon { zIndex: this.zIndex, }); this.polygons.push(polygon); - this.lineWidths.push(1); this.styles.push(style); }); } -- GitLab From 52b15aaf85238f2f6adb36cadeb99177d0462fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 12:47:02 +0100 Subject: [PATCH 09/29] feat(vector-map): hide operator dots when text is hidden and hide arrows when it is unreadable --- .../MapViewerVector.constants.ts | 7 +++++ .../utils/shapes/reaction/Reaction.ts | 31 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts index 9a036ac1..8e546623 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts @@ -12,6 +12,13 @@ export const BLACK_COLOR: Color = { rgb: -16777216, }; +export const REACTION_ELEMENT_TYPES = { + OPERATOR: 'operator', + SQUARE: 'square', + LINE: 'line', + ARROW: 'arrow', +}; + export const COMPARTMENT_SQUARE_POINTS: Array<ShapeRelAbs | ShapeRelAbsBezierPoint> = [ { type: 'REL_ABS_POINT', diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts index 9bd713ce..c4fc7499 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts @@ -9,7 +9,10 @@ import getArrowFeature from '@/components/Map/MapViewer/MapViewerVector/utils/sh import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import Polygon from 'ol/geom/Polygon'; import Style from 'ol/style/Style'; -import { WHITE_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import { + REACTION_ELEMENT_TYPES, + WHITE_COLOR, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import { FeatureLike } from 'ol/Feature'; import { MapInstance } from '@/types/map'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; @@ -157,6 +160,7 @@ export default class Reaction { pointToProjection: this.pointToProjection, }); if (startArrowFeature) { + startArrowFeature.set('elementType', REACTION_ELEMENT_TYPES.ARROW); startArrowFeature.set('lineWidth', line.width); startArrowFeature.setStyle(this.getStyle.bind(this)); arrowsFeatures.push(startArrowFeature); @@ -184,6 +188,7 @@ export default class Reaction { pointToProjection: this.pointToProjection, }); if (endArrowFeature) { + endArrowFeature.set('elementType', REACTION_ELEMENT_TYPES.ARROW); endArrowFeature.set('lineWidth', line.width); endArrowFeature.setStyle(this.getStyle.bind(this)); arrowsFeatures.push(endArrowFeature); @@ -206,6 +211,7 @@ export default class Reaction { lineWidth: line.width, id: this.id, type: FEATURE_TYPE.REACTION, + elementType: REACTION_ELEMENT_TYPES.LINE, }); lineFeature.setStyle(this.getStyle.bind(this)); @@ -254,6 +260,7 @@ export default class Reaction { lineWidth: this.line.width, id: this.id, type: FEATURE_TYPE.REACTION, + elementType: REACTION_ELEMENT_TYPES.SQUARE, }); squareFeature.setStyle(this.getStyle.bind(this)); return squareFeature; @@ -308,12 +315,15 @@ export default class Reaction { lineWidth: 1, id: this.id, type: FEATURE_TYPE.REACTION, + elementType: REACTION_ELEMENT_TYPES.OPERATOR, + fontSize: 10, }); circleFeature.setStyle(this.getStyle.bind(this)); return circleFeature; } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const styles: Array<Style> = []; const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); const style = feature.get('style'); @@ -323,16 +333,29 @@ export default class Reaction { const scale = minResolution / resolution; const lineWidth = feature.get('lineWidth') * scale; + const type = feature.get('elementType'); + const fontSize = feature.get('fontSize'); + + if (type === REACTION_ELEMENT_TYPES.OPERATOR && fontSize * scale <= 4) { + return []; + } + if (type === REACTION_ELEMENT_TYPES.ARROW && scale <= 0.08) { + return []; + } - if (style instanceof Style && style.getStroke()) { + if (style instanceof Style) { style.getStroke()?.setWidth(lineWidth); + style.getText()?.setScale(scale); + styles.push(style); } else if (Array.isArray(style)) { style.forEach(singleStyle => { - if (singleStyle instanceof Style && singleStyle.getStroke()) { + if (singleStyle instanceof Style) { singleStyle.getStroke()?.setWidth(lineWidth); + singleStyle.getText()?.setScale(scale); + styles.push(singleStyle); } }); } - return style; + return styles; } } -- GitLab From 10c49a948a2ea4663f1a06a621bc3ed7605840ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 12:53:38 +0100 Subject: [PATCH 10/29] feat(vector-map): add types const for map elements --- .../MapViewerVector/MapViewerVector.constants.ts | 9 +++++++++ .../utils/shapes/elements/BaseMultiPolygon.ts | 8 ++++++-- .../utils/shapes/elements/Compartment.ts | 7 ++++--- .../utils/shapes/elements/CompartmentPathway.ts | 3 ++- .../utils/shapes/elements/MapElement.ts | 11 ++++++----- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts index 8e546623..ac478d7b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts @@ -19,6 +19,15 @@ export const REACTION_ELEMENT_TYPES = { ARROW: 'arrow', }; +export const MAP_ELEMENT_TYPES = { + TEXT: 'text', + MODIFICATION: 'modification', + ACTIVITY_BORDER: 'activityBorder', + ENTITY: 'entity', + OVERLAY: 'overlay', + COMPARTMENT: 'compartment', +}; + export const COMPARTMENT_SQUARE_POINTS: Array<ShapeRelAbs | ShapeRelAbsBezierPoint> = [ { type: 'REL_ABS_POINT', diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index f039c015..cec6f17f 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -13,6 +13,7 @@ import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shape import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import { Color } from '@/types/models'; +import { MAP_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; export interface BaseMapElementProps { type: string; @@ -138,7 +139,7 @@ export default abstract class BaseMultiPolygon { pointToProjection: this.pointToProjection, }); const textPolygon = new Polygon([[textCoords, textCoords]]); - textPolygon.set('type', 'text'); + textPolygon.set('type', MAP_ELEMENT_TYPES.TEXT); textPolygon.set('text', this.text); textPolygon.set('fontSize', this.fontSize); const textStyle = getTextStyle({ @@ -194,7 +195,10 @@ export default abstract class BaseMultiPolygon { fontSize = (style.getGeometry() as Polygon).get('fontSize'); lineWidth = (style.getGeometry() as Polygon).get('lineWidth'); } - if (['modification', 'text'].includes(type) && textScale * fontSize <= 4) { + if ( + [MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && + textScale * fontSize <= 4 + ) { return; } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index 9adc9ece..9c50b1bc 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -13,6 +13,7 @@ import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shape import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; import { MapInstance } from '@/types/map'; import { Color } from '@/types/models'; +import { MAP_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; export interface CompartmentProps { id: number; @@ -108,7 +109,7 @@ export default abstract class Compartment extends BaseMultiPolygon { protected createPolygons(): void { const framePolygon = new Polygon([this.outerCoords, this.innerCoords]); - framePolygon.set('type', 'compartment'); + framePolygon.set('type', MAP_ELEMENT_TYPES.COMPARTMENT); this.styles.push( new Style({ geometry: framePolygon, @@ -119,7 +120,7 @@ export default abstract class Compartment extends BaseMultiPolygon { this.polygons.push(framePolygon); const outerPolygon = new Polygon([this.outerCoords]); - outerPolygon.set('type', 'compartment'); + outerPolygon.set('type', MAP_ELEMENT_TYPES.COMPARTMENT); outerPolygon.set('lineWidth', this.outerWidth); this.styles.push( new Style({ @@ -131,7 +132,7 @@ export default abstract class Compartment extends BaseMultiPolygon { this.polygons.push(outerPolygon); const innerPolygon = new Polygon([this.innerCoords]); - innerPolygon.set('type', 'compartment'); + innerPolygon.set('type', MAP_ELEMENT_TYPES.COMPARTMENT); innerPolygon.set('lineWidth', this.innerWidth); this.styles.push( new Style({ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index cc1e287c..bcc70aa1 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -7,6 +7,7 @@ import { } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; import { BLACK_COLOR, + MAP_ELEMENT_TYPES, WHITE_COLOR, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import Polygon from 'ol/geom/Polygon'; @@ -98,7 +99,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { this.pointToProjection({ x: this.x, y: this.y + this.height }), ], ]); - compartmentPolygon.set('type', 'compartment'); + compartmentPolygon.set('type', MAP_ELEMENT_TYPES.COMPARTMENT); compartmentPolygon.set('lineWidth', this.outerWidth); this.styles.push( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index c8934cc2..909c7c6b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -13,6 +13,7 @@ import { } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; import { BLACK_COLOR, + MAP_ELEMENT_TYPES, WHITE_COLOR, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon'; @@ -194,7 +195,7 @@ export default class MapElement extends BaseMultiPolygon { pointToProjection: this.pointToProjection, mirror: modification.direction && modification.direction === 'RIGHT', }); - modificationPolygon.set('type', 'modification'); + modificationPolygon.set('type', MAP_ELEMENT_TYPES.MODIFICATION); modificationPolygon.set('fontSize', modification.fontSize); modificationPolygon.set('lineWidth', 1); const modificationStyle = new Style({ @@ -224,7 +225,7 @@ export default class MapElement extends BaseMultiPolygon { const modificationTextPolygon = new Polygon([ [modificationTextCoords, modificationTextCoords], ]); - modificationTextPolygon.set('type', 'text'); + modificationTextPolygon.set('type', MAP_ELEMENT_TYPES.TEXT); modificationTextPolygon.set('text', modificationText); modificationTextPolygon.set('fontSize', modification.fontSize); const modificationTextStyle = getTextStyle({ @@ -250,7 +251,7 @@ export default class MapElement extends BaseMultiPolygon { height: this.height - homodimerOffset + 10, pointToProjection: this.pointToProjection, }); - activityBorderPolygon.set('type', 'activityBorder'); + activityBorderPolygon.set('type', MAP_ELEMENT_TYPES.ACTIVITY_BORDER); activityBorderPolygon.set('lineWidth', 1); const activityBorderStyle = getStyle({ geometry: activityBorderPolygon, @@ -273,7 +274,7 @@ export default class MapElement extends BaseMultiPolygon { height: this.height - homodimerOffset, pointToProjection: this.pointToProjection, }); - elementPolygon.set('type', 'element'); + elementPolygon.set('type', MAP_ELEMENT_TYPES.ENTITY); elementPolygon.set('lineWidth', this.lineWidth); const elementStyle = getStyle({ geometry: elementPolygon, @@ -309,7 +310,7 @@ export default class MapElement extends BaseMultiPolygon { this.pointToProjection({ x: xMin, y: entity.y2 }), ], ]); - polygon.set('type', 'overlay'); + polygon.set('type', MAP_ELEMENT_TYPES.OVERLAY); polygon.set('lineWidth', 1); const style = getStyle({ geometry: polygon, -- GitLab From ff64dbaa10c8466b6fae661f45ea7e4e7461ebed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 13:05:06 +0100 Subject: [PATCH 11/29] feat(vector-map): hide arrows on arrow layer when it is unreadable --- .../MapViewerVector/MapViewerVector.constants.ts | 8 ++++++++ .../MapViewerVector/utils/shapes/layer/Layer.ts | 13 ++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts index ac478d7b..dcebe773 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts @@ -28,6 +28,14 @@ export const MAP_ELEMENT_TYPES = { COMPARTMENT: 'compartment', }; +export const LAYER_ELEMENT_TYPES = { + TEXT: 'text', + OVAL: 'oval', + RECT: 'rect', + LINE: 'line', + ARROW: 'arrow', +}; + export const COMPARTMENT_SQUARE_POINTS: Array<ShapeRelAbs | ShapeRelAbsBezierPoint> = [ { type: 'REL_ABS_POINT', diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index 0554c9db..c9a8f317 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -19,6 +19,7 @@ import getArrowFeature from '@/components/Map/MapViewer/MapViewerVector/utils/sh import { FeatureLike } from 'ol/Feature'; import Style from 'ol/style/Style'; import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; +import { LAYER_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; export interface LayerProps { texts: Array<LayerText>; @@ -151,6 +152,7 @@ export default class Layer { geometry: polygon, style: polygonStyle, lineWidth: rect.lineWidth, + elementType: LAYER_ELEMENT_TYPES.RECT, }); rectFeature.setStyle(this.getStyle.bind(this)); return rectFeature; @@ -179,6 +181,7 @@ export default class Layer { geometry: polygon, style: polygonStyle, lineWidth: oval.lineWidth, + elementType: LAYER_ELEMENT_TYPES.OVAL, }); ovalFeature.setStyle(this.getStyle.bind(this)); return ovalFeature; @@ -227,6 +230,7 @@ export default class Layer { pointToProjection: this.pointToProjection, }); if (startArrowFeature) { + startArrowFeature.set('elementType', LAYER_ELEMENT_TYPES.ARROW); startArrowFeature.set('lineWidth', line.width); startArrowFeature.setStyle(this.getStyle.bind(this)); arrowsFeatures.push(startArrowFeature); @@ -255,6 +259,7 @@ export default class Layer { pointToProjection: this.pointToProjection, }); if (endArrowFeature) { + endArrowFeature.set('elementType', LAYER_ELEMENT_TYPES.ARROW); endArrowFeature.set('lineWidth', line.width); endArrowFeature.setStyle(this.getStyle.bind(this)); arrowsFeatures.push(endArrowFeature); @@ -275,6 +280,7 @@ export default class Layer { geometry: lineString, style: lineStyle, lineWidth: line.width, + elementType: LAYER_ELEMENT_TYPES.LINE, }); lineFeature.setStyle(this.getStyle.bind(this)); linesFeatures.push(lineFeature); @@ -292,8 +298,13 @@ export default class Layer { const scale = minResolution / resolution; const lineWidth = feature.get('lineWidth') * scale; + const type = feature.get('elementType'); - if (style instanceof Style && style.getStroke()) { + if (type === LAYER_ELEMENT_TYPES.ARROW && scale <= 0.08) { + return []; + } + + if (style instanceof Style) { style.getStroke()?.setWidth(lineWidth); } else if (Array.isArray(style)) { style.forEach(singleStyle => { -- GitLab From 0d0c96eeec7f3e0c6be36215561d7f4b1cf7cba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 14:00:48 +0100 Subject: [PATCH 12/29] feat(vector-map): hide element border when it is almost invisible --- .../MapViewerVector.constants.ts | 2 ++ .../reactionsLayer/useOlMapReactionsLayer.ts | 3 ++- .../utils/shapes/elements/BaseMultiPolygon.ts | 25 ++++++++++++++----- .../utils/shapes/elements/MapElement.test.ts | 1 + .../utils/shapes/elements/MapElement.ts | 3 +++ .../utils/shapes/elements/getShapePolygon.ts | 1 + .../utils/shapes/layer/Layer.ts | 17 ++++++++++--- 7 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts index dcebe773..0cd65f9a 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts @@ -2,6 +2,8 @@ import { Color, ShapeRelAbs, ShapeRelAbsBezierPoint } from '@/types/models'; export const VECTOR_MAP_LAYER_TYPE = 'vectorMapLayer'; +export const COMPLEX_SBO_TERM = 'SBO:0000253'; + export const WHITE_COLOR: Color = { alpha: 255, rgb: 16777215, 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 0dca84c6..b28af94d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -201,6 +201,7 @@ export const useOlMapReactionsLayer = ({ validElements.push( new MapElement({ id: element.id, + sboTerm: element.sboTerm, shapes: elementShapes, x: element.x, y: element.y, @@ -255,7 +256,7 @@ export const useOlMapReactionsLayer = ({ ...linesOverlaysFeatures, ...markerOverlaysFeatures, ]; - }, [elements, linesOverlaysFeatures, reactions]); + }, [elements, linesOverlaysFeatures, markerOverlaysFeatures, reactions]); const vectorSource = useMemo(() => { return new VectorSource({ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index cec6f17f..1513dd63 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -13,10 +13,14 @@ import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shape import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import { Color } from '@/types/models'; -import { MAP_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import { + COMPLEX_SBO_TERM, + MAP_ELEMENT_TYPES, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; export interface BaseMapElementProps { type: string; + sboTerm?: string; id: number; x: number; y: number; @@ -40,6 +44,8 @@ export interface BaseMapElementProps { export default abstract class BaseMultiPolygon { type: string; + sboTerm: string | undefined; + id: number; x: number; @@ -84,6 +90,7 @@ export default abstract class BaseMultiPolygon { constructor({ type, + sboTerm, id, x, y, @@ -104,6 +111,7 @@ export default abstract class BaseMultiPolygon { pointToProjection, }: BaseMapElementProps) { this.type = type; + this.sboTerm = sboTerm; this.id = id; this.x = x; this.y = y; @@ -202,14 +210,19 @@ export default abstract class BaseMultiPolygon { return; } + const clonedStyle = style.clone(); if (type === 'text') { - style.getText()?.setScale(textScale); - style.getText()?.setText(text); + clonedStyle.getText()?.setScale(textScale); + clonedStyle.getText()?.setText(text); } - if (style.getStroke() && lineWidth) { - style.getStroke()?.setWidth(lineWidth * textScale); + if (clonedStyle.getStroke() && lineWidth) { + if (lineWidth * textScale < 0.08 && this.sboTerm !== COMPLEX_SBO_TERM) { + clonedStyle.setStroke(null); + } else { + clonedStyle.getStroke()?.setWidth(lineWidth * textScale); + } } - styles.push(style); + styles.push(clonedStyle); }); return styles; } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts index 71c5c3c2..81665cc6 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts @@ -40,6 +40,7 @@ describe('MapElement', () => { }); props = { id: 1, + sboTerm: 'SBO:2313123', shapes: shapesFixture, x: 0, y: 0, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index 909c7c6b..575d8d77 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -31,6 +31,7 @@ import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/ export type MapElementProps = { id: number; + sboTerm: string; shapes: Array<Shape>; x: number; y: number; @@ -89,6 +90,7 @@ export default class MapElement extends BaseMultiPolygon { constructor({ id, + sboTerm, shapes, x, y, @@ -121,6 +123,7 @@ export default class MapElement extends BaseMultiPolygon { }: MapElementProps) { super({ type: FEATURE_TYPE.ALIAS, + sboTerm, id, x, y, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.ts index ef748554..7e2f7a59 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.ts @@ -48,6 +48,7 @@ export default function getShapePolygon({ return [mirroredX, coord[1]]; }); } + coords.push(coords[0]); return new Polygon([coords]); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index c9a8f317..6d17b6b1 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -289,6 +289,7 @@ export default class Layer { }; protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const styles: Array<Style> = []; const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); const style = feature.get('style'); @@ -303,16 +304,24 @@ export default class Layer { if (type === LAYER_ELEMENT_TYPES.ARROW && scale <= 0.08) { return []; } - + let clonedStyle: Style; if (style instanceof Style) { - style.getStroke()?.setWidth(lineWidth); + clonedStyle = style.clone(); + clonedStyle.getStroke()?.setWidth(lineWidth); + styles.push(clonedStyle); } else if (Array.isArray(style)) { style.forEach(singleStyle => { if (singleStyle instanceof Style && singleStyle.getStroke()) { - singleStyle.getStroke()?.setWidth(lineWidth); + clonedStyle = singleStyle.clone(); + if (lineWidth < 0.8) { + clonedStyle.getStroke()?.setWidth(lineWidth); + } else { + clonedStyle.setStroke(null); + } + styles.push(clonedStyle); } }); } - return style; + return styles; } } -- GitLab From 7ffb101013d79f4ee5eb61d6b229133fa64d522c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 14:24:00 +0100 Subject: [PATCH 13/29] fix(vector-map): correct incorrect bioEntity reducer assignment --- src/redux/bioEntity/bioEntity.reducers.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/redux/bioEntity/bioEntity.reducers.ts b/src/redux/bioEntity/bioEntity.reducers.ts index f5f4f94c..57c38c9d 100644 --- a/src/redux/bioEntity/bioEntity.reducers.ts +++ b/src/redux/bioEntity/bioEntity.reducers.ts @@ -141,18 +141,18 @@ export const setMultipleBioEntityContentsReducer = ( state: BioEntityContentsState, action: PayloadAction<Array<BioEntity>>, ): void => { - state.data = [ - { - data: action.payload.map(bioEntity => { - return { + state.data = action.payload.map(bioEntity => { + return { + data: [ + { bioEntity, perfect: true, - }; - }), + }, + ], + searchQueryElement: bioEntity.id.toString(), loading: 'succeeded', error: DEFAULT_ERROR, - searchQueryElement: 'asd', - }, - ]; + }; + }); state.loading = 'succeeded'; }; -- GitLab From 4a4bd9517626158c17ab3f2102efa3580bfa76af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 15:18:53 +0100 Subject: [PATCH 14/29] fix(vector-map): remove border disabling in Layer getStyle function --- .../MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index 6d17b6b1..dd8b81dc 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -311,13 +311,9 @@ export default class Layer { styles.push(clonedStyle); } else if (Array.isArray(style)) { style.forEach(singleStyle => { - if (singleStyle instanceof Style && singleStyle.getStroke()) { + if (singleStyle instanceof Style) { clonedStyle = singleStyle.clone(); - if (lineWidth < 0.8) { - clonedStyle.getStroke()?.setWidth(lineWidth); - } else { - clonedStyle.setStroke(null); - } + clonedStyle.getStroke()?.setWidth(lineWidth); styles.push(clonedStyle); } }); -- GitLab From 43460d9b74e03bde46473c53d8dff27f81d292e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 14 Nov 2024 15:29:39 +0100 Subject: [PATCH 15/29] feat(vector-map): scale element dashed border --- .../utils/shapes/elements/BaseMultiPolygon.ts | 23 +++++++++++-------- .../utils/shapes/layer/Layer.ts | 13 ++++------- .../utils/shapes/reaction/Reaction.ts | 11 ++++----- .../shapes/style/getScaledElementStyle.ts | 17 ++++++++++++++ 4 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 1513dd63..9157e7a5 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -166,7 +166,7 @@ export default abstract class BaseMultiPolygon { protected drawMultiPolygonFeature(mapInstance: MapInstance): void { this.feature = new Feature({ geometry: new MultiPolygon(this.polygons), - getTextScale: (resolution: number): number => { + getScale: (resolution: number): number => { const maxZoom = mapInstance?.getView().get('originalMaxZoom'); if (maxZoom) { const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); @@ -185,10 +185,10 @@ export default abstract class BaseMultiPolygon { protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { const styles: Array<Style> = []; - const getTextScale = feature.get('getTextScale'); - let textScale = 1; - if (getTextScale instanceof Function) { - textScale = getTextScale(resolution); + const getScale = feature.get('getScale'); + let scale = 1; + if (getScale instanceof Function) { + scale = getScale(resolution); } let type: string; @@ -205,21 +205,26 @@ export default abstract class BaseMultiPolygon { } if ( [MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && - textScale * fontSize <= 4 + scale * fontSize <= 4 ) { return; } const clonedStyle = style.clone(); if (type === 'text') { - clonedStyle.getText()?.setScale(textScale); + clonedStyle.getText()?.setScale(scale); clonedStyle.getText()?.setText(text); } if (clonedStyle.getStroke() && lineWidth) { - if (lineWidth * textScale < 0.08 && this.sboTerm !== COMPLEX_SBO_TERM) { + if (lineWidth * scale < 0.08 && this.sboTerm !== COMPLEX_SBO_TERM) { clonedStyle.setStroke(null); } else { - clonedStyle.getStroke()?.setWidth(lineWidth * textScale); + clonedStyle.getStroke()?.setWidth(lineWidth * scale); + const lineDash = clonedStyle.getStroke()?.getLineDash(); + if (lineDash) { + const newLineDash = lineDash.map(width => width * scale); + clonedStyle.getStroke()?.setLineDash(newLineDash); + } } } styles.push(clonedStyle); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index dd8b81dc..9a91384f 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -20,6 +20,7 @@ import { FeatureLike } from 'ol/Feature'; import Style from 'ol/style/Style'; import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { LAYER_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle'; export interface LayerProps { texts: Array<LayerText>; @@ -298,23 +299,19 @@ export default class Layer { } const scale = minResolution / resolution; - const lineWidth = feature.get('lineWidth') * scale; + const lineWidth = feature.get('lineWidth'); const type = feature.get('elementType'); if (type === LAYER_ELEMENT_TYPES.ARROW && scale <= 0.08) { return []; } - let clonedStyle: Style; + if (style instanceof Style) { - clonedStyle = style.clone(); - clonedStyle.getStroke()?.setWidth(lineWidth); - styles.push(clonedStyle); + styles.push(getScaledElementStyle(style, lineWidth, scale)); } else if (Array.isArray(style)) { style.forEach(singleStyle => { if (singleStyle instanceof Style) { - clonedStyle = singleStyle.clone(); - clonedStyle.getStroke()?.setWidth(lineWidth); - styles.push(clonedStyle); + styles.push(getScaledElementStyle(singleStyle, lineWidth, scale)); } }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts index c4fc7499..f8140764 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts @@ -19,6 +19,7 @@ import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shape import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { FEATURE_TYPE } from '@/constants/features'; +import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle'; export interface ReactionProps { id: number; @@ -332,7 +333,7 @@ export default class Reaction { } const scale = minResolution / resolution; - const lineWidth = feature.get('lineWidth') * scale; + const lineWidth = feature.get('lineWidth'); const type = feature.get('elementType'); const fontSize = feature.get('fontSize'); @@ -344,15 +345,11 @@ export default class Reaction { } if (style instanceof Style) { - style.getStroke()?.setWidth(lineWidth); - style.getText()?.setScale(scale); - styles.push(style); + styles.push(getScaledElementStyle(style, lineWidth, scale)); } else if (Array.isArray(style)) { style.forEach(singleStyle => { if (singleStyle instanceof Style) { - singleStyle.getStroke()?.setWidth(lineWidth); - singleStyle.getText()?.setScale(scale); - styles.push(singleStyle); + styles.push(getScaledElementStyle(singleStyle, lineWidth, scale)); } }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle.ts new file mode 100644 index 00000000..f4a4f77a --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle.ts @@ -0,0 +1,17 @@ +import Style from 'ol/style/Style'; + +export default function getScaledElementStyle( + style: Style, + lineWidth: number, + scale: number, +): Style { + const clonedStyle = style.clone(); + const lineDash = clonedStyle.getStroke()?.getLineDash(); + if (lineDash) { + const newLineDash = lineDash.map(width => width * scale); + clonedStyle.getStroke()?.setLineDash(newLineDash); + } + clonedStyle.getStroke()?.setWidth(lineWidth * scale); + clonedStyle.getText()?.setScale(scale); + return clonedStyle; +} -- GitLab From 36bcfb412b03dbbaef624d31358f6d29670237ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Fri, 15 Nov 2024 11:08:24 +0100 Subject: [PATCH 16/29] feat(vector-map): add spinner when diagram is loading --- src/components/Map/Map.component.tsx | 2 + .../MapLoader/MapLoader.component.test.tsx | 107 ++++++++++++++++++ .../Map/MapLoader/MapLoader.component.tsx | 50 ++++++++ .../Map/MapLoader/MapLoader.styles.css | 9 ++ .../reactionsLayer/useOlMapReactionsLayer.ts | 6 +- src/models/mocks/modelsMock.ts | 1 + src/redux/layers/layers.selectors.ts | 2 + .../modelElements/modelElements.selector.ts | 5 + .../newReactions/newReactions.selectors.ts | 5 + src/redux/shapes/shapes.selectors.ts | 15 +++ 10 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/components/Map/MapLoader/MapLoader.component.test.tsx create mode 100644 src/components/Map/MapLoader/MapLoader.component.tsx create mode 100644 src/components/Map/MapLoader/MapLoader.styles.css diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx index 67d4d216..67b7187c 100644 --- a/src/components/Map/Map.component.tsx +++ b/src/components/Map/Map.component.tsx @@ -2,6 +2,7 @@ import { Drawer } from '@/components/Map/Drawer'; import { Legend } from '@/components/Map/Legend'; import { MapViewer } from '@/components/Map/MapViewer'; +import { MapLoader } from '@/components/Map/MapLoader/MapLoader.component'; import { MapAdditionalActions } from './MapAdditionalActions'; import { MapAdditionalOptions } from './MapAdditionalOptions'; import { PluginsDrawer } from './PluginsDrawer'; @@ -18,6 +19,7 @@ export const Map = (): JSX.Element => { <PluginsDrawer /> <Legend /> <MapAdditionalActions /> + <MapLoader /> </div> ); }; diff --git a/src/components/Map/MapLoader/MapLoader.component.test.tsx b/src/components/Map/MapLoader/MapLoader.component.test.tsx new file mode 100644 index 00000000..eb35db6b --- /dev/null +++ b/src/components/Map/MapLoader/MapLoader.component.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { + getReduxWrapperWithStore, + InitialStoreState, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { StoreType } from '@/redux/store'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { newReactionsLoadingSelector } from '@/redux/newReactions/newReactions.selectors'; +import { modelElementsLoadingSelector } from '@/redux/modelElements/modelElements.selector'; +import { isDrawerOpenSelector } from '@/redux/drawer/drawer.selectors'; +import { vectorRenderingSelector } from '@/redux/models/models.selectors'; +import { + arrowTypesLoadingSelector, + bioShapesLoadingSelector, + lineTypesLoadingSelector, +} from '@/redux/shapes/shapes.selectors'; +import { layersLoadingSelector } from '@/redux/layers/layers.selectors'; +import { MapLoader } from './MapLoader.component'; + +jest.mock('../../../redux/hooks/useAppSelector', () => ({ + useAppSelector: jest.fn(), +})); +type SelectorFunction = (state: never) => string | boolean; + +const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStore); + + return ( + render( + <Wrapper> + <MapLoader /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('MapLoader', () => { + const mockUseAppSelector = useAppSelector as jest.Mock; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should not render the LoadingIndicator when no data is loading', () => { + mockUseAppSelector.mockImplementation(selector => { + const selectorMap = new Map<SelectorFunction, string | boolean>([ + [newReactionsLoadingSelector, 'succeeded'], + [modelElementsLoadingSelector, 'succeeded'], + [vectorRenderingSelector, true], + [bioShapesLoadingSelector, 'succeeded'], + [lineTypesLoadingSelector, 'succeeded'], + [arrowTypesLoadingSelector, 'succeeded'], + [layersLoadingSelector, 'succeeded'], + [isDrawerOpenSelector, false], + ]); + + return selectorMap.get(selector) ?? false; + }); + renderComponent(); + + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + it('should render the LoadingIndicator when vectorRendering is true and data is loading', () => { + mockUseAppSelector.mockImplementation(selector => { + const selectorMap = new Map<SelectorFunction, string | boolean>([ + [newReactionsLoadingSelector, 'pending'], + [modelElementsLoadingSelector, 'succeeded'], + [vectorRenderingSelector, true], + [bioShapesLoadingSelector, 'succeeded'], + [lineTypesLoadingSelector, 'succeeded'], + [arrowTypesLoadingSelector, 'succeeded'], + [layersLoadingSelector, 'succeeded'], + [isDrawerOpenSelector, false], + ]); + + return selectorMap.get(selector) ?? false; + }); + renderComponent(); + + expect(screen.queryByTestId('loading-indicator')).toBeInTheDocument(); + }); + + it('should not render the LoadingIndicator when vectorRendering is false even when data is loading', () => { + mockUseAppSelector.mockImplementation(selector => { + const selectorMap = new Map<SelectorFunction, string | boolean>([ + [newReactionsLoadingSelector, 'pending'], + [modelElementsLoadingSelector, 'succeeded'], + [vectorRenderingSelector, false], + [bioShapesLoadingSelector, 'succeeded'], + [lineTypesLoadingSelector, 'succeeded'], + [arrowTypesLoadingSelector, 'succeeded'], + [layersLoadingSelector, 'succeeded'], + [isDrawerOpenSelector, false], + ]); + + return selectorMap.get(selector) ?? false; + }); + renderComponent(); + + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Map/MapLoader/MapLoader.component.tsx b/src/components/Map/MapLoader/MapLoader.component.tsx new file mode 100644 index 00000000..fc457e0b --- /dev/null +++ b/src/components/Map/MapLoader/MapLoader.component.tsx @@ -0,0 +1,50 @@ +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { useMemo } from 'react'; +import { newReactionsLoadingSelector } from '@/redux/newReactions/newReactions.selectors'; +import { modelElementsLoadingSelector } from '@/redux/modelElements/modelElements.selector'; +import { vectorRenderingSelector } from '@/redux/models/models.selectors'; +import { + arrowTypesLoadingSelector, + bioShapesLoadingSelector, + lineTypesLoadingSelector, +} from '@/redux/shapes/shapes.selectors'; +import { layersLoadingSelector } from '@/redux/layers/layers.selectors'; +import './MapLoader.styles.css'; +import { isDrawerOpenSelector } from '@/redux/drawer/drawer.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; + +export const MapLoader = (): JSX.Element => { + const reactionsFetching = useAppSelector(newReactionsLoadingSelector); + const modelElementsFetching = useAppSelector(modelElementsLoadingSelector); + const vectorRendering = useAppSelector(vectorRenderingSelector); + const bioShapesFetching = useAppSelector(bioShapesLoadingSelector); + const lineTypesFetching = useAppSelector(lineTypesLoadingSelector); + const arrowTypesFetching = useAppSelector(arrowTypesLoadingSelector); + const layersLoading = useAppSelector(layersLoadingSelector); + + const isDrawerOpen = useAppSelector(isDrawerOpenSelector); + + const showLoader = useMemo(() => { + return [ + reactionsFetching, + modelElementsFetching, + bioShapesFetching, + lineTypesFetching, + arrowTypesFetching, + layersLoading, + ].includes('pending'); + }, [ + reactionsFetching, + modelElementsFetching, + bioShapesFetching, + lineTypesFetching, + arrowTypesFetching, + layersLoading, + ]); + + return ( + <div className={`map-loader transition-all duration-500 ${isDrawerOpen ? 'move-right' : ''}`}> + {vectorRendering && showLoader && <LoadingIndicator width={48} height={48} />} + </div> + ); +}; diff --git a/src/components/Map/MapLoader/MapLoader.styles.css b/src/components/Map/MapLoader/MapLoader.styles.css new file mode 100644 index 00000000..750e4cb6 --- /dev/null +++ b/src/components/Map/MapLoader/MapLoader.styles.css @@ -0,0 +1,9 @@ +.map-loader { + position: absolute; + left: 120px; + top: 128px; +} + +.map-loader.move-right { + left: 550px; +} 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 b28af94d..d8cad731 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -64,8 +64,10 @@ export const useOlMapReactionsLayer = ({ const pointToProjection = usePointToProjection(); useEffect(() => { - dispatch(getModelElements(currentModelId)); - dispatch(getNewReactions(currentModelId)); + if (currentModelId) { + dispatch(getModelElements(currentModelId)); + dispatch(getNewReactions(currentModelId)); + } }, [currentModelId, dispatch]); const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); diff --git a/src/models/mocks/modelsMock.ts b/src/models/mocks/modelsMock.ts index 1684bf93..53dac6b9 100644 --- a/src/models/mocks/modelsMock.ts +++ b/src/models/mocks/modelsMock.ts @@ -474,6 +474,7 @@ export const CORE_PD_MODEL_MOCK: MapModel = { modificationDates: [], minZoom: 2, maxZoom: 9, + vectorRendering: true, }; export const MODEL_WITH_DESCRIPTION: MapModel = { diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 987ec4ac..d29698e0 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -6,6 +6,8 @@ export const layersSelector = createSelector( state => state.layers?.data?.layers || [], ); +export const layersLoadingSelector = createSelector(rootSelector, state => state.layers.loading); + export const layersVisibilitySelector = createSelector( rootSelector, state => state.layers?.data?.layersVisibility || {}, diff --git a/src/redux/modelElements/modelElements.selector.ts b/src/redux/modelElements/modelElements.selector.ts index 54b4a75b..3a9d8b54 100644 --- a/src/redux/modelElements/modelElements.selector.ts +++ b/src/redux/modelElements/modelElements.selector.ts @@ -5,3 +5,8 @@ export const modelElementsSelector = createSelector( rootSelector, state => state.modelElements.data, ); + +export const modelElementsLoadingSelector = createSelector( + rootSelector, + state => state.modelElements.loading, +); diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index 4dc2babe..146bbc85 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -3,6 +3,11 @@ import { rootSelector } from '../root/root.selectors'; export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions); +export const newReactionsLoadingSelector = createSelector( + newReactionsSelector, + state => state.loading, +); + export const newReactionsDataSelector = createSelector( newReactionsSelector, reactions => reactions.data || [], diff --git a/src/redux/shapes/shapes.selectors.ts b/src/redux/shapes/shapes.selectors.ts index cdd4fa16..d23c9a97 100644 --- a/src/redux/shapes/shapes.selectors.ts +++ b/src/redux/shapes/shapes.selectors.ts @@ -8,12 +8,27 @@ export const bioShapesSelector = createSelector( shapes => shapes.bioShapesState.data, ); +export const bioShapesLoadingSelector = createSelector( + shapesSelector, + shapes => shapes.bioShapesState.loading, +); + export const lineTypesSelector = createSelector( shapesSelector, shapes => shapes.lineTypesState.data || {}, ); +export const lineTypesLoadingSelector = createSelector( + shapesSelector, + shapes => shapes.lineTypesState.loading, +); + export const arrowTypesSelector = createSelector( shapesSelector, shapes => shapes.arrowTypesState.data || {}, ); + +export const arrowTypesLoadingSelector = createSelector( + shapesSelector, + shapes => shapes.arrowTypesState.loading, +); -- GitLab From 1b2a9230d25639cb64df3bdb56c54eb82ebb3da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 18 Nov 2024 11:37:48 +0100 Subject: [PATCH 17/29] feat(vector-map): Optimize layers to avoid out of memory error --- .../reactionsLayer/processModelElements.ts | 120 ++++++++++++++++ .../reactionsLayer/useOlMapReactionsLayer.ts | 132 ++++-------------- .../utils/config/useOlMapVectorLayers.ts | 7 +- .../config/pinsLayer/useOlMapPinsLayer.ts | 17 ++- .../reactionsLayer/useOlMapReactionsLayer.ts | 22 +-- .../utils/config/useOlMapCommonLayers.ts | 5 +- src/utils/useDebouncedValue.test.ts | 50 +++++++ src/utils/useDebouncedValue.ts | 15 ++ 8 files changed, 237 insertions(+), 131 deletions(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts create mode 100644 src/utils/useDebouncedValue.test.ts create mode 100644 src/utils/useDebouncedValue.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts new file mode 100644 index 00000000..8aba562e --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -0,0 +1,120 @@ +import { ModelElement, ModelElements } from '@/types/models'; +import MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement'; +import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; +import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; +import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; +import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; +import { + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; +import { MapInstance } from '@/types/map'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { BioShapesDict, LineTypeDict } from '@/redux/shapes/shapes.types'; +import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; + +export default function processModelElements( + modelElements: ModelElements, + shapes: BioShapesDict, + lineTypes: LineTypeDict, + groupedElementsOverlays: Record<string, Array<OverlayBioEntityRender>>, + overlaysOrder: Array<OverlayOrder>, + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties, + mapInstance: MapInstance, + pointToProjection: UsePointToProjectionResult, +): Array<MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph> { + const validElements: Array< + MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph + > = []; + modelElements.content.forEach((element: ModelElement) => { + if (element.glyph) { + const glyph = new Glyph({ + id: element.glyph.id, + x: element.x, + y: element.y, + width: element.width, + height: element.height, + zIndex: element.z, + pointToProjection, + mapInstance, + }); + validElements.push(glyph); + return; + } + + if (element.sboTerm === 'SBO:0000290') { + const compartmentProps = { + id: element.id, + x: element.x, + y: element.y, + nameX: element.nameX, + nameY: element.nameY, + nameHeight: element.nameHeight, + nameWidth: element.nameWidth, + width: element.width, + height: element.height, + zIndex: element.z, + innerWidth: element.innerWidth, + outerWidth: element.outerWidth, + thickness: element.thickness, + fontColor: element.fontColor, + fillColor: element.fillColor, + borderColor: element.borderColor, + nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, + nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, + text: element.name, + fontSize: element.fontSize, + pointToProjection, + mapInstance, + }; + if (element.shape === 'OVAL_COMPARTMENT') { + validElements.push(new CompartmentCircle(compartmentProps)); + } else if (element.shape === 'SQUARE_COMPARTMENT') { + validElements.push(new CompartmentSquare(compartmentProps)); + } else if (element.shape === 'PATHWAY') { + validElements.push(new CompartmentPathway(compartmentProps)); + } + return; + } + const elementShapes = shapes[element.sboTerm]; + if (elementShapes) { + validElements.push( + new MapElement({ + id: element.id, + shapes: elementShapes, + x: element.x, + y: element.y, + nameX: element.nameX, + nameY: element.nameY, + nameHeight: element.nameHeight, + nameWidth: element.nameWidth, + width: element.width, + height: element.height, + zIndex: element.z, + lineWidth: element.lineWidth, + lineType: element.borderLineType, + fontColor: element.fontColor, + fillColor: element.fillColor, + borderColor: element.borderColor, + nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, + nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, + homodimer: element.homodimer, + activity: element.activity, + text: element.name, + fontSize: element.fontSize, + pointToProjection, + mapInstance, + modifications: element.modificationResidues, + lineTypes, + bioShapes: shapes, + overlays: groupedElementsOverlays[element.id], + overlaysOrder, + getOverlayColor, + }), + ); + } + }); + return validElements; +} 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 c879e113..87f31fcf 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -12,17 +12,12 @@ import { lineTypesSelector, } from '@/redux/shapes/shapes.selectors'; import { MapInstance } from '@/types/map'; -import { - HorizontalAlign, - VerticalAlign, -} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; import { modelElementsSelector } from '@/redux/modelElements/modelElements.selector'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; -import { ModelElement } from '@/types/models'; import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction'; @@ -41,6 +36,8 @@ import LineOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes import { markersSufraceOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors'; import { parseSurfaceMarkersToBioEntityRender } from '@/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender'; import MarkerOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay'; +import processModelElements from '@/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements'; +import useDebouncedValue from '@/utils/useDebouncedValue'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -58,7 +55,8 @@ export const useOlMapReactionsLayer = ({ const overlaysOrder = useSelector(getOverlayOrderSelector); const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); - + const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); + const debouncedBioEntities = useDebouncedValue(bioEntities, 2000); const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); const pointToProjection = usePointToProjection(); @@ -68,13 +66,13 @@ export const useOlMapReactionsLayer = ({ dispatch(getNewReactions(currentModelId)); }, [currentModelId, dispatch]); - const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); - const groupedElementsOverlays = useMemo(() => { - const elementsBioEntitesOverlay = bioEntities.filter(bioEntity => bioEntity.type !== 'line'); + const elementsBioEntitesOverlay = debouncedBioEntities.filter( + bioEntity => bioEntity.type !== 'line', + ); const grouped = groupBy(elementsBioEntitesOverlay, bioEntity => bioEntity.id.toString()); return getOverlays(grouped, getOverlayBioEntityColorByAvailableProperties); - }, [bioEntities, getOverlayBioEntityColorByAvailableProperties]); + }, [debouncedBioEntities, getOverlayBioEntityColorByAvailableProperties]); const linesOverlays = useMemo(() => { return bioEntities.filter(bioEntity => bioEntity.type === 'line'); @@ -142,99 +140,16 @@ export const useOlMapReactionsLayer = ({ if (!modelElements || !shapes) { return []; } - - const validElements: Array< - MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph - > = []; - modelElements.content.forEach((element: ModelElement) => { - if (element.glyph) { - const glyph = new Glyph({ - id: element.glyph.id, - x: element.x, - y: element.y, - width: element.width, - height: element.height, - zIndex: element.z, - pointToProjection, - mapInstance, - }); - validElements.push(glyph); - return; - } - - if (element.sboTerm === 'SBO:0000290') { - const compartmentProps = { - id: element.id, - x: element.x, - y: element.y, - nameX: element.nameX, - nameY: element.nameY, - nameHeight: element.nameHeight, - nameWidth: element.nameWidth, - width: element.width, - height: element.height, - zIndex: element.z, - innerWidth: element.innerWidth, - outerWidth: element.outerWidth, - thickness: element.thickness, - fontColor: element.fontColor, - fillColor: element.fillColor, - borderColor: element.borderColor, - nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, - nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, - text: element.name, - fontSize: element.fontSize, - pointToProjection, - mapInstance, - }; - if (element.shape === 'OVAL_COMPARTMENT') { - validElements.push(new CompartmentCircle(compartmentProps)); - } else if (element.shape === 'SQUARE_COMPARTMENT') { - validElements.push(new CompartmentSquare(compartmentProps)); - } else if (element.shape === 'PATHWAY') { - validElements.push(new CompartmentPathway(compartmentProps)); - } - return; - } - const elementShapes = shapes[element.sboTerm]; - if (elementShapes) { - validElements.push( - new MapElement({ - id: element.id, - shapes: elementShapes, - x: element.x, - y: element.y, - nameX: element.nameX, - nameY: element.nameY, - nameHeight: element.nameHeight, - nameWidth: element.nameWidth, - width: element.width, - height: element.height, - zIndex: element.z, - lineWidth: element.lineWidth, - lineType: element.borderLineType, - fontColor: element.fontColor, - fillColor: element.fillColor, - borderColor: element.borderColor, - nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, - nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, - homodimer: element.homodimer, - activity: element.activity, - text: element.name, - fontSize: element.fontSize, - pointToProjection, - mapInstance, - modifications: element.modificationResidues, - lineTypes, - bioShapes: shapes, - overlays: groupedElementsOverlays[element.id], - overlaysOrder, - getOverlayColor: getOverlayBioEntityColorByAvailableProperties, - }), - ); - } - }); - return validElements; + return processModelElements( + modelElements, + shapes, + lineTypes, + groupedElementsOverlays, + overlaysOrder, + getOverlayBioEntityColorByAvailableProperties, + mapInstance, + pointToProjection, + ); }, [ modelElements, shapes, @@ -257,11 +172,12 @@ export const useOlMapReactionsLayer = ({ ]; }, [elements, linesOverlaysFeatures, markerOverlaysFeatures, reactions]); - const vectorSource = useMemo(() => { - return new VectorSource({ - features, - }); - }, [features]); + const vectorSource = useMemo(() => new VectorSource(), []); + + useEffect(() => { + vectorSource.clear(); + vectorSource.addFeatures(features); + }, [features, vectorSource]); return useMemo(() => { const vectorLayer = new VectorLayer({ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts index 7ebb6e70..cebab4e0 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts @@ -2,8 +2,9 @@ import { MapInstance } from '@/types/map'; import { useOlMapWhiteCardLayer } from '@/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapWhiteCardLayer'; import { useOlMapAdditionalLayers } from '@/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers'; -import { MapConfig } from '../../MapViewerVector.types'; +import { useMemo } from 'react'; import { useOlMapReactionsLayer } from './reactionsLayer/useOlMapReactionsLayer'; +import { MapConfig } from '../../MapViewerVector.types'; interface UseOlMapLayersInput { mapInstance: MapInstance; @@ -14,5 +15,7 @@ export const useOlMapVectorLayers = ({ mapInstance }: UseOlMapLayersInput): MapC const whiteCardLayer = useOlMapWhiteCardLayer(); const additionalLayers = useOlMapAdditionalLayers(mapInstance); - return [whiteCardLayer, reactionsLayer, ...additionalLayers]; + return useMemo(() => { + return [whiteCardLayer, reactionsLayer, ...additionalLayers]; + }, [whiteCardLayer, reactionsLayer, additionalLayers]); }; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index 75d8c838..641b537d 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -11,7 +11,7 @@ import Feature from 'ol/Feature'; import { Geometry } from 'ol/geom'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getBioEntitiesFeatures } from './getBioEntitiesFeatures'; import { getMarkersFeatures } from './getMarkersFeatures'; @@ -67,19 +67,18 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> ], ); - const vectorSource = useMemo(() => { - return new VectorSource({ - features: [...elementsFeatures], - }); - }, [elementsFeatures]); + const vectorSource = useMemo(() => new VectorSource(), []); - const pinsLayer = useMemo( + useEffect(() => { + vectorSource.clear(); + vectorSource.addFeatures(elementsFeatures); + }, [elementsFeatures, vectorSource]); + + return useMemo( () => new VectorLayer({ source: vectorSource, }), [vectorSource], ); - - return pinsLayer; }; diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index c564b662..a6350f63 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -6,15 +6,15 @@ import { MarkerLine, Reaction } from '@/types/models'; import { LinePoint } from '@/types/reactions'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; -import { SimpleGeometry } from 'ol/geom'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Style from 'ol/style/Style'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { createOverlayLineFeature } from '@/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature'; +import { Geometry } from 'ol/geom'; import { getLineFeature } from './getLineFeature'; const getLinePoints = ({ start, end }: Pick<MarkerLine, 'start' | 'end'>): LinePoint => [ @@ -25,7 +25,7 @@ const getLinePoints = ({ start, end }: Pick<MarkerLine, 'start' | 'end'>): LineP const getReactionsLines = (reactions: Reaction[]): LinePoint[] => reactions.map(({ lines }) => lines.map(getLinePoints)).flat(); -export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<SimpleGeometry>>> => { +export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { const pointToProjection = usePointToProjection(); const reactions = useSelector(allReactionsSelectorOfCurrentMap); const markers = useSelector(markersLinesCurrentMapDataSelector); @@ -47,13 +47,15 @@ export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Simpl [markers, pointToProjection], ); - const vectorSource = useMemo(() => { - return new VectorSource({ - features: [...reactionsLinesFeatures, ...markerLinesFeatures], - }); - }, [reactionsLinesFeatures, markerLinesFeatures]); + const vectorSource = useMemo(() => new VectorSource(), []); - const reactionsLayer = useMemo( + useEffect(() => { + vectorSource.clear(); + vectorSource.addFeatures(reactionsLinesFeatures); + vectorSource.addFeatures(markerLinesFeatures); + }, [reactionsLinesFeatures, markerLinesFeatures, vectorSource]); + + return useMemo( () => new VectorLayer({ source: vectorSource, @@ -64,6 +66,4 @@ export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Simpl }), [vectorSource], ); - - return reactionsLayer; }; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts index bfec74c6..ad70201b 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts @@ -2,6 +2,7 @@ import { useOlMapPinsLayer } from '@/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer'; import { useOlMapReactionsLayer } from '@/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer'; import { useOlMapCommentsLayer } from '@/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer'; +import { useMemo } from 'react'; import { MapConfig } from '../../MapViewer.types'; export const useOlMapCommonLayers = (): MapConfig['layers'] => { @@ -9,5 +10,7 @@ export const useOlMapCommonLayers = (): MapConfig['layers'] => { const reactionsLayer = useOlMapReactionsLayer(); const commentsLayer = useOlMapCommentsLayer(); - return [pinsLayer, reactionsLayer, commentsLayer]; + return useMemo(() => { + return [pinsLayer, reactionsLayer, commentsLayer]; + }, [pinsLayer, reactionsLayer, commentsLayer]); }; diff --git a/src/utils/useDebouncedValue.test.ts b/src/utils/useDebouncedValue.test.ts new file mode 100644 index 00000000..d0691ad2 --- /dev/null +++ b/src/utils/useDebouncedValue.test.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-magic-numbers */ +import { act, renderHook } from '@testing-library/react'; +import useDebouncedValue from './useDebouncedValue'; + +jest.useFakeTimers(); + +describe('useDebouncedValue', () => { + it('should return the initial value immediately', () => { + const { result } = renderHook(() => useDebouncedValue('initial', 500)); + expect(result.current).toBe('initial'); + }); + + it('should update the value after the specified delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebouncedValue(value, delay), { + initialProps: { value: 'initial', delay: 500 }, + }); + + rerender({ value: 'updated', delay: 500 }); + + expect(result.current).toBe('initial'); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe('updated'); + }); + + it('should clear the timeout if value changes quickly', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebouncedValue(value, delay), { + initialProps: { value: 'initial', delay: 500 }, + }); + + rerender({ value: 'intermediate', delay: 500 }); + + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(result.current).toBe('initial'); + + rerender({ value: 'final', delay: 500 }); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe('final'); + }); +}); diff --git a/src/utils/useDebouncedValue.ts b/src/utils/useDebouncedValue.ts new file mode 100644 index 00000000..5d52da6b --- /dev/null +++ b/src/utils/useDebouncedValue.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; + +export default function useDebouncedValue<T>(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} -- GitLab From 5b3ba912e7f657ea91dc318ef9c04614d364bc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 18 Nov 2024 11:56:08 +0100 Subject: [PATCH 18/29] feat(vector-map): assign testStyle and strokeStyle to const in styleFunction --- .../utils/shapes/elements/BaseMultiPolygon.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 1513dd63..b53e6ee6 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -197,11 +197,12 @@ export default abstract class BaseMultiPolygon { let text: string; this.styles.forEach(style => { - if (style.getGeometry() instanceof Polygon) { - type = (style.getGeometry() as Polygon).get('type'); - text = (style.getGeometry() as Polygon).get('text'); - fontSize = (style.getGeometry() as Polygon).get('fontSize'); - lineWidth = (style.getGeometry() as Polygon).get('lineWidth'); + const styleGeometry = style.getGeometry(); + if (styleGeometry instanceof Polygon) { + type = styleGeometry.get('type'); + text = styleGeometry.get('text'); + fontSize = styleGeometry.get('fontSize'); + lineWidth = styleGeometry.get('lineWidth'); } if ( [MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && @@ -211,15 +212,17 @@ export default abstract class BaseMultiPolygon { } const clonedStyle = style.clone(); - if (type === 'text') { - clonedStyle.getText()?.setScale(textScale); - clonedStyle.getText()?.setText(text); + const textStyle = clonedStyle.getText(); + const strokeStyle = clonedStyle.getStroke(); + if (type === 'text' && textStyle) { + textStyle.setScale(textScale); + textStyle.setText(text); } - if (clonedStyle.getStroke() && lineWidth) { + if (strokeStyle && lineWidth) { if (lineWidth * textScale < 0.08 && this.sboTerm !== COMPLEX_SBO_TERM) { clonedStyle.setStroke(null); } else { - clonedStyle.getStroke()?.setWidth(lineWidth * textScale); + strokeStyle.setWidth(lineWidth * textScale); } } styles.push(clonedStyle); -- GitLab From 032c9eaafa04070cbcec658ab4402c2825671b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 21 Nov 2024 15:05:00 +0100 Subject: [PATCH 19/29] feat(vector-map): implement semantic view --- .../reactionsLayer/processModelElements.ts | 8 ++ .../reactionsLayer/useOlMapReactionsLayer.ts | 13 ++- .../utils/shapes/elements/BaseMultiPolygon.ts | 93 ++++++++++++++++++- .../utils/shapes/elements/Compartment.ts | 22 +++++ .../shapes/elements/CompartmentCircle.test.ts | 6 +- .../shapes/elements/CompartmentCircle.ts | 10 ++ .../elements/CompartmentPathway.test.ts | 6 +- .../shapes/elements/CompartmentPathway.ts | 20 +++- .../shapes/elements/CompartmentSquare.test.ts | 6 +- .../shapes/elements/CompartmentSquare.ts | 10 ++ .../utils/shapes/elements/MapElement.test.ts | 4 + .../utils/shapes/elements/MapElement.ts | 10 ++ .../utils/shapes/reaction/Reaction.test.ts | 2 + .../utils/shapes/reaction/Reaction.ts | 23 +++++ .../utils/shapes/text/getTextStyle.ts | 4 +- 15 files changed, 225 insertions(+), 12 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index 066712d0..42914e0d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -14,6 +14,7 @@ import { BioShapesDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; import { OverlayBioEntityRender } from '@/types/OLrendering'; import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import VectorSource from 'ol/source/Vector'; export default function processModelElements( modelElements: ModelElements, @@ -22,6 +23,7 @@ export default function processModelElements( groupedElementsOverlays: Record<string, Array<OverlayBioEntityRender>>, overlaysOrder: Array<OverlayOrder>, getOverlayColor: GetOverlayBioEntityColorByAvailableProperties, + vectorSource: VectorSource, mapInstance: MapInstance, pointToProjection: UsePointToProjectionResult, ): Array<MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph> { @@ -47,6 +49,8 @@ export default function processModelElements( if (element.sboTerm === 'SBO:0000290') { const compartmentProps = { id: element.id, + complexId: element.complex, + compartmentId: element.compartment, x: element.x, y: element.y, nameX: element.nameX, @@ -68,6 +72,7 @@ export default function processModelElements( fontSize: element.fontSize, pointToProjection, mapInstance, + vectorSource, }; if (element.shape === 'OVAL_COMPARTMENT') { validElements.push(new CompartmentCircle(compartmentProps)); @@ -83,6 +88,8 @@ export default function processModelElements( validElements.push( new MapElement({ id: element.id, + complexId: element.complex, + compartmentId: element.compartment, sboTerm: element.sboTerm, shapes: elementShapes, x: element.x, @@ -107,6 +114,7 @@ export default function processModelElements( fontSize: element.fontSize, pointToProjection, mapInstance, + vectorSource, modifications: element.modificationResidues, lineTypes, bioShapes: shapes, 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 43fabc21..514cd60d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -61,6 +61,8 @@ export const useOlMapReactionsLayer = ({ const pointToProjection = usePointToProjection(); + const vectorSource = useMemo(() => new VectorSource(), []); + useEffect(() => { if (currentModelId) { dispatch(getModelElements(currentModelId)); @@ -130,11 +132,12 @@ export const useOlMapReactionsLayer = ({ arrowTypes, shapes: reactionShapes, pointToProjection, + vectorSource, mapInstance, }); return reactionObject.features; }); - }, [arrowTypes, lineTypes, mapInstance, modelReactions, pointToProjection, shapes]); + }, [arrowTypes, lineTypes, mapInstance, modelReactions, pointToProjection, shapes, vectorSource]); const elements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph @@ -149,18 +152,20 @@ export const useOlMapReactionsLayer = ({ groupedElementsOverlays, overlaysOrder, getOverlayBioEntityColorByAvailableProperties, + vectorSource, mapInstance, pointToProjection, ); }, [ modelElements, shapes, - pointToProjection, - mapInstance, lineTypes, groupedElementsOverlays, overlaysOrder, getOverlayBioEntityColorByAvailableProperties, + vectorSource, + mapInstance, + pointToProjection, ]); const features = useMemo(() => { @@ -174,8 +179,6 @@ export const useOlMapReactionsLayer = ({ ]; }, [elements, linesOverlaysFeatures, markerOverlaysFeatures, reactions]); - const vectorSource = useMemo(() => new VectorSource(), []); - useEffect(() => { vectorSource.clear(); vectorSource.addFeatures(features); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 5997a743..2c046ee6 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -2,7 +2,7 @@ import Polygon from 'ol/geom/Polygon'; import { Style } from 'ol/style'; import Feature, { FeatureLike } from 'ol/Feature'; -import { MultiPolygon } from 'ol/geom'; +import { Geometry, MultiPolygon } from 'ol/geom'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { HorizontalAlign, @@ -17,11 +17,14 @@ import { COMPLEX_SBO_TERM, MAP_ELEMENT_TYPES, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import VectorSource from 'ol/source/Vector'; export interface BaseMapElementProps { type: string; sboTerm?: string; id: number; + complexId?: number | null; + compartmentId: number | null; x: number; y: number; width: number; @@ -39,6 +42,7 @@ export interface BaseMapElementProps { fillColor: Color; borderColor: Color; pointToProjection: UsePointToProjectionResult; + vectorSource: VectorSource; } export default abstract class BaseMultiPolygon { @@ -48,6 +52,10 @@ export default abstract class BaseMultiPolygon { id: number; + complexId?: number | null; + + compartmentId: number | null; + x: number; y: number; @@ -88,10 +96,14 @@ export default abstract class BaseMultiPolygon { pointToProjection: UsePointToProjectionResult; + vectorSource: VectorSource; + constructor({ type, sboTerm, id, + complexId, + compartmentId, x, y, width, @@ -109,10 +121,13 @@ export default abstract class BaseMultiPolygon { fillColor, borderColor, pointToProjection, + vectorSource, }: BaseMapElementProps) { this.type = type; this.sboTerm = sboTerm; this.id = id; + this.complexId = complexId; + this.compartmentId = compartmentId; this.x = x; this.y = y; this.width = width; @@ -130,6 +145,7 @@ export default abstract class BaseMultiPolygon { this.fillColor = fillColor; this.borderColor = borderColor; this.pointToProjection = pointToProjection; + this.vectorSource = vectorSource; } protected abstract createPolygons(): void; @@ -176,25 +192,90 @@ export default abstract class BaseMultiPolygon { } return 1; }, + getMapExtent: (resolution: number): [number, number, number, number] | undefined => { + const view = mapInstance?.getView(); + const center = view?.getCenter(); + const size = mapInstance?.getSize(); + + if (!size || !center) { + return undefined; + } + const extentWidth = size[0] * resolution; + const extentHeight = size[1] * resolution; + + return [ + center[0] - extentWidth / 2, + center[1] - extentHeight / 2, + center[0] + extentWidth / 2, + center[1] + extentHeight / 2, + ]; + }, id: this.id, + complexId: this.complexId, + compartmentId: this.compartmentId, type: this.type, }); - + this.feature.setId(this.id); this.feature.setStyle(this.getStyle.bind(this)); } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { const styles: Array<Style> = []; const getScale = feature.get('getScale'); + const getMapExtent = feature.get('getMapExtent'); let scale = 1; + let cover = false; + let coverRation: number = 1; if (getScale instanceof Function) { scale = getScale(resolution); } + let hide = false; + if (getMapExtent instanceof Function && this.type === 'COMPARTMENT') { + const mapExtent = getMapExtent(resolution); + const featureExtent = feature.getGeometry()?.getExtent(); + if (featureExtent && mapExtent) { + const mapArea = + Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); + const compartmentArea = + Math.abs(featureExtent[2] - featureExtent[0]) * + Math.abs(featureExtent[3] - featureExtent[1]); + coverRation = compartmentArea / mapArea; + if (coverRation < 0.05 && scale < 1) { + cover = true; + } + (feature as Feature).set('filled', cover); + } + } + + let complex: Feature<Geometry> | null; + let compartment: Feature<Geometry> | null; + if (this.complexId) { + complex = this.vectorSource.getFeatureById(this.complexId); + if (complex) { + if (complex.get('hidden')) { + hide = true; + } + } + } + if (this.compartmentId) { + compartment = this.vectorSource.getFeatureById(this.compartmentId); + if (compartment) { + if (compartment.get('filled')) { + hide = true; + } + } + } + (feature as Feature).set('hidden', hide); + if (hide) { + return undefined; + } + let type: string; let fontSize: number; let lineWidth: number; let text: string; + let coverStyle: Style | undefined; this.styles.forEach(style => { const styleGeometry = style.getGeometry(); @@ -203,6 +284,14 @@ export default abstract class BaseMultiPolygon { text = styleGeometry.get('text'); fontSize = styleGeometry.get('fontSize'); lineWidth = styleGeometry.get('lineWidth'); + coverStyle = styleGeometry.get('coverStyle'); + } + if (cover) { + if (coverStyle) { + coverStyle.setZIndex(this.zIndex + 1000); + styles.push(coverStyle); + } + return; } if ( [MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index 9c50b1bc..425ea167 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -14,9 +14,12 @@ import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/s import { MapInstance } from '@/types/map'; import { Color } from '@/types/models'; import { MAP_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import VectorSource from 'ol/source/Vector'; export interface CompartmentProps { id: number; + complexId?: number | null; + compartmentId: number | null; x: number; y: number; width: number; @@ -38,6 +41,7 @@ export interface CompartmentProps { borderColor: Color; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; + vectorSource: VectorSource; } export default abstract class Compartment extends BaseMultiPolygon { @@ -53,6 +57,8 @@ export default abstract class Compartment extends BaseMultiPolygon { constructor({ id, + complexId, + compartmentId, x, y, width, @@ -74,10 +80,13 @@ export default abstract class Compartment extends BaseMultiPolygon { borderColor, pointToProjection, mapInstance, + vectorSource, }: CompartmentProps) { super({ type: 'COMPARTMENT', id, + complexId, + compartmentId, x, y, width, @@ -95,6 +104,7 @@ export default abstract class Compartment extends BaseMultiPolygon { fillColor, borderColor, pointToProjection, + vectorSource, }); this.outerWidth = outerWidth; this.innerWidth = innerWidth; @@ -108,6 +118,18 @@ export default abstract class Compartment extends BaseMultiPolygon { protected abstract getCompartmentCoords(): void; protected createPolygons(): void { + const coverPolygon = new Polygon([this.outerCoords]); + const coverStyle = new Style({ + geometry: coverPolygon, + fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 255 }) }), + }); + coverPolygon.set('coverStyle', coverStyle); + this.styles.push( + new Style({ + geometry: coverPolygon, + }), + ); + const framePolygon = new Polygon([this.outerCoords, this.innerCoords]); framePolygon.set('type', MAP_ELEMENT_TYPES.COMPARTMENT); this.styles.push( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts index 3a880e94..c39c7fc5 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts @@ -17,6 +17,7 @@ import CompartmentCircle, { } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; +import VectorSource from 'ol/source/Vector'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -26,7 +27,7 @@ jest.mock('../coords/getEllipseCoords'); jest.mock('../style/getFill'); jest.mock('../style/rgbToHex'); -describe('MapElement', () => { +describe('CompartmentCircle', () => { let props: CompartmentCircleProps; beforeEach(() => { @@ -41,6 +42,8 @@ describe('MapElement', () => { }); props = { id: 1, + complexId: null, + compartmentId: null, x: 0, y: 0, width: 100, @@ -62,6 +65,7 @@ describe('MapElement', () => { nameHorizontalAlign: 'CENTER', pointToProjection: jest.fn(), mapInstance, + vectorSource: new VectorSource(), }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts index ad8440b3..55576865 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts @@ -14,9 +14,12 @@ import { import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; import { Color } from '@/types/models'; +import VectorSource from 'ol/source/Vector'; export type CompartmentCircleProps = { id: number; + complexId?: number | null; + compartmentId: number | null; x: number; y: number; width: number; @@ -38,11 +41,14 @@ export type CompartmentCircleProps = { nameHorizontalAlign?: HorizontalAlign; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; + vectorSource: VectorSource; }; export default class CompartmentCircle extends Compartment { constructor({ id, + complexId, + compartmentId, x, y, width, @@ -64,9 +70,12 @@ export default class CompartmentCircle extends Compartment { nameHorizontalAlign = 'CENTER', pointToProjection, mapInstance, + vectorSource, }: CompartmentCircleProps) { super({ id, + complexId, + compartmentId, x, y, width, @@ -88,6 +97,7 @@ export default class CompartmentCircle extends Compartment { borderColor, pointToProjection, mapInstance, + vectorSource, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts index db133e7f..96d27c33 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts @@ -17,6 +17,7 @@ import CompartmentPathway, { } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; +import VectorSource from 'ol/source/Vector'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -26,7 +27,7 @@ jest.mock('../coords/getEllipseCoords'); jest.mock('../style/getFill'); jest.mock('../style/rgbToHex'); -describe('MapElement', () => { +describe('CompartmentPathway', () => { let props: CompartmentPathwayProps; beforeEach(() => { @@ -41,6 +42,8 @@ describe('MapElement', () => { }); props = { id: 1, + complexId: null, + compartmentId: null, x: 0, y: 0, width: 100, @@ -60,6 +63,7 @@ describe('MapElement', () => { nameHorizontalAlign: 'CENTER', pointToProjection: jest.fn(() => [10, 10]), mapInstance, + vectorSource: new VectorSource(), }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index bcc70aa1..768519fa 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -14,9 +14,15 @@ import Polygon from 'ol/geom/Polygon'; import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon'; import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; import { Color } from '@/types/models'; +import VectorSource from 'ol/source/Vector'; +import { Style } from 'ol/style'; +import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; export type CompartmentPathwayProps = { id: number; + complexId?: number | null; + compartmentId: number | null; x: number; y: number; width: number; @@ -36,6 +42,7 @@ export type CompartmentPathwayProps = { nameHorizontalAlign?: HorizontalAlign; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; + vectorSource: VectorSource; }; export default class CompartmentPathway extends BaseMultiPolygon { @@ -43,6 +50,8 @@ export default class CompartmentPathway extends BaseMultiPolygon { constructor({ id, + complexId, + compartmentId, x, y, width, @@ -62,10 +71,13 @@ export default class CompartmentPathway extends BaseMultiPolygon { nameHorizontalAlign = 'CENTER', pointToProjection, mapInstance, + vectorSource, }: CompartmentPathwayProps) { super({ type: 'COMPARTMENT', id, + complexId, + compartmentId, x, y, width, @@ -83,6 +95,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { fillColor, borderColor, pointToProjection, + vectorSource, }); this.outerWidth = outerWidth; this.createPolygons(); @@ -97,11 +110,16 @@ export default class CompartmentPathway extends BaseMultiPolygon { this.pointToProjection({ x: this.x + this.width, y: this.y }), this.pointToProjection({ x: this.x + this.width, y: this.y + this.height }), this.pointToProjection({ x: this.x, y: this.y + this.height }), + this.pointToProjection({ x: this.x, y: this.y }), ], ]); compartmentPolygon.set('type', MAP_ELEMENT_TYPES.COMPARTMENT); compartmentPolygon.set('lineWidth', this.outerWidth); - + const coverStyle = new Style({ + geometry: compartmentPolygon, + fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 255 }) }), + }); + compartmentPolygon.set('coverStyle', coverStyle); this.styles.push( getStyle({ geometry: compartmentPolygon, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts index 45c5ad45..812eb5f1 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts @@ -16,6 +16,7 @@ import CompartmentSquare, { } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; +import VectorSource from 'ol/source/Vector'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -24,7 +25,7 @@ jest.mock('../coords/getPolygonCoords'); jest.mock('../style/getFill'); jest.mock('../style/rgbToHex'); -describe('MapElement', () => { +describe('CompartmentSquare', () => { let props: CompartmentSquareProps; beforeEach(() => { @@ -39,6 +40,8 @@ describe('MapElement', () => { }); props = { id: 1, + complexId: null, + compartmentId: null, x: 0, y: 0, width: 100, @@ -60,6 +63,7 @@ describe('MapElement', () => { nameHorizontalAlign: 'CENTER', pointToProjection: jest.fn(), mapInstance, + vectorSource: new VectorSource(), }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts index 1d71ac43..5b1598c8 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts @@ -13,9 +13,12 @@ import { import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; import { Color } from '@/types/models'; +import VectorSource from 'ol/source/Vector'; export type CompartmentSquareProps = { id: number; + complexId?: number | null; + compartmentId: number | null; x: number; y: number; width: number; @@ -37,11 +40,14 @@ export type CompartmentSquareProps = { nameHorizontalAlign?: HorizontalAlign; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; + vectorSource: VectorSource; }; export default class CompartmentSquare extends Compartment { constructor({ id, + complexId, + compartmentId, x, y, width, @@ -63,9 +69,12 @@ export default class CompartmentSquare extends Compartment { nameHorizontalAlign = 'CENTER', pointToProjection, mapInstance, + vectorSource, }: CompartmentSquareProps) { super({ id, + complexId, + compartmentId, x, y, width, @@ -87,6 +96,7 @@ export default class CompartmentSquare extends Compartment { borderColor, pointToProjection, mapInstance, + vectorSource, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts index 81665cc6..ba9c4612 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts @@ -17,6 +17,7 @@ import { } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import { shapesFixture } from '@/models/fixtures/shapesFixture'; +import VectorSource from 'ol/source/Vector'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -40,6 +41,8 @@ describe('MapElement', () => { }); props = { id: 1, + complexId: null, + compartmentId: null, sboTerm: 'SBO:2313123', shapes: shapesFixture, x: 0, @@ -61,6 +64,7 @@ describe('MapElement', () => { nameHorizontalAlign: 'CENTER', pointToProjection: jest.fn(), mapInstance, + vectorSource: new VectorSource(), getOverlayColor: (): string => '#ffffff', }; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index 575d8d77..00295941 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -28,9 +28,12 @@ import { getPolygonLatitudeCoordinates } from '@/components/Map/MapViewer/utils/ import { ZERO } from '@/constants/common'; import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import VectorSource from 'ol/source/Vector'; export type MapElementProps = { id: number; + complexId?: number | null; + compartmentId: number | null; sboTerm: string; shapes: Array<Shape>; x: number; @@ -55,6 +58,7 @@ export type MapElementProps = { activity?: boolean; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; + vectorSource: VectorSource; bioShapes?: BioShapesDict; lineTypes?: LineTypeDict; modifications?: Array<Modification>; @@ -90,6 +94,8 @@ export default class MapElement extends BaseMultiPolygon { constructor({ id, + complexId, + compartmentId, sboTerm, shapes, x, @@ -114,6 +120,7 @@ export default class MapElement extends BaseMultiPolygon { activity, pointToProjection, mapInstance, + vectorSource, bioShapes = {}, lineTypes = {}, modifications = [], @@ -125,6 +132,8 @@ export default class MapElement extends BaseMultiPolygon { type: FEATURE_TYPE.ALIAS, sboTerm, id, + complexId, + compartmentId, x, y, width, @@ -142,6 +151,7 @@ export default class MapElement extends BaseMultiPolygon { fillColor, borderColor, pointToProjection, + vectorSource, }); this.shapes = shapes; this.lineWidth = lineWidth; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts index 86025da4..70c5b3e4 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts @@ -10,6 +10,7 @@ import { shapesFixture } from '@/models/fixtures/shapesFixture'; import View from 'ol/View'; import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { ArrowType, LineType } from '@/types/models'; +import VectorSource from 'ol/source/Vector'; describe('Layer', () => { let props: ReactionProps; @@ -42,6 +43,7 @@ describe('Layer', () => { acc[arrow.arrowType] = arrow.shapes; return acc; }, {}), + vectorSource: new VectorSource(), mapInstance, }; }); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts index f8140764..4a4a9a61 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts @@ -20,6 +20,7 @@ import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shape import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { FEATURE_TYPE } from '@/constants/features'; import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle'; +import VectorSource from 'ol/source/Vector'; export interface ReactionProps { id: number; @@ -33,6 +34,7 @@ export interface ReactionProps { arrowTypes: ArrowTypeDict; shapes: Array<Shape>; pointToProjection: UsePointToProjectionResult; + vectorSource: VectorSource; mapInstance: MapInstance; } @@ -59,6 +61,8 @@ export default class Reaction { pointToProjection: UsePointToProjectionResult; + vectorSource: VectorSource; + mapInstance: MapInstance; features: Array<Feature> = []; @@ -75,6 +79,7 @@ export default class Reaction { arrowTypes, shapes, pointToProjection, + vectorSource, mapInstance, }: ReactionProps) { this.id = id; @@ -88,6 +93,7 @@ export default class Reaction { this.arrowTypes = arrowTypes; this.shapes = shapes; this.pointToProjection = pointToProjection; + this.vectorSource = vectorSource; this.mapInstance = mapInstance; this.drawReaction(); @@ -323,7 +329,24 @@ export default class Reaction { return circleFeature; } + protected isAnyOfElementsHidden(): boolean { + let isHidden = false; + [...this.products, ...this.reactants, ...this.modifiers].forEach(reactionElement => { + const feature = this.vectorSource.getFeatureById(reactionElement.element); + if (feature && feature.get('hidden')) { + isHidden = true; + return true; + } + return false; + }); + return isHidden; + } + protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + if (this.isAnyOfElementsHidden()) { + return undefined; + } + const styles: Array<Style> = []; const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts index da9eed33..4858fb21 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts @@ -8,12 +8,14 @@ export default function getTextStyle({ color, zIndex, horizontalAlign, + overflow = true, }: { text: string; fontSize: number; color: string; zIndex: number; horizontalAlign: HorizontalAlign; + overflow?: boolean; }): Style { return new Style({ text: new Text({ @@ -25,7 +27,7 @@ export default function getTextStyle({ placement: 'point', textAlign: horizontalAlign.toLowerCase() as CanvasTextAlign, textBaseline: 'middle', - overflow: true, + overflow, }), zIndex, }); -- GitLab From c6c92cbb876339f04f9d6983c9f7b6df7145f95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Fri, 22 Nov 2024 10:08:57 +0100 Subject: [PATCH 20/29] feat(vector-map): switching between background types --- .../MapNavigation.component.test.tsx | 8 ++ .../utils/useOverviewImageLinkActions.test.ts | 9 ++ .../ElementLink.component.test.tsx | 2 + .../AssociatedSubmap.component.test.tsx | 3 + .../BioEntitiesSubmapItem.component.test.tsx | 3 + .../PinsListItem.component.test.tsx | 4 + .../SubmapsDrawer/SubmapsDrawer.test.tsx | 3 + src/components/Map/Map.component.tsx | 7 +- .../MapAdditionalActions.component.test.tsx | 2 + .../utils/useAdditionalActions.test.ts | 2 + .../MapVectorBackgroundSelector.component.tsx | 27 ++++++ .../listeners/useOlMapVectorListeners.test.ts | 2 + .../reactionsLayer/processModelElements.ts | 3 + .../reactionsLayer/useOlMapReactionsLayer.ts | 5 ++ .../utils/config/useOlMapVectorLayers.test.ts | 2 + .../config/useOlMapWhiteCardLayer.test.ts | 2 + .../utils/shapes/elements/BaseMultiPolygon.ts | 70 ++++++++------- .../utils/shapes/elements/Compartment.ts | 3 + .../shapes/elements/CompartmentCircle.test.ts | 2 + .../shapes/elements/CompartmentCircle.ts | 3 + .../elements/CompartmentPathway.test.ts | 2 + .../shapes/elements/CompartmentPathway.ts | 3 + .../shapes/elements/CompartmentSquare.test.ts | 2 + .../shapes/elements/CompartmentSquare.ts | 3 + .../utils/shapes/elements/MapElement.test.ts | 2 + .../utils/shapes/elements/MapElement.ts | 3 + .../utils/config/useOlMapCommonLayers.test.ts | 2 + .../utils/config/useOlMapLayers.test.ts | 2 + .../utils/config/useOlMapTileLayer.test.ts | 2 + .../utils/config/useOlMapView.test.ts | 2 + .../utils/listeners/useOlMapListeners.test.ts | 2 + src/redux/map/map.constants.ts | 7 ++ src/redux/map/map.enums.ts | 7 ++ src/redux/map/map.fixtures.ts | 3 + src/redux/map/map.reducers.ts | 12 ++- src/redux/map/map.selectors.ts | 2 + src/redux/map/map.slice.ts | 3 + src/redux/map/map.types.ts | 4 +- .../checkIfIsMapUpdateActionValid.test.ts | 2 + .../map/middleware/map.middleware.test.ts | 2 + .../pluginsManager/map/data/getBounds.test.ts | 2 + .../map/fitBounds/fitBounds.test.ts | 3 + .../pluginsManager/map/openMap.test.ts | 4 + .../map/position/getCenter.test.ts | 2 + .../map/triggerSearch/triggerSearch.test.ts | 2 + .../pluginsManager/map/zoom/getZoom.test.ts | 3 + .../pluginsManager/map/zoom/setZoom.test.ts | 2 + src/shared/Select/Select.component.test.tsx | 41 +++++++++ src/shared/Select/Select.component.tsx | 90 +++++++++++++++++++ src/shared/Select/index.tsx | 1 + src/utils/map/useSetBounds.test.ts | 3 + .../useReduxBusQueryManager.test.ts | 2 + 52 files changed, 349 insertions(+), 35 deletions(-) create mode 100644 src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx create mode 100644 src/redux/map/map.enums.ts create mode 100644 src/shared/Select/Select.component.test.tsx create mode 100644 src/shared/Select/Select.component.tsx create mode 100644 src/shared/Select/index.tsx diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx index 2af7f3fb..35c89a0c 100644 --- a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx +++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.test.tsx @@ -10,6 +10,7 @@ import { } from '@/utils/testing/getReduxWrapperWithStore'; import { act, render, screen, within } from '@testing-library/react'; import { HISTAMINE_MAP_ID, MAIN_MAP_ID } from '@/constants/mocks'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { MapNavigation } from './MapNavigation.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -35,6 +36,7 @@ describe('MapNavigation - component', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); @@ -55,6 +57,7 @@ describe('MapNavigation - component', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); @@ -81,6 +84,7 @@ describe('MapNavigation - component', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); @@ -111,6 +115,7 @@ describe('MapNavigation - component', () => { modelId: HISTAMINE_MAP_ID, }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, loading: 'succeeded', error: { message: '', name: '' }, }, @@ -157,6 +162,7 @@ describe('MapNavigation - component', () => { modelId: HISTAMINE_MAP_ID, }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, loading: 'succeeded', error: { message: '', name: '' }, }, @@ -183,6 +189,7 @@ describe('MapNavigation - component', () => { modelId: HISTAMINE_MAP_ID, }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, loading: 'succeeded', error: { message: '', name: '' }, }, @@ -207,6 +214,7 @@ describe('MapNavigation - component', () => { modelId: HISTAMINE_MAP_ID, }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, loading: 'succeeded', error: { message: '', name: '' }, }, diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts index afcf6cb1..2df46d7c 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageLinkActions.test.ts @@ -17,6 +17,7 @@ import { OverviewImageLink } from '@/types/models'; import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { renderHook } from '@testing-library/react'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { FIRST_ARRAY_ELEMENT, NOOP, @@ -59,6 +60,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); @@ -109,6 +111,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); @@ -166,6 +169,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK_SHORT, @@ -244,6 +248,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK_SHORT, @@ -348,6 +353,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK_SHORT, @@ -405,6 +411,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK_SHORT, @@ -466,6 +473,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK_SHORT, @@ -529,6 +537,7 @@ describe('useOverviewImageLinkActions - hook', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK_SHORT, diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx index bb30b23f..9ed58cb2 100644 --- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx +++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/ElementsOnMapCell/ElementLink/ElementLink.component.test.tsx @@ -14,6 +14,7 @@ import { import { render, screen, waitFor } from '@testing-library/react'; import { HttpStatusCode } from 'axios'; import { MockStoreEnhanced } from 'redux-mock-store'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { ElementLink } from './ElementLink.component'; const mockedAxiosNewClient = mockNetworkNewAPIResponse(); @@ -208,6 +209,7 @@ describe('ElementLink - component', () => { lastPosition: DEFAULT_POSITION, }, ], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }, ); diff --git a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx index d3880a09..4cc91adc 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx @@ -18,6 +18,7 @@ import { getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; import { act, render, screen } from '@testing-library/react'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { AssociatedSubmap } from './AssociatedSubmap.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -91,6 +92,7 @@ describe('AssociatedSubmap - component', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, bioEntity: { ...BIOENTITY_INITIAL_STATE_MOCK, @@ -150,6 +152,7 @@ describe('AssociatedSubmap - component', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, bioEntity: { ...BIOENTITY_INITIAL_STATE_MOCK, diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx index aa753d71..0cc8a551 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesSubmapItem/BioEntitiesSubmapItem.component.test.tsx @@ -13,6 +13,7 @@ import { openedMapsInitialValueFixture, openedMapsThreeSubmapsFixture, } from '@/redux/map/map.fixtures'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { BioEntitiesSubmapItem } from './BioEntitiesSubmapItem.component'; const CORE_MAP_ID = 5053; @@ -100,6 +101,7 @@ describe('BioEntitiesSubmapItem - component', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); @@ -157,6 +159,7 @@ describe('BioEntitiesSubmapItem - component', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx index b54bea43..9cfb68d8 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx @@ -11,6 +11,7 @@ import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/ma import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { MockStoreEnhanced } from 'redux-mock-store'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { PinTypeWithNone } from '../PinsList.types'; import { PinsListItem } from './PinsListItem.component'; @@ -37,6 +38,7 @@ const INITIAL_STORE_STATE: InitialStoreState = { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }; @@ -169,6 +171,7 @@ describe('PinsListItem - component ', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }, ); @@ -202,6 +205,7 @@ describe('PinsListItem - component ', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }, ); diff --git a/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx b/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx index 42754f4e..825bccfd 100644 --- a/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx +++ b/src/components/Map/Drawer/SubmapsDrawer/SubmapsDrawer.test.tsx @@ -11,6 +11,7 @@ import { openedMapsInitialValueFixture, openedMapsThreeSubmapsFixture, } from '@/redux/map/map.fixtures'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { SubmapsDrawer } from './SubmapsDrawer'; const MAIN_MAP_ID = 5053; @@ -64,6 +65,7 @@ describe('SubmapsDrawer - component', () => { loading: 'succeeded', error: { name: '', message: '' }, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); @@ -109,6 +111,7 @@ describe('SubmapsDrawer - component', () => { openedMaps: openedMapsThreeSubmapsFixture, loading: 'succeeded', error: { name: '', message: '' }, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx index 67b7187c..d0927b3a 100644 --- a/src/components/Map/Map.component.tsx +++ b/src/components/Map/Map.component.tsx @@ -3,18 +3,23 @@ import { Drawer } from '@/components/Map/Drawer'; import { Legend } from '@/components/Map/Legend'; import { MapViewer } from '@/components/Map/MapViewer'; import { MapLoader } from '@/components/Map/MapLoader/MapLoader.component'; +import { MapVectorBackgroundSelector } from '@/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { vectorRenderingSelector } from '@/redux/models/models.selectors'; import { MapAdditionalActions } from './MapAdditionalActions'; import { MapAdditionalOptions } from './MapAdditionalOptions'; import { PluginsDrawer } from './PluginsDrawer'; export const Map = (): JSX.Element => { + const vectorRendering = useAppSelector(vectorRenderingSelector); return ( <div className="relative z-0 h-screen w-full overflow-hidden bg-black" data-testid="map-container" > <MapViewer /> - <MapAdditionalOptions /> + {!vectorRendering && <MapAdditionalOptions />} + {vectorRendering && <MapVectorBackgroundSelector />} <Drawer /> <PluginsDrawer /> <Legend /> diff --git a/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx b/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx index 06bd09fe..5fe7353f 100644 --- a/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx +++ b/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx @@ -11,6 +11,7 @@ import { import { act, render, screen } from '@testing-library/react'; import Map from 'ol/Map'; import { MockStoreEnhanced } from 'redux-mock-store'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { MapAdditionalActions } from './MapAdditionalActions.component'; import { useVisibleBioEntitiesPolygonCoordinates } from './utils/useVisibleBioEntitiesPolygonCoordinates'; @@ -145,6 +146,7 @@ describe('MapAdditionalActions - component', () => { message: '', }, openedMaps: [], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts b/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts index 898b10e0..8e9ca7df 100644 --- a/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts +++ b/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts @@ -8,6 +8,7 @@ import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreA import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { renderHook } from '@testing-library/react'; import Map from 'ol/Map'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { useAddtionalActions } from './useAdditionalActions'; import { useVisibleBioEntitiesPolygonCoordinates } from './useVisibleBioEntitiesPolygonCoordinates'; @@ -94,6 +95,7 @@ describe('useAddtionalActions - hook', () => { message: '', }, openedMaps: [], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }, { diff --git a/src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx b/src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx new file mode 100644 index 00000000..32af5af4 --- /dev/null +++ b/src/components/Map/MapVectorBackgroundSelector/MapVectorBackgroundSelector.component.tsx @@ -0,0 +1,27 @@ +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { mapBackgroundTypeSelector } from '@/redux/map/map.selectors'; +import { twMerge } from 'tailwind-merge'; +import { MAP_BACKGROUND_TYPES } from '@/redux/map/map.constants'; +import { setMapBackgroundType } from '@/redux/map/map.slice'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { Select } from '@/shared/Select'; + +export const MapVectorBackgroundSelector = (): JSX.Element => { + const dispatch = useAppDispatch(); + const backgroundType = useAppSelector(mapBackgroundTypeSelector); + + const handleChange = (selectedBackgroundType: number): void => { + dispatch(setMapBackgroundType(selectedBackgroundType)); + }; + + return ( + <div className={twMerge('absolute right-6 top-[calc(64px+40px+24px)] z-10 flex')}> + <Select + options={MAP_BACKGROUND_TYPES} + selectedId={backgroundType} + onChange={handleChange} + width={100} + /> + </div> + ); +}; diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts index 8b772615..2821fe91 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts @@ -5,6 +5,7 @@ import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithSto 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/mouseClick/mouseLeftClick/onMapLeftClick'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; jest.mock('./mouseClick/mouseLeftClick/onMapLeftClick', () => ({ __esModule: true, @@ -25,6 +26,7 @@ describe('useOlMapVectorListeners - util', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index 42914e0d..357350e5 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -26,6 +26,7 @@ export default function processModelElements( vectorSource: VectorSource, mapInstance: MapInstance, pointToProjection: UsePointToProjectionResult, + mapBackgroundType: number, ): Array<MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph> { const validElements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph @@ -73,6 +74,7 @@ export default function processModelElements( pointToProjection, mapInstance, vectorSource, + mapBackgroundType, }; if (element.shape === 'OVAL_COMPARTMENT') { validElements.push(new CompartmentCircle(compartmentProps)); @@ -121,6 +123,7 @@ export default function processModelElements( overlays: groupedElementsOverlays[element.id], overlaysOrder, getOverlayColor, + mapBackgroundType, }), ); } 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 514cd60d..a6c6253e 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -38,6 +38,7 @@ import { parseSurfaceMarkersToBioEntityRender } from '@/components/Map/MapViewer import MarkerOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay'; import processModelElements from '@/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements'; import useDebouncedValue from '@/utils/useDebouncedValue'; +import { mapBackgroundTypeSelector } from '@/redux/map/map.selectors'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -53,10 +54,12 @@ export const useOlMapReactionsLayer = ({ const lineTypes = useSelector(lineTypesSelector); const arrowTypes = useSelector(arrowTypesSelector); const overlaysOrder = useSelector(getOverlayOrderSelector); + const mapBackgroundType = useSelector(mapBackgroundTypeSelector); const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); const debouncedBioEntities = useDebouncedValue(bioEntities, 2000); + const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); const pointToProjection = usePointToProjection(); @@ -155,6 +158,7 @@ export const useOlMapReactionsLayer = ({ vectorSource, mapInstance, pointToProjection, + mapBackgroundType, ); }, [ modelElements, @@ -166,6 +170,7 @@ export const useOlMapReactionsLayer = ({ vectorSource, mapInstance, pointToProjection, + mapBackgroundType, ]); const features = useMemo(() => { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.test.ts index eee20645..fed682be 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.test.ts @@ -7,6 +7,7 @@ import VectorLayer from 'ol/layer/Vector'; import React from 'react'; import { useOlMap } from '@/components/Map/MapViewer/utils/useOlMap'; import { useOlMapVectorLayers } from '@/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; const useRefValue = { current: null, @@ -59,6 +60,7 @@ describe('useOlMapLayers - util', () => { message: '', }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); const dummyElement = document.createElement('div'); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapWhiteCardLayer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapWhiteCardLayer.test.ts index 2ee7f699..394abc0c 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapWhiteCardLayer.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapWhiteCardLayer.test.ts @@ -5,6 +5,7 @@ import { renderHook } from '@testing-library/react'; import BaseLayer from 'ol/layer/Base'; import VectorLayer from 'ol/layer/Vector'; import React from 'react'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { useOlMapWhiteCardLayer } from './useOlMapWhiteCardLayer'; const useRefValue = { @@ -58,6 +59,7 @@ describe('useOlMapWhiteCardLayer - util', () => { message: '', }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 2c046ee6..a28643fb 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -18,6 +18,7 @@ import { MAP_ELEMENT_TYPES, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import VectorSource from 'ol/source/Vector'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; export interface BaseMapElementProps { type: string; @@ -43,6 +44,7 @@ export interface BaseMapElementProps { borderColor: Color; pointToProjection: UsePointToProjectionResult; vectorSource: VectorSource; + mapBackgroundType: number; } export default abstract class BaseMultiPolygon { @@ -98,6 +100,8 @@ export default abstract class BaseMultiPolygon { vectorSource: VectorSource; + mapBackgroundType: number; + constructor({ type, sboTerm, @@ -122,6 +126,7 @@ export default abstract class BaseMultiPolygon { borderColor, pointToProjection, vectorSource, + mapBackgroundType, }: BaseMapElementProps) { this.type = type; this.sboTerm = sboTerm; @@ -146,6 +151,7 @@ export default abstract class BaseMultiPolygon { this.borderColor = borderColor; this.pointToProjection = pointToProjection; this.vectorSource = vectorSource; + this.mapBackgroundType = mapBackgroundType; } protected abstract createPolygons(): void; @@ -231,44 +237,46 @@ export default abstract class BaseMultiPolygon { } let hide = false; - if (getMapExtent instanceof Function && this.type === 'COMPARTMENT') { - const mapExtent = getMapExtent(resolution); - const featureExtent = feature.getGeometry()?.getExtent(); - if (featureExtent && mapExtent) { - const mapArea = - Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); - const compartmentArea = - Math.abs(featureExtent[2] - featureExtent[0]) * - Math.abs(featureExtent[3] - featureExtent[1]); - coverRation = compartmentArea / mapArea; - if (coverRation < 0.05 && scale < 1) { - cover = true; + if (this.mapBackgroundType === MapBackgroundsEnum.SEMANTIC) { + if (getMapExtent instanceof Function && this.type === 'COMPARTMENT') { + const mapExtent = getMapExtent(resolution); + const featureExtent = feature.getGeometry()?.getExtent(); + if (featureExtent && mapExtent) { + const mapArea = + Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); + const compartmentArea = + Math.abs(featureExtent[2] - featureExtent[0]) * + Math.abs(featureExtent[3] - featureExtent[1]); + coverRation = compartmentArea / mapArea; + if (coverRation < 0.05 && scale < 1) { + cover = true; + } + (feature as Feature).set('filled', cover); } - (feature as Feature).set('filled', cover); } - } - let complex: Feature<Geometry> | null; - let compartment: Feature<Geometry> | null; - if (this.complexId) { - complex = this.vectorSource.getFeatureById(this.complexId); - if (complex) { - if (complex.get('hidden')) { - hide = true; + let complex: Feature<Geometry> | null; + let compartment: Feature<Geometry> | null; + if (this.complexId) { + complex = this.vectorSource.getFeatureById(this.complexId); + if (complex) { + if (complex.get('hidden')) { + hide = true; + } } } - } - if (this.compartmentId) { - compartment = this.vectorSource.getFeatureById(this.compartmentId); - if (compartment) { - if (compartment.get('filled')) { - hide = true; + if (this.compartmentId) { + compartment = this.vectorSource.getFeatureById(this.compartmentId); + if (compartment) { + if (compartment.get('filled')) { + hide = true; + } } } - } - (feature as Feature).set('hidden', hide); - if (hide) { - return undefined; + (feature as Feature).set('hidden', hide); + if (hide) { + return undefined; + } } let type: string; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index 425ea167..c61b16d1 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -42,6 +42,7 @@ export interface CompartmentProps { pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; + mapBackgroundType: number; } export default abstract class Compartment extends BaseMultiPolygon { @@ -81,6 +82,7 @@ export default abstract class Compartment extends BaseMultiPolygon { pointToProjection, mapInstance, vectorSource, + mapBackgroundType, }: CompartmentProps) { super({ type: 'COMPARTMENT', @@ -105,6 +107,7 @@ export default abstract class Compartment extends BaseMultiPolygon { borderColor, pointToProjection, vectorSource, + mapBackgroundType, }); this.outerWidth = outerWidth; this.innerWidth = innerWidth; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts index c39c7fc5..ba61e067 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts @@ -18,6 +18,7 @@ import CompartmentCircle, { import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -66,6 +67,7 @@ describe('CompartmentCircle', () => { pointToProjection: jest.fn(), mapInstance, vectorSource: new VectorSource(), + mapBackgroundType: MapBackgroundsEnum.SEMANTIC, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts index 55576865..040cc8ee 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts @@ -42,6 +42,7 @@ export type CompartmentCircleProps = { pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; + mapBackgroundType: number; }; export default class CompartmentCircle extends Compartment { @@ -71,6 +72,7 @@ export default class CompartmentCircle extends Compartment { pointToProjection, mapInstance, vectorSource, + mapBackgroundType, }: CompartmentCircleProps) { super({ id, @@ -98,6 +100,7 @@ export default class CompartmentCircle extends Compartment { pointToProjection, mapInstance, vectorSource, + mapBackgroundType, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts index 96d27c33..26bf658b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts @@ -18,6 +18,7 @@ import CompartmentPathway, { import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -64,6 +65,7 @@ describe('CompartmentPathway', () => { pointToProjection: jest.fn(() => [10, 10]), mapInstance, vectorSource: new VectorSource(), + mapBackgroundType: MapBackgroundsEnum.SEMANTIC, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index 768519fa..0fc13627 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -43,6 +43,7 @@ export type CompartmentPathwayProps = { pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; + mapBackgroundType: number; }; export default class CompartmentPathway extends BaseMultiPolygon { @@ -72,6 +73,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { pointToProjection, mapInstance, vectorSource, + mapBackgroundType, }: CompartmentPathwayProps) { super({ type: 'COMPARTMENT', @@ -96,6 +98,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { borderColor, pointToProjection, vectorSource, + mapBackgroundType, }); this.outerWidth = outerWidth; this.createPolygons(); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts index 812eb5f1..ae4148d2 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts @@ -17,6 +17,7 @@ import CompartmentSquare, { import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -64,6 +65,7 @@ describe('CompartmentSquare', () => { pointToProjection: jest.fn(), mapInstance, vectorSource: new VectorSource(), + mapBackgroundType: MapBackgroundsEnum.SEMANTIC, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts index 5b1598c8..1732c177 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts @@ -41,6 +41,7 @@ export type CompartmentSquareProps = { pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; + mapBackgroundType: number; }; export default class CompartmentSquare extends Compartment { @@ -70,6 +71,7 @@ export default class CompartmentSquare extends Compartment { pointToProjection, mapInstance, vectorSource, + mapBackgroundType, }: CompartmentSquareProps) { super({ id, @@ -97,6 +99,7 @@ export default class CompartmentSquare extends Compartment { pointToProjection, mapInstance, vectorSource, + mapBackgroundType, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts index ba9c4612..1611b7fe 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts @@ -18,6 +18,7 @@ import { import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import { shapesFixture } from '@/models/fixtures/shapesFixture'; import VectorSource from 'ol/source/Vector'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -66,6 +67,7 @@ describe('MapElement', () => { mapInstance, vectorSource: new VectorSource(), getOverlayColor: (): string => '#ffffff', + mapBackgroundType: MapBackgroundsEnum.SEMANTIC, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index 00295941..4a0cd82c 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -65,6 +65,7 @@ export type MapElementProps = { overlays?: Array<OverlayBioEntityRender>; overlaysOrder?: Array<OverlayOrder>; getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + mapBackgroundType: number; }; export default class MapElement extends BaseMultiPolygon { @@ -127,6 +128,7 @@ export default class MapElement extends BaseMultiPolygon { overlays = [], overlaysOrder = [], getOverlayColor, + mapBackgroundType, }: MapElementProps) { super({ type: FEATURE_TYPE.ALIAS, @@ -152,6 +154,7 @@ export default class MapElement extends BaseMultiPolygon { borderColor, pointToProjection, vectorSource, + mapBackgroundType, }); this.shapes = shapes; this.lineWidth = lineWidth; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts index 852f8093..b3117b1f 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts @@ -6,6 +6,7 @@ import BaseLayer from 'ol/layer/Base'; import React from 'react'; import VectorLayer from 'ol/layer/Vector'; import { useOlMapCommonLayers } from '@/components/Map/MapViewer/utils/config/useOlMapCommonLayers'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; const useRefValue = { current: null, @@ -58,6 +59,7 @@ describe('useOlMapCommonLayers - util', () => { message: '', }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts index ffcd6326..b4bfec60 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts @@ -6,6 +6,7 @@ import BaseLayer from 'ol/layer/Base'; import TileLayer from 'ol/layer/Tile'; import React from 'react'; import VectorLayer from 'ol/layer/Vector'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { useOlMapLayers } from './useOlMapLayers'; const useRefValue = { @@ -59,6 +60,7 @@ describe('useOlMapLayers - util', () => { message: '', }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.test.ts index 8c5321cb..58e4aaa5 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.test.ts @@ -5,6 +5,7 @@ import { renderHook } from '@testing-library/react'; import BaseLayer from 'ol/layer/Base'; import TileLayer from 'ol/layer/Tile'; import React from 'react'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { useOlMapTileLayer } from './useOlMapTileLayer'; const useRefValue = { @@ -58,6 +59,7 @@ describe('useOlMapTileLayer - util', () => { message: '', }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts index 1b52b84c..d630b12b 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts @@ -11,6 +11,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { View } from 'ol'; import Map from 'ol/Map'; import React from 'react'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { useOlMap } from '../useOlMap'; import { useOlMapView } from './useOlMapView'; @@ -95,6 +96,7 @@ describe('useOlMapView - util', () => { message: '', }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, backgrounds: BACKGROUND_INITIAL_STATE_MOCK, }); diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.test.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.test.ts index 340b61de..94a53559 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.test.ts @@ -3,6 +3,7 @@ import { renderHook } from '@testing-library/react'; import { View } from 'ol'; import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import * as singleClickListener from './mapSingleClick/onMapSingleClick'; import * as positionListener from './onMapPositionChange'; import { useOlMapListeners } from './useOlMapListeners'; @@ -31,6 +32,7 @@ describe('useOlMapListeners - util', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }); diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts index 3b9e9e53..90648de6 100644 --- a/src/redux/map/map.constants.ts +++ b/src/redux/map/map.constants.ts @@ -6,10 +6,16 @@ import { DEFAULT_TILE_SIZE, } from '@/constants/map'; import { Point } from '@/types/map'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { MapData, MapState, OppenedMap } from './map.types'; export const MAIN_MAP = 'Main map'; +export const MAP_BACKGROUND_TYPES = [ + { id: MapBackgroundsEnum.NETWORK, name: 'Network' }, + { id: MapBackgroundsEnum.SEMANTIC, name: 'Semantic' }, +]; + export const MODEL_ID_DEFAULT: number = 0; export const BACKGROUND_ID_DEFAULT: number = 0; @@ -67,6 +73,7 @@ export const MAP_INITIAL_STATE: MapState = { loading: 'idle', error: { name: '', message: '' }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.NETWORK, }; export const INIT_MAP_SIZE_MODEL_ID_ERROR_PREFIX = 'Failed to initialize map size and model ID'; diff --git a/src/redux/map/map.enums.ts b/src/redux/map/map.enums.ts new file mode 100644 index 00000000..8483ca33 --- /dev/null +++ b/src/redux/map/map.enums.ts @@ -0,0 +1,7 @@ +/* eslint-disable no-magic-numbers */ +enum MapBackgroundsEnum { + NETWORK = 1, + SEMANTIC = 2, +} + +export default MapBackgroundsEnum; diff --git a/src/redux/map/map.fixtures.ts b/src/redux/map/map.fixtures.ts index 049bdc64..268b7f9d 100644 --- a/src/redux/map/map.fixtures.ts +++ b/src/redux/map/map.fixtures.ts @@ -1,5 +1,6 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { MODEL_ID_DEFAULT } from '@/redux/map/map.constants'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { MapData, MapState, OppenedMap } from './map.types'; export const openedMapsInitialValueFixture: OppenedMap[] = [ @@ -54,6 +55,7 @@ export const initialMapStateFixture: MapState = { loading: 'idle', error: DEFAULT_ERROR, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }; export const mapStateWithCurrentlySelectedMainMapFixture: MapState = { @@ -71,4 +73,5 @@ export const mapStateWithCurrentlySelectedMainMapFixture: MapState = { loading: 'idle', error: DEFAULT_ERROR, openedMaps: openedMapsInitialValueFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }; diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts index f193e753..5ae6707d 100644 --- a/src/redux/map/map.reducers.ts +++ b/src/redux/map/map.reducers.ts @@ -1,7 +1,7 @@ import { DEFAULT_ZOOM } from '@/constants/map'; import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; -import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { getPointMerged } from '../../utils/object/getPointMerged'; +import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; +import { getPointMerged } from '@/utils/object/getPointMerged'; import { initMapBackground, initMapPosition, @@ -221,3 +221,11 @@ export const initOpenedMapsReducer = (builder: ActionReducerMapBuilder<MapState> state.openedMaps = action.payload; }); }; + +export const setMapBackgroundTypeReducer = ( + state: MapState, + action: PayloadAction<number>, +): void => { + const { payload } = action; + state.backgroundType = payload; +}; diff --git a/src/redux/map/map.selectors.ts b/src/redux/map/map.selectors.ts index 257f99fd..8cb88035 100644 --- a/src/redux/map/map.selectors.ts +++ b/src/redux/map/map.selectors.ts @@ -34,3 +34,5 @@ export const mapDataLastZoomValue = createSelector( ); export const mapDataMaxZoomValue = createSelector(mapDataSizeSelector, model => model.maxZoom); + +export const mapBackgroundTypeSelector = createSelector(mapSelector, map => map.backgroundType); diff --git a/src/redux/map/map.slice.ts b/src/redux/map/map.slice.ts index 3106a118..dbfd8bbe 100644 --- a/src/redux/map/map.slice.ts +++ b/src/redux/map/map.slice.ts @@ -12,6 +12,7 @@ import { setActiveMapReducer, setLastPositionZoomReducer, setMapBackgroundReducer, + setMapBackgroundTypeReducer, setMapDataReducer, setMapPositionReducer, updateLastClickReducer, @@ -35,6 +36,7 @@ const mapSlice = createSlice({ setLastPositionZoom: setLastPositionZoomReducer, updateLastClick: updateLastClickReducer, updateLastRightClick: updateLastRightClickReducer, + setMapBackgroundType: setMapBackgroundTypeReducer, }, extraReducers: builder => { initMapPositionReducers(builder); @@ -57,6 +59,7 @@ export const { setLastPositionZoom, updateLastClick, updateLastRightClick, + setMapBackgroundType, } = mapSlice.actions; export default mapSlice.reducer; diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts index 72b600dc..dc838aaa 100644 --- a/src/redux/map/map.types.ts +++ b/src/redux/map/map.types.ts @@ -44,7 +44,9 @@ export type MapData = { }; }; -export type MapState = FetchDataState<MapData, MapData> & { openedMaps: OppenedMap[] }; +export type MapState = FetchDataState<MapData, MapData> & { openedMaps: OppenedMap[] } & { + backgroundType: number; +}; export type SetMapDataActionPayload = | (Omit<Partial<MapData>, 'position' | 'projectId'> & { diff --git a/src/redux/map/middleware/checkIfIsMapUpdateActionValid.test.ts b/src/redux/map/middleware/checkIfIsMapUpdateActionValid.test.ts index 5580c024..1509a089 100644 --- a/src/redux/map/middleware/checkIfIsMapUpdateActionValid.test.ts +++ b/src/redux/map/middleware/checkIfIsMapUpdateActionValid.test.ts @@ -1,5 +1,6 @@ import { RootState } from '@/redux/store'; import { Loading } from '@/types/loadingState'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { MAP_DATA_INITIAL_STATE, MIDDLEWARE_ALLOWED_ACTIONS, @@ -17,6 +18,7 @@ const state: Pick<RootState, 'map'> = { loading: 'idle' as Loading, error: { name: '', message: '' }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }; diff --git a/src/redux/map/middleware/map.middleware.test.ts b/src/redux/map/middleware/map.middleware.test.ts index 11015648..835c5e2d 100644 --- a/src/redux/map/middleware/map.middleware.test.ts +++ b/src/redux/map/middleware/map.middleware.test.ts @@ -3,6 +3,7 @@ import { modelsFixture } from '@/models/fixtures/modelsFixture'; import { Loading } from '@/types/loadingState'; import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { Action } from '@reduxjs/toolkit'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { MAP_DATA_INITIAL_STATE, MIDDLEWARE_ALLOWED_ACTIONS, @@ -63,6 +64,7 @@ const { store } = getReduxWrapperWithStore({ modelId: modelsFixture[0].idObject, }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { ...defaultSliceState, diff --git a/src/services/pluginsManager/map/data/getBounds.test.ts b/src/services/pluginsManager/map/data/getBounds.test.ts index 99f587da..b824df10 100644 --- a/src/services/pluginsManager/map/data/getBounds.test.ts +++ b/src/services/pluginsManager/map/data/getBounds.test.ts @@ -2,6 +2,7 @@ import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants'; import { store } from '@/redux/store'; import { Map } from 'ol'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { MapManager } from '../mapManager'; import { getBounds } from './getBounds'; @@ -45,6 +46,7 @@ describe('getBounds', () => { message: '', }, openedMaps: [], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any, diff --git a/src/services/pluginsManager/map/fitBounds/fitBounds.test.ts b/src/services/pluginsManager/map/fitBounds/fitBounds.test.ts index 1fd5ec32..de852078 100644 --- a/src/services/pluginsManager/map/fitBounds/fitBounds.test.ts +++ b/src/services/pluginsManager/map/fitBounds/fitBounds.test.ts @@ -2,6 +2,7 @@ import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants'; import { Map } from 'ol'; import { store } from '@/redux/store'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { fitBounds } from './fitBounds'; import { MapManager } from '../mapManager'; @@ -51,6 +52,7 @@ describe('fitBounds', () => { message: '', }, openedMaps: [], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any, @@ -101,6 +103,7 @@ describe('fitBounds', () => { message: '', }, openedMaps: [], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any, diff --git a/src/services/pluginsManager/map/openMap.test.ts b/src/services/pluginsManager/map/openMap.test.ts index 152f81b5..b3d01c8f 100644 --- a/src/services/pluginsManager/map/openMap.test.ts +++ b/src/services/pluginsManager/map/openMap.test.ts @@ -3,6 +3,7 @@ import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice'; import { RootState, store } from '@/redux/store'; import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; import { MODELS_MOCK, MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { PluginsEventBus } from '../pluginsEventBus'; import { openMap } from './openMap'; @@ -25,6 +26,7 @@ describe('openMap', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK_SHORT, @@ -50,6 +52,7 @@ describe('openMap', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK, @@ -77,6 +80,7 @@ describe('openMap', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, models: { data: MODELS_MOCK, diff --git a/src/services/pluginsManager/map/position/getCenter.test.ts b/src/services/pluginsManager/map/position/getCenter.test.ts index f9d9dd54..fe23d527 100644 --- a/src/services/pluginsManager/map/position/getCenter.test.ts +++ b/src/services/pluginsManager/map/position/getCenter.test.ts @@ -1,5 +1,6 @@ import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; import { RootState, store } from '@/redux/store'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { getCenter } from './getCenter'; jest.mock('../../../../redux/store'); @@ -25,6 +26,7 @@ describe('getCenter - plugin method', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }) as RootState, ); diff --git a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts index abb470aa..293f9476 100644 --- a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts +++ b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts @@ -11,6 +11,7 @@ import { RootState, store } from '@/redux/store'; import { mockNetworkNewAPIResponse, mockNetworkResponse } from '@/utils/mockNetworkResponse'; import { waitFor } from '@testing-library/react'; import { HttpStatusCode } from 'axios'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { triggerSearch } from './triggerSearch'; import { ERROR_INVALID_MODEL_ID_TYPE } from '../../errorMessages'; @@ -29,6 +30,7 @@ const MOCK_STATE = { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, configuration: CONFIGURATION_INITIAL_STORE_MOCKS, }; diff --git a/src/services/pluginsManager/map/zoom/getZoom.test.ts b/src/services/pluginsManager/map/zoom/getZoom.test.ts index b7808d53..669d8773 100644 --- a/src/services/pluginsManager/map/zoom/getZoom.test.ts +++ b/src/services/pluginsManager/map/zoom/getZoom.test.ts @@ -1,6 +1,7 @@ /* eslint-disable no-magic-numbers */ import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures'; import { RootState, store } from '@/redux/store'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { getZoom } from './getZoom'; jest.mock('../../../../redux/store'); @@ -28,6 +29,7 @@ describe('getZoom - plugin method', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }) as RootState, ); @@ -57,6 +59,7 @@ describe('getZoom - plugin method', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }) as RootState, ); diff --git a/src/services/pluginsManager/map/zoom/setZoom.test.ts b/src/services/pluginsManager/map/zoom/setZoom.test.ts index 55502f30..2d5f8a3d 100644 --- a/src/services/pluginsManager/map/zoom/setZoom.test.ts +++ b/src/services/pluginsManager/map/zoom/setZoom.test.ts @@ -4,6 +4,7 @@ import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/ma import { setLastPositionZoom } from '@/redux/map/map.slice'; import { RootState, store } from '@/redux/store'; import { ZodError } from 'zod'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { setZoom } from './setZoom'; jest.mock('../../../../redux/store'); @@ -36,6 +37,7 @@ describe('setZoom - plugin method', () => { loading: 'succeeded', error: { message: '', name: '' }, openedMaps: openedMapsThreeSubmapsFixture, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }) as RootState, ); diff --git a/src/shared/Select/Select.component.test.tsx b/src/shared/Select/Select.component.test.tsx new file mode 100644 index 00000000..d7db8199 --- /dev/null +++ b/src/shared/Select/Select.component.test.tsx @@ -0,0 +1,41 @@ +/* eslint-disable no-magic-numbers */ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { Select } from '.'; + +describe('Select Component', () => { + const mockOptions = [ + { id: 1, name: 'Option 1' }, + { id: 2, name: 'Option 2' }, + { id: 3, name: 'Option 3' }, + ]; + + const mockOnChange = jest.fn(); + + it('renders the Select component', () => { + render(<Select options={mockOptions} selectedId={1} onChange={mockOnChange} />); + expect(screen.getByTestId('select-component')).toBeInTheDocument(); + }); + + it('displays the selected option name', () => { + render(<Select options={mockOptions} selectedId={1} onChange={mockOnChange} />); + expect(screen.getByText('Option 1')).toBeInTheDocument(); + }); + + it('opens the dropdown when clicked', () => { + render(<Select options={mockOptions} selectedId={1} onChange={mockOnChange} />); + const toggleButton = screen.getByTestId('dropdown-button-name'); + + fireEvent.click(toggleButton); + expect(screen.getByRole('listbox')).toBeVisible(); + }); + + it('calls onChange with the correct value when an option is clicked', () => { + render(<Select options={mockOptions} selectedId={1} onChange={mockOnChange} />); + fireEvent.click(screen.getByTestId('dropdown-button-name')); + const optionToSelect = screen.getByText('Option 3'); + + fireEvent.click(optionToSelect); + expect(mockOnChange).toHaveBeenCalledWith(3); + }); +}); diff --git a/src/shared/Select/Select.component.tsx b/src/shared/Select/Select.component.tsx new file mode 100644 index 00000000..aa3ab6d5 --- /dev/null +++ b/src/shared/Select/Select.component.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { twMerge } from 'tailwind-merge'; +import { useSelect } from 'downshift'; +import { Icon } from '@/shared/Icon'; + +type SelectProps = { + options: Array<{ id: number; name: string }>; + selectedId: number; + onChange: (selectedId: number) => void; + width?: string | number; +}; + +export const Select = ({ + options, + selectedId, + onChange, + width = '100%', +}: SelectProps): React.JSX.Element => { + const selectedOption = options.find(option => option.id === selectedId); + + const { + isOpen, + highlightedIndex, + getToggleButtonProps, + getMenuProps, + getItemProps, + selectedItem, + } = useSelect({ + items: options, + selectedItem: selectedOption, + onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { + if (newSelectedItem) { + onChange(newSelectedItem.id); + } + }, + itemToString: item => (item ? item.name : ''), + }); + + const widthStyle = typeof width === 'number' ? { width: `${width}px` } : { width }; + + return ( + <div + data-testid="select-component" + className={twMerge( + 'relative rounded-t bg-white text-xs shadow-primary', + !isOpen && 'rounded-b', + )} + style={widthStyle} + > + <div + className={twMerge( + 'flex cursor-pointer flex-row items-center justify-between rounded-t p-2', + )} + {...getToggleButtonProps()} + > + <span data-testid="dropdown-button-name" className="font-medium"> + {selectedItem ? selectedItem.name : 'Select an option'} + </span> + <Icon + name="chevron-down" + className={twMerge('arrow-button h-6 w-6 fill-primary-500', isOpen && 'rotate-180')} + /> + </div> + <ul + className={twMerge( + 'absolute z-10 overflow-auto rounded-b bg-white shadow-lg', + !isOpen && 'hidden', + )} + style={widthStyle} + {...getMenuProps()} + > + {isOpen && + options.map((item, index) => ( + <li + className={twMerge( + 'border-t', + highlightedIndex === index && 'text-primary-500', + selectedItem?.id === item.id && 'font-bold', + 'flex flex-col p-2 shadow-sm', + )} + key={item.id} + {...getItemProps({ item, index })} + > + <span>{item.name}</span> + </li> + ))} + </ul> + </div> + ); +}; diff --git a/src/shared/Select/index.tsx b/src/shared/Select/index.tsx new file mode 100644 index 00000000..3be3db6b --- /dev/null +++ b/src/shared/Select/index.tsx @@ -0,0 +1 @@ +export { Select } from './Select.component'; diff --git a/src/utils/map/useSetBounds.test.ts b/src/utils/map/useSetBounds.test.ts index 71ee1260..fde34efd 100644 --- a/src/utils/map/useSetBounds.test.ts +++ b/src/utils/map/useSetBounds.test.ts @@ -4,6 +4,7 @@ import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants'; import { renderHook } from '@testing-library/react'; import { Map } from 'ol'; import { Coordinate } from 'ol/coordinate'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { getReduxWrapperWithStore } from '../testing/getReduxWrapperWithStore'; import { useSetBounds } from './useSetBounds'; @@ -34,6 +35,7 @@ describe('useSetBounds - hook', () => { message: '', }, openedMaps: [], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }, { @@ -79,6 +81,7 @@ describe('useSetBounds - hook', () => { message: '', }, openedMaps: [], + backgroundType: MapBackgroundsEnum.SEMANTIC, }, }, { diff --git a/src/utils/query-manager/useReduxBusQueryManager.test.ts b/src/utils/query-manager/useReduxBusQueryManager.test.ts index adecf259..e71069d6 100644 --- a/src/utils/query-manager/useReduxBusQueryManager.test.ts +++ b/src/utils/query-manager/useReduxBusQueryManager.test.ts @@ -2,6 +2,7 @@ import { MAP_DATA_INITIAL_STATE, OPENED_MAPS_INITIAL_STATE } from '@/redux/map/m import { Loading } from '@/types/loadingState'; import { renderHook, waitFor } from '@testing-library/react'; import mockRouter from 'next-router-mock'; +import MapBackgroundsEnum from '@/redux/map/map.enums'; import { getReduxWrapperWithStore } from '../testing/getReduxWrapperWithStore'; import { useReduxBusQueryManager } from './useReduxBusQueryManager'; @@ -70,6 +71,7 @@ describe('useReduxBusQueryManager - util', () => { }, }, openedMaps: OPENED_MAPS_INITIAL_STATE, + backgroundType: MapBackgroundsEnum.SEMANTIC, }, backgrounds: loadedDataMock, models: loadedDataMock, -- GitLab From 749885943c387b595136cb301cf0b83e77eafabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 25 Nov 2024 14:49:46 +0100 Subject: [PATCH 21/29] feat(vector-map): add text for filled compartments in a semantic view --- .../reactionsLayer/processModelElements.ts | 4 + .../reactionsLayer/useOlMapReactionsLayer.ts | 5 +- .../shapes/coords/findLargestExtent.test.ts | 16 +++ .../utils/shapes/coords/findLargestExtent.ts | 16 +++ .../shapes/coords/getDividedExtents.test.ts | 74 ++++++++++++ .../utils/shapes/coords/getDividedExtents.ts | 51 ++++++++ .../utils/shapes/elements/BaseMultiPolygon.ts | 84 ++++++------- .../utils/shapes/elements/Compartment.ts | 4 + .../shapes/elements/CompartmentCircle.test.ts | 8 ++ .../shapes/elements/CompartmentCircle.ts | 4 + .../elements/CompartmentPathway.test.ts | 8 ++ .../shapes/elements/CompartmentPathway.ts | 4 + .../shapes/elements/CompartmentSquare.test.ts | 8 ++ .../shapes/elements/CompartmentSquare.ts | 4 + .../utils/shapes/elements/MapElement.test.ts | 8 ++ .../utils/shapes/elements/MapElement.ts | 4 + .../elements/handleSemanticView.test.ts | 112 ++++++++++++++++++ .../shapes/elements/handleSemanticView.ts | 68 +++++++++++ .../utils/shapes/style/getCoverStyles.test.ts | 88 ++++++++++++++ .../utils/shapes/style/getCoverStyles.ts | 50 ++++++++ .../utils/shapes/text/getTextStyle.ts | 2 +- .../text/getWrappedTextWithFontSize.test.ts | 49 ++++++++ .../shapes/text/getWrappedTextWithFontSize.ts | 59 +++++++++ 23 files changed, 686 insertions(+), 44 deletions(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index 357350e5..2063c53c 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -15,6 +15,7 @@ import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; import { OverlayBioEntityRender } from '@/types/OLrendering'; import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export default function processModelElements( modelElements: ModelElements, @@ -27,6 +28,7 @@ export default function processModelElements( mapInstance: MapInstance, pointToProjection: UsePointToProjectionResult, mapBackgroundType: number, + mapSize: MapSize, ): Array<MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph> { const validElements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph @@ -75,6 +77,7 @@ export default function processModelElements( mapInstance, vectorSource, mapBackgroundType, + mapSize, }; if (element.shape === 'OVAL_COMPARTMENT') { validElements.push(new CompartmentCircle(compartmentProps)); @@ -124,6 +127,7 @@ export default function processModelElements( overlaysOrder, getOverlayColor, mapBackgroundType, + mapSize, }), ); } 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 a6c6253e..4e289297 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -38,7 +38,7 @@ import { parseSurfaceMarkersToBioEntityRender } from '@/components/Map/MapViewer import MarkerOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay'; import processModelElements from '@/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements'; import useDebouncedValue from '@/utils/useDebouncedValue'; -import { mapBackgroundTypeSelector } from '@/redux/map/map.selectors'; +import { mapBackgroundTypeSelector, mapDataSizeSelector } from '@/redux/map/map.selectors'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -51,6 +51,7 @@ export const useOlMapReactionsLayer = ({ const modelElements = useSelector(modelElementsSelector); const modelReactions = useSelector(newReactionsDataSelector); const shapes = useSelector(bioShapesSelector); + const mapSize = useSelector(mapDataSizeSelector); const lineTypes = useSelector(lineTypesSelector); const arrowTypes = useSelector(arrowTypesSelector); const overlaysOrder = useSelector(getOverlayOrderSelector); @@ -159,6 +160,7 @@ export const useOlMapReactionsLayer = ({ mapInstance, pointToProjection, mapBackgroundType, + mapSize, ); }, [ modelElements, @@ -171,6 +173,7 @@ export const useOlMapReactionsLayer = ({ mapInstance, pointToProjection, mapBackgroundType, + mapSize, ]); const features = useMemo(() => { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.test.ts new file mode 100644 index 00000000..2a41ebad --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.test.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-magic-numbers */ + +import findLargestExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent'; +import { Extent } from 'ol/extent'; + +describe('findLargestExtent', () => { + it('should find largest extent from a given array', () => { + const extents: Array<Extent> = [ + [100, 100, 200, 200], + [150, 200, 400, 500], + ]; + + const result = findLargestExtent(extents); + expect(result).toEqual(extents[1]); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.ts new file mode 100644 index 00000000..f013e03c --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.ts @@ -0,0 +1,16 @@ +import { Extent, getArea } from 'ol/extent'; + +export default function findLargestExtent(extents: Extent[]): Extent | null { + let largestExtent = null; + let maxArea = 0; + + extents.forEach(extent => { + const area = getArea(extent); + if (area > maxArea) { + maxArea = area; + largestExtent = extent; + } + }); + + return largestExtent; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.test.ts new file mode 100644 index 00000000..ea4a9f89 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.test.ts @@ -0,0 +1,74 @@ +/* eslint-disable no-magic-numbers */ +import { Extent } from 'ol/extent'; +import getDividedExtents from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents'; + +describe('getDividedExtents', () => { + it('should return original extents if there is no intersection with dividingExtent', () => { + const extentsArray: Array<Extent> = [ + [0, 0, 10, 10], + [20, 20, 30, 30], + ]; + const dividingExtent: Extent = [15, 15, 18, 18]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + expect(result).toEqual(extentsArray); + }); + + it('should divide extent when it intersects with dividingExtent', () => { + const extentsArray: Array<Extent> = [[0, 0, 20, 20]]; + const dividingExtent: Extent = [10, 10, 15, 15]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + const expected = [ + [0, 0, 10, 20], + [15, 0, 20, 20], + [0, 15, 20, 20], + [0, 0, 20, 10], + ]; + + expect(result).toEqual(expected); + }); + + it('should return a mix of original and divided extents when some extents intersect and others do not', () => { + const extentsArray: Array<Extent> = [ + [0, 0, 20, 20], + [25, 25, 30, 30], + ]; + const dividingExtent: Extent = [10, 10, 15, 15]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + const expected = [ + [0, 0, 10, 20], + [15, 0, 20, 20], + [0, 15, 20, 20], + [0, 0, 20, 10], + [25, 25, 30, 30], + ]; + + expect(result).toEqual(expected); + }); + + it('should handle case where dividingExtent completely overlaps an extent', () => { + const extentsArray: Array<Extent> = [[10, 10, 20, 20]]; + const dividingExtent: Extent = [10, 10, 20, 20]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + expect(result).toEqual([]); + }); + + it('should handle case where extents are completely outside dividingExtent', () => { + const extentsArray: Array<Extent> = [ + [0, 0, 5, 5], + [25, 25, 30, 30], + ]; + const dividingExtent: Extent = [10, 10, 20, 20]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + expect(result).toEqual(extentsArray); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.ts new file mode 100644 index 00000000..09b36084 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-magic-numbers */ +import { Extent } from 'ol/extent'; + +export default function getDividedExtents( + extentsArray: Array<Extent>, + dividingExtent: Extent, +): Array<Extent> { + let minX1: number; + let minY1: number; + let maxX1: number; + let maxY1: number; + const minX2 = dividingExtent[0]; + const minY2 = dividingExtent[1]; + const maxX2 = dividingExtent[2]; + const maxY2 = dividingExtent[3]; + let leftExtent: Extent; + let rightExtent: Extent; + let bottomExtent: Extent; + let topExtent: Extent; + const dividedExtents: Array<Extent> = []; + + extentsArray.forEach(extent => { + [minX1, minY1, maxX1, maxY1] = [...extent]; + + const intersects = minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; + if (intersects) { + if (minX2 <= minX1 && minY2 <= minY1 && maxX2 >= maxX1 && maxY2 >= maxY1) { + return; + } + if (minX2 > minX1) { + leftExtent = [minX1, minY1, minX2, maxY1]; + dividedExtents.push(leftExtent); + } + if (minX2 < maxX1) { + rightExtent = [maxX2, minY1, maxX1, maxY1]; + dividedExtents.push(rightExtent); + } + if (maxY2 < maxY1) { + topExtent = [minX1, maxY2, maxX1, maxY1]; + dividedExtents.push(topExtent); + } + if (minY2 > minY1) { + bottomExtent = [minX1, minY1, maxX1, minY2]; + dividedExtents.push(bottomExtent); + } + } else { + dividedExtents.push(extent); + } + }); + return dividedExtents; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index a28643fb..fab80515 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -2,7 +2,7 @@ import Polygon from 'ol/geom/Polygon'; import { Style } from 'ol/style'; import Feature, { FeatureLike } from 'ol/Feature'; -import { Geometry, MultiPolygon } from 'ol/geom'; +import { MultiPolygon } from 'ol/geom'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { HorizontalAlign, @@ -19,6 +19,10 @@ import { } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { Extent } from 'ol/extent'; +import { MapSize } from '@/redux/map/map.types'; +import getCoverStyles from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles'; +import handleSemanticView from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView'; export interface BaseMapElementProps { type: string; @@ -45,6 +49,7 @@ export interface BaseMapElementProps { pointToProjection: UsePointToProjectionResult; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; } export default abstract class BaseMultiPolygon { @@ -102,6 +107,8 @@ export default abstract class BaseMultiPolygon { mapBackgroundType: number; + mapSize: MapSize; + constructor({ type, sboTerm, @@ -127,6 +134,7 @@ export default abstract class BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }: BaseMapElementProps) { this.type = type; this.sboTerm = sboTerm; @@ -152,6 +160,7 @@ export default abstract class BaseMultiPolygon { this.pointToProjection = pointToProjection; this.vectorSource = vectorSource; this.mapBackgroundType = mapBackgroundType; + this.mapSize = mapSize; } protected abstract createPolygons(): void; @@ -188,6 +197,7 @@ export default abstract class BaseMultiPolygon { protected drawMultiPolygonFeature(mapInstance: MapInstance): void { this.feature = new Feature({ geometry: new MultiPolygon(this.polygons), + zIndex: this.zIndex, getScale: (resolution: number): number => { const maxZoom = mapInstance?.getView().get('originalMaxZoom'); if (maxZoom) { @@ -226,54 +236,33 @@ export default abstract class BaseMultiPolygon { } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + if (!(feature instanceof Feature)) { + return undefined; + } const styles: Array<Style> = []; const getScale = feature.get('getScale'); - const getMapExtent = feature.get('getMapExtent'); let scale = 1; let cover = false; - let coverRation: number = 1; + let largestExtent: Extent | null; + if (getScale instanceof Function) { scale = getScale(resolution); } let hide = false; if (this.mapBackgroundType === MapBackgroundsEnum.SEMANTIC) { - if (getMapExtent instanceof Function && this.type === 'COMPARTMENT') { - const mapExtent = getMapExtent(resolution); - const featureExtent = feature.getGeometry()?.getExtent(); - if (featureExtent && mapExtent) { - const mapArea = - Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); - const compartmentArea = - Math.abs(featureExtent[2] - featureExtent[0]) * - Math.abs(featureExtent[3] - featureExtent[1]); - coverRation = compartmentArea / mapArea; - if (coverRation < 0.05 && scale < 1) { - cover = true; - } - (feature as Feature).set('filled', cover); - } - } + const semanticViewData = handleSemanticView( + this.vectorSource, + feature, + resolution, + scale, + this.compartmentId, + this.complexId, + ); + cover = semanticViewData.cover; + hide = semanticViewData.hide; + largestExtent = semanticViewData.largestExtent; - let complex: Feature<Geometry> | null; - let compartment: Feature<Geometry> | null; - if (this.complexId) { - complex = this.vectorSource.getFeatureById(this.complexId); - if (complex) { - if (complex.get('hidden')) { - hide = true; - } - } - } - if (this.compartmentId) { - compartment = this.vectorSource.getFeatureById(this.compartmentId); - if (compartment) { - if (compartment.get('filled')) { - hide = true; - } - } - } - (feature as Feature).set('hidden', hide); if (hide) { return undefined; } @@ -290,17 +279,27 @@ export default abstract class BaseMultiPolygon { if (styleGeometry instanceof Polygon) { type = styleGeometry.get('type'); text = styleGeometry.get('text'); - fontSize = styleGeometry.get('fontSize'); + fontSize = styleGeometry.get('fontSize') || 10; lineWidth = styleGeometry.get('lineWidth'); coverStyle = styleGeometry.get('coverStyle'); } + if (cover) { - if (coverStyle) { - coverStyle.setZIndex(this.zIndex + 1000); - styles.push(coverStyle); + if (coverStyle && largestExtent) { + styles.push( + ...getCoverStyles( + coverStyle, + largestExtent, + this.text, + scale, + this.zIndex + 1000, + this.mapSize, + ), + ); } return; } + if ( [MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && scale * fontSize <= 4 @@ -329,6 +328,7 @@ export default abstract class BaseMultiPolygon { } styles.push(clonedStyle); }); + return styles; } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index c61b16d1..a74beed4 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -15,6 +15,7 @@ import { MapInstance } from '@/types/map'; import { Color } from '@/types/models'; import { MAP_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export interface CompartmentProps { id: number; @@ -43,6 +44,7 @@ export interface CompartmentProps { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; } export default abstract class Compartment extends BaseMultiPolygon { @@ -83,6 +85,7 @@ export default abstract class Compartment extends BaseMultiPolygon { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentProps) { super({ type: 'COMPARTMENT', @@ -108,6 +111,7 @@ export default abstract class Compartment extends BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }); this.outerWidth = outerWidth; this.innerWidth = innerWidth; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts index ba61e067..376e0408 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts @@ -19,6 +19,7 @@ import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -68,6 +69,13 @@ describe('CompartmentCircle', () => { mapInstance, vectorSource: new VectorSource(), mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts index 040cc8ee..bd80b095 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts @@ -15,6 +15,7 @@ import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; import { Color } from '@/types/models'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export type CompartmentCircleProps = { id: number; @@ -43,6 +44,7 @@ export type CompartmentCircleProps = { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; }; export default class CompartmentCircle extends Compartment { @@ -73,6 +75,7 @@ export default class CompartmentCircle extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentCircleProps) { super({ id, @@ -101,6 +104,7 @@ export default class CompartmentCircle extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts index 26bf658b..ef3d8cde 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts @@ -19,6 +19,7 @@ import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -66,6 +67,13 @@ describe('CompartmentPathway', () => { mapInstance, vectorSource: new VectorSource(), mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index 0fc13627..92bb522d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -18,6 +18,7 @@ import VectorSource from 'ol/source/Vector'; import { Style } from 'ol/style'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import { MapSize } from '@/redux/map/map.types'; export type CompartmentPathwayProps = { id: number; @@ -44,6 +45,7 @@ export type CompartmentPathwayProps = { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; }; export default class CompartmentPathway extends BaseMultiPolygon { @@ -74,6 +76,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentPathwayProps) { super({ type: 'COMPARTMENT', @@ -99,6 +102,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }); this.outerWidth = outerWidth; this.createPolygons(); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts index ae4148d2..ac2f9d52 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts @@ -18,6 +18,7 @@ import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -66,6 +67,13 @@ describe('CompartmentSquare', () => { mapInstance, vectorSource: new VectorSource(), mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts index 1732c177..f5f1df81 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts @@ -14,6 +14,7 @@ import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; import { Color } from '@/types/models'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export type CompartmentSquareProps = { id: number; @@ -42,6 +43,7 @@ export type CompartmentSquareProps = { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; }; export default class CompartmentSquare extends Compartment { @@ -72,6 +74,7 @@ export default class CompartmentSquare extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentSquareProps) { super({ id, @@ -100,6 +103,7 @@ export default class CompartmentSquare extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts index 1611b7fe..a3c6c679 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts @@ -19,6 +19,7 @@ import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shap import { shapesFixture } from '@/models/fixtures/shapesFixture'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -68,6 +69,13 @@ describe('MapElement', () => { vectorSource: new VectorSource(), getOverlayColor: (): string => '#ffffff', mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index 4a0cd82c..826e5fca 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -29,6 +29,7 @@ import { ZERO } from '@/constants/common'; import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export type MapElementProps = { id: number; @@ -66,6 +67,7 @@ export type MapElementProps = { overlaysOrder?: Array<OverlayOrder>; getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; mapBackgroundType: number; + mapSize: MapSize; }; export default class MapElement extends BaseMultiPolygon { @@ -129,6 +131,7 @@ export default class MapElement extends BaseMultiPolygon { overlaysOrder = [], getOverlayColor, mapBackgroundType, + mapSize, }: MapElementProps) { super({ type: FEATURE_TYPE.ALIAS, @@ -155,6 +158,7 @@ export default class MapElement extends BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }); this.shapes = shapes; this.lineWidth = lineWidth; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.test.ts new file mode 100644 index 00000000..0b7e01e1 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.test.ts @@ -0,0 +1,112 @@ +/* eslint-disable no-magic-numbers */ +import Feature from 'ol/Feature'; +import VectorSource from 'ol/source/Vector'; +import getDividedExtents from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents'; +import findLargestExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent'; +import handleSemanticView from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView'; +import Geometry from 'ol/geom/Geometry'; +import { fromExtent } from 'ol/geom/Polygon'; + +jest.mock('../coords/getDividedExtents'); +jest.mock('../coords/findLargestExtent'); + +describe('handleSemanticView', () => { + let vectorSource: VectorSource; + let feature: Feature; + + beforeEach(() => { + vectorSource = new VectorSource(); + feature = new Feature({ + geometry: fromExtent([0, 0, 100, 100]), + type: 'COMPARTMENT', + zIndex: 1, + getMapExtent: (): Array<number> => [0, 0, 100, 100], + filled: false, + hidden: false, + }); + + const mockGeometry = { + getExtent: jest.fn(() => [2, 0, 10, 10]), + } as unknown as Geometry; + + feature.getGeometry = jest.fn(() => mockGeometry); + }); + + it('should return cover = true, hide = false, and calculate largestExtent when feature meets cover conditions', () => { + jest + .spyOn(vectorSource, 'forEachFeatureIntersectingExtent') + .mockImplementation((_, callback) => { + callback( + new Feature({ + geometry: fromExtent([1, 0, 5, 5]), + hidden: false, + type: 'COMPARTMENT', + zIndex: 123, + filled: true, + getMapExtent: (): Array<number> => [1, 0, 5, 5], + }), + ); + }); + (getDividedExtents as jest.Mock).mockReturnValue([[0, 0, 10, 5]]); + (findLargestExtent as jest.Mock).mockReturnValue([0, 0, 10, 5]); + + const result = handleSemanticView(vectorSource, feature, 1, 0.5, null); + + expect(result).toEqual({ + cover: true, + hide: false, + largestExtent: [0, 0, 10, 5], + }); + + expect(feature.get('filled')).toBe(true); + expect(getDividedExtents).toHaveBeenCalled(); + expect(findLargestExtent).toHaveBeenCalled(); + }); + + it('should return hide = true when complexId points to a hidden feature', () => { + const complexFeature = new Feature({ hidden: true }); + jest + .spyOn(vectorSource, 'getFeatureById') + .mockImplementation(id => (id === 1 ? complexFeature : null)); + + const result = handleSemanticView(vectorSource, feature, 1, 1, null, 1); + + expect(result).toEqual({ + cover: false, + hide: true, + largestExtent: null, + }); + + expect(feature.get('hidden')).toBe(true); + }); + + it('should return hide = true when compartmentId points to a filled feature', () => { + const compartmentFeature = new Feature({ filled: true }); + jest + .spyOn(vectorSource, 'getFeatureById') + .mockImplementation(id => (id === 2 ? compartmentFeature : null)); + + const result = handleSemanticView(vectorSource, feature, 1, 1, 2); + + expect(result).toEqual({ + cover: false, + hide: true, + largestExtent: null, + }); + + expect(feature.get('hidden')).toBe(true); + }); + + it('should return cover = false and hide = false when feature does not meet any conditions', () => { + const result = handleSemanticView(vectorSource, feature, 1, 1, null); + + expect(result).toEqual({ + cover: false, + hide: false, + largestExtent: null, + }); + + expect(feature.get('filled')).toBe(false); + expect(feature.get('hidden')).toBe(false); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts new file mode 100644 index 00000000..83269b75 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-magic-numbers */ +import getDividedExtents from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents'; +import findLargestExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent'; +import Feature from 'ol/Feature'; +import VectorSource from 'ol/source/Vector'; +import { Extent } from 'ol/extent'; + +export default function handleSemanticView( + vectorSource: VectorSource, + feature: Feature, + resolution: number, + scale: number, + compartmentId: number | null, + complexId?: number | null, +): { cover: boolean; hide: boolean; largestExtent: Extent | null } { + const type = feature.get('type'); + const getMapExtent = feature.get('getMapExtent'); + let coverRatio = 1; + let cover = false; + let hide = false; + let largestExtent: Extent | null = null; + if (getMapExtent instanceof Function && type === 'COMPARTMENT') { + const mapExtent = getMapExtent(resolution); + const featureExtent = feature.getGeometry()?.getExtent(); + if (featureExtent && mapExtent) { + const mapArea = Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); + const compartmentArea = + Math.abs(featureExtent[2] - featureExtent[0]) * + Math.abs(featureExtent[3] - featureExtent[1]); + coverRatio = compartmentArea / mapArea; + if (coverRatio < 0.05 && scale < 1) { + cover = true; + let remainingExtents = [featureExtent]; + vectorSource.forEachFeatureIntersectingExtent(featureExtent, intersectingFeature => { + if ( + !intersectingFeature.get('hidden') && + intersectingFeature.get('type') === 'COMPARTMENT' && + intersectingFeature.get('zIndex') > feature.get('zIndex') && + intersectingFeature.get('filled') + ) { + const intersectingFeatureExtent = intersectingFeature.getGeometry()?.getExtent(); + if (intersectingFeatureExtent) { + remainingExtents = getDividedExtents(remainingExtents, intersectingFeatureExtent); + } + } + }); + largestExtent = findLargestExtent(remainingExtents) || featureExtent; + } + (feature as Feature).set('filled', cover); + } + } + + if (complexId) { + const complex = vectorSource.getFeatureById(complexId); + if (complex && complex.get('hidden')) { + hide = true; + } + } + if (compartmentId) { + const compartment = vectorSource.getFeatureById(compartmentId); + if (compartment && compartment.get('filled')) { + hide = true; + } + } + (feature as Feature).set('hidden', hide); + + return { cover, hide, largestExtent }; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.test.ts new file mode 100644 index 00000000..0e22293c --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.test.ts @@ -0,0 +1,88 @@ +/* eslint-disable no-magic-numbers */ +import Style from 'ol/style/Style'; +import { Extent } from 'ol/extent'; +import getWrappedTextWithFontSize from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; +import getCoverStyles from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; + +jest.mock('../text/getWrappedTextWithFontSize'); +jest.mock('../text/getTextStyle'); +jest.mock('../../../../../../../utils/map/latLngToPoint'); + +describe('getCoverStyles', () => { + it('should return cover and text styles based on the provided parameters', () => { + const coverStyle = new Style(); + const largestExtent: Extent = [10, 10, 50, 50]; + const text = 'Sample Text'; + const scale = 1; + const zIndex = 5; + const mapSize = { + width: 1000, + height: 800, + minZoom: 1, + maxZoom: 9, + tileSize: DEFAULT_TILE_SIZE, + }; + + (latLngToPoint as jest.Mock).mockImplementation(([lat, lng], size) => ({ + x: (lng * size.width) / 100, + y: (lat * size.height) / 100, + })); + + (getWrappedTextWithFontSize as jest.Mock).mockReturnValue({ + text: 'Sample\nText', + fontSize: 12, + }); + + const mockTextStyle = new Style(); + (getTextStyle as jest.Mock).mockReturnValue(mockTextStyle); + + const result = getCoverStyles(coverStyle, largestExtent, text, scale, zIndex, mapSize); + + expect(result).toHaveLength(2); + expect(result[0]).toBe(coverStyle); + expect(coverStyle.getZIndex()).toBe(zIndex); + + expect(result[1]).toBe(mockTextStyle); + expect(getWrappedTextWithFontSize).toHaveBeenCalledWith({ + text, + maxWidth: expect.any(Number), + maxHeight: expect.any(Number), + }); + + expect(getTextStyle).toHaveBeenCalledWith({ + text: 'Sample\nText', + fontSize: 12, + color: '#000', + zIndex, + horizontalAlign: 'CENTER', + }); + }); + + it('should handle empty text gracefully', () => { + const coverStyle = new Style(); + const largestExtent: Extent = [10, 10, 50, 50]; + const text = ''; + const scale = 1; + const zIndex = 5; + const mapSize = { + width: 1000, + height: 800, + minZoom: 1, + maxZoom: 9, + tileSize: DEFAULT_TILE_SIZE, + }; + + (getWrappedTextWithFontSize as jest.Mock).mockReturnValue({ + text: '', + fontSize: 0, + }); + + const result = getCoverStyles(coverStyle, largestExtent, text, scale, zIndex, mapSize); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(coverStyle); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.ts new file mode 100644 index 00000000..ce072c86 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-magic-numbers */ +import Style from 'ol/style/Style'; +import { Extent, getCenter } from 'ol/extent'; +import { toLonLat } from 'ol/proj'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; +import getWrappedTextWithFontSize from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize'; +import { Point } from 'ol/geom'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import { MapSize } from '@/redux/map/map.types'; + +export default function getCoverStyles( + coverStyle: Style, + largestExtent: Extent, + text: string, + scale: number, + zIndex: number, + mapSize: MapSize, +): Array<Style> { + const styles: Array<Style> = []; + coverStyle.setZIndex(zIndex); + styles.push(coverStyle); + + if (text) { + const [lng1, lat1] = toLonLat([largestExtent[0], largestExtent[1]]); + const [lng2, lat2] = toLonLat([largestExtent[2], largestExtent[3]]); + const point1 = latLngToPoint([lat1, lng1], mapSize); + const point2 = latLngToPoint([lat2, lng2], mapSize); + const maxWidth = point2.x - point1.x; + const maxHeight = Math.abs(Math.abs(point2.y) - Math.abs(point1.y)); + const { text: brokenText, fontSize: calculatedFontSize } = getWrappedTextWithFontSize({ + text, + maxWidth: maxWidth * scale * 0.9, + maxHeight: maxHeight * scale * 0.9, + }); + const center = getCenter(largestExtent); + const textGeometry = new Point([center[0], center[1]]); + + const textStyle = getTextStyle({ + text: brokenText.trim(), + fontSize: calculatedFontSize, + color: '#000', + zIndex, + horizontalAlign: 'CENTER', + }); + textStyle.setGeometry(textGeometry); + styles.push(textStyle); + } + + return styles; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts index 4858fb21..94ab9f92 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts @@ -8,7 +8,7 @@ export default function getTextStyle({ color, zIndex, horizontalAlign, - overflow = true, + overflow = false, }: { text: string; fontSize: number; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.test.ts new file mode 100644 index 00000000..2cfaa243 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.test.ts @@ -0,0 +1,49 @@ +/* eslint-disable no-magic-numbers */ +import getWrappedTextWithFontSize from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize'; + +describe('getWrappedTextWithFontSize', () => { + it('should return a wrapped text and font size for this text when maxWidth is limited and maxHeight is unlimited', () => { + const text = 'Wrapped text with font size test'; + const maxWidth = 15; + const maxHeight = 9999; + + const { text: wrappedText, fontSize } = getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + }); + + expect(wrappedText.trim()).toEqual('Wrapped text\nwith font size\ntest'); + expect(fontSize).toEqual(12); + }); + + it('should return a wrapped text and font size for this text when maxWidth is unlimited and maxHeight is limited', () => { + const text = 'Wrapped text with font size test'; + const maxWidth = 9999; + const maxHeight = 9; + + const { text: wrappedText, fontSize } = getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + }); + + expect(wrappedText.trim()).toEqual('Wrapped text with font size test'); + expect(fontSize).toEqual(4); + }); + + it('should return a wrapped text and font size for this text when maxWidth is limited and maxHeight is limited', () => { + const text = 'Wrapped text with font size test'; + const maxWidth = 20; + const maxHeight = 9; + + const { text: wrappedText, fontSize } = getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + }); + + expect(wrappedText.trim()).toEqual('Wrapped text with\nfont size test'); + expect(fontSize).toEqual(3); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.ts new file mode 100644 index 00000000..9398d90b --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.ts @@ -0,0 +1,59 @@ +/* eslint-disable no-magic-numbers */ +export default function getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + minFontSize = 3, + maxFontSize = 12, +}: { + text: string; + maxWidth: number; + maxHeight: number; + minFontSize?: number; + maxFontSize?: number; +}): { text: string; fontSize: number } { + const result = { + text, + fontSize: maxFontSize, + }; + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) { + return result; + } + + let resultFontSize = maxFontSize; + let resultText = text; + for (let testFontSize = maxFontSize; testFontSize >= minFontSize; testFontSize -= 1) { + context.font = `${testFontSize}px Arial`; + let currentLine = ''; + let splittedText = ''; + resultFontSize = testFontSize; + text.split(' ').forEach((word: string) => { + const testLine = currentLine ? `${currentLine} ${word}` : word; + const testWidth = context.measureText(testLine).width; + + if (testWidth > maxWidth && currentLine) { + splittedText += `\n${currentLine}`; + currentLine = word; + } else { + currentLine = testLine; + } + }); + if (currentLine) { + splittedText += `\n${currentLine}`; + } + const lines = splittedText.split('\n'); + const maxLineWidth = lines.reduce( + (maxFoundWidth, line) => Math.max(maxFoundWidth, context.measureText(line).width), + 0, + ); + if (maxLineWidth <= maxWidth && testFontSize * lines.length <= maxHeight) { + resultText = splittedText; + break; + } + } + result.text = resultText; + result.fontSize = resultFontSize; + return result; +} -- GitLab From dfb75c2bca0094cea7dc378fafe863b3290cd27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 25 Nov 2024 15:09:16 +0100 Subject: [PATCH 22/29] fix(vector-map): set default overflow value to true in getTextStyle --- .../MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts index 94ab9f92..4858fb21 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts @@ -8,7 +8,7 @@ export default function getTextStyle({ color, zIndex, horizontalAlign, - overflow = false, + overflow = true, }: { text: string; fontSize: number; -- GitLab From 2b06650833b74b822afb0623f447f85110c9544e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 25 Nov 2024 15:44:12 +0100 Subject: [PATCH 23/29] feat(vector-map): disable elements background color when data overlays are visible --- .../reactionsLayer/processModelElements.ts | 1 + .../utils/shapes/elements/Compartment.ts | 21 +++++++++++++++---- .../shapes/elements/CompartmentCircle.test.ts | 1 + .../shapes/elements/CompartmentCircle.ts | 3 +++ .../elements/CompartmentPathway.test.ts | 1 + .../shapes/elements/CompartmentPathway.ts | 7 ++++++- .../shapes/elements/CompartmentSquare.test.ts | 1 + .../shapes/elements/CompartmentSquare.ts | 3 +++ .../utils/shapes/elements/MapElement.ts | 2 +- 9 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index 2063c53c..51afc92f 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -73,6 +73,7 @@ export default function processModelElements( nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, text: element.name, fontSize: element.fontSize, + overlaysVisible: Boolean(overlaysOrder.length), pointToProjection, mapInstance, vectorSource, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index a74beed4..84bbba02 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -40,6 +40,7 @@ export interface CompartmentProps { nameHorizontalAlign: HorizontalAlign; fillColor: Color; borderColor: Color; + overlaysVisible: boolean; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; @@ -58,6 +59,8 @@ export default abstract class Compartment extends BaseMultiPolygon { thickness: number; + overlaysVisible: boolean; + constructor({ id, complexId, @@ -81,6 +84,7 @@ export default abstract class Compartment extends BaseMultiPolygon { nameHorizontalAlign, fillColor, borderColor, + overlaysVisible, pointToProjection, mapInstance, vectorSource, @@ -116,6 +120,7 @@ export default abstract class Compartment extends BaseMultiPolygon { this.outerWidth = outerWidth; this.innerWidth = innerWidth; this.thickness = thickness; + this.overlaysVisible = overlaysVisible; this.getCompartmentCoords(); this.createPolygons(); this.drawText(); @@ -142,7 +147,9 @@ export default abstract class Compartment extends BaseMultiPolygon { this.styles.push( new Style({ geometry: framePolygon, - fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 128 }) }), + fill: this.overlaysVisible + ? undefined + : getFill({ color: rgbToHex({ ...this.fillColor, alpha: 128 }) }), zIndex: this.zIndex, }), ); @@ -154,7 +161,9 @@ export default abstract class Compartment extends BaseMultiPolygon { this.styles.push( new Style({ geometry: outerPolygon, - stroke: getStroke({ color: rgbToHex(this.borderColor), width: this.outerWidth }), + stroke: this.overlaysVisible + ? getStroke({ width: this.outerWidth }) + : getStroke({ color: rgbToHex(this.borderColor), width: this.outerWidth }), zIndex: this.zIndex, }), ); @@ -166,8 +175,12 @@ export default abstract class Compartment extends BaseMultiPolygon { this.styles.push( new Style({ geometry: innerPolygon, - stroke: getStroke({ color: rgbToHex(this.borderColor), width: this.innerWidth }), - fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 9 }) }), + stroke: this.overlaysVisible + ? getStroke({ width: this.innerWidth }) + : getStroke({ color: rgbToHex(this.borderColor), width: this.innerWidth }), + fill: this.overlaysVisible + ? undefined + : getFill({ color: rgbToHex({ ...this.fillColor, alpha: 9 }) }), zIndex: this.zIndex, }), ); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts index 376e0408..876dd762 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts @@ -65,6 +65,7 @@ describe('CompartmentCircle', () => { nameWidth: 40, nameVerticalAlign: 'MIDDLE', nameHorizontalAlign: 'CENTER', + overlaysVisible: false, pointToProjection: jest.fn(), mapInstance, vectorSource: new VectorSource(), diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts index bd80b095..3dd38183 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts @@ -40,6 +40,7 @@ export type CompartmentCircleProps = { nameWidth: number; nameVerticalAlign?: VerticalAlign; nameHorizontalAlign?: HorizontalAlign; + overlaysVisible: boolean; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; @@ -71,6 +72,7 @@ export default class CompartmentCircle extends Compartment { nameWidth, nameVerticalAlign = 'MIDDLE', nameHorizontalAlign = 'CENTER', + overlaysVisible, pointToProjection, mapInstance, vectorSource, @@ -100,6 +102,7 @@ export default class CompartmentCircle extends Compartment { nameHorizontalAlign, fillColor, borderColor, + overlaysVisible, pointToProjection, mapInstance, vectorSource, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts index ef3d8cde..5e1df810 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts @@ -63,6 +63,7 @@ describe('CompartmentPathway', () => { nameWidth: 40, nameVerticalAlign: 'MIDDLE', nameHorizontalAlign: 'CENTER', + overlaysVisible: false, pointToProjection: jest.fn(() => [10, 10]), mapInstance, vectorSource: new VectorSource(), diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index 92bb522d..3c9f586a 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -41,6 +41,7 @@ export type CompartmentPathwayProps = { nameWidth: number; nameVerticalAlign?: VerticalAlign; nameHorizontalAlign?: HorizontalAlign; + overlaysVisible: boolean; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; @@ -51,6 +52,8 @@ export type CompartmentPathwayProps = { export default class CompartmentPathway extends BaseMultiPolygon { outerWidth: number; + overlaysVisible: boolean; + constructor({ id, complexId, @@ -72,6 +75,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { nameWidth, nameVerticalAlign = 'MIDDLE', nameHorizontalAlign = 'CENTER', + overlaysVisible, pointToProjection, mapInstance, vectorSource, @@ -105,6 +109,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { mapSize, }); this.outerWidth = outerWidth; + this.overlaysVisible = overlaysVisible; this.createPolygons(); this.drawText(); this.drawMultiPolygonFeature(mapInstance); @@ -131,7 +136,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { getStyle({ geometry: compartmentPolygon, borderColor: this.borderColor, - fillColor: { ...this.fillColor, alpha: 9 }, + fillColor: this.overlaysVisible ? undefined : { ...this.fillColor, alpha: 9 }, lineWidth: this.outerWidth, zIndex: this.zIndex, }), diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts index ac2f9d52..c7721148 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts @@ -63,6 +63,7 @@ describe('CompartmentSquare', () => { nameWidth: 40, nameVerticalAlign: 'MIDDLE', nameHorizontalAlign: 'CENTER', + overlaysVisible: false, pointToProjection: jest.fn(), mapInstance, vectorSource: new VectorSource(), diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts index f5f1df81..b4905d61 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts @@ -39,6 +39,7 @@ export type CompartmentSquareProps = { nameWidth: number; nameVerticalAlign?: VerticalAlign; nameHorizontalAlign?: HorizontalAlign; + overlaysVisible: boolean; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; vectorSource: VectorSource; @@ -70,6 +71,7 @@ export default class CompartmentSquare extends Compartment { nameWidth, nameVerticalAlign = 'MIDDLE', nameHorizontalAlign = 'CENTER', + overlaysVisible, pointToProjection, mapInstance, vectorSource, @@ -99,6 +101,7 @@ export default class CompartmentSquare extends Compartment { nameHorizontalAlign, fillColor, borderColor, + overlaysVisible, pointToProjection, mapInstance, vectorSource, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index 826e5fca..fc005145 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -299,7 +299,7 @@ export default class MapElement extends BaseMultiPolygon { const elementStyle = getStyle({ geometry: elementPolygon, borderColor: this.borderColor, - fillColor: this.overlays.length ? undefined : this.fillColor, + fillColor: this.overlaysOrder.length ? undefined : this.fillColor, lineWidth: this.lineWidth, lineDash: this.lineDash, zIndex: this.zIndex, -- GitLab From a8371323e45a268a2f46e4509953c5c24609f45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 26 Nov 2024 10:58:52 +0100 Subject: [PATCH 24/29] fix(vector-map): set zoom timeout for correct refresh of styles --- .../MapViewerVector/MapViewerVector.types.ts | 2 ++ .../reactionsLayer/useOlMapReactionsLayer.ts | 2 ++ .../utils/shapes/elements/BaseMultiPolygon.ts | 31 ++++++++++++++----- .../Map/MapViewer/utils/useOlMap.ts | 4 +-- src/constants/map.ts | 2 +- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts index e7ccb7c9..63fddd60 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts @@ -10,6 +10,8 @@ export type MapConfig = { export type VerticalAlign = 'TOP' | 'MIDDLE' | 'BOTTOM'; export type HorizontalAlign = 'LEFT' | 'RIGHT' | 'CENTER' | 'END' | 'START'; +export type ScaleFunction = (resolution: number) => number; + export type OverlayBioEntityGroupedElementsType = { [id: string]: Array<OverlayBioEntityRender & { amount: number }>; }; 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 4e289297..658e2a5d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -195,6 +195,8 @@ export const useOlMapReactionsLayer = ({ return useMemo(() => { const vectorLayer = new VectorLayer({ source: vectorSource, + updateWhileAnimating: true, + updateWhileInteracting: true, }); vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE); return vectorLayer; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index fab80515..1960681d 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -6,6 +6,7 @@ import { MultiPolygon } from 'ol/geom'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { HorizontalAlign, + ScaleFunction, VerticalAlign, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; import { MapInstance } from '@/types/map'; @@ -109,6 +110,11 @@ export default abstract class BaseMultiPolygon { mapSize: MapSize; + mapExtentCache: Map<number, [number, number, number, number]> = new Map< + number, + [number, number, number, number] + >(); + constructor({ type, sboTerm, @@ -198,17 +204,24 @@ export default abstract class BaseMultiPolygon { this.feature = new Feature({ geometry: new MultiPolygon(this.polygons), zIndex: this.zIndex, - getScale: (resolution: number): number => { + getScale: ((): ScaleFunction => { const maxZoom = mapInstance?.getView().get('originalMaxZoom'); - if (maxZoom) { - const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); + const minResolution = maxZoom + ? mapInstance?.getView().getResolutionForZoom(maxZoom) + : undefined; + + return (resolution: number): number => { if (minResolution) { return minResolution / resolution; } - } - return 1; - }, + return 1; + }; + })(), getMapExtent: (resolution: number): [number, number, number, number] | undefined => { + if (this.mapExtentCache.has(resolution)) { + return this.mapExtentCache.get(resolution); + } + const view = mapInstance?.getView(); const center = view?.getCenter(); const size = mapInstance?.getSize(); @@ -216,15 +229,19 @@ export default abstract class BaseMultiPolygon { if (!size || !center) { return undefined; } + const extentWidth = size[0] * resolution; const extentHeight = size[1] * resolution; - return [ + const extent: [number, number, number, number] = [ center[0] - extentWidth / 2, center[1] - extentHeight / 2, center[0] + extentWidth / 2, center[1] + extentHeight / 2, ]; + + this.mapExtentCache.set(resolution, extent); + return extent; }, id: this.id, complexId: this.complexId, diff --git a/src/components/Map/MapViewer/utils/useOlMap.ts b/src/components/Map/MapViewer/utils/useOlMap.ts index de917a95..68ec65bd 100644 --- a/src/components/Map/MapViewer/utils/useOlMap.ts +++ b/src/components/Map/MapViewer/utils/useOlMap.ts @@ -65,8 +65,8 @@ export const useOlMap: UseOlMap = ({ target } = {}) => { mouseWheelZoom: false, }).extend([ new MouseWheelZoom({ - duration: 0, - timeout: 20, + duration: 250, + timeout: 80, }), ]), target: target || mapRef.current, diff --git a/src/constants/map.ts b/src/constants/map.ts index 07775a81..5856c87c 100644 --- a/src/constants/map.ts +++ b/src/constants/map.ts @@ -12,7 +12,7 @@ export const DEFAULT_CENTER_Y = 0; export const LATLNG_FALLBACK: LatLng = [0, 0]; export const EXTENT_PADDING_MULTIPLICATOR = 1; -export const ZOOM_RESCALING_FACTOR = 3; +export const ZOOM_RESCALING_FACTOR = 2; export const DEFAULT_CENTER_POINT: Point = { x: DEFAULT_CENTER_X, -- GitLab From 73b83a8417738faa16df9e20466ed540beae6afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Wed, 27 Nov 2024 13:11:20 +0100 Subject: [PATCH 25/29] refactor(vector-map): modify redux to store element of multiple models --- .../listeners/useOlMapVectorListeners.ts | 20 +++--- .../reactionsLayer/processModelElements.ts | 6 +- .../reactionsLayer/useOlMapReactionsLayer.ts | 63 ++++++++++++++----- .../MapViewer/utils/config/useOlMapView.ts | 11 +--- src/redux/apiPath.ts | 6 +- src/redux/modelElements/modelElements.mock.ts | 2 +- .../modelElements.reducers.test.ts | 34 +++++----- .../modelElements/modelElements.reducers.ts | 17 +++-- .../modelElements/modelElements.selector.ts | 17 +++-- .../modelElements/modelElements.slice.ts | 2 +- .../modelElements.thunks.test.ts | 14 ++--- .../modelElements/modelElements.thunks.ts | 34 +++++----- .../modelElements/modelElements.types.ts | 6 +- .../newReactions/newReactions.constants.ts | 2 +- src/redux/newReactions/newReactions.mock.ts | 2 +- .../newReactions.reducers.test.ts | 32 +++++----- .../newReactions/newReactions.reducers.ts | 17 +++-- .../newReactions/newReactions.selectors.ts | 9 ++- .../newReactions/newReactions.thunks.test.ts | 10 +-- src/redux/newReactions/newReactions.thunks.ts | 8 ++- src/redux/newReactions/newReactions.types.ts | 4 +- 21 files changed, 190 insertions(+), 126 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts index 05993a81..444cf47b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts @@ -14,8 +14,8 @@ 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'; -import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors'; +import { modelElementsForCurrentModelSelector } from '@/redux/modelElements/modelElements.selector'; +import { newReactionsForCurrentModelSelector } from '@/redux/newReactions/newReactions.selectors'; interface UseOlMapVectorListenersInput { mapInstance: MapInstance; @@ -25,8 +25,8 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListeners const mapSize = useSelector(mapDataSizeSelector); const modelId = useSelector(currentModelIdSelector); const isResultDrawerOpen = useSelector(resultDrawerOpen); - const modelElements = useSelector(modelElementsSelector); - const reactions = useSelector(newReactionsDataSelector); + const modelElementsForCurrentModel = useSelector(modelElementsForCurrentModelSelector); + const newReactionsForCurrentModel = useSelector(newReactionsForCurrentModelSelector); const dispatch = useAppDispatch(); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); @@ -41,15 +41,21 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListeners dispatch, isResultDrawerOpen, comments, - modelElements?.content || [], - reactions, + modelElementsForCurrentModel || [], + newReactionsForCurrentModel, ), OPTIONS.clickPersistTime, { leading: false }, ); const handleRightClick = useDebouncedCallback( - onMapRightClick(mapSize, modelId, dispatch, modelElements?.content || [], reactions), + onMapRightClick( + mapSize, + modelId, + dispatch, + modelElementsForCurrentModel || [], + newReactionsForCurrentModel, + ), OPTIONS.clickPersistTime, { leading: false, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index 51afc92f..8deeb01e 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -1,4 +1,4 @@ -import { ModelElement, ModelElements } from '@/types/models'; +import { ModelElement } from '@/types/models'; import MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement'; import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; @@ -18,7 +18,7 @@ import VectorSource from 'ol/source/Vector'; import { MapSize } from '@/redux/map/map.types'; export default function processModelElements( - modelElements: ModelElements, + modelElements: Array<ModelElement>, shapes: BioShapesDict, lineTypes: LineTypeDict, groupedElementsOverlays: Record<string, Array<OverlayBioEntityRender>>, @@ -33,7 +33,7 @@ export default function processModelElements( const validElements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph > = []; - modelElements.content.forEach((element: ModelElement) => { + modelElements.forEach((element: ModelElement) => { if (element.glyph) { const glyph = new Glyph({ id: element.glyph.id, 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 658e2a5d..f28cc4bc 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -12,17 +12,17 @@ import { lineTypesSelector, } from '@/redux/shapes/shapes.selectors'; import { MapInstance } from '@/types/map'; -import { modelElementsSelector } from '@/redux/modelElements/modelElements.selector'; +import { modelElementsForCurrentModelSelector } from '@/redux/modelElements/modelElements.selector'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; 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 { newReactionsForCurrentModelSelector } from '@/redux/newReactions/newReactions.selectors'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import { getOverlayOrderSelector, @@ -39,6 +39,7 @@ import MarkerOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shap import processModelElements from '@/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements'; import useDebouncedValue from '@/utils/useDebouncedValue'; import { mapBackgroundTypeSelector, mapDataSizeSelector } from '@/redux/map/map.selectors'; +import { ZOOM_RESCALING_FACTOR } from '@/constants/map'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -48,8 +49,6 @@ export const useOlMapReactionsLayer = ({ const dispatch = useAppDispatch(); const currentModelId = useSelector(currentModelIdSelector); - const modelElements = useSelector(modelElementsSelector); - const modelReactions = useSelector(newReactionsDataSelector); const shapes = useSelector(bioShapesSelector); const mapSize = useSelector(mapDataSizeSelector); const lineTypes = useSelector(lineTypesSelector); @@ -59,6 +58,8 @@ export const useOlMapReactionsLayer = ({ const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); + const reactionsForCurrentModel = useAppSelector(newReactionsForCurrentModelSelector); + const modelElementsForCurrentModel = useAppSelector(modelElementsForCurrentModelSelector); const debouncedBioEntities = useDebouncedValue(bioEntities, 2000); const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); @@ -67,12 +68,23 @@ export const useOlMapReactionsLayer = ({ const vectorSource = useMemo(() => new VectorSource(), []); + const mapModelOriginalMaxZoom = mapInstance?.getView().get('originalMaxZoom'); + + const isCorrectMapInstanceViewScale = useMemo(() => { + return mapSize.maxZoom * ZOOM_RESCALING_FACTOR === mapModelOriginalMaxZoom; + }, [mapModelOriginalMaxZoom, mapSize.maxZoom]); + useEffect(() => { - if (currentModelId) { - dispatch(getModelElements(currentModelId)); - dispatch(getNewReactions(currentModelId)); + if (!currentModelId) { + return; + } + if (!modelElementsForCurrentModel) { + dispatch(getModelElementsForModel(currentModelId)); } - }, [currentModelId, dispatch]); + if (!reactionsForCurrentModel) { + dispatch(getNewReactionsForModel(currentModelId)); + } + }, [currentModelId, dispatch, reactionsForCurrentModel, modelElementsForCurrentModel]); const groupedElementsOverlays = useMemo(() => { const elementsBioEntitesOverlay = debouncedBioEntities.filter( @@ -87,6 +99,9 @@ export const useOlMapReactionsLayer = ({ }, [bioEntities]); const linesOverlaysFeatures = useMemo(() => { + if (!isCorrectMapInstanceViewScale) { + return []; + } return linesOverlays.map(lineOverlay => { return new LineOverlay({ lineOverlay, @@ -97,12 +112,16 @@ export const useOlMapReactionsLayer = ({ }); }, [ getOverlayBioEntityColorByAvailableProperties, + isCorrectMapInstanceViewScale, linesOverlays, mapInstance, pointToProjection, ]); const markerOverlaysFeatures = useMemo(() => { + if (!isCorrectMapInstanceViewScale) { + return []; + } return markersRender.map(marker => { return new MarkerOverlay({ markerOverlay: marker, @@ -113,13 +132,17 @@ export const useOlMapReactionsLayer = ({ }); }, [ getOverlayBioEntityColorByAvailableProperties, + isCorrectMapInstanceViewScale, mapInstance, markersRender, pointToProjection, ]); const reactions = useMemo(() => { - return modelReactions.map(reaction => { + if (!reactionsForCurrentModel || !isCorrectMapInstanceViewScale) { + return []; + } + return reactionsForCurrentModel.map(reaction => { const reactionShapes = shapes && shapes[reaction.sboTerm]; if (!reactionShapes) { return []; @@ -141,16 +164,25 @@ export const useOlMapReactionsLayer = ({ }); return reactionObject.features; }); - }, [arrowTypes, lineTypes, mapInstance, modelReactions, pointToProjection, shapes, vectorSource]); + }, [ + reactionsForCurrentModel, + isCorrectMapInstanceViewScale, + shapes, + lineTypes, + arrowTypes, + pointToProjection, + vectorSource, + mapInstance, + ]); const elements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph > = useMemo(() => { - if (!modelElements || !shapes) { + if (!modelElementsForCurrentModel || !shapes || !isCorrectMapInstanceViewScale) { return []; } return processModelElements( - modelElements, + modelElementsForCurrentModel, shapes, lineTypes, groupedElementsOverlays, @@ -163,8 +195,9 @@ export const useOlMapReactionsLayer = ({ mapSize, ); }, [ - modelElements, + modelElementsForCurrentModel, shapes, + isCorrectMapInstanceViewScale, lineTypes, groupedElementsOverlays, overlaysOrder, diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.ts index 77d8be74..a639ebd1 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapView.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapView.ts @@ -65,16 +65,7 @@ export const useOlMapView = ({ mapInstance }: UseOlMapViewInput): MapConfig['vie minZoom: mapSize.minZoom * ZOOM_RESCALING_FACTOR, extent, }), - [ - center, - mapInitialPosition.z, - mapSize.width, - mapSize.tileSize, - mapSize.height, - mapSize.maxZoom, - mapSize.minZoom, - extent, - ], + [center, mapInitialPosition.z, mapSize, extent], ); const view = useMemo(() => new View(viewConfig), [viewConfig]); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 5346ef4d..c0686ae3 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -44,7 +44,7 @@ export const apiPath = { getChemicalsStringWithColumnsTarget: (columns: string, target: string): string => `projects/${PROJECT_ID}/chemicals:search?columns=${columns}&target=${target}`, getModelsString: (): string => `projects/${PROJECT_ID}/models/`, - getModelElements: (modelId: number): string => + getModelElementsForModel: (modelId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/elements/?size=10000`, getShapes: (): string => `projects/${PROJECT_ID}/shapes/`, getLineTypes: (): string => `projects/${PROJECT_ID}/lineTypes/`, @@ -60,8 +60,8 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`, getGlyphImage: (glyphId: number): string => `projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`, - getNewReactions: (modelId: number): string => - `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/?size=2000`, + getNewReactionsForModel: (modelId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/?size=10000`, getNewReaction: (modelId: number, reactionId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/${reactionId}`, getChemicalsStringWithQuery: (searchQuery: string): string => diff --git a/src/redux/modelElements/modelElements.mock.ts b/src/redux/modelElements/modelElements.mock.ts index 5ad63a02..0ee98c43 100644 --- a/src/redux/modelElements/modelElements.mock.ts +++ b/src/redux/modelElements/modelElements.mock.ts @@ -2,7 +2,7 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = { - data: null, + data: {}, loading: 'idle', error: DEFAULT_ERROR, }; diff --git a/src/redux/modelElements/modelElements.reducers.test.ts b/src/redux/modelElements/modelElements.reducers.test.ts index d8960722..3554f048 100644 --- a/src/redux/modelElements/modelElements.reducers.test.ts +++ b/src/redux/modelElements/modelElements.reducers.test.ts @@ -9,13 +9,13 @@ import { HttpStatusCode } from 'axios'; import { unwrapResult } from '@reduxjs/toolkit'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import modelElementsReducer from '@/redux/modelElements/modelElements.slice'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); const INITIAL_STATE: ModelElementsState = { - data: null, + data: {}, loading: 'idle', error: { name: '', message: '' }, }; @@ -32,51 +32,51 @@ describe('model elements reducer', () => { expect(modelElementsReducer(undefined, action)).toEqual(INITIAL_STATE); }); - it('should update store after successful getModelElements query', async () => { + it('should update store after successful getModelElementsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, modelElementsFixture); - const { type } = await store.dispatch(getModelElements(0)); + const { type } = await store.dispatch(getModelElementsForModel(0)); const { data, loading, error } = store.getState().modelElements; - expect(type).toBe('vectorMap/getModelElements/fulfilled'); + expect(type).toBe('vectorMap/getModelElementsForModel/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(modelElementsFixture); + expect(data).toEqual({ 0: modelElementsFixture.content }); }); - it('should update store after failed getModelElements query', async () => { - mockedAxiosClient.onGet(apiPath.getModelElements(0)).reply(HttpStatusCode.NotFound, []); + it('should update store after failed getModelElementsForModel query', async () => { + mockedAxiosClient.onGet(apiPath.getModelElementsForModel(0)).reply(HttpStatusCode.NotFound, []); - const action = await store.dispatch(getModelElements(0)); + const action = await store.dispatch(getModelElementsForModel(0)); const { data, loading, error } = store.getState().modelElements; - expect(action.type).toBe('vectorMap/getModelElements/rejected'); + expect(action.type).toBe('vectorMap/getModelElementsForModel/rejected'); expect(() => unwrapResult(action)).toThrow( "Failed to fetch model elements: The page you're looking for doesn't exist. Please verify the URL and try again.", ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(null); + expect(data).toEqual({}); }); - it('should update store on loading getModelElements query', async () => { + it('should update store on loading getModelElementsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, modelElementsFixture); - const modelElementsPromise = store.dispatch(getModelElements(0)); + const modelElementsPromise = store.dispatch(getModelElementsForModel(0)); const { data, loading } = store.getState().modelElements; - expect(data).toEqual(null); + expect(data).toEqual({}); expect(loading).toEqual('pending'); modelElementsPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().modelElements; - expect(dataPromiseFulfilled).toEqual(modelElementsFixture); + expect(dataPromiseFulfilled).toEqual({ 0: modelElementsFixture.content }); expect(promiseFulfilled).toEqual('succeeded'); }); }); diff --git a/src/redux/modelElements/modelElements.reducers.ts b/src/redux/modelElements/modelElements.reducers.ts index fda618df..15a40c42 100644 --- a/src/redux/modelElements/modelElements.reducers.ts +++ b/src/redux/modelElements/modelElements.reducers.ts @@ -1,18 +1,25 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; export const getModelElementsReducer = ( builder: ActionReducerMapBuilder<ModelElementsState>, ): void => { - builder.addCase(getModelElements.pending, state => { + builder.addCase(getModelElementsForModel.pending, state => { state.loading = 'pending'; }); - builder.addCase(getModelElements.fulfilled, (state, action) => { - state.data = action.payload || null; + builder.addCase(getModelElementsForModel.fulfilled, (state, action) => { + const modelId = action.meta.arg; + if (state.data) { + state.data[modelId] = action.payload || []; + } else { + state.data = { + [modelId]: action.payload || [], + }; + } state.loading = 'succeeded'; }); - builder.addCase(getModelElements.rejected, state => { + builder.addCase(getModelElementsForModel.rejected, state => { state.loading = 'failed'; }); }; diff --git a/src/redux/modelElements/modelElements.selector.ts b/src/redux/modelElements/modelElements.selector.ts index 3a9d8b54..f9a654bf 100644 --- a/src/redux/modelElements/modelElements.selector.ts +++ b/src/redux/modelElements/modelElements.selector.ts @@ -1,12 +1,21 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; -export const modelElementsSelector = createSelector( - rootSelector, - state => state.modelElements.data, -); +export const modelElementsSelector = createSelector(rootSelector, state => state.modelElements); export const modelElementsLoadingSelector = createSelector( rootSelector, state => state.modelElements.loading, ); + +export const modelElementsDataSelector = createSelector( + modelElementsSelector, + modelElements => modelElements.data || {}, +); + +export const modelElementsForCurrentModelSelector = createSelector( + modelElementsDataSelector, + currentModelIdSelector, + (data, currentModelId) => data[currentModelId], +); diff --git a/src/redux/modelElements/modelElements.slice.ts b/src/redux/modelElements/modelElements.slice.ts index b9e7f411..56b5d77a 100644 --- a/src/redux/modelElements/modelElements.slice.ts +++ b/src/redux/modelElements/modelElements.slice.ts @@ -3,7 +3,7 @@ import { getModelElementsReducer } from '@/redux/modelElements/modelElements.red import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; const initialState: ModelElementsState = { - data: null, + data: {}, loading: 'idle', error: { name: '', message: '' }, }; diff --git a/src/redux/modelElements/modelElements.thunks.test.ts b/src/redux/modelElements/modelElements.thunks.test.ts index c5611de7..0ba825c9 100644 --- a/src/redux/modelElements/modelElements.thunks.test.ts +++ b/src/redux/modelElements/modelElements.thunks.test.ts @@ -8,7 +8,7 @@ import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import modelElementsReducer from '@/redux/modelElements/modelElements.slice'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -19,22 +19,22 @@ describe('model elements thunks', () => { store = createStoreInstanceUsingSliceReducer('modelElements', modelElementsReducer); }); - describe('getModelElements', () => { + describe('getModelElementsForModel', () => { it('should return data when data response from API is valid', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, modelElementsFixture); - const { payload } = await store.dispatch(getModelElements(0)); - expect(payload).toEqual(modelElementsFixture); + const { payload } = await store.dispatch(getModelElementsForModel(0)); + expect(payload).toEqual(modelElementsFixture.content); }); it('should return undefined when data response from API is not valid ', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - const { payload } = await store.dispatch(getModelElements(0)); + const { payload } = await store.dispatch(getModelElementsForModel(0)); expect(payload).toEqual(undefined); }); }); diff --git a/src/redux/modelElements/modelElements.thunks.ts b/src/redux/modelElements/modelElements.thunks.ts index 2c7e2f25..210b6560 100644 --- a/src/redux/modelElements/modelElements.thunks.ts +++ b/src/redux/modelElements/modelElements.thunks.ts @@ -4,25 +4,23 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; import { getError } from '@/utils/error-report/getError'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { ModelElements } from '@/types/models'; +import { ModelElement, ModelElements } from '@/types/models'; import { MODEL_ELEMENTS_FETCHING_ERROR_PREFIX } from '@/redux/modelElements/modelElements.constants'; import { modelElementSchema } from '@/models/modelElementSchema'; import { pageableSchema } from '@/models/pageableSchema'; -export const getModelElements = createAsyncThunk<ModelElements | undefined, number, ThunkConfig>( - 'vectorMap/getModelElements', - async (modelId: number) => { - try { - const response = await axiosInstanceNewAPI.get<ModelElements>( - apiPath.getModelElements(modelId), - ); - const isDataValid = validateDataUsingZodSchema( - response.data, - pageableSchema(modelElementSchema), - ); - return isDataValid ? response.data : undefined; - } catch (error) { - return Promise.reject(getError({ error, prefix: MODEL_ELEMENTS_FETCHING_ERROR_PREFIX })); - } - }, -); +export const getModelElementsForModel = createAsyncThunk< + Array<ModelElement> | undefined, + number, + ThunkConfig +>('vectorMap/getModelElementsForModel', async (modelId: number) => { + try { + const { data } = await axiosInstanceNewAPI.get<ModelElements>( + apiPath.getModelElementsForModel(modelId), + ); + const isDataValid = validateDataUsingZodSchema(data, pageableSchema(modelElementSchema)); + return isDataValid ? data.content : undefined; + } catch (error) { + return Promise.reject(getError({ error, prefix: MODEL_ELEMENTS_FETCHING_ERROR_PREFIX })); + } +}); diff --git a/src/redux/modelElements/modelElements.types.ts b/src/redux/modelElements/modelElements.types.ts index 0dfdb426..808f3ace 100644 --- a/src/redux/modelElements/modelElements.types.ts +++ b/src/redux/modelElements/modelElements.types.ts @@ -1,4 +1,6 @@ import { FetchDataState } from '@/types/fetchDataState'; -import { ModelElements } from '@/types/models'; +import { ModelElement } from '@/types/models'; -export type ModelElementsState = FetchDataState<ModelElements, null>; +type ModelElementsRecord = Record<number, Array<ModelElement>>; + +export type ModelElementsState = FetchDataState<ModelElementsRecord>; diff --git a/src/redux/newReactions/newReactions.constants.ts b/src/redux/newReactions/newReactions.constants.ts index 80ea28cc..e30ac5fa 100644 --- a/src/redux/newReactions/newReactions.constants.ts +++ b/src/redux/newReactions/newReactions.constants.ts @@ -2,7 +2,7 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; export const NEW_REACTIONS_INITIAL_STATE: NewReactionsState = { - data: [], + data: {}, loading: 'idle', error: DEFAULT_ERROR, }; diff --git a/src/redux/newReactions/newReactions.mock.ts b/src/redux/newReactions/newReactions.mock.ts index 3847aed6..f9a878ec 100644 --- a/src/redux/newReactions/newReactions.mock.ts +++ b/src/redux/newReactions/newReactions.mock.ts @@ -2,7 +2,7 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; export const NEW_REACTIONS_INITIAL_STATE_MOCK: NewReactionsState = { - data: [], + data: {}, loading: 'idle', error: DEFAULT_ERROR, }; diff --git a/src/redux/newReactions/newReactions.reducers.test.ts b/src/redux/newReactions/newReactions.reducers.test.ts index 21f8d668..206d34a7 100644 --- a/src/redux/newReactions/newReactions.reducers.test.ts +++ b/src/redux/newReactions/newReactions.reducers.test.ts @@ -10,7 +10,7 @@ import { unwrapResult } from '@reduxjs/toolkit'; import newReactionsReducer from '@/redux/newReactions/newReactions.slice'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock'; -import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -29,50 +29,50 @@ describe('newReactions reducer', () => { expect(newReactionsReducer(undefined, action)).toEqual(INITIAL_STATE); }); - it('should update store after successful getNewReactions query', async () => { + it('should update store after successful getNewReactionsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, newReactionsFixture); - const { type } = await store.dispatch(getNewReactions(1)); + const { type } = await store.dispatch(getNewReactionsForModel(1)); const { data, loading, error } = store.getState().newReactions; - expect(type).toBe('newReactions/getNewReactions/fulfilled'); + expect(type).toBe('newReactions/getNewReactionsForModel/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(newReactionsFixture.content); + expect(data).toEqual({ 1: newReactionsFixture.content }); }); - it('should update store after failed getNewReactions query', async () => { - mockedAxiosClient.onGet(apiPath.getNewReactions(1)).reply(HttpStatusCode.NotFound, []); + it('should update store after failed getNewReactionsForModel query', async () => { + mockedAxiosClient.onGet(apiPath.getNewReactionsForModel(1)).reply(HttpStatusCode.NotFound, []); - const action = await store.dispatch(getNewReactions(1)); + const action = await store.dispatch(getNewReactionsForModel(1)); const { data, loading, error } = store.getState().newReactions; - expect(action.type).toBe('newReactions/getNewReactions/rejected'); + expect(action.type).toBe('newReactions/getNewReactionsForModel/rejected'); expect(() => unwrapResult(action)).toThrow( "Failed to fetch new reactions: The page you're looking for doesn't exist. Please verify the URL and try again.", ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual([]); + expect(data).toEqual({}); }); - it('should update store on loading getNewReactions query', async () => { + it('should update store on loading getNewReactionsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, newReactionsFixture); - const newReactionsPromise = store.dispatch(getNewReactions(1)); + const newReactionsPromise = store.dispatch(getNewReactionsForModel(1)); const { data, loading } = store.getState().newReactions; - expect(data).toEqual([]); + expect(data).toEqual({}); expect(loading).toEqual('pending'); newReactionsPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().newReactions; - expect(dataPromiseFulfilled).toEqual(newReactionsFixture.content); + expect(dataPromiseFulfilled).toEqual({ 1: newReactionsFixture.content }); expect(promiseFulfilled).toEqual('succeeded'); }); }); diff --git a/src/redux/newReactions/newReactions.reducers.ts b/src/redux/newReactions/newReactions.reducers.ts index 306b306e..70454483 100644 --- a/src/redux/newReactions/newReactions.reducers.ts +++ b/src/redux/newReactions/newReactions.reducers.ts @@ -1,18 +1,25 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; export const getNewReactionsReducer = ( builder: ActionReducerMapBuilder<NewReactionsState>, ): void => { - builder.addCase(getNewReactions.pending, state => { + builder.addCase(getNewReactionsForModel.pending, state => { state.loading = 'pending'; }); - builder.addCase(getNewReactions.fulfilled, (state, action) => { - state.data = action.payload || []; + builder.addCase(getNewReactionsForModel.fulfilled, (state, action) => { + const modelId = action.meta.arg; + if (state.data) { + state.data[modelId] = action.payload || []; + } else { + state.data = { + [modelId]: action.payload || [], + }; + } state.loading = 'succeeded'; }); - builder.addCase(getNewReactions.rejected, state => { + builder.addCase(getNewReactionsForModel.rejected, state => { state.loading = 'failed'; }); }; diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index 146bbc85..eb461818 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -1,4 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { rootSelector } from '../root/root.selectors'; export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions); @@ -10,5 +11,11 @@ export const newReactionsLoadingSelector = createSelector( export const newReactionsDataSelector = createSelector( newReactionsSelector, - reactions => reactions.data || [], + reactions => reactions.data || {}, +); + +export const newReactionsForCurrentModelSelector = createSelector( + newReactionsDataSelector, + currentModelIdSelector, + (data, currentModelId) => data[currentModelId], ); diff --git a/src/redux/newReactions/newReactions.thunks.test.ts b/src/redux/newReactions/newReactions.thunks.test.ts index afd14d77..3bcc5a9b 100644 --- a/src/redux/newReactions/newReactions.thunks.test.ts +++ b/src/redux/newReactions/newReactions.thunks.test.ts @@ -9,7 +9,7 @@ import { HttpStatusCode } from 'axios'; import newReactionsReducer from '@/redux/newReactions/newReactions.slice'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture'; -import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -22,19 +22,19 @@ describe('newReactions thunks', () => { describe('getReactions', () => { it('should return data when data response from API is valid', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, newReactionsFixture); - const { payload } = await store.dispatch(getNewReactions(1)); + const { payload } = await store.dispatch(getNewReactionsForModel(1)); expect(payload).toEqual(newReactionsFixture.content); }); it('should return undefined when data response from API is not valid ', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - const { payload } = await store.dispatch(getNewReactions(1)); + const { payload } = await store.dispatch(getNewReactionsForModel(1)); expect(payload).toEqual(undefined); }); }); diff --git a/src/redux/newReactions/newReactions.thunks.ts b/src/redux/newReactions/newReactions.thunks.ts index 7e7081a8..2efd3f06 100644 --- a/src/redux/newReactions/newReactions.thunks.ts +++ b/src/redux/newReactions/newReactions.thunks.ts @@ -9,13 +9,15 @@ import { newReactionSchema } from '@/models/newReactionSchema'; import { pageableSchema } from '@/models/pageableSchema'; import { NEW_REACTIONS_FETCHING_ERROR_PREFIX } from '@/redux/newReactions/newReactions.constants'; -export const getNewReactions = createAsyncThunk< +export const getNewReactionsForModel = createAsyncThunk< Array<NewReaction> | undefined, number, ThunkConfig ->('newReactions/getNewReactions', async (modelId: number) => { +>('newReactions/getNewReactionsForModel', async (modelId: number) => { try { - const { data } = await axiosInstanceNewAPI.get<NewReactions>(apiPath.getNewReactions(modelId)); + const { data } = await axiosInstanceNewAPI.get<NewReactions>( + apiPath.getNewReactionsForModel(modelId), + ); const isDataValid = validateDataUsingZodSchema(data, pageableSchema(newReactionSchema)); return isDataValid ? data.content : undefined; } catch (error) { diff --git a/src/redux/newReactions/newReactions.types.ts b/src/redux/newReactions/newReactions.types.ts index 142906d7..7b36d1c5 100644 --- a/src/redux/newReactions/newReactions.types.ts +++ b/src/redux/newReactions/newReactions.types.ts @@ -1,4 +1,6 @@ import { FetchDataState } from '@/types/fetchDataState'; import { NewReaction } from '@/types/models'; -export type NewReactionsState = FetchDataState<NewReaction[]>; +type NewReactionsRecord = Record<number, NewReaction[]>; + +export type NewReactionsState = FetchDataState<NewReactionsRecord>; -- GitLab From dfe46d3b18422df76bf04ced265a49de6edf5ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 28 Nov 2024 13:13:04 +0100 Subject: [PATCH 26/29] refactor(vector-map): modify layers redux to store layers of multiple models --- .../LayersDrawer/LayersDrawer.component.tsx | 23 +++-- .../useOlMapAdditionalLayers.ts | 35 ++++--- src/redux/layers/layers.mock.ts | 12 ++- src/redux/layers/layers.reducers.test.ts | 20 ++-- src/redux/layers/layers.reducers.ts | 54 +++++++---- src/redux/layers/layers.selectors.ts | 26 ++++-- src/redux/layers/layers.slice.ts | 7 +- src/redux/layers/layers.thunks.test.ts | 8 +- src/redux/layers/layers.thunks.ts | 93 ++++++++++--------- src/redux/layers/layers.types.ts | 4 +- 10 files changed, 176 insertions(+), 106 deletions(-) diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index 7e614298..6ee65f6b 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -1,26 +1,37 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { DrawerHeading } from '@/shared/DrawerHeading'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { layersSelector, layersVisibilitySelector } from '@/redux/layers/layers.selectors'; +import { + layersForCurrentModelSelector, + layersVisibilityForCurrentModelSelector, +} from '@/redux/layers/layers.selectors'; import { Switch } from '@/shared/Switch'; import { setLayerVisibility } from '@/redux/layers/layers.slice'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; export const LayersDrawer = (): JSX.Element => { - const layers = useAppSelector(layersSelector); - const layersVisibility = useAppSelector(layersVisibilitySelector); + const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); + const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); + const currentModelId = useAppSelector(currentModelIdSelector); const dispatch = useAppDispatch(); return ( <div data-testid="layers-drawer" className="h-full max-h-full"> <DrawerHeading title="Layers" /> <div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto px-6"> - {layers.map(layer => ( + {layersForCurrentModel.map(layer => ( <div key={layer.details.id} className="flex items-center justify-between border-b p-4"> <h1>{layer.details.name}</h1> <Switch - isChecked={layersVisibility[layer.details.layerId]} + isChecked={layersVisibilityForCurrentModel[layer.details.layerId]} onToggle={value => - dispatch(setLayerVisibility({ visible: value, layerId: layer.details.layerId })) + dispatch( + setLayerVisibility({ + modelId: currentModelId, + visible: value, + layerId: layer.details.layerId, + }), + ) } /> </div> diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 98a4276a..f56532ef 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -6,14 +6,19 @@ import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { getLayers } from '@/redux/layers/layers.thunks'; -import { layersSelector, layersVisibilitySelector } from '@/redux/layers/layers.selectors'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; +import { + layersForCurrentModelSelector, + layersLoadingSelector, + layersVisibilityForCurrentModelSelector, +} from '@/redux/layers/layers.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { MapInstance } from '@/types/map'; import { LineString, MultiPolygon, Point } from 'ol/geom'; import Polygon from 'ol/geom/Polygon'; import Layer from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer'; import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; export const useOlMapAdditionalLayers = ( mapInstance: MapInstance, @@ -24,18 +29,26 @@ export const useOlMapAdditionalLayers = ( > => { const dispatch = useAppDispatch(); const currentModelId = useSelector(currentModelIdSelector); - const mapLayers = useSelector(layersSelector); - const layersVisibility = useSelector(layersVisibilitySelector); + + const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); + const layersLoading = useAppSelector(layersLoadingSelector); + const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); + const lineTypes = useSelector(lineTypesSelector); const arrowTypes = useSelector(arrowTypesSelector); const pointToProjection = usePointToProjection(); useEffect(() => { - dispatch(getLayers(currentModelId)); - }, [currentModelId, dispatch]); + if (!currentModelId) { + return; + } + if (layersLoading !== 'succeeded') { + dispatch(getLayersForModel(currentModelId)); + } + }, [currentModelId, dispatch, layersLoading]); const vectorLayers = useMemo(() => { - return mapLayers.map(layer => { + return layersForCurrentModel.map(layer => { const additionalLayer = new Layer({ texts: layer.texts, rects: layer.rects, @@ -50,16 +63,16 @@ export const useOlMapAdditionalLayers = ( }); return additionalLayer.vectorLayer; }); - }, [arrowTypes, lineTypes, mapInstance, mapLayers, pointToProjection]); + }, [arrowTypes, lineTypes, mapInstance, layersForCurrentModel, pointToProjection]); useEffect(() => { vectorLayers.forEach(layer => { const layerId = layer.get('id'); - if (layerId && layersVisibility[layerId] !== undefined) { - layer.setVisible(layersVisibility[layerId]); + if (layerId && layersVisibilityForCurrentModel[layerId] !== undefined) { + layer.setVisible(layersVisibilityForCurrentModel[layerId]); } }); - }, [layersVisibility, vectorLayers]); + }, [layersVisibilityForCurrentModel, vectorLayers]); return vectorLayers; }; diff --git a/src/redux/layers/layers.mock.ts b/src/redux/layers/layers.mock.ts index 9ec2ce4c..38e72675 100644 --- a/src/redux/layers/layers.mock.ts +++ b/src/redux/layers/layers.mock.ts @@ -1,7 +1,10 @@ -import { LayersState } from '@/redux/layers/layers.types'; +import { LayersState, LayersVisibilitiesState } from '@/redux/layers/layers.types'; import { DEFAULT_ERROR } from '@/constants/errors'; +import { FetchDataState } from '@/types/fetchDataState'; -export const LAYERS_STATE_INITIAL_MOCK: LayersState = { +export const LAYERS_STATE_INITIAL_MOCK: LayersState = {}; + +export const LAYERS_STATE_INITIAL_LAYER_MOCK: FetchDataState<LayersVisibilitiesState> = { data: { layers: [], layersVisibility: {}, @@ -9,3 +12,8 @@ export const LAYERS_STATE_INITIAL_MOCK: LayersState = { loading: 'idle', error: DEFAULT_ERROR, }; + +export const LAYER_STATE_DEFAULT_DATA = { + layers: [], + layersVisibility: {}, +}; diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts index 827c1dff..ffc0d923 100644 --- a/src/redux/layers/layers.reducers.test.ts +++ b/src/redux/layers/layers.reducers.test.ts @@ -7,8 +7,8 @@ import { import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { unwrapResult } from '@reduxjs/toolkit'; -import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; -import { getLayers } from '@/redux/layers/layers.thunks'; +import { LAYER_STATE_DEFAULT_DATA, LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; import { layersFixture } from '@/models/fixtures/layersFixture'; import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture'; import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture'; @@ -48,8 +48,8 @@ describe('layers reducer', () => { .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) .reply(HttpStatusCode.Ok, layerLinesFixture); - const { type } = await store.dispatch(getLayers(1)); - const { data, loading, error } = store.getState().layers; + const { type } = await store.dispatch(getLayersForModel(1)); + const { data, loading, error } = store.getState().layers[1]; expect(type).toBe('vectorMap/getLayers/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); @@ -72,8 +72,8 @@ describe('layers reducer', () => { it('should update store after failed getLayers query', async () => { mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.NotFound, []); - const action = await store.dispatch(getLayers(1)); - const { data, loading, error } = store.getState().layers; + const action = await store.dispatch(getLayersForModel(1)); + const { data, loading, error } = store.getState().layers[1]; expect(action.type).toBe('vectorMap/getLayers/rejected'); expect(() => unwrapResult(action)).toThrow( @@ -99,14 +99,14 @@ describe('layers reducer', () => { .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) .reply(HttpStatusCode.Ok, layerLinesFixture); - const layersPromise = store.dispatch(getLayers(1)); + const layersPromise = store.dispatch(getLayersForModel(1)); - const { data, loading } = store.getState().layers; - expect(data).toEqual({ layers: [], layersVisibility: {} }); + const { data, loading } = store.getState().layers[1]; + expect(data).toEqual(LAYER_STATE_DEFAULT_DATA); expect(loading).toEqual('pending'); layersPromise.then(() => { - const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().layers; + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().layers[1]; expect(dataPromiseFulfilled).toEqual({ layers: [ diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts index ed75a687..0cc1ad73 100644 --- a/src/redux/layers/layers.reducers.ts +++ b/src/redux/layers/layers.reducers.ts @@ -1,30 +1,52 @@ /* eslint-disable no-magic-numbers */ import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; -import { getLayers } from '@/redux/layers/layers.thunks'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; import { LayersState } from '@/redux/layers/layers.types'; +import { + LAYER_STATE_DEFAULT_DATA, + LAYERS_STATE_INITIAL_LAYER_MOCK, +} from '@/redux/layers/layers.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; -export const getLayersReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { - builder.addCase(getLayers.pending, state => { - state.loading = 'pending'; +export const getLayersForModelReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { + builder.addCase(getLayersForModel.pending, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'pending'; + } else { + state[modelId] = { ...LAYERS_STATE_INITIAL_LAYER_MOCK, loading: 'pending' }; + } }); - builder.addCase(getLayers.fulfilled, (state, action) => { - state.data = action.payload || { - layers: [], - layersVisibility: {}, - }; - state.loading = 'succeeded'; + builder.addCase(getLayersForModel.fulfilled, (state, action) => { + const modelId = action.meta.arg; + const data = action.payload || { ...LAYER_STATE_DEFAULT_DATA }; + if (state[modelId]) { + state[modelId].data = data; + state[modelId].loading = 'succeeded'; + } else { + state[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; + } }); - builder.addCase(getLayers.rejected, state => { - state.loading = 'failed'; + builder.addCase(getLayersForModel.rejected, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'failed'; + } else { + state[modelId] = { ...LAYERS_STATE_INITIAL_LAYER_MOCK, loading: 'failed' }; + } }); }; export const setLayerVisibilityReducer = ( state: LayersState, - action: PayloadAction<{ visible: boolean; layerId: string }>, + action: PayloadAction<{ modelId: number; visible: boolean; layerId: string }>, ): void => { - const { payload } = action; - if (state.data && state.data.layersVisibility[payload.layerId] !== undefined) { - state.data.layersVisibility[payload.layerId] = payload.visible; + const { modelId, visible, layerId } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + if (data.layersVisibility[layerId] !== undefined) { + data.layersVisibility[layerId] = visible; } }; diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index d29698e0..4d9e3f6d 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -1,14 +1,26 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; -export const layersSelector = createSelector( - rootSelector, - state => state.layers?.data?.layers || [], +export const layersSelector = createSelector(rootSelector, state => state.layers); + +export const layersStateForCurrentModelSelector = createSelector( + layersSelector, + currentModelIdSelector, + (state, currentModelId) => state[currentModelId], +); + +export const layersLoadingSelector = createSelector( + layersStateForCurrentModelSelector, + state => state?.loading, ); -export const layersLoadingSelector = createSelector(rootSelector, state => state.layers.loading); +export const layersVisibilityForCurrentModelSelector = createSelector( + layersStateForCurrentModelSelector, + state => state?.data?.layersVisibility || {}, +); -export const layersVisibilitySelector = createSelector( - rootSelector, - state => state.layers?.data?.layersVisibility || {}, +export const layersForCurrentModelSelector = createSelector( + layersStateForCurrentModelSelector, + state => state?.data?.layers || [], ); diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts index 47da06b0..7c07cdc0 100644 --- a/src/redux/layers/layers.slice.ts +++ b/src/redux/layers/layers.slice.ts @@ -1,6 +1,9 @@ import { createSlice } from '@reduxjs/toolkit'; import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; -import { getLayersReducer, setLayerVisibilityReducer } from '@/redux/layers/layers.reducers'; +import { + getLayersForModelReducer, + setLayerVisibilityReducer, +} from '@/redux/layers/layers.reducers'; export const layersSlice = createSlice({ name: 'layers', @@ -9,7 +12,7 @@ export const layersSlice = createSlice({ setLayerVisibility: setLayerVisibilityReducer, }, extraReducers: builder => { - getLayersReducer(builder); + getLayersForModelReducer(builder); }, }); diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts index a6c00186..bdc626d5 100644 --- a/src/redux/layers/layers.thunks.test.ts +++ b/src/redux/layers/layers.thunks.test.ts @@ -7,7 +7,7 @@ import { import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { LayersState } from '@/redux/layers/layers.types'; -import { getLayers } from '@/redux/layers/layers.thunks'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; import { layersFixture } from '@/models/fixtures/layersFixture'; import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture'; import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture'; @@ -23,7 +23,7 @@ describe('layers thunks', () => { store = createStoreInstanceUsingSliceReducer('layers', layersReducer); }); - describe('getLayers', () => { + describe('getLayersForModel', () => { it('should return data when data response from API is valid', async () => { mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.Ok, layersFixture); mockedAxiosClient @@ -39,7 +39,7 @@ describe('layers thunks', () => { .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) .reply(HttpStatusCode.Ok, layerLinesFixture); - const { payload } = await store.dispatch(getLayers(1)); + const { payload } = await store.dispatch(getLayersForModel(1)); expect(payload).toEqual({ layers: [ { @@ -61,7 +61,7 @@ describe('layers thunks', () => { .onGet(apiPath.getLayers(1)) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - const { payload } = await store.dispatch(getLayers(1)); + const { payload } = await store.dispatch(getLayersForModel(1)); expect(payload).toEqual(undefined); }); }); diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 9aa71e83..0f38e3d1 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -15,51 +15,52 @@ import { pageableSchema } from '@/models/pageableSchema'; import { layerOvalSchema } from '@/models/layerOvalSchema'; import { layerLineSchema } from '@/models/layerLineSchema'; -export const getLayers = createAsyncThunk<LayersVisibilitiesState | undefined, number, ThunkConfig>( - 'vectorMap/getLayers', - async (modelId: number) => { - try { - const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); - const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); - if (!isDataValid) { - return undefined; - } - let layers = await Promise.all( - data.content.map(async (layer: Layer) => { - const [textsResponse, rectsResponse, ovalsResponse, linesResponse] = await Promise.all([ - axiosInstanceNewAPI.get(apiPath.getLayerTexts(modelId, layer.id)), - axiosInstanceNewAPI.get(apiPath.getLayerRects(modelId, layer.id)), - axiosInstanceNewAPI.get(apiPath.getLayerOvals(modelId, layer.id)), - axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)), - ]); +export const getLayersForModel = createAsyncThunk< + LayersVisibilitiesState | undefined, + number, + ThunkConfig +>('vectorMap/getLayers', async (modelId: number) => { + try { + const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); + const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); + if (!isDataValid) { + return undefined; + } + let layers = await Promise.all( + data.content.map(async (layer: Layer) => { + const [textsResponse, rectsResponse, ovalsResponse, linesResponse] = await Promise.all([ + axiosInstanceNewAPI.get(apiPath.getLayerTexts(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerRects(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerOvals(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)), + ]); - return { - details: layer, - texts: textsResponse.data.content, - rects: rectsResponse.data.content, - ovals: ovalsResponse.data.content, - lines: linesResponse.data.content, - }; - }), + return { + details: layer, + texts: textsResponse.data.content, + rects: rectsResponse.data.content, + ovals: ovalsResponse.data.content, + lines: linesResponse.data.content, + }; + }), + ); + layers = layers.filter(layer => { + return ( + z.array(layerTextSchema).safeParse(layer.texts).success && + z.array(layerRectSchema).safeParse(layer.rects).success && + z.array(layerOvalSchema).safeParse(layer.ovals).success && + z.array(layerLineSchema).safeParse(layer.lines).success ); - layers = layers.filter(layer => { - return ( - z.array(layerTextSchema).safeParse(layer.texts).success && - z.array(layerRectSchema).safeParse(layer.rects).success && - z.array(layerOvalSchema).safeParse(layer.ovals).success && - z.array(layerLineSchema).safeParse(layer.lines).success - ); - }); - const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => { - acc[layer.details.layerId] = layer.details.visible; - return acc; - }, {}); - return { - layers, - layersVisibility, - }; - } catch (error) { - return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX })); - } - }, -); + }); + const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => { + acc[layer.details.layerId] = layer.details.visible; + return acc; + }, {}); + return { + layers, + layersVisibility, + }; + } catch (error) { + return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX })); + } +}); diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts index 63637690..c00d6a7e 100644 --- a/src/redux/layers/layers.types.ts +++ b/src/redux/layers/layers.types.ts @@ -1,4 +1,4 @@ -import { FetchDataState } from '@/types/fetchDataState'; +import { KeyedFetchDataState } from '@/types/fetchDataState'; import { Layer, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; export type LayerState = { @@ -18,4 +18,4 @@ export type LayersVisibilitiesState = { layers: LayerState[]; }; -export type LayersState = FetchDataState<LayersVisibilitiesState>; +export type LayersState = KeyedFetchDataState<LayersVisibilitiesState>; -- GitLab From 0555d455401e3025dda55ff0c30ea58f23c52f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 28 Nov 2024 13:31:26 +0100 Subject: [PATCH 27/29] refactor(vector-map): modify modelElements redux to store layers of multiple models --- .../reactionsLayer/useOlMapReactionsLayer.ts | 11 +++++-- src/redux/modelElements/modelElements.mock.ts | 8 +++-- .../modelElements.reducers.test.ts | 24 ++++++-------- .../modelElements/modelElements.reducers.ts | 31 +++++++++++++------ .../modelElements/modelElements.selector.ts | 18 +++++------ .../modelElements/modelElements.slice.ts | 10 ++---- .../modelElements/modelElements.types.ts | 6 ++-- 7 files changed, 57 insertions(+), 51 deletions(-) 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 99e05527..1037bd41 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -12,7 +12,10 @@ import { lineTypesSelector, } from '@/redux/shapes/shapes.selectors'; import { MapInstance } from '@/types/map'; -import { modelElementsForCurrentModelSelector } from '@/redux/modelElements/modelElements.selector'; +import { + modelElementsForCurrentModelSelector, + modelElementsLoadingSelector, +} from '@/redux/modelElements/modelElements.selector'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; @@ -61,6 +64,8 @@ export const useOlMapReactionsLayer = ({ const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); const reactionsForCurrentModel = useAppSelector(newReactionsForCurrentModelSelector); + const modelElementsLoading = useAppSelector(modelElementsLoadingSelector); + const modelElementsForCurrentModel = useAppSelector(modelElementsForCurrentModelSelector); const debouncedBioEntities = useDebouncedValue(bioEntities, 1000); const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); @@ -79,13 +84,13 @@ export const useOlMapReactionsLayer = ({ if (!currentModelId) { return; } - if (!modelElementsForCurrentModel) { + if (modelElementsLoading !== 'succeeded') { dispatch(getModelElementsForModel(currentModelId)); } if (!reactionsForCurrentModel) { dispatch(getNewReactionsForModel(currentModelId)); } - }, [currentModelId, dispatch, reactionsForCurrentModel, modelElementsForCurrentModel]); + }, [currentModelId, dispatch, reactionsForCurrentModel, modelElementsLoading]); useEffect(() => { if (overlaysOrder.length) { diff --git a/src/redux/modelElements/modelElements.mock.ts b/src/redux/modelElements/modelElements.mock.ts index 0ee98c43..583f03f3 100644 --- a/src/redux/modelElements/modelElements.mock.ts +++ b/src/redux/modelElements/modelElements.mock.ts @@ -1,8 +1,12 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; +import { FetchDataState } from '@/types/fetchDataState'; +import { ModelElement } from '@/types/models'; -export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = { - data: {}, +export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = {}; + +export const MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK: FetchDataState<Array<ModelElement>> = { + data: [], loading: 'idle', error: DEFAULT_ERROR, }; diff --git a/src/redux/modelElements/modelElements.reducers.test.ts b/src/redux/modelElements/modelElements.reducers.test.ts index 3554f048..fc5c5a6f 100644 --- a/src/redux/modelElements/modelElements.reducers.test.ts +++ b/src/redux/modelElements/modelElements.reducers.test.ts @@ -14,12 +14,6 @@ import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); -const INITIAL_STATE: ModelElementsState = { - data: {}, - loading: 'idle', - error: { name: '', message: '' }, -}; - describe('model elements reducer', () => { let store = {} as ToolkitStoreWithSingleSlice<ModelElementsState>; beforeEach(() => { @@ -29,7 +23,7 @@ describe('model elements reducer', () => { it('should match initial state', () => { const action = { type: 'unknown' }; - expect(modelElementsReducer(undefined, action)).toEqual(INITIAL_STATE); + expect(modelElementsReducer(undefined, action)).toEqual({}); }); it('should update store after successful getModelElementsForModel query', async () => { @@ -38,19 +32,19 @@ describe('model elements reducer', () => { .reply(HttpStatusCode.Ok, modelElementsFixture); const { type } = await store.dispatch(getModelElementsForModel(0)); - const { data, loading, error } = store.getState().modelElements; + const { data, loading, error } = store.getState().modelElements[0]; expect(type).toBe('vectorMap/getModelElementsForModel/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual({ 0: modelElementsFixture.content }); + expect(data).toEqual(modelElementsFixture.content); }); it('should update store after failed getModelElementsForModel query', async () => { mockedAxiosClient.onGet(apiPath.getModelElementsForModel(0)).reply(HttpStatusCode.NotFound, []); const action = await store.dispatch(getModelElementsForModel(0)); - const { data, loading, error } = store.getState().modelElements; + const { data, loading, error } = store.getState().modelElements[0]; expect(action.type).toBe('vectorMap/getModelElementsForModel/rejected'); expect(() => unwrapResult(action)).toThrow( @@ -58,7 +52,7 @@ describe('model elements reducer', () => { ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual({}); + expect(data).toEqual([]); }); it('should update store on loading getModelElementsForModel query', async () => { @@ -68,15 +62,15 @@ describe('model elements reducer', () => { const modelElementsPromise = store.dispatch(getModelElementsForModel(0)); - const { data, loading } = store.getState().modelElements; - expect(data).toEqual({}); + const { data, loading } = store.getState().modelElements[0]; + expect(data).toEqual([]); expect(loading).toEqual('pending'); modelElementsPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = - store.getState().modelElements; + store.getState().modelElements[0]; - expect(dataPromiseFulfilled).toEqual({ 0: modelElementsFixture.content }); + expect(dataPromiseFulfilled).toEqual(modelElementsFixture.content); expect(promiseFulfilled).toEqual('succeeded'); }); }); diff --git a/src/redux/modelElements/modelElements.reducers.ts b/src/redux/modelElements/modelElements.reducers.ts index 15a40c42..777295cf 100644 --- a/src/redux/modelElements/modelElements.reducers.ts +++ b/src/redux/modelElements/modelElements.reducers.ts @@ -1,25 +1,36 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; +import { MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK } from '@/redux/modelElements/modelElements.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; export const getModelElementsReducer = ( builder: ActionReducerMapBuilder<ModelElementsState>, ): void => { - builder.addCase(getModelElementsForModel.pending, state => { - state.loading = 'pending'; + builder.addCase(getModelElementsForModel.pending, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'pending'; + } else { + state[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'pending' }; + } }); builder.addCase(getModelElementsForModel.fulfilled, (state, action) => { const modelId = action.meta.arg; - if (state.data) { - state.data[modelId] = action.payload || []; + const data = action.payload || []; + if (state[modelId]) { + state[modelId].data = data; + state[modelId].loading = 'succeeded'; } else { - state.data = { - [modelId]: action.payload || [], - }; + state[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; } - state.loading = 'succeeded'; }); - builder.addCase(getModelElementsForModel.rejected, state => { - state.loading = 'failed'; + builder.addCase(getModelElementsForModel.rejected, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'failed'; + } else { + state[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'failed' }; + } }); }; diff --git a/src/redux/modelElements/modelElements.selector.ts b/src/redux/modelElements/modelElements.selector.ts index f9a654bf..4be70ab4 100644 --- a/src/redux/modelElements/modelElements.selector.ts +++ b/src/redux/modelElements/modelElements.selector.ts @@ -4,18 +4,18 @@ import { currentModelIdSelector } from '@/redux/models/models.selectors'; export const modelElementsSelector = createSelector(rootSelector, state => state.modelElements); -export const modelElementsLoadingSelector = createSelector( - rootSelector, - state => state.modelElements.loading, +export const modelElementsStateForCurrentModelSelector = createSelector( + modelElementsSelector, + currentModelIdSelector, + (state, currentModelId) => state[currentModelId], ); -export const modelElementsDataSelector = createSelector( - modelElementsSelector, - modelElements => modelElements.data || {}, +export const modelElementsLoadingSelector = createSelector( + modelElementsStateForCurrentModelSelector, + state => state?.loading, ); export const modelElementsForCurrentModelSelector = createSelector( - modelElementsDataSelector, - currentModelIdSelector, - (data, currentModelId) => data[currentModelId], + modelElementsStateForCurrentModelSelector, + state => state?.data || [], ); diff --git a/src/redux/modelElements/modelElements.slice.ts b/src/redux/modelElements/modelElements.slice.ts index 56b5d77a..cbf16d00 100644 --- a/src/redux/modelElements/modelElements.slice.ts +++ b/src/redux/modelElements/modelElements.slice.ts @@ -1,16 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; import { getModelElementsReducer } from '@/redux/modelElements/modelElements.reducers'; -import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; - -const initialState: ModelElementsState = { - data: {}, - loading: 'idle', - error: { name: '', message: '' }, -}; +import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; export const modelElements = createSlice({ name: 'modelElements', - initialState, + initialState: MODEL_ELEMENTS_INITIAL_STATE_MOCK, reducers: {}, extraReducers: builder => { getModelElementsReducer(builder); diff --git a/src/redux/modelElements/modelElements.types.ts b/src/redux/modelElements/modelElements.types.ts index 808f3ace..b2d4ee68 100644 --- a/src/redux/modelElements/modelElements.types.ts +++ b/src/redux/modelElements/modelElements.types.ts @@ -1,6 +1,4 @@ -import { FetchDataState } from '@/types/fetchDataState'; +import { KeyedFetchDataState } from '@/types/fetchDataState'; import { ModelElement } from '@/types/models'; -type ModelElementsRecord = Record<number, Array<ModelElement>>; - -export type ModelElementsState = FetchDataState<ModelElementsRecord>; +export type ModelElementsState = KeyedFetchDataState<Array<ModelElement>>; -- GitLab From d5dab69bd0e933077d90185cb3602012972b7163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 28 Nov 2024 13:46:29 +0100 Subject: [PATCH 28/29] refactor(vector-map): modify newReactions redux to store layers of multiple models --- .../reactionsLayer/useOlMapReactionsLayer.ts | 10 ++++-- .../newReactions/newReactions.constants.ts | 9 ------ src/redux/newReactions/newReactions.mock.ts | 8 +++-- .../newReactions.reducers.test.ts | 16 +++++----- .../newReactions/newReactions.reducers.ts | 31 +++++++++++++------ .../newReactions/newReactions.selectors.ts | 16 +++++----- src/redux/newReactions/newReactions.slice.ts | 4 +-- src/redux/newReactions/newReactions.types.ts | 6 ++-- 8 files changed, 54 insertions(+), 46 deletions(-) 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 1037bd41..f2de18c8 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -24,7 +24,10 @@ import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/ import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction'; -import { newReactionsForCurrentModelSelector } from '@/redux/newReactions/newReactions.selectors'; +import { + newReactionsForCurrentModelSelector, + newReactionsLoadingSelector, +} from '@/redux/newReactions/newReactions.selectors'; import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import { @@ -65,6 +68,7 @@ export const useOlMapReactionsLayer = ({ const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); const reactionsForCurrentModel = useAppSelector(newReactionsForCurrentModelSelector); const modelElementsLoading = useAppSelector(modelElementsLoadingSelector); + const reactionsLoading = useAppSelector(newReactionsLoadingSelector); const modelElementsForCurrentModel = useAppSelector(modelElementsForCurrentModelSelector); const debouncedBioEntities = useDebouncedValue(bioEntities, 1000); @@ -87,10 +91,10 @@ export const useOlMapReactionsLayer = ({ if (modelElementsLoading !== 'succeeded') { dispatch(getModelElementsForModel(currentModelId)); } - if (!reactionsForCurrentModel) { + if (reactionsLoading !== 'succeeded') { dispatch(getNewReactionsForModel(currentModelId)); } - }, [currentModelId, dispatch, reactionsForCurrentModel, modelElementsLoading]); + }, [currentModelId, dispatch, reactionsLoading, modelElementsLoading]); useEffect(() => { if (overlaysOrder.length) { diff --git a/src/redux/newReactions/newReactions.constants.ts b/src/redux/newReactions/newReactions.constants.ts index e30ac5fa..bb4677eb 100644 --- a/src/redux/newReactions/newReactions.constants.ts +++ b/src/redux/newReactions/newReactions.constants.ts @@ -1,10 +1 @@ -import { DEFAULT_ERROR } from '@/constants/errors'; -import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; - -export const NEW_REACTIONS_INITIAL_STATE: NewReactionsState = { - data: {}, - loading: 'idle', - error: DEFAULT_ERROR, -}; - export const NEW_REACTIONS_FETCHING_ERROR_PREFIX = 'Failed to fetch new reactions'; diff --git a/src/redux/newReactions/newReactions.mock.ts b/src/redux/newReactions/newReactions.mock.ts index f9a878ec..5b3c51a8 100644 --- a/src/redux/newReactions/newReactions.mock.ts +++ b/src/redux/newReactions/newReactions.mock.ts @@ -1,8 +1,12 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; +import { FetchDataState } from '@/types/fetchDataState'; +import { NewReaction } from '@/types/models'; -export const NEW_REACTIONS_INITIAL_STATE_MOCK: NewReactionsState = { - data: {}, +export const NEW_REACTIONS_INITIAL_STATE_MOCK: NewReactionsState = {}; + +export const NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK: FetchDataState<Array<NewReaction>> = { + data: [], loading: 'idle', error: DEFAULT_ERROR, }; diff --git a/src/redux/newReactions/newReactions.reducers.test.ts b/src/redux/newReactions/newReactions.reducers.test.ts index 206d34a7..0ceaa5b4 100644 --- a/src/redux/newReactions/newReactions.reducers.test.ts +++ b/src/redux/newReactions/newReactions.reducers.test.ts @@ -35,18 +35,18 @@ describe('newReactions reducer', () => { .reply(HttpStatusCode.Ok, newReactionsFixture); const { type } = await store.dispatch(getNewReactionsForModel(1)); - const { data, loading, error } = store.getState().newReactions; + const { data, loading, error } = store.getState().newReactions[1]; expect(type).toBe('newReactions/getNewReactionsForModel/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual({ 1: newReactionsFixture.content }); + expect(data).toEqual(newReactionsFixture.content); }); it('should update store after failed getNewReactionsForModel query', async () => { mockedAxiosClient.onGet(apiPath.getNewReactionsForModel(1)).reply(HttpStatusCode.NotFound, []); const action = await store.dispatch(getNewReactionsForModel(1)); - const { data, loading, error } = store.getState().newReactions; + const { data, loading, error } = store.getState().newReactions[1]; expect(action.type).toBe('newReactions/getNewReactionsForModel/rejected'); expect(() => unwrapResult(action)).toThrow( @@ -54,7 +54,7 @@ describe('newReactions reducer', () => { ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual({}); + expect(data).toEqual([]); }); it('should update store on loading getNewReactionsForModel query', async () => { @@ -64,15 +64,15 @@ describe('newReactions reducer', () => { const newReactionsPromise = store.dispatch(getNewReactionsForModel(1)); - const { data, loading } = store.getState().newReactions; - expect(data).toEqual({}); + const { data, loading } = store.getState().newReactions[1]; + expect(data).toEqual([]); expect(loading).toEqual('pending'); newReactionsPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = - store.getState().newReactions; + store.getState().newReactions[1]; - expect(dataPromiseFulfilled).toEqual({ 1: newReactionsFixture.content }); + expect(dataPromiseFulfilled).toEqual(newReactionsFixture.content); expect(promiseFulfilled).toEqual('succeeded'); }); }); diff --git a/src/redux/newReactions/newReactions.reducers.ts b/src/redux/newReactions/newReactions.reducers.ts index 70454483..c8963df9 100644 --- a/src/redux/newReactions/newReactions.reducers.ts +++ b/src/redux/newReactions/newReactions.reducers.ts @@ -1,25 +1,36 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; +import { NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK } from '@/redux/newReactions/newReactions.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; export const getNewReactionsReducer = ( builder: ActionReducerMapBuilder<NewReactionsState>, ): void => { - builder.addCase(getNewReactionsForModel.pending, state => { - state.loading = 'pending'; + builder.addCase(getNewReactionsForModel.pending, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'pending'; + } else { + state[modelId] = { ...NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK, loading: 'pending' }; + } }); builder.addCase(getNewReactionsForModel.fulfilled, (state, action) => { const modelId = action.meta.arg; - if (state.data) { - state.data[modelId] = action.payload || []; + const data = action.payload || []; + if (state[modelId]) { + state[modelId].data = data; + state[modelId].loading = 'succeeded'; } else { - state.data = { - [modelId]: action.payload || [], - }; + state[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; } - state.loading = 'succeeded'; }); - builder.addCase(getNewReactionsForModel.rejected, state => { - state.loading = 'failed'; + builder.addCase(getNewReactionsForModel.rejected, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'failed'; + } else { + state[modelId] = { ...NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK, loading: 'failed' }; + } }); }; diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index eb461818..6bf11f0e 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -4,18 +4,18 @@ import { rootSelector } from '../root/root.selectors'; export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions); -export const newReactionsLoadingSelector = createSelector( +export const newReactionsStateForCurrentModelSelector = createSelector( newReactionsSelector, - state => state.loading, + currentModelIdSelector, + (state, currentModelId) => state[currentModelId], ); -export const newReactionsDataSelector = createSelector( - newReactionsSelector, - reactions => reactions.data || {}, +export const newReactionsLoadingSelector = createSelector( + newReactionsStateForCurrentModelSelector, + state => state?.loading, ); export const newReactionsForCurrentModelSelector = createSelector( - newReactionsDataSelector, - currentModelIdSelector, - (data, currentModelId) => data[currentModelId], + newReactionsStateForCurrentModelSelector, + state => state?.data || [], ); diff --git a/src/redux/newReactions/newReactions.slice.ts b/src/redux/newReactions/newReactions.slice.ts index 5bb5198a..d603e579 100644 --- a/src/redux/newReactions/newReactions.slice.ts +++ b/src/redux/newReactions/newReactions.slice.ts @@ -1,10 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; -import { NEW_REACTIONS_INITIAL_STATE } from '@/redux/newReactions/newReactions.constants'; import { getNewReactionsReducer } from '@/redux/newReactions/newReactions.reducers'; +import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock'; export const newReactionsSlice = createSlice({ name: 'reactions', - initialState: NEW_REACTIONS_INITIAL_STATE, + initialState: NEW_REACTIONS_INITIAL_STATE_MOCK, reducers: {}, extraReducers: builder => { getNewReactionsReducer(builder); diff --git a/src/redux/newReactions/newReactions.types.ts b/src/redux/newReactions/newReactions.types.ts index 7b36d1c5..aff62402 100644 --- a/src/redux/newReactions/newReactions.types.ts +++ b/src/redux/newReactions/newReactions.types.ts @@ -1,6 +1,4 @@ -import { FetchDataState } from '@/types/fetchDataState'; +import { KeyedFetchDataState } from '@/types/fetchDataState'; import { NewReaction } from '@/types/models'; -type NewReactionsRecord = Record<number, NewReaction[]>; - -export type NewReactionsState = FetchDataState<NewReactionsRecord>; +export type NewReactionsState = KeyedFetchDataState<Array<NewReaction>>; -- GitLab From d4e93e3769ccd0a0757669137c774a5fc0bc1420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 28 Nov 2024 13:52:14 +0100 Subject: [PATCH 29/29] fix(vector-map): change condition of when to featch map data --- .../utils/config/additionalLayers/useOlMapAdditionalLayers.ts | 2 +- .../utils/config/reactionsLayer/useOlMapReactionsLayer.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index f56532ef..b37cffda 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -42,7 +42,7 @@ export const useOlMapAdditionalLayers = ( if (!currentModelId) { return; } - if (layersLoading !== 'succeeded') { + if (!['succeeded', 'pending'].includes(layersLoading)) { dispatch(getLayersForModel(currentModelId)); } }, [currentModelId, dispatch, layersLoading]); 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 f2de18c8..6f2aafa1 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -88,10 +88,10 @@ export const useOlMapReactionsLayer = ({ if (!currentModelId) { return; } - if (modelElementsLoading !== 'succeeded') { + if (!['succeeded', 'pending'].includes(modelElementsLoading)) { dispatch(getModelElementsForModel(currentModelId)); } - if (reactionsLoading !== 'succeeded') { + if (!['succeeded', 'pending'].includes(reactionsLoading)) { dispatch(getNewReactionsForModel(currentModelId)); } }, [currentModelId, dispatch, reactionsLoading, modelElementsLoading]); -- GitLab