Skip to content
Snippets Groups Projects
Commit 4d939b1f authored by mateusz-winiarczyk's avatar mateusz-winiarczyk
Browse files

feat(map): Center map on pin after click on pin icon in search drawer (MIN-216)

parent 8e502011
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...,!131feat(map): Center map on pin after click on pin icon in search drawer (MIN-216)
...@@ -7,6 +7,9 @@ import { ...@@ -7,6 +7,9 @@ import {
import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { StoreType } from '@/redux/store'; import { StoreType } from '@/redux/store';
import { BioEntity } from '@/types/models'; import { BioEntity } from '@/types/models';
import { act } from 'react-dom/test-utils';
import { MAP_INITIAL_STATE } from '@/redux/map/map.constants';
import { DEFAULT_MAX_ZOOM } from '@/constants/map';
import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem.component'; import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem.component';
const BIO_ENTITY = bioEntitiesContentFixture[0].bioEntity; const BIO_ENTITY = bioEntitiesContentFixture[0].bioEntity;
...@@ -84,4 +87,46 @@ describe('BioEntitiesPinsListItem - component ', () => { ...@@ -84,4 +87,46 @@ describe('BioEntitiesPinsListItem - component ', () => {
expect(screen.getByText(secondPinReferenceType, { exact: false })).toBeInTheDocument(); expect(screen.getByText(secondPinReferenceType, { exact: false })).toBeInTheDocument();
expect(screen.getByText(secondPinReferenceResource, { exact: false })).toBeInTheDocument(); expect(screen.getByText(secondPinReferenceResource, { exact: false })).toBeInTheDocument();
}); });
it('should center map to pin coordinates after click on pin icon', async () => {
const { store } = renderComponent(BIO_ENTITY.name, BIO_ENTITY, {
map: {
...MAP_INITIAL_STATE,
data: {
...MAP_INITIAL_STATE.data,
modelId: 5052,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
position: {
initial: {
x: 0,
y: 0,
z: 2,
},
last: {
x: 1,
y: 1,
z: 3,
},
},
},
},
});
const button = screen.getByTestId('center-to-pin-button');
expect(button).toBeInTheDocument();
act(() => {
button.click();
});
expect(store.getState().map.data.position.last).toEqual({
x: BIO_ENTITY.x,
y: BIO_ENTITY.y,
z: DEFAULT_MAX_ZOOM,
});
});
}); });
import { twMerge } from 'tailwind-merge';
import { Icon } from '@/shared/Icon'; import { Icon } from '@/shared/Icon';
import { BioEntity } from '@/types/models'; import { BioEntity } from '@/types/models';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { setMapPosition } from '@/redux/map/map.slice';
import { DEFAULT_MAX_ZOOM } from '@/constants/map';
import { getPinColor } from '../../../ResultsList/PinsList/PinsListItem/PinsListItem.component.utils'; import { getPinColor } from '../../../ResultsList/PinsList/PinsListItem/PinsListItem.component.utils';
interface BioEntitiesPinsListItemProps { interface BioEntitiesPinsListItemProps {
...@@ -12,10 +14,29 @@ export const BioEntitiesPinsListItem = ({ ...@@ -12,10 +14,29 @@ export const BioEntitiesPinsListItem = ({
name, name,
pin, pin,
}: BioEntitiesPinsListItemProps): JSX.Element => { }: BioEntitiesPinsListItemProps): JSX.Element => {
const dispatch = useAppDispatch();
const handleCenterMapToPin = (): void => {
dispatch(
setMapPosition({
x: pin.x,
y: pin.y,
z: DEFAULT_MAX_ZOOM,
}),
);
};
return ( return (
<div className="mb-4 flex w-full flex-col gap-3 rounded-lg border-[1px] border-solid border-greyscale-500 p-4"> <div className="mb-4 flex w-full flex-col gap-3 rounded-lg border-[1px] border-solid border-greyscale-500 p-4">
<div className="flex w-full flex-row items-center gap-2"> <div className="flex w-full flex-row items-center gap-2">
<Icon name="pin" className={twMerge('mr-2 shrink-0', getPinColor('bioEntity'))} /> <button
type="button"
onClick={handleCenterMapToPin}
className="mr-2 shrink-0"
data-testid="center-to-pin-button"
>
<Icon name="pin" className={getPinColor('bioEntity')} />
</button>
<p> <p>
{pin.stringType}: <span className="w-full font-bold">{name}</span> {pin.stringType}: <span className="w-full font-bold">{name}</span>
</p> </p>
......
...@@ -10,8 +10,27 @@ import { ...@@ -10,8 +10,27 @@ import {
} from '@/utils/testing/getReduxWrapperWithStore'; } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
// import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; // import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock';
import { act } from 'react-dom/test-utils';
import { PinTypeWithNone } from '../PinsList.types'; import { PinTypeWithNone } from '../PinsList.types';
import { PinsListItem } from './PinsListItem.component'; import { PinsListItem } from './PinsListItem.component';
import { useVisiblePinsPolygonCoordinates } from './hooks/useVisiblePinsPolygonCoordinates';
const setBounds = jest.fn();
setBounds.mockImplementation(() => {});
jest.mock('../../../../../../../utils/map/useSetBounds', () => ({
_esModule: true,
useSetBounds: (): jest.Mock => setBounds,
}));
const useVisiblePinsPolygonCoordinatesMock = useVisiblePinsPolygonCoordinates as jest.Mock;
jest.mock('./hooks/useVisiblePinsPolygonCoordinates', () => ({
_esModule: true,
useVisiblePinsPolygonCoordinates: jest.fn(),
}));
setBounds.mockImplementation(() => {});
const DRUGS_PIN = { const DRUGS_PIN = {
name: drugsFixture[0].targets[0].name, name: drugsFixture[0].targets[0].name,
...@@ -111,4 +130,37 @@ describe('PinsListItem - component ', () => { ...@@ -111,4 +130,37 @@ describe('PinsListItem - component ', () => {
expect(screen.queryByText('Available in submaps:')).toBeNull(); expect(screen.queryByText('Available in submaps:')).toBeNull();
}); });
it('should not call setBounds if coordinates do not exist', () => {
useVisiblePinsPolygonCoordinatesMock.mockImplementation(() => undefined);
renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs');
const buttonCenterMapToPin = screen.getByTestId('center-to-pin');
expect(buttonCenterMapToPin).toBeInTheDocument();
act(() => {
buttonCenterMapToPin.click();
});
expect(setBounds).not.toHaveBeenCalled();
});
it('should call setBounds if coordinates exist', () => {
useVisiblePinsPolygonCoordinatesMock.mockImplementation(() => [
[292, 333],
[341, 842],
]);
renderComponent(DRUGS_PIN.name, DRUGS_PIN.pin, 'drugs');
const buttonCenterMapToPin = screen.getByTestId('center-to-pin');
expect(buttonCenterMapToPin).toBeInTheDocument();
act(() => {
buttonCenterMapToPin.click();
});
expect(setBounds).toHaveBeenCalled();
});
}); });
import { Icon } from '@/shared/Icon'; import { Icon } from '@/shared/Icon';
import { PinDetailsItem } from '@/types/models'; import { PinDetailsItem } from '@/types/models';
import { twMerge } from 'tailwind-merge';
import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modelsDataSelector } from '@/redux/models/models.selectors'; import { modelsDataSelector } from '@/redux/models/models.selectors';
import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice'; import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice';
import { mapOpenedMapsSelector } from '@/redux/map/map.selectors'; import { mapOpenedMapsSelector } from '@/redux/map/map.selectors';
import { useSetBounds } from '@/utils/map/useSetBounds';
import { getListOfAvailableSubmaps, getPinColor } from './PinsListItem.component.utils'; import { getListOfAvailableSubmaps, getPinColor } from './PinsListItem.component.utils';
import { AvailableSubmaps, PinTypeWithNone } from '../PinsList.types'; import { AvailableSubmaps, PinTypeWithNone } from '../PinsList.types';
import { useVisiblePinsPolygonCoordinates } from './hooks/useVisiblePinsPolygonCoordinates';
interface PinsListItemProps { interface PinsListItemProps {
name: string; name: string;
...@@ -21,6 +22,8 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen ...@@ -21,6 +22,8 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen
const openedMaps = useAppSelector(mapOpenedMapsSelector); const openedMaps = useAppSelector(mapOpenedMapsSelector);
const models = useAppSelector(modelsDataSelector); const models = useAppSelector(modelsDataSelector);
const availableSubmaps = getListOfAvailableSubmaps(pin, models); const availableSubmaps = getListOfAvailableSubmaps(pin, models);
const coordinates = useVisiblePinsPolygonCoordinates(pin.targetElements);
const setBounds = useSetBounds();
const isMapAlreadyOpened = (modelId: number): boolean => const isMapAlreadyOpened = (modelId: number): boolean =>
openedMaps.some(map => map.modelId === modelId); openedMaps.some(map => map.modelId === modelId);
...@@ -33,10 +36,22 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen ...@@ -33,10 +36,22 @@ export const PinsListItem = ({ name, type, pin }: PinsListItemProps): JSX.Elemen
} }
}; };
const handleCenterMapToPin = (): void => {
if (!coordinates) return;
setBounds(coordinates);
};
return ( return (
<div className="mb-4 flex w-full flex-col gap-3 rounded-lg border-[1px] border-solid border-greyscale-500 p-4"> <div className="mb-4 flex w-full flex-col gap-3 rounded-lg border-[1px] border-solid border-greyscale-500 p-4">
<div className="flex w-full flex-row items-center gap-2"> <div className="flex w-full flex-row items-center gap-2">
<Icon name="pin" className={twMerge('mr-2 shrink-0', getPinColor(type))} /> <button
type="button"
className="mr-2 shrink-0"
onClick={handleCenterMapToPin}
data-testid="center-to-pin"
>
<Icon name="pin" className={getPinColor(type)} />
</button>
<p> <p>
Full name: <span className="w-full font-bold">{name}</span> Full name: <span className="w-full font-bold">{name}</span>
</p> </p>
......
/* eslint-disable no-magic-numbers */
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import { MAP_INITIAL_STATE } from '@/redux/map/map.constants';
import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { useVisiblePinsPolygonCoordinates } from './useVisiblePinsPolygonCoordinates';
describe('useVisiblePinsPolygonCoordinates - hook', () => {
it('should return undefined if receives empty array', () => {
const { Wrapper } = getReduxWrapperWithStore({
map: {
...MAP_INITIAL_STATE,
data: {
...MAP_INITIAL_STATE.data,
modelId: 5052,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
},
});
const { result } = renderHook(() => useVisiblePinsPolygonCoordinates([]), {
wrapper: Wrapper,
});
expect(result.current).toBe(undefined);
});
it('should return undefined if received array does not contain bioEntities with current map id', () => {
const { Wrapper } = getReduxWrapperWithStore({
map: {
...MAP_INITIAL_STATE,
data: {
...MAP_INITIAL_STATE.data,
modelId: 5052,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
},
});
const { result } = renderHook(
() =>
useVisiblePinsPolygonCoordinates([
{
...bioEntityContentFixture.bioEntity,
model: 52,
},
{
...bioEntityContentFixture.bioEntity,
model: 51,
},
]),
{
wrapper: Wrapper,
},
);
expect(result.current).toBe(undefined);
});
it('should return coordinates if received array contain bioEntities with current map id', () => {
const { Wrapper } = getReduxWrapperWithStore({
map: {
...MAP_INITIAL_STATE,
data: {
...MAP_INITIAL_STATE.data,
modelId: 5052,
size: {
width: 256,
height: 256,
tileSize: 256,
minZoom: 1,
maxZoom: 1,
},
},
},
});
const { result } = renderHook(
() =>
useVisiblePinsPolygonCoordinates([
{
...bioEntityContentFixture.bioEntity,
model: 5051,
x: 97,
y: 53,
z: 1,
},
{
...bioEntityContentFixture.bioEntity,
model: 5052,
x: 12,
y: 25,
z: 1,
},
{
...bioEntityContentFixture.bioEntity,
model: 5052,
x: 16,
y: 16,
z: 1,
},
]),
{
wrapper: Wrapper,
},
);
expect(result.current).toEqual([
[-18158992, 16123932],
[-17532820, 17532820],
]);
});
});
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { mapModelIdSelector } from '@/redux/map/map.selectors';
import { Point } from '@/types/map';
import { PinDetailsItem } from '@/types/models';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { isPointValid } from '@/utils/point/isPointValid';
import { Coordinate } from 'ol/coordinate';
import { useMemo } from 'react';
const VALID_POLYGON_COORDINATES_LENGTH = 2;
export const useVisiblePinsPolygonCoordinates = (
pinTargetElements: PinDetailsItem['targetElements'],
): Coordinate[] | undefined => {
const pointToProjection = usePointToProjection();
const currentModelId = useAppSelector(mapModelIdSelector);
const currentMapPinElements = useMemo(
() => pinTargetElements.filter(el => el.model === currentModelId),
[currentModelId, pinTargetElements],
);
const polygonPoints = useMemo((): Point[] => {
const allX = currentMapPinElements.map(({ x }) => x);
const allY = currentMapPinElements.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);
}, [currentMapPinElements]);
const polygonCoordinates = useMemo(
() => polygonPoints.map(point => pointToProjection(point)),
[polygonPoints, pointToProjection],
);
if (polygonCoordinates.length !== VALID_POLYGON_COORDINATES_LENGTH) {
return undefined;
}
return polygonCoordinates;
};
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