Skip to content
Snippets Groups Projects
Commit 4f64395b authored by Miłosz Grocholewski's avatar Miłosz Grocholewski
Browse files

feat(vector-map): implement reactions between two elements

parent 2f80506e
No related branches found
No related tags found
1 merge request!283feat(vector-map): implement reactions between two elements
Showing
with 694 additions and 261 deletions
......@@ -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({
......
......@@ -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');
......
......@@ -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');
......
/* 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');
......
......@@ -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);
});
......
......@@ -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);
}
}
/* 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();
});
});
/* 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;
}
......@@ -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);
......
/* 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 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 getMultiPolygon({
export default function getShapePolygon({
shape,
x,
y,
width,
height,
shapes,
pointToProjection,
mirror,
pointToProjection,
}: {
shape: Shape;
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,
});
}
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);
if (mirror) {
const centroid = getCentroid(coords);
coords = coords.map(coord => {
const mirroredX = 2 * centroid[0] - coord[0];
coords = coords.map(coord => {
const mirroredX = 2 * centroid[0] - coord[0];
return [mirroredX, coord[1]];
});
}
return [mirroredX, coord[1]];
});
}
return new Polygon([coords]);
});
return new Polygon([coords]);
}
/* 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;
};
}
/* 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>);
});
});
/* 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 };
};
}
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 },
});
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 },
});
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 },
});
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(),
});
......@@ -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(),
});
......@@ -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,
});
......@@ -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])),
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment