diff --git a/CHANGELOG b/CHANGELOG index fa563f9d10a89561e1f4f27b0eb4bd028d6649be..112b3e102d47b4eb89e5e25ae1ced92191a02027 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 bead3d12015b534bd89e5f5cc0beff50f93caeae..33216241a6256adb94cfb5414169ebcc5b6f0c25 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 d5d284892aeb7aeff9b263df4d8db19e93d50dfb..a52cd458507ab2dcf153c14a04acefdf7ae82498 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 996d2d1c32589c53684be6a25e362289b0ca9177..0fa4d06f1128a897b2efb484962d6342d05cb06c 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 0fedae9f89f1f2c788b52e133b64b58b1e5f7623..f86ee0fe825823f7b186616676c4fa3efc700388 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 138ab32fd28777c55abbf9a0b09c972e34b883e4..b91b2b33c59d4b6b3532edf5bf3fcbc43c31f995 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 e04f2880a546ad753225e2301de34487753f3c53..2b695b6e6b892a118e15eadf811bea4b63d43fb1 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 33013f2182bcfc3a424a5568ec7be512966e4d4d..f0cb170f1013b63c30247baf4af8f730be7be640 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 49ec30027077460bfdde9b9ff2b6e89961c0273a..b884f07959ef46d2d11e36372ad2bfa9ee9ab6fc 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 207c91c02692e871a917dd463f7e5e111e5eb21f..6b7a9b9df402651327afef1f245c34fe9f747679 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 8a69f07b6570907182960db7bd2e770d5b6e6644..363a01553ff227d30676d7384e0b3fc91265fd6b 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 cf2a56fa4b8be87addbede3d14a1971310bf5f7f..d24ece7cd88fbe2fdd5f6f6a9b62f229ea668a6c 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 af110ac0f2bf6e3813d09daa697427bf445a93da..70e392de47fcf2be6aabd9ee800069b60c682a58 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; };