Skip to content
Snippets Groups Projects
Commit 0fde397d authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc
Browse files

Merge branch 'overlay-query-params' into 'development'

Overlay query params

See merge request !94
parents 9f047443 27ff5a17
No related branches found
No related tags found
3 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!94Overlay query params,!92feat(overlays): added ability to turn on multiple overlays
Pipeline #84033 passed
Showing
with 370 additions and 24 deletions
import { Fill, Style } from 'ol/style';
import { Fill, Stroke, Style } from 'ol/style';
import { fromExtent } from 'ol/geom/Polygon';
import Feature from 'ol/Feature';
import type Polygon from 'ol/geom/Polygon';
const createFeatureFromExtent = ([xMin, yMin, xMax, yMax]: number[]): Feature<Polygon> =>
new Feature({ geometry: fromExtent([xMin, yMin, xMax, yMax]) });
const getBioEntityOverlayFeatureStyle = (color: string): Style =>
new Style({ fill: new Fill({ color }), stroke: new Stroke({ color: 'black', width: 1 }) });
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 }) }));
const feature = createFeatureFromExtent([xMin, yMin, xMax, yMax]);
feature.setStyle(getBioEntityOverlayFeatureStyle(color));
return feature;
};
......@@ -3,14 +3,18 @@ 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 { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils';
import { ZERO } from '@/constants/common';
import { createOverlayGeometryFeature } from './createOverlayGeometryFeature';
import { getColorByAvailableProperties } from './getColorByAvailableProperties';
import { getPolygonLatitudeCoordinates } from './getPolygonLatitudeCoordinates';
type GetOverlayFeaturesProps = {
bioEntities: OverlayBioEntityRender[];
pointToProjection: UsePointToProjectionResult;
getHex3ColorGradientColorWithAlpha: GetHex3ColorGradientColorWithAlpha;
defaultColor: string;
overlaysOrder: OverlayOrder[];
};
export const getOverlayFeatures = ({
......@@ -18,13 +22,27 @@ export const getOverlayFeatures = ({
pointToProjection,
getHex3ColorGradientColorWithAlpha,
defaultColor,
overlaysOrder,
}: GetOverlayFeaturesProps): Feature<Polygon>[] =>
bioEntities.map(entity =>
createOverlayGeometryFeature(
bioEntities.map(entity => {
/**
* Depending on number of active overlays
* it's required to calculate xMin and xMax coordinates of the polygon
* so "entity" might be devided equali between active overlays
*/
const { xMin, xMax } = getPolygonLatitudeCoordinates({
width: entity.width,
nOverlays: overlaysOrder.length,
xMin: entity.x1,
overlayIndexBasedOnOrder:
overlaysOrder.find(({ id }) => id === entity.overlayId)?.index || ZERO,
});
return createOverlayGeometryFeature(
[
...pointToProjection({ x: entity.x1, y: entity.y1 }),
...pointToProjection({ x: entity.x2, y: entity.y2 }),
...pointToProjection({ x: xMin, y: entity.y1 }),
...pointToProjection({ x: xMax, y: entity.y2 }),
],
getColorByAvailableProperties(entity, getHex3ColorGradientColorWithAlpha, defaultColor),
),
);
);
});
import { getPolygonLatitudeCoordinates } from './getPolygonLatitudeCoordinates';
describe('getPolygonLatitudeCoordinates', () => {
const cases = [
{
width: 80,
nOverlays: 3,
xMin: 2137.5,
overlayIndexBasedOnOrder: 2,
expected: { xMin: 2190.83, xMax: 2217.5 },
},
{
width: 120,
nOverlays: 6,
xMin: 2137.5,
overlayIndexBasedOnOrder: 5,
expected: { xMin: 2237.5, xMax: 2257.5 },
},
{
width: 40,
nOverlays: 1,
xMin: 2137.5,
overlayIndexBasedOnOrder: 0,
expected: { xMin: 2137.5, xMax: 2177.5 },
},
];
it.each(cases)(
'should return the correct latitude coordinates for width=$width, nOverlays=$nOverlays, xMin=$xMin, and overlayIndexBasedOnOrder=$overlayIndexBasedOnOrder',
({ width, nOverlays, xMin, overlayIndexBasedOnOrder, expected }) => {
const result = getPolygonLatitudeCoordinates({
width,
nOverlays,
xMin,
overlayIndexBasedOnOrder,
});
expect(result).toEqual(expected);
},
);
});
import { roundToTwoDigits } from '@/utils/number/roundToTwoDigits';
type GetLatitudeCoordinatesProps = {
width: number;
nOverlays: number;
/** bottom left corner of entity drawn on the map */
xMin: number;
overlayIndexBasedOnOrder: number;
};
type PolygonLatitudeCoordinates = {
xMin: number;
xMax: number;
};
export const getPolygonLatitudeCoordinates = ({
width,
nOverlays,
xMin,
overlayIndexBasedOnOrder,
}: GetLatitudeCoordinatesProps): PolygonLatitudeCoordinates => {
const polygonWidth = width / nOverlays;
const newXMin = xMin + polygonWidth * overlayIndexBasedOnOrder;
const xMax = newXMin + polygonWidth;
return { xMin: roundToTwoDigits(newXMin), xMax: roundToTwoDigits(xMax) };
};
import Geometry from 'ol/geom/Geometry';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Feature } from 'ol';
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 { Feature } from 'ol';
import {
getOverlayOrderSelector,
overlayBioEntitiesForCurrentModelSelector,
} from '@/redux/overlayBioEntity/overlayBioEntity.selector';
import { getOverlayFeatures } from './getOverlayFeatures';
/**
* Prerequisites: "view" button triggers opening overlays -> it triggers downloading overlayBioEntityData for given overlay for ALL available submaps(models)
*
* 1. For each active overlay
* 2. get overlayBioEntity data (current map data passed by selector)
* 3. based on nOverlays, calculate coordinates for given overlayBioEntity to render Polygon from extend
* 4. Calculate coordinates in following steps:
* - polygonWidth = width/nOverlays
* - xMin = xMin + polygonWidth * overlayIndexBasedOnOrder
* - xMax = xMin + polygonWidth
* - yMin,yMax -> is const taken from store
* 5. generate Feature(xMin,yMin,xMax,yMax)
*/
export const useOlMapOverlaysLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => {
const pointToProjection = usePointToProjection();
const { getHex3ColorGradientColorWithAlpha, defaultColorHex } = useTriColorLerp();
const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector);
const overlaysOrder = useAppSelector(getOverlayOrderSelector);
const features = useMemo(
() =>
......@@ -21,8 +39,15 @@ export const useOlMapOverlaysLayer = (): VectorLayer<VectorSource<Feature<Geomet
pointToProjection,
getHex3ColorGradientColorWithAlpha,
defaultColor: defaultColorHex,
overlaysOrder,
}),
[bioEntities, getHex3ColorGradientColorWithAlpha, pointToProjection, defaultColorHex],
[
bioEntities,
getHex3ColorGradientColorWithAlpha,
pointToProjection,
defaultColorHex,
overlaysOrder,
],
);
const vectorSource = useMemo(() => {
......
import { createSelector } from '@reduxjs/toolkit';
import { OverlayBioEntityRender } from '@/types/OLrendering';
import { rootSelector } from '../root/root.selectors';
import { currentModelIdSelector } from '../models/models.selectors';
import { overlaysIdsAndOrderSelector } from '../overlays/overlays.selectors';
import { calculateOvarlaysOrder } from './overlayBioEntity.utils';
export const overlayBioEntitySelector = createSelector(
rootSelector,
......@@ -17,17 +20,36 @@ export const activeOverlaysIdSelector = createSelector(
state => state.overlaysId,
);
const FIRST_ENTITY_INDEX = 0;
// TODO, improve selector when multioverlay algorithm comes in place
export const overlayBioEntitiesForCurrentModelSelector = createSelector(
overlayBioEntityDataSelector,
activeOverlaysIdSelector,
currentModelIdSelector,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(data, currentModelId) => data[Object.keys(data)[FIRST_ENTITY_INDEX]]?.[currentModelId] ?? [], // temporary solution untill multioverlay algorithm comes in place
(data, activeOverlaysIds, currentModelId) => {
const result: OverlayBioEntityRender[] = [];
activeOverlaysIds.forEach(overlayId => {
if (data[overlayId]?.[currentModelId]) {
result.push(...data[overlayId][currentModelId]);
}
});
return result;
},
);
export const isOverlayActiveSelector = createSelector(
[activeOverlaysIdSelector, (_, overlayId: number): number => overlayId],
(overlaysId, overlayId) => overlaysId.includes(overlayId),
);
export const getOverlayOrderSelector = createSelector(
overlaysIdsAndOrderSelector,
activeOverlaysIdSelector,
(overlaysIdsAndOrder, activeOverlaysIds) => {
const activeOverlaysIdsAndOrder = overlaysIdsAndOrder.filter(({ idObject }) =>
activeOverlaysIds.includes(idObject),
);
return calculateOvarlaysOrder(activeOverlaysIdsAndOrder);
},
);
......@@ -9,6 +9,8 @@ import { parseOverlayBioEntityToOlRenderingFormat } from './overlayBioEntity.uti
import { apiPath } from '../apiPath';
import { modelsIdsSelector } from '../models/models.selectors';
import type { RootState } from '../store';
import { setMapBackground } from '../map/map.slice';
import { emptyBackgroundIdSelector } from '../backgrounds/background.selectors';
type GetOverlayBioEntityThunkProps = {
overlayId: number;
......@@ -54,3 +56,19 @@ export const getOverlayBioEntityForAllModels = createAsyncThunk<
await Promise.all(asyncGetOverlayBioEntityFunctions);
},
);
type GetInitOverlaysProps = { overlaysId: number[] };
export const getInitOverlays = createAsyncThunk<void, GetInitOverlaysProps, { state: RootState }>(
'appInit/getInitOverlays',
async ({ overlaysId }, { dispatch, getState }): Promise<void> => {
const state = getState();
const emptyBackgroundId = emptyBackgroundIdSelector(state);
if (emptyBackgroundId) {
dispatch(setMapBackground(emptyBackgroundId));
}
overlaysId.forEach(id => dispatch(getOverlayBioEntityForAllModels({ overlayId: id })));
},
);
import { calculateOvarlaysOrder } from './overlayBioEntity.utils';
describe('calculateOverlaysOrder', () => {
const cases = [
{
data: [
{ idObject: 1, order: 11 },
{ idObject: 2, order: 12 },
{ idObject: 3, order: 13 },
],
expected: [
{ id: 1, order: 11, calculatedOrder: 1, index: 0 },
{ id: 2, order: 12, calculatedOrder: 2, index: 1 },
{ id: 3, order: 13, calculatedOrder: 3, index: 2 },
],
},
// different order
{
data: [
{ idObject: 2, order: 12 },
{ idObject: 3, order: 13 },
{ idObject: 1, order: 11 },
],
expected: [
{ id: 1, order: 11, calculatedOrder: 1, index: 0 },
{ id: 2, order: 12, calculatedOrder: 2, index: 1 },
{ id: 3, order: 13, calculatedOrder: 3, index: 2 },
],
},
{
data: [
{ idObject: 1, order: 11 },
{ idObject: 2, order: 11 },
{ idObject: 3, order: 11 },
],
expected: [
{ id: 1, order: 11, calculatedOrder: 1, index: 0 },
{ id: 2, order: 11, calculatedOrder: 2, index: 1 },
{ id: 3, order: 11, calculatedOrder: 3, index: 2 },
],
},
// different order
{
data: [
{ idObject: 2, order: 11 },
{ idObject: 3, order: 11 },
{ idObject: 1, order: 11 },
],
expected: [
{ id: 1, order: 11, calculatedOrder: 1, index: 0 },
{ id: 2, order: 11, calculatedOrder: 2, index: 1 },
{ id: 3, order: 11, calculatedOrder: 3, index: 2 },
],
},
{
data: [],
expected: [],
},
];
it.each(cases)('should return valid overlays order', ({ data, expected }) => {
expect(calculateOvarlaysOrder(data)).toStrictEqual(expected);
});
});
import { ONE } from '@/constants/common';
import { OverlayBioEntityRender } from '@/types/OLrendering';
import { OverlayBioEntity } from '@/types/models';
......@@ -23,3 +24,45 @@ export const parseOverlayBioEntityToOlRenderingFormat = (
}
return acc;
}, []);
export type OverlayIdAndOrder = {
idObject: number;
order: number;
};
export type OverlayOrder = {
id: number;
order: number;
calculatedOrder: number;
index: number;
};
const byOrderOrId = (a: OverlayOrder, b: OverlayOrder): number => {
if (a.order === b.order) {
return a.id - b.id;
}
return a.order - b.order;
};
/** function calculates order of the function based on "order" property in ovarlay data. */
export const calculateOvarlaysOrder = (
overlaysIdsAndOrder: OverlayIdAndOrder[],
): OverlayOrder[] => {
const overlaysOrder = overlaysIdsAndOrder.map(({ idObject, order }, index) => ({
id: idObject,
order,
calculatedOrder: 0,
index,
}));
overlaysOrder.sort(byOrderOrId);
overlaysOrder.forEach((overlay, index) => {
const updatedOverlay = { ...overlay };
updatedOverlay.calculatedOrder = index + ONE;
updatedOverlay.index = index;
overlaysOrder[index] = updatedOverlay;
});
return overlaysOrder;
};
......@@ -8,6 +8,10 @@ export const overlaysDataSelector = createSelector(
overlays => overlays?.data || [],
);
export const overlaysIdsAndOrderSelector = createSelector(overlaysDataSelector, overlays =>
overlays.map(({ idObject, order }) => ({ idObject, order })),
);
export const loadingAddOverlay = createSelector(
overlaysSelector,
state => state.addOverlay.loading,
......
......@@ -17,6 +17,7 @@ import {
import { getSearchData } from '../search/search.thunks';
import { setPerfectMatch } from '../search/search.slice';
import { getSessionValid } from '../user/user.thunks';
import { getInitOverlays } from '../overlayBioEntity/overlayBioEntity.thunk';
import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks';
import { getStatisticsById } from '../statistics/statistics.thunks';
......@@ -29,7 +30,7 @@ export const fetchInitialAppData = createAsyncThunk<
InitializeAppParams,
{ dispatch: AppDispatch }
>('appInit/fetchInitialAppData', async ({ queryData }, { dispatch }): Promise<void> => {
/** Fetch all data required for renderin map */
/** Fetch all data required for rendering map */
await Promise.all([
dispatch(getConfigurationOptions()),
dispatch(getProjectById(PROJECT_ID)),
......@@ -64,4 +65,9 @@ export const fetchInitialAppData = createAsyncThunk<
);
dispatch(openSearchDrawerWithSelectedTab(getDefaultSearchTab(queryData.searchValue)));
}
/** fetch overlays */
if (queryData.overlaysId) {
dispatch(getInitOverlays({ overlaysId: queryData.overlaysId }));
}
});
import { QueryDataParams } from '@/types/query';
import { createSelector } from '@reduxjs/toolkit';
import { ZERO } from '@/constants/common';
import { mapDataSelector } from '../map/map.selectors';
import { perfectMatchSelector, searchValueSelector } from '../search/search.selectors';
import { activeOverlaysIdSelector } from '../overlayBioEntity/overlayBioEntity.selector';
export const queryDataParamsSelector = createSelector(
searchValueSelector,
perfectMatchSelector,
mapDataSelector,
(searchValue, perfectMatch, { modelId, backgroundId, position }): QueryDataParams => ({
searchValue: searchValue.join(';'),
activeOverlaysIdSelector,
(
searchValue,
perfectMatch,
modelId,
backgroundId,
...position.last,
}),
{ modelId, backgroundId, position },
activeOverlaysId,
): QueryDataParams => {
const queryDataParams: QueryDataParams = {
searchValue: searchValue.join(';'),
perfectMatch,
modelId,
backgroundId,
...position.last,
};
if (activeOverlaysId.length > ZERO) {
queryDataParams.overlaysId = activeOverlaysId.join(',');
}
return queryDataParams;
},
);
......@@ -3,9 +3,13 @@ import { Color } from './models';
export type OverlayBioEntityRender = {
id: number;
modelId: number;
/** bottom left corner of whole element, Xmin */
x1: number;
/** bottom left corner of whole element, Ymin */
y1: number;
/** top righ corner of whole element, xMax */
x2: number;
/** top righ corner of whole element, yMax */
y2: number;
width: number;
height: number;
......
......@@ -6,6 +6,7 @@ export interface QueryData {
modelId?: number;
backgroundId?: number;
initialPosition?: Partial<Point>;
overlaysId?: number[];
}
export interface QueryDataParams {
......@@ -16,6 +17,7 @@ export interface QueryDataParams {
x?: number;
y?: number;
z?: number;
overlaysId?: string;
}
export interface QueryDataRouterParams {
......@@ -26,4 +28,5 @@ export interface QueryDataRouterParams {
x?: string;
y?: string;
z?: string;
overlaysId?: string;
}
/* eslint-disable no-magic-numbers */
import { roundToTwoDigits } from './roundToTwoDigits';
describe('roundToTwoDiggits', () => {
it('should round a positive number with more than two decimal places to two decimal places', () => {
expect(roundToTwoDigits(3.14159265359)).toBe(3.14);
expect(roundToTwoDigits(2.71828182845)).toBe(2.72);
expect(roundToTwoDigits(1.23456789)).toBe(1.23);
});
it('should round a negative number with more than two decimal places to two decimal places', () => {
expect(roundToTwoDigits(-3.14159265359)).toBe(-3.14);
expect(roundToTwoDigits(-2.71828182845)).toBe(-2.72);
expect(roundToTwoDigits(-1.23456789)).toBe(-1.23);
});
it('should round a number with exactly two decimal places to two decimal places', () => {
expect(roundToTwoDigits(3.14)).toBe(3.14);
expect(roundToTwoDigits(2.72)).toBe(2.72);
expect(roundToTwoDigits(1.23)).toBe(1.23);
});
it('should round a number with less than two decimal places to two decimal places', () => {
expect(roundToTwoDigits(3)).toBe(3.0);
expect(roundToTwoDigits(2.7)).toBe(2.7);
expect(roundToTwoDigits(1.2)).toBe(1.2);
});
it('should round zero to two decimal places', () => {
expect(roundToTwoDigits(0)).toBe(0);
});
});
const TWO_DIGITS = 100;
export const roundToTwoDigits = (x: number): number => Math.round(x * TWO_DIGITS) / TWO_DIGITS;
......@@ -10,6 +10,7 @@ describe('parseQueryToTypes', () => {
modelId: undefined,
backgroundId: undefined,
initialPosition: { x: undefined, y: undefined, z: undefined },
overlaysId: undefined,
});
expect(parseQueryToTypes({ perfectMatch: 'true' })).toEqual({
......@@ -18,6 +19,7 @@ describe('parseQueryToTypes', () => {
modelId: undefined,
backgroundId: undefined,
initialPosition: { x: undefined, y: undefined, z: undefined },
overlaysId: undefined,
});
expect(parseQueryToTypes({ perfectMatch: 'false' })).toEqual({
......@@ -26,6 +28,7 @@ describe('parseQueryToTypes', () => {
modelId: undefined,
backgroundId: undefined,
initialPosition: { x: undefined, y: undefined, z: undefined },
overlaysId: undefined,
});
expect(parseQueryToTypes({ modelId: '666' })).toEqual({
......@@ -34,6 +37,7 @@ describe('parseQueryToTypes', () => {
modelId: 666,
backgroundId: undefined,
initialPosition: { x: undefined, y: undefined, z: undefined },
overlaysId: undefined,
});
expect(parseQueryToTypes({ x: '2137' })).toEqual({
searchValue: undefined,
......@@ -41,6 +45,7 @@ describe('parseQueryToTypes', () => {
modelId: undefined,
backgroundId: undefined,
initialPosition: { x: 2137, y: undefined, z: undefined },
overlaysId: undefined,
});
expect(parseQueryToTypes({ y: '1372' })).toEqual({
searchValue: undefined,
......@@ -48,6 +53,7 @@ describe('parseQueryToTypes', () => {
modelId: undefined,
backgroundId: undefined,
initialPosition: { x: undefined, y: 1372, z: undefined },
overlaysId: undefined,
});
expect(parseQueryToTypes({ z: '3721' })).toEqual({
searchValue: undefined,
......@@ -55,6 +61,16 @@ describe('parseQueryToTypes', () => {
modelId: undefined,
backgroundId: undefined,
initialPosition: { x: undefined, y: undefined, z: 3721 },
overlaysId: undefined,
});
expect(parseQueryToTypes({ overlaysId: '1,2,3' })).toEqual({
searchValue: undefined,
perfectMatch: false,
modelId: undefined,
backgroundId: undefined,
initialPosition: { x: undefined, y: undefined, z: undefined },
// eslint-disable-next-line no-magic-numbers
overlaysId: [1, 2, 3],
});
});
});
......@@ -10,4 +10,5 @@ export const parseQueryToTypes = (query: QueryDataRouterParams): QueryData => ({
y: Number(query.y) || undefined,
z: Number(query.z) || undefined,
},
overlaysId: query.overlaysId?.split(',').map(Number),
});
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