diff --git a/docs/types/BioEntity.md b/docs/types/BioEntity.md index faba904a6d2d79970ad823787f8cdfcaf8dcf40e..587d005dc0a12f16b38b90e4b70370c673577ab9 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 61add5938186e8ca9789714a847e79f08c99e6b0..c595975e26bce469e4e6743d0cfea9ad35a474e7 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 f7b21990191335c10d63a42f6a8a9fd0e75e1a81..538edf56303c796466366c5063b3dc6884a2ec8c 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 5067368d3757c24aefc2457d3e0f28d7e9c18fe0..33686dced99968f69a96162f98bc1546b749b0b4 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 cf9262f861c4ff75d07427cb83819edff836dc15..b4bce8d7982fe4d1a52e4c7277d340c3b6f33b91 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 0000000000000000000000000000000000000000..bdf6026934bf9f8b697885cdd0feb391fcd1f5ad --- /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 ef477137f87a54fa0a010c24eb3769624f22f6e8..1852806c020ed102c5bae9743a8a935086475311 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 47c47d389b5aa9ecc893045687eb015171ce62ea..4c0a4aff44904869a45a28ffc97efda41fa235ad 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 8eb98121453483ad0285984b036d836b6174413f..c47c01b5d9ab60c0ae118558e383f80434018286 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 5fd6ac55e3334d2f42a015635c97fa281f4144e3..a368e292ba6429c0195f071ee4bc19df78044bd2 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 0000000000000000000000000000000000000000..e0235269d1d38a2002ceca0cfd41b4a951528dd2 --- /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 0000000000000000000000000000000000000000..0d497ac9641f91e3cb1883f48a22661673f8c078 --- /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 51aaeb6572a795ed8e191fd4590e03674f5f3c13..5b38d5851545cf5cfc6412a2a37d3b6af0a69102 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 f65c7f04551ba952e7be17cf396a0fa4a1edb2d7..e80f32eff32904a0b78befbf7b064fde37bf320d 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 ed69e8141e58fbe4002bb5a614fd3e673329377c..eedb213a85e069120b3992a3cc62f271e00dbd59 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 fc94e5d757ee08819957f28410df92faebf59ee7..0c8b1125bf67d4a8ad2330eb24dec4d555cdae1a 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 cf8a6e58e8bad0bee61593660b332c9b8a95760c..5c74e70432cfb7c626184d82d4342e848e853412 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);