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

add: map location btn and fit bounds logic

parent bb7f50c3
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!103feat: Add location button and zoom to pins business logic (MIN-184)
Pipeline #84233 passed
Showing
with 754 additions and 29 deletions
import { store } from '@/redux/store';
import { MapInstanceProvider } from '@/utils/context/mapInstanceContext';
import { ReactNode } from 'react';
import { Provider } from 'react-redux';
import { store } from '@/redux/store';
interface AppWrapperProps {
children: ReactNode;
}
export const AppWrapper = ({ children }: AppWrapperProps): JSX.Element => (
<Provider store={store}>{children}</Provider>
<MapInstanceProvider>
<Provider store={store}>{children}</Provider>
</MapInstanceProvider>
);
/* eslint-disable no-magic-numbers */
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { AppDispatch, RootState } from '@/redux/store';
import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { AppDispatch, RootState, StoreType } from '@/redux/store';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import { InitialStoreState } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { act, render, screen } from '@testing-library/react';
import Map from 'ol/Map';
import { MockStoreEnhanced } from 'redux-mock-store';
import { MapAdditionalActions } from './MapAdditionalActions.component';
import { useVisibleBioEntitiesPolygonCoordinates } from './utils/useVisibleBioEntitiesPolygonCoordinates';
const setBounds = jest.fn();
jest.mock('../../../utils/map/useSetBounds', () => ({
_esModule: true,
useSetBounds: (): jest.Mock => setBounds,
}));
jest.mock('./utils/useVisibleBioEntitiesPolygonCoordinates', () => ({
_esModule: true,
useVisibleBioEntitiesPolygonCoordinates: jest.fn(),
}));
const useVisibleBioEntitiesPolygonCoordinatesMock =
useVisibleBioEntitiesPolygonCoordinates as jest.Mock;
setBounds.mockImplementation(() => {});
const renderComponent = (
initialStore?: InitialStoreState,
......@@ -22,10 +47,33 @@ const renderComponent = (
);
};
const renderComponentWithMapInstance = (initialStore?: InitialStoreState): { store: StoreType } => {
const dummyElement = document.createElement('div');
const mapInstance = new Map({ target: dummyElement });
const { Wrapper, store } = getReduxWrapperWithStore(initialStore, {
mapInstanceContextValue: {
mapInstance,
setMapInstance: () => {},
},
});
return (
render(
<Wrapper>
<MapAdditionalActions />
</Wrapper>,
),
{
store,
}
);
};
describe('MapAdditionalActions - component', () => {
describe('when always', () => {
beforeEach(() => {
renderComponent();
renderComponent(INITIAL_STORE_STATE_MOCK);
});
it('should render zoom in button', () => {
......@@ -49,7 +97,7 @@ describe('MapAdditionalActions - component', () => {
describe('when clicked on zoom in button', () => {
it('should dispatch varyPositionZoom action with valid delta', () => {
const { store } = renderComponent();
const { store } = renderComponent(INITIAL_STORE_STATE_MOCK);
const image = screen.getByAltText('zoom in button icon');
const button = image.closest('button');
button!.click();
......@@ -64,7 +112,7 @@ describe('MapAdditionalActions - component', () => {
describe('when clicked on zoom in button', () => {
it('should dispatch varyPositionZoom action with valid delta', () => {
const { store } = renderComponent();
const { store } = renderComponent(INITIAL_STORE_STATE_MOCK);
const image = screen.getByAltText('zoom out button icon');
const button = image.closest('button');
button!.click();
......@@ -77,7 +125,41 @@ describe('MapAdditionalActions - component', () => {
});
});
describe.skip('when clicked on location button', () => {
// TODO: implelemnt test
describe('when clicked on location button', () => {
it('setBounds should be called', () => {
useVisibleBioEntitiesPolygonCoordinatesMock.mockImplementation(() => [
[128, 128],
[192, 192],
]);
renderComponentWithMapInstance({
map: {
data: {
...MAP_DATA_INITIAL_STATE,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
loading: 'idle',
error: {
name: '',
message: '',
},
openedMaps: [],
},
});
const image = screen.getByAltText('location button icon');
const button = image.closest('button');
act(() => {
button!.click();
});
expect(setBounds).toHaveBeenCalled();
});
});
});
/* eslint-disable no-magic-numbers */
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import Map from 'ol/Map';
import { useAddtionalActions } from './useAdditionalActions';
import { useVisibleBioEntitiesPolygonCoordinates } from './useVisibleBioEntitiesPolygonCoordinates';
jest.mock('./useVisibleBioEntitiesPolygonCoordinates', () => ({
_esModule: true,
useVisibleBioEntitiesPolygonCoordinates: jest.fn(),
}));
const useVisibleBioEntitiesPolygonCoordinatesMock =
useVisibleBioEntitiesPolygonCoordinates as jest.Mock;
describe('useAddtionalActions - hook', () => {
describe('on zoomIn', () => {
it('should dispatch varyPositionZoom action with valid delta', () => {
const { Wrapper, store } = getReduxStoreWithActionsListener();
const { Wrapper, store } = getReduxStoreWithActionsListener(INITIAL_STORE_STATE_MOCK);
const {
result: {
current: { zoomIn },
......@@ -27,7 +41,7 @@ describe('useAddtionalActions - hook', () => {
describe('on zoomOut', () => {
it('should dispatch varyPositionZoom action with valid delta', () => {
const { Wrapper, store } = getReduxStoreWithActionsListener();
const { Wrapper, store } = getReduxStoreWithActionsListener(INITIAL_STORE_STATE_MOCK);
const {
result: {
current: { zoomOut },
......@@ -47,6 +61,79 @@ describe('useAddtionalActions - hook', () => {
});
describe('on zoomInToBioEntities', () => {
// TODO: implelemnt test
describe('when there are valid polygon coordinates', () => {
beforeEach(() => {
useVisibleBioEntitiesPolygonCoordinatesMock.mockImplementation(() => [
[128, 128],
[192, 192],
]);
});
it('should return valid results', () => {
const dummyElement = document.createElement('div');
const mapInstance = new Map({ target: dummyElement });
const { Wrapper } = getReduxWrapperWithStore(
{
map: {
data: {
...MAP_DATA_INITIAL_STATE,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
loading: 'idle',
error: {
name: '',
message: '',
},
openedMaps: [],
},
},
{
mapInstanceContextValue: {
mapInstance,
setMapInstance: () => {},
},
},
);
const {
result: {
current: { zoomInToBioEntities },
},
} = renderHook(() => useAddtionalActions(), {
wrapper: Wrapper,
});
expect(zoomInToBioEntities()).toStrictEqual({
extent: [128, 128, 192, 192],
options: { maxZoom: 1, padding: [128, 128, 128, 128], size: undefined },
// size is real size on the screen, so it'll be undefined in the jest
});
});
});
describe('when there are no polygon coordinates', () => {
beforeEach(() => {
useVisibleBioEntitiesPolygonCoordinatesMock.mockImplementation(() => undefined);
});
it('should return undefined', () => {
const { Wrapper } = getReduxStoreWithActionsListener(INITIAL_STORE_STATE_MOCK);
const {
result: {
current: { zoomInToBioEntities },
},
} = renderHook(() => useAddtionalActions(), {
wrapper: Wrapper,
});
expect(zoomInToBioEntities()).toBeUndefined();
});
});
});
});
import { varyPositionZoom } from '@/redux/map/map.slice';
import { SetBoundsResult, useSetBounds } from '@/utils/map/useSetBounds';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { MAP_ZOOM_IN_DELTA, MAP_ZOOM_OUT_DELTA } from '../MappAdditionalActions.constants';
import { useVisibleBioEntitiesPolygonCoordinates } from './useVisibleBioEntitiesPolygonCoordinates';
interface UseAddtionalActionsResult {
zoomIn(): void;
......@@ -11,6 +13,16 @@ interface UseAddtionalActionsResult {
export const useAddtionalActions = (): UseAddtionalActionsResult => {
const dispatch = useDispatch();
const setBounds = useSetBounds();
const polygonCoordinates = useVisibleBioEntitiesPolygonCoordinates();
const zoomInToBioEntities = (): SetBoundsResult | undefined => {
if (!polygonCoordinates) {
return undefined;
}
return setBounds(polygonCoordinates);
};
const varyZoomByDelta = useCallback(
(delta: number) => {
......@@ -22,6 +34,6 @@ export const useAddtionalActions = (): UseAddtionalActionsResult => {
return {
zoomIn: () => varyZoomByDelta(MAP_ZOOM_IN_DELTA),
zoomOut: () => varyZoomByDelta(MAP_ZOOM_OUT_DELTA),
zoomInToBioEntities: (): void => {},
zoomInToBioEntities,
};
};
import { drugsFixture } from '@/models/fixtures/drugFixtures';
/* eslint-disable no-magic-numbers */
import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import { BIOENTITY_INITIAL_STATE_MOCK } from '@/redux/bioEntity/bioEntity.mock';
import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { RootState } from '@/redux/store';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import { CHEMICALS_INITIAL_STATE_MOCK } from '../../../../redux/chemicals/chemicals.mock';
import { DRUGS_INITIAL_STATE_MOCK } from '../../../../redux/drugs/drugs.mock';
import { DEFAULT_POSITION, MAIN_MAP, MAP_INITIAL_STATE } from '../../../../redux/map/map.constants';
import { MODELS_INITIAL_STATE_MOCK } from '../../../../redux/models/models.mock';
import { useVisibleBioEntitiesPolygonCoordinates } from './useVisibleBioEntitiesPolygonCoordinates';
/* key elements of the state:
- this state simulates situation where there is:
-- one searched element
-- of currently selected map
-- for each content/chemicals/drugs data set
- the key differences in this states are x/y/z coordinates of element's bioEntities
*/
const getInitalState = (
{ hideElements }: { hideElements: boolean } = { hideElements: false },
): RootState => {
const elementsLimit = hideElements ? 0 : 1;
return {
...INITIAL_STORE_STATE_MOCK,
drawer: {
...DRAWER_INITIAL_STATE,
searchDrawerState: {
...DRAWER_INITIAL_STATE.searchDrawerState,
selectedSearchElement: 'search',
},
},
models: {
...MODELS_INITIAL_STATE_MOCK,
data: [
{
...modelsFixture[0],
idObject: 5052,
},
],
},
map: {
...MAP_INITIAL_STATE,
data: {
...MAP_INITIAL_STATE.data,
modelId: 5052,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
openedMaps: [{ modelId: 5052, modelName: MAIN_MAP, lastPosition: DEFAULT_POSITION }],
},
bioEntity: {
...BIOENTITY_INITIAL_STATE_MOCK,
data: [
{
searchQueryElement: 'search',
data: [
{
...bioEntityContentFixture,
bioEntity: {
...bioEntityContentFixture.bioEntity,
model: 5052,
x: 16,
y: 16,
z: 1,
},
},
].slice(0, elementsLimit),
loading: 'succeeded',
error: { message: '', name: '' },
},
],
},
chemicals: {
...CHEMICALS_INITIAL_STATE_MOCK,
data: [
{
searchQueryElement: 'search',
data: [
{
...chemicalsFixture[0],
targets: [
{
...chemicalsFixture[0].targets[0],
targetElements: [
{
...chemicalsFixture[0].targets[0].targetElements[0],
model: 5052,
x: 32,
y: 32,
z: 1,
},
],
},
],
},
].slice(0, elementsLimit),
loading: 'succeeded',
error: { message: '', name: '' },
},
{
searchQueryElement: 'not-search',
data: [
{
...chemicalsFixture[0],
targets: [
{
...chemicalsFixture[0].targets[0],
targetElements: [
{
...chemicalsFixture[0].targets[0].targetElements[0],
model: 5052,
x: 8,
y: 2,
z: 9,
},
],
},
],
},
].slice(0, elementsLimit),
loading: 'succeeded',
error: { message: '', name: '' },
},
],
},
drugs: {
...DRUGS_INITIAL_STATE_MOCK,
data: [
{
searchQueryElement: 'search',
data: [
{
...drugsFixture[0],
targets: [
{
...drugsFixture[0].targets[0],
targetElements: [
{
...drugsFixture[0].targets[0].targetElements[0],
model: 5052,
x: 128,
y: 128,
z: 1,
},
],
},
],
},
].slice(0, elementsLimit),
loading: 'succeeded',
error: { message: '', name: '' },
},
{
searchQueryElement: 'not-search',
data: [
{
...drugsFixture[0],
targets: [
{
...drugsFixture[0].targets[0],
targetElements: [
{
...drugsFixture[0].targets[0].targetElements[0],
model: 5052,
x: 100,
y: 50,
z: 4,
},
],
},
],
},
].slice(0, elementsLimit),
loading: 'succeeded',
error: { message: '', name: '' },
},
],
},
};
};
describe('useVisibleBioEntitiesPolygonCoordinates - hook', () => {
describe('when allVisibleBioEntities is empty', () => {
const { Wrapper } = getReduxWrapperWithStore(getInitalState({ hideElements: true }));
const { result } = renderHook(() => useVisibleBioEntitiesPolygonCoordinates(), {
wrapper: Wrapper,
});
it('should return undefined', () => {
expect(result.current).toBe(undefined);
});
});
describe('when allVisibleBioEntities has data', () => {
const { Wrapper } = getReduxWrapperWithStore(getInitalState());
const { result } = renderHook(() => useVisibleBioEntitiesPolygonCoordinates(), {
wrapper: Wrapper,
});
it('should return undefined', () => {
expect(result.current).toStrictEqual([
[-17532820, -0],
[0, 17532820],
]);
});
});
});
import { allVisibleBioEntitiesSelector } from '@/redux/bioEntity/bioEntity.selectors';
import { Point } from '@/types/map';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { isPointValid } from '@/utils/point/isPointValid';
import { Coordinate } from 'ol/coordinate';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
const VALID_POLYGON_COORDINATES_LENGTH = 2;
export const useVisibleBioEntitiesPolygonCoordinates = (): Coordinate[] | undefined => {
const allVisibleBioEntities = useSelector(allVisibleBioEntitiesSelector);
const pointToProjection = usePointToProjection();
const polygonPoints = useMemo((): Point[] => {
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);
}, [allVisibleBioEntities]);
const polygonCoordinates = useMemo(
() => polygonPoints.map(point => pointToProjection(point)),
[polygonPoints, pointToProjection],
);
if (polygonCoordinates.length !== VALID_POLYGON_COORDINATES_LENGTH) {
return undefined;
}
return polygonCoordinates;
};
import Map from 'ol/Map';
import View from 'ol/View';
import BaseLayer from 'ol/layer/Base';
export type MapInstance = Map | undefined;
export type MapConfig = {
view: View;
layers: BaseLayer[];
......
/* eslint-disable no-magic-numbers */
import { MapInstance } from '@/types/map';
import { useEffect } from 'react';
import { MapConfig, MapInstance } from '../../MapViewer.types';
import { MapConfig } from '../../MapViewer.types';
import { useOlMapOverlaysLayer } from './overlaysLayer/useOlMapOverlaysLayer';
import { useOlMapPinsLayer } from './pinsLayer/useOlMapPinsLayer';
import { useOlMapReactionsLayer } from './reactionsLayer/useOlMapReactionsLayer';
......
/* eslint-disable no-magic-numbers */
import { OPTIONS } from '@/constants/map';
import { mapDataInitialPositionSelector } from '@/redux/map/map.selectors';
import { Point } from '@/types/map';
import { MapInstance, Point } from '@/types/map';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { View } from 'ol';
import { useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { MapConfig, MapInstance } from '../../MapViewer.types';
import { MapConfig } from '../../MapViewer.types';
interface UseOlMapViewInput {
mapInstance: MapInstance;
......
......@@ -2,16 +2,16 @@ import { OPTIONS } from '@/constants/map';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { mapDataSizeSelector } from '@/redux/map/map.selectors';
import { currentModelIdSelector } from '@/redux/models/models.selectors';
import { MapInstance } from '@/types/map';
import { View } from 'ol';
import { unByKey } from 'ol/Observable';
import { Coordinate } from 'ol/coordinate';
import { Pixel } from 'ol/pixel';
import { useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useDebouncedCallback } from 'use-debounce';
import { Pixel } from 'ol/pixel';
import { Coordinate } from 'ol/coordinate';
import { MapInstance } from '../../MapViewer.types';
import { onMapSingleClick } from './mapSingleClick/onMapSingleClick';
import { onMapRightClick } from './mapRightClick/onMapRightClick';
import { onMapSingleClick } from './mapSingleClick/onMapSingleClick';
import { onMapPositionChange } from './onMapPositionChange';
interface UseOlMapListenersInput {
......
import { MapInstance } from '@/types/map';
import { useMapInstance } from '@/utils/context/mapInstanceContext';
import Map from 'ol/Map';
import React, { MutableRefObject, useEffect, useState } from 'react';
import { MapInstance } from '../MapViewer.types';
import { Zoom } from 'ol/control';
import React, { MutableRefObject, useEffect } from 'react';
import { useOlMapLayers } from './config/useOlMapLayers';
import { useOlMapView } from './config/useOlMapView';
import { useOlMapListeners } from './listeners/useOlMapListeners';
......@@ -17,7 +19,7 @@ type UseOlMap = (input?: UseOlMapInput) => UseOlMapOutput;
export const useOlMap: UseOlMap = ({ target } = {}) => {
const mapRef = React.useRef<null | HTMLDivElement>(null);
const [mapInstance, setMapInstance] = useState<MapInstance>(undefined);
const { mapInstance, setMapInstance } = useMapInstance();
const view = useOlMapView({ mapInstance });
useOlMapLayers({ mapInstance });
useOlMapListeners({ view, mapInstance });
......@@ -32,8 +34,15 @@ export const useOlMap: UseOlMap = ({ target } = {}) => {
target: target || mapRef.current,
});
// remove zoom controls as we are using our own
map.getControls().forEach(mapControl => {
if (mapControl instanceof Zoom) {
map.removeControl(mapControl);
}
});
setMapInstance(currentMap => currentMap || map);
}, [target]);
}, [target, setMapInstance]);
return {
mapRef,
......
import { z } from 'zod';
/* This schema is used only for local Point objects, it's NOT returned from backend */
export const mapPointSchema = z.object({
x: z.number().finite().nonnegative(),
y: z.number().finite().nonnegative(),
z: z.number().finite().nonnegative().optional(),
});
......@@ -3,11 +3,13 @@ import { rootSelector } from '@/redux/root/root.selectors';
import { MultiSearchData } from '@/types/fetchDataState';
import { BioEntity, BioEntityContent, MapModel } from '@/types/models';
import { createSelector } from '@reduxjs/toolkit';
import { searchedChemicalsBioEntitesOfCurrentMapSelector } from '../chemicals/chemicals.selectors';
import { currentSelectedBioEntityIdSelector } from '../contextMenu/contextMenu.selector';
import {
currentSearchedBioEntityId,
currentSelectedSearchElement,
} from '../drawer/drawer.selectors';
import { currentSelectedBioEntityIdSelector } from '../contextMenu/contextMenu.selector';
import { searchedDrugsBioEntitesOfCurrentMapSelector } from '../drugs/drugs.selectors';
import { currentModelIdSelector, modelsDataSelector } from '../models/models.selectors';
export const bioEntitySelector = createSelector(rootSelector, state => state.bioEntity);
......@@ -105,3 +107,12 @@ export const bioEntitiesPerModelSelector = createSelector(
);
},
);
export const allVisibleBioEntitiesSelector = createSelector(
searchedBioEntitesSelectorOfCurrentMap,
searchedChemicalsBioEntitesOfCurrentMapSelector,
searchedDrugsBioEntitesOfCurrentMapSelector,
(content, chemicals, drugs): BioEntity[] => {
return [content, chemicals, drugs].flat();
},
);
import Map from 'ol/Map';
export interface Point {
x: number;
y: number;
......@@ -5,3 +7,5 @@ export interface Point {
}
export type LatLng = [number, number];
export type MapInstance = Map | undefined;
/* excluded from map.ts due to depenceny cycle */
import { useOlMapOverlaysLayer } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer';
import { useOlMapPinsLayer } from '@/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer';
import { useOlMapReactionsLayer } from '@/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer';
import { useOlMapTileLayer } from '@/components/Map/MapViewer/utils/config/useOlMapTileLayer';
export type MapLayers =
| {
tileLayer: ReturnType<typeof useOlMapTileLayer>;
reactionsLayer: ReturnType<typeof useOlMapReactionsLayer>;
pinsLayer: ReturnType<typeof useOlMapPinsLayer>;
overlaysLayer: ReturnType<typeof useOlMapOverlaysLayer>;
}
| undefined;
import { MapInstance } from '@/types/map';
import { Dispatch, SetStateAction, createContext, useContext, useMemo, useState } from 'react';
export interface MapInstanceContext {
mapInstance: MapInstance;
setMapInstance: Dispatch<SetStateAction<MapInstance>>;
}
export const MapInstanceContext = createContext<MapInstanceContext>({
mapInstance: undefined,
setMapInstance: () => {},
});
export const useMapInstance = (): MapInstanceContext => useContext(MapInstanceContext);
export interface MapInstanceProviderProps {
children: React.ReactNode;
initialValue?: MapInstanceContext;
}
export const MapInstanceProvider = ({
children,
initialValue,
}: MapInstanceProviderProps): JSX.Element => {
const [mapInstance, setMapInstance] = useState<MapInstance>(initialValue?.mapInstance);
const mapInstanceContextValue = useMemo(
() => ({
mapInstance,
setMapInstance,
}),
[mapInstance],
);
return (
<MapInstanceContext.Provider value={mapInstanceContextValue}>
{children}
</MapInstanceContext.Provider>
);
};
/* eslint-disable no-magic-numbers */
import { ONE } from '@/constants/common';
import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants';
import { renderHook } from '@testing-library/react';
import { Map } from 'ol';
import { Coordinate } from 'ol/coordinate';
import { getReduxWrapperWithStore } from '../testing/getReduxWrapperWithStore';
import { useSetBounds } from './useSetBounds';
describe('useSetBounds - hook', () => {
const coordinates: Coordinate[] = [
[128, 128],
[192, 192],
];
describe('when mapInstance is not set', () => {
it('setBounds should return void', () => {
const { Wrapper } = getReduxWrapperWithStore(
{
map: {
data: {
...MAP_DATA_INITIAL_STATE,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
loading: 'idle',
error: {
name: '',
message: '',
},
openedMaps: [],
},
},
{
mapInstanceContextValue: {
mapInstance: undefined,
setMapInstance: () => {},
},
},
);
const {
result: { current: setBounds },
} = renderHook(() => useSetBounds(), { wrapper: Wrapper });
expect(setBounds(coordinates)).toBe(undefined);
});
});
describe('when mapInstance is set', () => {
const dummyElement = document.createElement('div');
const mapInstance = new Map({ target: dummyElement });
const view = mapInstance.getView();
const getViewSpy = jest.spyOn(mapInstance, 'getView');
const fitSpy = jest.spyOn(view, 'fit');
it('setBounds should set return void', () => {
const { Wrapper } = getReduxWrapperWithStore(
{
map: {
data: {
...MAP_DATA_INITIAL_STATE,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
loading: 'idle',
error: {
name: '',
message: '',
},
openedMaps: [],
},
},
{
mapInstanceContextValue: {
mapInstance,
setMapInstance: () => {},
},
},
);
const {
result: { current: setBounds },
} = renderHook(() => useSetBounds(), { wrapper: Wrapper });
expect(setBounds(coordinates)).toStrictEqual({
extent: [128, 128, 192, 192],
options: { maxZoom: 1, padding: [128, 128, 128, 128], size: undefined },
// size is real size on the screen, so it'll be undefined in the jest
});
expect(getViewSpy).toHaveBeenCalledTimes(ONE);
expect(fitSpy).toHaveBeenCalledWith([128, 128, 192, 192], {
maxZoom: 1,
padding: [128, 128, 128, 128],
size: undefined,
});
});
});
});
import { HALF } from '@/constants/dividers';
import { DEFAULT_TILE_SIZE } from '@/constants/map';
import { mapDataSizeSelector } from '@/redux/map/map.selectors';
import { MapInstance } from '@/types/map';
import { FitOptions } from 'ol/View';
import { Coordinate } from 'ol/coordinate';
import { Extent, boundingExtent } from 'ol/extent';
import { useSelector } from 'react-redux';
import { useMapInstance } from '../context/mapInstanceContext';
export interface SetBoundsResult {
extent: Extent;
options: FitOptions;
}
type SetBounds = (coordinates: Coordinate[]) => SetBoundsResult | undefined;
const BOUNDS_PADDING = DEFAULT_TILE_SIZE / HALF;
const DEFAULT_PADDING = [BOUNDS_PADDING, BOUNDS_PADDING, BOUNDS_PADDING, BOUNDS_PADDING];
/* prettier-ignore */
export const handleSetBounds =
(mapInstance: MapInstance, maxZoom: number, coordinates: Coordinate[]): SetBoundsResult | undefined => {
if (!mapInstance) {
return undefined;
}
const extent = boundingExtent(coordinates);
const options: FitOptions = {
size: mapInstance.getSize(),
padding: DEFAULT_PADDING,
maxZoom,
};
mapInstance.getView().fit(extent, options);
return { extent, options };
};
export const useSetBounds = (): SetBounds => {
const { mapInstance } = useMapInstance();
const { maxZoom } = useSelector(mapDataSizeSelector);
const setBounds = (coordinates: Coordinate[]): SetBoundsResult | undefined =>
handleSetBounds(mapInstance, maxZoom, coordinates);
return setBounds;
};
/* eslint-disable no-magic-numbers */
import { Point } from '@/types/map';
import { isPointValid } from './isPointValid';
describe('isPointValid - util', () => {
const cases = [
[true, 1, 1, undefined], // x, y valid, z undefined
[true, 1, 1, 1], // x, y, z valid
[false, 1, undefined, 1], // y undefined
[false, undefined, 1, 1], // x undefined
[false, undefined, undefined, 1], // x, y undefined
[false, 1, -1, 1], // y negative
[false, -1, 1, 1], // x negative
[false, -1, -1, 1], // x, y negative
[false, -1, -1, -1], // x, y, z negative
];
it.each(cases)('should return %s for point x=%s, y=%s, z=%s', (result, x, y, z) => {
expect(isPointValid({ x, y, z } as Point)).toBe(result);
});
});
import { mapPointSchema } from '@/models/mapPoint';
import { Point } from '@/types/map';
export const isPointValid = (point: Point): boolean => {
const { success } = mapPointSchema.safeParse(point);
return success;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment