From 5e8d7f4cc7c93c702045a6e58134bc42206c978d Mon Sep 17 00:00:00 2001 From: Piotr Gawron <p.gawron@atcomp.pl> Date: Fri, 18 Oct 2024 09:23:50 +0200 Subject: [PATCH] click on compartment/pathway in semantic view selects this element on map --- CHANGELOG | 6 ++ .../mapRightClick/onMapRightClick.test.ts | 8 +- .../mapRightClick/onMapRightClick.ts | 5 +- .../mapSingleClick/getSearchResults.test.ts | 3 + .../mapSingleClick/getSearchResults.ts | 11 ++- .../mapSingleClick/onMapSingleClick.test.ts | 6 ++ .../mapSingleClick/onMapSingleClick.ts | 8 +- .../utils/listeners/useOlMapListeners.ts | 25 +++++- .../Map/MapViewer/utils/useOlMap.ts | 2 +- .../map/triggerSearch/searchByCoordinates.ts | 4 + .../map/triggerSearch/triggerSearch.ts | 2 +- .../search/getElementsByCoordinates.test.ts | 18 ++++- src/utils/search/getElementsByCoordinates.ts | 80 ++++++++++++++++++- 13 files changed, 157 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fa563f9d..112b3e10 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +minerva-front (18.0.0~beta.6) stable; urgency=medium + * Small improvements: clicking on pathway/compartment in semantic view + highlight element (#300) + + -- Piotr Gawron <piotr.gawron@uni.lu> Fri, 18 Oct 2024 13:00:00 +0200 + minerva-front (18.0.0~beta.5) stable; urgency=medium * Small improvements: when ToS is defined ask user to accept it (#298) * Small improvements: there is a waiting spinner after clicking on download diff --git a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts index bead3d12..33216241 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts @@ -47,7 +47,7 @@ describe('onMapRightClick - util', () => { minZoom: 2, maxZoom: 9, }; - const handler = onMapRightClick(mapSize, modelId, dispatch); + const handler = onMapRightClick(mapSize, modelId, dispatch, false); const coordinate = [90, 90]; const pixel = [250, 250]; @@ -74,7 +74,7 @@ describe('onMapRightClick - util', () => { minZoom: 2, maxZoom: 9, }; - const handler = onMapRightClick(mapSize, modelId, dispatch); + const handler = onMapRightClick(mapSize, modelId, dispatch, false); const coordinate = [90, 90]; const point = { x: 180.0008084837557, y: 179.99919151624428 }; const pixel = [250, 250]; @@ -110,7 +110,7 @@ describe('onMapRightClick - util', () => { .reply(HttpStatusCode.Ok, [ELEMENT_SEARCH_RESULT_MOCK_ALIAS]); it('does fire search result for right click action handler', async () => { - const handler = onMapRightClick(mapSize, modelId, dispatch); + const handler = onMapRightClick(mapSize, modelId, dispatch, false); await handler(coordinate, pixel); await waitFor(() => expect(handleSearchResultForRightClickActionSpy).toBeCalled()); }); @@ -139,7 +139,7 @@ describe('onMapRightClick - util', () => { .reply(HttpStatusCode.Ok, [ELEMENT_SEARCH_RESULT_MOCK_REACTION]); it('does fire search result for right click action - handle reaction', async () => { - const handler = onMapRightClick(mapSize, modelId, dispatch); + const handler = onMapRightClick(mapSize, modelId, dispatch, false); await handler(coordinate, pixel); await waitFor(() => expect(handleSearchResultForRightClickActionSpy).toBeCalled()); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts index d5d28489..a52cd458 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts @@ -13,7 +13,8 @@ import { handleSearchResultForRightClickAction } from './handleSearchResultForRi /* prettier-ignore */ export const onMapRightClick = - (mapSize: MapSize, modelId: number, dispatch: AppDispatch) => async (coordinate: Coordinate, pixel: Pixel): Promise<void> => { + (mapSize: MapSize, modelId: number,dispatch: AppDispatch, shouldConsiderZoomLevel:boolean, + considerZoomLevel?:number, ) => async (coordinate: Coordinate, pixel: Pixel): Promise<void> => { const [lng, lat] = toLonLat(coordinate); const point = latLngToPoint([lat, lng], mapSize); @@ -22,7 +23,7 @@ export const onMapRightClick = dispatch(handleDataReset); dispatch(openContextMenu(pixel)); - const { searchResults } = await getSearchResults({ coordinate, mapSize, modelId }); + const { searchResults } = await getSearchResults({ coordinate, mapSize, modelId, shouldConsiderZoomLevel, considerZoomLevel }); if (!searchResults || searchResults.length === SIZE_OF_EMPTY_ARRAY) { return; } diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.test.ts index 996d2d1c..0fa4d06f 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.test.ts @@ -34,6 +34,7 @@ describe('getSearchResults - util', () => { coordinate, mapSize, modelId, + shouldConsiderZoomLevel: false, }); expect(result).toEqual({ @@ -66,6 +67,7 @@ describe('getSearchResults - util', () => { coordinate, mapSize, modelId, + shouldConsiderZoomLevel: false, }); expect(result).toEqual({ @@ -100,6 +102,7 @@ describe('getSearchResults - util', () => { coordinate, mapSize, modelId, + shouldConsiderZoomLevel: false, }); expect(result).toEqual({ diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.ts index 0fedae9f..f86ee0fe 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getSearchResults.ts @@ -10,19 +10,28 @@ interface GetSearchResultsInput { coordinate: Coordinate; mapSize: MapSize; modelId: number; + shouldConsiderZoomLevel: boolean; + considerZoomLevel?: number; } export const getSearchResults = async ({ coordinate, mapSize, modelId, + shouldConsiderZoomLevel, + considerZoomLevel, }: GetSearchResultsInput): Promise<{ searchResults: ElementSearchResult[] | undefined; point: Point; }> => { const [lng, lat] = toLonLat(coordinate); const point = latLngToPoint([lat, lng], mapSize); - const searchResults = await getElementsByPoint({ point, currentModelId: modelId }); + const searchResults = await getElementsByPoint({ + point, + currentModelId: modelId, + shouldConsiderZoomLevel, + considerZoomLevel, + }); return { searchResults, diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts index 138ab32f..b91b2b33 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.test.ts @@ -68,6 +68,7 @@ describe('onMapSingleClick - util', () => { ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, [], + false, ); const coordinate = [90, 90]; const event = getEvent(coordinate); @@ -102,6 +103,7 @@ describe('onMapSingleClick - util', () => { ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, [], + false, ); const coordinate = [90, 90]; const point = { x: 180.0008084837557, y: 179.99919151624428 }; @@ -143,6 +145,7 @@ describe('onMapSingleClick - util', () => { ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, [], + false, ); const coordinate = [180, 180]; const point = { x: 360.0032339350228, y: 359.9967660649771 }; @@ -202,6 +205,7 @@ describe('onMapSingleClick - util', () => { ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, [], + false, ); await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).not.toBeCalled()); @@ -242,6 +246,7 @@ describe('onMapSingleClick - util', () => { ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, [], + false, ); await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).toBeCalled()); @@ -284,6 +289,7 @@ describe('onMapSingleClick - util', () => { ZOOM_MOCK, IS_RESULT_DRAWER_OPEN_MOCK, [], + false, ); await handler(event, mapInstanceMock); await waitFor(() => expect(handleSearchResultActionSpy).toBeCalled()); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts index e04f2880..2b695b6e 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts @@ -15,7 +15,9 @@ import { handleSearchResultAction } from './handleSearchResultAction'; /* prettier-ignore */ export const onMapSingleClick = (mapSize: MapSize, modelId: number, dispatch: AppDispatch, searchDistance: string | undefined, maxZoom: number, zoom: number, isResultDrawerOpen: boolean, - comments: Comment[]) => + comments: Comment[], shouldConsiderZoomLevel:boolean, + considerZoomLevel?:number, + ) => async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => { const [lng, lat] = toLonLat(coordinate); const point = latLngToPoint([lat, lng], mapSize); @@ -34,7 +36,9 @@ export const onMapSingleClick = // so we need to reset all the data before updating dispatch(handleDataReset); - const {searchResults} = await getSearchResults({ coordinate, mapSize, modelId }); + const {searchResults} = await getSearchResults({ coordinate, mapSize, modelId, shouldConsiderZoomLevel, + considerZoomLevel, + }); if (!searchResults || searchResults.length === SIZE_OF_EMPTY_ARRAY) { return; } diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts index 33013f21..f0cb170f 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts @@ -7,7 +7,7 @@ import { mapDataMaxZoomValue, mapDataSizeSelector, } from '@/redux/map/map.selectors'; -import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { currentModelIdSelector, currentModelSelector } from '@/redux/models/models.selectors'; import { MapInstance } from '@/types/map'; import { View } from 'ol'; import { unByKey } from 'ol/Observable'; @@ -17,6 +17,12 @@ import { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useDebouncedCallback } from 'use-debounce'; import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors'; +import { currentBackgroundSelector } from '@/redux/backgrounds/background.selectors'; +import { + PATHWAYS_AND_COMPARTMENTS_BACKGROUND, + SEMANTIC_BACKGROUND, +} from '@/redux/backgrounds/backgrounds.constants'; +import { TWO } from '@/constants/common'; import { onMapRightClick } from './mapRightClick/onMapRightClick'; import { onMapSingleClick } from './mapSingleClick/onMapSingleClick'; import { onMapPositionChange } from './onMapPositionChange'; @@ -30,10 +36,13 @@ interface UseOlMapListenersInput { export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput): void => { const mapSize = useSelector(mapDataSizeSelector); + const model = useSelector(currentModelSelector); const modelId = useSelector(currentModelIdSelector); + const modelMinZoom = model?.minZoom || TWO; + const lastZoom = useSelector(mapDataLastZoomValue) || TWO; + const background = useSelector(currentBackgroundSelector); const searchDistance = useSelector(searchDistanceValSelector); const maxZoom = useSelector(mapDataMaxZoomValue); - const lastZoom = useSelector(mapDataLastZoomValue); const isResultDrawerOpen = useSelector(resultDrawerOpen); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); @@ -44,7 +53,14 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) useHandlePinIconClick(); const handleRightClick = useDebouncedCallback( - onMapRightClick(mapSize, modelId, dispatch), + onMapRightClick( + mapSize, + modelId, + dispatch, + background?.name === SEMANTIC_BACKGROUND || + background?.name === PATHWAYS_AND_COMPARTMENTS_BACKGROUND, + lastZoom - modelMinZoom, + ), OPTIONS.clickPersistTime, { leading: false, @@ -67,6 +83,9 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) lastZoom || DEFAULT_ZOOM, isResultDrawerOpen, comments, + background?.name === SEMANTIC_BACKGROUND || + background?.name === PATHWAYS_AND_COMPARTMENTS_BACKGROUND, + lastZoom - modelMinZoom, ), OPTIONS.clickPersistTime, { leading: false }, diff --git a/src/components/Map/MapViewer/utils/useOlMap.ts b/src/components/Map/MapViewer/utils/useOlMap.ts index 49ec3002..b884f079 100644 --- a/src/components/Map/MapViewer/utils/useOlMap.ts +++ b/src/components/Map/MapViewer/utils/useOlMap.ts @@ -25,7 +25,7 @@ export const useOlMap: UseOlMap = ({ target } = {}) => { useOlMapListeners({ view, mapInstance }); useEffect(() => { - // checking if innerHTML is empty due to possibility of target element cloning by openlayers map instance + // checking if innerHTML is empty due to possibility of target element cloning by OpenLayers map instance if (!mapRef.current || mapRef.current.innerHTML !== '') { return; } diff --git a/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts b/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts index 207c91c0..6b7a9b9d 100644 --- a/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts +++ b/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts @@ -12,8 +12,10 @@ import { Coordinates } from './triggerSearch.types'; export const searchByCoordinates = async ( coordinates: Coordinates, modelId: number, + shouldConsiderZoomLevel: boolean, hasFitBounds?: boolean, fitBoundsZoom?: number, + considerZoomLevel?: number, ): Promise<void> => { const { dispatch, getState } = store; // side-effect below is to prevent complications with data update - old data may conflict with new data @@ -28,6 +30,8 @@ export const searchByCoordinates = async ( const searchResults = await getElementsByPoint({ point: coordinates, currentModelId: modelId, + shouldConsiderZoomLevel, + considerZoomLevel, }); if (!searchResults || searchResults?.length === SIZE_OF_EMPTY_ARRAY) { diff --git a/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts b/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts index 8a69f07b..363a0155 100644 --- a/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts +++ b/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts @@ -28,6 +28,6 @@ export async function triggerSearch(params: SearchParams): Promise<void> { throw new Error(ERROR_INVALID_MODEL_ID_TYPE); } - searchByCoordinates(params.coordinates, params.modelId, params.fitBounds, params.zoom); + searchByCoordinates(params.coordinates, params.modelId, false, params.fitBounds, params.zoom); } } diff --git a/src/utils/search/getElementsByCoordinates.test.ts b/src/utils/search/getElementsByCoordinates.test.ts index cf2a56fa..d24ece7c 100644 --- a/src/utils/search/getElementsByCoordinates.test.ts +++ b/src/utils/search/getElementsByCoordinates.test.ts @@ -18,7 +18,11 @@ describe('getElementsByPoint - utils', () => { .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, currentModelId)) .reply(HttpStatusCode.Ok, elementSearchResultFixture); - const response = await getElementsByPoint({ point, currentModelId }); + const response = await getElementsByPoint({ + point, + currentModelId, + shouldConsiderZoomLevel: false, + }); expect(response).toEqual(elementSearchResultFixture); }); @@ -27,7 +31,11 @@ describe('getElementsByPoint - utils', () => { .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, currentModelId)) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - const response = await getElementsByPoint({ point, currentModelId }); + const response = await getElementsByPoint({ + point, + currentModelId, + shouldConsiderZoomLevel: false, + }); expect(response).toEqual(undefined); }); @@ -36,7 +44,11 @@ describe('getElementsByPoint - utils', () => { .onGet(apiPath.getSingleBioEntityContentsStringWithCoordinates(point, currentModelId)) .reply(HttpStatusCode.Ok, []); - const response = await getElementsByPoint({ point, currentModelId }); + const response = await getElementsByPoint({ + point, + currentModelId, + shouldConsiderZoomLevel: false, + }); expect(response).toEqual([]); }); }); diff --git a/src/utils/search/getElementsByCoordinates.ts b/src/utils/search/getElementsByCoordinates.ts index af110ac0..70e392de 100644 --- a/src/utils/search/getElementsByCoordinates.ts +++ b/src/utils/search/getElementsByCoordinates.ts @@ -1,25 +1,97 @@ import { elementSearchResult } from '@/models/elementSearchResult'; import { apiPath } from '@/redux/apiPath'; -import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { axiosInstance, axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; import { Point } from '@/types/map'; -import { ElementSearchResult } from '@/types/models'; +import { BioEntity, ElementSearchResult } from '@/types/models'; import { z } from 'zod'; +import { ONE, ZERO } from '@/constants/common'; import { validateDataUsingZodSchema } from '../validateDataUsingZodSchema'; +interface FirstVisibleParentArgs { + bioEntity: BioEntity; + considerZoomLevel: number; +} + +export const getFirstVisibleParent = async ({ + bioEntity, + considerZoomLevel, +}: FirstVisibleParentArgs): Promise<BioEntity> => { + let parentId = bioEntity.complex; + if (!parentId) { + parentId = bioEntity.compartment; + } + if (parentId) { + const parentResponse = await axiosInstanceNewAPI.get<BioEntity>( + apiPath.getElementById(parentId, bioEntity.model), + ); + const parent = parentResponse.data; + if (parseInt(parent.visibilityLevel, 10) > Math.ceil(considerZoomLevel)) { + return getFirstVisibleParent({ + bioEntity: parent, + considerZoomLevel, + }); + } + return parent; + } + // eslint-disable-next-line no-console + console.log(`Cannot find visible parent for object. (zoomLevel=${considerZoomLevel})`, bioEntity); + return bioEntity; +}; + interface Args { point: Point; currentModelId: number; + shouldConsiderZoomLevel: boolean; + considerZoomLevel?: number; } +const FRACTIONAL_ZOOM_AT_WHICH_IMAGE_LAYER_CHANGE = 0.415; + export const getElementsByPoint = async ({ point, currentModelId, + shouldConsiderZoomLevel, + considerZoomLevel, }: Args): Promise<ElementSearchResult[] | undefined> => { + let result: ElementSearchResult[]; const response = await axiosInstance.get<ElementSearchResult[]>( apiPath.getSingleBioEntityContentsStringWithCoordinates(point, currentModelId), ); - const isDataValid = validateDataUsingZodSchema(response.data, z.array(elementSearchResult)); - return isDataValid ? response.data : undefined; + if (!isDataValid) { + return undefined; + } + result = response.data; + + if (shouldConsiderZoomLevel && result.length > ZERO && result[ZERO].type === 'ALIAS') { + const elementResponse = await axiosInstanceNewAPI.get<BioEntity>( + apiPath.getElementById(result[ZERO].id, result[ZERO].modelId), + ); + const element = elementResponse.data; + if ( + parseInt(element.visibilityLevel, 10) - (ONE - FRACTIONAL_ZOOM_AT_WHICH_IMAGE_LAYER_CHANGE) > + (considerZoomLevel || Number.MAX_SAFE_INTEGER) + ) { + const visibleParent = await getFirstVisibleParent({ + bioEntity: element, + considerZoomLevel: considerZoomLevel || Number.MAX_SAFE_INTEGER, + }); + let id: number; + if (typeof visibleParent.id === 'string') { + id = parseInt(visibleParent.id, 10); + } else { + id = visibleParent.id; + } + result = [ + { + id, + type: 'ALIAS', + modelId: visibleParent.model, + }, + ]; + } + } + + return result; }; -- GitLab