From 692843631bc548212a2e1fbda17a33e71c96f04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 28 Oct 2024 10:32:44 +0100 Subject: [PATCH] Resolve MIN-39 "Feat/ glyphs" --- docs/types/BioEntity.md | 5 +- docs/types/Chemical.md | 5 +- docs/types/Drug.md | 5 +- .../BioEntitiesAccordion.component.test.tsx | 4 +- .../reactionsLayer/useOlMapReactionsLayer.ts | 135 +++++++++-------- .../utils/shapes/Compartment.ts | 137 ++++++++++++++++++ .../utils/shapes/elements/BaseMultiPolygon.ts | 8 +- .../shapes/elements/CompartmentCircle.test.ts | 6 +- .../elements/CompartmentPathway.test.ts | 6 +- .../shapes/elements/CompartmentSquare.test.ts | 6 +- .../utils/shapes/elements/Glyph.test.ts | 75 ++++++++++ .../utils/shapes/elements/Glyph.ts | 123 ++++++++++++++++ .../utils/shapes/elements/MapElement.test.ts | 6 +- .../Map/MapViewer/utils/useOlMap.ts | 8 + src/models/glyphSchema.ts | 1 + src/redux/apiPath.ts | 2 + src/utils/map/pointToLatLng.ts | 1 - 17 files changed, 454 insertions(+), 79 deletions(-) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts diff --git a/docs/types/BioEntity.md b/docs/types/BioEntity.md index faba904a..587d005d 100644 --- a/docs/types/BioEntity.md +++ b/docs/types/BioEntity.md @@ -162,9 +162,12 @@ "properties": { "file": { "type": "number" + }, + "id": { + "type": "number" } }, - "required": ["file"], + "required": ["file", "id"], "additionalProperties": false }, { diff --git a/docs/types/Chemical.md b/docs/types/Chemical.md index 61add593..c595975e 100644 --- a/docs/types/Chemical.md +++ b/docs/types/Chemical.md @@ -230,9 +230,12 @@ "properties": { "file": { "type": "number" + }, + "id": { + "type": "number" } }, - "required": ["file"], + "required": ["file", "id"], "additionalProperties": false }, { diff --git a/docs/types/Drug.md b/docs/types/Drug.md index f7b21990..538edf56 100644 --- a/docs/types/Drug.md +++ b/docs/types/Drug.md @@ -211,9 +211,12 @@ "properties": { "file": { "type": "number" + }, + "id": { + "type": "number" } }, - "required": ["file"], + "required": ["file", "id"], "additionalProperties": false }, { diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx index 5067368d..33686dce 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx @@ -96,8 +96,8 @@ describe('BioEntitiesAccordion - component', () => { }); expect(screen.getByText('Content (10)')).toBeInTheDocument(); - expect(screen.getByText('Core PD map (4)')).toBeInTheDocument(); - expect(screen.getByText('Histamine signaling (4)')).toBeInTheDocument(); + expect(screen.getByText('Core PD map (6)')).toBeInTheDocument(); + expect(screen.getByText('Histamine signaling (2)')).toBeInTheDocument(); expect(screen.getByText('PRKN substrates (2)')).toBeInTheDocument(); }); 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 cf9262f8..b4bce8d7 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -19,6 +19,7 @@ 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'; export const useOlMapReactionsLayer = ({ @@ -37,48 +38,70 @@ export const useOlMapReactionsLayer = ({ const shapes = useSelector(bioShapesSelector); const lineTypes = useSelector(lineTypesSelector); - const elements: Array<MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway> = - useMemo(() => { - if (!modelElements || !shapes) return []; + const elements: Array< + MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph + > = useMemo(() => { + if (!modelElements || !shapes) { + return []; + } - const validElements: Array< - MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway - > = []; - modelElements.content.forEach((element: ModelElement) => { - const shape = shapes.find(bioShape => bioShape.sboTerm === element.sboTerm); - if (shape) { - validElements.push( - new MapElement({ - shapes: shape.shapes, - 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, - }), - ); - } else if (element.sboTerm === 'SBO:0000290') { - const compartmentProps = { + 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 = { + 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 shape = shapes.find(bioShape => bioShape.sboTerm === element.sboTerm); + if (shape) { + validElements.push( + new MapElement({ + shapes: shape.shapes, x: element.x, y: element.y, nameX: element.nameX, @@ -88,33 +111,31 @@ export const useOlMapReactionsLayer = ({ width: element.width, height: element.height, zIndex: element.z, - innerWidth: element.innerWidth, - outerWidth: element.outerWidth, - thickness: element.thickness, + 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, - }; - 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 validElements; - }, [modelElements, shapes, pointToProjection, mapInstance, lineTypes]); + modifications: element.modificationResidues, + lineTypes, + bioShapes: shapes, + }), + ); + } + }); + return validElements; + }, [modelElements, shapes, pointToProjection, mapInstance, lineTypes]); const features = useMemo(() => { - return elements.map(element => element.multiPolygonFeature); + return elements.map(element => element.feature); }, [elements]); const vectorSource = useMemo(() => { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment.ts new file mode 100644 index 00000000..bdf60269 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment.ts @@ -0,0 +1,137 @@ +/* eslint-disable no-magic-numbers */ +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; +import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon'; +import { Coordinate } from 'ol/coordinate'; +import Polygon from 'ol/geom/Polygon'; +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 getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; +import { MapInstance } from '@/types/map'; +import { Color } from '@/types/models'; + +export interface CompartmentProps { + x: number; + y: number; + width: number; + height: number; + thickness: number; + outerWidth: number; + innerWidth: number; + zIndex: number; + text: string; + fontSize: number; + nameX: number; + nameY: number; + nameWidth: number; + nameHeight: number; + fontColor: Color; + nameVerticalAlign: VerticalAlign; + nameHorizontalAlign: HorizontalAlign; + fillColor: Color; + borderColor: Color; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; +} + +export default abstract class Compartment extends BaseMultiPolygon { + outerCoords: Array<Coordinate> = []; + + innerCoords: Array<Coordinate> = []; + + outerWidth: number; + + innerWidth: number; + + thickness: number; + + constructor({ + x, + y, + width, + height, + thickness, + outerWidth, + innerWidth, + zIndex, + text, + fontSize, + nameX, + nameY, + nameWidth, + nameHeight, + fontColor, + nameVerticalAlign, + nameHorizontalAlign, + fillColor, + borderColor, + pointToProjection, + mapInstance, + }: CompartmentProps) { + super({ + x, + y, + width, + height, + zIndex, + text, + fontSize, + nameX, + nameY, + nameWidth, + nameHeight, + fontColor, + nameVerticalAlign, + nameHorizontalAlign, + fillColor, + borderColor, + pointToProjection, + }); + this.outerWidth = outerWidth; + this.innerWidth = innerWidth; + this.thickness = thickness; + this.getCompartmentCoords(); + this.createPolygons(); + this.drawText(); + this.drawMultiPolygonFeature(mapInstance); + } + + protected abstract getCompartmentCoords(): void; + + protected createPolygons(): void { + const framePolygon = new Polygon([this.outerCoords, this.innerCoords]); + this.styles.push( + new Style({ + geometry: framePolygon, + fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 128 }) }), + zIndex: this.zIndex, + }), + ); + this.polygons.push(framePolygon); + + const outerPolygon = new Polygon([this.outerCoords]); + this.styles.push( + new Style({ + geometry: outerPolygon, + stroke: getStroke({ color: rgbToHex(this.borderColor), width: this.outerWidth }), + zIndex: this.zIndex, + }), + ); + this.polygons.push(outerPolygon); + + const innerPolygon = new Polygon([this.innerCoords]); + this.styles.push( + new Style({ + geometry: innerPolygon, + stroke: getStroke({ color: rgbToHex(this.borderColor), width: this.innerWidth }), + fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 9 }) }), + zIndex: this.zIndex, + }), + ); + this.polygons.push(innerPolygon); + } +} 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 ef477137..1852806c 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -73,7 +73,7 @@ export default abstract class BaseMultiPolygon { polygonsTexts: Array<string> = []; - multiPolygonFeature: Feature = new Feature(); + feature: Feature = new Feature(); pointToProjection: UsePointToProjectionResult; @@ -145,21 +145,21 @@ export default abstract class BaseMultiPolygon { } protected drawMultiPolygonFeature(mapInstance: MapInstance): void { - this.multiPolygonFeature = new Feature({ + this.feature = new Feature({ geometry: new MultiPolygon(this.polygons), getTextScale: (resolution: number): number => { const maxZoom = mapInstance?.getView().getMaxZoom(); if (maxZoom) { const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); if (minResolution) { - return Math.round((minResolution / resolution) * 100) / 100; + return minResolution / resolution; } } return 1; }, }); - this.multiPolygonFeature.setStyle(this.styleFunction.bind(this)); + this.feature.setStyle(this.styleFunction.bind(this)); } protected styleFunction(feature: FeatureLike, resolution: number): Style | Array<Style> | void { 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 47c47d38..4c0a4aff 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 @@ -101,13 +101,13 @@ describe('MapElement', () => { const multiPolygon = new CompartmentCircle(props); expect(multiPolygon.polygons.length).toBe(4); - expect(multiPolygon.multiPolygonFeature).toBeInstanceOf(Feature); - expect(multiPolygon.multiPolygonFeature.getGeometry()).toBeInstanceOf(MultiPolygon); + expect(multiPolygon.feature).toBeInstanceOf(Feature); + expect(multiPolygon.feature.getGeometry()).toBeInstanceOf(MultiPolygon); }); it('should apply correct styles to the feature', () => { const multiPolygon = new CompartmentCircle(props); - const feature = multiPolygon.multiPolygonFeature; + const { feature } = multiPolygon; const style = feature.getStyleFunction()?.call(multiPolygon, feature, 1); 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 8eb98121..c47c01b5 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 @@ -99,13 +99,13 @@ describe('MapElement', () => { const multiPolygon = new CompartmentPathway(props); expect(multiPolygon.polygons.length).toBe(2); - expect(multiPolygon.multiPolygonFeature).toBeInstanceOf(Feature); - expect(multiPolygon.multiPolygonFeature.getGeometry()).toBeInstanceOf(MultiPolygon); + expect(multiPolygon.feature).toBeInstanceOf(Feature); + expect(multiPolygon.feature.getGeometry()).toBeInstanceOf(MultiPolygon); }); it('should apply correct styles to the feature', () => { const multiPolygon = new CompartmentPathway(props); - const feature = multiPolygon.multiPolygonFeature; + const { feature } = multiPolygon; const style = feature.getStyleFunction()?.call(multiPolygon, feature, 1); 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 5fd6ac55..a368e292 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 @@ -101,13 +101,13 @@ describe('MapElement', () => { const multiPolygon = new CompartmentSquare(props); expect(multiPolygon.polygons.length).toBe(4); - expect(multiPolygon.multiPolygonFeature).toBeInstanceOf(Feature); - expect(multiPolygon.multiPolygonFeature.getGeometry()).toBeInstanceOf(MultiPolygon); + expect(multiPolygon.feature).toBeInstanceOf(Feature); + expect(multiPolygon.feature.getGeometry()).toBeInstanceOf(MultiPolygon); }); it('should apply correct styles to the feature', () => { const multiPolygon = new CompartmentSquare(props); - const feature = multiPolygon.multiPolygonFeature; + const { feature } = multiPolygon; const style = feature.getStyleFunction()?.call(multiPolygon, feature, 1); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts new file mode 100644 index 00000000..e0235269 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-magic-numbers */ +import { Feature, Map, View } from 'ol'; +import { Style, Icon } from 'ol/style'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import Glyph, { + GlyphProps, +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; +import { MapInstance } from '@/types/map'; +import Polygon from 'ol/geom/Polygon'; +import { BASE_NEW_API_URL } from '@/constants'; + +describe('Glyph', () => { + let props: GlyphProps; + let glyph: Glyph; + let mapInstance: MapInstance; + let pointToProjectionMock: jest.MockedFunction<UsePointToProjectionResult>; + + beforeEach(() => { + const dummyElement = document.createElement('div'); + mapInstance = new Map({ + target: dummyElement, + view: new View({ + zoom: 5, + minZoom: 3, + maxZoom: 7, + }), + }); + + pointToProjectionMock = jest.fn().mockReturnValue([10, 20]); + + props = { + id: 1, + x: 10, + y: 20, + width: 32, + height: 32, + zIndex: 1, + pointToProjection: pointToProjectionMock, + mapInstance, + }; + glyph = new Glyph(props); + }); + + it('should initialize with correct feature and style properties', () => { + expect(glyph.feature).toBeInstanceOf(Feature); + const geometry = glyph.feature.getGeometry(); + expect(geometry).toBeInstanceOf(Polygon); + expect(geometry?.getCoordinates()).toEqual([ + [ + [10, 20], + [10, 20], + [10, 20], + [10, 20], + [10, 20], + ], + ]); + + expect(glyph.style).toBeInstanceOf(Style); + const image = glyph.style.getImage() as Icon; + expect(image).toBeInstanceOf(Icon); + expect(image.getSrc()).toBe(`${BASE_NEW_API_URL}projects/pdmap_appu_test/glyphs/1/fileContent`); + }); + + it('should scale image based on map resolution', () => { + const getImageScale = glyph.feature.get('getImageScale'); + const getAnchorAndCoords = glyph.feature.get('getAnchorAndCoords'); + if (mapInstance) { + const resolution = mapInstance + .getView() + .getResolutionForZoom(mapInstance.getView().getMaxZoom()); + expect(getImageScale(resolution)).toBe(1); + expect(getAnchorAndCoords()).toEqual({ anchor: [0, 0], coords: [0, 0] }); + } + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts new file mode 100644 index 00000000..0d497ac9 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts @@ -0,0 +1,123 @@ +/* eslint-disable no-magic-numbers */ +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Feature } from 'ol'; +import Style from 'ol/style/Style'; +import Icon from 'ol/style/Icon'; +import { FeatureLike } from 'ol/Feature'; +import { MapInstance } from '@/types/map'; +import { apiPath } from '@/redux/apiPath'; +import { BASE_NEW_API_URL } from '@/constants'; +import Polygon from 'ol/geom/Polygon'; +import { Point } from 'ol/geom'; +import { Coordinate } from 'ol/coordinate'; + +export type GlyphProps = { + id: number; + x: number; + y: number; + width: number; + height: number; + zIndex: number; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; +}; + +export default class Glyph { + feature: Feature<Polygon>; + + style: Style; + + width: number; + + height: number; + + x: number; + + y: number; + + widthOnMap: number; + + heightOnMap: number; + + pixelRatio: number = 1; + + pointToProjection: UsePointToProjectionResult; + + constructor({ id, x, y, width, height, zIndex, pointToProjection, mapInstance }: GlyphProps) { + this.width = width; + this.height = height; + this.x = x; + this.y = y; + this.pointToProjection = pointToProjection; + const point1 = this.pointToProjection({ x: 0, y: 0 }); + const point2 = this.pointToProjection({ x: this.width, y: this.height }); + this.widthOnMap = Math.abs(point2[0] - point1[0]); + this.heightOnMap = Math.abs(point2[1] - point1[1]); + const minResolution = mapInstance?.getView().getMinResolution(); + if (minResolution) { + this.pixelRatio = this.widthOnMap / minResolution / this.width; + } + const polygon = new Polygon([ + [ + pointToProjection({ x, y }), + pointToProjection({ x: x + width, y }), + pointToProjection({ x: x + width, y: y + height }), + pointToProjection({ x, y: y + height }), + pointToProjection({ x, y }), + ], + ]); + const iconFeature = new Feature({ + geometry: polygon, + getImageScale: (resolution: number): number => { + if (mapInstance) { + return mapInstance.getView().getMinResolution() / resolution; + } + return 1; + }, + getAnchorAndCoords: (): { anchor: Array<number>; coords: Coordinate } => { + const center = mapInstance?.getView().getCenter(); + let anchorX = 0; + let anchorY = 0; + if (center) { + anchorX = + (center[0] - this.pointToProjection({ x: this.x, y: this.y })[0]) / this.widthOnMap; + anchorY = + -(center[1] - this.pointToProjection({ x: this.x, y: this.y })[1]) / this.heightOnMap; + } + return { anchor: [anchorX, anchorY], coords: center || [0, 0] }; + }, + }); + this.style = new Style({ + image: new Icon({ + anchor: [0, 0], + src: `${BASE_NEW_API_URL}${apiPath.getGlyphImage(id)}`, + size: [width, height], + }), + zIndex, + }); + iconFeature.setStyle(this.styleFunction.bind(this)); + this.feature = iconFeature; + } + + protected styleFunction(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const getImageScale = feature.get('getImageScale'); + const getAnchorAndCoords = feature.get('getAnchorAndCoords'); + let imageScale = 1; + let anchor = [0, 0]; + let coords = this.pointToProjection({ x: this.x, y: this.y }); + if (getImageScale instanceof Function) { + imageScale = getImageScale(resolution); + } + if (getAnchorAndCoords instanceof Function) { + const anchorAndCoords = getAnchorAndCoords(); + anchor = anchorAndCoords.anchor; + coords = anchorAndCoords.coords; + } + if (this.style.getImage()) { + this.style.getImage()?.setScale(imageScale * this.pixelRatio); + (this.style.getImage() as Icon).setAnchor(anchor); + this.style.setGeometry(new Point(coords)); + } + return this.style; + } +} 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 51aaeb65..5b38d585 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 @@ -93,13 +93,13 @@ describe('MapElement', () => { const multiPolygon = new MapElement(props); expect(multiPolygon.polygons.length).toBe(2); - expect(multiPolygon.multiPolygonFeature).toBeInstanceOf(Feature); - expect(multiPolygon.multiPolygonFeature.getGeometry()).toBeInstanceOf(MultiPolygon); + expect(multiPolygon.feature).toBeInstanceOf(Feature); + expect(multiPolygon.feature.getGeometry()).toBeInstanceOf(MultiPolygon); }); it('should apply correct styles to the feature', () => { const multiPolygon = new MapElement(props); - const feature = multiPolygon.multiPolygonFeature; + const { feature } = multiPolygon; const style = feature.getStyleFunction()?.call(multiPolygon, feature, 1); diff --git a/src/components/Map/MapViewer/utils/useOlMap.ts b/src/components/Map/MapViewer/utils/useOlMap.ts index f65c7f04..e80f32ef 100644 --- a/src/components/Map/MapViewer/utils/useOlMap.ts +++ b/src/components/Map/MapViewer/utils/useOlMap.ts @@ -7,6 +7,7 @@ import { useOlMapVectorLayers } from '@/components/Map/MapViewer/MapViewerVector import LayerGroup from 'ol/layer/Group'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { vectorRenderingSelector } from '@/redux/models/models.selectors'; +import { defaults, MouseWheelZoom } from 'ol/interaction'; import { useOlMapLayers } from './config/useOlMapLayers'; import { useOlMapView } from './config/useOlMapView'; import { useOlMapListeners } from './listeners/useOlMapListeners'; @@ -48,6 +49,13 @@ export const useOlMap: UseOlMap = ({ target } = {}) => { } const map = new Map({ + interactions: defaults({ + mouseWheelZoom: false, + }).extend([ + new MouseWheelZoom({ + duration: 0, + }), + ]), target: target || mapRef.current, }); diff --git a/src/models/glyphSchema.ts b/src/models/glyphSchema.ts index ed69e814..eedb213a 100644 --- a/src/models/glyphSchema.ts +++ b/src/models/glyphSchema.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; export const glyphSchema = z.object({ + id: z.number(), file: z.number(), }); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index fc94e5d7..0c8b1125 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -62,6 +62,8 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/ovals/`, getLayerLines: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`, + getGlyphImage: (glyphId: number): string => + `projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`, getChemicalsStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`, getAllOverlaysByProjectIdQuery: ( diff --git a/src/utils/map/pointToLatLng.ts b/src/utils/map/pointToLatLng.ts index cf8a6e58..5c74e704 100644 --- a/src/utils/map/pointToLatLng.ts +++ b/src/utils/map/pointToLatLng.ts @@ -24,7 +24,6 @@ export const pointToLngLat = (point: Point, mapSize?: MapSize): LatLng => { if (!isMapSizeValid || !mapSize) { return LATLNG_FALLBACK; } - const { x: xOffset, y: yOffset } = getPointOffset(point, mapSize); const pixelsPerLonDegree = mapSize.tileSize / FULL_CIRCLE_DEGREES; const pixelsPerLonRadian = mapSize.tileSize / (2 * Math.PI); -- GitLab