diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts index a896f4be019afda0cdea7b9ff858bae65e0e6045..e7ccb7c9fa70b4bc304697f8d6d64a5beca9556b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts @@ -1,5 +1,6 @@ import View from 'ol/View'; import BaseLayer from 'ol/layer/Base'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; export type MapConfig = { view: View; @@ -8,3 +9,7 @@ export type MapConfig = { export type VerticalAlign = 'TOP' | 'MIDDLE' | 'BOTTOM'; export type HorizontalAlign = 'LEFT' | 'RIGHT' | 'CENTER' | 'END' | 'START'; + +export type OverlayBioEntityGroupedElementsType = { + [id: string]: Array<OverlayBioEntityRender & { amount: number }>; +}; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts new file mode 100644 index 0000000000000000000000000000000000000000..8aba562e8bc3001d6e6bf2db476e920332eeb0f8 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -0,0 +1,120 @@ +import { ModelElement, ModelElements } from '@/types/models'; +import MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement'; +import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; +import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; +import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; +import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; +import { + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; +import { MapInstance } from '@/types/map'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { BioShapesDict, LineTypeDict } from '@/redux/shapes/shapes.types'; +import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; + +export default function processModelElements( + modelElements: ModelElements, + shapes: BioShapesDict, + lineTypes: LineTypeDict, + groupedElementsOverlays: Record<string, Array<OverlayBioEntityRender>>, + overlaysOrder: Array<OverlayOrder>, + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties, + mapInstance: MapInstance, + pointToProjection: UsePointToProjectionResult, +): Array<MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph> { + const validElements: Array< + MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph + > = []; + modelElements.content.forEach((element: ModelElement) => { + if (element.glyph) { + const glyph = new Glyph({ + id: element.glyph.id, + x: element.x, + y: element.y, + width: element.width, + height: element.height, + zIndex: element.z, + pointToProjection, + mapInstance, + }); + validElements.push(glyph); + return; + } + + if (element.sboTerm === 'SBO:0000290') { + const compartmentProps = { + id: element.id, + x: element.x, + y: element.y, + nameX: element.nameX, + nameY: element.nameY, + nameHeight: element.nameHeight, + nameWidth: element.nameWidth, + width: element.width, + height: element.height, + zIndex: element.z, + innerWidth: element.innerWidth, + outerWidth: element.outerWidth, + thickness: element.thickness, + fontColor: element.fontColor, + fillColor: element.fillColor, + borderColor: element.borderColor, + nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, + nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, + text: element.name, + fontSize: element.fontSize, + pointToProjection, + mapInstance, + }; + if (element.shape === 'OVAL_COMPARTMENT') { + validElements.push(new CompartmentCircle(compartmentProps)); + } else if (element.shape === 'SQUARE_COMPARTMENT') { + validElements.push(new CompartmentSquare(compartmentProps)); + } else if (element.shape === 'PATHWAY') { + validElements.push(new CompartmentPathway(compartmentProps)); + } + return; + } + const elementShapes = shapes[element.sboTerm]; + if (elementShapes) { + validElements.push( + new MapElement({ + id: element.id, + shapes: elementShapes, + x: element.x, + y: element.y, + nameX: element.nameX, + nameY: element.nameY, + nameHeight: element.nameHeight, + nameWidth: element.nameWidth, + width: element.width, + height: element.height, + zIndex: element.z, + lineWidth: element.lineWidth, + lineType: element.borderLineType, + fontColor: element.fontColor, + fillColor: element.fillColor, + borderColor: element.borderColor, + nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, + nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, + homodimer: element.homodimer, + activity: element.activity, + text: element.name, + fontSize: element.fontSize, + pointToProjection, + mapInstance, + modifications: element.modificationResidues, + lineTypes, + bioShapes: shapes, + overlays: groupedElementsOverlays[element.id], + overlaysOrder, + getOverlayColor, + }), + ); + } + }); + return validElements; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index 00ff8cb89f65c58492a94d0f9388455d49a8123f..87f31fcf34f378176ec4fc91d070135875204e5f 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -12,23 +12,32 @@ import { lineTypesSelector, } from '@/redux/shapes/shapes.selectors'; import { MapInstance } from '@/types/map'; -import { - HorizontalAlign, - VerticalAlign, -} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; import { modelElementsSelector } from '@/redux/modelElements/modelElements.selector'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; -import { ModelElement } from '@/types/models'; import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction'; import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors'; import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import { + getOverlayOrderSelector, + overlayBioEntitiesForCurrentModelSelector, +} from '@/redux/overlayBioEntity/overlayBioEntity.selector'; +import { groupBy } from '@/utils/array/groupBy'; +import { useGetOverlayColor } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import getOverlays from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays'; +import LineOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay'; +import { markersSufraceOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors'; +import { parseSurfaceMarkersToBioEntityRender } from '@/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender'; +import MarkerOverlay from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay'; +import processModelElements from '@/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements'; +import useDebouncedValue from '@/utils/useDebouncedValue'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -36,18 +45,70 @@ export const useOlMapReactionsLayer = ({ mapInstance: MapInstance; }): VectorLayer<VectorSource<Feature>> => { const dispatch = useAppDispatch(); + + const currentModelId = useSelector(currentModelIdSelector); const modelElements = useSelector(modelElementsSelector); const modelReactions = useSelector(newReactionsDataSelector); - const currentModelId = useSelector(currentModelIdSelector); + const shapes = useSelector(bioShapesSelector); + const lineTypes = useSelector(lineTypesSelector); + const arrowTypes = useSelector(arrowTypesSelector); + const overlaysOrder = useSelector(getOverlayOrderSelector); + const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); + const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); + const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); + const debouncedBioEntities = useDebouncedValue(bioEntities, 2000); + const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); + + const pointToProjection = usePointToProjection(); + useEffect(() => { dispatch(getModelElements(currentModelId)); dispatch(getNewReactions(currentModelId)); }, [currentModelId, dispatch]); - const pointToProjection = usePointToProjection(); - const shapes = useSelector(bioShapesSelector); - const lineTypes = useSelector(lineTypesSelector); - const arrowTypes = useSelector(arrowTypesSelector); + const groupedElementsOverlays = useMemo(() => { + const elementsBioEntitesOverlay = debouncedBioEntities.filter( + bioEntity => bioEntity.type !== 'line', + ); + const grouped = groupBy(elementsBioEntitesOverlay, bioEntity => bioEntity.id.toString()); + return getOverlays(grouped, getOverlayBioEntityColorByAvailableProperties); + }, [debouncedBioEntities, getOverlayBioEntityColorByAvailableProperties]); + + const linesOverlays = useMemo(() => { + return bioEntities.filter(bioEntity => bioEntity.type === 'line'); + }, [bioEntities]); + + const linesOverlaysFeatures = useMemo(() => { + return linesOverlays.map(lineOverlay => { + return new LineOverlay({ + lineOverlay, + getOverlayColor: getOverlayBioEntityColorByAvailableProperties, + pointToProjection, + mapInstance, + }).lineFeature; + }); + }, [ + getOverlayBioEntityColorByAvailableProperties, + linesOverlays, + mapInstance, + pointToProjection, + ]); + + const markerOverlaysFeatures = useMemo(() => { + return markersRender.map(marker => { + return new MarkerOverlay({ + markerOverlay: marker, + getOverlayColor: getOverlayBioEntityColorByAvailableProperties, + pointToProjection, + mapInstance, + }).markerFeature; + }); + }, [ + getOverlayBioEntityColorByAvailableProperties, + mapInstance, + markersRender, + pointToProjection, + ]); const reactions = useMemo(() => { return modelReactions.map(reaction => { @@ -79,109 +140,44 @@ export const useOlMapReactionsLayer = ({ if (!modelElements || !shapes) { return []; } - - const validElements: Array< - MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph - > = []; - modelElements.content.forEach((element: ModelElement) => { - if (element.glyph) { - const glyph = new Glyph({ - id: element.glyph.id, - x: element.x, - y: element.y, - width: element.width, - height: element.height, - zIndex: element.z, - pointToProjection, - mapInstance, - }); - validElements.push(glyph); - return; - } - - if (element.sboTerm === 'SBO:0000290') { - const compartmentProps = { - id: element.id, - x: element.x, - y: element.y, - nameX: element.nameX, - nameY: element.nameY, - nameHeight: element.nameHeight, - nameWidth: element.nameWidth, - width: element.width, - height: element.height, - zIndex: element.z, - innerWidth: element.innerWidth, - outerWidth: element.outerWidth, - thickness: element.thickness, - fontColor: element.fontColor, - fillColor: element.fillColor, - borderColor: element.borderColor, - nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, - nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, - text: element.name, - fontSize: element.fontSize, - pointToProjection, - mapInstance, - }; - if (element.shape === 'OVAL_COMPARTMENT') { - validElements.push(new CompartmentCircle(compartmentProps)); - } else if (element.shape === 'SQUARE_COMPARTMENT') { - validElements.push(new CompartmentSquare(compartmentProps)); - } else if (element.shape === 'PATHWAY') { - validElements.push(new CompartmentPathway(compartmentProps)); - } - return; - } - const elementShapes = shapes[element.sboTerm]; - if (elementShapes) { - validElements.push( - new MapElement({ - id: element.id, - shapes: elementShapes, - x: element.x, - y: element.y, - nameX: element.nameX, - nameY: element.nameY, - nameHeight: element.nameHeight, - nameWidth: element.nameWidth, - width: element.width, - height: element.height, - zIndex: element.z, - lineWidth: element.lineWidth, - lineType: element.borderLineType, - fontColor: element.fontColor, - fillColor: element.fillColor, - borderColor: element.borderColor, - nameVerticalAlign: element.nameVerticalAlign as VerticalAlign, - nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign, - homodimer: element.homodimer, - activity: element.activity, - text: element.name, - fontSize: element.fontSize, - pointToProjection, - mapInstance, - modifications: element.modificationResidues, - lineTypes, - bioShapes: shapes, - }), - ); - } - }); - return validElements; - }, [modelElements, shapes, pointToProjection, mapInstance, lineTypes]); + return processModelElements( + modelElements, + shapes, + lineTypes, + groupedElementsOverlays, + overlaysOrder, + getOverlayBioEntityColorByAvailableProperties, + mapInstance, + pointToProjection, + ); + }, [ + modelElements, + shapes, + pointToProjection, + mapInstance, + lineTypes, + groupedElementsOverlays, + overlaysOrder, + getOverlayBioEntityColorByAvailableProperties, + ]); const features = useMemo(() => { const reactionsFeatures = reactions.flat(); const elementsFeatures = elements.map(element => element.feature); - return [...reactionsFeatures, ...elementsFeatures]; - }, [elements, reactions]); + return [ + ...reactionsFeatures, + ...elementsFeatures, + ...linesOverlaysFeatures, + ...markerOverlaysFeatures, + ]; + }, [elements, linesOverlaysFeatures, markerOverlaysFeatures, reactions]); - const vectorSource = useMemo(() => { - return new VectorSource({ - features, - }); - }, [features]); + const vectorSource = useMemo(() => new VectorSource(), []); + + useEffect(() => { + vectorSource.clear(); + vectorSource.addFeatures(features); + }, [features, vectorSource]); return useMemo(() => { const vectorLayer = new VectorLayer({ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts index 7ebb6e709f662cef89e30112e052bad29c871324..cebab4e092508f7d15bbeaf8a649166db7dfbe02 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts @@ -2,8 +2,9 @@ import { MapInstance } from '@/types/map'; import { useOlMapWhiteCardLayer } from '@/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapWhiteCardLayer'; import { useOlMapAdditionalLayers } from '@/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers'; -import { MapConfig } from '../../MapViewerVector.types'; +import { useMemo } from 'react'; import { useOlMapReactionsLayer } from './reactionsLayer/useOlMapReactionsLayer'; +import { MapConfig } from '../../MapViewerVector.types'; interface UseOlMapLayersInput { mapInstance: MapInstance; @@ -14,5 +15,7 @@ export const useOlMapVectorLayers = ({ mapInstance }: UseOlMapLayersInput): MapC const whiteCardLayer = useOlMapWhiteCardLayer(); const additionalLayers = useOlMapAdditionalLayers(mapInstance); - return [whiteCardLayer, reactionsLayer, ...additionalLayers]; + return useMemo(() => { + return [whiteCardLayer, reactionsLayer, ...additionalLayers]; + }, [whiteCardLayer, reactionsLayer, additionalLayers]); }; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts index 64c1f92d23ef03b84c8c1a39f226655f4fa2f189..71c5c3c2bb31a688274e51b808af8bea595f9d44 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts @@ -60,6 +60,7 @@ describe('MapElement', () => { nameHorizontalAlign: 'CENTER', pointToProjection: jest.fn(), mapInstance, + getOverlayColor: (): string => '#ffffff', }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index dae35c65e06fb8b226ca18a2946961ad95f1465e..94d5f96f4fe3c23d0529a14cf983b3514ba20b0e 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -22,6 +22,11 @@ import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shape import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import { BioShapesDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { FEATURE_TYPE } from '@/constants/features'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { getPolygonLatitudeCoordinates } from '@/components/Map/MapViewer/utils/config/overlaysLayer/getPolygonLatitudeCoordinates'; +import { ZERO } from '@/constants/common'; +import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; export type MapElementProps = { id: number; @@ -51,6 +56,9 @@ export type MapElementProps = { bioShapes?: BioShapesDict; lineTypes?: LineTypeDict; modifications?: Array<Modification>; + overlays?: Array<OverlayBioEntityRender>; + overlaysOrder?: Array<OverlayOrder>; + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; }; export default class MapElement extends BaseMultiPolygon { @@ -72,6 +80,12 @@ export default class MapElement extends BaseMultiPolygon { lineDash: Array<number> = []; + overlays: Array<OverlayBioEntityRender> = []; + + overlaysOrder: Array<OverlayOrder> = []; + + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + constructor({ id, shapes, @@ -100,6 +114,9 @@ export default class MapElement extends BaseMultiPolygon { bioShapes = {}, lineTypes = {}, modifications = [], + overlays = [], + overlaysOrder = [], + getOverlayColor, }: MapElementProps) { super({ type: FEATURE_TYPE.ALIAS, @@ -130,6 +147,9 @@ export default class MapElement extends BaseMultiPolygon { this.bioShapes = bioShapes; this.lineTypes = lineTypes; this.modifications = modifications; + this.overlays = overlays; + this.overlaysOrder = overlaysOrder; + this.getOverlayColor = getOverlayColor; this.createPolygons(); this.drawText(); this.drawMultiPolygonFeature(mapInstance); @@ -160,6 +180,7 @@ export default class MapElement extends BaseMultiPolygon { } this.drawElementPolygon(homodimerShift, homodimerOffset); } + this.drawOverlays(); } drawModification(modification: Modification, shapes: Array<Shape>): void { @@ -250,7 +271,7 @@ export default class MapElement extends BaseMultiPolygon { const elementStyle = getStyle({ geometry: elementPolygon, borderColor: this.borderColor, - fillColor: this.fillColor, + fillColor: this.overlays.length ? undefined : this.fillColor, lineWidth: this.lineWidth, lineDash: this.lineDash, zIndex: this.zIndex, @@ -260,4 +281,37 @@ export default class MapElement extends BaseMultiPolygon { this.styles.push(elementStyle); }); } + + drawOverlays(): void { + this.overlays.forEach(entity => { + if (entity.value === Infinity) { + return; + } + const { xMin, xMax } = getPolygonLatitudeCoordinates({ + width: entity.width, + nOverlays: this.overlaysOrder.length, + xMin: entity.x1, + overlayIndexBasedOnOrder: + this.overlaysOrder.find(({ id }) => id === entity.overlayId)?.index || ZERO, + }); + const color = this.getOverlayColor(entity); + const polygon = new Polygon([ + [ + this.pointToProjection({ x: xMin, y: entity.y1 }), + this.pointToProjection({ x: xMax, y: entity.y1 }), + this.pointToProjection({ x: xMax, y: entity.y2 }), + this.pointToProjection({ x: xMin, y: entity.y2 }), + ], + ]); + const style = getStyle({ + geometry: polygon, + borderColor: color, + fillColor: color, + zIndex: this.zIndex, + }); + this.polygons.push(polygon); + this.lineWidths.push(1); + this.styles.push(style); + }); + } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4efbc9d6e3f4b4c78f4689ca0ecb77ef8e230ba4 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.test.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-magic-numbers */ +import { Feature, Map } from 'ol'; +import { Stroke, Style } from 'ol/style'; +import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; +import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import View from 'ol/View'; +import LineOverlay, { + LineOverlayProps, +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay'; +import { Coordinate } from 'ol/coordinate'; + +jest.mock('../style/getStroke'); +jest.mock('../style/getFill'); +jest.mock('../style/rgbToHex'); + +describe('LineOverlay', () => { + let props: LineOverlayProps; + + beforeEach(() => { + const dummyElement = document.createElement('div'); + const mapInstance = new Map({ + target: dummyElement, + view: new View({ + zoom: 5, + minZoom: 3, + maxZoom: 7, + }), + }); + props = { + lineOverlay: { + color: null, + height: 2, + id: '3', + modelId: 1, + overlayId: 0, + type: 'line', + value: 0.43, + width: 2, + x1: 1, + x2: 3, + y1: 3, + y2: 1, + }, + getOverlayColor: (): string => '#AABB11', + pointToProjection: ({ x, y }: { x: number; y: number }): Coordinate => [x, y], + mapInstance, + }; + + (getStroke as jest.Mock).mockReturnValue(new Stroke()); + (getFill as jest.Mock).mockReturnValue(new Style()); + (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); + }); + + it('should initialize line overlay feature', () => { + const lineOverlay = new LineOverlay(props); + + expect(lineOverlay.lineFeature).toBeInstanceOf(Feature); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6d8eecb4099b69fe38221288af470ec2d76554c --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/LineOverlay.ts @@ -0,0 +1,75 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { LineString } from 'ol/geom'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import Style from 'ol/style/Style'; +import { MapInstance } from '@/types/map'; + +export type LineOverlayProps = { + lineOverlay: OverlayBioEntityRender; + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; +}; + +export default class LineOverlay { + lineOverlay: OverlayBioEntityRender; + + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + + pointToProjection: UsePointToProjectionResult; + + mapInstance: MapInstance; + + lineFeature: Feature; + + constructor({ lineOverlay, getOverlayColor, pointToProjection, mapInstance }: LineOverlayProps) { + this.lineOverlay = lineOverlay; + this.getOverlayColor = getOverlayColor; + this.pointToProjection = pointToProjection; + this.mapInstance = mapInstance; + this.lineFeature = this.drawOverlay(); + } + + drawOverlay(): Feature { + const points = [ + this.pointToProjection({ x: this.lineOverlay.x1, y: this.lineOverlay.y1 }), + this.pointToProjection({ x: this.lineOverlay.x2, y: this.lineOverlay.y2 }), + ]; + const color = this.getOverlayColor(this.lineOverlay); + const lineString = new LineString(points); + const lineStyle = getStyle({ + geometry: lineString, + borderColor: color, + lineWidth: 6, + zIndex: 99999, + }); + const lineFeature = new Feature<LineString>({ + geometry: lineString, + style: lineStyle, + lineWidth: 6, + }); + lineFeature.setStyle(this.getStyle.bind(this)); + return lineFeature; + } + + protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); + const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); + const style = feature.get('style'); + if (!minResolution || !style) { + return []; + } + + const scale = minResolution / resolution; + const lineWidth = feature.get('lineWidth') * scale; + + if (style instanceof Style && style.getStroke()) { + style.getStroke()?.setWidth(lineWidth); + } + return style; + } +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fe3c4aad5120a9cd322f84b943b40c039195543 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.test.ts @@ -0,0 +1,61 @@ +/* eslint-disable no-magic-numbers */ +import { Feature, Map } from 'ol'; +import { Stroke, Style } from 'ol/style'; +import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; +import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import View from 'ol/View'; +import { Coordinate } from 'ol/coordinate'; +import MarkerOverlay, { + MarkerOverlayProps, +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay'; + +jest.mock('../style/getStroke'); +jest.mock('../style/getFill'); +jest.mock('../style/rgbToHex'); + +describe('MarkerOverlay', () => { + let props: MarkerOverlayProps; + + beforeEach(() => { + const dummyElement = document.createElement('div'); + const mapInstance = new Map({ + target: dummyElement, + view: new View({ + zoom: 5, + minZoom: 3, + maxZoom: 7, + }), + }); + props = { + markerOverlay: { + color: null, + height: 2, + hexColor: '#A756BA90', + id: '3', + modelId: 1, + overlayId: 0, + type: 'rectangle', + value: 0.43, + width: 2, + x1: 1, + x2: 3, + y1: 3, + y2: 1, + }, + getOverlayColor: (): string => '#AABB11', + pointToProjection: ({ x, y }: { x: number; y: number }): Coordinate => [x, y], + mapInstance, + }; + + (getStroke as jest.Mock).mockReturnValue(new Stroke()); + (getFill as jest.Mock).mockReturnValue(new Style()); + (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); + }); + + it('should initialize line overlay feature', () => { + const markerOverlay = new MarkerOverlay(props); + + expect(markerOverlay.markerFeature).toBeInstanceOf(Feature); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0336335d7a158c1d13c8895a59504673eff8f27 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/MarkerOverlay.ts @@ -0,0 +1,83 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import Style from 'ol/style/Style'; +import { MapInstance } from '@/types/map'; +import Polygon from 'ol/geom/Polygon'; + +export type MarkerOverlayProps = { + markerOverlay: OverlayBioEntityRender; + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; +}; + +export default class MarkerOverlay { + markerOverlay: OverlayBioEntityRender; + + getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; + + pointToProjection: UsePointToProjectionResult; + + mapInstance: MapInstance; + + markerFeature: Feature; + + constructor({ + markerOverlay, + getOverlayColor, + pointToProjection, + mapInstance, + }: MarkerOverlayProps) { + this.markerOverlay = markerOverlay; + this.getOverlayColor = getOverlayColor; + this.pointToProjection = pointToProjection; + this.mapInstance = mapInstance; + this.markerFeature = this.drawOverlay(); + } + + drawOverlay(): Feature { + const color = this.getOverlayColor(this.markerOverlay); + const polygon = new Polygon([ + [ + this.pointToProjection({ x: this.markerOverlay.x1, y: this.markerOverlay.y1 }), + this.pointToProjection({ x: this.markerOverlay.x2, y: this.markerOverlay.y1 }), + this.pointToProjection({ x: this.markerOverlay.x2, y: this.markerOverlay.y2 }), + this.pointToProjection({ x: this.markerOverlay.x1, y: this.markerOverlay.y2 }), + ], + ]); + const style = getStyle({ + geometry: polygon, + fillColor: this.markerOverlay.hexColor || color, + lineWidth: 1, + zIndex: 99999, + }); + const markerFeature = new Feature<Polygon>({ + geometry: polygon, + style, + lineWidth: 1, + }); + markerFeature.setStyle(this.getStyle.bind(this)); + return markerFeature; + } + + protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); + const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); + const style = feature.get('style'); + if (!minResolution || !style) { + return []; + } + + const scale = minResolution / resolution; + const lineWidth = feature.get('lineWidth') * scale; + + if (style instanceof Style && style.getStroke()) { + style.getStroke()?.setWidth(lineWidth); + } + return style; + } +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e328d4e4e0cef2b34061c915fc1d55cec8e41d03 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.test.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import calculateOverlayDimensions from './calculateOverlayDimensions'; + +describe('calculateOverlayDimensions', () => { + it('should calculate overlay dimensions and update y1, y2, and height', () => { + const overlay: OverlayBioEntityRender & { amount: number } = { + amount: 3, + color: null, + height: 100, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }; + const entityOverlays: Array<OverlayBioEntityRender> = [ + { + color: null, + height: 100, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + const index = 1; + const result = calculateOverlayDimensions(overlay, index, 4, 100, entityOverlays); + + expect(result.height).toEqual(75); + expect(result.y1).toEqual(entityOverlays[index - 1].y1 + 75); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ac9819ca24c4d360fbc2090de13a1c873d3852c --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; + +export default function calculateOverlayDimensions( + overlay: OverlayBioEntityRender & { amount: number }, + index: number, + totalAmount: number, + totalHeight: number, + entityOverlays: Array<OverlayBioEntityRender>, +): OverlayBioEntityRender { + const ratio = overlay.amount / totalAmount; + const overlayHeight = ratio * totalHeight; + const overlayEntity = { ...overlay, height: overlayHeight }; + if (index !== 0) { + overlayEntity.y2 = entityOverlays[index - 1].y1; + } + overlayEntity.y1 = overlayEntity.y2 + overlayHeight; + + return overlayEntity; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e19c1bfc632f4a3f4e78a92b21d3aa8c9da615f0 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.test.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import findMatchingSubmapLinkRectangle from './findMatchingSubmapLinkRectangle'; + +describe('findMatchingSubmapLinkRectangle', () => { + const elements: Array<OverlayBioEntityRender & { amount: number }> = [ + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + it('should find a matching submap link rectangle by value or color', () => { + const overlayBioEntity: OverlayBioEntityRender = { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }; + + const result = findMatchingSubmapLinkRectangle(elements, overlayBioEntity); + expect(result).toEqual(elements[0]); + }); + + it('should return undefined if no matching element is found', () => { + const overlayBioEntity: OverlayBioEntityRender = { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.21, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }; + + const result = findMatchingSubmapLinkRectangle(elements, overlayBioEntity); + expect(result).toBeUndefined(); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.ts new file mode 100644 index 0000000000000000000000000000000000000000..25305cc3f6977796f3f02d0c685effe6f256f6c5 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle.ts @@ -0,0 +1,19 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; + +export default function findMatchingSubmapLinkRectangle( + elements: Array<OverlayBioEntityRender & { amount: number }>, + overlayBioEntity: OverlayBioEntityRender, +): (OverlayBioEntityRender & { amount: number }) | undefined { + return elements.find(element => { + const hasAllRequiredValueProperties = element.value && overlayBioEntity.value; + const isValueEqual = hasAllRequiredValueProperties && element.value === overlayBioEntity.value; + + const hasAllRequiredColorProperties = element.color && overlayBioEntity.color; + const isColorEqual = + hasAllRequiredColorProperties && + element.color?.alpha === overlayBioEntity?.color?.alpha && + element.color?.rgb === overlayBioEntity?.color?.rgb; + + return Boolean(isValueEqual || isColorEqual); + }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9fef8cf2f1ab61e5462f4e5a9b2f667492c0b71 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.test.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import groupOverlayEntities from './groupOverlayEntities'; + +describe('groupOverlays', () => { + it('should group overlay entities correctly by overlayId, value, and color', () => { + const overlayBioEntities: Array<OverlayBioEntityRender> = [ + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '1', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '1', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 2, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + const result = groupOverlayEntities(overlayBioEntities); + expect(result['1'][0].amount).toBe(2); + expect(result['2'][0].amount).toBe(1); + expect(result['3']).toBeUndefined(); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.ts new file mode 100644 index 0000000000000000000000000000000000000000..8cc7c0043eabf5328a350603e63131357a83f452 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/getOverlays.ts @@ -0,0 +1,26 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import groupOverlayEntities from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities'; +import processOverlayGroupedElements from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements'; + +export default function getOverlays( + groupedOverlays: Record<string, Array<OverlayBioEntityRender>>, + getColor: GetOverlayBioEntityColorByAvailableProperties, +): Record<string, Array<OverlayBioEntityRender>> { + const resultEntityOverlays: Record<string, Array<OverlayBioEntityRender>> = {}; + + Object.entries(groupedOverlays).forEach(([key, overlayBioEntities]) => { + const entityOverlays: Array<OverlayBioEntityRender> = []; + if (!resultEntityOverlays[key]) { + resultEntityOverlays[key] = []; + } + + const groupedElements = groupOverlayEntities(overlayBioEntities); + + processOverlayGroupedElements(groupedElements, entityOverlays, getColor); + + resultEntityOverlays[key].push(...entityOverlays); + }); + + return resultEntityOverlays; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f58872b69f34f21e02621ca5d633ac858712d7c --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.test.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import groupOverlayEntities from './groupOverlayEntities'; + +describe('groupOverlayEntities', () => { + it('should group overlay entities correctly by overlayId, value, and color', () => { + const overlayBioEntities: Array<OverlayBioEntityRender> = [ + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '1', + modelId: 0, + overlayId: 0, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '2', + modelId: 0, + overlayId: 0, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + color: null, + height: 50, + hexColor: '#0000001a', + id: '3', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + const result = groupOverlayEntities(overlayBioEntities); + + expect(result['0'][0].amount).toBe(2); + expect(result['1'][0].amount).toBe(1); + expect(result['2']).toBeUndefined(); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.ts new file mode 100644 index 0000000000000000000000000000000000000000..984574cfe4b7379ef61f6ff6c592b2ccb32a0f5d --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/groupOverlayEntities.ts @@ -0,0 +1,34 @@ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import findMatchingSubmapLinkRectangle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/findMatchingSubmapLinkRectangle'; +import { OverlayBioEntityGroupedElementsType } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +export default function groupOverlayEntities( + overlayBioEntities: Array<OverlayBioEntityRender>, +): OverlayBioEntityGroupedElementsType { + const groupedElements: OverlayBioEntityGroupedElementsType = {}; + + overlayBioEntities.forEach(overlayBioEntity => { + if (overlayBioEntity.type !== 'submap-link') { + return; + } + if (!groupedElements[overlayBioEntity.overlayId]) { + groupedElements[overlayBioEntity.overlayId] = []; + } + + const matchedElement = findMatchingSubmapLinkRectangle( + groupedElements[overlayBioEntity.overlayId], + overlayBioEntity, + ); + + if (!matchedElement) { + groupedElements[overlayBioEntity.overlayId].push({ + ...overlayBioEntity, + amount: 1, + }); + } else { + matchedElement.amount += 1; + } + }); + + return groupedElements; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..680002ea1ba899f15df25fe9af4fe8227aae5c1c --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.test.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import processOverlayGroupedElements from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements'; +import { OverlayBioEntityGroupedElementsType } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +describe('processOverlayGroupedElements', () => { + it('should correctly process overlay grouped elements and add to entityOverlays', () => { + const groupedElements = { + '1': [ + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '5', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '5', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ], + } as OverlayBioEntityGroupedElementsType; + + const entityOverlays: Array<OverlayBioEntityRender> = []; + const getColor: GetOverlayBioEntityColorByAvailableProperties = jest.fn(() => 'color'); + + processOverlayGroupedElements(groupedElements, entityOverlays, getColor); + + expect(entityOverlays.length).toBe(2); + expect(entityOverlays[0].height).toEqual(25); + expect(entityOverlays[1].height).toEqual(25); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e2a9b434f656123cf48262b0c6ae547f83903b2 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/processOverlayGroupedElements.ts @@ -0,0 +1,35 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import sortElementOverlayByColor from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor'; +import calculateOverlayDimensions from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/calculateOverlayDimensions'; +import { OverlayBioEntityGroupedElementsType } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +export default function processOverlayGroupedElements( + groupedElements: OverlayBioEntityGroupedElementsType, + entityOverlays: Array<OverlayBioEntityRender>, + getColor: GetOverlayBioEntityColorByAvailableProperties, +): void { + Object.values(groupedElements).forEach(elementOverlay => { + const overlaysPerGroup: Array<OverlayBioEntityRender> = []; + sortElementOverlayByColor(elementOverlay, getColor); + + const totalHeight = elementOverlay[0].height; + const totalAmount = elementOverlay.reduce( + (accumulator: number, overlay) => accumulator + overlay.amount, + 0, + ); + + elementOverlay.forEach((overlay, index) => { + const overlayEntity = calculateOverlayDimensions( + overlay, + index, + totalAmount, + totalHeight, + overlaysPerGroup, + ); + overlaysPerGroup.push(overlayEntity); + }); + entityOverlays.push(...overlaysPerGroup); + }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f39cc2aa9d26b8460f694daf3fe95ee6e600af2 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.test.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-magic-numbers */ +import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import sortElementOverlayByColor from './sortElementOverlayByColor'; + +describe('sortElementOverlayByColor', () => { + it('should sort elements by color', () => { + const elementOverlay: Array<OverlayBioEntityRender & { amount: number }> = [ + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '5', + modelId: 0, + overlayId: 0, + type: 'submap-link', + value: 0.1, + width: 100, + x1: 1200, + x2: 1300, + y1: 550, + y2: 500, + }, + { + amount: 1, + color: null, + height: 50, + hexColor: '#0000001a', + id: '2', + modelId: 0, + overlayId: 1, + type: 'submap-link', + value: -0.43, + width: 100, + x1: 200, + x2: 300, + y1: 750, + y2: 700, + }, + ]; + + const getColor: GetOverlayBioEntityColorByAvailableProperties = jest.fn( + entity => `#A633C${entity.id}`, + ); + sortElementOverlayByColor(elementOverlay, getColor); + + expect(elementOverlay[0].id).toEqual('2'); + expect(elementOverlay[1].id).toEqual('5'); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.ts new file mode 100644 index 0000000000000000000000000000000000000000..b185b6373eecc6a0d06e364f053b8565454031db --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/overlay/sortElementOverlayByColor.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-magic-numbers */ +import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; +import { OverlayBioEntityRender } from '@/types/OLrendering'; + +export default function sortElementOverlayByColor( + elementOverlay: Array<OverlayBioEntityRender & { amount: number }>, + getColor: GetOverlayBioEntityColorByAvailableProperties, +): void { + elementOverlay.sort((a, b) => { + const colorA = getColor(a); + const colorB = getColor(b); + if (colorA === colorB) { + return 0; + } + if (colorA < colorB) { + return -1; + } + return 1; + }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts index 6c98bf802207863a86679b0f3a9e43c95f80d0f3..96083434e3f49d298495931f119bcd3b96f314b7 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts @@ -19,8 +19,8 @@ export default function getStyle({ zIndex = 1, }: { geometry?: Geometry; - borderColor?: Color; - fillColor?: Color; + borderColor?: Color | string; + fillColor?: Color | string; lineWidth?: number; lineDash?: Array<number>; zIndex?: number; @@ -28,11 +28,11 @@ export default function getStyle({ return new Style({ geometry, stroke: getStroke({ - color: rgbToHex(borderColor), + color: typeof borderColor === 'string' ? borderColor : rgbToHex(borderColor), width: lineWidth, lineDash, }), - fill: getFill({ color: rgbToHex(fillColor) }), + fill: getFill({ color: typeof fillColor === 'string' ? fillColor : rgbToHex(fillColor) }), zIndex, }); } diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts index e11025d81b6401b8b26fc0879d8b51d255a1e034..a6ca120fdf276150bbf8e84435d36d82d63d46c6 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayGeometryFeature.ts @@ -5,7 +5,10 @@ import { Fill, Stroke, Style } from 'ol/style'; import { createFeatureFromExtent } from './createFeatureFromExtent'; const getBioEntityOverlayFeatureStyle = (color: string): Style => - new Style({ fill: new Fill({ color }), stroke: new Stroke({ color: 'black', width: 1 }) }); + new Style({ + fill: new Fill({ color }), + stroke: new Stroke({ color: 'black', width: 1 }), + }); export const createOverlayGeometryFeature = ( [xMin, yMin, xMax, yMax]: number[], diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts index 46d7190721fbb14929d055cdeead41208bde90d3..0ac575a40485c369e3f791e2a6af4e27b2671075 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.ts @@ -14,7 +14,9 @@ import { getHexStringColorFromRGBIntWithAlpha } from '@/utils/convert/getHexStri import { getHexTricolorGradientColorWithAlpha } from '@/utils/convert/getHexTricolorGradientColorWithAlpha'; import { useCallback, useMemo } from 'react'; -type GetOverlayBioEntityColorByAvailableProperties = (entity: OverlayBioEntityRender) => string; +export type GetOverlayBioEntityColorByAvailableProperties = ( + entity: OverlayBioEntityRender, +) => string; type UseTriColorLerpReturn = { getOverlayBioEntityColorByAvailableProperties: GetOverlayBioEntityColorByAvailableProperties; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts index 107add31699aceb508557e10d9b54420c4d5171a..020e6ae987dd7d3c16964637ee8ef92bf4fe0bd4 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts @@ -22,6 +22,7 @@ export const useOverlayFeatures = (): Feature<Polygon>[] | Feature<SimpleGeometr const overlaysOrder = useAppSelector(getOverlayOrderSelector); const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); + const bioEntities = useBioEntitiesWithSubmapsLinks(); const markersFeatures = useMemo( diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index 75d8c838d5ae06005c42beacd068044fe7ff7dbc..641b537da8e0fd922993eadc7c3e2f0291d8dc0c 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -11,7 +11,7 @@ import Feature from 'ol/Feature'; import { Geometry } from 'ol/geom'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getBioEntitiesFeatures } from './getBioEntitiesFeatures'; import { getMarkersFeatures } from './getMarkersFeatures'; @@ -67,19 +67,18 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> ], ); - const vectorSource = useMemo(() => { - return new VectorSource({ - features: [...elementsFeatures], - }); - }, [elementsFeatures]); + const vectorSource = useMemo(() => new VectorSource(), []); - const pinsLayer = useMemo( + useEffect(() => { + vectorSource.clear(); + vectorSource.addFeatures(elementsFeatures); + }, [elementsFeatures, vectorSource]); + + return useMemo( () => new VectorLayer({ source: vectorSource, }), [vectorSource], ); - - return pinsLayer; }; diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index c564b6622773e5e8b26812bbbab2ea20cf757dc3..a6350f6357cf3b6d6bcd8acedbcca8099010e5d1 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -6,15 +6,15 @@ import { MarkerLine, Reaction } from '@/types/models'; import { LinePoint } from '@/types/reactions'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; -import { SimpleGeometry } from 'ol/geom'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Style from 'ol/style/Style'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { createOverlayLineFeature } from '@/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature'; +import { Geometry } from 'ol/geom'; import { getLineFeature } from './getLineFeature'; const getLinePoints = ({ start, end }: Pick<MarkerLine, 'start' | 'end'>): LinePoint => [ @@ -25,7 +25,7 @@ const getLinePoints = ({ start, end }: Pick<MarkerLine, 'start' | 'end'>): LineP const getReactionsLines = (reactions: Reaction[]): LinePoint[] => reactions.map(({ lines }) => lines.map(getLinePoints)).flat(); -export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<SimpleGeometry>>> => { +export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { const pointToProjection = usePointToProjection(); const reactions = useSelector(allReactionsSelectorOfCurrentMap); const markers = useSelector(markersLinesCurrentMapDataSelector); @@ -47,13 +47,15 @@ export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Simpl [markers, pointToProjection], ); - const vectorSource = useMemo(() => { - return new VectorSource({ - features: [...reactionsLinesFeatures, ...markerLinesFeatures], - }); - }, [reactionsLinesFeatures, markerLinesFeatures]); + const vectorSource = useMemo(() => new VectorSource(), []); - const reactionsLayer = useMemo( + useEffect(() => { + vectorSource.clear(); + vectorSource.addFeatures(reactionsLinesFeatures); + vectorSource.addFeatures(markerLinesFeatures); + }, [reactionsLinesFeatures, markerLinesFeatures, vectorSource]); + + return useMemo( () => new VectorLayer({ source: vectorSource, @@ -64,6 +66,4 @@ export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Simpl }), [vectorSource], ); - - return reactionsLayer; }; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts index ded469b28450cc91d57fd1cc0410b380e9de994c..852f8093ee021e4863f614c8ec194cd484a7ff0e 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.test.ts @@ -88,11 +88,4 @@ describe('useOlMapCommonLayers - util', () => { expect(result[2]).toBeInstanceOf(VectorLayer); expect(result[2].getSourceState()).toBe('ready'); }); - - it('should return valid VectorLayer instance [4]', () => { - const result = getRenderedHookResults(); - - expect(result[3]).toBeInstanceOf(VectorLayer); - expect(result[3].getSourceState()).toBe('ready'); - }); }); diff --git a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts index bca964e7fd5f23b3ac90b2be4eb6e5e2640fb2f4..ad70201bb3a2d1eb82a70eff6229a098f885bf78 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapCommonLayers.ts @@ -1,15 +1,16 @@ /* eslint-disable no-magic-numbers */ -import { useOlMapOverlaysLayer } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer'; import { useOlMapPinsLayer } from '@/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer'; import { useOlMapReactionsLayer } from '@/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer'; import { useOlMapCommentsLayer } from '@/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer'; +import { useMemo } from 'react'; import { MapConfig } from '../../MapViewer.types'; export const useOlMapCommonLayers = (): MapConfig['layers'] => { - const overlaysLayer = useOlMapOverlaysLayer(); const pinsLayer = useOlMapPinsLayer(); const reactionsLayer = useOlMapReactionsLayer(); const commentsLayer = useOlMapCommentsLayer(); - return [overlaysLayer, pinsLayer, reactionsLayer, commentsLayer]; + return useMemo(() => { + return [pinsLayer, reactionsLayer, commentsLayer]; + }, [pinsLayer, reactionsLayer, commentsLayer]); }; diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts index c0be0af81ff66c3ec74e7d2ebb0825c3df17e38e..ffcd6326cfbef787a7561c27b6b4a7333a731bde 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts @@ -5,6 +5,7 @@ import { renderHook } from '@testing-library/react'; import BaseLayer from 'ol/layer/Base'; import TileLayer from 'ol/layer/Tile'; import React from 'react'; +import VectorLayer from 'ol/layer/Vector'; import { useOlMapLayers } from './useOlMapLayers'; const useRefValue = { @@ -74,4 +75,11 @@ describe('useOlMapLayers - util', () => { expect(result[0]).toBeInstanceOf(TileLayer); expect(result[0].getSourceState()).toBe('ready'); }); + + it('should return valid VectorLayer instance [2]', () => { + const result = getRenderedHookResults(); + + expect(result[1]).toBeInstanceOf(VectorLayer); + expect(result[1].getSourceState()).toBe('ready'); + }); }); diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts index f05f8c9d85e764a7074a9e35d98f5d695a65ae92..10169c987bf91c24241479ea9a1acb666bd7b64a 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts @@ -1,9 +1,11 @@ /* eslint-disable no-magic-numbers */ +import { useOlMapOverlaysLayer } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer'; import { MapConfig } from '../../MapViewer.types'; import { useOlMapTileLayer } from './useOlMapTileLayer'; export const useOlMapLayers = (): MapConfig['layers'] => { + const overlaysLayer = useOlMapOverlaysLayer(); const tileLayer = useOlMapTileLayer(); - return [tileLayer]; + return [tileLayer, overlaysLayer]; }; diff --git a/src/utils/useDebouncedValue.test.ts b/src/utils/useDebouncedValue.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0691ad24ba8d165c5f9c55ddde375a2df98d982 --- /dev/null +++ b/src/utils/useDebouncedValue.test.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-magic-numbers */ +import { act, renderHook } from '@testing-library/react'; +import useDebouncedValue from './useDebouncedValue'; + +jest.useFakeTimers(); + +describe('useDebouncedValue', () => { + it('should return the initial value immediately', () => { + const { result } = renderHook(() => useDebouncedValue('initial', 500)); + expect(result.current).toBe('initial'); + }); + + it('should update the value after the specified delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebouncedValue(value, delay), { + initialProps: { value: 'initial', delay: 500 }, + }); + + rerender({ value: 'updated', delay: 500 }); + + expect(result.current).toBe('initial'); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe('updated'); + }); + + it('should clear the timeout if value changes quickly', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebouncedValue(value, delay), { + initialProps: { value: 'initial', delay: 500 }, + }); + + rerender({ value: 'intermediate', delay: 500 }); + + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(result.current).toBe('initial'); + + rerender({ value: 'final', delay: 500 }); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe('final'); + }); +}); diff --git a/src/utils/useDebouncedValue.ts b/src/utils/useDebouncedValue.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d52da6b550e3aef6c118a1bb5a1d6e78548448d --- /dev/null +++ b/src/utils/useDebouncedValue.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; + +export default function useDebouncedValue<T>(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}