Skip to content
Snippets Groups Projects
Commit 5e8d7f4c authored by Piotr Gawron's avatar Piotr Gawron
Browse files

click on compartment/pathway in semantic view selects this element on map

parent dd4cb9f8
No related branches found
No related tags found
2 merge requests!275Resolve "cookie banner for matomo",!263Resolve "clicking on pathway/compartment in pathway and compartment view does not work"
Pipeline #96394 passed
Showing
with 157 additions and 21 deletions
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
......
......@@ -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());
});
......
......@@ -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;
}
......
......@@ -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({
......
......@@ -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,
......
......@@ -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());
......
......@@ -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;
}
......
......@@ -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 },
......
......@@ -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;
}
......
......@@ -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) {
......
......@@ -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);
}
}
......@@ -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([]);
});
});
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;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment