Skip to content
Snippets Groups Projects
Commit d14aa91d authored by Adrian Orłów's avatar Adrian Orłów
Browse files

feat: add base multisearch implementation

parent 40617a4a
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!163feat: add multisearch (MIN-210)
Pipeline #88061 passed
Showing
with 264 additions and 18 deletions
import { PIN_PATH2D, PIN_SIZE } from '@/constants/canvas';
import { PIN_PATH2D, PIN_SIZE, TEXT_COLOR } from '@/constants/canvas';
import { HALF, ONE_AND_HALF, QUARTER, THIRD, TWO_AND_HALF } from '@/constants/dividers';
import { DEFAULT_FONT_FAMILY } from '@/constants/font';
import { Point } from '@/types/map';
......@@ -12,6 +12,7 @@ const BIG_TEXT_VALUE = 100;
interface Args {
color: string;
value: number;
textColor?: string;
}
export const drawPinOnCanvas = (
......@@ -42,7 +43,7 @@ export const getTextPosition = (textWidth: number, textHeight: number): Point =>
});
export const drawNumberOnCanvas = (
{ value }: Pick<Args, 'value'>,
{ value, textColor }: Pick<Args, 'value' | 'textColor'>,
ctx: CanvasRenderingContext2D,
): void => {
const text = `${value}`;
......@@ -53,7 +54,7 @@ export const drawNumberOnCanvas = (
const textHeight = textMetrics.fontBoundingBoxAscent + textMetrics.fontBoundingBoxDescent;
const { x, y } = getTextPosition(textWidth, textHeight);
ctx.fillStyle = 'white';
ctx.fillStyle = textColor || TEXT_COLOR;
ctx.textBaseline = 'top';
ctx.font = `${fontSize}px ${DEFAULT_FONT_FAMILY}`;
ctx.fillText(text, x, y);
......@@ -70,7 +71,7 @@ export const getCanvasIcon = (
drawPinOnCanvas(args, ctx);
if (args?.value !== undefined) {
drawNumberOnCanvas({ value: args.value }, ctx);
drawNumberOnCanvas({ value: args.value, textColor: args?.textColor }, ctx);
}
return canvas;
......
......@@ -11,10 +11,12 @@ export const getBioEntitiesFeatures = (
pointToProjection,
type,
entityNumber,
activeIds,
}: {
pointToProjection: UsePointToProjectionResult;
type: PinType;
entityNumber: EntityNumber;
activeIds: (string | number)[];
},
): Feature[] => {
return bioEntites.map(bioEntity =>
......@@ -23,6 +25,7 @@ export const getBioEntitiesFeatures = (
type,
// pin's index number
value: entityNumber?.[bioEntity.elementId],
isActive: activeIds.includes(bioEntity.id),
}),
);
};
import { PINS_COLORS } from '@/constants/canvas';
import { PINS_COLORS, TEXT_COLOR } from '@/constants/canvas';
import { BioEntity } from '@/types/models';
import { PinType } from '@/types/pin';
import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString';
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import { Feature } from 'ol';
import { getPinFeature } from './getPinFeature';
import { getPinStyle } from './getPinStyle';
const INACTIVE_ELEMENT_OPACITY = 0.5;
export const getBioEntitySingleFeature = (
bioEntity: BioEntity,
{
pointToProjection,
type,
value,
isActive,
}: {
pointToProjection: UsePointToProjectionResult;
type: PinType;
value: number;
isActive: boolean;
},
): Feature => {
const color = isActive
? PINS_COLORS[type]
: addAlphaToHexString(PINS_COLORS.bioEntity, INACTIVE_ELEMENT_OPACITY);
const textColor = isActive
? TEXT_COLOR
: addAlphaToHexString(TEXT_COLOR, INACTIVE_ELEMENT_OPACITY);
const feature = getPinFeature(bioEntity, pointToProjection);
const style = getPinStyle({
color: PINS_COLORS[type],
color,
value,
textColor,
});
feature.setStyle(style);
......
......@@ -4,7 +4,15 @@ import Icon from 'ol/style/Icon';
import Style from 'ol/style/Style';
import { getCanvasIcon } from '../getCanvasIcon';
export const getPinStyle = ({ value, color }: { value?: number; color: string }): Style =>
export const getPinStyle = ({
value,
color,
textColor,
}: {
value?: number;
color: string;
textColor?: string;
}): Style =>
new Style({
image: new Icon({
displacement: [ZERO, PIN_SIZE.height],
......@@ -13,6 +21,7 @@ export const getPinStyle = ({ value, color }: { value?: number; color: string })
img: getCanvasIcon({
color,
value,
textColor,
}),
}),
});
/* eslint-disable no-magic-numbers */
import { searchedBioEntitesSelectorOfCurrentMap } from '@/redux/bioEntity/bioEntity.selectors';
import { searchedChemicalsBioEntitesOfCurrentMapSelector } from '@/redux/chemicals/chemicals.selectors';
import { searchedDrugsBioEntitesOfCurrentMapSelector } from '@/redux/drugs/drugs.selectors';
import {
allBioEntitesSelectorOfCurrentMap,
allVisibleBioEntitiesIdsSelector
} from '@/redux/bioEntity/bioEntity.selectors';
import {
allChemicalsBioEntitesOfCurrentMapSelector
} from '@/redux/chemicals/chemicals.selectors';
import {
allDrugsBioEntitesOfCurrentMapSelector
} from '@/redux/drugs/drugs.selectors';
import { entityNumberDataSelector } from '@/redux/entityNumber/entityNumber.selectors';
import { markersPinsOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
......@@ -16,9 +23,10 @@ import { getMarkersFeatures } from './getMarkersFeatures';
export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => {
const pointToProjection = usePointToProjection();
const contentBioEntites = useSelector(searchedBioEntitesSelectorOfCurrentMap);
const chemicalsBioEntities = useSelector(searchedChemicalsBioEntitesOfCurrentMapSelector);
const drugsBioEntities = useSelector(searchedDrugsBioEntitesOfCurrentMapSelector);
const activeIds = useSelector(allVisibleBioEntitiesIdsSelector);
const contentBioEntites = useSelector(allBioEntitesSelectorOfCurrentMap);
const chemicalsBioEntities = useSelector(allChemicalsBioEntitesOfCurrentMapSelector);
const drugsBioEntities = useSelector(allDrugsBioEntitesOfCurrentMapSelector);
const markersEntities = useSelector(markersPinsOfCurrentMapDataSelector);
const entityNumber = useSelector(entityNumberDataSelector);
......@@ -29,16 +37,19 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>
pointToProjection,
type: 'bioEntity',
entityNumber,
activeIds,
}),
getBioEntitiesFeatures(chemicalsBioEntities, {
pointToProjection,
type: 'chemicals',
entityNumber,
activeIds,
}),
getBioEntitiesFeatures(drugsBioEntities, {
pointToProjection,
type: 'drugs',
entityNumber,
activeIds,
}),
getMarkersFeatures(markersEntities, { pointToProjection }),
].flat(),
......@@ -49,6 +60,7 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>
pointToProjection,
markersEntities,
entityNumber,
activeIds,
],
);
......
import { allBioEntitiesElementsIdsSelector } from '@/redux/bioEntity/bioEntity.selectors';
import { currentSelectedSearchElement } from '@/redux/drawer/drawer.selectors';
import { selectTab } from '@/redux/drawer/drawer.slice';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { PluginsEventBus as EventBus } from '@/services/pluginsManager/pluginsEventBus';
import { ClickedPinIcon } from '@/services/pluginsManager/pluginsEventBus/pluginsEventBus.types';
import isUUID from 'is-uuid';
import { useCallback, useEffect } from 'react';
export const useHandlePinIconClick = (): void => {
const dispatch = useAppDispatch();
const currentTab = useAppSelector(currentSelectedSearchElement);
const idsTabs = useAppSelector(allBioEntitiesElementsIdsSelector);
const onPinIconClick = useCallback(
({ id }: ClickedPinIcon): void => {
const newTab = idsTabs[id];
const isTabAlreadySelected = newTab === currentTab;
const isMarker = isUUID.anyNonNil(`${id}`);
if (!newTab || isTabAlreadySelected || isMarker) {
return;
}
dispatch(selectTab(idsTabs[id]));
},
[idsTabs, dispatch, currentTab],
);
useEffect(() => {
EventBus.addLocalListener('onPinIconClick', onPinIconClick);
return () => {
EventBus.removeLocalListener('onPinIconClick', onPinIconClick);
};
}, [onPinIconClick]);
};
import { DEFAULT_ZOOM, OPTIONS } from '@/constants/map';
import { searchDistanceValSelector } from '@/redux/configuration/configuration.selectors';
import { resultDrawerOpen } from '@/redux/drawer/drawer.selectors';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import {
mapDataLastZoomValue,
......@@ -14,12 +16,11 @@ import { Pixel } from 'ol/pixel';
import { useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useDebouncedCallback } from 'use-debounce';
import { searchDistanceValSelector } from '@/redux/configuration/configuration.selectors';
import { resultDrawerOpen } from '@/redux/drawer/drawer.selectors';
import { onMapRightClick } from './mapRightClick/onMapRightClick';
import { onMapSingleClick } from './mapSingleClick/onMapSingleClick';
import { onMapPositionChange } from './onMapPositionChange';
import { onPointerMove } from './onPointerMove';
import { useHandlePinIconClick } from './pinIconClick/useHandlePinIconClick';
interface UseOlMapListenersInput {
view: View;
......@@ -36,6 +37,7 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput)
const coordinate = useRef<Coordinate>([]);
const pixel = useRef<Pixel>([]);
const dispatch = useAppDispatch();
useHandlePinIconClick(modelId);
const handleRightClick = useDebouncedCallback(
onMapRightClick(mapSize, modelId, dispatch),
......
......@@ -11,7 +11,7 @@ export const PIN_SIZE = {
export const PINS_COLORS: Record<PinType, string> = {
drugs: '#F48C41',
chemicals: '#640CE3',
chemicals: '#008325',
bioEntity: '#106AD7',
};
......@@ -22,4 +22,6 @@ export const PINS_COLOR_WITH_NONE: Record<PinTypeWithNone, string> = {
export const LINE_COLOR = '#00AAFF';
export const TEXT_COLOR = '#FFFFFF';
export const LINE_WIDTH = 6;
import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { rootSelector } from '@/redux/root/root.selectors';
import { ElementIdTabObj } from '@/types/elements';
import { MultiSearchData } from '@/types/fetchDataState';
import { BioEntity, BioEntityContent, MapModel } from '@/types/models';
import { createSelector } from '@reduxjs/toolkit';
import {
allChemicalsBioEntitesOfAllMapsSelector,
allChemicalsIdTabSelectorOfCurrentMap,
searchedChemicalsBioEntitesOfCurrentMapSelector,
} from '../chemicals/chemicals.selectors';
import { currentSelectedBioEntityIdSelector } from '../contextMenu/contextMenu.selector';
......@@ -14,6 +16,7 @@ import {
} from '../drawer/drawer.selectors';
import {
allDrugsBioEntitesOfAllMapsSelector,
allDrugsIdTabSelectorOfCurrentMap,
searchedDrugsBioEntitesOfCurrentMapSelector,
} from '../drugs/drugs.selectors';
import { currentModelIdSelector, modelsDataSelector } from '../models/models.selectors';
......@@ -92,6 +95,44 @@ export const searchedBioEntitesSelectorOfCurrentMap = createSelector(
},
);
export const allBioEntitesSelectorOfCurrentMap = createSelector(
bioEntitySelector,
currentModelIdSelector,
(bioEntities, currentModelId): BioEntity[] => {
if (!bioEntities) {
return [];
}
return (bioEntities?.data || [])
.map(({ data }) => data || [])
.flat()
.filter(({ bioEntity }) => bioEntity.model === currentModelId)
.map(({ bioEntity }) => bioEntity);
},
);
export const allBioEntitesIdTabSelectorOfCurrentMap = createSelector(
bioEntitySelector,
currentModelIdSelector,
(bioEntities, currentModelId): ElementIdTabObj => {
if (!bioEntities) {
return {};
}
return Object.fromEntries(
(bioEntities?.data || [])
.map(({ data, searchQueryElement }): [typeof data, string] => [data, searchQueryElement])
.map(([data, tab]) =>
(data || [])
.flat()
.filter(({ bioEntity }) => bioEntity.model === currentModelId)
.map(d => [d.bioEntity.id, tab]),
)
.flat(),
);
},
);
export const numberOfBioEntitiesSelector = createSelector(
bioEntitiesForSelectedSearchElement,
state => (state?.data ? state.data.length : SIZE_OF_EMPTY_ARRAY),
......@@ -129,6 +170,13 @@ export const allVisibleBioEntitiesSelector = createSelector(
},
);
export const allVisibleBioEntitiesIdsSelector = createSelector(
allVisibleBioEntitiesSelector,
(elements): (string | number)[] => {
return elements.map(e => e.id);
},
);
export const allContentBioEntitesSelectorOfAllMaps = createSelector(
bioEntitySelector,
(bioEntities): BioEntity[] => {
......@@ -152,6 +200,19 @@ export const allBioEntitiesSelector = createSelector(
},
);
export const allBioEntitiesElementsIdsSelector = createSelector(
allBioEntitesIdTabSelectorOfCurrentMap,
allChemicalsIdTabSelectorOfCurrentMap,
allDrugsIdTabSelectorOfCurrentMap,
(content, chemicals, drugs): ElementIdTabObj => {
return {
...content,
...chemicals,
...drugs,
};
},
);
export const currentDrawerBioEntitySelector = createSelector(
allBioEntitiesSelector,
currentSearchedBioEntityId,
......
import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { rootSelector } from '@/redux/root/root.selectors';
import { ElementId, ElementIdTabObj, Tab } from '@/types/elements';
import { MultiSearchData } from '@/types/fetchDataState';
import { BioEntity, Chemical } from '@/types/models';
import { createSelector } from '@reduxjs/toolkit';
......@@ -41,6 +42,47 @@ export const searchedChemicalsBioEntitesOfCurrentMapSelector = createSelector(
},
);
export const allChemicalsBioEntitesOfCurrentMapSelector = createSelector(
chemicalsSelector,
currentModelIdSelector,
(chemicalsState, currentModelId): BioEntity[] => {
return (chemicalsState?.data || [])
.map(({ data }) => data || [])
.flat()
.map(({ targets }) => targets.map(({ targetElements }) => targetElements))
.flat()
.flat()
.filter(bioEntity => bioEntity.model === currentModelId);
},
);
export const allChemicalsIdTabSelectorOfCurrentMap = createSelector(
chemicalsSelector,
currentModelIdSelector,
(chemicalsState, currentModelId): ElementIdTabObj => {
if (!chemicalsState) {
return {};
}
return Object.fromEntries(
(chemicalsState?.data || [])
.map(({ data, searchQueryElement }): [typeof data, string] => [data, searchQueryElement])
.map(([data, tab]) =>
(data || []).map(({ targets }): [ElementId, Tab][] =>
targets
.map(({ targetElements }) => targetElements)
.flat()
.flat()
.filter(bioEntity => bioEntity.model === currentModelId)
.map(bioEntity => [bioEntity.id, tab]),
),
)
.flat()
.flat(),
);
},
);
export const allChemicalsBioEntitesOfAllMapsSelector = createSelector(
chemicalsSelector,
(chemicalsState): BioEntity[] => {
......
import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { rootSelector } from '@/redux/root/root.selectors';
import { ElementId, ElementIdTabObj, Tab } from '@/types/elements';
import { MultiSearchData } from '@/types/fetchDataState';
import { BioEntity, Drug } from '@/types/models';
import { createSelector } from '@reduxjs/toolkit';
......@@ -55,6 +56,47 @@ export const searchedDrugsBioEntitesOfCurrentMapSelector = createSelector(
},
);
export const allDrugsBioEntitesOfCurrentMapSelector = createSelector(
drugsSelector,
currentModelIdSelector,
(drugsState, currentModelId): BioEntity[] => {
return (drugsState?.data || [])
.map(({ data }) => data || [])
.flat()
.map(({ targets }) => targets.map(({ targetElements }) => targetElements))
.flat()
.flat()
.filter(bioEntity => bioEntity.model === currentModelId);
},
);
export const allDrugsIdTabSelectorOfCurrentMap = createSelector(
drugsSelector,
currentModelIdSelector,
(drugsState, currentModelId): ElementIdTabObj => {
if (!drugsState) {
return {};
}
return Object.fromEntries(
(drugsState?.data || [])
.map(({ data, searchQueryElement }): [typeof data, string] => [data, searchQueryElement])
.map(([data, tab]) =>
(data || []).map(({ targets }): [ElementId, Tab][] =>
targets
.map(({ targetElements }) => targetElements)
.flat()
.flat()
.filter(bioEntity => bioEntity.model === currentModelId)
.map(bioEntity => [bioEntity.id, tab]),
),
)
.flat()
.flat(),
);
},
);
export const allDrugsBioEntitesOfAllMapsSelector = createSelector(
drugsSelector,
(drugsState): BioEntity[] => {
......
......@@ -30,3 +30,5 @@ export const ALLOWED_PLUGINS_EVENTS = Object.values(PLUGINS_EVENTS).flatMap(obj
);
export const LISTENER_NOT_FOUND = -1;
export const LOCAL_LISTENER_ID = 'local';
/* eslint-disable no-magic-numbers */
import { CreatedOverlay, MapOverlay } from '@/types/models';
import { showToast } from '@/utils/showToast';
import { ERROR_INVALID_EVENT_TYPE, ERROR_PLUGIN_CRASH } from '../errorMessages';
import {
ALLOWED_PLUGINS_EVENTS,
LISTENER_NOT_FOUND,
LOCAL_LISTENER_ID,
} from './pluginsEventBus.constants';
import type {
CenteredCoordinates,
ClickedBioEntity,
......@@ -13,8 +19,6 @@ import type {
SearchData,
ZoomChanged,
} from './pluginsEventBus.types';
import { ALLOWED_PLUGINS_EVENTS, LISTENER_NOT_FOUND } from './pluginsEventBus.constants';
import { ERROR_INVALID_EVENT_TYPE, ERROR_PLUGIN_CRASH } from '../errorMessages';
export function dispatchEvent(type: 'onPluginUnload', data: PluginUnloaded): void;
export function dispatchEvent(type: 'onAddDataOverlay', createdOverlay: CreatedOverlay): void;
......@@ -67,6 +71,10 @@ export const PluginsEventBus: PluginsEventBusType = {
});
},
addLocalListener: (type: Events, callback: (data: unknown) => void) => {
PluginsEventBus.addListener(LOCAL_LISTENER_ID, LOCAL_LISTENER_ID, type, callback);
},
removeListener: (hash: string, type: Events, callback: unknown) => {
const eventIndex = PluginsEventBus.events.findIndex(
event => event.hash === hash && event.type === type && event.callback === callback,
......@@ -79,6 +87,10 @@ export const PluginsEventBus: PluginsEventBusType = {
}
},
removeLocalListener: (type: Events, callback: unknown) => {
PluginsEventBus.removeListener(LOCAL_LISTENER_ID, type, callback);
},
removeAllListeners: (hash: string) => {
PluginsEventBus.events = PluginsEventBus.events.filter(event => event.hash !== hash);
},
......
......@@ -102,7 +102,9 @@ export type PluginsEventBusType = {
type: Events,
callback: (data: unknown) => void,
) => void;
addLocalListener: <T>(type: Events, callback: (data: T) => void) => void;
removeListener: (hash: string, type: Events, callback: unknown) => void;
removeLocalListener: <T>(type: Events, callback: T) => void;
removeAllListeners: (hash: string) => void;
dispatchEvent: typeof dispatchEvent;
};
export type ElementId = string | number;
export type Tab = string;
export type ElementIdTab = [ElementId, Tab];
export type ElementIdTabObj = Record<ElementId, Tab>;
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