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

Resolve MIN-35 "hypothetical option"

parent 275ee483
No related branches found
No related tags found
1 merge request!259Resolve MIN-35 "hypothetical option"
Showing
with 416 additions and 23 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 MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement';
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 MapElement({
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,28 +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,
bioShapes: shapes.data,
});
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 MapElement => 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 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';
......
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