From 9a8699f74486ee4afe2297de75a124e8952435db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Mon, 4 Mar 2024 18:25:29 +0100 Subject: [PATCH] feat: add bio entities events + hover w/o tests --- docs/plugins/events.md | 2 +- package-lock.json | 24 ++++++++++ package.json | 2 + .../BioEntityDrawer.component.tsx | 8 ++-- .../overlaysLayer/createFeatureFromExtent.ts | 17 ++++++- .../createOverlayGeometryFeature.ts | 6 ++- ...createOverlaySubmapLinkRectangleFeature.ts | 4 +- .../overlaysLayer/useOverlayFeatures.ts | 3 ++ .../config/pinsLayer/getPinFeature.test.ts | 2 +- .../utils/config/pinsLayer/getPinFeature.ts | 6 ++- .../mapFeatureClick/useMapFeatureClick.ts | 15 ------ .../mapRightClick/onMapRightClick.ts | 8 ++-- .../mapSingleClick/handleAliasResults.ts | 3 +- .../mapSingleClick/handleFeaturesClick.ts | 39 +++++++++++++++ .../mapSingleClick/onMapSingleClick.ts | 15 ++++-- .../utils/listeners/onPointerMove.ts | 26 ++++++++++ .../utils/listeners/useOlMapListeners.ts | 13 ++--- src/constants/features.ts | 9 ++++ src/redux/bioEntity/bioEntity.selectors.ts | 47 ++++++++++++++++++- src/redux/chemicals/chemicals.selectors.ts | 12 +++++ src/redux/drawer/drawer.reducers.ts | 1 - src/redux/drawer/drawer.types.ts | 3 ++ src/redux/drugs/drugs.selectors.ts | 12 +++++ .../pluginsEventBus.constants.ts | 2 +- .../pluginsEventBus/pluginsEventBus.ts | 2 +- .../pluginsEventBus/pluginsEventBus.types.ts | 2 +- 26 files changed, 234 insertions(+), 49 deletions(-) delete mode 100644 src/components/Map/MapViewer/utils/listeners/mapFeatureClick/useMapFeatureClick.ts create mode 100644 src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts create mode 100644 src/components/Map/MapViewer/utils/listeners/onPointerMove.ts create mode 100644 src/constants/features.ts diff --git a/docs/plugins/events.md b/docs/plugins/events.md index 7ad4cf64..63072dc2 100644 --- a/docs/plugins/events.md +++ b/docs/plugins/events.md @@ -215,7 +215,7 @@ To listen for specific events, plugins can use the `addListener` method in `even } ``` -- onSurfaceOverlayClick - triggered when someone clicks on a overlay surface; the element to which the pin is attached is passed as an argument. Example argument: +- onSurfaceClick - triggered when someone clicks on a overlay surface; the element to which the pin is attached is passed as an argument. Example argument: ```javascript { diff --git a/package-lock.json b/package-lock.json index 016ad699..a1966db0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "crypto-js": "^4.2.0", "downshift": "^8.2.3", "eslint-config-next": "13.4.19", + "is-uuid": "^1.0.2", "molart": "github:davidhoksza/MolArt", "next": "13.4.19", "ol": "^8.1.0", @@ -49,6 +50,7 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.2", "@types/crypto-js": "^4.2.2", + "@types/is-uuid": "^1.0.2", "@types/jest": "^29.5.5", "@types/react-redux": "^7.1.26", "@types/redux-mock-store": "^1.0.6", @@ -2319,6 +2321,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/is-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-S+gWwUEApOjGCCO5LQrft4kciGWatvB0LyiyWTXSlDkclZBr6glSgstET573GsC5QPFdw2NeOw2PHOOMuTDFSQ==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -7968,6 +7976,11 @@ "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", "dev": true }, + "node_modules/is-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==" + }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -15724,6 +15737,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "@types/is-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-S+gWwUEApOjGCCO5LQrft4kciGWatvB0LyiyWTXSlDkclZBr6glSgstET573GsC5QPFdw2NeOw2PHOOMuTDFSQ==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -19786,6 +19805,11 @@ "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", "dev": true }, + "is-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==" + }, "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", diff --git a/package.json b/package.json index f3a8ec09..f44f22ef 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "crypto-js": "^4.2.0", "downshift": "^8.2.3", "eslint-config-next": "13.4.19", + "is-uuid": "^1.0.2", "molart": "github:davidhoksza/MolArt", "next": "13.4.19", "ol": "^8.1.0", @@ -63,6 +64,7 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.2", "@types/crypto-js": "^4.2.2", + "@types/is-uuid": "^1.0.2", "@types/jest": "^29.5.5", "@types/react-redux": "^7.1.26", "@types/redux-mock-store": "^1.0.6", diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx index 2ee7eb5b..03c3f495 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx @@ -1,7 +1,7 @@ import { ZERO } from '@/constants/common'; import { - searchedFromMapBioEntityElement, - searchedFromMapBioEntityElementRelatedSubmapSelector, + currentDrawerBioEntityRelatedSubmapSelector, + currentDrawerBioEntitySelector, } from '@/redux/bioEntity/bioEntity.selectors'; import { getChemicalsForBioEntityDrawerTarget, @@ -22,8 +22,8 @@ const TARGET_PREFIX: ElementSearchResultType = `ALIAS`; export const BioEntityDrawer = (): React.ReactNode => { const dispatch = useAppDispatch(); - const bioEntityData = useAppSelector(searchedFromMapBioEntityElement); - const relatedSubmap = useAppSelector(searchedFromMapBioEntityElementRelatedSubmapSelector); + const bioEntityData = useAppSelector(currentDrawerBioEntitySelector); + const relatedSubmap = useAppSelector(currentDrawerBioEntityRelatedSubmapSelector); const currentTargetId = bioEntityData?.id ? `${TARGET_PREFIX}:${bioEntityData.id}` : ''; const fetchChemicalsForTarget = (): void => { diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createFeatureFromExtent.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createFeatureFromExtent.ts index f79764b2..a7daf843 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createFeatureFromExtent.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createFeatureFromExtent.ts @@ -1,5 +1,18 @@ +import { FEATURE_TYPE } from '@/constants/features'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import isUUID from 'is-uuid'; import Feature from 'ol/Feature'; import Polygon, { fromExtent } from 'ol/geom/Polygon'; -export const createFeatureFromExtent = ([xMin, yMin, xMax, yMax]: number[]): Feature<Polygon> => - new Feature({ geometry: fromExtent([xMin, yMin, xMax, yMax]), type: 'surface' }); +export const createFeatureFromExtent = ( + [xMin, yMin, xMax, yMax]: number[], + entityId: OverlayBioEntityRender['id'], +): Feature<Polygon> => { + const isMarker = isUUID.anyNonNil(`${entityId}`); + + return new Feature({ + geometry: fromExtent([xMin, yMin, xMax, yMax]), + id: entityId, + type: isMarker ? FEATURE_TYPE.SURFACE_MARKER : FEATURE_TYPE.SURFACE_OVERLAY, + }); +}; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts index b294d492..e11025d8 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts @@ -1,6 +1,7 @@ -import { Fill, Stroke, Style } from 'ol/style'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; import Feature from 'ol/Feature'; import type Polygon from 'ol/geom/Polygon'; +import { Fill, Stroke, Style } from 'ol/style'; import { createFeatureFromExtent } from './createFeatureFromExtent'; const getBioEntityOverlayFeatureStyle = (color: string): Style => @@ -9,8 +10,9 @@ const getBioEntityOverlayFeatureStyle = (color: string): Style => export const createOverlayGeometryFeature = ( [xMin, yMin, xMax, yMax]: number[], color: string, + entityId: OverlayBioEntityRender['id'], ): Feature<Polygon> => { - const feature = createFeatureFromExtent([xMin, yMin, xMax, yMax]); + const feature = createFeatureFromExtent([xMin, yMin, xMax, yMax], entityId); feature.setStyle(getBioEntityOverlayFeatureStyle(color)); return feature; }; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.ts index cef98354..8a051f2f 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlaySubmapLinkRectangleFeature.ts @@ -1,4 +1,5 @@ /* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; import Feature from 'ol/Feature'; import type Polygon from 'ol/geom/Polygon'; import { createFeatureFromExtent } from './createFeatureFromExtent'; @@ -7,8 +8,9 @@ import { getOverlaySubmapLinkRectangleFeatureStyle } from './getOverlaySubmapLin export const createOverlaySubmapLinkRectangleFeature = ( [xMin, yMin, xMax, yMax]: number[], color: string | null, + entityId: OverlayBioEntityRender['id'], ): Feature<Polygon> => { - const feature = createFeatureFromExtent([xMin, yMin, xMax, yMax]); + const feature = createFeatureFromExtent([xMin, yMin, xMax, yMax], entityId); feature.setStyle(getOverlaySubmapLinkRectangleFeatureStyle(color)); return feature; }; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts index 93e24ec8..107add31 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts @@ -35,6 +35,7 @@ export const useOverlayFeatures = (): Feature<Polygon>[] | Feature<SimpleGeometr ...pointToProjection({ x: entity.x2, y: entity.y2 }), ], entity?.hexColor || color, + entity.id, ); }), [getOverlayBioEntityColorByAvailableProperties, markersRender, pointToProjection], @@ -67,6 +68,7 @@ export const useOverlayFeatures = (): Feature<Polygon>[] | Feature<SimpleGeometr ...pointToProjection({ x: xMax, y: entity.y2 }), ], entity.value === Infinity ? null : color, + entity.id, ); } @@ -77,6 +79,7 @@ export const useOverlayFeatures = (): Feature<Polygon>[] | Feature<SimpleGeometr ...pointToProjection({ x: xMax, y: entity.y2 }), ], color, + entity.id, ); } diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts index 28610fd8..80af09f6 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.test.ts @@ -24,7 +24,7 @@ describe('getPinFeature - subUtil', () => { }); it('should return id as name', () => { - expect(result.get('name')).toBe(bioEntity.id); + expect(result.get('id')).toBe(bioEntity.id); }); it('should return point parsed with point to projection', () => { diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts index 31d33f86..51d4f8d3 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts @@ -1,8 +1,10 @@ import { ZERO } from '@/constants/common'; import { HALF } from '@/constants/dividers'; +import { FEATURE_TYPE } from '@/constants/features'; import { Marker } from '@/redux/markers/markers.types'; import { BioEntity } from '@/types/models'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import isUUID from 'is-uuid'; import { Feature } from 'ol'; import { Point } from 'ol/geom'; @@ -10,6 +12,8 @@ export const getPinFeature = ( { x, y, width, height, id }: Pick<BioEntity, 'id' | 'width' | 'height' | 'x' | 'y'> | Marker, pointToProjection: UsePointToProjectionResult, ): Feature => { + const isMarker = isUUID.anyNonNil(`${id}`); + const point = { x: x + (width || ZERO) / HALF, y: y + (height || ZERO) / HALF, @@ -18,7 +22,7 @@ export const getPinFeature = ( const feature = new Feature({ geometry: new Point(pointToProjection(point)), id, - type: 'pin', + type: isMarker ? FEATURE_TYPE.PIN_ICON_MARKER : FEATURE_TYPE.PIN_ICON_BIOENTITY, }); return feature; diff --git a/src/components/Map/MapViewer/utils/listeners/mapFeatureClick/useMapFeatureClick.ts b/src/components/Map/MapViewer/utils/listeners/mapFeatureClick/useMapFeatureClick.ts deleted file mode 100644 index 2ab3ea05..00000000 --- a/src/components/Map/MapViewer/utils/listeners/mapFeatureClick/useMapFeatureClick.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { FeatureLike } from 'ol/Feature'; - -interface UseMapFeatureClickResult { - handleFeatureClick(feature: FeatureLike): void; -} - -export const useMapFeatureClick = (): UseMapFeatureClickResult => { - const handleFeatureClick = (feature: FeatureLike): void => { - console.log(feature.get('type')); - }; - - return { - handleFeatureClick, - }; -}; diff --git a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts index 3d66ff05..a957771e 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts @@ -1,12 +1,12 @@ +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { openContextMenu } from '@/redux/contextMenu/contextMenu.slice'; +import { MapSize } from '@/redux/map/map.types'; import { AppDispatch } from '@/redux/store'; -import { Pixel } from 'ol/pixel'; import { Coordinate } from 'ol/coordinate'; -import { MapSize } from '@/redux/map/map.types'; -import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { Pixel } from 'ol/pixel'; +import { getSearchResults } from '../mapSingleClick/getSearchResults'; import { handleDataReset } from '../mapSingleClick/handleDataReset'; import { handleSearchResultForRightClickAction } from './handleSearchResultForRightClickAction'; -import { getSearchResults } from '../mapSingleClick/getSearchResults'; /* prettier-ignore */ export const onMapRightClick = diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts index b1487583..f3ead6cc 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts @@ -1,5 +1,5 @@ import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks'; -import { openBioEntityDrawerById } from '@/redux/drawer/drawer.slice'; +import { openBioEntityDrawerById, selectTab } from '@/redux/drawer/drawer.slice'; import { AppDispatch } from '@/redux/store'; import { ElementSearchResult } from '@/types/models'; @@ -8,6 +8,7 @@ export const handleAliasResults = (dispatch: AppDispatch) => async ({ id }: ElementSearchResult): Promise<void> => { + dispatch(selectTab(`${id}`)); dispatch(openBioEntityDrawerById(id)); dispatch( getMultiBioEntity({ diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts new file mode 100644 index 00000000..3c1e5923 --- /dev/null +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick.ts @@ -0,0 +1,39 @@ +import { FEATURE_TYPE, PIN_ICON_ANY, SURFACE_ANY } from '@/constants/features'; +import { openBioEntityDrawerById } from '@/redux/drawer/drawer.slice'; +import { clearSearchData } from '@/redux/search/search.slice'; +import { AppDispatch } from '@/redux/store'; +import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus'; +import { FeatureLike } from 'ol/Feature'; + +interface HandleFeaturesClickResult { + shouldBlockCoordSearch: boolean; +} + +export const handleFeaturesClick = ( + features: FeatureLike[], + dispatch: AppDispatch, +): HandleFeaturesClickResult => { + let shouldBlockCoordSearch = false; + const pinFeatures = features.filter(feature => PIN_ICON_ANY.includes(feature.get('type'))); + const surfaceFeatures = features.filter(feature => SURFACE_ANY.includes(feature.get('type'))); + + pinFeatures.forEach(pin => { + const pinId = pin.get('id') as string | number; + PluginsEventBus.dispatchEvent('onPinIconClick', { id: pinId }); + + if (pin.get('type') === FEATURE_TYPE.PIN_ICON_BIOENTITY) { + dispatch(clearSearchData()); + dispatch(openBioEntityDrawerById(pinId)); + shouldBlockCoordSearch = true; + } + }); + + surfaceFeatures.forEach(surface => { + const surfaceId = surface.get('id') as string | number; + PluginsEventBus.dispatchEvent('onSurfaceClick', { id: surfaceId }); + }); + + return { + shouldBlockCoordSearch, + }; +}; diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts index 4661b41a..3d1e425c 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts @@ -1,16 +1,25 @@ import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { MapSize } from '@/redux/map/map.types'; import { AppDispatch } from '@/redux/store'; -import { MapInstance } from '@/types/map'; -import { MapBrowserEvent } from 'ol'; +import { Map, MapBrowserEvent } from 'ol'; +import { FeatureLike } from 'ol/Feature'; import { getSearchResults } from './getSearchResults'; import { handleDataReset } from './handleDataReset'; +import { handleFeaturesClick } from './handleFeaturesClick'; import { handleSearchResultAction } from './handleSearchResultAction'; /* prettier-ignore */ export const onMapSingleClick = (mapSize: MapSize, modelId: number, dispatch: AppDispatch) => - async ({ coordinate }: Pick<MapBrowserEvent<UIEvent>, 'coordinate'>, mapInstance: MapInstance): Promise<void> => { + async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => { + const featuresAtPixel: FeatureLike[] = []; + mapInstance.forEachFeatureAtPixel(pixel, (feature) => featuresAtPixel.push(feature)); + const { shouldBlockCoordSearch } = handleFeaturesClick(featuresAtPixel, dispatch); + + if (shouldBlockCoordSearch) { + return; + } + // side-effect below is to prevent complications with data update - old data may conflict with new data // so we need to reset all the data before updating dispatch(handleDataReset); diff --git a/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts b/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts new file mode 100644 index 00000000..47f99775 --- /dev/null +++ b/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts @@ -0,0 +1,26 @@ +import { PIN_ICON_ANY } from '@/constants/features'; +import { Map } from 'ol'; +import MapBrowserEvent from 'ol/MapBrowserEvent'; + +/* prettier-ignore */ +export const onPointerMove = + (mapInstance: Map, event: MapBrowserEvent<PointerEvent>): void => { + if (event.dragging) { + return; + } + + const pixel = mapInstance.getEventPixel(event.originalEvent); + const feature = mapInstance.forEachFeatureAtPixel(pixel, firstFeature => { + const isPinIcon = PIN_ICON_ANY.includes(firstFeature.get('type')); + if (!isPinIcon) { + return undefined; + } + + return firstFeature; + }); + + const target = mapInstance.getTarget(); + if (target && typeof target !== 'string' && 'style' in target) { + target.style.cursor = feature ? 'pointer' : ''; + } + }; diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts index 1c0b9fcf..7ca778b5 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts @@ -10,10 +10,10 @@ import { Pixel } from 'ol/pixel'; import { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useDebouncedCallback } from 'use-debounce'; -import { useMapFeatureClick } from './mapFeatureClick/useMapFeatureClick'; import { onMapRightClick } from './mapRightClick/onMapRightClick'; import { onMapSingleClick } from './mapSingleClick/onMapSingleClick'; import { onMapPositionChange } from './onMapPositionChange'; +import { onPointerMove } from './onPointerMove'; interface UseOlMapListenersInput { view: View; @@ -24,7 +24,6 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) const mapSize = useSelector(mapDataSizeSelector); const modelId = useSelector(currentModelIdSelector); const mapLastZoomValue = useSelector(mapDataLastZoomValue); - const { handleFeatureClick } = useMapFeatureClick(); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); const dispatch = useAppDispatch(); @@ -60,21 +59,19 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) return; } - const key = mapInstance.on('click', e => { - mapInstance.forEachFeatureAtPixel(e.pixel, handleFeatureClick); - }); + const key = mapInstance.on('pointermove', event => onPointerMove(mapInstance, event)); // eslint-disable-next-line consistent-return return () => unByKey(key); - }, [mapInstance, handleFeatureClick]); + }, [mapInstance]); useEffect(() => { if (!mapInstance) { return; } - const key = mapInstance.on('singleclick', ({ coordinate: ciird }) => - handleMapSingleClick({ coordinate }, mapInstance), + const key = mapInstance.on('singleclick', event => + handleMapSingleClick({ coordinate: event.coordinate, pixel: event.pixel }, mapInstance), ); // eslint-disable-next-line consistent-return diff --git a/src/constants/features.ts b/src/constants/features.ts new file mode 100644 index 00000000..4995bbcf --- /dev/null +++ b/src/constants/features.ts @@ -0,0 +1,9 @@ +export const FEATURE_TYPE = { + PIN_ICON_BIOENTITY: 'PIN_ICON_BIOENTITY', + PIN_ICON_MARKER: 'PIN_ICON_MARKER', + SURFACE_OVERLAY: 'SURFACE_OVERLAY', + SURFACE_MARKER: 'SURFACE_MARKER', +} as const; + +export const PIN_ICON_ANY = [FEATURE_TYPE.PIN_ICON_BIOENTITY, FEATURE_TYPE.PIN_ICON_MARKER]; +export const SURFACE_ANY = [FEATURE_TYPE.SURFACE_OVERLAY, FEATURE_TYPE.SURFACE_MARKER]; diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts index b9566eb7..8bf9877d 100644 --- a/src/redux/bioEntity/bioEntity.selectors.ts +++ b/src/redux/bioEntity/bioEntity.selectors.ts @@ -3,13 +3,19 @@ import { rootSelector } from '@/redux/root/root.selectors'; import { MultiSearchData } from '@/types/fetchDataState'; import { BioEntity, BioEntityContent, MapModel } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; -import { searchedChemicalsBioEntitesOfCurrentMapSelector } from '../chemicals/chemicals.selectors'; +import { + allChemicalsBioEntitesOfAllMapsSelector, + searchedChemicalsBioEntitesOfCurrentMapSelector, +} from '../chemicals/chemicals.selectors'; import { currentSelectedBioEntityIdSelector } from '../contextMenu/contextMenu.selector'; import { currentSearchedBioEntityId, currentSelectedSearchElement, } from '../drawer/drawer.selectors'; -import { searchedDrugsBioEntitesOfCurrentMapSelector } from '../drugs/drugs.selectors'; +import { + allDrugsBioEntitesOfAllMapsSelector, + searchedDrugsBioEntitesOfCurrentMapSelector, +} from '../drugs/drugs.selectors'; import { currentModelIdSelector, modelsDataSelector } from '../models/models.selectors'; export const bioEntitySelector = createSelector(rootSelector, state => state.bioEntity); @@ -122,3 +128,40 @@ export const allVisibleBioEntitiesSelector = createSelector( return [content, chemicals, drugs].flat(); }, ); + +export const allContentBioEntitesSelectorOfAllMaps = createSelector( + bioEntitySelector, + (bioEntities): BioEntity[] => { + if (!bioEntities) { + return []; + } + + return (bioEntities?.data || []) + .map(({ data }) => data || []) + .flat() + .map(({ bioEntity }) => bioEntity); + }, +); + +export const allBioEntitiesSelector = createSelector( + allContentBioEntitesSelectorOfAllMaps, + allChemicalsBioEntitesOfAllMapsSelector, + allDrugsBioEntitesOfAllMapsSelector, + (content, chemicals, drugs): BioEntity[] => { + return [content, chemicals, drugs].flat(); + }, +); + +export const currentDrawerBioEntitySelector = createSelector( + allBioEntitiesSelector, + currentSearchedBioEntityId, + (bioEntities, currentBioEntityId): BioEntity | undefined => + bioEntities.find(({ id }) => id === currentBioEntityId), +); + +export const currentDrawerBioEntityRelatedSubmapSelector = createSelector( + currentDrawerBioEntitySelector, + modelsDataSelector, + (bioEntity, models): MapModel | undefined => + models.find(({ idObject }) => idObject === bioEntity?.submodel?.mapId), +); diff --git a/src/redux/chemicals/chemicals.selectors.ts b/src/redux/chemicals/chemicals.selectors.ts index 03f829f8..bb8d4aee 100644 --- a/src/redux/chemicals/chemicals.selectors.ts +++ b/src/redux/chemicals/chemicals.selectors.ts @@ -41,6 +41,18 @@ export const searchedChemicalsBioEntitesOfCurrentMapSelector = createSelector( }, ); +export const allChemicalsBioEntitesOfAllMapsSelector = createSelector( + chemicalsSelector, + (chemicalsState): BioEntity[] => { + return (chemicalsState?.data || []) + .map(({ data }) => data || []) + .flat() + .map(({ targets }) => targets.map(({ targetElements }) => targetElements)) + .flat() + .flat(); + }, +); + export const loadingChemicalsStatusSelector = createSelector( chemicalsForSelectedSearchElementSelector, state => state?.loading, diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts index 3a72aa53..29333ec2 100644 --- a/src/redux/drawer/drawer.reducers.ts +++ b/src/redux/drawer/drawer.reducers.ts @@ -111,7 +111,6 @@ export const openBioEntityDrawerByIdReducer = ( state.isOpen = true; state.drawerName = 'bio-entity'; state.bioEntityDrawerState.bioentityId = action.payload; - state.searchDrawerState.selectedSearchElement = action.payload.toString(); }; export const getBioEntityDrugsForTargetReducers = ( diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts index a348517b..a0d51989 100644 --- a/src/redux/drawer/drawer.types.ts +++ b/src/redux/drawer/drawer.types.ts @@ -43,3 +43,6 @@ export type OpenReactionDrawerByIdAction = PayloadAction<OpenReactionDrawerByIdP export type OpenBioEntityDrawerByIdPayload = number | string; export type OpenBioEntityDrawerByIdAction = PayloadAction<OpenBioEntityDrawerByIdPayload>; + +export type SetSelectedSearchElementPayload = string; +export type SetSelectedSearchElementAction = PayloadAction<SetSelectedSearchElementPayload>; diff --git a/src/redux/drugs/drugs.selectors.ts b/src/redux/drugs/drugs.selectors.ts index 45e9c16c..f5c74de2 100644 --- a/src/redux/drugs/drugs.selectors.ts +++ b/src/redux/drugs/drugs.selectors.ts @@ -54,3 +54,15 @@ export const searchedDrugsBioEntitesOfCurrentMapSelector = createSelector( .filter(bioEntity => bioEntity.model === currentModelId); }, ); + +export const allDrugsBioEntitesOfAllMapsSelector = createSelector( + drugsSelector, + (drugsState): BioEntity[] => { + return (drugsState?.data || []) + .map(({ data }) => data || []) + .flat() + .map(({ targets }) => targets.map(({ targetElements }) => targetElements)) + .flat() + .flat(); + }, +); diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts index 5a5eb652..2969f8ab 100644 --- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts +++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts @@ -15,7 +15,7 @@ const PLUGINS_EVENTS = { onCenterChanged: 'onCenterChanged', onBioEntityClick: 'onBioEntityClick', onPinIconClick: 'onPinIconClick', - onSurfaceOverlayClick: 'onSurfaceOverlayClick', + onSurfaceClick: 'onSurfaceClick', }, search: { onSearch: 'onSearch', diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts index aa7221ab..66d4ab43 100644 --- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts +++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts @@ -24,7 +24,7 @@ export function dispatchEvent(type: 'onZoomChanged', data: ZoomChanged): void; export function dispatchEvent(type: 'onCenterChanged', data: CenteredCoordinates): void; export function dispatchEvent(type: 'onBioEntityClick', data: ClickedBioEntity): void; export function dispatchEvent(type: 'onPinIconClick', data: ClickedPinIcon): void; -export function dispatchEvent(type: 'onSurfaceOverlayClick', data: ClickedSurfaceOverlay): void; +export function dispatchEvent(type: 'onSurfaceClick', data: ClickedSurfaceOverlay): void; export function dispatchEvent(type: 'onSearch', data: SearchData): void; export function dispatchEvent(type: Events, data: EventsData): void { if (!ALLOWED_PLUGINS_EVENTS.includes(type)) throw new Error(`Invalid event type: ${type}`); diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts index 4260f399..a013af76 100644 --- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts +++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts @@ -7,7 +7,7 @@ export type OverlayEvents = | 'onRemoveDataOverlay' | 'onShowOverlay' | 'onHideOverlay' - | 'onSurfaceOverlayClick'; + | 'onSurfaceClick'; export type SubmapEvents = | 'onSubmapOpen' | 'onSubmapClose' -- GitLab