Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • minerva/frontend
1 result
Show changes
Commits on Source (4)
Showing
with 465 additions and 22 deletions
......@@ -50,6 +50,14 @@ export const NavBar = (): JSX.Element => {
}
};
const toggleDrawerLayers = (): void => {
if (store.getState().drawer.isOpen && store.getState().drawer.drawerName === 'layers') {
dispatch(closeDrawer());
} else {
dispatch(openDrawer('layers'));
}
};
const toggleDrawerLegend = (): void => {
if (store.getState().legend.isOpen) {
dispatch(closeLegend());
......@@ -77,6 +85,7 @@ export const NavBar = (): JSX.Element => {
</a>
<IconButton icon="plugin" onClick={toggleDrawerPlugins} title="Available plugins" />
<IconButton icon="export" onClick={toggleDrawerExport} title="Export" />
<IconButton icon="layers" onClick={toggleDrawerLayers} title="Layers" />
</div>
<div className="flex flex-col gap-[10px]">
<IconButton icon="legend" onClick={toggleDrawerLegend} title="Legend" />
......
......@@ -3,6 +3,7 @@ import { drawerSelector } from '@/redux/drawer/drawer.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { twMerge } from 'tailwind-merge';
import { CommentDrawer } from '@/components/Map/Drawer/CommentDrawer';
import { LayersDrawer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawer.component';
import { AvailablePluginsDrawer } from './AvailablePluginsDrawer';
import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component';
import { ExportDrawer } from './ExportDrawer';
......@@ -32,6 +33,7 @@ export const Drawer = (): JSX.Element => {
{isOpen && drawerName === 'export' && <ExportDrawer />}
{isOpen && drawerName === 'available-plugins' && <AvailablePluginsDrawer />}
{isOpen && drawerName === 'comment' && <CommentDrawer />}
{isOpen && drawerName === 'layers' && <LayersDrawer />}
</div>
);
};
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { StoreType } from '@/redux/store';
import { render, screen } from '@testing-library/react';
import { openedExportDrawerFixture } from '@/redux/drawer/drawerFixture';
import { LayersDrawer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawer.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<LayersDrawer />
</Wrapper>,
),
{
store,
}
);
};
describe('ExportDrawer - component', () => {
it('should display drawer heading', () => {
renderComponent();
expect(screen.getByText('Layers')).toBeInTheDocument();
});
it('should close drawer after clicking close button', () => {
const { store } = renderComponent({
drawer: openedExportDrawerFixture,
});
const closeButton = screen.getByRole('close-drawer-button');
closeButton.click();
const {
drawer: { isOpen },
} = store.getState();
expect(isOpen).toBe(false);
});
});
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { DrawerHeading } from '@/shared/DrawerHeading';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { layersSelector, layersVisibilitySelector } from '@/redux/layers/layers.selectors';
import { Switch } from '@/shared/Switch';
import { setLayerVisibility } from '@/redux/layers/layers.slice';
export const LayersDrawer = (): JSX.Element => {
const layers = useAppSelector(layersSelector);
const layersVisibility = useAppSelector(layersVisibilitySelector);
const dispatch = useAppDispatch();
return (
<div data-testid="layers-drawer" className="h-full max-h-full">
<DrawerHeading title="Layers" />
<div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto px-6">
{layers.map(layer => (
<div key={layer.details.id} className="flex items-center justify-between border-b p-4">
<h1>{layer.details.name}</h1>
<Switch
isChecked={layersVisibility[layer.details.layerId]}
onToggle={value =>
dispatch(setLayerVisibility({ visible: value, layerId: layer.details.layerId }))
}
/>
</div>
))}
</div>
</div>
);
};
export { LayersDrawer } from './LayersDrawer.component';
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;
};
/* eslint-disable no-magic-numbers */
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import VectorLayer from 'ol/layer/Vector';
import { initialMapStateFixture } from '@/redux/map/map.fixtures';
import { BACKGROUND_INITIAL_STATE_MOCK } from '@/redux/backgrounds/background.mock';
import { Map } from 'ol';
import { useOlMapAdditionalLayers } from '@/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers';
describe('useOlMapAdditionalLayers - util', () => {
it('should return VectorLayer', () => {
const { Wrapper } = getReduxWrapperWithStore({
map: initialMapStateFixture,
backgrounds: BACKGROUND_INITIAL_STATE_MOCK,
});
const dummyElement = document.createElement('div');
const mapInstance = new Map({ target: dummyElement });
const { result } = renderHook(() => useOlMapAdditionalLayers(mapInstance), {
wrapper: Wrapper,
});
expect(result.current).toBeInstanceOf(Array<VectorLayer>);
});
});
/* eslint-disable no-magic-numbers */
import { Feature } from 'ol';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { currentModelIdSelector } from '@/redux/models/models.selectors';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getLayers } from '@/redux/layers/layers.thunks';
import { layersSelector, layersVisibilitySelector } from '@/redux/layers/layers.selectors';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { MapInstance } from '@/types/map';
import { LineString, MultiPolygon, Point } from 'ol/geom';
import Polygon from 'ol/geom/Polygon';
import Layer from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer';
import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors';
export const useOlMapAdditionalLayers = (
mapInstance: MapInstance,
): Array<
VectorLayer<
VectorSource<Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>
>
> => {
const dispatch = useAppDispatch();
const currentModelId = useSelector(currentModelIdSelector);
const mapLayers = useSelector(layersSelector);
const layersVisibility = useSelector(layersVisibilitySelector);
const lineTypes = useSelector(lineTypesSelector);
const arrowTypes = useSelector(arrowTypesSelector);
const pointToProjection = usePointToProjection();
useEffect(() => {
dispatch(getLayers(currentModelId));
}, [currentModelId, dispatch]);
const vectorLayers = useMemo(() => {
return mapLayers.map(layer => {
const additionalLayer = new Layer({
texts: layer.texts,
rects: layer.rects,
ovals: layer.ovals,
lines: layer.lines,
visible: layer.details.visible,
layerId: layer.details.layerId,
lineTypes,
arrowTypes,
mapInstance,
pointToProjection,
});
return additionalLayer.vectorLayer;
});
}, [arrowTypes, lineTypes, mapInstance, mapLayers, pointToProjection]);
useEffect(() => {
vectorLayers.forEach(layer => {
const layerId = layer.get('id');
if (layerId && layersVisibility[layerId] !== undefined) {
layer.setVisible(layersVisibility[layerId]);
}
});
}, [layersVisibility, vectorLayers]);
return vectorLayers;
};
......@@ -4,9 +4,9 @@ import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { useEffect, useMemo } from 'react';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import CustomMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/CustomMultiPolygon';
import MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement';
import { useSelector } from 'react-redux';
import { shapesSelector } from '@/redux/shapes/shapes.selectors';
import { bioShapesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors';
import { MapInstance } from '@/types/map';
import {
HorizontalAlign,
......@@ -16,6 +16,9 @@ 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/elements/CompartmentSquare';
import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle';
import { ModelElement } from '@/types/models';
export const useOlMapReactionsLayer = ({
mapInstance,
......@@ -30,14 +33,18 @@ export const useOlMapReactionsLayer = ({
}, [currentModelId, dispatch]);
const pointToProjection = usePointToProjection();
const shapes = useSelector(shapesSelector);
const shapes = useSelector(bioShapesSelector);
const lineTypes = useSelector(lineTypesSelector);
const elements = useMemo(() => {
if (modelElements) {
return modelElements.content.map(element => {
const shape = shapes.data.find(bioShape => bioShape.sboTerm === element.sboTerm);
if (shape) {
return new CustomMultiPolygon({
const elements: Array<MapElement | CompartmentCircle | CompartmentSquare> = useMemo(() => {
if (!modelElements || !shapes) return [];
const validElements: Array<MapElement | CompartmentCircle | CompartmentSquare> = [];
modelElements.content.forEach((element: ModelElement) => {
const shape = shapes.find(bioShape => bioShape.sboTerm === element.sboTerm);
if (shape) {
validElements.push(
new MapElement({
shapes: shape.shapes,
x: element.x,
y: element.y,
......@@ -49,26 +56,59 @@ export const useOlMapReactionsLayer = ({
height: element.height,
zIndex: element.z,
lineWidth: element.lineWidth,
lineType: element.borderLineType,
fontColor: element.fontColor,
fillColor: element.fillColor,
borderColor: element.borderColor,
nameVerticalAlign: element.nameVerticalAlign as VerticalAlign,
nameHorizontalAlign: element.nameHorizontalAlign as HorizontalAlign,
homodimer: element.homodimer,
activity: element.activity,
text: element.name,
fontSize: element.fontSize,
pointToProjection,
mapInstance,
});
modifications: element.modificationResidues,
lineTypes,
bioShapes: shapes,
}),
);
} else if (element.sboTerm === 'SBO:0000290') {
const compartmentProps = {
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 undefined;
});
}
return [];
}, [mapInstance, pointToProjection, shapes.data, modelElements]);
}
});
return validElements;
}, [modelElements, shapes, pointToProjection, mapInstance, lineTypes]);
const features = useMemo(() => {
return elements
.filter((element): element is CustomMultiPolygon => element !== undefined)
.map(element => element.multiPolygonFeature);
return elements.map(element => element.multiPolygonFeature);
}, [elements]);
const vectorSource = useMemo(() => {
......
/* eslint-disable no-magic-numbers */
import { MapInstance } from '@/types/map';
import { useOlMapWhiteCardLayer } from '@/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapWhiteCardLayer';
import { useOlMapAdditionalLayers } from '@/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers';
import { MapConfig } from '../../MapViewerVector.types';
import { useOlMapReactionsLayer } from './reactionsLayer/useOlMapReactionsLayer';
......@@ -11,6 +12,7 @@ interface UseOlMapLayersInput {
export const useOlMapVectorLayers = ({ mapInstance }: UseOlMapLayersInput): MapConfig['layers'] => {
const reactionsLayer = useOlMapReactionsLayer({ mapInstance });
const whiteCardLayer = useOlMapWhiteCardLayer();
const additionalLayers = useOlMapAdditionalLayers(mapInstance);
return [whiteCardLayer, reactionsLayer];
return [whiteCardLayer, reactionsLayer, ...additionalLayers];
};
/* eslint-disable no-magic-numbers */
import { Coordinate } from 'ol/coordinate';
import getCentroid from './getCentroid';
describe('getCentroid', () => {
it('should return a centroid for coordinates', () => {
const coords: Array<Coordinate> = [
[0, 0],
[20, 0],
[35, 10],
[20, 20],
[10, 30],
[0, 20],
];
const result = getCentroid(coords);
expect(result[0]).toBeCloseTo(13.46);
expect(result[1]).toBeCloseTo(12.05);
});
});
/* eslint-disable no-magic-numbers */
import { Coordinate } from 'ol/coordinate';
export default function getCentroid(coords: Array<Coordinate>): Coordinate {
let area = 0;
let centroidX = 0;
let centroidY = 0;
const numPoints = coords.length;
for (let i = 0; i < numPoints; i += 1) {
const x1 = coords[i][0];
const y1 = coords[i][1];
const x2 = coords[(i + 1) % numPoints][0];
const y2 = coords[(i + 1) % numPoints][1];
const crossProduct = x1 * y2 - x2 * y1;
area += crossProduct;
centroidX += (x1 + x2) * crossProduct;
centroidY += (y1 + y2) * crossProduct;
}
area /= 2;
centroidX /= 6 * area;
centroidY /= 6 * area;
return [centroidX, centroidY];
}
/* eslint-disable no-magic-numbers */
import getCoordsX from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsX';
import getCoordsY from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsY';
import getCoordsX from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsX';
import getCoordsY from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsY';
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import { ShapeCurvePoint } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types';
import getCurveCoords from './getCurveCoords';
......