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

Merge remote-tracking branch 'origin/development' into feature/overview-image-interactive

parents 781de2b8 4c1a0299
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...,!77feat: add overview image interactive layer
Pipeline #83492 passed
Showing
with 496 additions and 10 deletions
......@@ -10,7 +10,11 @@ export const GeneralOverlays = (): JSX.Element => {
<p className="mb-5 text-sm font-semibold">General Overlays:</p>
<ul>
{generalPublicOverlays.map(overlay => (
<OverlayListItem key={overlay.idObject} name={overlay.name} />
<OverlayListItem
key={overlay.idObject}
name={overlay.name}
overlayId={overlay.idObject}
/>
))}
</ul>
</div>
......
import { StoreType } from '@/redux/store';
import { render, screen } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import {
BACKGROUNDS_MOCK,
BACKGROUND_INITIAL_STATE_MOCK,
} from '@/redux/backgrounds/background.mock';
import { initialMapStateFixture } from '@/redux/map/map.fixtures';
import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
import { OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK } from '@/redux/overlayBioEntity/overlayBioEntity.mock';
import { HttpStatusCode } from 'axios';
import { overlayBioEntityFixture } from '@/models/fixtures/overlayBioEntityFixture';
import { apiPath } from '@/redux/apiPath';
import { CORE_PD_MODEL_MOCK } from '@/models/mocks/modelsMock';
import { MODELS_INITIAL_STATE_MOCK } from '@/redux/models/models.mock';
import { parseOverlayBioEntityToOlRenderingFormat } from '@/redux/overlayBioEntity/overlayBioEntity.utils';
import { OverlayListItem } from './OverlayListItem.component';
const mockedAxiosNewClient = mockNetworkNewAPIResponse();
const DEFAULT_BACKGROUND_ID = 0;
const EMPTY_BACKGROUND_ID = 15;
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<OverlayListItem name="Ageing brain" />
<OverlayListItem name="Ageing brain" overlayId={21} />
</Wrapper>,
),
{
......@@ -29,8 +46,31 @@ describe('OverlayListItem - component', () => {
expect(screen.getByRole('button', { name: 'View' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
});
// TODO implement when connecting logic to component
it.skip('should trigger view overlays on view button click', () => {});
it('should trigger view overlays on view button click and switch background to Empty if available', async () => {
const OVERLAY_ID = 21;
const { store } = renderComponent({
map: initialMapStateFixture,
backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK,
models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] },
});
mockedAxiosNewClient
.onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: 5053 }))
.reply(HttpStatusCode.Ok, overlayBioEntityFixture);
expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID);
const ViewButton = screen.getByRole('button', { name: 'View' });
await act(() => {
ViewButton.click();
});
expect(store.getState().map.data.backgroundId).toBe(EMPTY_BACKGROUND_ID);
expect(store.getState().overlayBioEntity.data).toEqual(
parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID),
);
});
// TODO implement when connecting logic to component
it.skip('should trigger download overlay to PC on download button click', () => {});
});
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getOverlayBioEntityForAllModels } from '@/redux/overlayBioEntity/overlayBioEntity.thunk';
import { Button } from '@/shared/Button';
import { useEmptyBackground } from './hooks/useEmptyBackground';
interface OverlayListItemProps {
name: string;
overlayId: number;
}
export const OverlayListItem = ({ name }: OverlayListItemProps): JSX.Element => {
const onViewOverlay = (): void => {};
export const OverlayListItem = ({ name, overlayId }: OverlayListItemProps): JSX.Element => {
const onDownloadOverlay = (): void => {};
const dispatch = useAppDispatch();
const { setBackgroundtoEmptyIfAvailable } = useEmptyBackground();
const onViewOverlay = (): void => {
setBackgroundtoEmptyIfAvailable();
dispatch(getOverlayBioEntityForAllModels({ overlayId }));
};
return (
<li className="flex flex-row flex-nowrap justify-between pl-5 [&:not(:last-of-type)]:mb-4">
......
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { initialMapStateFixture } from '@/redux/map/map.fixtures';
import {
BACKGROUNDS_MOCK,
BACKGROUND_INITIAL_STATE_MOCK,
} from '@/redux/backgrounds/background.mock';
import { renderHook } from '@testing-library/react';
import { useEmptyBackground } from './useEmptyBackground';
const DEFAULT_BACKGROUND_ID = 0;
const EMPTY_BACKGROUND_ID = 15;
describe('useEmptyBackground - hook', () => {
describe('returns setEmptyBackground function', () => {
it('should not set background to "Empty" if its not available', () => {
const { Wrapper, store } = getReduxWrapperWithStore({
map: initialMapStateFixture,
backgrounds: BACKGROUND_INITIAL_STATE_MOCK,
});
const { result } = renderHook(() => useEmptyBackground(), { wrapper: Wrapper });
expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID);
result.current.setBackgroundtoEmptyIfAvailable();
expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID);
});
it('should set background to "Empty" if its available', () => {
const { Wrapper, store } = getReduxWrapperWithStore({
map: initialMapStateFixture,
backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
});
const { result } = renderHook(() => useEmptyBackground(), { wrapper: Wrapper });
expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID);
result.current.setBackgroundtoEmptyIfAvailable();
expect(store.getState().map.data.backgroundId).toBe(EMPTY_BACKGROUND_ID);
});
});
});
import { useCallback } from 'react';
import { emptyBackgroundIdSelector } from '@/redux/backgrounds/background.selectors';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { setMapBackground } from '@/redux/map/map.slice';
type UseEmptyBackgroundReturn = {
setBackgroundtoEmptyIfAvailable: () => void;
};
export const useEmptyBackground = (): UseEmptyBackgroundReturn => {
const dispatch = useAppDispatch();
const emptyBackgroundId = useAppSelector(emptyBackgroundIdSelector);
const setBackgroundtoEmptyIfAvailable = useCallback(() => {
if (emptyBackgroundId) {
dispatch(setMapBackground(emptyBackgroundId));
}
}, [dispatch, emptyBackgroundId]);
return { setBackgroundtoEmptyIfAvailable };
};
import { createOverlayGeometryFeature } from './createOverlayGeometryFeature';
describe('createOverlayGeometryFeature', () => {
it('should create a feature with the correct geometry and style', () => {
const xMin = 0;
const yMin = 0;
const xMax = 10;
const yMax = 10;
const colorHexString = '#FF0000';
const feature = createOverlayGeometryFeature([xMin, yMin, xMax, yMax], colorHexString);
expect(feature.getGeometry()!.getCoordinates()).toEqual([
[
[xMin, yMin],
[xMin, yMax],
[xMax, yMax],
[xMax, yMin],
[xMin, yMin],
],
]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - getStyle() is not typed
expect(feature.getStyle().getFill().getColor()).toEqual(colorHexString);
});
it('should create a feature with the correct geometry and style when using a different color', () => {
const xMin = -5;
const yMin = -5;
const xMax = 5;
const yMax = 5;
const colorHexString = '#00FF00';
const feature = createOverlayGeometryFeature([xMin, yMin, xMax, yMax], colorHexString);
expect(feature.getGeometry()!.getCoordinates()).toEqual([
[
[xMin, yMin],
[xMin, yMax],
[xMax, yMax],
[xMax, yMin],
[xMin, yMin],
],
]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - getStyle() is not typed
expect(feature.getStyle().getFill().getColor()).toEqual(colorHexString);
});
});
import { Fill, Style } from 'ol/style';
import { fromExtent } from 'ol/geom/Polygon';
import Feature from 'ol/Feature';
import type Polygon from 'ol/geom/Polygon';
export const createOverlayGeometryFeature = (
[xMin, yMin, xMax, yMax]: number[],
color: string,
): Feature<Polygon> => {
const feature = new Feature({ geometry: fromExtent([xMin, yMin, xMax, yMax]) });
feature.setStyle(new Style({ fill: new Fill({ color }) }));
return feature;
};
import { OverlayBioEntityRender } from '@/types/OLrendering';
import { getColorByAvailableProperties } from './getColorByAvailableProperties';
describe('getColorByAvailableProperties', () => {
const ENTITY: OverlayBioEntityRender = {
id: 0,
modelId: 0,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
width: 0,
height: 0,
value: null,
overlayId: 0,
color: null,
};
const getHexTricolorGradientColorWithAlpha = jest.fn().mockReturnValue('#FFFFFF');
const defaultColor = '#000000';
beforeEach(() => {
jest.clearAllMocks();
});
it('should return the result of getHexTricolorGradientColorWithAlpha if entity has a value equal to 0', () => {
const entity = { ...ENTITY, value: 0 };
const result = getColorByAvailableProperties(
entity,
getHexTricolorGradientColorWithAlpha,
defaultColor,
);
expect(result).toEqual('#FFFFFF');
expect(getHexTricolorGradientColorWithAlpha).toHaveBeenCalledWith(entity.value);
});
it('should return the result of getHexTricolorGradientColorWithAlpha if entity has a value', () => {
const entity = { ...ENTITY, value: -0.2137 };
const result = getColorByAvailableProperties(
entity,
getHexTricolorGradientColorWithAlpha,
defaultColor,
);
expect(result).toEqual('#FFFFFF');
expect(getHexTricolorGradientColorWithAlpha).toHaveBeenCalledWith(entity.value);
});
it('should return the result of convertDecimalToHex if entity has a color', () => {
const entity = { ...ENTITY, color: { rgb: -65536, alpha: 0 } }; // red color
const result = getColorByAvailableProperties(
entity,
getHexTricolorGradientColorWithAlpha,
defaultColor,
);
expect(result).toEqual('#ff0000');
expect(getHexTricolorGradientColorWithAlpha).not.toHaveBeenCalled();
});
it('should return the default color if entity has neither a value nor a color', () => {
const result = getColorByAvailableProperties(
ENTITY,
getHexTricolorGradientColorWithAlpha,
defaultColor,
);
expect(result).toEqual('#000000');
expect(getHexTricolorGradientColorWithAlpha).not.toHaveBeenCalled();
});
});
import { ZERO } from '@/constants/common';
import type { GetHex3ColorGradientColorWithAlpha } from '@/hooks/useTriColorLerp';
import { OverlayBioEntityRender } from '@/types/OLrendering';
import { convertDecimalToHexColor } from '@/utils/convert/convertDecimalToHex';
export const getColorByAvailableProperties = (
entity: OverlayBioEntityRender,
getHexTricolorGradientColorWithAlpha: GetHex3ColorGradientColorWithAlpha,
defaultColor: string,
): string => {
if (typeof entity.value === 'number') {
return getHexTricolorGradientColorWithAlpha(entity.value || ZERO);
}
if (entity.color) {
return convertDecimalToHexColor(entity.color.rgb);
}
return defaultColor;
};
import type { GetHex3ColorGradientColorWithAlpha } from '@/hooks/useTriColorLerp';
import { OverlayBioEntityRender } from '@/types/OLrendering';
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import type Feature from 'ol/Feature';
import type Polygon from 'ol/geom/Polygon';
import { createOverlayGeometryFeature } from './createOverlayGeometryFeature';
import { getColorByAvailableProperties } from './getColorByAvailableProperties';
type GetOverlayFeaturesProps = {
bioEntities: OverlayBioEntityRender[];
pointToProjection: UsePointToProjectionResult;
getHex3ColorGradientColorWithAlpha: GetHex3ColorGradientColorWithAlpha;
defaultColor: string;
};
export const getOverlayFeatures = ({
bioEntities,
pointToProjection,
getHex3ColorGradientColorWithAlpha,
defaultColor,
}: GetOverlayFeaturesProps): Feature<Polygon>[] =>
bioEntities.map(entity =>
createOverlayGeometryFeature(
[
...pointToProjection({ x: entity.x1, y: entity.y1 }),
...pointToProjection({ x: entity.x2, y: entity.y2 }),
],
getColorByAvailableProperties(entity, getHex3ColorGradientColorWithAlpha, defaultColor),
),
);
import Geometry from 'ol/geom/Geometry';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { useMemo } from 'react';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { useTriColorLerp } from '@/hooks/useTriColorLerp';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { overlayBioEntitiesForCurrentModelSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector';
import { getOverlayFeatures } from './getOverlayFeatures';
export const useOlMapOverlaysLayer = (): VectorLayer<VectorSource<Geometry>> => {
const pointToProjection = usePointToProjection();
const { getHex3ColorGradientColorWithAlpha, defaultColorHex } = useTriColorLerp();
const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector);
const features = useMemo(
() =>
getOverlayFeatures({
bioEntities,
pointToProjection,
getHex3ColorGradientColorWithAlpha,
defaultColor: defaultColorHex,
}),
[bioEntities, getHex3ColorGradientColorWithAlpha, pointToProjection, defaultColorHex],
);
const vectorSource = useMemo(() => {
return new VectorSource({
features,
});
}, [features]);
const overlaysLayer = useMemo(
() =>
new VectorLayer({
source: vectorSource,
}),
[vectorSource],
);
return overlaysLayer;
};
......@@ -4,6 +4,7 @@ import { MapConfig, MapInstance } from '../../MapViewer.types';
import { useOlMapPinsLayer } from './pinsLayer/useOlMapPinsLayer';
import { useOlMapReactionsLayer } from './reactionsLayer/useOlMapReactionsLayer';
import { useOlMapTileLayer } from './useOlMapTileLayer';
import { useOlMapOverlaysLayer } from './overlaysLayer/useOlMapOverlaysLayer';
interface UseOlMapLayersInput {
mapInstance: MapInstance;
......@@ -13,14 +14,15 @@ export const useOlMapLayers = ({ mapInstance }: UseOlMapLayersInput): MapConfig[
const tileLayer = useOlMapTileLayer();
const pinsLayer = useOlMapPinsLayer();
const reactionsLayer = useOlMapReactionsLayer();
const overlaysLayer = useOlMapOverlaysLayer();
useEffect(() => {
if (!mapInstance) {
return;
}
mapInstance.setLayers([tileLayer, reactionsLayer, pinsLayer]);
}, [reactionsLayer, tileLayer, pinsLayer, mapInstance]);
mapInstance.setLayers([tileLayer, reactionsLayer, pinsLayer, overlaysLayer]);
}, [reactionsLayer, tileLayer, pinsLayer, mapInstance, overlaysLayer]);
return [tileLayer, pinsLayer, reactionsLayer];
return [tileLayer, pinsLayer, reactionsLayer, overlaysLayer];
};
export const EMPTY_BACKGROUND_NAME = 'Empty';
export const WHITE_HEX_OPACITY_0 = '#00000000';
import { useCallback } from 'react';
import { WHITE_HEX_OPACITY_0 } from '@/constants/hexColors';
import {
maxColorValSelector,
minColorValSelector,
neutralColorValSelector,
overlayOpacitySelector,
simpleColorValSelector,
} from '@/redux/configuration/configuration.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { getHexTricolorGradientColorWithAlpha } from '@/utils/convert/getHexTricolorGradientColorWithAlpha';
import { ONE } from '@/constants/common';
import { addAlphaToHexString } from '../utils/convert/addAlphaToHexString';
export type GetHex3ColorGradientColorWithAlpha = (position: number) => string;
type UseTriColorLerpReturn = {
getHex3ColorGradientColorWithAlpha: GetHex3ColorGradientColorWithAlpha;
defaultColorHex: string;
};
export const useTriColorLerp = (): UseTriColorLerpReturn => {
const minColorValHexString = useAppSelector(minColorValSelector) || '';
const maxColorValHexString = useAppSelector(maxColorValSelector) || '';
const neutralColorValHexString = useAppSelector(neutralColorValSelector) || '';
const overlayOpacityValue = useAppSelector(overlayOpacitySelector) || ONE;
const simpleColorValue = useAppSelector(simpleColorValSelector) || WHITE_HEX_OPACITY_0;
const getHex3ColorGradientColorWithAlpha = useCallback(
(position: number) =>
getHexTricolorGradientColorWithAlpha({
leftColor: minColorValHexString,
middleColor: neutralColorValHexString,
rightColor: maxColorValHexString,
position,
alpha: Number(overlayOpacityValue),
}),
[minColorValHexString, neutralColorValHexString, maxColorValHexString, overlayOpacityValue],
);
const defaultColorHex = addAlphaToHexString(simpleColorValue, Number(overlayOpacityValue));
return { getHex3ColorGradientColorWithAlpha, defaultColorHex };
};
import { z } from 'zod';
export const configurationOptionSchema = z.object({
idObject: z.number(),
type: z.string(),
valueType: z.string(),
commonName: z.string(),
isServerSide: z.boolean(),
value: z.string().optional(),
group: z.string(),
});
import { ZOD_SEED } from '@/constants';
import { z } from 'zod';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createFixture } from 'zod-fixture';
import { overlayBioEntitySchema } from '../overlayBioEntitySchema';
export const overlayBioEntityFixture = createFixture(z.array(overlayBioEntitySchema), {
seed: ZOD_SEED,
array: { min: 3, max: 3 },
});
import { ConfigurationOption } from '@/types/models';
export const CONFIGURATION_OPTIONS_TYPES_MOCK: string[] = [
'MIN_COLOR_VAL',
'MAX_COLOR_VAL',
'SIMPLE_COLOR_VAL',
'NEUTRAL_COLOR_VAL',
];
export const CONFIGURATION_OPTIONS_COLOURS_MOCK: ConfigurationOption[] = [
{
idObject: 29,
type: 'MIN_COLOR_VAL',
valueType: 'COLOR',
commonName: 'Overlay color for negative values',
isServerSide: false,
value: 'FF0000',
group: 'Overlays',
},
{
idObject: 30,
type: 'MAX_COLOR_VAL',
valueType: 'COLOR',
commonName: 'Overlay color for postive values',
isServerSide: false,
value: '0000FF',
group: 'Overlays',
},
{
idObject: 31,
type: 'SIMPLE_COLOR_VAL',
valueType: 'COLOR',
commonName: 'Overlay color when no values are defined',
isServerSide: false,
value: '00FF00',
group: 'Overlays',
},
{
idObject: 32,
type: 'NEUTRAL_COLOR_VAL',
valueType: 'COLOR',
commonName: 'Overlay color for value=0',
isServerSide: false,
value: 'FFFFFF',
group: 'Overlays',
},
];
......@@ -457,3 +457,21 @@ export const MODELS_MOCK_SHORT: MapModel[] = [
maxZoom: 5,
},
];
export const CORE_PD_MODEL_MOCK: MapModel = {
idObject: 5053,
width: 26779.25,
height: 13503.0,
defaultCenterX: null,
defaultCenterY: null,
description: '',
name: 'Core PD map',
defaultZoomLevel: null,
tileSize: 256,
references: [],
authors: [],
creationDate: null,
modificationDates: [],
minZoom: 2,
maxZoom: 9,
};
import { z } from 'zod';
import { overlayLeftBioEntitySchema } from './overlayLeftBioEntitySchema';
import { overlayRightBioEntitySchema } from './overlayRightBioEntitySchema';
export const overlayBioEntitySchema = z.object({
left: overlayLeftBioEntitySchema,
right: overlayRightBioEntitySchema,
});
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