From 0a6f0a4f3ca256eea8782b923840ad354126487b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Tue, 16 Apr 2024 16:29:11 +0200
Subject: [PATCH] feat: add multipin icon w/o tests

---
 package-lock.json                             | 20 ++++++
 package.json                                  |  1 +
 .../MapViewer/utils/config/getCanvasIcon.ts   |  9 +--
 .../utils/config/getCanvasMultiIcon.ts        | 27 ++++++++
 .../pinsLayer/getBioEntitiesFeatures.ts       | 13 ++--
 .../config/pinsLayer/getMultipinCanvasArgs.ts | 38 +++++++++++
 .../pinsLayer/getMultipinSingleFeature.ts     | 44 +++++++++++++
 .../config/pinsLayer/getMultipinStyle.ts      | 31 +++++++++
 .../pinsLayer/getMultipinsBioEntities.ts      | 58 +++++++++++++++++
 .../pinsLayer/getMultipinsBioEntitiesIds.ts   |  4 ++
 .../config/pinsLayer/getMultipinsFeatures.ts  | 26 ++++++++
 .../config/pinsLayer/useOlMapPinsLayer.ts     | 65 +++++++++----------
 src/constants/canvas.ts                       |  5 ++
 src/constants/pin.ts                          |  3 +
 src/models/idSchema.ts                        |  2 +-
 src/models/referenceSchema.ts                 |  2 +-
 src/models/targetParticipantSchema.ts         |  2 +-
 src/redux/bioEntity/bioEntity.selectors.ts    | 33 ++++++++--
 src/types/bioEntity.ts                        |  8 +++
 19 files changed, 335 insertions(+), 56 deletions(-)
 create mode 100644 src/components/Map/MapViewer/utils/config/getCanvasMultiIcon.ts
 create mode 100644 src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts
 create mode 100644 src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts
 create mode 100644 src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinStyle.ts
 create mode 100644 src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts
 create mode 100644 src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntitiesIds.ts
 create mode 100644 src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsFeatures.ts
 create mode 100644 src/constants/pin.ts
 create mode 100644 src/types/bioEntity.ts

diff --git a/package-lock.json b/package-lock.json
index 18eaa14c..ff30a5f4 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 54dfdef6..5dfdf66c 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/MapViewer/utils/config/getCanvasIcon.ts b/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts
index 6b4b4b29..317a9678 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 00000000..498897ab
--- /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 c6ae99fb..4103719a 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/getMultipinCanvasArgs.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinCanvasArgs.ts
new file mode 100644
index 00000000..61726c49
--- /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.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinSingleFeature.ts
new file mode 100644
index 00000000..b4a48ede
--- /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 00000000..f79e9266
--- /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.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMultipinsBioEntities.ts
new file mode 100644
index 00000000..2ee7139c
--- /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 00000000..ffe07c3a
--- /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 00000000..4ebcb067
--- /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 30a9e0e1..75d8c838 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/constants/canvas.ts b/src/constants/canvas.ts
index 00eeeff6..29931be1 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 00000000..0af91625
--- /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 2ccc4d78..9a8ef7d8 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 44a1e0c6..97c8941c 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 fbf82147..b114594c 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 25479877..ea4d5444 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 00000000..1d798469
--- /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[];
-- 
GitLab