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

feat(vector-map): add drawing compartments

parent 38507a53
No related branches found
No related tags found
3 merge requests!271Resolve MIN-39 "Feat/ glyphs",!260feat(vector-map): add drawing compartments,!259Resolve MIN-35 "hypothetical option"
Showing
with 1053 additions and 110 deletions
import { ColorObject } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
import {
ColorObject,
EllipseCenter,
EllipseRadius,
ShapeCurvePoint,
ShapePoint,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
export const WHITE_COLOR: ColorObject = {
alpha: 255,
......@@ -9,3 +15,153 @@ export const BLACK_COLOR: ColorObject = {
alpha: 255,
rgb: -16777216,
};
export const COMPARTMENT_SQUARE_POINTS: Array<ShapePoint | ShapeCurvePoint> = [
{
type: 'REL_ABS_POINT',
absoluteX: 10.0,
absoluteY: 0.0,
relativeX: 0.0,
relativeY: 0.0,
relativeHeightForX: null,
relativeWidthForY: null,
},
{
type: 'REL_ABS_POINT',
absoluteX: -10.0,
absoluteY: 0.0,
relativeX: 100.0,
relativeY: 0.0,
relativeHeightForX: null,
relativeWidthForY: null,
},
{
type: 'REL_ABS_BEZIER_POINT',
absoluteX1: 0.0,
absoluteY1: 10.0,
relativeX1: 100.0,
relativeY1: 0.0,
relativeHeightForX1: null,
relativeWidthForY1: null,
absoluteX2: -5.0,
absoluteY2: 0.0,
relativeX2: 100.0,
relativeY2: 0.0,
relativeHeightForX2: null,
relativeWidthForY2: null,
absoluteX3: 0.0,
absoluteY3: 5.0,
relativeX3: 100.0,
relativeY3: 0.0,
relativeHeightForX3: null,
relativeWidthForY3: null,
},
{
type: 'REL_ABS_POINT',
absoluteX: 0.0,
absoluteY: -10.0,
relativeX: 100.0,
relativeY: 100.0,
relativeHeightForX: null,
relativeWidthForY: null,
},
{
type: 'REL_ABS_BEZIER_POINT',
absoluteX1: -10.0,
absoluteY1: 0.0,
relativeX1: 100.0,
relativeY1: 100.0,
relativeHeightForX1: null,
relativeWidthForY1: null,
absoluteX2: 0.0,
absoluteY2: -5.0,
relativeX2: 100.0,
relativeY2: 100.0,
relativeHeightForX2: null,
relativeWidthForY2: null,
absoluteX3: -5.0,
absoluteY3: 0.0,
relativeX3: 100.0,
relativeY3: 100.0,
relativeHeightForX3: null,
relativeWidthForY3: null,
},
{
type: 'REL_ABS_POINT',
absoluteX: 10.0,
absoluteY: 0.0,
relativeX: 0.0,
relativeY: 100.0,
relativeHeightForX: null,
relativeWidthForY: null,
},
{
type: 'REL_ABS_BEZIER_POINT',
absoluteX1: 0.0,
absoluteY1: -10.0,
relativeX1: 0.0,
relativeY1: 100.0,
relativeHeightForX1: null,
relativeWidthForY1: null,
absoluteX2: 5.0,
absoluteY2: 0.0,
relativeX2: 0.0,
relativeY2: 100.0,
relativeHeightForX2: null,
relativeWidthForY2: null,
absoluteX3: 0.0,
absoluteY3: -5.0,
relativeX3: 0.0,
relativeY3: 100.0,
relativeHeightForX3: null,
relativeWidthForY3: null,
},
{
type: 'REL_ABS_POINT',
absoluteX: 0.0,
absoluteY: 10.0,
relativeX: 0.0,
relativeY: 0.0,
relativeHeightForX: null,
relativeWidthForY: null,
},
{
type: 'REL_ABS_BEZIER_POINT',
absoluteX1: 10.0,
absoluteY1: 0.0,
relativeX1: 0.0,
relativeY1: 0.0,
relativeHeightForX1: null,
relativeWidthForY1: null,
absoluteX2: 0.0,
absoluteY2: 5.0,
relativeX2: 0.0,
relativeY2: 0.0,
relativeHeightForX2: null,
relativeWidthForY2: null,
absoluteX3: 5.0,
absoluteY3: 0.0,
relativeX3: 0.0,
relativeY3: 0.0,
relativeHeightForX3: null,
relativeWidthForY3: null,
},
];
export const COMPARTMENT_CIRCLE_CENTER: EllipseCenter = {
type: 'REL_ABS_POINT',
absoluteX: 0.0,
absoluteY: 0.0,
relativeX: 50.0,
relativeY: 50.0,
relativeHeightForX: null,
relativeWidthForY: null,
};
export const COMPARTMENT_CIRCLE_RADIUS: EllipseRadius = {
type: 'REL_ABS_RADIUS',
absoluteX: 0.0,
absoluteY: 0.0,
relativeX: 50.0,
relativeY: 50.0,
};
......@@ -45,3 +45,21 @@ export type ShapeCurvePoint = {
relativeHeightForX3: number | null;
relativeWidthForY3: number | null;
};
export type EllipseCenter = {
type: string;
absoluteX: number;
absoluteY: number;
relativeX: number;
relativeY: number;
relativeHeightForX: number | null;
relativeWidthForY: number | null;
};
export type EllipseRadius = {
type: string;
absoluteX: number;
absoluteY: number;
relativeX: number;
relativeY: number;
};
......@@ -16,6 +16,8 @@ import { modelElementsSelector } from '@/redux/modelElements/modelElements.selec
import { currentModelIdSelector } from '@/redux/models/models.selectors';
import { getModelElements } from '@/redux/modelElements/modelElements.thunks';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare';
import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle';
export const useOlMapReactionsLayer = ({
mapInstance,
......@@ -33,10 +35,10 @@ export const useOlMapReactionsLayer = ({
const shapes = useSelector(bioShapesSelector);
const lineTypes = useSelector(lineTypesSelector);
const elements: Array<MapElement> = useMemo(() => {
const elements: Array<MapElement | CompartmentCircle | CompartmentSquare> = useMemo(() => {
if (!modelElements || !shapes) return [];
const validElements: Array<MapElement> = [];
const validElements: Array<MapElement | CompartmentCircle | CompartmentSquare> = [];
modelElements.content.forEach(element => {
const shape = shapes.find(bioShape => bioShape.sboTerm === element.sboTerm);
if (shape) {
......@@ -60,6 +62,7 @@ export const useOlMapReactionsLayer = ({
nameVerticalAlign: element.nameVerticalAlign as VerticalAlign,
nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign,
text: element.name,
fontSize: element.fontSize,
pointToProjection,
mapInstance,
modifications: element.modificationResidues,
......@@ -67,6 +70,35 @@ export const useOlMapReactionsLayer = ({
bioShapes: shapes,
}),
);
} else if (element.sboTerm === 'SBO:0000290') {
const compartmentProps = {
x: element.x,
y: element.y,
nameX: element.nameX,
nameY: element.nameY,
nameHeight: element.nameHeight,
nameWidth: element.nameWidth,
width: element.width,
height: element.height,
zIndex: element.z,
innerWidth: element.innerWidth,
outerWidth: element.outerWidth,
thickness: element.thickness,
fontColor: element.fontColor,
fillColor: element.fillColor,
borderColor: element.borderColor,
nameVerticalAlign: element.nameVerticalAlign as VerticalAlign,
nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign,
text: element.name,
fontSize: element.fontSize,
pointToProjection,
mapInstance,
};
if (element.shape === 'OVAL_COMPARTMENT') {
validElements.push(new CompartmentCircle(compartmentProps));
} else if (element.shape === 'SQUARE_COMPARTMENT') {
validElements.push(new CompartmentSquare(compartmentProps));
}
}
});
return validElements;
......
/* eslint-disable no-magic-numbers */
import Polygon from 'ol/geom/Polygon';
import { Style } from 'ol/style';
import Feature, { FeatureLike } from 'ol/Feature';
import { MultiPolygon } from 'ol/geom';
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import {
ColorObject,
HorizontalAlign,
VerticalAlign,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
import { MapInstance } from '@/types/map';
import getText from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getText';
import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex';
export interface BaseMapElementProps {
x: number;
y: number;
width: number;
height: number;
zIndex: number;
text: string;
fontSize: number;
nameX: number;
nameY: number;
nameWidth: number;
nameHeight: number;
fontColor: ColorObject;
nameVerticalAlign: VerticalAlign;
nameHorizontalAlign: HorizontalAlign;
fillColor: ColorObject;
borderColor: ColorObject;
pointToProjection: UsePointToProjectionResult;
}
export default abstract class BaseMultiPolygon {
x: number;
y: number;
width: number;
height: number;
zIndex: number;
text: string;
fontSize: number;
nameX: number;
nameY: number;
nameWidth: number;
nameHeight: number;
fontColor: ColorObject;
nameVerticalAlign: VerticalAlign;
nameHorizontalAlign: HorizontalAlign;
fillColor: ColorObject;
borderColor: ColorObject;
polygons: Array<Polygon> = [];
styles: Array<Style> = [];
polygonsTexts: Array<string> = [];
multiPolygonFeature: Feature = new Feature();
pointToProjection: UsePointToProjectionResult;
constructor({
x,
y,
width,
height,
zIndex,
text,
fontSize,
nameX,
nameY,
nameWidth,
nameHeight,
fontColor,
nameVerticalAlign,
nameHorizontalAlign,
fillColor,
borderColor,
pointToProjection,
}: BaseMapElementProps) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.zIndex = zIndex;
this.text = text;
this.fontSize = fontSize;
this.nameX = nameX;
this.nameY = nameY;
this.nameWidth = nameWidth;
this.nameHeight = nameHeight;
this.fontColor = fontColor;
this.nameVerticalAlign = nameVerticalAlign;
this.nameHorizontalAlign = nameHorizontalAlign;
this.fillColor = fillColor;
this.borderColor = borderColor;
this.pointToProjection = pointToProjection;
}
protected abstract createPolygons(): void;
protected drawText(): void {
if (this.text) {
const { textCoords, textStyle } = getText({
text: this.text,
fontSize: this.fontSize,
x: this.nameX,
y: this.nameY,
width: this.nameWidth,
height: this.nameHeight,
color: rgbToHex(this.fontColor),
zIndex: this.zIndex,
verticalAlign: this.nameVerticalAlign,
horizontalAlign: this.nameHorizontalAlign,
pointToProjection: this.pointToProjection,
});
this.styles.push(textStyle);
this.polygonsTexts.push(this.text);
this.polygons.push(new Polygon([[textCoords, textCoords]]));
}
}
protected drawMultiPolygonFeature(mapInstance: MapInstance): void {
this.multiPolygonFeature = new Feature({
geometry: new MultiPolygon(this.polygons),
getTextScale: (resolution: number): number => {
const maxZoom = mapInstance?.getView().getMaxZoom();
if (maxZoom) {
const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom);
if (minResolution) {
return Math.round((minResolution / resolution) * 100) / 100;
}
}
return 1;
},
});
this.multiPolygonFeature.setStyle(this.styleFunction.bind(this));
}
protected styleFunction(feature: FeatureLike, resolution: number): Style | Array<Style> | void {
const getTextScale = feature.get('getTextScale');
let textScale = 1;
if (getTextScale instanceof Function) {
textScale = getTextScale(resolution);
}
let index = 0;
this.styles.forEach(style => {
if (style.getText()) {
if (this.fontSize * textScale > 4) {
style.getText()?.setScale(textScale);
style.getText()?.setText(this.polygonsTexts[index]);
index += 1;
} else {
style.getText()?.setText(undefined);
}
}
});
return this.styles;
}
}
/* eslint-disable no-magic-numbers */
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import {
ColorObject,
HorizontalAlign,
VerticalAlign,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/BaseMultiPolygon';
import { Coordinate } from 'ol/coordinate';
import Polygon from 'ol/geom/Polygon';
import { Style } from 'ol/style';
import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill';
import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex';
import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke';
import { MapInstance } from '@/types/map';
export interface CompartmentProps {
x: number;
y: number;
width: number;
height: number;
thickness: number;
outerWidth: number;
innerWidth: number;
zIndex: number;
text: string;
fontSize: number;
nameX: number;
nameY: number;
nameWidth: number;
nameHeight: number;
fontColor: ColorObject;
nameVerticalAlign: VerticalAlign;
nameHorizontalAlign: HorizontalAlign;
fillColor: ColorObject;
borderColor: ColorObject;
pointToProjection: UsePointToProjectionResult;
mapInstance: MapInstance;
}
export default abstract class Compartment extends BaseMultiPolygon {
outerCoords: Array<Coordinate> = [];
innerCoords: Array<Coordinate> = [];
outerWidth: number;
innerWidth: number;
thickness: number;
constructor({
x,
y,
width,
height,
thickness,
outerWidth,
innerWidth,
zIndex,
text,
fontSize,
nameX,
nameY,
nameWidth,
nameHeight,
fontColor,
nameVerticalAlign,
nameHorizontalAlign,
fillColor,
borderColor,
pointToProjection,
mapInstance,
}: CompartmentProps) {
super({
x,
y,
width,
height,
zIndex,
text,
fontSize,
nameX,
nameY,
nameWidth,
nameHeight,
fontColor,
nameVerticalAlign,
nameHorizontalAlign,
fillColor,
borderColor,
pointToProjection,
});
this.outerWidth = outerWidth;
this.innerWidth = innerWidth;
this.thickness = thickness;
this.getCompartmentCoords();
this.createPolygons();
this.drawText();
this.drawMultiPolygonFeature(mapInstance);
}
protected abstract getCompartmentCoords(): void;
protected createPolygons(): void {
const framePolygon = new Polygon([this.outerCoords, this.innerCoords]);
this.styles.push(
new Style({
geometry: framePolygon,
fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 128 }) }),
zIndex: this.zIndex,
}),
);
this.polygons.push(framePolygon);
const outerPolygon = new Polygon([this.outerCoords]);
this.styles.push(
new Style({
geometry: outerPolygon,
stroke: getStroke({ color: rgbToHex(this.borderColor), width: this.outerWidth }),
zIndex: this.zIndex,
}),
);
this.polygons.push(outerPolygon);
const innerPolygon = new Polygon([this.innerCoords]);
this.styles.push(
new Style({
geometry: innerPolygon,
stroke: getStroke({ color: rgbToHex(this.borderColor), width: this.innerWidth }),
fill: getFill({ color: rgbToHex({ ...this.fillColor, alpha: 9 }) }),
zIndex: this.zIndex,
}),
);
this.polygons.push(innerPolygon);
}
}
/* eslint-disable no-magic-numbers */
import { Feature, Map } from 'ol';
import { Fill, Style, Text } from 'ol/style';
import { Polygon, MultiPolygon } from 'ol/geom';
import getText from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getText';
import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon';
import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke';
import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill';
import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex';
import View from 'ol/View';
import {
WHITE_COLOR,
BLACK_COLOR,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
import CompartmentCircle, {
CompartmentCircleProps,
} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle';
import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords';
jest.mock('./getText');
jest.mock('./getMultiPolygon');
jest.mock('./getStroke');
jest.mock('./getEllipseCoords');
jest.mock('./getFill');
jest.mock('./rgbToHex');
describe('MapElement', () => {
let props: CompartmentCircleProps;
beforeEach(() => {
const dummyElement = document.createElement('div');
const mapInstance = new Map({
target: dummyElement,
view: new View({
zoom: 5,
minZoom: 3,
maxZoom: 7,
}),
});
props = {
x: 0,
y: 0,
width: 100,
height: 100,
zIndex: 1,
fillColor: WHITE_COLOR,
borderColor: BLACK_COLOR,
fontColor: BLACK_COLOR,
innerWidth: 1,
outerWidth: 2,
thickness: 12,
text: 'Test Text',
fontSize: 12,
nameX: 10,
nameY: 20,
nameHeight: 30,
nameWidth: 40,
nameVerticalAlign: 'MIDDLE',
nameHorizontalAlign: 'CENTER',
pointToProjection: jest.fn(),
mapInstance,
};
(getText as jest.Mock).mockReturnValue({
textCoords: [0, 0],
textStyle: new Style({
text: new Text({
text: props.text,
font: `bold ${props.fontSize}px Arial`,
fill: new Fill({
color: '#000',
}),
placement: 'point',
textAlign: 'center',
textBaseline: 'middle',
}),
}),
});
(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');
(getEllipseCoords as jest.Mock).mockReturnValue([
[10, 10],
[20, 20],
[30, 30],
]);
});
it('should initialize with correct default properties', () => {
const multiPolygon = new CompartmentCircle(props);
expect(multiPolygon.polygons.length).toBe(4);
expect(multiPolygon.multiPolygonFeature).toBeInstanceOf(Feature);
expect(multiPolygon.multiPolygonFeature.getGeometry()).toBeInstanceOf(MultiPolygon);
});
it('should apply correct styles to the feature', () => {
const multiPolygon = new CompartmentCircle(props);
const feature = multiPolygon.multiPolygonFeature;
const style = feature.getStyleFunction()?.call(multiPolygon, feature, 1);
if (Array.isArray(style)) {
expect(style.length).toBeGreaterThan(0);
} else {
expect(style).toBeInstanceOf(Style);
}
});
});
/* eslint-disable no-magic-numbers */
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import { MapInstance } from '@/types/map';
import {
ColorObject,
HorizontalAlign,
VerticalAlign,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
import {
BLACK_COLOR,
COMPARTMENT_CIRCLE_CENTER,
COMPARTMENT_CIRCLE_RADIUS,
WHITE_COLOR,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords';
import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment';
export type CompartmentCircleProps = {
x: number;
y: number;
width: number;
height: number;
zIndex: number;
fillColor?: ColorObject;
borderColor?: ColorObject;
fontColor?: ColorObject;
innerWidth?: number;
outerWidth?: number;
thickness?: number;
text?: string;
fontSize?: number;
nameX: number;
nameY: number;
nameHeight: number;
nameWidth: number;
nameVerticalAlign?: VerticalAlign;
nameHorizontalAlign?: HorizontalAlign;
pointToProjection: UsePointToProjectionResult;
mapInstance: MapInstance;
};
export default class CompartmentCircle extends Compartment {
constructor({
x,
y,
width,
height,
zIndex,
fillColor = WHITE_COLOR,
borderColor = BLACK_COLOR,
fontColor = BLACK_COLOR,
innerWidth = 1,
outerWidth = 2,
thickness = 12,
text = '',
fontSize = 12,
nameX,
nameY,
nameHeight,
nameWidth,
nameVerticalAlign = 'MIDDLE',
nameHorizontalAlign = 'CENTER',
pointToProjection,
mapInstance,
}: CompartmentCircleProps) {
super({
x,
y,
width,
height,
thickness,
outerWidth,
innerWidth,
zIndex,
text,
fontSize,
nameX,
nameY,
nameWidth,
nameHeight,
fontColor,
nameVerticalAlign,
nameHorizontalAlign,
fillColor,
borderColor,
pointToProjection,
mapInstance,
});
}
protected getCompartmentCoords(): void {
this.outerCoords = getEllipseCoords({
x: this.x,
y: this.y,
center: COMPARTMENT_CIRCLE_CENTER,
radius: COMPARTMENT_CIRCLE_RADIUS,
height: this.height,
width: this.width,
points: 40,
pointToProjection: this.pointToProjection,
});
this.innerCoords = getEllipseCoords({
x: this.x + this.thickness,
y: this.y + this.thickness,
center: COMPARTMENT_CIRCLE_CENTER,
radius: COMPARTMENT_CIRCLE_RADIUS,
height: this.height - 2 * this.thickness,
width: this.width - 2 * this.thickness,
points: 40,
pointToProjection: this.pointToProjection,
});
}
}
/* eslint-disable no-magic-numbers */
import { Feature, Map } from 'ol';
import { Fill, Style, Text } from 'ol/style';
import { Polygon, MultiPolygon } from 'ol/geom';
import getText from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getText';
import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon';
import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke';
import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill';
import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex';
import View from 'ol/View';
import {
WHITE_COLOR,
BLACK_COLOR,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
import CompartmentSquare, {
CompartmentSquareProps,
} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare';
import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords';
jest.mock('./getText');
jest.mock('./getMultiPolygon');
jest.mock('./getStroke');
jest.mock('./getPolygonCoords');
jest.mock('./getFill');
jest.mock('./rgbToHex');
describe('MapElement', () => {
let props: CompartmentSquareProps;
beforeEach(() => {
const dummyElement = document.createElement('div');
const mapInstance = new Map({
target: dummyElement,
view: new View({
zoom: 5,
minZoom: 3,
maxZoom: 7,
}),
});
props = {
x: 0,
y: 0,
width: 100,
height: 100,
zIndex: 1,
fillColor: WHITE_COLOR,
borderColor: BLACK_COLOR,
fontColor: BLACK_COLOR,
innerWidth: 1,
outerWidth: 2,
thickness: 12,
text: 'Test Text',
fontSize: 12,
nameX: 10,
nameY: 20,
nameHeight: 30,
nameWidth: 40,
nameVerticalAlign: 'MIDDLE',
nameHorizontalAlign: 'CENTER',
pointToProjection: jest.fn(),
mapInstance,
};
(getText as jest.Mock).mockReturnValue({
textCoords: [0, 0],
textStyle: new Style({
text: new Text({
text: props.text,
font: `bold ${props.fontSize}px Arial`,
fill: new Fill({
color: '#000',
}),
placement: 'point',
textAlign: 'center',
textBaseline: 'middle',
}),
}),
});
(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');
(getPolygonCoords as jest.Mock).mockReturnValue([
[10, 10],
[20, 20],
[30, 30],
]);
});
it('should initialize with correct default properties', () => {
const multiPolygon = new CompartmentSquare(props);
expect(multiPolygon.polygons.length).toBe(4);
expect(multiPolygon.multiPolygonFeature).toBeInstanceOf(Feature);
expect(multiPolygon.multiPolygonFeature.getGeometry()).toBeInstanceOf(MultiPolygon);
});
it('should apply correct styles to the feature', () => {
const multiPolygon = new CompartmentSquare(props);
const feature = multiPolygon.multiPolygonFeature;
const style = feature.getStyleFunction()?.call(multiPolygon, feature, 1);
if (Array.isArray(style)) {
expect(style.length).toBeGreaterThan(0);
} else {
expect(style).toBeInstanceOf(Style);
}
});
});
/* eslint-disable no-magic-numbers */
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import { MapInstance } from '@/types/map';
import {
ColorObject,
HorizontalAlign,
VerticalAlign,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
import {
BLACK_COLOR,
COMPARTMENT_SQUARE_POINTS,
WHITE_COLOR,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords';
import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment';
export type CompartmentSquareProps = {
x: number;
y: number;
width: number;
height: number;
zIndex: number;
fillColor?: ColorObject;
borderColor?: ColorObject;
fontColor?: ColorObject;
innerWidth?: number;
outerWidth?: number;
thickness?: number;
text?: string;
fontSize?: number;
nameX: number;
nameY: number;
nameHeight: number;
nameWidth: number;
nameVerticalAlign?: VerticalAlign;
nameHorizontalAlign?: HorizontalAlign;
pointToProjection: UsePointToProjectionResult;
mapInstance: MapInstance;
};
export default class CompartmentSquare extends Compartment {
constructor({
x,
y,
width,
height,
zIndex,
fillColor = WHITE_COLOR,
borderColor = BLACK_COLOR,
fontColor = BLACK_COLOR,
innerWidth = 1,
outerWidth = 2,
thickness = 12,
text = '',
fontSize = 12,
nameX,
nameY,
nameHeight,
nameWidth,
nameVerticalAlign = 'MIDDLE',
nameHorizontalAlign = 'CENTER',
pointToProjection,
mapInstance,
}: CompartmentSquareProps) {
super({
x,
y,
width,
height,
thickness,
outerWidth,
innerWidth,
zIndex,
text,
fontSize,
nameX,
nameY,
nameWidth,
nameHeight,
fontColor,
nameVerticalAlign,
nameHorizontalAlign,
fillColor,
borderColor,
pointToProjection,
mapInstance,
});
}
protected getCompartmentCoords(): void {
this.outerCoords = getPolygonCoords({
points: COMPARTMENT_SQUARE_POINTS,
x: this.x,
y: this.y,
height: this.height,
width: this.width,
pointToProjection: this.pointToProjection,
});
this.innerCoords = getPolygonCoords({
points: COMPARTMENT_SQUARE_POINTS,
x: this.x + this.thickness,
y: this.y + this.thickness,
height: this.height - 2 * this.thickness,
width: this.width - 2 * this.thickness,
pointToProjection: this.pointToProjection,
});
}
}
......@@ -107,14 +107,4 @@ describe('MapElement', () => {
expect(style).toBeInstanceOf(Style);
}
});
it('should update text style based on resolution', () => {
const multiPolygon = new MapElement(props);
const feature = multiPolygon.multiPolygonFeature;
multiPolygon.styleFunction(feature, 1000);
if (multiPolygon.textStyle) {
expect(multiPolygon.textStyle.getText()?.getScale()).toBe(1.22);
}
});
});
/* eslint-disable no-magic-numbers */
import { Style, Text } from 'ol/style';
import Feature, { FeatureLike } from 'ol/Feature';
import { MultiPolygon } from 'ol/geom';
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke';
import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill';
import Polygon from 'ol/geom/Polygon';
import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon';
import getText from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getText';
import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex';
import { BioShape, LineType, Modification, Shape } from '@/types/models';
import { MapInstance } from '@/types/map';
......@@ -20,6 +17,7 @@ import {
BLACK_COLOR,
WHITE_COLOR,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/BaseMultiPolygon';
export type MapElementProps = {
shapes: Array<Shape>;
......@@ -34,7 +32,7 @@ export type MapElementProps = {
lineWidth?: number;
lineType?: string;
text?: string;
fontSize?: string | number;
fontSize?: number;
nameX: number;
nameY: number;
nameHeight: number;
......@@ -48,16 +46,18 @@ export type MapElementProps = {
modifications?: Array<Modification>;
};
export default class MapElement {
textStyle: Style | undefined;
export default class MapElement extends BaseMultiPolygon {
shapes: Array<Shape>;
lineWidth: number;
polygons: Array<Polygon> = [];
lineType: string | undefined;
styles: Array<Style> = [];
bioShapes: Array<BioShape>;
polygonsTexts: Array<string> = [];
lineTypes: Array<LineType>;
multiPolygonFeature: Feature;
modifications: Array<Modification>;
constructor({
shapes,
......@@ -85,32 +85,44 @@ export default class MapElement {
lineTypes = [],
modifications = [],
}: MapElementProps) {
if (text) {
const { textCoords, textStyle } = getText({
text,
fontSize,
x: nameX,
y: nameY,
width: nameWidth,
height: nameHeight,
color: rgbToHex(fontColor),
zIndex,
verticalAlign: nameVerticalAlign,
horizontalAlign: nameHorizontalAlign,
pointToProjection,
});
this.styles.push(textStyle);
this.polygonsTexts.push(text);
this.polygons.push(new Polygon([[textCoords, textCoords]]));
}
super({
x,
y,
width,
height,
zIndex,
text,
fontSize,
nameX,
nameY,
nameWidth,
nameHeight,
fontColor,
nameVerticalAlign,
nameHorizontalAlign,
fillColor,
borderColor,
pointToProjection,
});
this.shapes = shapes;
this.lineWidth = lineWidth;
this.lineType = lineType;
this.bioShapes = bioShapes;
this.lineTypes = lineTypes;
this.modifications = modifications;
this.createPolygons();
this.drawText();
this.drawMultiPolygonFeature(mapInstance);
}
protected createPolygons(): void {
const multiPolygon: Array<Polygon> = [];
modifications.forEach(modification => {
this.modifications.forEach(modification => {
if (modification.state === null) {
return;
}
const shape = bioShapes.find(bioShape => bioShape.sboTerm === modification.sboTerm);
const shape = this.bioShapes.find(bioShape => bioShape.sboTerm === modification.sboTerm);
if (!shape) {
return;
}
......@@ -120,7 +132,7 @@ export default class MapElement {
width: modification.width,
height: modification.height,
shapes: shape.shapes,
pointToProjection,
pointToProjection: this.pointToProjection,
mirror: modification.direction && modification.direction === 'RIGHT',
});
multiPolygon.push(...multiPolygonModification.flat());
......@@ -151,12 +163,19 @@ export default class MapElement {
});
});
const elementMultiPolygon = getMultiPolygon({ x, y, width, height, shapes, pointToProjection });
const elementMultiPolygon = getMultiPolygon({
x: this.x,
y: this.y,
width: this.width,
height: this.height,
shapes: this.shapes,
pointToProjection: this.pointToProjection,
});
this.polygons = [...this.polygons, ...multiPolygon, ...elementMultiPolygon.flat()];
let lineDash: Array<number> = [];
if (lineType) {
const lineTypeFound = lineTypes.find(type => type.name === lineType);
if (this.lineType) {
const lineTypeFound = this.lineTypes.find(type => type.name === this.lineType);
if (lineTypeFound) {
lineDash = lineTypeFound.pattern;
}
......@@ -165,48 +184,11 @@ export default class MapElement {
this.styles.push(
new Style({
geometry: polygon,
stroke: getStroke({ color: rgbToHex(borderColor), width: lineWidth, lineDash }),
fill: getFill({ color: rgbToHex(fillColor) }),
zIndex,
stroke: getStroke({ color: rgbToHex(this.borderColor), width: this.lineWidth, lineDash }),
fill: getFill({ color: rgbToHex(this.fillColor) }),
zIndex: this.zIndex,
}),
);
});
this.multiPolygonFeature = new Feature({
geometry: new MultiPolygon([...this.polygons]),
getTextScale: (resolution: number): number => {
const maxZoom = mapInstance?.getView().getMaxZoom();
if (maxZoom) {
const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom);
if (minResolution) {
return Math.round((minResolution / resolution) * 100) / 100;
}
}
return 1;
},
});
this.multiPolygonFeature.setStyle(this.styleFunction.bind(this));
}
styleFunction(feature: FeatureLike, resolution: number): Style | Array<Style> | void {
const getTextScale = feature.get('getTextScale');
let textScale = 1;
if (getTextScale instanceof Function) {
textScale = getTextScale(resolution);
}
let index = 0;
this.styles.forEach(style => {
if (style.getText()) {
if (textScale > 0.3) {
style.getText()?.setScale(textScale);
style.getText()?.setText(this.polygonsTexts[index]);
index += 1;
} else {
style.getText()?.setText(undefined);
}
}
});
return this.styles;
}
}
......@@ -3,24 +3,10 @@ import getCoordsX from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/
import getCoordsY from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsY';
import { Coordinate } from 'ol/coordinate';
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
type EllipseCenter = {
type: string;
absoluteX: number;
absoluteY: number;
relativeX: number;
relativeY: number;
relativeHeightForX: number | null;
relativeWidthForY: number | null;
};
type EllipseRadius = {
type: string;
absoluteX: number;
absoluteY: number;
relativeX: number;
relativeY: number;
};
import {
EllipseCenter,
EllipseRadius,
} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
export default function getEllipseCoords({
x,
......
......@@ -18,7 +18,7 @@ describe('getText', () => {
});
expect(textCoords).toEqual([50, 50]);
expect(textStyle.getText()?.getFont()).toEqual('bold 12px Arial');
expect(textStyle.getText()?.getFont()).toEqual('12pt Arial');
});
it('should return correct text coordinates and style when text is aligned to bottom', () => {
......@@ -36,6 +36,6 @@ describe('getText', () => {
expect(textCoords).toEqual([70, 121]);
expect(textStyle.getText()?.getFont()).toEqual('bold 18px Arial');
expect(textStyle.getText()?.getFont()).toEqual('18pt Arial');
});
});
......@@ -61,19 +61,22 @@ export default function getText({
geometry: (feature: FeatureLike): Geometry | RenderFeature | undefined => {
const geometry = feature.getGeometry();
if (geometry && geometry.getType() === 'MultiPolygon') {
return (geometry as MultiPolygon).getPolygon(0).getInteriorPoint();
return (geometry as MultiPolygon)
.getPolygon((geometry as MultiPolygon).getPolygons().length - 1)
.getInteriorPoint();
}
return undefined;
},
text: new Text({
text,
font: `bold ${fontSize}px Arial`,
font: `${fontSize}pt Arial`,
fill: new Fill({
color,
}),
placement: 'point',
textAlign: 'center',
textAlign: horizontalAlign.toLowerCase() as CanvasTextAlign,
textBaseline: 'middle',
overflow: true,
}),
zIndex,
});
......
......@@ -37,6 +37,10 @@ export const modelElementSchema = z.object({
formerSymbols: z.array(z.string()),
activity: z.boolean().optional(),
lineWidth: z.number().optional(),
innerWidth: z.number().optional(),
outerWidth: z.number().optional(),
thickness: z.number().optional(),
shape: z.enum(['SQUARE_COMPARTMENT', 'OVAL_COMPARTMENT', 'PATHWAY']).optional(),
complex: z.number().nullable().optional(),
initialAmount: z.number().nullable().optional(),
charge: z.number().nullable().optional(),
......
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