From 5b34ca5cd49c40598fd86be2c383b6bdd6a75fbe Mon Sep 17 00:00:00 2001
From: mateusz-winiarczyk <mateusz.winiarczyk@appunite.com>
Date: Thu, 7 Mar 2024 13:15:51 +0100
Subject: [PATCH] feat(bounds): plugins bounds (MIN-228)

---
 docs/plugins/bounds.md                        |  43 +++++
 docs/plugins/errors.md                        |   4 +-
 docs/plugins/submaps.md                       |  10 ++
 index.d.ts                                    |   9 +
 .../MapAdditionalActions.component.test.tsx   |   2 +-
 .../utils/useAdditionalActions.test.ts        |   2 +-
 .../mapSingleClick/handleAliasResults.ts      |  12 +-
 .../mapSingleClick/handleReactionResults.ts   |  13 +-
 .../handleSearchResultAction.ts               |   6 +-
 .../Map/MapViewer/utils/useOlMap.ts           |   6 +-
 src/services/pluginsManager/errorMessages.ts  |   5 +-
 .../pluginsManager/map/data/getBounds.test.ts |  60 +++++++
 .../pluginsManager/map/data/getBounds.ts      |  37 ++++
 .../map/fitBounds/fitBounds.constants.ts      |   5 +
 .../map/fitBounds/fitBounds.test.ts           | 122 ++++++++++++++
 .../pluginsManager/map/fitBounds/fitBounds.ts |  45 +++++
 .../map/fitBounds/fitBounds.utils.test.ts     |  66 ++++++++
 .../map/fitBounds/fitBounds.utils.ts          |  14 ++
 .../pluginsManager/map/fitBounds/index.ts     |   1 +
 .../pluginsManager/map/getOpenMapId.test.ts   |  40 +++++
 .../pluginsManager/map/getOpenMapId.ts        |  13 ++
 .../pluginsManager/map/mapManager.test.ts     |  34 ++++
 src/services/pluginsManager/map/mapManager.ts |  18 ++
 .../map/triggerSearch/getPolygonPoints.ts     |  33 ++++
 ...sibleBioEntitiesPolygonCoordinates.test.ts | 158 ++++++++++++++++++
 ...getVisibleBioEntitiesPolygonCoordinates.ts |  33 ++++
 .../map/triggerSearch/searchByCoordinates.ts  |   4 +-
 .../map/triggerSearch/searchByQuery.test.ts   | 119 +++++++++++++
 .../map/triggerSearch/searchByQuery.ts        |  18 +-
 .../map/triggerSearch/searchFitBounds.test.ts | 105 ++++++++++++
 .../map/triggerSearch/searchFitBounds.ts      |  15 ++
 .../map/triggerSearch/triggerSearch.test.ts   |   5 +-
 .../map/triggerSearch/triggerSearch.ts        |   4 +-
 src/services/pluginsManager/pluginsManager.ts |   7 +
 src/utils/context/mapInstanceContext.tsx      |  21 ++-
 src/utils/map/useSetBounds.test.ts            |   4 +-
 36 files changed, 1064 insertions(+), 29 deletions(-)
 create mode 100644 docs/plugins/bounds.md
 create mode 100644 src/services/pluginsManager/map/data/getBounds.test.ts
 create mode 100644 src/services/pluginsManager/map/data/getBounds.ts
 create mode 100644 src/services/pluginsManager/map/fitBounds/fitBounds.constants.ts
 create mode 100644 src/services/pluginsManager/map/fitBounds/fitBounds.test.ts
 create mode 100644 src/services/pluginsManager/map/fitBounds/fitBounds.ts
 create mode 100644 src/services/pluginsManager/map/fitBounds/fitBounds.utils.test.ts
 create mode 100644 src/services/pluginsManager/map/fitBounds/fitBounds.utils.ts
 create mode 100644 src/services/pluginsManager/map/fitBounds/index.ts
 create mode 100644 src/services/pluginsManager/map/getOpenMapId.test.ts
 create mode 100644 src/services/pluginsManager/map/getOpenMapId.ts
 create mode 100644 src/services/pluginsManager/map/mapManager.test.ts
 create mode 100644 src/services/pluginsManager/map/mapManager.ts
 create mode 100644 src/services/pluginsManager/map/triggerSearch/getPolygonPoints.ts
 create mode 100644 src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts
 create mode 100644 src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.ts
 create mode 100644 src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts
 create mode 100644 src/services/pluginsManager/map/triggerSearch/searchFitBounds.test.ts
 create mode 100644 src/services/pluginsManager/map/triggerSearch/searchFitBounds.ts

diff --git a/docs/plugins/bounds.md b/docs/plugins/bounds.md
new file mode 100644
index 00000000..a9863e90
--- /dev/null
+++ b/docs/plugins/bounds.md
@@ -0,0 +1,43 @@
+### Bounds
+
+#### Get Bounds
+
+To get bounds of the current active map, plugins can use the `getBounds` method defined in `window.minerva.map.data` object available globally. It returns object with properties x1, y1, x2, y2
+
+- x1, y1 - top left corner coordinates
+- x2, y2 - right bottom corner coordinates
+
+Example of returned object:
+
+```javascript
+{
+  x1: 12853,
+  y1: 4201,
+  x2: 23327,
+  y2: 9575
+}
+```
+
+##### Example of getBounds method usage:
+
+```javascript
+window.minerva.map.data.getBounds();
+```
+
+#### Fit bounds
+
+To zoom in the map in a way that rectangle defined by coordinates is visible, plugins can use the `fitBounds` method defined in `window.minerva.map` object available globally. This method takes one argument: object with properties x1, y1, x2, y2.
+
+- x1, y1 - top left corner coordinates
+- x2, y2 - right bottom corner coordinates
+
+##### Example of fitBounds method usage:
+
+```javascript
+window.minerva.map.fitBounds({
+  x1: 14057.166666666668,
+  y1: 6805.337365980873,
+  x2: 14057.166666666668,
+  y2: 6805.337365980873,
+});
+```
diff --git a/docs/plugins/errors.md b/docs/plugins/errors.md
index 2795ec51..fca95f8b 100644
--- a/docs/plugins/errors.md
+++ b/docs/plugins/errors.md
@@ -4,13 +4,15 @@
 
 - **Map with provided id does not exist**: This error occurs when the provided map id does not correspond to any existing map.
 
+- **Unable to retrieve the id of the active map: the modelId is not a number**: This error occurs when the modelId parameter provided from store to retrieve the id of the active map is not a number.
+
 ## Search Errors
 
 - **Invalid query type. The query should be of string type**: This error occurs when the query parameter is not of string type.
 
 - **Invalid coordinates type or values**: This error occurs when the coordinates parameter is missing keys, or its values are not of number type.
 
-- **Invalid model id type. The model should be of number type**: This error occurs when the modelId parameter is not of number type.
+- **Invalid model id type. The model id should be a number**: This error occurs when the modelId parameter is not of number type.
 
 ## Project Errors
 
diff --git a/docs/plugins/submaps.md b/docs/plugins/submaps.md
index 54c6a340..aab6dc23 100644
--- a/docs/plugins/submaps.md
+++ b/docs/plugins/submaps.md
@@ -1,5 +1,15 @@
 ### Submaps
 
+#### Get current open map id
+
+To get current open map id, plugins can use the `getOpenMapId` method defined in `window.minerva.map.data` object available globally. It returns id of current open map.
+
+##### Example of getOpenMapId method usage:
+
+```javascript
+window.minerva.map.data.getOpenMapId();
+```
+
 #### Get Models
 
 To get data about all available submaps, plugins can use the `getModels` method defined in `window.minerva.map.data`. This method returns array with data about all submaps.
diff --git a/index.d.ts b/index.d.ts
index db769bd2..3cd0efd1 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,3 +1,9 @@
+import { getBounds } from '@/services/pluginsManager/map/data/getBounds';
+import { fitBounds } from '@/services/pluginsManager/map/fitBounds';
+import { getOpenMapId } from '@/services/pluginsManager/map/getOpenMapId';
+import { triggerSearch } from '@/services/pluginsManager/map/triggerSearch';
+import { MinervaConfiguration } from '@/services/pluginsManager/pluginsManager';
+import { MapInstance } from '@/types/map';
 import { getModels } from '@/services/pluginsManager/map/models/getModels';
 import { OpenMapArgs, openMap } from '@/services/pluginsManager/map/openMap';
 import { triggerSearch } from '@/services/pluginsManager/map/triggerSearch';
@@ -36,8 +42,11 @@ declare global {
       };
       map: {
         data: {
+          getBounds: typeof getBounds;
+          getOpenMapId: typeof getOpenMapId;
           getModels: typeof getModels;
         };
+        fitBounds: typeof fitBounds;
         openMap: typeof openMap;
         triggerSearch: typeof triggerSearch;
       };
diff --git a/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx b/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx
index f65f5b60..06bd09fe 100644
--- a/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx
+++ b/src/components/Map/MapAdditionalActions/MapAdditionalActions.component.test.tsx
@@ -54,7 +54,7 @@ const renderComponentWithMapInstance = (initialStore?: InitialStoreState): { sto
   const { Wrapper, store } = getReduxWrapperWithStore(initialStore, {
     mapInstanceContextValue: {
       mapInstance,
-      setMapInstance: () => {},
+      handleSetMapInstance: () => {},
     },
   });
 
diff --git a/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts b/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts
index f711b392..b862beae 100644
--- a/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts
+++ b/src/components/Map/MapAdditionalActions/utils/useAdditionalActions.test.ts
@@ -99,7 +99,7 @@ describe('useAddtionalActions - hook', () => {
           {
             mapInstanceContextValue: {
               mapInstance,
-              setMapInstance: () => {},
+              handleSetMapInstance: () => {},
             },
           },
         );
diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts
index b1487583..cd7aefe9 100644
--- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts
@@ -1,11 +1,12 @@
 import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks';
 import { openBioEntityDrawerById } from '@/redux/drawer/drawer.slice';
 import { AppDispatch } from '@/redux/store';
+import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds';
 import { ElementSearchResult } from '@/types/models';
 
 /* prettier-ignore */
 export const handleAliasResults =
-  (dispatch: AppDispatch) =>
+  (dispatch: AppDispatch, hasFitBounds?: boolean, fitBoundsZoom?: number) =>
     async ({ id }: ElementSearchResult): Promise<void> => {
 
       dispatch(openBioEntityDrawerById(id));
@@ -14,5 +15,12 @@ export const handleAliasResults =
           searchQueries: [id.toString()],
           isPerfectMatch: true
         }),
-      );
+      )
+        .unwrap().then(() => {
+          if (hasFitBounds) {
+            searchFitBounds(fitBoundsZoom);
+          }
+        }).catch(() => {
+          // TODO to discuss manage state of failure
+        });
     };
diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
index 55c244ad..d8282817 100644
--- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
@@ -3,12 +3,13 @@ import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks';
 import { openReactionDrawerById } from '@/redux/drawer/drawer.slice';
 import { getReactionsByIds } from '@/redux/reactions/reactions.thunks';
 import { AppDispatch } from '@/redux/store';
+import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds';
 import { ElementSearchResult, Reaction } from '@/types/models';
 import { PayloadAction } from '@reduxjs/toolkit';
 
 /* prettier-ignore */
 export const handleReactionResults =
-  (dispatch: AppDispatch) =>
+  (dispatch: AppDispatch, hasFitBounds?: boolean, fitBoundsZoom?: number) =>
     async ({ id }: ElementSearchResult): Promise<void> => {
       const data = await dispatch(getReactionsByIds([id])) as PayloadAction<Reaction[] | undefined>;
       const payload = data?.payload;
@@ -28,6 +29,12 @@ export const handleReactionResults =
         getMultiBioEntity({
           searchQueries: bioEntitiesIds,
           isPerfectMatch: true },
-        ),
-      );
+        )
+      ).unwrap().then(() => {
+        if (hasFitBounds) {
+          searchFitBounds(fitBoundsZoom);
+        }
+      }).catch(() => {
+        // TODO to discuss manage state of failure
+      });
     };
diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleSearchResultAction.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleSearchResultAction.ts
index 23ae912e..c3663e41 100644
--- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleSearchResultAction.ts
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleSearchResultAction.ts
@@ -8,11 +8,15 @@ import { handleReactionResults } from './handleReactionResults';
 interface HandleSearchResultActionInput {
   searchResults: ElementSearchResult[];
   dispatch: AppDispatch;
+  hasFitBounds?: boolean;
+  fitBoundsZoom?: number;
 }
 
 export const handleSearchResultAction = async ({
   searchResults,
   dispatch,
+  hasFitBounds,
+  fitBoundsZoom,
 }: HandleSearchResultActionInput): Promise<void> => {
   const closestSearchResult = searchResults[FIRST_ARRAY_ELEMENT];
   const { type } = closestSearchResult;
@@ -21,7 +25,7 @@ export const handleSearchResultAction = async ({
     REACTION: handleReactionResults,
   }[type];
 
-  await action(dispatch)(closestSearchResult);
+  await action(dispatch, hasFitBounds, fitBoundsZoom)(closestSearchResult);
 
   if (type === 'ALIAS') {
     PluginsEventBus.dispatchEvent('onBioEntityClick', closestSearchResult);
diff --git a/src/components/Map/MapViewer/utils/useOlMap.ts b/src/components/Map/MapViewer/utils/useOlMap.ts
index 326e8ec8..49ec3002 100644
--- a/src/components/Map/MapViewer/utils/useOlMap.ts
+++ b/src/components/Map/MapViewer/utils/useOlMap.ts
@@ -19,7 +19,7 @@ type UseOlMap = (input?: UseOlMapInput) => UseOlMapOutput;
 
 export const useOlMap: UseOlMap = ({ target } = {}) => {
   const mapRef = React.useRef<null | HTMLDivElement>(null);
-  const { mapInstance, setMapInstance } = useMapInstance();
+  const { mapInstance, handleSetMapInstance } = useMapInstance();
   const view = useOlMapView({ mapInstance });
   useOlMapLayers({ mapInstance });
   useOlMapListeners({ view, mapInstance });
@@ -41,8 +41,8 @@ export const useOlMap: UseOlMap = ({ target } = {}) => {
       }
     });
 
-    setMapInstance(currentMap => currentMap || map);
-  }, [target, setMapInstance]);
+    handleSetMapInstance(map);
+  }, [target, handleSetMapInstance]);
 
   return {
     mapRef,
diff --git a/src/services/pluginsManager/errorMessages.ts b/src/services/pluginsManager/errorMessages.ts
index 7d356494..753b06f0 100644
--- a/src/services/pluginsManager/errorMessages.ts
+++ b/src/services/pluginsManager/errorMessages.ts
@@ -1,6 +1,7 @@
 export const ERROR_MAP_NOT_FOUND = 'Map with provided id does not exist';
 export const ERROR_INVALID_QUERY_TYPE = 'Invalid query type. The query should be of string type';
 export const ERROR_INVALID_COORDINATES = 'Invalid coordinates type or values';
-export const ERROR_INVALID_MODEL_ID_TYPE =
-  'Invalid model id type. The model should be of number type';
+export const ERROR_INVALID_MODEL_ID_TYPE = 'Invalid model id type. The model id should be a number';
 export const ERROR_PROJECT_NOT_FOUND = 'Project does not exist';
+export const ERROR_INVALID_MODEL_ID_TYPE_FOR_RETRIEVAL =
+  'Unable to retrieve the id of the active map: the modelId is not a number';
diff --git a/src/services/pluginsManager/map/data/getBounds.test.ts b/src/services/pluginsManager/map/data/getBounds.test.ts
new file mode 100644
index 00000000..99f587da
--- /dev/null
+++ b/src/services/pluginsManager/map/data/getBounds.test.ts
@@ -0,0 +1,60 @@
+/* eslint-disable no-magic-numbers */
+import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants';
+import { store } from '@/redux/store';
+import { Map } from 'ol';
+import { MapManager } from '../mapManager';
+import { getBounds } from './getBounds';
+
+describe('getBounds', () => {
+  it('should return undefined if map instance does not exist', () => {
+    expect(getBounds()).toEqual(undefined);
+  });
+  it('should return current bounds if map instance exist', () => {
+    const dummyElement = document.createElement('div');
+    const mapInstance = new Map({ target: dummyElement });
+    MapManager.setMapInstance(mapInstance);
+
+    jest.spyOn(mapInstance, 'getView').mockImplementation(
+      () =>
+        ({
+          calculateExtent: () => [
+            -14409068.309137221, 17994265.029590994, -13664805.690862779, 18376178.970409006,
+          ],
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        }) as any,
+    );
+
+    const getStateSpy = jest.spyOn(store, 'getState');
+    getStateSpy.mockImplementation(
+      () =>
+        ({
+          map: {
+            data: {
+              ...MAP_DATA_INITIAL_STATE,
+              size: {
+                width: 26779.25,
+                height: 13503,
+                tileSize: 256,
+                minZoom: 2,
+                maxZoom: 9,
+              },
+            },
+            loading: 'idle',
+            error: {
+              name: '',
+              message: '',
+            },
+            openedMaps: [],
+          },
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        }) as any,
+    );
+
+    expect(getBounds()).toEqual({
+      x1: 15044,
+      y1: 4441,
+      x2: 17034,
+      y2: 5461,
+    });
+  });
+});
diff --git a/src/services/pluginsManager/map/data/getBounds.ts b/src/services/pluginsManager/map/data/getBounds.ts
new file mode 100644
index 00000000..626e13f7
--- /dev/null
+++ b/src/services/pluginsManager/map/data/getBounds.ts
@@ -0,0 +1,37 @@
+import { mapDataSizeSelector } from '@/redux/map/map.selectors';
+import { store } from '@/redux/store';
+import { latLngToPoint } from '@/utils/map/latLngToPoint';
+import { toLonLat } from 'ol/proj';
+import { MapManager } from '../mapManager';
+
+type GetBoundsReturnType =
+  | {
+      x1: number;
+      x2: number;
+      y1: number;
+      y2: number;
+    }
+  | undefined;
+
+export const getBounds = (): GetBoundsReturnType => {
+  const mapInstance = MapManager.getMapInstance();
+
+  if (!mapInstance) return undefined;
+
+  const [minx, miny, maxx, maxy] = mapInstance.getView().calculateExtent(mapInstance.getSize());
+
+  const mapSize = mapDataSizeSelector(store.getState());
+
+  const [lngX1, latY1] = toLonLat([minx, maxy]);
+  const [lngX2, latY2] = toLonLat([maxx, miny]);
+
+  const { x: x1, y: y1 } = latLngToPoint([latY1, lngX1], mapSize, { rounded: true });
+  const { x: x2, y: y2 } = latLngToPoint([latY2, lngX2], mapSize, { rounded: true });
+
+  return {
+    x1,
+    y1,
+    x2,
+    y2,
+  };
+};
diff --git a/src/services/pluginsManager/map/fitBounds/fitBounds.constants.ts b/src/services/pluginsManager/map/fitBounds/fitBounds.constants.ts
new file mode 100644
index 00000000..af85941c
--- /dev/null
+++ b/src/services/pluginsManager/map/fitBounds/fitBounds.constants.ts
@@ -0,0 +1,5 @@
+import { HALF } from '@/constants/dividers';
+import { DEFAULT_TILE_SIZE } from '@/constants/map';
+
+const BOUNDS_PADDING = DEFAULT_TILE_SIZE / HALF;
+export const DEFAULT_PADDING = [BOUNDS_PADDING, BOUNDS_PADDING, BOUNDS_PADDING, BOUNDS_PADDING];
diff --git a/src/services/pluginsManager/map/fitBounds/fitBounds.test.ts b/src/services/pluginsManager/map/fitBounds/fitBounds.test.ts
new file mode 100644
index 00000000..1f45aecf
--- /dev/null
+++ b/src/services/pluginsManager/map/fitBounds/fitBounds.test.ts
@@ -0,0 +1,122 @@
+/* eslint-disable no-magic-numbers */
+import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants';
+import { Map } from 'ol';
+import { store } from '@/redux/store';
+import { fitBounds } from './fitBounds';
+import { MapManager } from '../mapManager';
+
+jest.mock('../../../../redux/store');
+
+describe('fitBounds', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+    MapManager.mapInstance = null;
+  });
+  it('fitBounds should return undefined', () => {
+    expect(
+      fitBounds({
+        x1: 5,
+        y1: 10,
+        x2: 15,
+        y2: 20,
+      }),
+    ).toBe(undefined);
+  });
+
+  describe('when mapInstance is set', () => {
+    it('should call and set map instance view properly', () => {
+      const dummyElement = document.createElement('div');
+      const mapInstance = new Map({ target: dummyElement });
+      MapManager.setMapInstance(mapInstance);
+      const view = mapInstance.getView();
+      const getViewSpy = jest.spyOn(mapInstance, 'getView');
+      const fitSpy = jest.spyOn(view, 'fit');
+      const getStateSpy = jest.spyOn(store, 'getState');
+      getStateSpy.mockImplementation(
+        () =>
+          ({
+            map: {
+              data: {
+                ...MAP_DATA_INITIAL_STATE,
+                size: {
+                  width: 256,
+                  height: 256,
+                  tileSize: 256,
+                  minZoom: 1,
+                  maxZoom: 1,
+                },
+              },
+              loading: 'idle',
+              error: {
+                name: '',
+                message: '',
+              },
+              openedMaps: [],
+            },
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          }) as any,
+      );
+
+      fitBounds({
+        x1: 10,
+        y1: 10,
+        x2: 15,
+        y2: 20,
+      });
+
+      expect(getViewSpy).toHaveBeenCalledTimes(1);
+      expect(fitSpy).toHaveBeenCalledWith([-18472078, 16906648, -17689363, 18472078], {
+        maxZoom: 1,
+        padding: [128, 128, 128, 128],
+        size: undefined,
+      });
+    });
+    it('should use max zoom value', () => {
+      const dummyElement = document.createElement('div');
+      const mapInstance = new Map({ target: dummyElement });
+      MapManager.setMapInstance(mapInstance);
+      const view = mapInstance.getView();
+      const getViewSpy = jest.spyOn(mapInstance, 'getView');
+      const fitSpy = jest.spyOn(view, 'fit');
+      const getStateSpy = jest.spyOn(store, 'getState');
+      getStateSpy.mockImplementation(
+        () =>
+          ({
+            map: {
+              data: {
+                ...MAP_DATA_INITIAL_STATE,
+                size: {
+                  width: 256,
+                  height: 256,
+                  tileSize: 256,
+                  minZoom: 1,
+                  maxZoom: 99,
+                },
+              },
+              loading: 'idle',
+              error: {
+                name: '',
+                message: '',
+              },
+              openedMaps: [],
+            },
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          }) as any,
+      );
+
+      fitBounds({
+        x1: 10,
+        y1: 10,
+        x2: 15,
+        y2: 20,
+      });
+
+      expect(getViewSpy).toHaveBeenCalledTimes(1);
+      expect(fitSpy).toHaveBeenCalledWith([-18472078, 16906648, -17689363, 18472078], {
+        maxZoom: 99,
+        padding: [128, 128, 128, 128],
+        size: undefined,
+      });
+    });
+  });
+});
diff --git a/src/services/pluginsManager/map/fitBounds/fitBounds.ts b/src/services/pluginsManager/map/fitBounds/fitBounds.ts
new file mode 100644
index 00000000..079c6d9c
--- /dev/null
+++ b/src/services/pluginsManager/map/fitBounds/fitBounds.ts
@@ -0,0 +1,45 @@
+import { FitOptions } from 'ol/View';
+import { boundingExtent } from 'ol/extent';
+import { mapDataSizeSelector } from '@/redux/map/map.selectors';
+import { store } from '@/redux/store';
+import { MapManager } from '../mapManager';
+import { pointToProjection } from './fitBounds.utils';
+import { DEFAULT_PADDING } from './fitBounds.constants';
+
+type FitBoundsArgs = {
+  x1: number;
+  x2: number;
+  y1: number;
+  y2: number;
+};
+
+export const fitBounds = ({ x1, y1, x2, y2 }: FitBoundsArgs): void => {
+  const mapInstance = MapManager.getMapInstance();
+
+  if (!mapInstance) return;
+
+  const mapSize = mapDataSizeSelector(store.getState());
+
+  const points = [
+    {
+      x: x1,
+      y: y2,
+    },
+    {
+      x: x2,
+      y: y1,
+    },
+  ];
+
+  const coordinates = points.map(point => pointToProjection(point, mapSize));
+
+  const extent = boundingExtent(coordinates);
+
+  const options: FitOptions = {
+    size: mapInstance.getSize(),
+    padding: DEFAULT_PADDING,
+    maxZoom: mapSize.maxZoom,
+  };
+
+  mapInstance.getView().fit(extent, options);
+};
diff --git a/src/services/pluginsManager/map/fitBounds/fitBounds.utils.test.ts b/src/services/pluginsManager/map/fitBounds/fitBounds.utils.test.ts
new file mode 100644
index 00000000..6d2f5cac
--- /dev/null
+++ b/src/services/pluginsManager/map/fitBounds/fitBounds.utils.test.ts
@@ -0,0 +1,66 @@
+/* eslint-disable no-magic-numbers */
+import { pointToProjection } from './fitBounds.utils';
+
+describe('pointToProjection - util', () => {
+  describe('when mapSize arg is invalid', () => {
+    const validPoint = {
+      x: 0,
+      y: 0,
+    };
+
+    const invalidMapSize = {
+      width: -256 * 10,
+      height: -256 * 10,
+      tileSize: -256,
+      minZoom: -1,
+      maxZoom: -10,
+    };
+
+    it('should return fallback value on function call', () => {
+      expect(pointToProjection(validPoint, invalidMapSize)).toStrictEqual([0, -0]);
+    });
+  });
+
+  describe('when point and map size is valid', () => {
+    const validPoint = {
+      x: 256 * 100,
+      y: 256 * 100,
+    };
+
+    const validMapSize = {
+      width: 256 * 10,
+      height: 256 * 10,
+      tileSize: 256,
+      minZoom: 1,
+      maxZoom: 10,
+    };
+
+    const results = [380712659, -238107693];
+
+    it('should return valid lat lng value on function call', () => {
+      const [x, y] = pointToProjection(validPoint, validMapSize);
+
+      expect(x).toBe(results[0]);
+      expect(y).toBe(results[1]);
+    });
+  });
+  describe('when point arg is invalid', () => {
+    const invalidPoint = {
+      x: 'x',
+      y: 'y',
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    } as any;
+
+    const validMapSize = {
+      width: 256 * 10,
+      height: 256 * 10,
+      tileSize: 256,
+      minZoom: 1,
+      maxZoom: 10,
+    };
+
+    it('should return fallback value on function call', () => {
+      expect(pointToProjection(invalidPoint, validMapSize)).toStrictEqual([0, 0]);
+    });
+  });
+});
diff --git a/src/services/pluginsManager/map/fitBounds/fitBounds.utils.ts b/src/services/pluginsManager/map/fitBounds/fitBounds.utils.ts
new file mode 100644
index 00000000..de206bd4
--- /dev/null
+++ b/src/services/pluginsManager/map/fitBounds/fitBounds.utils.ts
@@ -0,0 +1,14 @@
+import { LATLNG_FALLBACK } from '@/constants/map';
+import { MapSize } from '@/redux/map/map.types';
+import { Point } from '@/types/map';
+import { pointToLngLat } from '@/utils/map/pointToLatLng';
+import { fromLonLat } from 'ol/proj';
+
+export const pointToProjection = (point: Point, mapSize: MapSize): number[] => {
+  const [lng, lat] = pointToLngLat(point, mapSize);
+  const projection = fromLonLat([lng, lat]);
+  const projectionRounded = projection.map(v => Math.round(v));
+  const isValid = !projectionRounded.some(v => Number.isNaN(v));
+
+  return isValid ? projectionRounded : LATLNG_FALLBACK;
+};
diff --git a/src/services/pluginsManager/map/fitBounds/index.ts b/src/services/pluginsManager/map/fitBounds/index.ts
new file mode 100644
index 00000000..28bd7717
--- /dev/null
+++ b/src/services/pluginsManager/map/fitBounds/index.ts
@@ -0,0 +1 @@
+export { fitBounds } from './fitBounds';
diff --git a/src/services/pluginsManager/map/getOpenMapId.test.ts b/src/services/pluginsManager/map/getOpenMapId.test.ts
new file mode 100644
index 00000000..cdbe2147
--- /dev/null
+++ b/src/services/pluginsManager/map/getOpenMapId.test.ts
@@ -0,0 +1,40 @@
+import { RootState, store } from '@/redux/store';
+import { initialMapStateFixture } from '@/redux/map/map.fixtures';
+import { getOpenMapId } from './getOpenMapId';
+import { ERROR_INVALID_MODEL_ID_TYPE_FOR_RETRIEVAL } from '../errorMessages';
+
+describe('getOpenMapId', () => {
+  const getStateMock = jest.spyOn(store, 'getState');
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+  it('should return the modelId of the current map', () => {
+    getStateMock.mockImplementation(
+      () =>
+        ({
+          map: initialMapStateFixture,
+        }) as RootState,
+    );
+
+    expect(getOpenMapId()).toEqual(initialMapStateFixture.data.modelId);
+  });
+
+  it('should throw an error if modelId is not a number', () => {
+    getStateMock.mockImplementation(
+      () =>
+        ({
+          map: {
+            ...initialMapStateFixture,
+            data: {
+              ...initialMapStateFixture.data,
+              modelId: null,
+            },
+          },
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        }) as any,
+    );
+
+    expect(() => getOpenMapId()).toThrowError(ERROR_INVALID_MODEL_ID_TYPE_FOR_RETRIEVAL);
+  });
+});
diff --git a/src/services/pluginsManager/map/getOpenMapId.ts b/src/services/pluginsManager/map/getOpenMapId.ts
new file mode 100644
index 00000000..dd3cc808
--- /dev/null
+++ b/src/services/pluginsManager/map/getOpenMapId.ts
@@ -0,0 +1,13 @@
+import { store } from '@/redux/store';
+import { ERROR_INVALID_MODEL_ID_TYPE_FOR_RETRIEVAL } from '../errorMessages';
+
+export const getOpenMapId = (): number => {
+  const currentMap = store.getState().map.data;
+  const openMapId = currentMap.modelId;
+
+  if (typeof openMapId !== 'number') {
+    throw new Error(ERROR_INVALID_MODEL_ID_TYPE_FOR_RETRIEVAL);
+  }
+
+  return openMapId;
+};
diff --git a/src/services/pluginsManager/map/mapManager.test.ts b/src/services/pluginsManager/map/mapManager.test.ts
new file mode 100644
index 00000000..9f3349c9
--- /dev/null
+++ b/src/services/pluginsManager/map/mapManager.test.ts
@@ -0,0 +1,34 @@
+import { Map } from 'ol';
+
+import { MapInstance } from '@/types/map';
+import { MapManager } from './mapManager';
+
+describe('MapManager', () => {
+  describe('getMapInstance', () => {
+    it('should return null if no map instance is set', () => {
+      expect(MapManager.getMapInstance()).toBeNull();
+    });
+
+    it('should return the set map instance', () => {
+      const dummyElement = document.createElement('div');
+      const mapInstance = new Map({ target: dummyElement });
+      MapManager.setMapInstance(mapInstance);
+      expect(MapManager.getMapInstance()).toEqual(mapInstance);
+    });
+  });
+
+  describe('setMapInstance', () => {
+    beforeEach(() => {
+      MapManager.mapInstance = null;
+    });
+    it('should set the map instance', () => {
+      const dummyElement = document.createElement('div');
+      const mapInstance = new Map({ target: dummyElement });
+      MapManager.setMapInstance(mapInstance);
+      expect(MapManager.mapInstance).toEqual(mapInstance);
+    });
+    it('should throw error if map instance is not valid', () => {
+      expect(() => MapManager.setMapInstance({} as MapInstance)).toThrow('Not valid map instance');
+    });
+  });
+});
diff --git a/src/services/pluginsManager/map/mapManager.ts b/src/services/pluginsManager/map/mapManager.ts
new file mode 100644
index 00000000..a710f208
--- /dev/null
+++ b/src/services/pluginsManager/map/mapManager.ts
@@ -0,0 +1,18 @@
+import { MapInstance } from '@/types/map';
+import { Map } from 'ol';
+
+type MapManagerType = {
+  mapInstance: null | MapInstance;
+  setMapInstance: (mapInstance: MapInstance) => void;
+  getMapInstance: () => MapInstance | null;
+};
+
+export const MapManager: MapManagerType = {
+  mapInstance: null,
+
+  setMapInstance: (mapInstance: MapInstance) => {
+    if (!(mapInstance instanceof Map)) throw new Error('Not valid map instance');
+    MapManager.mapInstance = mapInstance;
+  },
+  getMapInstance: () => MapManager.mapInstance,
+};
diff --git a/src/services/pluginsManager/map/triggerSearch/getPolygonPoints.ts b/src/services/pluginsManager/map/triggerSearch/getPolygonPoints.ts
new file mode 100644
index 00000000..ce27f4ba
--- /dev/null
+++ b/src/services/pluginsManager/map/triggerSearch/getPolygonPoints.ts
@@ -0,0 +1,33 @@
+import { allVisibleBioEntitiesSelector } from '@/redux/bioEntity/bioEntity.selectors';
+import { store } from '@/redux/store';
+import { isPointValid } from '@/utils/point/isPointValid';
+
+type Points = {
+  x: number;
+  y: number;
+}[];
+
+export const getPolygonPoints = (): Points => {
+  const allVisibleBioEntities = allVisibleBioEntitiesSelector(store.getState());
+  const allX = allVisibleBioEntities.map(({ x }) => x);
+  const allY = allVisibleBioEntities.map(({ y }) => y);
+
+  const minX = Math.min(...allX);
+  const maxX = Math.max(...allX);
+
+  const minY = Math.min(...allY);
+  const maxY = Math.max(...allY);
+
+  const points = [
+    {
+      x: minX,
+      y: maxY,
+    },
+    {
+      x: maxX,
+      y: minY,
+    },
+  ];
+
+  return points.filter(isPointValid);
+};
diff --git a/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts b/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts
new file mode 100644
index 00000000..47fc1165
--- /dev/null
+++ b/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.test.ts
@@ -0,0 +1,158 @@
+/* eslint-disable no-magic-numbers */
+import { MAP_INITIAL_STATE } from '@/redux/map/map.constants';
+import { RootState, store } from '@/redux/store';
+import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
+import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants';
+
+import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock';
+import { getVisibleBioEntitiesPolygonCoordinates } from './getVisibleBioEntitiesPolygonCoordinates';
+
+jest.mock('../../../../redux/store');
+
+const getStateSpy = jest.spyOn(store, 'getState');
+
+describe('getVisibleBioEntitiesPolygonCoordinates', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+  it('should return undefined if received array does not contain bioEntities with current map id', () => {
+    getStateSpy.mockImplementation(
+      () =>
+        ({
+          models: MODELS_DATA_MOCK_WITH_MAIN_MAP,
+          map: {
+            ...MAP_INITIAL_STATE,
+            data: {
+              ...MAP_INITIAL_STATE.data,
+              modelId: 5052,
+              size: {
+                width: 256,
+                height: 256,
+                tileSize: 256,
+                minZoom: 1,
+                maxZoom: 1,
+              },
+            },
+          },
+          bioEntity: {
+            loading: 'succeeded',
+            error: { message: '', name: '' },
+          },
+          drugs: {
+            loading: 'succeeded',
+            error: { message: '', name: '' },
+          },
+          drawer: {
+            ...DRAWER_INITIAL_STATE,
+            bioEntityDrawerState: {
+              bioentityId: undefined,
+              drugs: {},
+              chemicals: {},
+            },
+          },
+        }) as RootState,
+    );
+
+    expect(getVisibleBioEntitiesPolygonCoordinates()).toBe(undefined);
+  });
+  it('should return coordinates, max zoom, and map instance if received array contain bioEntities with current map id and max zoom', () => {
+    getStateSpy.mockImplementation(
+      () =>
+        ({
+          models: MODELS_DATA_MOCK_WITH_MAIN_MAP,
+          map: {
+            ...MAP_INITIAL_STATE,
+            data: {
+              ...MAP_INITIAL_STATE.data,
+              modelId: 52,
+              size: {
+                width: 256,
+                height: 256,
+                tileSize: 256,
+                minZoom: 1,
+                maxZoom: 5,
+              },
+            },
+          },
+          bioEntity: {
+            data: [
+              {
+                searchQueryElement: bioEntityContentFixture.bioEntity.name,
+                loading: 'succeeded',
+                error: { name: '', message: '' },
+                data: [
+                  {
+                    ...bioEntityContentFixture,
+                    bioEntity: {
+                      ...bioEntityContentFixture.bioEntity,
+                      model: 52,
+                      x: 97,
+                      y: 53,
+                      z: 1,
+                    },
+                  },
+                  {
+                    ...bioEntityContentFixture,
+                    bioEntity: {
+                      ...bioEntityContentFixture.bioEntity,
+                      model: 52,
+                      x: 12,
+                      y: 25,
+                      z: 1,
+                    },
+                  },
+                ],
+              },
+            ],
+            loading: 'succeeded',
+            error: { message: '', name: '' },
+          },
+          drugs: {
+            data: [
+              {
+                searchQueryElement: '',
+                loading: 'succeeded',
+                error: { name: '', message: '' },
+                data: undefined,
+              },
+            ],
+            loading: 'succeeded',
+            error: { message: '', name: '' },
+          },
+          chemicals: {
+            data: [
+              {
+                searchQueryElement: '',
+                loading: 'succeeded',
+                error: { name: '', message: '' },
+                data: undefined,
+              },
+            ],
+            loading: 'succeeded',
+            error: { message: '', name: '' },
+          },
+          drawer: {
+            ...DRAWER_INITIAL_STATE,
+            bioEntityDrawerState: {
+              bioentityId: undefined,
+              drugs: {},
+              chemicals: {},
+            },
+            searchDrawerState: {
+              ...DRAWER_INITIAL_STATE.searchDrawerState,
+              selectedSearchElement: bioEntityContentFixture.bioEntity.name,
+            },
+          },
+        }) as RootState,
+    );
+
+    expect(getVisibleBioEntitiesPolygonCoordinates()).toEqual({
+      mapInstance: null,
+      maxZoom: 5,
+      polygonCoordinates: [
+        [-18158992, 11740728],
+        [-4852834, 16123932],
+      ],
+    });
+  });
+});
diff --git a/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.ts b/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.ts
new file mode 100644
index 00000000..c1fba338
--- /dev/null
+++ b/src/services/pluginsManager/map/triggerSearch/getVisibleBioEntitiesPolygonCoordinates.ts
@@ -0,0 +1,33 @@
+import { store } from '@/redux/store';
+import { mapDataSizeSelector } from '@/redux/map/map.selectors';
+import { MapInstance } from '@/types/map';
+import { MapManager } from '../mapManager';
+import { pointToProjection } from '../fitBounds/fitBounds.utils';
+import { getPolygonPoints } from './getPolygonPoints';
+
+const VALID_POLYGON_COORDINATES_LENGTH = 2;
+
+export const getVisibleBioEntitiesPolygonCoordinates = ():
+  | {
+      polygonCoordinates: number[][];
+      maxZoom: number;
+      mapInstance: MapInstance | null;
+    }
+  | undefined => {
+  const mapSize = mapDataSizeSelector(store.getState());
+  const { maxZoom } = mapDataSizeSelector(store.getState());
+
+  const polygonPoints = getPolygonPoints();
+
+  const polygonCoordinates = polygonPoints.map(point => pointToProjection(point, mapSize));
+
+  if (polygonCoordinates.length !== VALID_POLYGON_COORDINATES_LENGTH) {
+    return undefined;
+  }
+
+  return {
+    polygonCoordinates,
+    maxZoom,
+    mapInstance: MapManager.getMapInstance(),
+  };
+};
diff --git a/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts b/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts
index bdd6a539..c04dfaac 100644
--- a/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts
+++ b/src/services/pluginsManager/map/triggerSearch/searchByCoordinates.ts
@@ -8,6 +8,8 @@ import { Coordinates } from './triggerSearch.types';
 export const searchByCoordinates = async (
   coordinates: Coordinates,
   modelId: number,
+  hasFitBounds?: boolean,
+  fitBoundsZoom?: number,
 ): Promise<void> => {
   const { dispatch } = store;
   // side-effect below is to prevent complications with data update - old data may conflict with new data
@@ -23,5 +25,5 @@ export const searchByCoordinates = async (
     return;
   }
 
-  handleSearchResultAction({ searchResults, dispatch });
+  handleSearchResultAction({ searchResults, dispatch, hasFitBounds, fitBoundsZoom });
 };
diff --git a/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts b/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts
new file mode 100644
index 00000000..e804a294
--- /dev/null
+++ b/src/services/pluginsManager/map/triggerSearch/searchByQuery.test.ts
@@ -0,0 +1,119 @@
+import { RootState, store } from '@/redux/store';
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import { apiPath } from '@/redux/apiPath';
+import { HttpStatusCode } from 'axios';
+import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture';
+import { drugsFixture } from '@/models/fixtures/drugFixtures';
+import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture';
+import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants';
+import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock';
+import { MAP_INITIAL_STATE } from '@/redux/map/map.constants';
+import { waitFor } from '@testing-library/react';
+import { searchByQuery } from './searchByQuery';
+import { searchFitBounds } from './searchFitBounds';
+
+const MOCK_SEARCH_BY_QUERY_STORE = {
+  models: MODELS_DATA_MOCK_WITH_MAIN_MAP,
+  map: {
+    ...MAP_INITIAL_STATE,
+    data: {
+      ...MAP_INITIAL_STATE.data,
+      modelId: 5052,
+      size: {
+        width: 256,
+        height: 256,
+        tileSize: 256,
+        minZoom: 1,
+        maxZoom: 1,
+      },
+    },
+  },
+  bioEntity: {
+    loading: 'succeeded',
+    error: { message: '', name: '' },
+  },
+  drugs: {
+    loading: 'succeeded',
+    error: { message: '', name: '' },
+  },
+  drawer: {
+    ...DRAWER_INITIAL_STATE,
+    bioEntityDrawerState: {
+      bioentityId: undefined,
+      drugs: {},
+      chemicals: {},
+    },
+  },
+};
+
+const mockedAxiosClient = mockNetworkNewAPIResponse();
+const SEARCH_QUERY = 'park7';
+
+jest.mock('./searchFitBounds');
+jest.mock('../../../../redux/store');
+const dispatchSpy = jest.spyOn(store, 'dispatch');
+const getStateSpy = jest.spyOn(store, 'getState');
+
+describe('searchByQuery', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+  it('should fit bounds after search if hasFitBounds param is true', async () => {
+    dispatchSpy.mockImplementation(() => ({
+      unwrap: (): Promise<void> => Promise.resolve(),
+    }));
+
+    getStateSpy.mockImplementation(() => MOCK_SEARCH_BY_QUERY_STORE as RootState);
+    mockedAxiosClient
+      .onGet(
+        apiPath.getBioEntityContentsStringWithQuery({
+          searchQuery: SEARCH_QUERY,
+          isPerfectMatch: false,
+        }),
+      )
+      .reply(HttpStatusCode.Ok, bioEntityResponseFixture);
+
+    mockedAxiosClient
+      .onGet(apiPath.getDrugsStringWithQuery(SEARCH_QUERY))
+      .reply(HttpStatusCode.Ok, drugsFixture);
+
+    mockedAxiosClient
+      .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY))
+      .reply(HttpStatusCode.Ok, chemicalsFixture);
+
+    searchByQuery(SEARCH_QUERY, false, true);
+
+    await waitFor(() => {
+      expect(searchFitBounds).toHaveBeenCalled();
+    });
+  });
+  it('should not fit bounds after search if hasFitBounds param is false', async () => {
+    dispatchSpy.mockImplementation(() => ({
+      unwrap: (): Promise<void> => Promise.resolve(),
+    }));
+
+    getStateSpy.mockImplementation(() => MOCK_SEARCH_BY_QUERY_STORE as RootState);
+    mockedAxiosClient
+      .onGet(
+        apiPath.getBioEntityContentsStringWithQuery({
+          searchQuery: SEARCH_QUERY,
+          isPerfectMatch: false,
+        }),
+      )
+      .reply(HttpStatusCode.Ok, bioEntityResponseFixture);
+
+    mockedAxiosClient
+      .onGet(apiPath.getDrugsStringWithQuery(SEARCH_QUERY))
+      .reply(HttpStatusCode.Ok, drugsFixture);
+
+    mockedAxiosClient
+      .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY))
+      .reply(HttpStatusCode.Ok, chemicalsFixture);
+
+    searchByQuery(SEARCH_QUERY, false, false);
+
+    await waitFor(() => {
+      expect(searchFitBounds).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts b/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts
index 01d06037..4f48c0a7 100644
--- a/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts
+++ b/src/services/pluginsManager/map/triggerSearch/searchByQuery.ts
@@ -2,13 +2,27 @@ import { getSearchValuesArrayAndTrimToSeven } from '@/components/FunctionalArea/
 import { getSearchData } from '@/redux/search/search.thunks';
 import { store } from '@/redux/store';
 import { displaySearchDrawerWithSelectedDefaultTab } from './displaySearchDrawerWithSelectedDefaultTab';
+import { searchFitBounds } from './searchFitBounds';
 
-export const searchByQuery = (query: string, perfectSearch: boolean | undefined): void => {
+export const searchByQuery = (
+  query: string,
+  perfectSearch: boolean | undefined,
+  hasFitBounds?: boolean,
+): void => {
   const { dispatch } = store;
   const searchValues = getSearchValuesArrayAndTrimToSeven(query);
   const isPerfectMatch = !!perfectSearch;
 
-  dispatch(getSearchData({ searchQueries: searchValues, isPerfectMatch }));
+  dispatch(getSearchData({ searchQueries: searchValues, isPerfectMatch }))
+    ?.unwrap()
+    .then(() => {
+      if (hasFitBounds) {
+        searchFitBounds();
+      }
+    })
+    .catch(() => {
+      // TODO to discuss manage state of failure
+    });
 
   displaySearchDrawerWithSelectedDefaultTab(searchValues);
 };
diff --git a/src/services/pluginsManager/map/triggerSearch/searchFitBounds.test.ts b/src/services/pluginsManager/map/triggerSearch/searchFitBounds.test.ts
new file mode 100644
index 00000000..25847a33
--- /dev/null
+++ b/src/services/pluginsManager/map/triggerSearch/searchFitBounds.test.ts
@@ -0,0 +1,105 @@
+/* eslint-disable no-magic-numbers */
+import { handleSetBounds } from '@/utils/map/useSetBounds';
+import { Map } from 'ol';
+import * as getVisibleBioEntitiesPolygonCoordinates from './getVisibleBioEntitiesPolygonCoordinates';
+import { searchFitBounds } from './searchFitBounds';
+
+jest.mock('../../../../utils/map/useSetBounds');
+
+jest.mock('./getVisibleBioEntitiesPolygonCoordinates', () => ({
+  __esModule: true,
+  ...jest.requireActual('./getVisibleBioEntitiesPolygonCoordinates'),
+}));
+
+const getVisibleBioEntitiesPolygonCoordinatesSpy = jest.spyOn(
+  getVisibleBioEntitiesPolygonCoordinates,
+  'getVisibleBioEntitiesPolygonCoordinates',
+);
+
+describe('searchFitBounds', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+  it('should not handle set bounds if data is not valid', () => {
+    getVisibleBioEntitiesPolygonCoordinatesSpy.mockImplementation(() => undefined);
+
+    searchFitBounds();
+
+    expect(handleSetBounds).not.toHaveBeenCalled();
+  });
+  it('should not handle set bounds if map instance is not valid', () => {
+    getVisibleBioEntitiesPolygonCoordinatesSpy.mockImplementation(() => ({
+      mapInstance: null,
+      maxZoom: 5,
+      polygonCoordinates: [
+        [231, 231],
+        [842, 271],
+      ],
+    }));
+
+    searchFitBounds();
+
+    expect(handleSetBounds).not.toHaveBeenCalled();
+  });
+  it('should handle set bounds if provided data is valid', () => {
+    const dummyElement = document.createElement('div');
+    const mapInstance = new Map({ target: dummyElement });
+    const maxZoom = 5;
+    const polygonCoordinates = [
+      [231, 231],
+      [842, 271],
+    ];
+
+    getVisibleBioEntitiesPolygonCoordinatesSpy.mockImplementation(() => ({
+      mapInstance,
+      maxZoom,
+      polygonCoordinates,
+    }));
+
+    searchFitBounds();
+
+    expect(handleSetBounds).toHaveBeenCalled();
+    expect(handleSetBounds).toHaveBeenCalledWith(mapInstance, maxZoom, polygonCoordinates);
+  });
+  it('should handle set bounds with max zoom if zoom is not provided in argument', () => {
+    const dummyElement = document.createElement('div');
+    const mapInstance = new Map({ target: dummyElement });
+    const maxZoom = 23;
+    const polygonCoordinates = [
+      [231, 231],
+      [842, 271],
+    ];
+
+    getVisibleBioEntitiesPolygonCoordinatesSpy.mockImplementation(() => ({
+      mapInstance,
+      maxZoom,
+      polygonCoordinates,
+    }));
+
+    searchFitBounds();
+
+    expect(handleSetBounds).toHaveBeenCalled();
+    expect(handleSetBounds).toHaveBeenCalledWith(mapInstance, maxZoom, polygonCoordinates);
+  });
+  it('should handle set bounds with zoom provided in argument instead of max zoom', () => {
+    const zoom = 12;
+    const dummyElement = document.createElement('div');
+    const mapInstance = new Map({ target: dummyElement });
+    const maxZoom = 23;
+    const polygonCoordinates = [
+      [231, 231],
+      [842, 271],
+    ];
+
+    getVisibleBioEntitiesPolygonCoordinatesSpy.mockImplementation(() => ({
+      mapInstance,
+      maxZoom,
+      polygonCoordinates,
+    }));
+
+    searchFitBounds(zoom);
+
+    expect(handleSetBounds).toHaveBeenCalled();
+    expect(handleSetBounds).toHaveBeenCalledWith(mapInstance, zoom, polygonCoordinates);
+  });
+});
diff --git a/src/services/pluginsManager/map/triggerSearch/searchFitBounds.ts b/src/services/pluginsManager/map/triggerSearch/searchFitBounds.ts
new file mode 100644
index 00000000..9c2954eb
--- /dev/null
+++ b/src/services/pluginsManager/map/triggerSearch/searchFitBounds.ts
@@ -0,0 +1,15 @@
+import { handleSetBounds } from '@/utils/map/useSetBounds';
+import { getVisibleBioEntitiesPolygonCoordinates } from './getVisibleBioEntitiesPolygonCoordinates';
+
+export const searchFitBounds = (zoom?: number): void => {
+  const data = getVisibleBioEntitiesPolygonCoordinates();
+
+  if (data) {
+    const { polygonCoordinates, maxZoom, mapInstance } = data;
+
+    if (!mapInstance) return;
+
+    const setBoundsZoom = zoom || maxZoom;
+    handleSetBounds(mapInstance, setBoundsZoom, polygonCoordinates);
+  }
+};
diff --git a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts
index 3c7408eb..1b522bb0 100644
--- a/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts
+++ b/src/services/pluginsManager/map/triggerSearch/triggerSearch.test.ts
@@ -10,6 +10,7 @@ import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFix
 import { waitFor } from '@testing-library/react';
 import { handleSearchResultAction } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleSearchResultAction';
 import { triggerSearch } from './triggerSearch';
+import { ERROR_INVALID_MODEL_ID_TYPE } from '../../errorMessages';
 
 const mockedAxiosClient = mockNetworkNewAPIResponse();
 const mockedAxiosOldClient = mockNetworkResponse();
@@ -148,9 +149,7 @@ describe('triggerSearch', () => {
         modelId: '53' as any,
       };
 
-      await expect(triggerSearch(invalidParams)).rejects.toThrowError(
-        'Invalid model id type. The model should be of number type',
-      );
+      await expect(triggerSearch(invalidParams)).rejects.toThrowError(ERROR_INVALID_MODEL_ID_TYPE);
     });
     it('should search result with proper data', async () => {
       mockedAxiosOldClient
diff --git a/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts b/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts
index 8f1b42fe..8a69f07b 100644
--- a/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts
+++ b/src/services/pluginsManager/map/triggerSearch/triggerSearch.ts
@@ -12,7 +12,7 @@ export async function triggerSearch(params: SearchParams): Promise<void> {
     if (typeof params.query !== 'string') {
       throw new Error(ERROR_INVALID_QUERY_TYPE);
     }
-    searchByQuery(params.query, params.perfectSearch);
+    searchByQuery(params.query, params.perfectSearch, params.fitBounds);
   } else {
     const areCoordinatesInvalidType =
       typeof params.coordinates !== 'object' || params.coordinates === null;
@@ -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);
+    searchByCoordinates(params.coordinates, params.modelId, params.fitBounds, params.zoom);
   }
 }
diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts
index 23a78ff8..c0b7bd2b 100644
--- a/src/services/pluginsManager/pluginsManager.ts
+++ b/src/services/pluginsManager/pluginsManager.ts
@@ -15,6 +15,10 @@ import { getOrganism } from './project/data/getOrganism';
 import type { PluginsManagerType } from './pluginsManager.types';
 import { configurationMapper } from './pluginsManager.utils';
 
+import { getBounds } from './map/data/getBounds';
+import { fitBounds } from './map/fitBounds';
+import { getOpenMapId } from './map/getOpenMapId';
+
 export const PluginsManager: PluginsManagerType = {
   hashedPlugins: {},
   setHashedPlugin({ pluginUrl, pluginScript }) {
@@ -34,8 +38,11 @@ export const PluginsManager: PluginsManagerType = {
       },
       map: {
         data: {
+          getBounds,
+          getOpenMapId,
           getModels,
         },
+        fitBounds,
         openMap,
         triggerSearch,
       },
diff --git a/src/utils/context/mapInstanceContext.tsx b/src/utils/context/mapInstanceContext.tsx
index 1c0982d8..85fe8fc8 100644
--- a/src/utils/context/mapInstanceContext.tsx
+++ b/src/utils/context/mapInstanceContext.tsx
@@ -1,14 +1,15 @@
+import { MapManager } from '@/services/pluginsManager/map/mapManager';
 import { MapInstance } from '@/types/map';
-import { Dispatch, SetStateAction, createContext, useContext, useMemo, useState } from 'react';
+import { createContext, useCallback, useContext, useMemo, useState } from 'react';
 
 export interface MapInstanceContext {
   mapInstance: MapInstance;
-  setMapInstance: Dispatch<SetStateAction<MapInstance>>;
+  handleSetMapInstance: (mapInstance: MapInstance) => void;
 }
 
 export const MapInstanceContext = createContext<MapInstanceContext>({
   mapInstance: undefined,
-  setMapInstance: () => {},
+  handleSetMapInstance: () => {},
 });
 
 export const useMapInstance = (): MapInstanceContext => useContext(MapInstanceContext);
@@ -24,12 +25,22 @@ export const MapInstanceProvider = ({
 }: MapInstanceProviderProps): JSX.Element => {
   const [mapInstance, setMapInstance] = useState<MapInstance>(initialValue?.mapInstance);
 
+  const handleSetMapInstance = useCallback(
+    (map: MapInstance) => {
+      if (!mapInstance) {
+        setMapInstance(map);
+        MapManager.setMapInstance(map);
+      }
+    },
+    [mapInstance],
+  );
+
   const mapInstanceContextValue = useMemo(
     () => ({
       mapInstance,
-      setMapInstance,
+      handleSetMapInstance,
     }),
-    [mapInstance],
+    [mapInstance, handleSetMapInstance],
   );
 
   return (
diff --git a/src/utils/map/useSetBounds.test.ts b/src/utils/map/useSetBounds.test.ts
index 4e00469d..71ee1260 100644
--- a/src/utils/map/useSetBounds.test.ts
+++ b/src/utils/map/useSetBounds.test.ts
@@ -39,7 +39,7 @@ describe('useSetBounds - hook', () => {
         {
           mapInstanceContextValue: {
             mapInstance: undefined,
-            setMapInstance: () => {},
+            handleSetMapInstance: () => {},
           },
         },
       );
@@ -84,7 +84,7 @@ describe('useSetBounds - hook', () => {
         {
           mapInstanceContextValue: {
             mapInstance,
-            setMapInstance: () => {},
+            handleSetMapInstance: () => {},
           },
         },
       );
-- 
GitLab