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 b4bce8d7982fe4d1a52e4c7277d340c3b6f33b91..66ddd44856ee26ccc4b789ec09e1a5cddf894db5 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -6,7 +6,11 @@ import { useEffect, useMemo } from 'react'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement'; import { useSelector } from 'react-redux'; -import { bioShapesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors'; +import { + arrowTypesSelector, + bioShapesSelector, + lineTypesSelector, +} from '@/redux/shapes/shapes.selectors'; import { MapInstance } from '@/types/map'; import { HorizontalAlign, @@ -21,6 +25,9 @@ import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/ import { ModelElement } from '@/types/models'; import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; +import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction'; +import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors'; +import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -29,14 +36,32 @@ export const useOlMapReactionsLayer = ({ }): VectorLayer<VectorSource<Feature>> => { const dispatch = useAppDispatch(); const modelElements = useSelector(modelElementsSelector); + const modelReactions = useSelector(newReactionsDataSelector); const currentModelId = useSelector(currentModelIdSelector); useEffect(() => { dispatch(getModelElements(currentModelId)); + dispatch(getNewReactions(currentModelId)); }, [currentModelId, dispatch]); const pointToProjection = usePointToProjection(); const shapes = useSelector(bioShapesSelector); const lineTypes = useSelector(lineTypesSelector); + const arrowTypes = useSelector(arrowTypesSelector); + + const reactions = useMemo(() => { + return modelReactions.map(reaction => { + const reactionObject = new Reaction({ + line: reaction.line, + products: reaction.products, + reactants: reaction.reactants, + zIndex: reaction.z, + lineTypes, + arrowTypes, + pointToProjection, + }); + return reactionObject.features; + }); + }, [arrowTypes, lineTypes, modelReactions, pointToProjection]); const elements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph @@ -135,8 +160,10 @@ export const useOlMapReactionsLayer = ({ }, [modelElements, shapes, pointToProjection, mapInstance, lineTypes]); const features = useMemo(() => { - return elements.map(element => element.feature); - }, [elements]); + const reactionsFeatures = reactions.flat(); + const elementsFeatures = elements.map(element => element.feature); + return [...reactionsFeatures, ...elementsFeatures]; + }, [elements, reactions]); const vectorSource = useMemo(() => { return new VectorSource({ 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 4c0a4aff44904869a45a28ffc97efda41fa235ad..70fb9ce7bc355513eb4d16875dfb1ab618962962 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 @@ -3,7 +3,7 @@ import { Feature, Map } from 'ol'; import { Fill, Style, Text } from 'ol/style'; import { Polygon, MultiPolygon } from 'ol/geom'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; -import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon'; +import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; @@ -20,7 +20,7 @@ import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shap jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); -jest.mock('./getMultiPolygon'); +jest.mock('./getShapePolygon'); jest.mock('../style/getStroke'); jest.mock('../coords/getEllipseCoords'); jest.mock('../style/getFill'); @@ -78,7 +78,7 @@ describe('MapElement', () => { }), ); (getTextCoords as jest.Mock).mockReturnValue([10, 10]); - (getMultiPolygon as jest.Mock).mockReturnValue([ + (getShapePolygon as jest.Mock).mockReturnValue( new Polygon([ [ [0, 0], @@ -86,7 +86,7 @@ describe('MapElement', () => { [2, 2], ], ]), - ]); + ); (getStroke as jest.Mock).mockReturnValue(new Style()); (getFill as jest.Mock).mockReturnValue(new Style()); (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); 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 c47c01b5d9ab60c0ae118558e383f80434018286..caa015c610e4c6c65b282acae535e5cf462899bd 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 @@ -3,7 +3,7 @@ import { Feature, Map } from 'ol'; import { Fill, Style, Text } from 'ol/style'; import { Polygon, MultiPolygon } from 'ol/geom'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; -import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon'; +import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; @@ -20,7 +20,7 @@ import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shap jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); -jest.mock('./getMultiPolygon'); +jest.mock('./getShapePolygon'); jest.mock('../style/getStroke'); jest.mock('../coords/getEllipseCoords'); jest.mock('../style/getFill'); @@ -76,7 +76,7 @@ describe('MapElement', () => { }), ); (getTextCoords as jest.Mock).mockReturnValue([10, 10]); - (getMultiPolygon as jest.Mock).mockReturnValue([ + (getShapePolygon as jest.Mock).mockReturnValue( new Polygon([ [ [0, 0], @@ -84,7 +84,7 @@ describe('MapElement', () => { [2, 2], ], ]), - ]); + ); (getStroke as jest.Mock).mockReturnValue(new Style()); (getFill as jest.Mock).mockReturnValue(new Style()); (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); 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 a368e292ba6429c0195f071ee4bc19df78044bd2..17bc27013654817113e830a393c49269ec8eccd9 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 @@ -1,9 +1,8 @@ /* eslint-disable no-magic-numbers */ import { Feature, Map } from 'ol'; import { Fill, Style, Text } from 'ol/style'; -import { Polygon, MultiPolygon } from 'ol/geom'; +import { MultiPolygon } from 'ol/geom'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; -import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon'; import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; @@ -19,7 +18,6 @@ import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; jest.mock('../text/getTextStyle'); -jest.mock('./getMultiPolygon'); jest.mock('../text/getTextCoords'); jest.mock('../style/getStroke'); jest.mock('../coords/getPolygonCoords'); @@ -78,15 +76,6 @@ describe('MapElement', () => { }), ); (getTextCoords as jest.Mock).mockReturnValue([10, 10]); - (getMultiPolygon as jest.Mock).mockReturnValue([ - new Polygon([ - [ - [0, 0], - [1, 1], - [2, 2], - ], - ]), - ]); (getStroke as jest.Mock).mockReturnValue(new Style()); (getFill as jest.Mock).mockReturnValue(new Style()); (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); 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 5b38d5851545cf5cfc6412a2a37d3b6af0a69102..eddaf77da5a4bc09713ef1c1247e425ef5a6cbba 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 @@ -3,7 +3,7 @@ import { Feature, Map } from 'ol'; import { Fill, Style, Text } from 'ol/style'; import { Polygon, MultiPolygon } from 'ol/geom'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; -import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon'; +import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; @@ -16,10 +16,11 @@ import { BLACK_COLOR, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; +import { shapesFixture } from '@/models/fixtures/shapesFixture'; jest.mock('../text/getTextStyle'); jest.mock('../text/getTextCoords'); -jest.mock('./getMultiPolygon'); +jest.mock('./getShapePolygon'); jest.mock('../style/getStroke'); jest.mock('../style/getFill'); jest.mock('../style/rgbToHex'); @@ -38,7 +39,7 @@ describe('MapElement', () => { }), }); props = { - shapes: [], + shapes: shapesFixture, x: 0, y: 0, width: 100, @@ -75,7 +76,7 @@ describe('MapElement', () => { }), ); (getTextCoords as jest.Mock).mockReturnValue([10, 10]); - (getMultiPolygon as jest.Mock).mockReturnValue([ + (getShapePolygon as jest.Mock).mockReturnValue( new Polygon([ [ [0, 0], @@ -83,7 +84,7 @@ describe('MapElement', () => { [2, 2], ], ]), - ]); + ); (getStroke as jest.Mock).mockReturnValue(new Style()); (getFill as jest.Mock).mockReturnValue(new Style()); (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); @@ -92,7 +93,7 @@ describe('MapElement', () => { it('should initialize with correct default properties', () => { const multiPolygon = new MapElement(props); - expect(multiPolygon.polygons.length).toBe(2); + expect(multiPolygon.polygons.length).toBe(3); expect(multiPolygon.feature).toBeInstanceOf(Feature); expect(multiPolygon.feature.getGeometry()).toBeInstanceOf(MultiPolygon); }); 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 b9fc0551e99f00892cc0480b70a5b2e706310ea9..4dc8eebc89f334166598c01119111cc9f32ceb42 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -4,7 +4,6 @@ import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import Polygon from 'ol/geom/Polygon'; -import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon'; import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; import { BioShape, Color, LineType, Modification, Shape } from '@/types/models'; import { MapInstance } from '@/types/map'; @@ -20,6 +19,7 @@ import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/s import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; export type MapElementProps = { shapes: Array<Shape>; @@ -160,25 +160,26 @@ export default class MapElement extends BaseMultiPolygon { } drawModification(modification: Modification, shapes: Array<Shape>): void { - const multiPolygonModification = getMultiPolygon({ - x: modification.x, - y: modification.y, - width: modification.width, - height: modification.height, - shapes, - pointToProjection: this.pointToProjection, - mirror: modification.direction && modification.direction === 'RIGHT', - }); - this.polygons.push(...multiPolygonModification); - multiPolygonModification.forEach((polygon: Polygon) => { + shapes.forEach(shape => { + const modificationPolygon = getShapePolygon({ + shape, + x: modification.x, + y: modification.y, + width: modification.width, + height: modification.height, + pointToProjection: this.pointToProjection, + mirror: modification.direction && modification.direction === 'RIGHT', + }); const modificationStyle = new Style({ - geometry: polygon, + geometry: modificationPolygon, stroke: getStroke({ color: rgbToHex(modification.borderColor) }), fill: getFill({ color: rgbToHex(modification.fillColor) }), zIndex: modification.z, }); + this.polygons.push(modificationPolygon); this.styles.push(modificationStyle); }); + const modificationText = modification.stateAbbreviation ? modification.stateAbbreviation : modification.name; @@ -211,48 +212,46 @@ export default class MapElement extends BaseMultiPolygon { } drawActiveBorder(homodimerShift: number, homodimerOffset: number): void { - const activityBorderElement = getMultiPolygon({ - x: this.x + homodimerShift - 5, - y: this.y + homodimerShift - 5, - width: this.width - homodimerOffset + 10, - height: this.height - homodimerOffset + 10, - shapes: this.shapes, - pointToProjection: this.pointToProjection, - }); - activityBorderElement.forEach(polygon => { - this.styles.push( - getStyle({ - geometry: polygon, - fillColor: { rgb: 0, alpha: 0 }, - lineDash: [3, 5], - zIndex: this.zIndex, - }), - ); + this.shapes.forEach(shape => { + const activityBorderPolygon = getShapePolygon({ + shape, + x: this.x + homodimerShift - 5, + y: this.y + homodimerShift - 5, + width: this.width - homodimerOffset + 10, + height: this.height - homodimerOffset + 10, + pointToProjection: this.pointToProjection, + }); + const activityBorderStyle = getStyle({ + geometry: activityBorderPolygon, + fillColor: { rgb: 0, alpha: 0 }, + lineDash: [3, 5], + zIndex: this.zIndex, + }); + this.polygons.push(activityBorderPolygon); + this.styles.push(activityBorderStyle); }); - this.polygons.push(...activityBorderElement); } drawElementPolygon(homodimerShift: number, homodimerOffset: number): void { - const elementPolygon = getMultiPolygon({ - x: this.x + homodimerShift, - y: this.y + homodimerShift, - width: this.width - homodimerOffset, - height: this.height - homodimerOffset, - shapes: this.shapes, - pointToProjection: this.pointToProjection, - }); - elementPolygon.forEach(polygon => { - this.styles.push( - getStyle({ - geometry: polygon, - borderColor: this.borderColor, - fillColor: this.fillColor, - lineWidth: this.lineWidth, - lineDash: this.lineDash, - zIndex: this.zIndex, - }), - ); + this.shapes.forEach(shape => { + const elementPolygon = getShapePolygon({ + shape, + x: this.x + homodimerShift, + y: this.y + homodimerShift, + width: this.width - homodimerOffset, + height: this.height - homodimerOffset, + pointToProjection: this.pointToProjection, + }); + const elementStyle = getStyle({ + geometry: elementPolygon, + borderColor: this.borderColor, + fillColor: this.fillColor, + lineWidth: this.lineWidth, + lineDash: this.lineDash, + zIndex: this.zIndex, + }); + this.polygons.push(elementPolygon); + this.styles.push(elementStyle); }); - this.polygons.push(...elementPolygon); } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..f94790c7a4ba06ec06d4c859b4dfb8c19d95d4a6 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.test.ts @@ -0,0 +1,125 @@ +/* eslint-disable no-magic-numbers */ +import { Feature } from 'ol'; +import { Fill, Stroke, Style } from 'ol/style'; +import { Polygon, MultiPolygon } from 'ol/geom'; +import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import getArrowFeature from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature'; +import { ArrowType } from '@/types/models'; +import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; + +jest.mock('../style/getStyle'); +jest.mock('./getShapePolygon'); + +describe('getArrowFeature', () => { + const props = { + arrowTypes: [ + { + arrowType: 'FULL', + shapes: [ + { + type: 'POLYGON', + fill: false, + points: [ + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 0.0, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: null, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 100.0, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: null, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 7.612046748871326, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: 19.134171618254495, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 7.612046748871326, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: -19.134171618254495, + }, + { + type: 'REL_ABS_POINT', + absoluteX: 0.0, + absoluteY: 0.0, + relativeX: 100.0, + relativeY: 50.0, + relativeHeightForX: null, + relativeWidthForY: null, + }, + ], + }, + ], + } as ArrowType, + ], + arrow: { length: 15, arrowType: 'FULL', angle: 2.74, lineType: 'SOLID' }, + x: 0, + y: 0, + zIndex: 1, + rotation: 1, + lineWidth: 1, + color: BLACK_COLOR, + pointToProjection: jest.fn(() => [10, 10]), + }; + const polygon = new Polygon([ + [ + [0, 0], + [1, 1], + [2, 2], + ], + ]); + + beforeEach(() => { + (getStyle as jest.Mock).mockReturnValue( + new Style({ + geometry: polygon, + stroke: new Stroke({}), + fill: new Fill({}), + }), + ); + (getShapePolygon as jest.Mock).mockReturnValue( + new Polygon([ + [ + [0, 0], + [1, 1], + [2, 2], + ], + ]), + ); + }); + + it('should return arrow feature', () => { + const arrowFeature = getArrowFeature(props); + + expect(arrowFeature).toBeInstanceOf(Feature<MultiPolygon>); + }); + + it('should handle missing arrowType in props', () => { + const invalidProps = { ...props, arrowTypes: [] }; + expect(() => getArrowFeature(invalidProps)).not.toThrow(); + }); + + it('should call getStyle', () => { + getArrowFeature(props); + expect(getStyle).toBeCalled(); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d7e53d60d3db92f920399748a88e41eb0c7514b --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature.ts @@ -0,0 +1,63 @@ +/* eslint-disable no-magic-numbers */ +import Style from 'ol/style/Style'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import { Feature } from 'ol'; +import { MultiPolygon } from 'ol/geom'; +import { Arrow, ArrowType, Color } from '@/types/models'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; +import Polygon from 'ol/geom/Polygon'; + +export default function getArrowFeature({ + arrowTypes, + arrow, + x, + y, + zIndex, + rotation, + lineWidth, + color, + pointToProjection, +}: { + arrowTypes: Array<ArrowType>; + arrow: Arrow; + x: number; + y: number; + zIndex: number; + rotation: number; + lineWidth: number; + color: Color; + pointToProjection: UsePointToProjectionResult; +}): undefined | Feature<MultiPolygon> { + const arrowShapes = arrowTypes.find(arrowType => arrowType.arrowType === arrow.arrowType)?.shapes; + if (!arrowShapes) { + return undefined; + } + const arrowStyles: Array<Style> = []; + const arrowPolygons: Array<Polygon> = []; + arrowShapes.forEach(shape => { + const arrowPolygon = getShapePolygon({ + shape, + x, + y: y - arrow.length / 2, + width: arrow.length, + height: arrow.length, + pointToProjection, + }); + const style = getStyle({ + geometry: arrowPolygon, + zIndex, + borderColor: color, + fillColor: shape.fill === false ? undefined : color, + lineWidth, + }); + arrowPolygon.rotate(rotation, pointToProjection({ x, y })); + arrowStyles.push(style); + arrowPolygons.push(arrowPolygon); + }); + const arrowFeature = new Feature({ + geometry: new MultiPolygon(arrowPolygons), + }); + arrowFeature.setStyle(arrowStyles); + return arrowFeature; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.ts deleted file mode 100644 index d91c741abd1a4e477e3c5f6ed09a4fafad650b11..0000000000000000000000000000000000000000 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; -import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; -import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; -import Polygon from 'ol/geom/Polygon'; -import { Coordinate } from 'ol/coordinate'; -import { Shape } from '@/types/models'; -import getCentroid from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCentroid'; - -export default function getMultiPolygon({ - x, - y, - width, - height, - shapes, - pointToProjection, - mirror, -}: { - x: number; - y: number; - width: number; - height: number; - shapes: Array<Shape>; - pointToProjection: UsePointToProjectionResult; - mirror?: boolean; -}): Array<Polygon> { - return shapes.map(shape => { - let coords: Array<Coordinate> = []; - if (shape.type === 'POLYGON') { - coords = getPolygonCoords({ points: shape.points, x, y, height, width, pointToProjection }); - } else if (shape.type === 'ELLIPSE') { - coords = getEllipseCoords({ - x, - y, - center: shape.center, - radius: shape.radius, - height, - width, - pointToProjection, - }); - } - - if (mirror) { - const centroid = getCentroid(coords); - - coords = coords.map(coord => { - const mirroredX = 2 * centroid[0] - coord[0]; - - return [mirroredX, coord[1]]; - }); - } - - return new Polygon([coords]); - }); -} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.test.ts similarity index 87% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.test.ts index b4d697f3bfa5d3888af75c238ca1d2231716c803..0074f57be0512f7db5b60f44e7702a0deb85afb6 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.test.ts @@ -3,12 +3,12 @@ import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/s import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; import Polygon from 'ol/geom/Polygon'; import { Shape } from '@/types/models'; -import getMultiPolygon from './getMultiPolygon'; +import getShapePolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon'; jest.mock('../coords/getPolygonCoords'); jest.mock('../coords/getEllipseCoords'); -describe('getMultiPolygon', () => { +describe('getShapePolygon', () => { const mockPointToProjection = jest.fn(point => [point.x, point.y]); beforeEach(() => { @@ -75,15 +75,16 @@ describe('getMultiPolygon', () => { ]; (getPolygonCoords as jest.Mock).mockReturnValue(mockPolygonCoords); - const result = getMultiPolygon({ - x, - y, - width, - height, - shapes, - pointToProjection: mockPointToProjection, + const result = shapes.map(shape => { + return getShapePolygon({ + shape, + x, + y, + width, + height, + pointToProjection: mockPointToProjection, + }); }); - expect(result).toHaveLength(1); expect(result[0]).toBeInstanceOf(Polygon); expect(result[0].getCoordinates()).toEqual([mockPolygonCoords]); @@ -133,13 +134,15 @@ describe('getMultiPolygon', () => { ]; (getEllipseCoords as jest.Mock).mockReturnValue(mockEllipseCoords); - const result = getMultiPolygon({ - x, - y, - width, - height, - shapes, - pointToProjection: mockPointToProjection, + const result = shapes.map(shape => { + return getShapePolygon({ + shape, + x, + y, + width, + height, + pointToProjection: mockPointToProjection, + }); }); expect(result).toHaveLength(1); @@ -223,13 +226,15 @@ describe('getMultiPolygon', () => { (getPolygonCoords as jest.Mock).mockReturnValue(mockPolygonCoords); (getEllipseCoords as jest.Mock).mockReturnValue(mockEllipseCoords); - const result = getMultiPolygon({ - x, - y, - width, - height, - shapes, - pointToProjection: mockPointToProjection, + const result = shapes.map(shape => { + return getShapePolygon({ + shape, + x, + y, + width, + height, + pointToProjection: mockPointToProjection, + }); }); expect(result).toHaveLength(2); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f2a5280f8284c0dd5c73f0bf9c421319bb1eb78 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getShapePolygon.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-magic-numbers */ +import Polygon from 'ol/geom/Polygon'; +import { Coordinate } from 'ol/coordinate'; +import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; +import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; +import getCentroid from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCentroid'; +import { Shape } from '@/types/models'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; + +export default function getShapePolygon({ + shape, + x, + y, + width, + height, + mirror, + pointToProjection, +}: { + shape: Shape; + x: number; + y: number; + width: number; + height: number; + mirror?: boolean; + pointToProjection: UsePointToProjectionResult; +}): Polygon { + let coords: Array<Coordinate> = []; + if (shape.type === 'POLYGON') { + coords = getPolygonCoords({ points: shape.points, x, y, height, width, pointToProjection }); + } else if (shape.type === 'ELLIPSE') { + coords = getEllipseCoords({ + x, + y, + center: shape.center, + radius: shape.radius, + height, + width, + pointToProjection, + }); + } + + if (mirror) { + const centroid = getCentroid(coords); + + coords = coords.map(coord => { + const mirroredX = 2 * centroid[0] - coord[0]; + + return [mirroredX, coord[1]]; + }); + } + + return new Polygon([coords]); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index c6322fef57135c9667c283129883f050d1856580..a9adc4d4298e6f353262c251fc499fce19c64f15 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -1,13 +1,5 @@ /* eslint-disable no-magic-numbers */ -import { - Arrow, - ArrowType, - LayerLine, - LayerOval, - LayerRect, - LayerText, - LineType, -} from '@/types/models'; +import { ArrowType, LayerLine, LayerOval, LayerRect, LayerText, LineType } from '@/types/models'; import { MapInstance } from '@/types/map'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; @@ -22,10 +14,8 @@ import { HorizontalAlign, VerticalAlign, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; -import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon'; -import Style from 'ol/style/Style'; -import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import getRotation from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation'; +import getArrowFeature from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature'; export interface LayerProps { texts: Array<LayerText>; @@ -63,6 +53,8 @@ export default class Layer { arrowFeatures: Array<Feature<MultiPolygon>>; + pointToProjection: UsePointToProjectionResult; + vectorSource: VectorSource< Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon> >; @@ -89,10 +81,11 @@ export default class Layer { this.lines = lines; this.lineTypes = lineTypes; this.arrowTypes = arrowTypes; - this.textFeatures = this.getTextsFeatures(mapInstance, pointToProjection); - this.rectFeatures = this.getRectsFeatures(pointToProjection); - this.ovalFeatures = this.getOvalsFeatures(pointToProjection); - const { linesFeatures, arrowsFeatures } = this.getLinesFeatures(pointToProjection); + this.pointToProjection = pointToProjection; + this.textFeatures = this.getTextsFeatures(mapInstance); + this.rectFeatures = this.getRectsFeatures(); + this.ovalFeatures = this.getOvalsFeatures(); + const { linesFeatures, arrowsFeatures } = this.getLinesFeatures(); this.lineFeatures = linesFeatures; this.arrowFeatures = arrowsFeatures; this.vectorSource = new VectorSource({ @@ -111,10 +104,7 @@ export default class Layer { this.vectorLayer.set('id', layerId); } - private getTextsFeatures = ( - mapInstance: MapInstance, - pointToProjection: UsePointToProjectionResult, - ): Array<Feature<Point>> => { + private getTextsFeatures = (mapInstance: MapInstance): Array<Feature<Point>> => { const textObjects = this.texts.map(text => { return new Text({ x: text.x, @@ -127,23 +117,21 @@ export default class Layer { text: text.notes, verticalAlign: text.verticalAlign as VerticalAlign, horizontalAlign: text.horizontalAlign as HorizontalAlign, - pointToProjection, + pointToProjection: this.pointToProjection, mapInstance, }); }); return textObjects.map(text => text.feature); }; - private getRectsFeatures = ( - pointToProjection: UsePointToProjectionResult, - ): Array<Feature<Polygon>> => { + private getRectsFeatures = (): Array<Feature<Polygon>> => { return this.rects.map(rect => { const polygon = new Polygon([ [ - pointToProjection({ x: rect.x, y: rect.y }), - pointToProjection({ x: rect.x + rect.width, y: rect.y }), - pointToProjection({ x: rect.x + rect.width, y: rect.y + rect.height }), - pointToProjection({ x: rect.x, y: rect.y + rect.height }), + this.pointToProjection({ x: rect.x, y: rect.y }), + this.pointToProjection({ x: rect.x + rect.width, y: rect.y }), + this.pointToProjection({ x: rect.x + rect.width, y: rect.y + rect.height }), + this.pointToProjection({ x: rect.x, y: rect.y + rect.height }), ], ]); const polygonStyle = getStyle({ @@ -161,16 +149,14 @@ export default class Layer { }); }; - private getOvalsFeatures = ( - pointToProjection: UsePointToProjectionResult, - ): Array<Feature<Polygon>> => { + private getOvalsFeatures = (): Array<Feature<Polygon>> => { return this.ovals.map(oval => { const coords = getEllipseCoords({ x: oval.x + oval.width / 2, y: oval.y + oval.height / 2, height: oval.height, width: oval.width, - pointToProjection, + pointToProjection: this.pointToProjection, points: 36, }); const polygon = new Polygon([coords]); @@ -189,9 +175,7 @@ export default class Layer { }); }; - private getLinesFeatures = ( - pointToProjection: UsePointToProjectionResult, - ): { + private getLinesFeatures = (): { linesFeatures: Array<Feature<LineString>>; arrowsFeatures: Array<Feature<MultiPolygon>>; } => { @@ -203,31 +187,65 @@ export default class Layer { .map((segment, index) => { if (index === 0) { return [ - pointToProjection({ x: segment.x1, y: segment.y1 }), - pointToProjection({ x: segment.x2, y: segment.y2 }), + this.pointToProjection({ x: segment.x1, y: segment.y1 }), + this.pointToProjection({ x: segment.x2, y: segment.y2 }), ]; } - return [pointToProjection({ x: segment.x2, y: segment.y2 })]; + return [this.pointToProjection({ x: segment.x2, y: segment.y2 })]; }) .flat(); - const firstSegment = line.segments[0]; - const startArrowRotation = getRotation( - [firstSegment.x1, firstSegment.y1], - [firstSegment.x2, firstSegment.y2], - ); - const shortenedX1 = firstSegment.x1 + line.startArrow.length * Math.cos(startArrowRotation); - const shortenedY1 = firstSegment.y1 - line.startArrow.length * Math.sin(startArrowRotation); - points[0] = pointToProjection({ x: shortenedX1, y: shortenedY1 }); + if (line.startArrow.arrowType !== 'NONE') { + const firstSegment = line.segments[0]; + const startArrowRotation = getRotation( + [firstSegment.x1, firstSegment.y1], + [firstSegment.x2, firstSegment.y2], + ); + const shortenedX1 = firstSegment.x1 + line.startArrow.length * Math.cos(startArrowRotation); + const shortenedY1 = firstSegment.y1 - line.startArrow.length * Math.sin(startArrowRotation); + points[0] = this.pointToProjection({ x: shortenedX1, y: shortenedY1 }); + + const startArrowFeature = getArrowFeature({ + arrowTypes: this.arrowTypes, + arrow: line.startArrow, + x: shortenedX1, + y: shortenedY1, + zIndex: line.z, + rotation: startArrowRotation, + lineWidth: line.width, + color: line.color, + pointToProjection: this.pointToProjection, + }); + if (startArrowFeature) { + arrowsFeatures.push(startArrowFeature); + } + } + + if (line.endArrow.arrowType !== 'NONE') { + const lastSegment = line.segments[line.segments.length - 1]; + const endArrowRotation = getRotation( + [lastSegment.x1, lastSegment.y1], + [lastSegment.x2, lastSegment.y2], + ); + const shortenedX2 = lastSegment.x2 - line.endArrow.length * Math.cos(endArrowRotation); + const shortenedY2 = lastSegment.y2 - line.endArrow.length * Math.sin(endArrowRotation); + points[points.length - 1] = this.pointToProjection({ x: shortenedX2, y: shortenedY2 }); - const lastSegment = line.segments[line.segments.length - 1]; - const endArrowRotation = getRotation( - [lastSegment.x1, lastSegment.y1], - [lastSegment.x2, lastSegment.y2], - ); - const shortenedX2 = lastSegment.x2 - line.endArrow.length * Math.cos(endArrowRotation); - const shortenedY2 = lastSegment.y2 - line.endArrow.length * Math.sin(endArrowRotation); - points[points.length - 1] = pointToProjection({ x: shortenedX2, y: shortenedY2 }); + const endArrowFeature = getArrowFeature({ + arrowTypes: this.arrowTypes, + arrow: line.endArrow, + x: shortenedX2, + y: shortenedY2, + zIndex: line.z, + rotation: endArrowRotation, + lineWidth: line.width, + color: line.color, + pointToProjection: this.pointToProjection, + }); + if (endArrowFeature) { + arrowsFeatures.push(endArrowFeature); + } + } const lineString = new LineString(points); @@ -248,109 +266,7 @@ export default class Layer { }); lineFeature.setStyle(lineStyle); linesFeatures.push(lineFeature); - - arrowsFeatures.push( - ...this.getLineArrowsFeatures({ - line, - pointToProjection, - startArrowX: firstSegment.x1, - startArrowY: firstSegment.y1, - startArrowRotation, - endArrowX: shortenedX2, - endArrowY: shortenedY2, - endArrowRotation, - }), - ); }); return { linesFeatures, arrowsFeatures }; }; - - private getLineArrowsFeatures = ({ - line, - pointToProjection, - startArrowX, - startArrowY, - startArrowRotation, - endArrowX, - endArrowY, - endArrowRotation, - }: { - line: LayerLine; - pointToProjection: UsePointToProjectionResult; - startArrowX: number; - startArrowY: number; - startArrowRotation: number; - endArrowX: number; - endArrowY: number; - endArrowRotation: number; - }): Array<Feature<MultiPolygon>> => { - const arrowsFeatures: Array<Feature<MultiPolygon>> = []; - const startArrowFeature = this.getLineArrowFeature( - line.startArrow, - startArrowX, - startArrowY, - line.z, - startArrowRotation, - line.width, - pointToProjection, - ); - if (startArrowFeature) { - arrowsFeatures.push(startArrowFeature); - } - - const endArrowFeature = this.getLineArrowFeature( - line.endArrow, - endArrowX, - endArrowY, - line.z, - endArrowRotation, - line.width, - pointToProjection, - ); - if (endArrowFeature) { - arrowsFeatures.push(endArrowFeature); - } - return arrowsFeatures; - }; - - private getLineArrowFeature = ( - arrow: Arrow, - x: number, - y: number, - zIndex: number, - rotation: number, - lineWidth: number, - pointToProjection: UsePointToProjectionResult, - ): undefined | Feature<MultiPolygon> => { - const arrowShapes = this.arrowTypes.find(arrowType => arrowType.arrowType === arrow.arrowType) - ?.shapes; - if (!arrowShapes) { - return undefined; - } - const arrowMultiPolygon = getMultiPolygon({ - x, - y: y - arrow.length / 2, - width: arrow.length, - height: arrow.length, - shapes: arrowShapes, - pointToProjection, - }); - const arrowStyles: Array<Style> = []; - arrowMultiPolygon.forEach(polygon => { - const style = getStyle({ - geometry: polygon, - zIndex, - borderColor: BLACK_COLOR, - fillColor: BLACK_COLOR, - lineWidth, - }); - arrowStyles.push(style); - polygon.rotate(rotation, pointToProjection({ x, y })); - }); - const arrowFeature = new Feature({ - geometry: new MultiPolygon(arrowMultiPolygon), - }); - arrowFeature.setStyle(arrowStyles); - return arrowFeature; - }; } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3d84f4b244a5c1f52d4e24580c57ddb3bdc9ca7 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.test.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-magic-numbers */ +import { Feature } from 'ol'; +import Reaction, { + ReactionProps, +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction'; +import { newReactionFixture } from '@/models/fixtures/newReactionFixture'; +import { lineTypesFixture } from '@/models/fixtures/lineTypesFixture'; +import { arrowTypesFixture } from '@/models/fixtures/arrowTypesFixture'; + +describe('Layer', () => { + let props: ReactionProps; + + beforeEach(() => { + props = { + line: newReactionFixture.line, + products: newReactionFixture.products, + reactants: newReactionFixture.reactants, + zIndex: newReactionFixture.z, + pointToProjection: jest.fn(() => [10, 10]), + lineTypes: lineTypesFixture, + arrowTypes: arrowTypesFixture, + }; + }); + + it('should initialize a Reaction class', () => { + const reaction = new Reaction(props); + + expect(reaction.features.length).toBe(5); + expect(reaction.features).toBeInstanceOf(Array<Feature>); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b084d812ea3eb9f4c248cb3b1b2bff5347fbabf --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts @@ -0,0 +1,159 @@ +/* eslint-disable no-magic-numbers */ +import { ArrowType, Line, LineType, ReactionProduct } from '@/types/models'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Feature } from 'ol'; +import { LineString, MultiPolygon } from 'ol/geom'; +import getRotation from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import getArrowFeature from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getArrowFeature'; + +export interface ReactionProps { + line: Line; + products: Array<ReactionProduct>; + reactants: Array<ReactionProduct>; + zIndex: number; + lineTypes: Array<LineType>; + arrowTypes: Array<ArrowType>; + pointToProjection: UsePointToProjectionResult; +} + +export default class Reaction { + line: Line; + + products: Array<ReactionProduct>; + + reactants: Array<ReactionProduct>; + + zIndex: number; + + lineTypes: Array<LineType>; + + arrowTypes: Array<ArrowType>; + + pointToProjection: UsePointToProjectionResult; + + features: Array<Feature> = []; + + constructor({ + line, + products, + reactants, + zIndex, + lineTypes, + arrowTypes, + pointToProjection, + }: ReactionProps) { + this.line = line; + this.products = products; + this.reactants = reactants; + this.zIndex = zIndex; + this.lineTypes = lineTypes; + this.arrowTypes = arrowTypes; + this.pointToProjection = pointToProjection; + let lineFeature = this.getLineFeature(this.line); + this.features.push(lineFeature.lineFeature); + this.features.push(...lineFeature.arrowsFeatures); + this.products.forEach(product => { + lineFeature = this.getLineFeature(product.line); + this.features.push(lineFeature.lineFeature); + this.features.push(...lineFeature.arrowsFeatures); + }); + this.reactants.forEach(reactant => { + lineFeature = this.getLineFeature(reactant.line); + this.features.push(lineFeature.lineFeature); + this.features.push(...lineFeature.arrowsFeatures); + }); + } + + private getLineFeature = ( + line: Line, + ): { + lineFeature: Feature<LineString>; + arrowsFeatures: Array<Feature<MultiPolygon>>; + } => { + const arrowsFeatures: Array<Feature<MultiPolygon>> = []; + const points = line.segments + .map((segment, index) => { + if (index === 0) { + return [ + this.pointToProjection({ x: segment.x1, y: segment.y1 }), + this.pointToProjection({ x: segment.x2, y: segment.y2 }), + ]; + } + return [this.pointToProjection({ x: segment.x2, y: segment.y2 })]; + }) + .flat(); + + if (line.startArrow.arrowType !== 'NONE') { + const firstSegment = line.segments[0]; + let startArrowRotation = getRotation( + [firstSegment.x1, firstSegment.y1], + [firstSegment.x2, firstSegment.y2], + ); + startArrowRotation += Math.PI; + const shortenedX1 = firstSegment.x1 - line.startArrow.length * Math.cos(startArrowRotation); + const shortenedY1 = firstSegment.y1 + line.startArrow.length * Math.sin(startArrowRotation); + points[0] = this.pointToProjection({ x: shortenedX1, y: shortenedY1 }); + const startArrowFeature = getArrowFeature({ + arrowTypes: this.arrowTypes, + arrow: line.startArrow, + x: shortenedX1, + y: shortenedY1, + zIndex: this.zIndex, + rotation: startArrowRotation, + lineWidth: line.width, + color: line.color, + pointToProjection: this.pointToProjection, + }); + if (startArrowFeature) { + arrowsFeatures.push(startArrowFeature); + } + } + + if (line.endArrow.arrowType !== 'NONE') { + const lastSegment = line.segments[line.segments.length - 1]; + const endArrowRotation = getRotation( + [lastSegment.x1, lastSegment.y1], + [lastSegment.x2, lastSegment.y2], + ); + const shortenedX2 = lastSegment.x2 - line.endArrow.length * Math.cos(endArrowRotation); + const shortenedY2 = lastSegment.y2 + line.endArrow.length * Math.sin(endArrowRotation); + points[points.length - 1] = this.pointToProjection({ x: shortenedX2, y: shortenedY2 }); + const endArrowFeature = getArrowFeature({ + arrowTypes: this.arrowTypes, + arrow: line.endArrow, + x: shortenedX2, + y: shortenedY2, + zIndex: this.zIndex, + rotation: endArrowRotation, + lineWidth: line.width, + color: line.color, + pointToProjection: this.pointToProjection, + }); + if (endArrowFeature) { + arrowsFeatures.push(endArrowFeature); + } + } + + const lineString = new LineString(points); + + let lineDash; + const lineTypeFound = this.lineTypes.find(type => type.name === line.lineType); + if (lineTypeFound) { + lineDash = lineTypeFound.pattern; + } + const lineStyle = getStyle({ + geometry: lineString, + borderColor: line.color, + lineWidth: line.width, + lineDash, + zIndex: this.zIndex, + }); + const lineFeature = new Feature<LineString>({ + geometry: lineString, + }); + lineFeature.setStyle(lineStyle); + + return { lineFeature, arrowsFeatures }; + }; +} diff --git a/src/models/fixtures/newReactionFixture.ts b/src/models/fixtures/newReactionFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..e195b08c1fb50de0b37f8787a2452f7290a580a2 --- /dev/null +++ b/src/models/fixtures/newReactionFixture.ts @@ -0,0 +1,9 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { newReactionSchema } from '@/models/newReactionSchema'; + +export const newReactionFixture = createFixture(newReactionSchema, { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/fixtures/newReactionsFixture.ts b/src/models/fixtures/newReactionsFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..88b3014ff37c5b78f068804aac8c6c0451a21eeb --- /dev/null +++ b/src/models/fixtures/newReactionsFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { pageableSchema } from '@/models/pageableSchema'; +import { newReactionSchema } from '@/models/newReactionSchema'; + +export const newReactionsFixture = createFixture(pageableSchema(newReactionSchema), { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/fixtures/shapesFixture.ts b/src/models/fixtures/shapesFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..b802d13e1185ed18f4b02c848d1c0372321cde81 --- /dev/null +++ b/src/models/fixtures/shapesFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +import { z } from 'zod'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { shapeSchema } from '@/models/shapeSchema'; + +export const shapesFixture = createFixture(z.array(shapeSchema), { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/newReactionSchema.ts b/src/models/newReactionSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..674f6a2a6920d25c3fd2bcd7b8be6a1a2b38a587 --- /dev/null +++ b/src/models/newReactionSchema.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import { lineSchema } from '@/models/lineSchema'; +import { reactionProduct } from '@/models/reactionProduct'; +import { referenceSchema } from '@/models/referenceSchema'; + +export const newReactionSchema = z.object({ + id: z.number(), + abbreviation: z.string().nullable(), + elementId: z.string(), + formula: z.string().nullable(), + geneProteinReaction: z.string().nullable(), + idReaction: z.string(), + kinetics: z.null(), + line: lineSchema, + lowerBound: z.number().nullable(), + mechanicalConfidenceScore: z.number().int().nullable(), + model: z.number(), + modifiers: z.array(reactionProduct), + name: z.string(), + notes: z.string(), + operators: z.array(z.unknown()), + processCoordinates: z.null(), + products: z.array(reactionProduct), + reactants: z.array(reactionProduct), + references: z.array(referenceSchema), + reversible: z.boolean(), + sboTerm: z.string(), + subsystem: z.null(), + symbol: z.null(), + synonyms: z.array(z.unknown()), + upperBound: z.null(), + visibilityLevel: z.string(), + z: z.number(), +}); diff --git a/src/models/reactionProduct.ts b/src/models/reactionProduct.ts index 96905877910e382f1736d595351286f5fa636807..7fe435ecc936ddbaa6adae51e37934d90e629853 100644 --- a/src/models/reactionProduct.ts +++ b/src/models/reactionProduct.ts @@ -4,6 +4,6 @@ import { lineSchema } from './lineSchema'; export const reactionProduct = z.object({ id: z.number(), line: lineSchema, - stoichiometry: z.null(), + stoichiometry: z.number().nullable(), element: z.number(), }); diff --git a/src/models/shapeEllipseSchema.ts b/src/models/shapeEllipseSchema.ts index 920940192e207b79b9619ac8d461a07da5bc5bc7..843b653680714fa2e7e95b33d1df9828ea30445e 100644 --- a/src/models/shapeEllipseSchema.ts +++ b/src/models/shapeEllipseSchema.ts @@ -3,6 +3,7 @@ import { shapeRelAbsSchema } from '@/models/shapeRelAbsSchema'; export const shapeEllipseSchema = z.object({ type: z.literal('ELLIPSE'), + fill: z.boolean().nullable().optional(), center: shapeRelAbsSchema, radius: shapeRelAbsSchema, }); diff --git a/src/models/shapePolygonSchema.ts b/src/models/shapePolygonSchema.ts index d7e2c59d8a2dc7d559c34d3ff6f8c4ce24df6dec..a3647cd1c9f6d7577c20f38ab04bde967829d87c 100644 --- a/src/models/shapePolygonSchema.ts +++ b/src/models/shapePolygonSchema.ts @@ -4,5 +4,6 @@ import { shapeRelAbsSchema } from '@/models/shapeRelAbsSchema'; export const shapePolygonSchema = z.object({ type: z.literal('POLYGON'), + fill: z.boolean().nullable().optional(), points: z.array(z.union([shapeRelAbsSchema, shapeRelAbsBezierPointSchema])), }); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 0c8b1125bf67d4a8ad2330eb24dec4d555cdae1a..784ec8427b0058270d8fc5cd3ac533e812fe680a 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -64,6 +64,8 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`, getGlyphImage: (glyphId: number): string => `projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`, + getNewReactions: (modelId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/?size=2000`, getChemicalsStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`, getAllOverlaysByProjectIdQuery: ( diff --git a/src/redux/newReactions/newReactions.constants.ts b/src/redux/newReactions/newReactions.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..80ea28ccf9f366c876a3dff5d6582db073fabe48 --- /dev/null +++ b/src/redux/newReactions/newReactions.constants.ts @@ -0,0 +1,10 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; + +export const NEW_REACTIONS_INITIAL_STATE: NewReactionsState = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, +}; + +export const NEW_REACTIONS_FETCHING_ERROR_PREFIX = 'Failed to fetch new reactions'; diff --git a/src/redux/newReactions/newReactions.mock.ts b/src/redux/newReactions/newReactions.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..3847aed65b5a817df00b05ba02e4a5a5c7e63133 --- /dev/null +++ b/src/redux/newReactions/newReactions.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; + +export const NEW_REACTIONS_INITIAL_STATE_MOCK: NewReactionsState = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/newReactions/newReactions.reducers.test.ts b/src/redux/newReactions/newReactions.reducers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..21f8d6686e63ce18e3b924d3fa25f6f57e4c6687 --- /dev/null +++ b/src/redux/newReactions/newReactions.reducers.test.ts @@ -0,0 +1,79 @@ +/* eslint-disable no-magic-numbers */ +import { apiPath } from '@/redux/apiPath'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { unwrapResult } from '@reduxjs/toolkit'; +import newReactionsReducer from '@/redux/newReactions/newReactions.slice'; +import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; +import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock'; +import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture'; + +const mockedAxiosClient = mockNetworkNewAPIResponse(); + +const INITIAL_STATE: NewReactionsState = NEW_REACTIONS_INITIAL_STATE_MOCK; + +describe('newReactions reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<NewReactionsState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('newReactions', newReactionsReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(newReactionsReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + + it('should update store after successful getNewReactions query', async () => { + mockedAxiosClient + .onGet(apiPath.getNewReactions(1)) + .reply(HttpStatusCode.Ok, newReactionsFixture); + + const { type } = await store.dispatch(getNewReactions(1)); + const { data, loading, error } = store.getState().newReactions; + expect(type).toBe('newReactions/getNewReactions/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(newReactionsFixture.content); + }); + + it('should update store after failed getNewReactions query', async () => { + mockedAxiosClient.onGet(apiPath.getNewReactions(1)).reply(HttpStatusCode.NotFound, []); + + const action = await store.dispatch(getNewReactions(1)); + const { data, loading, error } = store.getState().newReactions; + + expect(action.type).toBe('newReactions/getNewReactions/rejected'); + expect(() => unwrapResult(action)).toThrow( + "Failed to fetch new reactions: The page you're looking for doesn't exist. Please verify the URL and try again.", + ); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual([]); + }); + + it('should update store on loading getNewReactions query', async () => { + mockedAxiosClient + .onGet(apiPath.getNewReactions(1)) + .reply(HttpStatusCode.Ok, newReactionsFixture); + + const newReactionsPromise = store.dispatch(getNewReactions(1)); + + const { data, loading } = store.getState().newReactions; + expect(data).toEqual([]); + expect(loading).toEqual('pending'); + + newReactionsPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = + store.getState().newReactions; + + expect(dataPromiseFulfilled).toEqual(newReactionsFixture.content); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/newReactions/newReactions.reducers.ts b/src/redux/newReactions/newReactions.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..306b306e770cfb0097e256e1aed05fa8017d2990 --- /dev/null +++ b/src/redux/newReactions/newReactions.reducers.ts @@ -0,0 +1,18 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; + +export const getNewReactionsReducer = ( + builder: ActionReducerMapBuilder<NewReactionsState>, +): void => { + builder.addCase(getNewReactions.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getNewReactions.fulfilled, (state, action) => { + state.data = action.payload || []; + state.loading = 'succeeded'; + }); + builder.addCase(getNewReactions.rejected, state => { + state.loading = 'failed'; + }); +}; diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..4dc2babe517c9850b03cf090644ebe2389550dcd --- /dev/null +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -0,0 +1,9 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { rootSelector } from '../root/root.selectors'; + +export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions); + +export const newReactionsDataSelector = createSelector( + newReactionsSelector, + reactions => reactions.data || [], +); diff --git a/src/redux/newReactions/newReactions.slice.ts b/src/redux/newReactions/newReactions.slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bb5198a1f58d89036338741124f555a64c9b71a --- /dev/null +++ b/src/redux/newReactions/newReactions.slice.ts @@ -0,0 +1,14 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { NEW_REACTIONS_INITIAL_STATE } from '@/redux/newReactions/newReactions.constants'; +import { getNewReactionsReducer } from '@/redux/newReactions/newReactions.reducers'; + +export const newReactionsSlice = createSlice({ + name: 'reactions', + initialState: NEW_REACTIONS_INITIAL_STATE, + reducers: {}, + extraReducers: builder => { + getNewReactionsReducer(builder); + }, +}); + +export default newReactionsSlice.reducer; diff --git a/src/redux/newReactions/newReactions.thunks.test.ts b/src/redux/newReactions/newReactions.thunks.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..afd14d77104b12dcbfaa00d460ca2221244947b9 --- /dev/null +++ b/src/redux/newReactions/newReactions.thunks.test.ts @@ -0,0 +1,41 @@ +/* eslint-disable no-magic-numbers */ +import { apiPath } from '@/redux/apiPath'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import newReactionsReducer from '@/redux/newReactions/newReactions.slice'; +import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; +import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture'; +import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; + +const mockedAxiosClient = mockNetworkNewAPIResponse(); + +describe('newReactions thunks', () => { + let store = {} as ToolkitStoreWithSingleSlice<NewReactionsState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('newReactions', newReactionsReducer); + }); + + describe('getReactions', () => { + it('should return data when data response from API is valid', async () => { + mockedAxiosClient + .onGet(apiPath.getNewReactions(1)) + .reply(HttpStatusCode.Ok, newReactionsFixture); + + const { payload } = await store.dispatch(getNewReactions(1)); + expect(payload).toEqual(newReactionsFixture.content); + }); + + it('should return undefined when data response from API is not valid ', async () => { + mockedAxiosClient + .onGet(apiPath.getNewReactions(1)) + .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); + + const { payload } = await store.dispatch(getNewReactions(1)); + expect(payload).toEqual(undefined); + }); + }); +}); diff --git a/src/redux/newReactions/newReactions.thunks.ts b/src/redux/newReactions/newReactions.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e7081a85787f62592bce8fc9095714a45eef4b3 --- /dev/null +++ b/src/redux/newReactions/newReactions.thunks.ts @@ -0,0 +1,24 @@ +import { apiPath } from '@/redux/apiPath'; +import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; +import { NewReaction, NewReactions } from '@/types/models'; +import { ThunkConfig } from '@/types/store'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { getError } from '@/utils/error-report/getError'; +import { newReactionSchema } from '@/models/newReactionSchema'; +import { pageableSchema } from '@/models/pageableSchema'; +import { NEW_REACTIONS_FETCHING_ERROR_PREFIX } from '@/redux/newReactions/newReactions.constants'; + +export const getNewReactions = createAsyncThunk< + Array<NewReaction> | undefined, + number, + ThunkConfig +>('newReactions/getNewReactions', async (modelId: number) => { + try { + const { data } = await axiosInstanceNewAPI.get<NewReactions>(apiPath.getNewReactions(modelId)); + const isDataValid = validateDataUsingZodSchema(data, pageableSchema(newReactionSchema)); + return isDataValid ? data.content : undefined; + } catch (error) { + return Promise.reject(getError({ error, prefix: NEW_REACTIONS_FETCHING_ERROR_PREFIX })); + } +}); diff --git a/src/redux/newReactions/newReactions.types.ts b/src/redux/newReactions/newReactions.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..142906d7f46037ae9789eee1e74504ea2408d55f --- /dev/null +++ b/src/redux/newReactions/newReactions.types.ts @@ -0,0 +1,4 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { NewReaction } from '@/types/models'; + +export type NewReactionsState = FetchDataState<NewReaction[]>; diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index 8b1cec49d6e6fe6104c2487e898f080f322e2a1a..007c2b355b0bb4cc0dbf4a5d5e0a9d84b7b8d19c 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -6,6 +6,7 @@ import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.co import { SHAPES_STATE_INITIAL_MOCK } from '@/redux/shapes/shapes.mock'; import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; +import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock'; import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock'; import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock'; import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock'; @@ -53,6 +54,7 @@ export const INITIAL_STORE_STATE_MOCK: RootState = { oauth: OAUTH_INITIAL_STATE_MOCK, overlays: OVERLAYS_INITIAL_STATE_MOCK, reactions: REACTIONS_STATE_INITIAL_MOCK, + newReactions: NEW_REACTIONS_INITIAL_STATE_MOCK, configuration: CONFIGURATION_INITIAL_STATE, constant: CONSTANT_INITIAL_STATE, overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, diff --git a/src/redux/store.ts b/src/redux/store.ts index f6b8ad65c8648756943c421cc42fd796f75e6bd7..a5d31bb57d0a4db6a0bae569a15a89579317c211 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -19,6 +19,7 @@ import overlaysReducer from '@/redux/overlays/overlays.slice'; import projectReducer from '@/redux/project/project.slice'; import projectsReducer from '@/redux/projects/projects.slice'; import reactionsReducer from '@/redux/reactions/reactions.slice'; +import newReactionsReducer from '@/redux/newReactions/newReactions.slice'; import searchReducer from '@/redux/search/search.slice'; import userReducer from '@/redux/user/user.slice'; import { @@ -66,6 +67,7 @@ export const reducers = { modelElements: modelElementsReducer, layers: layersReducer, reactions: reactionsReducer, + newReactions: newReactionsReducer, contextMenu: contextMenuReducer, cookieBanner: cookieBannerReducer, user: userReducer, diff --git a/src/types/models.ts b/src/types/models.ts index 1b09b1d9d20308b26566400d6d29efdb3698e9c1..1e6ac2bb90801824bc5b8f1da37e20b71e2cf98d 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -80,6 +80,8 @@ import { arrowTypeSchema } from '@/models/arrowTypeSchema'; import { arrowSchema } from '@/models/arrowSchema'; import { shapeRelAbsSchema } from '@/models/shapeRelAbsSchema'; import { shapeRelAbsBezierPointSchema } from '@/models/shapeRelAbsBezierPointSchema'; +import { newReactionSchema } from '@/models/newReactionSchema'; +import { reactionProduct } from '@/models/reactionProduct'; export type Project = z.infer<typeof projectSchema>; export type OverviewImageView = z.infer<typeof overviewImageView>; @@ -116,6 +118,10 @@ export type BioEntityContent = z.infer<typeof bioEntityContentSchema>; export type BioEntityResponse = z.infer<typeof bioEntityResponseSchema>; export type Chemical = z.infer<typeof chemicalSchema>; export type Reaction = z.infer<typeof reactionSchema>; +export type NewReaction = z.infer<typeof newReactionSchema>; +const newReactionsSchema = pageableSchema(newReactionSchema); +export type NewReactions = z.infer<typeof newReactionsSchema>; +export type ReactionProduct = z.infer<typeof reactionProduct>; export type Reference = z.infer<typeof referenceSchema>; export type ReactionLine = z.infer<typeof reactionLineSchema>; export type ElementSearchResult = z.infer<typeof elementSearchResult>;