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

feat(overlays): added ability to turn on multiple overlays

parent e461460d
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 #83882 passed
Showing
with 301 additions and 16 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 { roundToTwoDiggits } from '@/utils/number/roundToTwoDiggits';
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: roundToTwoDiggits(newXMin), xMax: roundToTwoDiggits(xMax) };
};
......@@ -5,13 +5,31 @@ 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 {
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<Geometry>> => {
const pointToProjection = usePointToProjection();
const { getHex3ColorGradientColorWithAlpha, defaultColorHex } = useTriColorLerp();
const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector);
const overlaysOrder = useAppSelector(getOverlayOrderSelector);
const features = useMemo(
() =>
......@@ -20,8 +38,15 @@ export const useOlMapOverlaysLayer = (): VectorLayer<VectorSource<Geometry>> =>
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);
},
);
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,44 @@ export const parseOverlayBioEntityToOlRenderingFormat = (
}
return acc;
}, []);
export type OverlayIdAndOrder = {
idObject: number;
order: number;
};
export type OverlayOrder = {
id: number;
order: number;
calculatedOrder: number;
index: number;
};
/** 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,
}));
/** if two overlays have the same order, order is determined by id of the overlay */
overlaysOrder.sort((a, b) => {
if (a.order === b.order) {
return a.id - b.id;
}
return a.order - b.order;
});
overlaysOrder.forEach((overlay, index) => {
const updatedOverlay = { ...overlay };
updatedOverlay.calculatedOrder = index + ONE;
updatedOverlay.index = index;
overlaysOrder[index] = updatedOverlay;
});
return overlaysOrder;
};
......@@ -7,3 +7,7 @@ export const overlaysDataSelector = createSelector(
overlaysSelector,
overlays => overlays?.data || [],
);
export const overlaysIdsAndOrderSelector = createSelector(overlaysDataSelector, overlays =>
overlays.map(({ idObject, order }) => ({ idObject, order })),
);
......@@ -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;
......
/* eslint-disable no-magic-numbers */
import { roundToTwoDiggits } from './roundToTwoDiggits';
describe('roundToTwoDiggits', () => {
it('should round a positive number with more than two decimal places to two decimal places', () => {
expect(roundToTwoDiggits(3.14159265359)).toBe(3.14);
expect(roundToTwoDiggits(2.71828182845)).toBe(2.72);
expect(roundToTwoDiggits(1.23456789)).toBe(1.23);
});
it('should round a negative number with more than two decimal places to two decimal places', () => {
expect(roundToTwoDiggits(-3.14159265359)).toBe(-3.14);
expect(roundToTwoDiggits(-2.71828182845)).toBe(-2.72);
expect(roundToTwoDiggits(-1.23456789)).toBe(-1.23);
});
it('should round a number with exactly two decimal places to two decimal places', () => {
expect(roundToTwoDiggits(3.14)).toBe(3.14);
expect(roundToTwoDiggits(2.72)).toBe(2.72);
expect(roundToTwoDiggits(1.23)).toBe(1.23);
});
it('should round a number with less than two decimal places to two decimal places', () => {
expect(roundToTwoDiggits(3)).toBe(3.0);
expect(roundToTwoDiggits(2.7)).toBe(2.7);
expect(roundToTwoDiggits(1.2)).toBe(1.2);
});
it('should round zero to two decimal places', () => {
expect(roundToTwoDiggits(0)).toBe(0);
});
});
const TWO_DIGITS = 100;
export const roundToTwoDiggits = (x: number): number => Math.round(x * TWO_DIGITS) / TWO_DIGITS;
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