diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts index 90887721e354c6af5a68d1368cc74ee74d86181f..9f27f8567185a59c2a6bffb5a94a56ca857d8ec4 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts @@ -1,13 +1,19 @@ -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; }; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayFeatures.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayFeatures.ts index a2a7fb430edee5f165119643802bda01c8477cce..45723ea8732116db8d006f6ad510d4c5f36f5646 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayFeatures.ts @@ -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), - ), - ); + ); + }); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..abb97f156bf3ede2a7ffec1edbff01222478b99f --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates.test.ts @@ -0,0 +1,40 @@ +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); + }, + ); +}); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b3937831210f6b967a8bba559bf2a28ce0b2e10 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates.ts @@ -0,0 +1,26 @@ +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) }; +}; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts index 011dac0370767f058d4ff3744fc52365c9eec692..7f80ae6c0adf4fbda7bddfc10e8417e90d7592ea 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts @@ -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(() => { diff --git a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts index 00803c03fd9160c852813bd5b5195168531e2843..f3dbd72c42bc7913506827d52cfb8aea2f1d7e53 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts @@ -1,6 +1,9 @@ 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); + }, +); diff --git a/src/redux/overlayBioEntity/overlayBioEntity.utils.test.ts b/src/redux/overlayBioEntity/overlayBioEntity.utils.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5714a9a799a8b56f2d6ae54ccac8055261fa613c --- /dev/null +++ b/src/redux/overlayBioEntity/overlayBioEntity.utils.test.ts @@ -0,0 +1,64 @@ +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); + }); +}); diff --git a/src/redux/overlayBioEntity/overlayBioEntity.utils.ts b/src/redux/overlayBioEntity/overlayBioEntity.utils.ts index b875e1bba425bc9f7f9a08617d120cdd8ed4a4c8..ab187513315a37a4c613af29d5e5be40ab7ef72c 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.utils.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.utils.ts @@ -1,3 +1,4 @@ +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; +}; diff --git a/src/redux/overlays/overlays.selectors.ts b/src/redux/overlays/overlays.selectors.ts index f9d36cad0a63c93ae55565e795b897c6bbd4db3e..5e7bb8c07e9fbb76c1b1e4c05ea8a838c9607253 100644 --- a/src/redux/overlays/overlays.selectors.ts +++ b/src/redux/overlays/overlays.selectors.ts @@ -7,3 +7,7 @@ export const overlaysDataSelector = createSelector( overlaysSelector, overlays => overlays?.data || [], ); + +export const overlaysIdsAndOrderSelector = createSelector(overlaysDataSelector, overlays => + overlays.map(({ idObject, order }) => ({ idObject, order })), +); diff --git a/src/types/OLrendering.ts b/src/types/OLrendering.ts index 3a651659743e46e286dda16845ea676fcfa4e314..11ab030c2ca0dd67874f94da6b29309e81f52e1c 100644 --- a/src/types/OLrendering.ts +++ b/src/types/OLrendering.ts @@ -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; diff --git a/src/utils/number/roundToTwoDiggits.test.ts b/src/utils/number/roundToTwoDiggits.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e93ab76934dfdb76430e99ffdbaa5b1fbae2f33c --- /dev/null +++ b/src/utils/number/roundToTwoDiggits.test.ts @@ -0,0 +1,32 @@ +/* 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); + }); +}); diff --git a/src/utils/number/roundToTwoDiggits.ts b/src/utils/number/roundToTwoDiggits.ts new file mode 100644 index 0000000000000000000000000000000000000000..c529f3d5c379f283768894ee9e316c67be0e305d --- /dev/null +++ b/src/utils/number/roundToTwoDiggits.ts @@ -0,0 +1,2 @@ +const TWO_DIGITS = 100; +export const roundToTwoDiggits = (x: number): number => Math.round(x * TWO_DIGITS) / TWO_DIGITS;