diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index 57b5623a8fca1dacbe72929893c978e04adc1352..2eaeebda4dd16f78781b9facfcde3a9f6e8b8cc7 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -15,6 +15,7 @@ import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; import { OverlayBioEntityRender } from '@/types/OLrendering'; import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export default function processModelElements( modelElements: ModelElements, @@ -27,6 +28,7 @@ export default function processModelElements( mapInstance: MapInstance, pointToProjection: UsePointToProjectionResult, mapBackgroundType: number, + mapSize: MapSize, ): Array<MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph> { const validElements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph @@ -76,6 +78,7 @@ export default function processModelElements( mapInstance, vectorSource, mapBackgroundType, + mapSize, }; if (element.shape === 'OVAL_COMPARTMENT') { validElements.push(new CompartmentCircle(compartmentProps)); @@ -125,6 +128,7 @@ export default function processModelElements( overlaysOrder, getOverlayColor, mapBackgroundType, + mapSize, }), ); } 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 a6c6253e03e93b6ae74672775c0bf040338474d5..4e289297b127de56bb0bbda73f398ae625ef2383 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -38,7 +38,7 @@ import { parseSurfaceMarkersToBioEntityRender } from '@/components/Map/MapViewer 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'; -import { mapBackgroundTypeSelector } from '@/redux/map/map.selectors'; +import { mapBackgroundTypeSelector, mapDataSizeSelector } from '@/redux/map/map.selectors'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -51,6 +51,7 @@ export const useOlMapReactionsLayer = ({ const modelElements = useSelector(modelElementsSelector); const modelReactions = useSelector(newReactionsDataSelector); const shapes = useSelector(bioShapesSelector); + const mapSize = useSelector(mapDataSizeSelector); const lineTypes = useSelector(lineTypesSelector); const arrowTypes = useSelector(arrowTypesSelector); const overlaysOrder = useSelector(getOverlayOrderSelector); @@ -159,6 +160,7 @@ export const useOlMapReactionsLayer = ({ mapInstance, pointToProjection, mapBackgroundType, + mapSize, ); }, [ modelElements, @@ -171,6 +173,7 @@ export const useOlMapReactionsLayer = ({ mapInstance, pointToProjection, mapBackgroundType, + mapSize, ]); const features = useMemo(() => { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a41ebad7e443805cc4c5aae43aee00c9f70ae7a --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.test.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-magic-numbers */ + +import findLargestExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent'; +import { Extent } from 'ol/extent'; + +describe('findLargestExtent', () => { + it('should find largest extent from a given array', () => { + const extents: Array<Extent> = [ + [100, 100, 200, 200], + [150, 200, 400, 500], + ]; + + const result = findLargestExtent(extents); + expect(result).toEqual(extents[1]); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.ts new file mode 100644 index 0000000000000000000000000000000000000000..f013e03ccdc2da57eb4cc1f8e0ec67c6ac038c6b --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent.ts @@ -0,0 +1,16 @@ +import { Extent, getArea } from 'ol/extent'; + +export default function findLargestExtent(extents: Extent[]): Extent | null { + let largestExtent = null; + let maxArea = 0; + + extents.forEach(extent => { + const area = getArea(extent); + if (area > maxArea) { + maxArea = area; + largestExtent = extent; + } + }); + + return largestExtent; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea4a9f8956a28abd4c43c37345688cd668502ce6 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.test.ts @@ -0,0 +1,74 @@ +/* eslint-disable no-magic-numbers */ +import { Extent } from 'ol/extent'; +import getDividedExtents from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents'; + +describe('getDividedExtents', () => { + it('should return original extents if there is no intersection with dividingExtent', () => { + const extentsArray: Array<Extent> = [ + [0, 0, 10, 10], + [20, 20, 30, 30], + ]; + const dividingExtent: Extent = [15, 15, 18, 18]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + expect(result).toEqual(extentsArray); + }); + + it('should divide extent when it intersects with dividingExtent', () => { + const extentsArray: Array<Extent> = [[0, 0, 20, 20]]; + const dividingExtent: Extent = [10, 10, 15, 15]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + const expected = [ + [0, 0, 10, 20], + [15, 0, 20, 20], + [0, 15, 20, 20], + [0, 0, 20, 10], + ]; + + expect(result).toEqual(expected); + }); + + it('should return a mix of original and divided extents when some extents intersect and others do not', () => { + const extentsArray: Array<Extent> = [ + [0, 0, 20, 20], + [25, 25, 30, 30], + ]; + const dividingExtent: Extent = [10, 10, 15, 15]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + const expected = [ + [0, 0, 10, 20], + [15, 0, 20, 20], + [0, 15, 20, 20], + [0, 0, 20, 10], + [25, 25, 30, 30], + ]; + + expect(result).toEqual(expected); + }); + + it('should handle case where dividingExtent completely overlaps an extent', () => { + const extentsArray: Array<Extent> = [[10, 10, 20, 20]]; + const dividingExtent: Extent = [10, 10, 20, 20]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + expect(result).toEqual([]); + }); + + it('should handle case where extents are completely outside dividingExtent', () => { + const extentsArray: Array<Extent> = [ + [0, 0, 5, 5], + [25, 25, 30, 30], + ]; + const dividingExtent: Extent = [10, 10, 20, 20]; + + const result = getDividedExtents(extentsArray, dividingExtent); + + expect(result).toEqual(extentsArray); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f36e3e519fb9acfe794f3386a3ea2200613e796 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-magic-numbers */ +import { Extent } from 'ol/extent'; + +export default function getDividedExtents( + extentsArray: Array<Extent>, + dividingExtent: Extent, +): Array<Extent> { + const minX2 = dividingExtent[0]; + const minY2 = dividingExtent[1]; + const maxX2 = dividingExtent[2]; + const maxY2 = dividingExtent[3]; + const dividedExtents: Array<Extent> = []; + + extentsArray.forEach(extent => { + const [minX1, minY1, maxX1, maxY1] = [...extent]; + + const intersects = minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; + if (intersects) { + if (minX2 <= minX1 && minY2 <= minY1 && maxX2 >= maxX1 && maxY2 >= maxY1) { + return; + } + if (minX2 > minX1) { + const leftExtent = [minX1, minY1, minX2, maxY1]; + dividedExtents.push(leftExtent); + } + if (minX2 < maxX1) { + const rightExtent = [maxX2, minY1, maxX1, maxY1]; + dividedExtents.push(rightExtent); + } + if (maxY2 < maxY1) { + const topExtent = [minX1, maxY2, maxX1, maxY1]; + dividedExtents.push(topExtent); + } + if (minY2 > minY1) { + const bottomExtent = [minX1, minY1, maxX1, minY2]; + dividedExtents.push(bottomExtent); + } + } else { + dividedExtents.push(extent); + } + }); + return dividedExtents; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 0843cc9114060ddbc65523b715acf1d81c2fa642..fab805158ec53aa81d2eefe3d88f359949f6620f 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -2,7 +2,7 @@ import Polygon from 'ol/geom/Polygon'; import { Style } from 'ol/style'; import Feature, { FeatureLike } from 'ol/Feature'; -import { Geometry, MultiPolygon } from 'ol/geom'; +import { MultiPolygon } from 'ol/geom'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { HorizontalAlign, @@ -19,6 +19,10 @@ import { } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { Extent } from 'ol/extent'; +import { MapSize } from '@/redux/map/map.types'; +import getCoverStyles from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles'; +import handleSemanticView from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView'; export interface BaseMapElementProps { type: string; @@ -45,6 +49,7 @@ export interface BaseMapElementProps { pointToProjection: UsePointToProjectionResult; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; } export default abstract class BaseMultiPolygon { @@ -102,6 +107,8 @@ export default abstract class BaseMultiPolygon { mapBackgroundType: number; + mapSize: MapSize; + constructor({ type, sboTerm, @@ -127,6 +134,7 @@ export default abstract class BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }: BaseMapElementProps) { this.type = type; this.sboTerm = sboTerm; @@ -152,6 +160,7 @@ export default abstract class BaseMultiPolygon { this.pointToProjection = pointToProjection; this.vectorSource = vectorSource; this.mapBackgroundType = mapBackgroundType; + this.mapSize = mapSize; } protected abstract createPolygons(): void; @@ -188,6 +197,7 @@ export default abstract class BaseMultiPolygon { protected drawMultiPolygonFeature(mapInstance: MapInstance): void { this.feature = new Feature({ geometry: new MultiPolygon(this.polygons), + zIndex: this.zIndex, getScale: (resolution: number): number => { const maxZoom = mapInstance?.getView().get('originalMaxZoom'); if (maxZoom) { @@ -220,61 +230,39 @@ export default abstract class BaseMultiPolygon { complexId: this.complexId, compartmentId: this.compartmentId, type: this.type, - zIndex: this.zIndex, }); this.feature.setId(this.id); this.feature.setStyle(this.getStyle.bind(this)); } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + if (!(feature instanceof Feature)) { + return undefined; + } const styles: Array<Style> = []; const getScale = feature.get('getScale'); - const getMapExtent = feature.get('getMapExtent'); let scale = 1; let cover = false; - let coverRation: number = 1; + let largestExtent: Extent | null; + if (getScale instanceof Function) { scale = getScale(resolution); } let hide = false; if (this.mapBackgroundType === MapBackgroundsEnum.SEMANTIC) { - if (getMapExtent instanceof Function && this.type === 'COMPARTMENT') { - const mapExtent = getMapExtent(resolution); - const featureExtent = feature.getGeometry()?.getExtent(); - if (featureExtent && mapExtent) { - const mapArea = - Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); - const compartmentArea = - Math.abs(featureExtent[2] - featureExtent[0]) * - Math.abs(featureExtent[3] - featureExtent[1]); - coverRation = compartmentArea / mapArea; - if (coverRation < 0.05 && scale < 1) { - cover = true; - } - (feature as Feature).set('filled', cover); - } - } + const semanticViewData = handleSemanticView( + this.vectorSource, + feature, + resolution, + scale, + this.compartmentId, + this.complexId, + ); + cover = semanticViewData.cover; + hide = semanticViewData.hide; + largestExtent = semanticViewData.largestExtent; - let complex: Feature<Geometry> | null; - let compartment: Feature<Geometry> | null; - if (this.complexId) { - complex = this.vectorSource.getFeatureById(this.complexId); - if (complex) { - if (complex.get('hidden')) { - hide = true; - } - } - } - if (this.compartmentId) { - compartment = this.vectorSource.getFeatureById(this.compartmentId); - if (compartment) { - if (compartment.get('filled')) { - hide = true; - } - } - } - (feature as Feature).set('hidden', hide); if (hide) { return undefined; } @@ -291,17 +279,27 @@ export default abstract class BaseMultiPolygon { if (styleGeometry instanceof Polygon) { type = styleGeometry.get('type'); text = styleGeometry.get('text'); - fontSize = styleGeometry.get('fontSize'); + fontSize = styleGeometry.get('fontSize') || 10; lineWidth = styleGeometry.get('lineWidth'); coverStyle = styleGeometry.get('coverStyle'); } + if (cover) { - if (coverStyle) { - coverStyle.setZIndex(this.zIndex + 1000); - styles.push(coverStyle); + if (coverStyle && largestExtent) { + styles.push( + ...getCoverStyles( + coverStyle, + largestExtent, + this.text, + scale, + this.zIndex + 1000, + this.mapSize, + ), + ); } return; } + if ( [MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && scale * fontSize <= 4 @@ -330,6 +328,7 @@ export default abstract class BaseMultiPolygon { } styles.push(clonedStyle); }); + return styles; } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index c61b16d1f3d353696a52cb3115727258280fb429..a74beed43155e5da2edf7641e5116d1142f68454 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -15,6 +15,7 @@ import { MapInstance } from '@/types/map'; import { Color } from '@/types/models'; import { MAP_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export interface CompartmentProps { id: number; @@ -43,6 +44,7 @@ export interface CompartmentProps { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; } export default abstract class Compartment extends BaseMultiPolygon { @@ -83,6 +85,7 @@ export default abstract class Compartment extends BaseMultiPolygon { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentProps) { super({ type: 'COMPARTMENT', @@ -108,6 +111,7 @@ export default abstract class Compartment extends BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }); this.outerWidth = outerWidth; this.innerWidth = innerWidth; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts index ba61e067d0cd195436f958ac8ed6935e55d5081e..376e0408f23fb0f6692e1eda9c0e39fde8307596 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts @@ -19,6 +19,7 @@ import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -68,6 +69,13 @@ describe('CompartmentCircle', () => { mapInstance, vectorSource: new VectorSource(), mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts index 040cc8eedd4c24d911395acd306d06fcd21b2a9a..bd80b0958be5bb639c9bfb6296b7b211dc63cd7b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts @@ -15,6 +15,7 @@ import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; import { Color } from '@/types/models'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export type CompartmentCircleProps = { id: number; @@ -43,6 +44,7 @@ export type CompartmentCircleProps = { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; }; export default class CompartmentCircle extends Compartment { @@ -73,6 +75,7 @@ export default class CompartmentCircle extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentCircleProps) { super({ id, @@ -101,6 +104,7 @@ export default class CompartmentCircle extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts index 26bf658b0450675f590529ddca69b3bec198943b..ef3d8cde19da7f38a0a7746bcb378e660153165c 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.test.ts @@ -19,6 +19,7 @@ import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -66,6 +67,13 @@ describe('CompartmentPathway', () => { mapInstance, vectorSource: new VectorSource(), mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index 0fc1362727c426c653619aebfd6ed0a49f1335b6..92bb522d23eb04a138c1780ac37a392cdb804cea 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -18,6 +18,7 @@ import VectorSource from 'ol/source/Vector'; import { Style } from 'ol/style'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import { MapSize } from '@/redux/map/map.types'; export type CompartmentPathwayProps = { id: number; @@ -44,6 +45,7 @@ export type CompartmentPathwayProps = { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; }; export default class CompartmentPathway extends BaseMultiPolygon { @@ -74,6 +76,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentPathwayProps) { super({ type: 'COMPARTMENT', @@ -99,6 +102,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }); this.outerWidth = outerWidth; this.createPolygons(); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts index ae4148d289a05838338d74c10dc1c1732cc03c20..ac2f9d5231589c1e1bbfa632c19f0493fa437f31 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts @@ -18,6 +18,7 @@ import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -66,6 +67,13 @@ describe('CompartmentSquare', () => { mapInstance, vectorSource: new VectorSource(), mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts index 1732c177dabca24d370522e51a3adc9c6dca5996..f5f1df8147bf3c07e02487c5c114f65f2525fd6b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts @@ -14,6 +14,7 @@ import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; import { Color } from '@/types/models'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export type CompartmentSquareProps = { id: number; @@ -42,6 +43,7 @@ export type CompartmentSquareProps = { mapInstance: MapInstance; vectorSource: VectorSource; mapBackgroundType: number; + mapSize: MapSize; }; export default class CompartmentSquare extends Compartment { @@ -72,6 +74,7 @@ export default class CompartmentSquare extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }: CompartmentSquareProps) { super({ id, @@ -100,6 +103,7 @@ export default class CompartmentSquare extends Compartment { mapInstance, vectorSource, mapBackgroundType, + mapSize, }); } 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 1611b7fe058d11ee1cacc475b0113d9667cdcdbc..a3c6c67997f8c3ced49597d5f4790b6cb58f3e3b 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 @@ -19,6 +19,7 @@ import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shap import { shapesFixture } from '@/models/fixtures/shapesFixture'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); @@ -68,6 +69,13 @@ describe('MapElement', () => { vectorSource: new VectorSource(), getOverlayColor: (): string => '#ffffff', mapBackgroundType: MapBackgroundsEnum.SEMANTIC, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (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 4a0cd82c7e9ec6849d083707a0770359d8eea34e..826e5fca10286f45e954e2e98b23daaa035d072b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -29,6 +29,7 @@ import { ZERO } from '@/constants/common'; import { OverlayOrder } from '@/redux/overlayBioEntity/overlayBioEntity.utils'; import { GetOverlayBioEntityColorByAvailableProperties } from '@/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor'; import VectorSource from 'ol/source/Vector'; +import { MapSize } from '@/redux/map/map.types'; export type MapElementProps = { id: number; @@ -66,6 +67,7 @@ export type MapElementProps = { overlaysOrder?: Array<OverlayOrder>; getOverlayColor: GetOverlayBioEntityColorByAvailableProperties; mapBackgroundType: number; + mapSize: MapSize; }; export default class MapElement extends BaseMultiPolygon { @@ -129,6 +131,7 @@ export default class MapElement extends BaseMultiPolygon { overlaysOrder = [], getOverlayColor, mapBackgroundType, + mapSize, }: MapElementProps) { super({ type: FEATURE_TYPE.ALIAS, @@ -155,6 +158,7 @@ export default class MapElement extends BaseMultiPolygon { pointToProjection, vectorSource, mapBackgroundType, + mapSize, }); this.shapes = shapes; this.lineWidth = lineWidth; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b7e01e1131522f36a917e49f25213d7d7aa6189 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.test.ts @@ -0,0 +1,112 @@ +/* eslint-disable no-magic-numbers */ +import Feature from 'ol/Feature'; +import VectorSource from 'ol/source/Vector'; +import getDividedExtents from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents'; +import findLargestExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent'; +import handleSemanticView from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView'; +import Geometry from 'ol/geom/Geometry'; +import { fromExtent } from 'ol/geom/Polygon'; + +jest.mock('../coords/getDividedExtents'); +jest.mock('../coords/findLargestExtent'); + +describe('handleSemanticView', () => { + let vectorSource: VectorSource; + let feature: Feature; + + beforeEach(() => { + vectorSource = new VectorSource(); + feature = new Feature({ + geometry: fromExtent([0, 0, 100, 100]), + type: 'COMPARTMENT', + zIndex: 1, + getMapExtent: (): Array<number> => [0, 0, 100, 100], + filled: false, + hidden: false, + }); + + const mockGeometry = { + getExtent: jest.fn(() => [2, 0, 10, 10]), + } as unknown as Geometry; + + feature.getGeometry = jest.fn(() => mockGeometry); + }); + + it('should return cover = true, hide = false, and calculate largestExtent when feature meets cover conditions', () => { + jest + .spyOn(vectorSource, 'forEachFeatureIntersectingExtent') + .mockImplementation((_, callback) => { + callback( + new Feature({ + geometry: fromExtent([1, 0, 5, 5]), + hidden: false, + type: 'COMPARTMENT', + zIndex: 123, + filled: true, + getMapExtent: (): Array<number> => [1, 0, 5, 5], + }), + ); + }); + (getDividedExtents as jest.Mock).mockReturnValue([[0, 0, 10, 5]]); + (findLargestExtent as jest.Mock).mockReturnValue([0, 0, 10, 5]); + + const result = handleSemanticView(vectorSource, feature, 1, 0.5, null); + + expect(result).toEqual({ + cover: true, + hide: false, + largestExtent: [0, 0, 10, 5], + }); + + expect(feature.get('filled')).toBe(true); + expect(getDividedExtents).toHaveBeenCalled(); + expect(findLargestExtent).toHaveBeenCalled(); + }); + + it('should return hide = true when complexId points to a hidden feature', () => { + const complexFeature = new Feature({ hidden: true }); + jest + .spyOn(vectorSource, 'getFeatureById') + .mockImplementation(id => (id === 1 ? complexFeature : null)); + + const result = handleSemanticView(vectorSource, feature, 1, 1, null, 1); + + expect(result).toEqual({ + cover: false, + hide: true, + largestExtent: null, + }); + + expect(feature.get('hidden')).toBe(true); + }); + + it('should return hide = true when compartmentId points to a filled feature', () => { + const compartmentFeature = new Feature({ filled: true }); + jest + .spyOn(vectorSource, 'getFeatureById') + .mockImplementation(id => (id === 2 ? compartmentFeature : null)); + + const result = handleSemanticView(vectorSource, feature, 1, 1, 2); + + expect(result).toEqual({ + cover: false, + hide: true, + largestExtent: null, + }); + + expect(feature.get('hidden')).toBe(true); + }); + + it('should return cover = false and hide = false when feature does not meet any conditions', () => { + const result = handleSemanticView(vectorSource, feature, 1, 1, null); + + expect(result).toEqual({ + cover: false, + hide: false, + largestExtent: null, + }); + + expect(feature.get('filled')).toBe(false); + expect(feature.get('hidden')).toBe(false); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts new file mode 100644 index 0000000000000000000000000000000000000000..83269b75f248c1e41a8b44b8fcb2e7b7160f1448 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-magic-numbers */ +import getDividedExtents from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getDividedExtents'; +import findLargestExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/findLargestExtent'; +import Feature from 'ol/Feature'; +import VectorSource from 'ol/source/Vector'; +import { Extent } from 'ol/extent'; + +export default function handleSemanticView( + vectorSource: VectorSource, + feature: Feature, + resolution: number, + scale: number, + compartmentId: number | null, + complexId?: number | null, +): { cover: boolean; hide: boolean; largestExtent: Extent | null } { + const type = feature.get('type'); + const getMapExtent = feature.get('getMapExtent'); + let coverRatio = 1; + let cover = false; + let hide = false; + let largestExtent: Extent | null = null; + if (getMapExtent instanceof Function && type === 'COMPARTMENT') { + const mapExtent = getMapExtent(resolution); + const featureExtent = feature.getGeometry()?.getExtent(); + if (featureExtent && mapExtent) { + const mapArea = Math.abs(mapExtent[2] - mapExtent[0]) * Math.abs(mapExtent[3] - mapExtent[1]); + const compartmentArea = + Math.abs(featureExtent[2] - featureExtent[0]) * + Math.abs(featureExtent[3] - featureExtent[1]); + coverRatio = compartmentArea / mapArea; + if (coverRatio < 0.05 && scale < 1) { + cover = true; + let remainingExtents = [featureExtent]; + vectorSource.forEachFeatureIntersectingExtent(featureExtent, intersectingFeature => { + if ( + !intersectingFeature.get('hidden') && + intersectingFeature.get('type') === 'COMPARTMENT' && + intersectingFeature.get('zIndex') > feature.get('zIndex') && + intersectingFeature.get('filled') + ) { + const intersectingFeatureExtent = intersectingFeature.getGeometry()?.getExtent(); + if (intersectingFeatureExtent) { + remainingExtents = getDividedExtents(remainingExtents, intersectingFeatureExtent); + } + } + }); + largestExtent = findLargestExtent(remainingExtents) || featureExtent; + } + (feature as Feature).set('filled', cover); + } + } + + if (complexId) { + const complex = vectorSource.getFeatureById(complexId); + if (complex && complex.get('hidden')) { + hide = true; + } + } + if (compartmentId) { + const compartment = vectorSource.getFeatureById(compartmentId); + if (compartment && compartment.get('filled')) { + hide = true; + } + } + (feature as Feature).set('hidden', hide); + + return { cover, hide, largestExtent }; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e22293c5bdb85bd53191ed7d3a784a48310f5e0 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.test.ts @@ -0,0 +1,88 @@ +/* eslint-disable no-magic-numbers */ +import Style from 'ol/style/Style'; +import { Extent } from 'ol/extent'; +import getWrappedTextWithFontSize from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; +import getCoverStyles from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; + +jest.mock('../text/getWrappedTextWithFontSize'); +jest.mock('../text/getTextStyle'); +jest.mock('../../../../../../../utils/map/latLngToPoint'); + +describe('getCoverStyles', () => { + it('should return cover and text styles based on the provided parameters', () => { + const coverStyle = new Style(); + const largestExtent: Extent = [10, 10, 50, 50]; + const text = 'Sample Text'; + const scale = 1; + const zIndex = 5; + const mapSize = { + width: 1000, + height: 800, + minZoom: 1, + maxZoom: 9, + tileSize: DEFAULT_TILE_SIZE, + }; + + (latLngToPoint as jest.Mock).mockImplementation(([lat, lng], size) => ({ + x: (lng * size.width) / 100, + y: (lat * size.height) / 100, + })); + + (getWrappedTextWithFontSize as jest.Mock).mockReturnValue({ + text: 'Sample\nText', + fontSize: 12, + }); + + const mockTextStyle = new Style(); + (getTextStyle as jest.Mock).mockReturnValue(mockTextStyle); + + const result = getCoverStyles(coverStyle, largestExtent, text, scale, zIndex, mapSize); + + expect(result).toHaveLength(2); + expect(result[0]).toBe(coverStyle); + expect(coverStyle.getZIndex()).toBe(zIndex); + + expect(result[1]).toBe(mockTextStyle); + expect(getWrappedTextWithFontSize).toHaveBeenCalledWith({ + text, + maxWidth: expect.any(Number), + maxHeight: expect.any(Number), + }); + + expect(getTextStyle).toHaveBeenCalledWith({ + text: 'Sample\nText', + fontSize: 12, + color: '#000', + zIndex, + horizontalAlign: 'CENTER', + }); + }); + + it('should handle empty text gracefully', () => { + const coverStyle = new Style(); + const largestExtent: Extent = [10, 10, 50, 50]; + const text = ''; + const scale = 1; + const zIndex = 5; + const mapSize = { + width: 1000, + height: 800, + minZoom: 1, + maxZoom: 9, + tileSize: DEFAULT_TILE_SIZE, + }; + + (getWrappedTextWithFontSize as jest.Mock).mockReturnValue({ + text: '', + fontSize: 0, + }); + + const result = getCoverStyles(coverStyle, largestExtent, text, scale, zIndex, mapSize); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(coverStyle); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce072c86cc472abb701f351e0d09a4c7dc2e1e3b --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getCoverStyles.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-magic-numbers */ +import Style from 'ol/style/Style'; +import { Extent, getCenter } from 'ol/extent'; +import { toLonLat } from 'ol/proj'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; +import getWrappedTextWithFontSize from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize'; +import { Point } from 'ol/geom'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import { MapSize } from '@/redux/map/map.types'; + +export default function getCoverStyles( + coverStyle: Style, + largestExtent: Extent, + text: string, + scale: number, + zIndex: number, + mapSize: MapSize, +): Array<Style> { + const styles: Array<Style> = []; + coverStyle.setZIndex(zIndex); + styles.push(coverStyle); + + if (text) { + const [lng1, lat1] = toLonLat([largestExtent[0], largestExtent[1]]); + const [lng2, lat2] = toLonLat([largestExtent[2], largestExtent[3]]); + const point1 = latLngToPoint([lat1, lng1], mapSize); + const point2 = latLngToPoint([lat2, lng2], mapSize); + const maxWidth = point2.x - point1.x; + const maxHeight = Math.abs(Math.abs(point2.y) - Math.abs(point1.y)); + const { text: brokenText, fontSize: calculatedFontSize } = getWrappedTextWithFontSize({ + text, + maxWidth: maxWidth * scale * 0.9, + maxHeight: maxHeight * scale * 0.9, + }); + const center = getCenter(largestExtent); + const textGeometry = new Point([center[0], center[1]]); + + const textStyle = getTextStyle({ + text: brokenText.trim(), + fontSize: calculatedFontSize, + color: '#000', + zIndex, + horizontalAlign: 'CENTER', + }); + textStyle.setGeometry(textGeometry); + styles.push(textStyle); + } + + return styles; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2cfaa2436cd3d07f407c765b96b303702726923f --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.test.ts @@ -0,0 +1,49 @@ +/* eslint-disable no-magic-numbers */ +import getWrappedTextWithFontSize from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize'; + +describe('getWrappedTextWithFontSize', () => { + it('should return a wrapped text and font size for this text when maxWidth is limited and maxHeight is unlimited', () => { + const text = 'Wrapped text with font size test'; + const maxWidth = 15; + const maxHeight = 9999; + + const { text: wrappedText, fontSize } = getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + }); + + expect(wrappedText.trim()).toEqual('Wrapped text\nwith font size\ntest'); + expect(fontSize).toEqual(12); + }); + + it('should return a wrapped text and font size for this text when maxWidth is unlimited and maxHeight is limited', () => { + const text = 'Wrapped text with font size test'; + const maxWidth = 9999; + const maxHeight = 9; + + const { text: wrappedText, fontSize } = getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + }); + + expect(wrappedText.trim()).toEqual('Wrapped text with font size test'); + expect(fontSize).toEqual(4); + }); + + it('should return a wrapped text and font size for this text when maxWidth is limited and maxHeight is limited', () => { + const text = 'Wrapped text with font size test'; + const maxWidth = 20; + const maxHeight = 9; + + const { text: wrappedText, fontSize } = getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + }); + + expect(wrappedText.trim()).toEqual('Wrapped text with\nfont size test'); + expect(fontSize).toEqual(3); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.ts new file mode 100644 index 0000000000000000000000000000000000000000..90e1e00f82d5898f92176dcdc03a8224231e07b7 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getWrappedTextWithFontSize.ts @@ -0,0 +1,59 @@ +/* eslint-disable no-magic-numbers */ +export default function getWrappedTextWithFontSize({ + text, + maxWidth, + maxHeight, + minFontSize = 1, + maxFontSize = 12, +}: { + text: string; + maxWidth: number; + maxHeight: number; + minFontSize?: number; + maxFontSize?: number; +}): { text: string; fontSize: number } { + const result = { + text, + fontSize: maxFontSize, + }; + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) { + return result; + } + + let resultFontSize = maxFontSize; + let resultText = text; + for (let testFontSize = maxFontSize; testFontSize >= minFontSize; testFontSize -= 1) { + context.font = `${testFontSize}px Arial`; + let currentLine = ''; + let splittedText = ''; + resultFontSize = testFontSize; + text.split(' ').forEach((word: string) => { + const testLine = currentLine ? `${currentLine} ${word}` : word; + const testWidth = context.measureText(testLine).width; + + if (testWidth > maxWidth && currentLine) { + splittedText += `\n${currentLine}`; + currentLine = word; + } else { + currentLine = testLine; + } + }); + if (currentLine) { + splittedText += `\n${currentLine}`; + } + const lines = splittedText.split('\n'); + const maxLineWidth = lines.reduce( + (maxFoundWidth, line) => Math.max(maxFoundWidth, context.measureText(line).width), + 0, + ); + if (maxLineWidth <= maxWidth && testFontSize * lines.length <= maxHeight) { + resultText = splittedText; + break; + } + } + result.text = resultText; + result.fontSize = resultFontSize; + return result; +}