diff --git a/package-lock.json b/package-lock.json index 18eaa14c5846910e2f3e54e7f06fffe2987b4f93..ff30a5f424e87ddc41de6f6b36b62cd92e0cd417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "molart": "github:davidhoksza/MolArt", "next": "13.4.19", "ol": "^8.1.0", + "polished": "^4.3.1", "postcss": "8.4.29", "query-string": "7.1.3", "react": "18.2.0", @@ -11127,6 +11128,17 @@ "node": ">=8" } }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/postcss": { "version": "8.4.29", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", @@ -22127,6 +22139,14 @@ "find-up": "^4.0.0" } }, + "polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "requires": { + "@babel/runtime": "^7.17.8" + } + }, "postcss": { "version": "8.4.29", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", diff --git a/package.json b/package.json index 54dfdef6f81b5365f64141c7b74a92f419bdae94..5dfdf66c2a78719e09c8182a366f6b2433ced51e 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "molart": "github:davidhoksza/MolArt", "next": "13.4.19", "ol": "^8.1.0", + "polished": "^4.3.1", "postcss": "8.4.29", "query-string": "7.1.3", "react": "18.2.0", diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx index 2c62fa09c0e73a4074af78ff745fb7a745018f1d..691b1568b442a61bc54d910880ece0d7cd5a63cc 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx @@ -16,7 +16,13 @@ import { MockStoreEnhanced } from 'redux-mock-store'; import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem.component'; import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; -const BIO_ENTITY = bioEntitiesContentFixture[0].bioEntity; +const BIO_ENTITY = { + ...bioEntitiesContentFixture[0].bioEntity, + fullName: 'fullName_', + name: 'name_', + symbol: 'symbol_', +}; + const INITIAL_STORE_WITH_ENTITY_NUMBER: InitialStoreState = { entityNumber: { data: { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx index 2eb6f897def7a8412c5189963957f58e0115327b..685bfdbdd59f2948df45081a45e3eb95d35517a5 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx @@ -73,8 +73,8 @@ describe('BioEntitiesAccordion - component', () => { }); expect(screen.getByText('Content (10)')).toBeInTheDocument(); - expect(screen.getByText('Core PD map (4)')).toBeInTheDocument(); + expect(screen.getByText('Core PD map (3)')).toBeInTheDocument(); expect(screen.getByText('Histamine signaling (4)')).toBeInTheDocument(); - expect(screen.getByText('PRKN substrates (2)')).toBeInTheDocument(); + expect(screen.getByText('PRKN substrates (3)')).toBeInTheDocument(); }); }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/AccordionsDetails/AccordionsDetails.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/AccordionsDetails/AccordionsDetails.component.test.tsx index 280246c664c61e8c7263fd08edb4f26a8a0887c1..4460c7c02c5866a7c86b753884814838da9cf3ec 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/AccordionsDetails/AccordionsDetails.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/AccordionsDetails/AccordionsDetails.component.test.tsx @@ -1,5 +1,4 @@ /* eslint-disable no-magic-numbers */ -import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; import { drugsFixture } from '@/models/fixtures/drugFixtures'; import { StoreType } from '@/redux/store'; import { @@ -16,12 +15,6 @@ const DRUGS_PINS_LIST = drugsFixture.map(drug => ({ data: drug, })); -const CHEMICALS_PINS_LIST = chemicalsFixture.map(chemical => ({ - id: chemical.id.id, - name: chemical.name, - data: chemical, -})); - const renderComponent = ( pinsList: PinItem[], initialStoreState: InitialStoreState = {}, @@ -64,13 +57,4 @@ describe('AccordionsDetails - component', () => { expect(screen.getByText(firstDrugSynonym, { exact: false })).toBeInTheDocument(); expect(screen.getByText(secondDrugSynonym, { exact: false })).toBeInTheDocument(); }); - it('should display direct evidence publications for chemicals', () => { - renderComponent(CHEMICALS_PINS_LIST); - - const chemicalsAdditionalInfo = chemicalsFixture[0].directEvidence - ? chemicalsFixture[0].directEvidence - : ''; - - expect(screen.getAllByText(chemicalsAdditionalInfo, { exact: false })[0]).toBeInTheDocument(); - }); }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx index c1ed276c8a3efaf6727c2b790cd33080a2e84680..b1d74b817a28845a1f3b4f7f48374e864a5b6517 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.test.tsx @@ -79,8 +79,11 @@ describe('PinsListItem - component ', () => { expect(screen.getByText(firstPinElementType, { exact: false })).toBeInTheDocument(); expect(screen.getByText(firstPinElementResource, { exact: false })).toBeInTheDocument(); - expect(screen.getByText(secondPinElementType, { exact: false })).toBeInTheDocument(); - expect(screen.getByText(secondPinElementResource, { exact: false })).toBeInTheDocument(); + + if (!secondPinElementType) { + expect(screen.queryByText(secondPinElementType, { exact: false })).toBeNull(); + expect(screen.queryByText(secondPinElementResource, { exact: false })).toBeNull(); + } }); it('should display list of references for pin', () => { renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs', BIO_ENTITY, INITIAL_STORE_STATE); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx index 254d0bc3197d2042db7280d7519517a8e0a70cc3..d51600d35d0412cb6bd2d794b7349bd54054deaf 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/PinsList/PinsListItem/PinsListItem.component.tsx @@ -91,6 +91,10 @@ export const PinsListItem = ({ <div className="font-bold">Elements:</div> {'targetParticipants' in pin && pin.targetParticipants.map(participant => { + if (!participant?.link) { + return null; + } + return ( // participant.id is almost always = 0 <li key={`${participant.id}-${participant.link}`} className="my-2 px-2"> diff --git a/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts b/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts index 6b4b4b2997aed8bbb93335bcfbb0bacdae6fc38c..317a9678198208bc00e9570e6f956afc0079ade3 100644 --- a/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts +++ b/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts @@ -9,14 +9,14 @@ const SMALL_TEXT_VALUE = 1; const MEDIUM_TEXT_VALUE = 10; const BIG_TEXT_VALUE = 100; -interface Args { +export interface GetCanvasIconArgs { color: string; value: number; textColor?: string; } export const drawPinOnCanvas = ( - { color }: Pick<Args, 'color'>, + { color }: Pick<GetCanvasIconArgs, 'color'>, ctx: CanvasRenderingContext2D, ): void => { const path = new Path2D(PIN_PATH2D); @@ -43,7 +43,7 @@ export const getTextPosition = (textWidth: number, textHeight: number): Point => }); export const drawNumberOnCanvas = ( - { value, textColor }: Pick<Args, 'value' | 'textColor'>, + { value, textColor }: Pick<GetCanvasIconArgs, 'value' | 'textColor'>, ctx: CanvasRenderingContext2D, ): void => { const text = `${value}`; @@ -61,7 +61,7 @@ export const drawNumberOnCanvas = ( }; export const getCanvasIcon = ( - args: Omit<Args, 'value'> & { value?: number }, + args: Omit<GetCanvasIconArgs, 'value'> & { value?: number }, ): HTMLCanvasElement => { const canvas = createCanvas(PIN_SIZE); const ctx = canvas.getContext('2d'); @@ -70,6 +70,7 @@ export const getCanvasIcon = ( } drawPinOnCanvas(args, ctx); + if (args?.value !== undefined) { drawNumberOnCanvas({ value: args.value, textColor: args?.textColor }, ctx); } diff --git a/src/components/Map/MapViewer/utils/config/getCanvasMultiIcon.ts b/src/components/Map/MapViewer/utils/config/getCanvasMultiIcon.ts new file mode 100644 index 0000000000000000000000000000000000000000..498897ab7bdec445a7eeffabfd4fc58cef4b55ff --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/getCanvasMultiIcon.ts @@ -0,0 +1,27 @@ +import { MULTIICON_RATIO, PIN_SIZE } from '@/constants/canvas'; +import { ONE, ZERO } from '@/constants/common'; +import { createCanvas } from '@/utils/canvas/getCanvas'; + +const drawIconOnCanvas = ( + ctx: CanvasRenderingContext2D, + icon: HTMLCanvasElement, + index: number, +): void => { + ctx.drawImage(icon, ZERO, index * PIN_SIZE.height * MULTIICON_RATIO); +}; + +export const getCavasMultiIcon = (icons: HTMLCanvasElement[]): HTMLCanvasElement => { + const canvas = createCanvas({ + width: PIN_SIZE.width, + height: PIN_SIZE.height * (ONE + (icons.length - ONE) * MULTIICON_RATIO), + }); + + const ctx = canvas.getContext('2d'); + if (!ctx) { + return canvas; + } + + icons.reverse().forEach((icon, index) => drawIconOnCanvas(ctx, icon, index)); + + return canvas; +}; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts index c6ae99fb8406a32056296741c76bd766043e493c..4103719aa1ac8767c06c9cce13cefbd4d65c035f 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts @@ -1,31 +1,28 @@ import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; -import { BioEntity } from '@/types/models'; -import { PinType } from '@/types/pin'; +import { BioEntityWithPinType } from '@/types/bioEntity'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; import { getBioEntitySingleFeature } from './getBioEntitySingleFeature'; export const getBioEntitiesFeatures = ( - bioEntites: BioEntity[], + bioEntites: BioEntityWithPinType[], { pointToProjection, - type, entityNumber, activeIds, }: { pointToProjection: UsePointToProjectionResult; - type: PinType; entityNumber: EntityNumber; - activeIds?: (string | number)[]; + activeIds: (string | number)[]; }, ): Feature[] => { return bioEntites.map(bioEntity => getBioEntitySingleFeature(bioEntity, { pointToProjection, - type, + type: bioEntity.type, // pin's index number value: entityNumber?.[bioEntity.elementId], - isActive: activeIds ? activeIds.includes(bioEntity.id) : true, + isActive: activeIds.includes(bioEntity.id), }), ); }; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts index 9cc723f41ad1365d5bca71da14326ac754651296..ea28c2543c3ca8e368b66490cf2b872f4e656d64 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntititesFeatures.test.ts @@ -27,15 +27,15 @@ describe('getBioEntitiesFeatures - subUtil', () => { map: initialMapStateFixture, }); const bioEntititesContent = bioEntitiesContentFixture; - const bioEntities = bioEntititesContent.map(({ bioEntity }) => bioEntity); + const bioEntities = bioEntititesContent.map(({ bioEntity }) => ({ + ...bioEntity, + type: 'bioEntity' as PinType, + })); const pointToProjection = getPointToProjection(Wrapper); - const pinTypes: PinType[] = ['bioEntity', 'drugs', 'chemicals']; - - it.each(pinTypes)('should return array of instances of Feature with Style type=%s', type => { + it('should return array of instances of Feature with Style type=%s', () => { const result = getBioEntitiesFeatures(bioEntities, { pointToProjection, - type, entityNumber: {}, activeIds: [bioEntities[FIRST_ARRAY_ELEMENT].id], }); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts new file mode 100644 index 0000000000000000000000000000000000000000..61726c4926a67f6e0e48a21d8367f82509123e9b --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts @@ -0,0 +1,38 @@ +import { PINS_COLORS, TEXT_COLOR } from '@/constants/canvas'; +import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; +import { BioEntityWithPinType } from '@/types/bioEntity'; +import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString'; +import { mix } from 'polished'; +import { GetCanvasIconArgs } from '../getCanvasIcon'; + +const INACTIVE_ELEMENT_OPACITY = 0.5; +const DARK_COLOR_MIX_RATIO = 0.25; + +interface Options { + entityNumber: EntityNumber; + activeIds: (string | number)[]; + isDarkColor?: boolean; +} + +export const getMultipinCanvasArgs = ( + { type, ...element }: BioEntityWithPinType, + { entityNumber, activeIds, isDarkColor }: Options, +): GetCanvasIconArgs => { + const value = entityNumber?.[element.elementId]; + const isActive = activeIds.includes(element.id); + const baseColor = isDarkColor + ? mix(DARK_COLOR_MIX_RATIO, '#000', PINS_COLORS[type]) + : PINS_COLORS[type]; + + const color = isActive ? baseColor : addAlphaToHexString(baseColor, INACTIVE_ELEMENT_OPACITY); + + const textColor = isActive + ? TEXT_COLOR + : addAlphaToHexString(TEXT_COLOR, INACTIVE_ELEMENT_OPACITY); + + return { + color, + textColor, + value, + }; +}; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..10906ccf7ab8f950ad4ab1d773210424b1fec595 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.test.ts @@ -0,0 +1,87 @@ +import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; +import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; +import { initialMapStateFixture } from '@/redux/map/map.fixtures'; +import { MultiPinBioEntity } from '@/types/bioEntity'; +import { PinType } from '@/types/pin'; +import { UsePointToProjectionResult, usePointToProjection } from '@/utils/map/usePointToProjection'; +import { + GetReduxWrapperUsingSliceReducer, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; +import { Feature } from 'ol'; +import Style from 'ol/style/Style'; +import { getMultipinSingleFeature } from './getMultipinSingleFeature'; +import * as getMultipinStyle from './getMultipinStyle'; + +jest.mock('./getMultipinStyle', () => ({ + __esModule: true, + ...jest.requireActual('./getMultipinStyle'), +})); + +const ONE_MULTI_BIO_ENTITIES: MultiPinBioEntity = [ + { + ...bioEntityContentFixture.bioEntity, + type: 'bioEntity' as PinType, + x: 100, + y: 100, + }, + { + ...bioEntityContentFixture.bioEntity, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, +]; + +const ENTITY_NUMBER: EntityNumber = { + [ONE_MULTI_BIO_ENTITIES[FIRST_ARRAY_ELEMENT].elementId]: 100, +}; + +const getMultipinStyleSpy = jest.spyOn(getMultipinStyle, 'getMultipinStyle'); + +const getPointToProjection = ( + wrapper: ReturnType<GetReduxWrapperUsingSliceReducer>['Wrapper'], +): UsePointToProjectionResult => { + const { result: usePointToProjectionHook } = renderHook(() => usePointToProjection(), { + wrapper, + }); + + return usePointToProjectionHook.current; +}; + +describe('getMultipinSingleFeature - subUtil', () => { + const { Wrapper } = getReduxWrapperWithStore({ + map: initialMapStateFixture, + }); + const pointToProjection = getPointToProjection(Wrapper); + + it('should return instance of Feature with Style type=%s', () => { + const result = getMultipinSingleFeature(ONE_MULTI_BIO_ENTITIES, { + pointToProjection, + entityNumber: ENTITY_NUMBER, + activeIds: [], + }); + + const style = result.getStyle() as Style; + + expect(result).toBeInstanceOf(Feature); + expect(style).toBeInstanceOf(Style); + }); + + it('should run getPinStyle with valid args for type=%s', () => { + getMultipinSingleFeature(ONE_MULTI_BIO_ENTITIES, { + pointToProjection, + entityNumber: ENTITY_NUMBER, + activeIds: [], + }); + + expect(getMultipinStyleSpy).toHaveBeenCalledWith({ + pins: [ + { color: '#0c4fa180', textColor: '#FFFFFF80', value: 100 }, + { color: '#F48C4180', textColor: '#FFFFFF80', value: undefined }, + ], + }); + }); +}); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4a48ede09cab0a641a6349d8a50ca48d332346a --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts @@ -0,0 +1,44 @@ +import { ONE, ZERO } from '@/constants/common'; +import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; +import { BioEntityWithPinType, MultiPinBioEntity } from '@/types/bioEntity'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Feature } from 'ol'; +import { getMultipinCanvasArgs } from './getMultipinCanvasArgs'; +import { getMultipinStyle } from './getMultipinStyle'; +import { getPinFeature } from './getPinFeature'; + +export const getMultipinSingleFeature = ( + multipin: MultiPinBioEntity, + { + pointToProjection, + entityNumber, + activeIds, + }: { + pointToProjection: UsePointToProjectionResult; + entityNumber: EntityNumber; + activeIds: (string | number)[]; + }, +): Feature => { + const [mainElement, ...sortedElements] = multipin.sort( + (a, b) => (activeIds.includes(b.id) ? ONE : ZERO) - (activeIds.includes(a.id) ? ONE : ZERO), + ); + const feature = getPinFeature(mainElement, pointToProjection); + + const canvasPinsArgMainElement = getMultipinCanvasArgs(mainElement, { + activeIds, + entityNumber, + isDarkColor: true, + }); + + const canvasPinsArgs = sortedElements.map((element: BioEntityWithPinType) => + getMultipinCanvasArgs(element, { + activeIds, + entityNumber: {}, // additional elements id's should be not visible + }), + ); + + const style = getMultipinStyle({ pins: [canvasPinsArgMainElement, ...canvasPinsArgs] }); + + feature.setStyle(style); + return feature; +}; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinStyle.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinStyle.ts new file mode 100644 index 0000000000000000000000000000000000000000..f79e9266fe6dd134c5c9e1f5781bd644eac64367 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinStyle.ts @@ -0,0 +1,31 @@ +import { MULTIICON_RATIO, PIN_SIZE } from '@/constants/canvas'; +import { ONE, ZERO } from '@/constants/common'; +import Icon from 'ol/style/Icon'; +import Style from 'ol/style/Style'; +import { GetCanvasIconArgs, getCanvasIcon } from '../getCanvasIcon'; +import { getCavasMultiIcon } from '../getCanvasMultiIcon'; + +interface Args { + pins: GetCanvasIconArgs[]; +} + +export const getMultipinStyle = ({ pins }: Args): Style => { + const icons = pins.map(({ color, value, textColor }) => + getCanvasIcon({ + color, + value, + textColor, + }), + ); + + const img = getCavasMultiIcon(icons); + + return new Style({ + image: new Icon({ + displacement: [ZERO, PIN_SIZE.height * (ONE + (icons.length - ONE) * MULTIICON_RATIO)], + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + img, + }), + }); +}; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.test.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c5778deb7d30cbad7c1f14e0d3f1f2c5b6185eb --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.test.ts @@ -0,0 +1,76 @@ +import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { MultiPinBioEntity } from '@/types/bioEntity'; +import { PinType } from '@/types/pin'; +import { getMultipinsBioEntities } from './getMultipinsBioEntities'; + +const ZERO_MULTI_BIO_ENTITIES: MultiPinBioEntity = [ + { + ...bioEntityContentFixture.bioEntity, + type: 'bioEntity' as PinType, + }, + { + ...bioEntityContentFixture.bioEntity, + x: 1000, + type: 'bioEntity' as PinType, + }, +]; + +const ONE_MULTI_BIO_ENTITIES: MultiPinBioEntity = [ + { + ...bioEntityContentFixture.bioEntity, + type: 'bioEntity' as PinType, + x: 100, + y: 100, + }, + { + ...bioEntityContentFixture.bioEntity, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, +]; + +const FEW_MULTI_BIO_ENTITIES_WITH_MULTIPLIED_TYPE: MultiPinBioEntity = [ + { + ...bioEntityContentFixture.bioEntity, + type: 'bioEntity' as PinType, + x: 100, + y: 100, + }, + { + ...bioEntityContentFixture.bioEntity, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, + { + ...bioEntityContentFixture.bioEntity, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, + { + ...bioEntityContentFixture.bioEntity, + type: 'drugs' as PinType, + x: 100, + y: 100, + }, +]; + +describe('getMultipinsBioEntities - util', () => { + it('should return empty array if theres no multi pins', () => { + expect(getMultipinsBioEntities({ bioEntities: ZERO_MULTI_BIO_ENTITIES })).toStrictEqual([]); + }); + + it('should return valid multi pins', () => { + expect(getMultipinsBioEntities({ bioEntities: ONE_MULTI_BIO_ENTITIES })).toStrictEqual([ + ONE_MULTI_BIO_ENTITIES, + ]); + }); + + it('should return valid multi pins if theres few types of pins', () => { + expect( + getMultipinsBioEntities({ bioEntities: FEW_MULTI_BIO_ENTITIES_WITH_MULTIPLIED_TYPE }), + ).toStrictEqual([ONE_MULTI_BIO_ENTITIES]); + }); +}); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ee7139cac0b9e48ae611fe3b0b66520fa79e2be --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts @@ -0,0 +1,58 @@ +import { ONE } from '@/constants/common'; +import { BioEntityWithPinType, MultiPinBioEntity } from '@/types/bioEntity'; +import { BioEntity } from '@/types/models'; +import { PinType } from '@/types/pin'; + +interface Args { + bioEntities: MultiPinBioEntity; +} + +const SEPARATOR = '-'; +const POSITION_PRESCISION_SEPERATOR = '.'; + +const getUniqueKey = (element: Pick<BioEntity, 'x' | 'y'>): string => { + const [x] = `${element.x}`.split(POSITION_PRESCISION_SEPERATOR); + const [y] = `${element.y}`.split(POSITION_PRESCISION_SEPERATOR); + + return [x, y].join(SEPARATOR); +}; + +const groupByPosition = ( + accumulator: Record<string, MultiPinBioEntity>, + element: BioEntityWithPinType, +): Record<string, MultiPinBioEntity> => { + const key = getUniqueKey(element); + + return { + ...accumulator, + [key]: accumulator[key] ? [...accumulator[key], element] : [element], + }; +}; + +const toUniqueTypeMultipin = (multipin: MultiPinBioEntity): MultiPinBioEntity => { + const allTypes: PinType[] = multipin.map(pin => pin.type); + const uniqueTypes = [...new Set(allTypes)]; + + return uniqueTypes + .map(type => multipin.find(pin => pin.type === type)) + .filter((value): value is BioEntityWithPinType => value !== undefined); +}; + +export const getMultipinsBioEntities = ({ bioEntities }: Args): MultiPinBioEntity[] => { + const multipiledBioEntities = bioEntities.filter( + baseElement => + bioEntities.filter(element => getUniqueKey(baseElement) === getUniqueKey(element)).length > + ONE, + ); + + const duplicatedMultipinsGroupedByPosition = multipiledBioEntities.reduce( + groupByPosition, + {} as Record<string, MultiPinBioEntity>, + ); + + const allGroupedMultipins = Object.values(duplicatedMultipinsGroupedByPosition); + const uniqueTypeGroupedMultipins = allGroupedMultipins.map(toUniqueTypeMultipin); + const multipiledMultiPins = uniqueTypeGroupedMultipins.filter(multipin => multipin.length > ONE); + + return multipiledMultiPins; +}; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffe07c3a1c3a2189522a5246553a5e6e14af9d1c --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts @@ -0,0 +1,4 @@ +import { MultiPinBioEntity } from '@/types/bioEntity'; + +export const getMultipinBioEntititesIds = (multipins: MultiPinBioEntity[]): (string | number)[] => + multipins.flat().map(({ id }) => id); diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ebcb067d4b7def348074cb366a97d4a656a1c72 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts @@ -0,0 +1,26 @@ +import { EntityNumber } from '@/redux/entityNumber/entityNumber.types'; +import { MultiPinBioEntity } from '@/types/bioEntity'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Feature } from 'ol'; +import { getMultipinSingleFeature } from './getMultipinSingleFeature'; + +export const getMultipinFeatures = ( + multipins: MultiPinBioEntity[], + { + pointToProjection, + entityNumber, + activeIds, + }: { + pointToProjection: UsePointToProjectionResult; + entityNumber: EntityNumber; + activeIds: (string | number)[]; + }, +): Feature[] => { + return multipins.map(multipin => + getMultipinSingleFeature(multipin, { + pointToProjection, + entityNumber, + activeIds, + }), + ); +}; diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index 30a9e0e1db0c483e6b74f291262a29e58eedb8e9..75d8c838d5ae06005c42beacd068044fe7ff7dbc 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -1,72 +1,69 @@ /* eslint-disable no-magic-numbers */ import { - allBioEntitesSelectorOfCurrentMap, - allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSelector, + allBioEntitiesWithTypeOfCurrentMapSelector, 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 { BioEntity } from '@/types/models'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import Feature from 'ol/Feature'; import { Geometry } from 'ol/geom'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getBioEntitiesFeatures } from './getBioEntitiesFeatures'; import { getMarkersFeatures } from './getMarkersFeatures'; +import { getMultipinsBioEntities } from './getMultipinsBioEntities'; +import { getMultipinBioEntititesIds } from './getMultipinsBioEntitiesIds'; +import { getMultipinFeatures } from './getMultipinsFeatures'; export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { const pointToProjection = usePointToProjection(); const activeIds = useSelector(allVisibleBioEntitiesIdsSelector); - const contentBioEntites = useSelector(allBioEntitesSelectorOfCurrentMap); - const chemicalsBioEntities = useSelector(allChemicalsBioEntitesOfCurrentMapSelector); - const drugsBioEntities = useSelector(allDrugsBioEntitesOfCurrentMapSelector); + const bioEntities = useSelector(allBioEntitiesWithTypeOfCurrentMapSelector); const markersEntities = useSelector(markersPinsOfCurrentMapDataSelector); const entityNumber = useSelector(entityNumberDataSelector); - const submapConnections = useSelector( - allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSelector, + const multiPinsBioEntities = useMemo( + () => + getMultipinsBioEntities({ + bioEntities, + }), + [bioEntities], + ); + const multipinsIds = getMultipinBioEntititesIds(multiPinsBioEntities); + const isMultiPin = useCallback( + (b: BioEntity): boolean => multipinsIds.includes(b.id), + [multipinsIds], ); const elementsFeatures = useMemo( () => [ - getBioEntitiesFeatures(contentBioEntites, { - pointToProjection, - type: 'bioEntity', - entityNumber, - activeIds, - }), - getBioEntitiesFeatures(chemicalsBioEntities, { - pointToProjection, - type: 'chemicals', - entityNumber, - activeIds, - }), - getBioEntitiesFeatures(drugsBioEntities, { + getBioEntitiesFeatures( + bioEntities.filter(b => !isMultiPin(b)), + { + pointToProjection, + entityNumber, + activeIds, + }, + ), + getMultipinFeatures(multiPinsBioEntities, { pointToProjection, - type: 'drugs', entityNumber, activeIds, }), - getBioEntitiesFeatures(submapConnections, { - pointToProjection, - type: 'bioEntity', - entityNumber, - }), getMarkersFeatures(markersEntities, { pointToProjection }), ].flat(), [ - contentBioEntites, - drugsBioEntities, - chemicalsBioEntities, + bioEntities, pointToProjection, - markersEntities, entityNumber, activeIds, - submapConnections, + multiPinsBioEntities, + markersEntities, + isMultiPin, ], ); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts index e36e92a7b655a1c1b20a77f41effe2707c51fab3..f3136895e438517686533a5925b974a840bc6349 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts @@ -29,12 +29,16 @@ describe('handleAliasResults - util', () => { beforeEach(() => { jest.clearAllMocks(); - const bioEntityWithIdReaction = bioEntityResponseFixture.content.find(c => - Boolean(c.bioEntity.idReaction), - )?.bioEntity || { id: ZERO }; + const bioEntityWithIdReaction = bioEntityResponseFixture.content + .filter(c => Boolean(c.bioEntity.idReaction)) + ?.map(b => b.bioEntity || { id: ZERO }); mockedAxiosOldClient - .onGet(apiPath.getReactionsWithIds([Number(`${bioEntityWithIdReaction.id}`)])) + .onGet( + apiPath.getReactionsWithIds( + bioEntityWithIdReaction.map(bioEntity => Number(`${bioEntity.id}`)), + ), + ) .reply(HttpStatusCode.Ok, []); }); describe('when matching bioEntity not found', () => { @@ -132,6 +136,7 @@ describe('handleAliasResults - util', () => { y: 700, width: 50, height: 50, + idReaction: undefined, }, }, ], @@ -156,8 +161,6 @@ describe('handleAliasResults - util', () => { 'project/getBioEntityContents/pending', 'project/getBioEntityContents/fulfilled', 'entityNumber/addNumbersToEntityNumberData', - 'reactions/getByIds/pending', - 'reactions/getByIds/fulfilled', 'project/getMultiBioEntity/fulfilled', 'drawer/selectTab', 'drawer/openBioEntityDrawerById', diff --git a/src/constants/canvas.ts b/src/constants/canvas.ts index 00eeeff646f3a79c325e15be42acd11c754c003f..29931be199a20e245d7c54b6185d186b9e089839 100644 --- a/src/constants/canvas.ts +++ b/src/constants/canvas.ts @@ -4,6 +4,9 @@ import { PinType } from '@/types/pin'; export const PIN_PATH2D = 'M12.3077 0C6.25641 0 0 4.61538 0 12.3077C0 19.5897 11.0769 30.9744 11.5897 31.4872C11.7949 31.6923 12 31.7949 12.3077 31.7949C12.6154 31.7949 12.8205 31.6923 13.0256 31.4872C13.5385 30.9744 24.6154 19.6923 24.6154 12.3077C24.6154 4.61538 18.359 0 12.3077 0Z'; +export const PIN_COVER_PATH2D = + 'M12.5 2C7.26267 2 2 5.93609 2 12.3871C2 13.7597 2.54385 15.5184 3.55264 17.5238C4.54347 19.4936 5.89191 21.5252 7.29136 23.3952C9.35206 26.1488 11.4511 28.4566 12.5022 29.569C13.5556 28.4621 15.6525 26.1723 17.7106 23.4313C19.1091 21.5689 20.4565 19.5418 21.4465 17.569C22.4535 15.5627 23 13.7891 23 12.3871C23 5.93609 17.7373 2 12.5 2ZM0 12.3871C0 4.64516 6.35417 0 12.5 0C18.6458 0 25 4.64516 25 12.3871C25 19.8194 13.75 31.1742 13.2292 31.6903C13.0208 31.8968 12.8125 32 12.5 32C12.1875 32 11.9792 31.8968 11.7708 31.6903C11.25 31.1742 0 19.7161 0 12.3871Z'; + export const PIN_SIZE = { width: 25, height: 32, @@ -25,3 +28,5 @@ export const LINE_COLOR = '#00AAFF'; export const TEXT_COLOR = '#FFFFFF'; export const LINE_WIDTH = 6; + +export const MULTIICON_RATIO = 0.2; diff --git a/src/constants/pin.ts b/src/constants/pin.ts new file mode 100644 index 0000000000000000000000000000000000000000..0af91625d8fdae1ec8f7a2a6417e96831f4f6b66 --- /dev/null +++ b/src/constants/pin.ts @@ -0,0 +1,3 @@ +import { PinType } from '@/types/pin'; + +export const DEFAULT_PIN_TYPE: PinType = 'bioEntity'; diff --git a/src/models/idSchema.ts b/src/models/idSchema.ts index 2ccc4d780bd3185097e86bdac53ec45abd1c848b..9a8ef7d8caaf19e7442410001a34d212562e1397 100644 --- a/src/models/idSchema.ts +++ b/src/models/idSchema.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const idSchema = z.object({ annotatorClassName: z.string(), id: z.number(), - link: z.string(), + link: z.string().nullable(), resource: z.string(), type: z.string(), }); diff --git a/src/models/referenceSchema.ts b/src/models/referenceSchema.ts index 44a1e0c6d58c37cc92ef8ec2a0acd796007467fc..97c8941c9f2e8a584cd74d6ea725a5541e441fb5 100644 --- a/src/models/referenceSchema.ts +++ b/src/models/referenceSchema.ts @@ -3,7 +3,7 @@ import { articleSchema } from './articleSchema'; export const referenceSchema = z.object({ link: z.string().url().nullable(), - article: articleSchema.optional(), + article: articleSchema.optional().nullable(), type: z.string(), resource: z.string(), id: z.number(), diff --git a/src/models/targetParticipantSchema.ts b/src/models/targetParticipantSchema.ts index fbf821471b0d1272660a51743fe05801eea43ca3..b114594c7a657c200d6e98d98a9a4c5a06811b01 100644 --- a/src/models/targetParticipantSchema.ts +++ b/src/models/targetParticipantSchema.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const targetParticipantSchema = z.object({ - link: z.string(), + link: z.string().nullable(), type: z.string(), resource: z.string(), id: z.number(), diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts index 25479877fe57e2a9a3a7cdbd7ee80fc545759a49..ea4d544415b41e42bc602d9ca29d427b1883ea26 100644 --- a/src/redux/bioEntity/bioEntity.selectors.ts +++ b/src/redux/bioEntity/bioEntity.selectors.ts @@ -1,11 +1,13 @@ import { ONE, SIZE_OF_EMPTY_ARRAY, ZERO } from '@/constants/common'; import { rootSelector } from '@/redux/root/root.selectors'; +import { BioEntityWithPinType } from '@/types/bioEntity'; import { ElementIdTabObj } from '@/types/elements'; import { MultiSearchData } from '@/types/fetchDataState'; import { BioEntity, BioEntityContent, MapModel } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; import { allChemicalsBioEntitesOfAllMapsSelector, + allChemicalsBioEntitesOfCurrentMapSelector, allChemicalsIdTabSelectorOfCurrentMap, chemicalsBioEntitiesForSelectedSearchElementSelector, searchedChemicalsBioEntitesOfCurrentMapSelector, @@ -17,6 +19,7 @@ import { } from '../drawer/drawer.selectors'; import { allDrugsBioEntitesOfAllMapsSelector, + allDrugsBioEntitesOfCurrentMapSelector, allDrugsIdTabSelectorOfCurrentMap, drugsBioEntitiesForSelectedSearchElementSelector, searchedDrugsBioEntitesOfCurrentMapSelector, @@ -214,13 +217,6 @@ export const allElementsForSearchElementNumberByModelId = createSelector( }, ); -export const allVisibleBioEntitiesIdsSelector = createSelector( - allVisibleBioEntitiesSelector, - (elements): (string | number)[] => { - return elements.map(e => e.id); - }, -); - export const allContentBioEntitesSelectorOfAllMaps = createSelector( bioEntitySelector, (bioEntities): BioEntity[] => { @@ -282,3 +278,26 @@ export const allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSele ); }, ); + +export const allBioEntitiesWithTypeOfCurrentMapSelector = createSelector( + allBioEntitesSelectorOfCurrentMap, + allChemicalsBioEntitesOfCurrentMapSelector, + allDrugsBioEntitesOfCurrentMapSelector, + allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSelector, + (content, chemicals, drugs, submapConnections): BioEntityWithPinType[] => { + return [ + content.map(v => ({ ...v, type: 'bioEntity' as const })), + chemicals.map(v => ({ ...v, type: 'chemicals' as const })), + drugs.map(v => ({ ...v, type: 'drugs' as const })), + submapConnections.map(v => ({ ...v, type: 'bioEntity' as const })), + ].flat(); + }, +); + +export const allVisibleBioEntitiesIdsSelector = createSelector( + allVisibleBioEntitiesSelector, + allSubmapConnectionsBioEntityOfCurrentSubmapWithRealConnectionsSelector, + (elements, submapConnections): (string | number)[] => { + return [...elements, ...submapConnections].map(e => e.id); + }, +); diff --git a/src/types/bioEntity.ts b/src/types/bioEntity.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d7984699daa6b6beddc0e655b10f537119f4eb0 --- /dev/null +++ b/src/types/bioEntity.ts @@ -0,0 +1,8 @@ +import { BioEntity } from './models'; +import { PinType } from './pin'; + +export interface BioEntityWithPinType extends BioEntity { + type: PinType; +} + +export type MultiPinBioEntity = BioEntityWithPinType[];