From 7b44a3113c13856aad6c1bbe675430e79ef81b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Wed, 23 Oct 2024 11:10:28 +0200 Subject: [PATCH] feat(vector-map): add drawer to control individual layers + add support for text layer --- .../NavBar/NavBar.component.tsx | 9 + .../Map/Drawer/Drawer.component.tsx | 2 + .../LayersDrawer.component.test.tsx | 46 +++ .../LayersDrawer/LayersDrawer.component.tsx | 31 ++ .../Map/Drawer/LayersDrawer/index.ts | 1 + .../useOlMapAdditionalLayers.test.ts | 25 ++ .../useOlMapAdditionalLayers.ts | 65 ++++ .../reactionsLayer/useOlMapReactionsLayer.ts | 9 +- .../utils/config/useOlMapVectorLayers.ts | 4 +- .../{ => coords}/getBezierCurve.test.ts | 0 .../shapes/{ => coords}/getBezierCurve.ts | 0 .../shapes/{ => coords}/getCentroid.test.ts | 0 .../utils/shapes/{ => coords}/getCentroid.ts | 0 .../shapes/{ => coords}/getCoordsX.test.ts | 0 .../utils/shapes/{ => coords}/getCoordsX.ts | 0 .../shapes/{ => coords}/getCoordsY.test.ts | 0 .../utils/shapes/{ => coords}/getCoordsY.ts | 0 .../{ => coords}/getCurveCoords.test.ts | 4 +- .../shapes/{ => coords}/getCurveCoords.ts | 4 +- .../{ => coords}/getEllipseCoords.test.ts | 4 +- .../shapes/{ => coords}/getEllipseCoords.ts | 52 +-- .../{ => coords}/getPolygonCoords.test.ts | 8 +- .../shapes/{ => coords}/getPolygonCoords.ts | 8 +- .../utils/shapes/coords/getRotation.test.ts | 19 ++ .../utils/shapes/coords/getRotation.ts | 6 + .../shapes/{ => elements}/BaseMultiPolygon.ts | 23 +- .../shapes/{ => elements}/Compartment.ts | 8 +- .../{ => elements}/CompartmentCircle.test.ts | 34 +- .../{ => elements}/CompartmentCircle.ts | 4 +- .../{ => elements}/CompartmentSquare.test.ts | 34 +- .../{ => elements}/CompartmentSquare.ts | 4 +- .../shapes/{ => elements}/MapElement.test.ts | 30 +- .../utils/shapes/{ => elements}/MapElement.ts | 31 +- .../{ => elements}/getMultiPolygon.test.ts | 8 +- .../shapes/{ => elements}/getMultiPolygon.ts | 6 +- .../utils/shapes/getStroke.test.ts | 21 -- .../utils/shapes/getText.test.ts | 41 --- .../MapViewerVector/utils/shapes/getText.ts | 85 ----- .../utils/shapes/layer/Layer.test.ts | 145 ++++++++ .../utils/shapes/layer/Layer.ts | 320 ++++++++++++++++++ .../utils/shapes/{ => style}/getFill.test.ts | 0 .../utils/shapes/{ => style}/getFill.ts | 0 .../utils/shapes/style/getStroke.test.ts | 27 ++ .../utils/shapes/{ => style}/getStroke.ts | 3 + .../utils/shapes/style/getStyle.test.ts | 52 +++ .../utils/shapes/style/getStyle.ts | 38 +++ .../utils/shapes/{ => style}/rgbToHex.test.ts | 0 .../utils/shapes/{ => style}/rgbToHex.ts | 0 .../utils/shapes/text/Text.test.ts | 73 ++++ .../MapViewerVector/utils/shapes/text/Text.ts | 115 +++++++ .../utils/shapes/text/getTextCoords.test.ts | 20 ++ .../utils/shapes/text/getTextCoords.ts | 48 +++ .../utils/shapes/text/getTextStyle.test.ts | 26 ++ .../utils/shapes/text/getTextStyle.ts | 32 ++ src/models/arrowTypeSchema.ts | 7 + src/models/fixtures/arrowTypesFixture.ts | 10 + src/models/fixtures/layerLinesFixture.ts | 10 + src/models/fixtures/layerOvalsFixture.ts | 10 + src/models/fixtures/layerRectsFixture.ts | 10 + src/models/fixtures/layerTextsFixture.ts | 10 + src/models/fixtures/layersFixture.ts | 10 + src/models/fixtures/modelElementsFixture.ts | 5 +- src/models/layerLineSchema.ts | 15 + src/models/layerOvalSchema.ts | 15 + src/models/layerRectSchema.ts | 16 + src/models/layerSchema.ts | 10 + src/models/layerTextSchema.ts | 22 ++ src/models/modelElementsSchema.ts | 11 - src/redux/apiPath.ts | 10 + src/redux/layers/layers.constants.ts | 1 + src/redux/layers/layers.mock.ts | 11 + src/redux/layers/layers.reducers.test.ts | 128 +++++++ src/redux/layers/layers.reducers.ts | 30 ++ src/redux/layers/layers.selectors.ts | 12 + src/redux/layers/layers.slice.ts | 18 + src/redux/layers/layers.thunks.test.ts | 68 ++++ src/redux/layers/layers.thunks.ts | 65 ++++ src/redux/layers/layers.types.ts | 21 ++ .../modelElements/modelElements.thunks.ts | 8 +- src/redux/root/init.thunks.ts | 4 +- src/redux/root/root.fixtures.ts | 2 + src/redux/shapes/shapes.constants.ts | 2 + src/redux/shapes/shapes.mock.ts | 5 + src/redux/shapes/shapes.reducers.test.ts | 47 ++- src/redux/shapes/shapes.reducers.ts | 15 +- src/redux/shapes/shapes.selectors.ts | 5 + src/redux/shapes/shapes.slice.ts | 7 +- src/redux/shapes/shapes.thunks.test.ts | 21 +- src/redux/shapes/shapes.thunks.ts | 18 +- src/redux/shapes/shapes.types.ts | 5 +- src/redux/store.ts | 2 + src/shared/Icon/Icon.component.tsx | 2 + src/shared/Icon/Icons/LayersIcon.tsx | 39 +++ src/types/drawerName.ts | 3 +- src/types/iconTypes.ts | 1 + src/types/models.ts | 21 +- 96 files changed, 1958 insertions(+), 299 deletions(-) create mode 100644 src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.test.tsx create mode 100644 src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx create mode 100644 src/components/Map/Drawer/LayersDrawer/index.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getBezierCurve.test.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getBezierCurve.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCentroid.test.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCentroid.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCoordsX.test.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCoordsX.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCoordsY.test.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCoordsY.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCurveCoords.test.ts (96%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getCurveCoords.ts (95%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getEllipseCoords.test.ts (95%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getEllipseCoords.ts (61%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getPolygonCoords.test.ts (95%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => coords}/getPolygonCoords.ts (90%) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.ts rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/BaseMultiPolygon.ts (88%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/Compartment.ts (95%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/CompartmentCircle.test.ts (79%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/CompartmentCircle.ts (96%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/CompartmentSquare.test.ts (79%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/CompartmentSquare.ts (96%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/MapElement.test.ts (80%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/MapElement.ts (92%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/getMultiPolygon.test.ts (96%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => elements}/getMultiPolygon.ts (91%) delete mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke.test.ts delete mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.test.ts delete mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => style}/getFill.test.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => style}/getFill.ts (100%) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke.test.ts rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => style}/getStroke.ts (79%) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => style}/rgbToHex.test.ts (100%) rename src/components/Map/MapViewer/MapViewerVector/utils/shapes/{ => style}/rgbToHex.ts (100%) create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.test.ts create mode 100644 src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts create mode 100644 src/models/arrowTypeSchema.ts create mode 100644 src/models/fixtures/arrowTypesFixture.ts create mode 100644 src/models/fixtures/layerLinesFixture.ts create mode 100644 src/models/fixtures/layerOvalsFixture.ts create mode 100644 src/models/fixtures/layerRectsFixture.ts create mode 100644 src/models/fixtures/layerTextsFixture.ts create mode 100644 src/models/fixtures/layersFixture.ts create mode 100644 src/models/layerLineSchema.ts create mode 100644 src/models/layerOvalSchema.ts create mode 100644 src/models/layerRectSchema.ts create mode 100644 src/models/layerSchema.ts create mode 100644 src/models/layerTextSchema.ts delete mode 100644 src/models/modelElementsSchema.ts create mode 100644 src/redux/layers/layers.constants.ts create mode 100644 src/redux/layers/layers.mock.ts create mode 100644 src/redux/layers/layers.reducers.test.ts create mode 100644 src/redux/layers/layers.reducers.ts create mode 100644 src/redux/layers/layers.selectors.ts create mode 100644 src/redux/layers/layers.slice.ts create mode 100644 src/redux/layers/layers.thunks.test.ts create mode 100644 src/redux/layers/layers.thunks.ts create mode 100644 src/redux/layers/layers.types.ts create mode 100644 src/shared/Icon/Icons/LayersIcon.tsx diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.tsx index 7eb7b2b0..644b830c 100644 --- a/src/components/FunctionalArea/NavBar/NavBar.component.tsx +++ b/src/components/FunctionalArea/NavBar/NavBar.component.tsx @@ -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" /> diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx index 71f10406..b18e949f 100644 --- a/src/components/Map/Drawer/Drawer.component.tsx +++ b/src/components/Map/Drawer/Drawer.component.tsx @@ -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> ); }; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.test.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.test.tsx new file mode 100644 index 00000000..e40ea751 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.test.tsx @@ -0,0 +1,46 @@ +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); + }); +}); diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx new file mode 100644 index 00000000..7e614298 --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -0,0 +1,31 @@ +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> + ); +}; diff --git a/src/components/Map/Drawer/LayersDrawer/index.ts b/src/components/Map/Drawer/LayersDrawer/index.ts new file mode 100644 index 00000000..c608bace --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/index.ts @@ -0,0 +1 @@ +export { LayersDrawer } from './LayersDrawer.component'; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.test.ts new file mode 100644 index 00000000..68151ba0 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.test.ts @@ -0,0 +1,25 @@ +/* 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>); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts new file mode 100644 index 00000000..98a4276a --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -0,0 +1,65 @@ +/* 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; +}; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index d61918dc..9bc0d809 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -4,7 +4,7 @@ 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 { bioShapesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors'; import { MapInstance } from '@/types/map'; @@ -16,8 +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/CompartmentSquare'; -import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle'; +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, @@ -39,7 +40,7 @@ export const useOlMapReactionsLayer = ({ if (!modelElements || !shapes) return []; const validElements: Array<MapElement | CompartmentCircle | CompartmentSquare> = []; - modelElements.content.forEach(element => { + modelElements.content.forEach((element: ModelElement) => { const shape = shapes.find(bioShape => bioShape.sboTerm === element.sboTerm); if (shape) { validElements.push( diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts index 52cf8ab0..7ebb6e70 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/useOlMapVectorLayers.ts @@ -1,6 +1,7 @@ /* 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]; }; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getBezierCurve.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBezierCurve.test.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getBezierCurve.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBezierCurve.test.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getBezierCurve.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBezierCurve.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getBezierCurve.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBezierCurve.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCentroid.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCentroid.test.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCentroid.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCentroid.test.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCentroid.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCentroid.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCentroid.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCentroid.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsX.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsX.test.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsX.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsX.test.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsX.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsX.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsX.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsX.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsY.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsY.test.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsY.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsY.test.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsY.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsY.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsY.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsY.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords.test.ts similarity index 96% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords.test.ts index 8ce8b569..d212e9ea 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords.test.ts @@ -1,6 +1,6 @@ /* 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'; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords.ts similarity index 95% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords.ts index 2b695634..ebfed6b6 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords.ts @@ -1,7 +1,7 @@ import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Coordinate } from 'ol/coordinate'; -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 { ShapeCurvePoint } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; export default function getCurveCoords({ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords.test.ts similarity index 95% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords.test.ts index cf6f4c49..4a0d514a 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords.test.ts @@ -1,6 +1,6 @@ /* 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 getEllipseCoords from './getEllipseCoords'; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords.ts similarity index 61% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords.ts index b3a307a0..45a7df69 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords.ts @@ -1,6 +1,6 @@ /* 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 { Coordinate } from 'ol/coordinate'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { @@ -20,31 +20,39 @@ export default function getEllipseCoords({ }: { x: number; y: number; - center: EllipseCenter; - radius: EllipseRadius; + center?: EllipseCenter; + radius?: EllipseRadius; height: number; width: number; pointToProjection: UsePointToProjectionResult; points?: number; }): Array<Coordinate> { - const centerX = getCoordsX( - x, - center.absoluteX, - center.relativeX, - center.relativeHeightForX, - height, - width, - ); - const centerY = getCoordsY( - y, - center.absoluteY, - center.relativeY, - center.relativeWidthForY, - height, - width, - ); - const radiusX = radius.absoluteX + (radius.relativeX * width) / 100; - const radiusY = radius.absoluteY + (radius.relativeY * height) / 100; + let centerX = x; + let centerY = y; + let radiusX = width / 2; + let radiusY = height / 2; + if (center) { + centerX = getCoordsX( + x, + center.absoluteX, + center.relativeX, + center.relativeHeightForX, + height, + width, + ); + centerY = getCoordsY( + y, + center.absoluteY, + center.relativeY, + center.relativeWidthForY, + height, + width, + ); + } + if (radius) { + radiusX = radius.absoluteX + (radius.relativeX * width) / 100; + radiusY = radius.absoluteY + (radius.relativeY * height) / 100; + } let angle; let coordsX; let coordsY; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords.test.ts similarity index 95% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords.test.ts index e1911f7b..d88a2c04 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords.test.ts @@ -1,8 +1,8 @@ /* 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 getCurveCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords'; -import getBezierCurve from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getBezierCurve'; +import getCoordsX from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsX'; +import getCoordsY from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsY'; +import getCurveCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords'; +import getBezierCurve from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBezierCurve'; import { ShapePoint, ShapeCurvePoint, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords.ts similarity index 90% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords.ts index b60b0016..dac21118 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords.ts @@ -1,10 +1,10 @@ /* eslint-disable no-magic-numbers */ import { Coordinate } from 'ol/coordinate'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; -import getBezierCurve from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getBezierCurve'; -import getCoordsX from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsX'; -import getCoordsY from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCoordsY'; -import getCurveCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCurveCoords'; +import getBezierCurve from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBezierCurve'; +import getCoordsX from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsX'; +import getCoordsY from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCoordsY'; +import getCurveCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCurveCoords'; import { ShapeCurvePoint, ShapePoint, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.test.ts new file mode 100644 index 00000000..363add1a --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.test.ts @@ -0,0 +1,19 @@ +/* eslint-disable no-magic-numbers */ +import getRotation from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation'; +import { Coordinate } from 'ol/coordinate'; + +const testCases: Array<[Coordinate, Coordinate, number]> = [ + [[0, 0], [1, 1], -0.785], + [[1, 1], [1, 2], -1.57], + [[2, 2], [3, 0], 1.107], +]; + +describe('getRotation', () => { + it.each(testCases)( + 'should return the correct rotation for start: %s and end: %s', + (start: Coordinate, end: Coordinate, expected: number) => { + const result = getRotation(start, end); + expect(result).toBeCloseTo(expected); + }, + ); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.ts new file mode 100644 index 00000000..5a647fce --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getRotation.ts @@ -0,0 +1,6 @@ +/* eslint-disable no-magic-numbers */ +import { Coordinate } from 'ol/coordinate'; + +export default function getRotation(start: Coordinate, end: Coordinate): number { + return Math.atan2(-end[1] + start[1], end[0] - start[0]); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts similarity index 88% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/BaseMultiPolygon.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 62ed6ea9..30ece3e9 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -10,8 +10,9 @@ import { 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'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; export interface BaseMapElementProps { x: number; @@ -118,22 +119,28 @@ export default abstract class BaseMultiPolygon { protected drawText(): void { if (this.text) { - const { textCoords, textStyle } = getText({ - text: this.text, - fontSize: this.fontSize, + const textCoords = getTextCoords({ x: this.nameX, y: this.nameY, width: this.nameWidth, height: this.nameHeight, - color: rgbToHex(this.fontColor), - zIndex: this.zIndex, + fontSize: this.fontSize, verticalAlign: this.nameVerticalAlign, horizontalAlign: this.nameHorizontalAlign, pointToProjection: this.pointToProjection, }); + const textPolygon = new Polygon([[textCoords, textCoords]]); + const textStyle = getTextStyle({ + text: this.text, + fontSize: this.fontSize, + color: rgbToHex(this.fontColor), + zIndex: this.zIndex, + horizontalAlign: this.nameHorizontalAlign, + }); + textStyle.setGeometry(textPolygon); this.styles.push(textStyle); this.polygonsTexts.push(this.text); - this.polygons.push(new Polygon([[textCoords, textCoords]])); + this.polygons.push(textPolygon); } } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts similarity index 95% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index deb5a5c5..786f8c1e 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -5,13 +5,13 @@ import { HorizontalAlign, VerticalAlign, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; -import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/BaseMultiPolygon'; +import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/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 getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; import { MapInstance } from '@/types/map'; export interface CompartmentProps { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts similarity index 79% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts index cfc57c56..47c47d38 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.test.ts @@ -2,11 +2,11 @@ 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 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'; import View from 'ol/View'; import { WHITE_COLOR, @@ -14,15 +14,17 @@ import { } 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'; +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; +import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; -jest.mock('./getText'); +jest.mock('../text/getTextStyle'); +jest.mock('../text/getTextCoords'); jest.mock('./getMultiPolygon'); -jest.mock('./getStroke'); -jest.mock('./getEllipseCoords'); -jest.mock('./getFill'); -jest.mock('./rgbToHex'); +jest.mock('../style/getStroke'); +jest.mock('../coords/getEllipseCoords'); +jest.mock('../style/getFill'); +jest.mock('../style/rgbToHex'); describe('MapElement', () => { let props: CompartmentCircleProps; @@ -61,9 +63,8 @@ describe('MapElement', () => { mapInstance, }; - (getText as jest.Mock).mockReturnValue({ - textCoords: [0, 0], - textStyle: new Style({ + (getTextStyle as jest.Mock).mockReturnValue( + new Style({ text: new Text({ text: props.text, font: `bold ${props.fontSize}px Arial`, @@ -75,7 +76,8 @@ describe('MapElement', () => { textBaseline: 'middle', }), }), - }); + ); + (getTextCoords as jest.Mock).mockReturnValue([10, 10]); (getMultiPolygon as jest.Mock).mockReturnValue([ new Polygon([ [ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts similarity index 96% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts index f5f3c51a..5665fc29 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentCircle.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle.ts @@ -12,8 +12,8 @@ import { 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'; +import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; +import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; export type CompartmentCircleProps = { x: number; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts similarity index 79% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts index d9913b38..5fd6ac55 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.test.ts @@ -2,11 +2,11 @@ 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 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'; import View from 'ol/View'; import { WHITE_COLOR, @@ -14,15 +14,17 @@ import { } 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'; +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; +import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; -jest.mock('./getText'); +jest.mock('../text/getTextStyle'); jest.mock('./getMultiPolygon'); -jest.mock('./getStroke'); -jest.mock('./getPolygonCoords'); -jest.mock('./getFill'); -jest.mock('./rgbToHex'); +jest.mock('../text/getTextCoords'); +jest.mock('../style/getStroke'); +jest.mock('../coords/getPolygonCoords'); +jest.mock('../style/getFill'); +jest.mock('../style/rgbToHex'); describe('MapElement', () => { let props: CompartmentSquareProps; @@ -61,9 +63,8 @@ describe('MapElement', () => { mapInstance, }; - (getText as jest.Mock).mockReturnValue({ - textCoords: [0, 0], - textStyle: new Style({ + (getTextStyle as jest.Mock).mockReturnValue( + new Style({ text: new Text({ text: props.text, font: `bold ${props.fontSize}px Arial`, @@ -75,7 +76,8 @@ describe('MapElement', () => { textBaseline: 'middle', }), }), - }); + ); + (getTextCoords as jest.Mock).mockReturnValue([10, 10]); (getMultiPolygon as jest.Mock).mockReturnValue([ new Polygon([ [ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts similarity index 96% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts index 79962ee7..d868bc11 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/CompartmentSquare.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare.ts @@ -11,8 +11,8 @@ import { 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'; +import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; +import Compartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment'; export type CompartmentSquareProps = { x: number; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts similarity index 80% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts index 004f90cd..51aaeb65 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.test.ts @@ -2,25 +2,27 @@ 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 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'; import MapElement, { MapElementProps, -} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement'; +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement'; import View from 'ol/View'; import { WHITE_COLOR, BLACK_COLOR, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; -jest.mock('./getText'); +jest.mock('../text/getTextStyle'); +jest.mock('../text/getTextCoords'); jest.mock('./getMultiPolygon'); -jest.mock('./getStroke'); -jest.mock('./getFill'); -jest.mock('./rgbToHex'); +jest.mock('../style/getStroke'); +jest.mock('../style/getFill'); +jest.mock('../style/rgbToHex'); describe('MapElement', () => { let props: MapElementProps; @@ -58,9 +60,8 @@ describe('MapElement', () => { mapInstance, }; - (getText as jest.Mock).mockReturnValue({ - textCoords: [0, 0], - textStyle: new Style({ + (getTextStyle as jest.Mock).mockReturnValue( + new Style({ text: new Text({ text: props.text, font: `bold ${props.fontSize}px Arial`, @@ -72,7 +73,8 @@ describe('MapElement', () => { textBaseline: 'middle', }), }), - }); + ); + (getTextCoords as jest.Mock).mockReturnValue([10, 10]); (getMultiPolygon as jest.Mock).mockReturnValue([ new Polygon([ [ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts similarity index 92% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts index 2e72e1a7..025ec2d8 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/MapElement.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement.ts @@ -1,11 +1,11 @@ /* eslint-disable no-magic-numbers */ import { Style, Text } from 'ol/style'; 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 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/getMultiPolygon'; -import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex'; +import getMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; import { BioShape, LineType, Modification, Shape } from '@/types/models'; import { MapInstance } from '@/types/map'; import { @@ -17,7 +17,8 @@ import { BLACK_COLOR, WHITE_COLOR, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; -import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/BaseMultiPolygon'; +import BaseMultiPolygon from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; export type MapElementProps = { shapes: Array<Shape>; @@ -203,12 +204,10 @@ export default class MapElement extends BaseMultiPolygon { }); activityBorderElement.forEach(polygon => { this.styles.push( - new Style({ + getStyle({ geometry: polygon, - fill: getFill({ color: 'rgba(0, 0, 0, 0)' }), - stroke: getStroke({ - lineDash: [3, 5], - }), + fillColor: { rgb: 0, alpha: 0 }, + lineDash: [3, 5], zIndex: this.zIndex, }), ); @@ -227,14 +226,12 @@ export default class MapElement extends BaseMultiPolygon { }); elementPolygon.forEach(polygon => { this.styles.push( - new Style({ + getStyle({ geometry: polygon, - stroke: getStroke({ - color: rgbToHex(this.borderColor), - width: this.lineWidth, - lineDash: this.lineDash, - }), - fill: getFill({ color: rgbToHex(this.fillColor) }), + borderColor: this.borderColor, + fillColor: this.fillColor, + lineWidth: this.lineWidth, + lineDash: this.lineDash, zIndex: this.zIndex, }), ); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.test.ts similarity index 96% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.test.ts index 76036f49..dbe051bd 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.test.ts @@ -1,12 +1,12 @@ /* eslint-disable no-magic-numbers */ -import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords'; -import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords'; +import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; +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'; -jest.mock('./getPolygonCoords'); -jest.mock('./getEllipseCoords'); +jest.mock('../coords/getPolygonCoords'); +jest.mock('../coords/getEllipseCoords'); describe('getMultiPolygon', () => { const mockPointToProjection = jest.fn(point => [point.x, point.y]); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.ts similarity index 91% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.ts index e5effdb7..d91c741a 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/getMultiPolygon.ts @@ -1,11 +1,11 @@ /* eslint-disable no-magic-numbers */ -import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getPolygonCoords'; -import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getEllipseCoords'; +import getPolygonCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getPolygonCoords'; +import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import Polygon from 'ol/geom/Polygon'; import { Coordinate } from 'ol/coordinate'; import { Shape } from '@/types/models'; -import getCentroid from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/getCentroid'; +import getCentroid from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getCentroid'; export default function getMultiPolygon({ x, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke.test.ts deleted file mode 100644 index 70a27921..00000000 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { Stroke } from 'ol/style'; -import getStroke from './getStroke'; - -describe('getStroke', () => { - it('should return a Stoke object with the default color and width', () => { - const result = getStroke({}); - expect(result).toBeInstanceOf(Stroke); - expect(result.getColor()).toBe('#000'); - expect(result.getWidth()).toBe(1); - }); - - it('should return a Stoke object with the provided color and width', () => { - const color = '#ff0000'; - const width = 2; - const result = getStroke({ color, width }); - expect(result).toBeInstanceOf(Stroke); - expect(result.getColor()).toBe(color); - expect(result.getWidth()).toBe(width); - }); -}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.test.ts deleted file mode 100644 index e25b4df7..00000000 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; -import getText from './getText'; - -describe('getText', () => { - const mockPointToProjection: UsePointToProjectionResult = jest.fn(point => [point.x, point.y]); - it('should return correct text coordinates and style when text is centered', () => { - const { textCoords, textStyle } = getText({ - x: 0, - y: 0, - height: 100, - width: 100, - text: 'Text test', - fontSize: 12, - verticalAlign: 'MIDDLE', - horizontalAlign: 'CENTER', - pointToProjection: mockPointToProjection, - }); - - expect(textCoords).toEqual([50, 50]); - expect(textStyle.getText()?.getFont()).toEqual('12pt Arial'); - }); - - it('should return correct text coordinates and style when text is aligned to bottom', () => { - const { textCoords, textStyle } = getText({ - x: 20, - y: 30, - height: 100, - width: 100, - text: 'Text test', - fontSize: 18, - verticalAlign: 'BOTTOM', - horizontalAlign: 'CENTER', - pointToProjection: mockPointToProjection, - }); - - expect(textCoords).toEqual([70, 121]); - - expect(textStyle.getText()?.getFont()).toEqual('18pt Arial'); - }); -}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.ts deleted file mode 100644 index 09dffbe2..00000000 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getText.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { Fill, Text } from 'ol/style'; -import Style from 'ol/style/Style'; -import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; -import { Coordinate } from 'ol/coordinate'; -import { - HorizontalAlign, - VerticalAlign, -} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; -import { FeatureLike } from 'ol/Feature'; -import { Geometry, MultiPolygon } from 'ol/geom'; -import RenderFeature from 'ol/render/Feature'; - -export default function getText({ - x, - y, - height, - width, - text = '', - fontSize = 12, - color = '#000', - zIndex = 1, - verticalAlign = 'MIDDLE', - horizontalAlign = 'CENTER', - pointToProjection, -}: { - x: number; - y: number; - height: number; - width: number; - text: string; - fontSize?: string | number; - color?: string; - zIndex?: number; - verticalAlign?: VerticalAlign; - horizontalAlign?: HorizontalAlign; - pointToProjection: UsePointToProjectionResult; -}): { textCoords: Coordinate; textStyle: Style } { - const minX = x; - const maxX = x + width; - const minY = y; - const maxY = y + height; - - let textY = (minY + maxY) / 2; - if (verticalAlign === 'TOP') { - textY = minY + +fontSize / 2; - } else if (verticalAlign === 'BOTTOM') { - textY = maxY - +fontSize / 2; - } - - let textX = (minX + maxX) / 2; - if (['LEFT', 'START'].includes(horizontalAlign)) { - textX = minX; - } else if (['RIGHT', 'END'].includes(horizontalAlign)) { - textX = maxX; - } - - const textCoords = pointToProjection({ x: textX, y: textY }); - - const textStyle = new Style({ - geometry: (feature: FeatureLike): Geometry | RenderFeature | undefined => { - const geometry = feature.getGeometry(); - if (geometry && geometry.getType() === 'MultiPolygon') { - return (geometry as MultiPolygon) - .getPolygon((geometry as MultiPolygon).getPolygons().length - 1) - .getInteriorPoint(); - } - return undefined; - }, - text: new Text({ - text, - font: `${fontSize}pt Arial`, - fill: new Fill({ - color, - }), - placement: 'point', - textAlign: horizontalAlign.toLowerCase() as CanvasTextAlign, - textBaseline: 'middle', - overflow: true, - }), - zIndex, - }); - - return { textCoords, textStyle }; -} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts new file mode 100644 index 00000000..ab6e4eb0 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts @@ -0,0 +1,145 @@ +/* eslint-disable no-magic-numbers */ +import { Map } from 'ol'; +import { Style, Text } from 'ol/style'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import View from 'ol/View'; +import { + WHITE_COLOR, + BLACK_COLOR, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; +import Layer, { + LayerProps, +} from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; + +jest.mock('../text/getTextCoords'); +jest.mock('../text/getTextStyle'); +jest.mock('../style/getStroke'); +jest.mock('../style/rgbToHex'); + +describe('Layer', () => { + let props: LayerProps; + + beforeEach(() => { + const dummyElement = document.createElement('div'); + const mapInstance = new Map({ + target: dummyElement, + view: new View({ + zoom: 5, + minZoom: 3, + maxZoom: 7, + }), + }); + props = { + texts: [ + { + id: 1, + x: 10, + y: 10, + z: 3, + width: 100, + height: 100, + fontSize: 12, + size: 12312, + notes: 'XYZ', + glyph: null, + elementId: '34', + verticalAlign: 'MIDDLE', + horizontalAlign: 'CENTER', + backgroundColor: WHITE_COLOR, + borderColor: BLACK_COLOR, + color: BLACK_COLOR, + }, + ], + rects: [ + { + id: 1, + x: 10, + y: 10, + z: 3, + width: 100, + height: 100, + size: 12312, + elementId: '341', + lineWidth: 2, + borderColor: BLACK_COLOR, + fillColor: WHITE_COLOR, + }, + ], + ovals: [ + { + id: 1, + x: 10, + y: 10, + z: 3, + width: 100, + height: 100, + size: 12312, + elementId: '341', + lineWidth: 2, + borderColor: BLACK_COLOR, + }, + ], + lines: [ + { + id: 120899, + width: 5.0, + color: { + alpha: 255, + rgb: -16777216, + }, + z: 0, + segments: [ + { + x1: 36.0, + y1: 39.0, + x2: 213.0, + y2: 41.0, + }, + ], + startArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15.0, + }, + endArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15.0, + }, + lineType: 'SOLID', + }, + ], + visible: true, + layerId: '23', + pointToProjection: jest.fn(point => [point.x, point.y]), + mapInstance, + lineTypes: [], + arrowTypes: [], + }; + (getTextStyle as jest.Mock).mockReturnValue( + new Style({ + text: new Text({}), + }), + ); + (getTextCoords as jest.Mock).mockReturnValue([10, 10]); + (getStroke as jest.Mock).mockReturnValue(new Style()); + (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); + }); + + it('should initialize a Layer class', () => { + const layer = new Layer(props); + + expect(layer.textFeatures.length).toBe(1); + expect(layer.rectFeatures.length).toBe(1); + expect(layer.ovalFeatures.length).toBe(1); + expect(layer.vectorSource).toBeInstanceOf(VectorSource); + expect(layer.vectorLayer).toBeInstanceOf(VectorLayer); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts new file mode 100644 index 00000000..259bff25 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -0,0 +1,320 @@ +/* eslint-disable no-magic-numbers */ +import { + Arrow, + ArrowType, + LayerLine, + LayerOval, + LayerRect, + LayerText, + LineType, +} from '@/types/models'; +import { MapInstance } from '@/types/map'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Feature } from 'ol'; +import { LineString, MultiPolygon, Point } from 'ol/geom'; +import Text from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text'; +import Polygon from 'ol/geom/Polygon'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import getEllipseCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getEllipseCoords'; +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +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'; + +export interface LayerProps { + texts: Array<LayerText>; + rects: Array<LayerRect>; + ovals: Array<LayerOval>; + lines: Array<LayerLine>; + visible: boolean; + layerId: string; + lineTypes: Array<LineType>; + arrowTypes: Array<ArrowType>; + mapInstance: MapInstance; + pointToProjection: UsePointToProjectionResult; +} + +export default class Layer { + texts: Array<LayerText>; + + rects: Array<LayerRect>; + + ovals: Array<LayerOval>; + + lines: Array<LayerLine>; + + lineTypes: Array<LineType>; + + arrowTypes: Array<ArrowType>; + + textFeatures: Array<Feature<Point>>; + + rectFeatures: Array<Feature<Polygon>>; + + ovalFeatures: Array<Feature<Polygon>>; + + lineFeatures: Array<Feature<LineString>>; + + arrowFeatures: Array<Feature<MultiPolygon>>; + + vectorSource: VectorSource< + Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon> + >; + + vectorLayer: VectorLayer< + VectorSource<Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>> + >; + + constructor({ + texts, + rects, + ovals, + lines, + visible, + layerId, + lineTypes, + arrowTypes, + mapInstance, + pointToProjection, + }: LayerProps) { + this.texts = texts; + this.rects = rects; + this.ovals = ovals; + 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.lineFeatures = linesFeatures; + this.arrowFeatures = arrowsFeatures; + this.vectorSource = new VectorSource({ + features: [ + ...this.textFeatures, + ...this.rectFeatures, + ...this.ovalFeatures, + ...this.lineFeatures, + ...this.arrowFeatures, + ], + }); + this.vectorLayer = new VectorLayer({ + source: this.vectorSource, + visible, + }); + this.vectorLayer.set('id', layerId); + } + + private getTextsFeatures = ( + mapInstance: MapInstance, + pointToProjection: UsePointToProjectionResult, + ): Array<Feature<Point>> => { + const textObjects = this.texts.map(text => { + return new Text({ + x: text.x, + y: text.y, + zIndex: text.z, + width: text.width, + height: text.height, + fontColor: text.color, + fontSize: text.fontSize, + text: text.notes, + verticalAlign: text.verticalAlign as VerticalAlign, + horizontalAlign: text.horizontalAlign as HorizontalAlign, + pointToProjection, + mapInstance, + }); + }); + return textObjects.map(text => text.feature); + }; + + private getRectsFeatures = ( + pointToProjection: UsePointToProjectionResult, + ): 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 }), + ], + ]); + const polygonStyle = getStyle({ + geometry: polygon, + borderColor: rect.borderColor, + fillColor: rect.fillColor, + lineWidth: rect.lineWidth, + zIndex: rect.z, + }); + const rectFeature = new Feature<Polygon>({ + geometry: polygon, + }); + rectFeature.setStyle(polygonStyle); + return rectFeature; + }); + }; + + private getOvalsFeatures = ( + pointToProjection: UsePointToProjectionResult, + ): Array<Feature<Polygon>> => { + return this.ovals.map(oval => { + const coords = getEllipseCoords({ + x: oval.x, + y: oval.y, + height: oval.height, + width: oval.width, + pointToProjection, + points: 36, + }); + const polygon = new Polygon([coords]); + const polygonStyle = getStyle({ + geometry: polygon, + borderColor: oval.borderColor, + fillColor: { rgb: 0, alpha: 0 }, + lineWidth: oval.lineWidth, + zIndex: oval.z, + }); + const ovalFeature = new Feature<Polygon>({ + geometry: polygon, + }); + ovalFeature.setStyle(polygonStyle); + return ovalFeature; + }); + }; + + private getLinesFeatures = ( + pointToProjection: UsePointToProjectionResult, + ): { + linesFeatures: Array<Feature<LineString>>; + arrowsFeatures: Array<Feature<MultiPolygon>>; + } => { + const linesFeatures: Array<Feature<LineString>> = []; + const arrowsFeatures: Array<Feature<MultiPolygon>> = []; + + this.lines.forEach(line => { + const points = line.segments + .map((segment, index) => { + if (index === 0) { + return [ + pointToProjection({ x: segment.x1, y: segment.y1 }), + pointToProjection({ x: segment.x2, y: segment.y2 }), + ]; + } + return [pointToProjection({ x: segment.x2, y: segment.y2 })]; + }) + .flat(); + 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: line.z, + }); + const lineFeature = new Feature<LineString>({ + geometry: lineString, + }); + lineFeature.setStyle(lineStyle); + linesFeatures.push(lineFeature); + arrowsFeatures.push(...this.getLineArrowsFeatures(line, pointToProjection)); + }); + return { linesFeatures, arrowsFeatures }; + }; + + private getLineArrowsFeatures = ( + line: LayerLine, + pointToProjection: UsePointToProjectionResult, + ): Array<Feature<MultiPolygon>> => { + const arrowsFeatures: Array<Feature<MultiPolygon>> = []; + const firstSegment = line.segments[0]; + const startArrowRotation = getRotation( + [firstSegment.x1, firstSegment.y1], + [firstSegment.x2, firstSegment.y2], + ); + const startArrowFeature = this.getLineArrowFeature( + line.startArrow, + firstSegment.x1, + firstSegment.y1, + line.z, + startArrowRotation, + line.width, + pointToProjection, + ); + if (startArrowFeature) { + arrowsFeatures.push(startArrowFeature); + } + + const lastSegment = line.segments[line.segments.length - 1]; + const endArrowRotation = getRotation( + [lastSegment.x1, lastSegment.y1], + [lastSegment.x2, lastSegment.y2], + ); + const endArrowFeature = this.getLineArrowFeature( + line.endArrow, + lastSegment.x2, + lastSegment.y2, + line.z, + endArrowRotation, + line.width, + pointToProjection, + ); + if (endArrowFeature) { + arrowsFeatures.push(endArrowFeature); + } + return arrowsFeatures; + }; + + private getLineArrowFeature = ( + arrow: Arrow, + x: number, + y: number, + zIndex: number, + rotation: number, + lineWidth: number, + pointToProjection: UsePointToProjectionResult, + ): undefined | Feature<MultiPolygon> => { + const arrowShapes = this.arrowTypes.find(arrowType => arrowType.arrowType === arrow.arrowType) + ?.shapes; + if (!arrowShapes) { + return undefined; + } + const arrowMultiPolygon = getMultiPolygon({ + x, + y: y - arrow.length / 2, + width: arrow.length, + height: arrow.length, + shapes: arrowShapes, + pointToProjection, + }); + const arrowStyles: Array<Style> = []; + arrowMultiPolygon.forEach(polygon => { + const style = getStyle({ + geometry: polygon, + zIndex, + borderColor: BLACK_COLOR, + fillColor: BLACK_COLOR, + lineWidth, + }); + arrowStyles.push(style); + polygon.rotate(rotation, pointToProjection({ x, y })); + }); + const arrowFeature = new Feature({ + geometry: new MultiPolygon(arrowMultiPolygon), + }); + arrowFeature.setStyle(arrowStyles); + return arrowFeature; + }; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill.test.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill.test.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getFill.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke.test.ts new file mode 100644 index 00000000..9f485a73 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke.test.ts @@ -0,0 +1,27 @@ +/* eslint-disable no-magic-numbers */ +import { Stroke } from 'ol/style'; +import getStroke from './getStroke'; + +describe('getStroke', () => { + it('should return a Stroke object with the default color and width', () => { + const result = getStroke({}); + expect(result).toBeInstanceOf(Stroke); + expect(result.getColor()).toEqual('#000'); + expect(result.getWidth()).toEqual(1); + expect(result.getLineDash()).toEqual([]); + expect(result.getLineCap()).toEqual('butt'); + }); + + it('should return a Stroke object with the provided values', () => { + const color = '#ff0000'; + const width = 2; + const lineDash = [10, 5]; + const lineCap = 'round'; + const result = getStroke({ color, width, lineDash, lineCap }); + expect(result).toBeInstanceOf(Stroke); + expect(result.getColor()).toEqual(color); + expect(result.getWidth()).toEqual(width); + expect(result.getLineDash()).toEqual(lineDash); + expect(result.getLineCap()).toEqual(lineCap); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke.ts similarity index 79% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke.ts index 65328448..5625aedb 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/getStroke.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke.ts @@ -5,14 +5,17 @@ export default function getStroke({ color = '#000', width = 1, lineDash = [], + lineCap = 'butt', }: { color?: string; width?: number; lineDash?: Array<number>; + lineCap?: string; }): Stroke { return new Stroke({ color, width, lineDash, + lineCap: lineCap as CanvasLineCap, }); } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.test.ts new file mode 100644 index 00000000..e46f9ef0 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.test.ts @@ -0,0 +1,52 @@ +/* eslint-disable no-magic-numbers */ +import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle'; +import Style from 'ol/style/Style'; +import Polygon from 'ol/geom/Polygon'; + +describe('getStyle', () => { + it('should return a Style object with the default values', () => { + const result = getStyle({}); + expect(result).toBeInstanceOf(Style); + expect(result.getGeometry()).toEqual(null); + expect(result.getStroke()?.getWidth()).toEqual(1); + expect(result.getStroke()?.getColor()).toEqual('#000000FF'); + expect(result.getStroke()?.getLineDash()).toEqual([]); + expect(result.getFill()?.getColor()).toEqual('#FFFFFFFF'); + expect(result.getZIndex()).toEqual(1); + }); + + it('should return a Style object with the provided color and width', () => { + const geometry = new Polygon([ + [ + [10, 10], + [10, 10], + ], + ]); + const borderColor = { + alpha: 255, + rgb: -16777216, + }; + const fillColor = { + alpha: 255, + rgb: -5646081, + }; + const lineWidth = 3; + const lineDash = [10, 5]; + const zIndex = 2; + const result = getStyle({ + geometry, + borderColor, + fillColor, + lineWidth, + lineDash, + zIndex, + }); + expect(result).toBeInstanceOf(Style); + expect(result.getGeometry()).toEqual(geometry); + expect(result.getStroke()?.getWidth()).toEqual(lineWidth); + expect(result.getStroke()?.getColor()).toEqual('#000000FF'); + expect(result.getStroke()?.getLineDash()).toEqual(lineDash); + expect(result.getFill()?.getColor()).toEqual('#A9D8FFFF'); + expect(result.getZIndex()).toEqual(zIndex); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts new file mode 100644 index 00000000..62f16157 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStyle.ts @@ -0,0 +1,38 @@ +/* eslint-disable no-magic-numbers */ +import Style from 'ol/style/Style'; +import { Geometry } from 'ol/geom'; +import getStroke from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getStroke'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import { + BLACK_COLOR, + WHITE_COLOR, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import { ColorObject } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; +import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; + +export default function getStyle({ + geometry, + borderColor = BLACK_COLOR, + fillColor = WHITE_COLOR, + lineWidth = 1, + lineDash = [], + zIndex = 1, +}: { + geometry?: Geometry; + borderColor?: ColorObject; + fillColor?: ColorObject; + lineWidth?: number; + lineDash?: Array<number>; + zIndex?: number; +}): Style { + return new Style({ + geometry, + stroke: getStroke({ + color: rgbToHex(borderColor), + width: lineWidth, + lineDash, + }), + fill: getFill({ color: rgbToHex(fillColor) }), + zIndex, + }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex.test.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex.test.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex.test.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex.ts similarity index 100% rename from src/components/Map/MapViewer/MapViewerVector/utils/shapes/rgbToHex.ts rename to src/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex.ts diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.test.ts new file mode 100644 index 00000000..68b8d9f8 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.test.ts @@ -0,0 +1,73 @@ +/* eslint-disable no-magic-numbers */ +import { Map } from 'ol'; +import { Style } from 'ol/style'; +import Text, { TextProps } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import View from 'ol/View'; +import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; + +jest.mock('./getTextCoords'); +jest.mock('./getTextStyle'); +jest.mock('../style/rgbToHex'); + +describe('Text', () => { + let props: TextProps; + + 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, + text: 'Test', + fontSize: 12, + fontColor: BLACK_COLOR, + verticalAlign: 'MIDDLE', + horizontalAlign: 'CENTER', + pointToProjection: jest.fn(), + mapInstance, + }; + + (getTextStyle as jest.Mock).mockReturnValue(new Style()); + (getTextCoords as jest.Mock).mockReturnValue([10, 10]); + (rgbToHex as jest.Mock).mockReturnValue('#FFFFFF'); + }); + + it('should apply correct styles to the feature', () => { + const text = new Text(props); + const { feature } = text; + + const style = feature.getStyleFunction()?.call(text, feature, 1); + + if (Array.isArray(style)) { + expect(style.length).toBeGreaterThan(0); + } else { + expect(style).toBeInstanceOf(Style); + } + }); + + it('should hide text when the scaled font size is too small', () => { + const text = new Text(props); + const { feature } = text; + + const style = feature.getStyleFunction()?.call(text, feature, 20); + + if (Array.isArray(style)) { + expect(style[0].getText()?.getText()).toBeUndefined(); + } else { + expect(style?.getText()?.getText()).toBeUndefined(); + } + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts new file mode 100644 index 00000000..687cf2fc --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts @@ -0,0 +1,115 @@ +/* eslint-disable no-magic-numbers */ +import { + ColorObject, + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; +import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/rgbToHex'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import Style from 'ol/style/Style'; +import { Point } from 'ol/geom'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import { MapInstance } from '@/types/map'; +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; + +export interface TextProps { + x: number; + y: number; + width: number; + height: number; + zIndex: number; + text: string; + fontSize: number; + fontColor: ColorObject; + verticalAlign: VerticalAlign; + horizontalAlign: HorizontalAlign; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; +} + +export default class Text { + text: string; + + fontSize: number; + + style: Style; + + point: Point; + + feature: Feature<Point>; + + constructor({ + x, + y, + width, + height, + zIndex, + text, + fontSize, + fontColor, + verticalAlign, + horizontalAlign, + pointToProjection, + mapInstance, + }: TextProps) { + this.text = text; + this.fontSize = fontSize; + + const textCoords = getTextCoords({ + x, + y, + height, + width, + fontSize, + verticalAlign, + horizontalAlign, + pointToProjection, + }); + const textStyle = getTextStyle({ + text, + fontSize, + color: rgbToHex(fontColor), + zIndex, + horizontalAlign, + }); + this.point = new Point(textCoords); + this.style = textStyle; + this.style.setGeometry(this.point); + + this.feature = new Feature({ + geometry: this.point, + 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.feature.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); + } + + if (this.style.getText()) { + if (this.fontSize * textScale > 4) { + this.style.getText()?.setScale(textScale); + this.style.getText()?.setText(this.text); + } else { + this.style.getText()?.setText(undefined); + } + } + return this.style; + } +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.test.ts new file mode 100644 index 00000000..6121a92d --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.test.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-magic-numbers */ +import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; + +describe('getTextCoords', () => { + it('should return a text coords', () => { + const mockPointToProjection: UsePointToProjectionResult = jest.fn(point => [point.x, point.y]); + const textCoords = getTextCoords({ + x: 20, + y: 20, + height: 100, + width: 100, + fontSize: 12, + verticalAlign: 'MIDDLE', + horizontalAlign: 'CENTER', + pointToProjection: mockPointToProjection, + }); + expect(textCoords).toEqual([70, 70]); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.ts new file mode 100644 index 00000000..3dcbb8fa --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords.ts @@ -0,0 +1,48 @@ +/* eslint-disable no-magic-numbers */ +import { + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Coordinate } from 'ol/coordinate'; + +export default function getTextCoords({ + x, + y, + height, + width, + fontSize, + verticalAlign, + horizontalAlign, + pointToProjection, +}: { + x: number; + y: number; + height: number; + width: number; + fontSize: number; + verticalAlign: VerticalAlign; + horizontalAlign: HorizontalAlign; + pointToProjection: UsePointToProjectionResult; +}): Coordinate { + const minX = x; + const maxX = x + width; + const minY = y; + const maxY = y + height; + + let textY = (minY + maxY) / 2; + if (verticalAlign === 'TOP') { + textY = minY + (fontSize * 4) / 6; + } else if (verticalAlign === 'BOTTOM') { + textY = maxY - (fontSize * 4) / 6; + } + + let textX = (minX + maxX) / 2; + if (['LEFT', 'START'].includes(horizontalAlign)) { + textX = minX; + } else if (['RIGHT', 'END'].includes(horizontalAlign)) { + textX = maxX; + } + + return pointToProjection({ x: textX, y: textY }); +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.test.ts new file mode 100644 index 00000000..d16623b5 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.test.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-magic-numbers */ +import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; +import Style from 'ol/style/Style'; + +describe('getTextStyle', () => { + it('should return a text style object', () => { + const text = 'Text styl test'; + const fontSize = 12; + const color = '#CCFFCC'; + const zIndex = 122; + const horizontalAlign = 'CENTER'; + const textStyle = getTextStyle({ + text, + fontSize, + color, + zIndex, + horizontalAlign, + }); + expect(textStyle).toBeInstanceOf(Style); + expect(textStyle.getText()?.getText()).toBe(text); + expect(textStyle.getText()?.getFont()).toBe('12pt Arial'); + expect(textStyle.getText()?.getFill()?.getColor()).toBe(color); + expect(textStyle.getZIndex()).toBe(zIndex); + expect(textStyle.getText()?.getTextAlign()).toBe(horizontalAlign.toLowerCase()); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts new file mode 100644 index 00000000..da9eed33 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle.ts @@ -0,0 +1,32 @@ +import { Fill, Text } from 'ol/style'; +import Style from 'ol/style/Style'; +import { HorizontalAlign } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +export default function getTextStyle({ + text, + fontSize, + color, + zIndex, + horizontalAlign, +}: { + text: string; + fontSize: number; + color: string; + zIndex: number; + horizontalAlign: HorizontalAlign; +}): Style { + return new Style({ + text: new Text({ + text, + font: `${fontSize}pt Arial`, + fill: new Fill({ + color, + }), + placement: 'point', + textAlign: horizontalAlign.toLowerCase() as CanvasTextAlign, + textBaseline: 'middle', + overflow: true, + }), + zIndex, + }); +} diff --git a/src/models/arrowTypeSchema.ts b/src/models/arrowTypeSchema.ts new file mode 100644 index 00000000..929f6b5a --- /dev/null +++ b/src/models/arrowTypeSchema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { shapeSchema } from '@/models/shapeSchema'; + +export const arrowTypeSchema = z.object({ + arrowType: z.string(), + shapes: z.array(shapeSchema), +}); diff --git a/src/models/fixtures/arrowTypesFixture.ts b/src/models/fixtures/arrowTypesFixture.ts new file mode 100644 index 00000000..2d452353 --- /dev/null +++ b/src/models/fixtures/arrowTypesFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +import { z } from 'zod'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { arrowTypeSchema } from '@/models/arrowTypeSchema'; + +export const arrowTypesFixture = createFixture(z.array(arrowTypeSchema), { + seed: ZOD_SEED, + array: { min: 3, max: 3 }, +}); diff --git a/src/models/fixtures/layerLinesFixture.ts b/src/models/fixtures/layerLinesFixture.ts new file mode 100644 index 00000000..fc15d3c2 --- /dev/null +++ b/src/models/fixtures/layerLinesFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { pageableSchema } from '@/models/pageableSchema'; +import { layerLineSchema } from '@/models/layerLineSchema'; + +export const layerLinesFixture = createFixture(pageableSchema(layerLineSchema), { + seed: ZOD_SEED, + array: { min: 3, max: 3 }, +}); diff --git a/src/models/fixtures/layerOvalsFixture.ts b/src/models/fixtures/layerOvalsFixture.ts new file mode 100644 index 00000000..50544273 --- /dev/null +++ b/src/models/fixtures/layerOvalsFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { pageableSchema } from '@/models/pageableSchema'; +import { layerOvalSchema } from '@/models/layerOvalSchema'; + +export const layerOvalsFixture = createFixture(pageableSchema(layerOvalSchema), { + seed: ZOD_SEED, + array: { min: 3, max: 3 }, +}); diff --git a/src/models/fixtures/layerRectsFixture.ts b/src/models/fixtures/layerRectsFixture.ts new file mode 100644 index 00000000..98469f8b --- /dev/null +++ b/src/models/fixtures/layerRectsFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { pageableSchema } from '@/models/pageableSchema'; +import { layerRectSchema } from '@/models/layerRectSchema'; + +export const layerRectsFixture = createFixture(pageableSchema(layerRectSchema), { + seed: ZOD_SEED, + array: { min: 3, max: 3 }, +}); diff --git a/src/models/fixtures/layerTextsFixture.ts b/src/models/fixtures/layerTextsFixture.ts new file mode 100644 index 00000000..26e01467 --- /dev/null +++ b/src/models/fixtures/layerTextsFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { layerTextSchema } from '@/models/layerTextSchema'; +import { pageableSchema } from '@/models/pageableSchema'; + +export const layerTextsFixture = createFixture(pageableSchema(layerTextSchema), { + seed: ZOD_SEED, + array: { min: 3, max: 3 }, +}); diff --git a/src/models/fixtures/layersFixture.ts b/src/models/fixtures/layersFixture.ts new file mode 100644 index 00000000..65a4841a --- /dev/null +++ b/src/models/fixtures/layersFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { layerSchema } from '@/models/layerSchema'; +import { pageableSchema } from '@/models/pageableSchema'; + +export const layersFixture = createFixture(pageableSchema(layerSchema), { + seed: ZOD_SEED, + array: { min: 1, max: 1 }, +}); diff --git a/src/models/fixtures/modelElementsFixture.ts b/src/models/fixtures/modelElementsFixture.ts index 87c8fb2e..0c3df37b 100644 --- a/src/models/fixtures/modelElementsFixture.ts +++ b/src/models/fixtures/modelElementsFixture.ts @@ -1,9 +1,10 @@ import { ZOD_SEED } from '@/constants'; // eslint-disable-next-line import/no-extraneous-dependencies import { createFixture } from 'zod-fixture'; -import { modelElementsSchema } from '@/models/modelElementsSchema'; +import { modelElementSchema } from '@/models/modelElementSchema'; +import { pageableSchema } from '@/models/pageableSchema'; -export const modelElementsFixture = createFixture(modelElementsSchema, { +export const modelElementsFixture = createFixture(pageableSchema(modelElementSchema), { seed: ZOD_SEED, array: { min: 3, max: 3 }, }); diff --git a/src/models/layerLineSchema.ts b/src/models/layerLineSchema.ts new file mode 100644 index 00000000..e454f930 --- /dev/null +++ b/src/models/layerLineSchema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; +import { colorSchema } from '@/models/colorSchema'; +import { segmentSchema } from '@/models/segmentSchema'; +import { arrowSchema } from '@/models/arrowSchema'; + +export const layerLineSchema = z.object({ + id: z.number().int().positive(), + width: z.number(), + color: colorSchema, + z: z.number(), + segments: z.array(segmentSchema), + startArrow: arrowSchema, + endArrow: arrowSchema, + lineType: z.string(), +}); diff --git a/src/models/layerOvalSchema.ts b/src/models/layerOvalSchema.ts new file mode 100644 index 00000000..abd708ef --- /dev/null +++ b/src/models/layerOvalSchema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; +import { colorSchema } from '@/models/colorSchema'; + +export const layerOvalSchema = z.object({ + id: z.number().int().positive(), + x: z.number(), + y: z.number(), + z: z.number(), + width: z.number(), + height: z.number(), + lineWidth: z.number(), + size: z.number(), + elementId: z.string(), + borderColor: colorSchema, +}); diff --git a/src/models/layerRectSchema.ts b/src/models/layerRectSchema.ts new file mode 100644 index 00000000..440c96d4 --- /dev/null +++ b/src/models/layerRectSchema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; +import { colorSchema } from '@/models/colorSchema'; + +export const layerRectSchema = z.object({ + id: z.number().int().positive(), + x: z.number(), + y: z.number(), + z: z.number(), + width: z.number(), + height: z.number(), + lineWidth: z.number(), + size: z.number(), + fillColor: colorSchema, + borderColor: colorSchema, + elementId: z.string(), +}); diff --git a/src/models/layerSchema.ts b/src/models/layerSchema.ts new file mode 100644 index 00000000..be2ba10b --- /dev/null +++ b/src/models/layerSchema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const layerSchema = z.object({ + id: z.number(), + layerId: z.string(), + name: z.string(), + visible: z.boolean(), + locked: z.boolean(), + empty: z.boolean(), +}); diff --git a/src/models/layerTextSchema.ts b/src/models/layerTextSchema.ts new file mode 100644 index 00000000..3ad77ed0 --- /dev/null +++ b/src/models/layerTextSchema.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; +import { colorSchema } from '@/models/colorSchema'; +import { glyphSchema } from '@/models/glyphSchema'; + +export const layerTextSchema = z.object({ + id: z.number(), + x: z.number(), + y: z.number(), + z: z.number(), + width: z.number(), + height: z.number(), + fontSize: z.number(), + size: z.number(), + notes: z.string(), + glyph: glyphSchema.nullable(), + elementId: z.string(), + verticalAlign: z.string(), + horizontalAlign: z.string(), + backgroundColor: colorSchema, + borderColor: colorSchema, + color: colorSchema, +}); diff --git a/src/models/modelElementsSchema.ts b/src/models/modelElementsSchema.ts deleted file mode 100644 index 19969ba1..00000000 --- a/src/models/modelElementsSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; -import { modelElementSchema } from '@/models/modelElementSchema'; - -export const modelElementsSchema = z.object({ - content: z.array(modelElementSchema), - totalPages: z.number(), - totalElements: z.number(), - numberOfElements: z.number(), - size: z.number(), - number: z.number(), -}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index b830ff10..c193f101 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -52,6 +52,16 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/elements/?size=10000`, getShapes: (): string => `projects/${PROJECT_ID}/shapes/`, getLineTypes: (): string => `projects/${PROJECT_ID}/lineTypes/`, + getArrowTypes: (): string => `projects/${PROJECT_ID}/arrowTypes/`, + getLayers: (modelId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/`, + getLayerTexts: (modelId: number, layerId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/`, + getLayerRects: (modelId: number, layerId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/rects/`, + getLayerOvals: (modelId: number, layerId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/ovals/`, + getLayerLines: (modelId: number, layerId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`, getChemicalsStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`, getAllOverlaysByProjectIdQuery: ( diff --git a/src/redux/layers/layers.constants.ts b/src/redux/layers/layers.constants.ts new file mode 100644 index 00000000..56736b9f --- /dev/null +++ b/src/redux/layers/layers.constants.ts @@ -0,0 +1 @@ +export const LAYERS_FETCHING_ERROR_PREFIX = 'Failed to fetch layers'; diff --git a/src/redux/layers/layers.mock.ts b/src/redux/layers/layers.mock.ts new file mode 100644 index 00000000..9ec2ce4c --- /dev/null +++ b/src/redux/layers/layers.mock.ts @@ -0,0 +1,11 @@ +import { LayersState } from '@/redux/layers/layers.types'; +import { DEFAULT_ERROR } from '@/constants/errors'; + +export const LAYERS_STATE_INITIAL_MOCK: LayersState = { + data: { + layers: [], + layersVisibility: {}, + }, + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts new file mode 100644 index 00000000..827c1dff --- /dev/null +++ b/src/redux/layers/layers.reducers.test.ts @@ -0,0 +1,128 @@ +/* eslint-disable no-magic-numbers */ +import { apiPath } from '@/redux/apiPath'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { unwrapResult } from '@reduxjs/toolkit'; +import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; +import { getLayers } from '@/redux/layers/layers.thunks'; +import { layersFixture } from '@/models/fixtures/layersFixture'; +import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture'; +import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture'; +import { layerOvalsFixture } from '@/models/fixtures/layerOvalsFixture'; +import { layerLinesFixture } from '@/models/fixtures/layerLinesFixture'; +import { LayersState } from './layers.types'; +import layersReducer from './layers.slice'; + +const mockedAxiosClient = mockNetworkNewAPIResponse(); + +const INITIAL_STATE: LayersState = LAYERS_STATE_INITIAL_MOCK; + +describe('layers reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<LayersState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('layers', layersReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(layersReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + + it('should update store after successful getLayers query', async () => { + mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.Ok, layersFixture); + mockedAxiosClient + .onGet(apiPath.getLayerTexts(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerTextsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerRects(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerRectsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerOvals(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerOvalsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerLinesFixture); + + const { type } = await store.dispatch(getLayers(1)); + const { data, loading, error } = store.getState().layers; + expect(type).toBe('vectorMap/getLayers/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual({ + layers: [ + { + details: layersFixture.content[0], + texts: layerTextsFixture.content, + rects: layerRectsFixture.content, + ovals: layerOvalsFixture.content, + lines: layerLinesFixture.content, + }, + ], + layersVisibility: { + [layersFixture.content[0].layerId]: layersFixture.content[0].visible, + }, + }); + }); + + it('should update store after failed getLayers query', async () => { + mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.NotFound, []); + + const action = await store.dispatch(getLayers(1)); + const { data, loading, error } = store.getState().layers; + + expect(action.type).toBe('vectorMap/getLayers/rejected'); + expect(() => unwrapResult(action)).toThrow( + "Failed to fetch layers: The page you're looking for doesn't exist. Please verify the URL and try again.", + ); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual({ layers: [], layersVisibility: {} }); + }); + + it('should update store on loading getLayers query', async () => { + mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.Ok, layersFixture); + mockedAxiosClient + .onGet(apiPath.getLayerTexts(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerTextsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerRects(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerRectsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerOvals(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerOvalsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerLinesFixture); + + const layersPromise = store.dispatch(getLayers(1)); + + const { data, loading } = store.getState().layers; + expect(data).toEqual({ layers: [], layersVisibility: {} }); + expect(loading).toEqual('pending'); + + layersPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().layers; + + expect(dataPromiseFulfilled).toEqual({ + layers: [ + { + details: layersFixture.content[0], + texts: layerTextsFixture.content, + rects: layerRectsFixture.content, + ovals: layerOvalsFixture.content, + lines: layerLinesFixture.content, + }, + ], + layersVisibility: { + [layersFixture.content[0].layerId]: layersFixture.content[0].visible, + }, + }); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts new file mode 100644 index 00000000..ed75a687 --- /dev/null +++ b/src/redux/layers/layers.reducers.ts @@ -0,0 +1,30 @@ +/* eslint-disable no-magic-numbers */ +import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; +import { getLayers } from '@/redux/layers/layers.thunks'; +import { LayersState } from '@/redux/layers/layers.types'; + +export const getLayersReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { + builder.addCase(getLayers.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getLayers.fulfilled, (state, action) => { + state.data = action.payload || { + layers: [], + layersVisibility: {}, + }; + state.loading = 'succeeded'; + }); + builder.addCase(getLayers.rejected, state => { + state.loading = 'failed'; + }); +}; + +export const setLayerVisibilityReducer = ( + state: LayersState, + action: PayloadAction<{ visible: boolean; layerId: string }>, +): void => { + const { payload } = action; + if (state.data && state.data.layersVisibility[payload.layerId] !== undefined) { + state.data.layersVisibility[payload.layerId] = payload.visible; + } +}; diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts new file mode 100644 index 00000000..987ec4ac --- /dev/null +++ b/src/redux/layers/layers.selectors.ts @@ -0,0 +1,12 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { rootSelector } from '@/redux/root/root.selectors'; + +export const layersSelector = createSelector( + rootSelector, + state => state.layers?.data?.layers || [], +); + +export const layersVisibilitySelector = createSelector( + rootSelector, + state => state.layers?.data?.layersVisibility || {}, +); diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts new file mode 100644 index 00000000..47da06b0 --- /dev/null +++ b/src/redux/layers/layers.slice.ts @@ -0,0 +1,18 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; +import { getLayersReducer, setLayerVisibilityReducer } from '@/redux/layers/layers.reducers'; + +export const layersSlice = createSlice({ + name: 'layers', + initialState: LAYERS_STATE_INITIAL_MOCK, + reducers: { + setLayerVisibility: setLayerVisibilityReducer, + }, + extraReducers: builder => { + getLayersReducer(builder); + }, +}); + +export const { setLayerVisibility } = layersSlice.actions; + +export default layersSlice.reducer; diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts new file mode 100644 index 00000000..a6c00186 --- /dev/null +++ b/src/redux/layers/layers.thunks.test.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-magic-numbers */ +import { apiPath } from '@/redux/apiPath'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { LayersState } from '@/redux/layers/layers.types'; +import { getLayers } from '@/redux/layers/layers.thunks'; +import { layersFixture } from '@/models/fixtures/layersFixture'; +import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture'; +import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture'; +import { layerOvalsFixture } from '@/models/fixtures/layerOvalsFixture'; +import { layerLinesFixture } from '@/models/fixtures/layerLinesFixture'; +import layersReducer from './layers.slice'; + +const mockedAxiosClient = mockNetworkNewAPIResponse(); + +describe('layers thunks', () => { + let store = {} as ToolkitStoreWithSingleSlice<LayersState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('layers', layersReducer); + }); + + describe('getLayers', () => { + it('should return data when data response from API is valid', async () => { + mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.Ok, layersFixture); + mockedAxiosClient + .onGet(apiPath.getLayerTexts(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerTextsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerRects(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerRectsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerOvals(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerOvalsFixture); + mockedAxiosClient + .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) + .reply(HttpStatusCode.Ok, layerLinesFixture); + + const { payload } = await store.dispatch(getLayers(1)); + expect(payload).toEqual({ + layers: [ + { + details: layersFixture.content[0], + texts: layerTextsFixture.content, + rects: layerRectsFixture.content, + ovals: layerOvalsFixture.content, + lines: layerLinesFixture.content, + }, + ], + layersVisibility: { + [layersFixture.content[0].layerId]: layersFixture.content[0].visible, + }, + }); + }); + + it('should return undefined when data response from API is not valid ', async () => { + mockedAxiosClient + .onGet(apiPath.getLayers(1)) + .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); + + const { payload } = await store.dispatch(getLayers(1)); + expect(payload).toEqual(undefined); + }); + }); +}); diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts new file mode 100644 index 00000000..9aa71e83 --- /dev/null +++ b/src/redux/layers/layers.thunks.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; +import { apiPath } from '@/redux/apiPath'; +import { Layer, Layers } from '@/types/models'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { ThunkConfig } from '@/types/store'; +import { getError } from '@/utils/error-report/getError'; +import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; +import { layerSchema } from '@/models/layerSchema'; +import { LAYERS_FETCHING_ERROR_PREFIX } from '@/redux/layers/layers.constants'; +import { LayersVisibilitiesState } from '@/redux/layers/layers.types'; +import { layerTextSchema } from '@/models/layerTextSchema'; +import { layerRectSchema } from '@/models/layerRectSchema'; +import { pageableSchema } from '@/models/pageableSchema'; +import { layerOvalSchema } from '@/models/layerOvalSchema'; +import { layerLineSchema } from '@/models/layerLineSchema'; + +export const getLayers = createAsyncThunk<LayersVisibilitiesState | undefined, number, ThunkConfig>( + 'vectorMap/getLayers', + async (modelId: number) => { + try { + const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); + const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); + if (!isDataValid) { + return undefined; + } + let layers = await Promise.all( + data.content.map(async (layer: Layer) => { + const [textsResponse, rectsResponse, ovalsResponse, linesResponse] = await Promise.all([ + axiosInstanceNewAPI.get(apiPath.getLayerTexts(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerRects(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerOvals(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)), + ]); + + return { + details: layer, + texts: textsResponse.data.content, + rects: rectsResponse.data.content, + ovals: ovalsResponse.data.content, + lines: linesResponse.data.content, + }; + }), + ); + layers = layers.filter(layer => { + return ( + z.array(layerTextSchema).safeParse(layer.texts).success && + z.array(layerRectSchema).safeParse(layer.rects).success && + z.array(layerOvalSchema).safeParse(layer.ovals).success && + z.array(layerLineSchema).safeParse(layer.lines).success + ); + }); + const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => { + acc[layer.details.layerId] = layer.details.visible; + return acc; + }, {}); + return { + layers, + layersVisibility, + }; + } catch (error) { + return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX })); + } + }, +); diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts new file mode 100644 index 00000000..63637690 --- /dev/null +++ b/src/redux/layers/layers.types.ts @@ -0,0 +1,21 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { Layer, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; + +export type LayerState = { + details: Layer; + texts: LayerText[]; + rects: LayerRect[]; + ovals: LayerOval[]; + lines: LayerLine[]; +}; + +export type LayerVisibilityState = { + [key: string]: boolean; +}; + +export type LayersVisibilitiesState = { + layersVisibility: LayerVisibilityState; + layers: LayerState[]; +}; + +export type LayersState = FetchDataState<LayersVisibilitiesState>; diff --git a/src/redux/modelElements/modelElements.thunks.ts b/src/redux/modelElements/modelElements.thunks.ts index 7898db9a..2c7e2f25 100644 --- a/src/redux/modelElements/modelElements.thunks.ts +++ b/src/redux/modelElements/modelElements.thunks.ts @@ -4,9 +4,10 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; import { getError } from '@/utils/error-report/getError'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { modelElementsSchema } from '@/models/modelElementsSchema'; import { ModelElements } from '@/types/models'; import { MODEL_ELEMENTS_FETCHING_ERROR_PREFIX } from '@/redux/modelElements/modelElements.constants'; +import { modelElementSchema } from '@/models/modelElementSchema'; +import { pageableSchema } from '@/models/pageableSchema'; export const getModelElements = createAsyncThunk<ModelElements | undefined, number, ThunkConfig>( 'vectorMap/getModelElements', @@ -15,7 +16,10 @@ export const getModelElements = createAsyncThunk<ModelElements | undefined, numb const response = await axiosInstanceNewAPI.get<ModelElements>( apiPath.getModelElements(modelId), ); - const isDataValid = validateDataUsingZodSchema(response.data, modelElementsSchema); + const isDataValid = validateDataUsingZodSchema( + response.data, + pageableSchema(modelElementSchema), + ); return isDataValid ? response.data : undefined; } catch (error) { return Promise.reject(getError({ error, prefix: MODEL_ELEMENTS_FETCHING_ERROR_PREFIX })); diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts index 5f031272..34f7765e 100644 --- a/src/redux/root/init.thunks.ts +++ b/src/redux/root/init.thunks.ts @@ -15,7 +15,7 @@ import { import { openSelectProjectModal } from '@/redux/modal/modal.slice'; import { getProjects } from '@/redux/projects/projects.thunks'; import { getSubmapConnectionsBioEntity } from '@/redux/bioEntity/thunks/getSubmapConnectionsBioEntity'; -import { getLineTypes, getShapes } from '@/redux/shapes/shapes.thunks'; +import { getArrowTypes, getLineTypes, getShapes } from '@/redux/shapes/shapes.thunks'; import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks'; import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks'; import { @@ -61,8 +61,8 @@ export const fetchInitialAppData = createAsyncThunk< dispatch(getModels()), dispatch(getShapes()), dispatch(getLineTypes()), + dispatch(getArrowTypes()), ]); - if (queryData.pluginsId) { await dispatch( getInitPlugins({ diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index 16bbee96..8b1cec49 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -5,6 +5,7 @@ import { COMMENT_INITIAL_STATE_MOCK } from '@/redux/comment/comment.mock'; import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.constants'; import { SHAPES_STATE_INITIAL_MOCK } from '@/redux/shapes/shapes.mock'; import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; +import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock'; import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock'; import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock'; @@ -44,6 +45,7 @@ export const INITIAL_STORE_STATE_MOCK: RootState = { chemicals: CHEMICALS_INITIAL_STATE_MOCK, models: MODELS_INITIAL_STATE_MOCK, modelElements: MODEL_ELEMENTS_INITIAL_STATE_MOCK, + layers: LAYERS_STATE_INITIAL_MOCK, bioEntity: BIOENTITY_INITIAL_STATE_MOCK, backgrounds: BACKGROUND_INITIAL_STATE_MOCK, drawer: drawerInitialStateMock, diff --git a/src/redux/shapes/shapes.constants.ts b/src/redux/shapes/shapes.constants.ts index 1557cae9..18da4b1b 100644 --- a/src/redux/shapes/shapes.constants.ts +++ b/src/redux/shapes/shapes.constants.ts @@ -1,3 +1,5 @@ export const SHAPES_FETCHING_ERROR_PREFIX = 'Failed to fetch shapes'; export const LINE_TYPES_FETCHING_ERROR_PREFIX = 'Failed to fetch line types'; + +export const ARROW_TYPES_FETCHING_ERROR_PREFIX = 'Failed to fetch arrow types'; diff --git a/src/redux/shapes/shapes.mock.ts b/src/redux/shapes/shapes.mock.ts index 2ede21d1..b1ad3a5e 100644 --- a/src/redux/shapes/shapes.mock.ts +++ b/src/redux/shapes/shapes.mock.ts @@ -12,4 +12,9 @@ export const SHAPES_STATE_INITIAL_MOCK: ShapesState = { loading: 'idle', error: DEFAULT_ERROR, }, + arrowTypesState: { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, + }, }; diff --git a/src/redux/shapes/shapes.reducers.test.ts b/src/redux/shapes/shapes.reducers.test.ts index d9dcd3cf..9eab2b21 100644 --- a/src/redux/shapes/shapes.reducers.test.ts +++ b/src/redux/shapes/shapes.reducers.test.ts @@ -9,8 +9,9 @@ import { unwrapResult } from '@reduxjs/toolkit'; import { bioShapesFixture } from '@/models/fixtures/bioShapesFixture'; import { SHAPES_STATE_INITIAL_MOCK } from '@/redux/shapes/shapes.mock'; import { lineTypesFixture } from '@/models/fixtures/lineTypesFixture'; +import { arrowTypesFixture } from '@/models/fixtures/arrowTypesFixture'; import shapesReducer from './shapes.slice'; -import { getLineTypes, getShapes } from './shapes.thunks'; +import { getArrowTypes, getLineTypes, getShapes } from './shapes.thunks'; import { ShapesState } from './shapes.types'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -51,6 +52,17 @@ describe('shapes reducer', () => { expect(data).toEqual(lineTypesFixture); }); + it('should update store after successful getArrowTypes query', async () => { + mockedAxiosClient.onGet(apiPath.getArrowTypes()).reply(HttpStatusCode.Ok, arrowTypesFixture); + + const { type } = await store.dispatch(getArrowTypes()); + const { data, loading, error } = store.getState().shapes.arrowTypesState; + expect(type).toBe('vectorMap/getArrowTypes/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(arrowTypesFixture); + }); + it('should update store after failed getShapes query', async () => { mockedAxiosClient.onGet(apiPath.getShapes()).reply(HttpStatusCode.NotFound, []); @@ -81,6 +93,21 @@ describe('shapes reducer', () => { expect(data).toEqual([]); }); + it('should update store after failed getArrowTypes query', async () => { + mockedAxiosClient.onGet(apiPath.getArrowTypes()).reply(HttpStatusCode.NotFound, []); + + const action = await store.dispatch(getArrowTypes()); + const { data, loading, error } = store.getState().shapes.arrowTypesState; + + expect(action.type).toBe('vectorMap/getArrowTypes/rejected'); + expect(() => unwrapResult(action)).toThrow( + "Failed to fetch arrow types: The page you're looking for doesn't exist. Please verify the URL and try again.", + ); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual([]); + }); + it('should update store on loading getShapes query', async () => { mockedAxiosClient.onGet(apiPath.getShapes()).reply(HttpStatusCode.Ok, bioShapesFixture); @@ -116,4 +143,22 @@ describe('shapes reducer', () => { expect(promiseFulfilled).toEqual('succeeded'); }); }); + + it('should update store on loading getArrowTypes query', async () => { + mockedAxiosClient.onGet(apiPath.getArrowTypes()).reply(HttpStatusCode.Ok, arrowTypesFixture); + + const arrowTypesPromise = store.dispatch(getArrowTypes()); + + const { data, loading } = store.getState().shapes.arrowTypesState; + expect(data).toEqual([]); + expect(loading).toEqual('pending'); + + arrowTypesPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = + store.getState().shapes.arrowTypesState; + + expect(dataPromiseFulfilled).toEqual(arrowTypesFixture); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); }); diff --git a/src/redux/shapes/shapes.reducers.ts b/src/redux/shapes/shapes.reducers.ts index 3e01282a..525cde54 100644 --- a/src/redux/shapes/shapes.reducers.ts +++ b/src/redux/shapes/shapes.reducers.ts @@ -1,6 +1,6 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { ShapesState } from '@/redux/shapes/shapes.types'; -import { getLineTypes, getShapes } from '@/redux/shapes/shapes.thunks'; +import { getArrowTypes, getLineTypes, getShapes } from '@/redux/shapes/shapes.thunks'; export const getShapesReducer = (builder: ActionReducerMapBuilder<ShapesState>): void => { builder.addCase(getShapes.pending, state => { @@ -27,3 +27,16 @@ export const getLineTypesReducer = (builder: ActionReducerMapBuilder<ShapesState state.lineTypesState.loading = 'failed'; }); }; + +export const getArrowTypesReducer = (builder: ActionReducerMapBuilder<ShapesState>): void => { + builder.addCase(getArrowTypes.pending, state => { + state.arrowTypesState.loading = 'pending'; + }); + builder.addCase(getArrowTypes.fulfilled, (state, action) => { + state.arrowTypesState.data = action.payload || []; + state.arrowTypesState.loading = 'succeeded'; + }); + builder.addCase(getArrowTypes.rejected, state => { + state.arrowTypesState.loading = 'failed'; + }); +}; diff --git a/src/redux/shapes/shapes.selectors.ts b/src/redux/shapes/shapes.selectors.ts index 0d042b2c..35569752 100644 --- a/src/redux/shapes/shapes.selectors.ts +++ b/src/redux/shapes/shapes.selectors.ts @@ -13,6 +13,11 @@ export const lineTypesSelector = createSelector( shapes => shapes.lineTypesState.data, ); +export const arrowTypesSelector = createSelector( + shapesSelector, + shapes => shapes.arrowTypesState.data, +); + export const shapeBySBOSelector = createSelector( [shapesSelector, (_state, shapeSBO: string): string => shapeSBO], (shapes, shapeSBO) => diff --git a/src/redux/shapes/shapes.slice.ts b/src/redux/shapes/shapes.slice.ts index 5b3b81a0..7fcecfa0 100644 --- a/src/redux/shapes/shapes.slice.ts +++ b/src/redux/shapes/shapes.slice.ts @@ -1,5 +1,9 @@ import { createSlice } from '@reduxjs/toolkit'; -import { getLineTypesReducer, getShapesReducer } from '@/redux/shapes/shapes.reducers'; +import { + getArrowTypesReducer, + getLineTypesReducer, + getShapesReducer, +} from '@/redux/shapes/shapes.reducers'; import { SHAPES_STATE_INITIAL_MOCK } from '@/redux/shapes/shapes.mock'; export const shapesSlice = createSlice({ @@ -9,6 +13,7 @@ export const shapesSlice = createSlice({ extraReducers: builder => { getShapesReducer(builder); getLineTypesReducer(builder); + getArrowTypesReducer(builder); }, }); diff --git a/src/redux/shapes/shapes.thunks.test.ts b/src/redux/shapes/shapes.thunks.test.ts index f2f2413c..7b9980bf 100644 --- a/src/redux/shapes/shapes.thunks.test.ts +++ b/src/redux/shapes/shapes.thunks.test.ts @@ -8,8 +8,9 @@ import { HttpStatusCode } from 'axios'; import { bioShapesFixture } from '@/models/fixtures/bioShapesFixture'; import { ShapesState } from '@/redux/shapes/shapes.types'; import { lineTypesFixture } from '@/models/fixtures/lineTypesFixture'; +import { arrowTypesFixture } from '@/models/fixtures/arrowTypesFixture'; import shapesReducer from './shapes.slice'; -import { getLineTypes, getShapes } from './shapes.thunks'; +import { getArrowTypes, getLineTypes, getShapes } from './shapes.thunks'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -54,4 +55,22 @@ describe('shapes thunks', () => { expect(payload).toEqual(undefined); }); }); + + describe('getArrowTypes', () => { + it('should return data when data response from API is valid', async () => { + mockedAxiosClient.onGet(apiPath.getArrowTypes()).reply(HttpStatusCode.Ok, arrowTypesFixture); + + const { payload } = await store.dispatch(getArrowTypes()); + expect(payload).toEqual(arrowTypesFixture); + }); + + it('should return undefined when data response from API is not valid ', async () => { + mockedAxiosClient + .onGet(apiPath.getArrowTypes()) + .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); + + const { payload } = await store.dispatch(getArrowTypes()); + expect(payload).toEqual(undefined); + }); + }); }); diff --git a/src/redux/shapes/shapes.thunks.ts b/src/redux/shapes/shapes.thunks.ts index 90c1c41c..f882d2fb 100644 --- a/src/redux/shapes/shapes.thunks.ts +++ b/src/redux/shapes/shapes.thunks.ts @@ -1,17 +1,19 @@ import { bioShapeSchema } from '@/models/bioShapeSchema'; import { apiPath } from '@/redux/apiPath'; -import { BioShape, LineType } from '@/types/models'; +import { ArrowType, BioShape, LineType } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { z } from 'zod'; import { ThunkConfig } from '@/types/store'; import { getError } from '@/utils/error-report/getError'; import { + ARROW_TYPES_FETCHING_ERROR_PREFIX, LINE_TYPES_FETCHING_ERROR_PREFIX, SHAPES_FETCHING_ERROR_PREFIX, } from '@/redux/shapes/shapes.constants'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; import { lineTypeSchema } from '@/models/lineTypeSchema'; +import { arrowTypeSchema } from '@/models/arrowTypeSchema'; export const getShapes = createAsyncThunk<BioShape[] | undefined, void, ThunkConfig>( 'vectorMap/getShapes', @@ -40,3 +42,17 @@ export const getLineTypes = createAsyncThunk<LineType[] | undefined, void, Thunk } }, ); + +export const getArrowTypes = createAsyncThunk<ArrowType[] | undefined, void, ThunkConfig>( + 'vectorMap/getArrowTypes', + async () => { + try { + const { data } = await axiosInstanceNewAPI.get<ArrowType[]>(apiPath.getArrowTypes()); + const isDataValid = validateDataUsingZodSchema(data, z.array(arrowTypeSchema)); + + return isDataValid ? data : undefined; + } catch (error) { + return Promise.reject(getError({ error, prefix: ARROW_TYPES_FETCHING_ERROR_PREFIX })); + } + }, +); diff --git a/src/redux/shapes/shapes.types.ts b/src/redux/shapes/shapes.types.ts index 254bc237..e597d194 100644 --- a/src/redux/shapes/shapes.types.ts +++ b/src/redux/shapes/shapes.types.ts @@ -1,11 +1,14 @@ import { FetchDataState } from '@/types/fetchDataState'; -import { BioShape, LineType } from '@/types/models'; +import { ArrowType, BioShape, LineType } from '@/types/models'; export type LineTypesState = FetchDataState<LineType[], []>; +export type ArrowTypesState = FetchDataState<ArrowType[], []>; + export type BioShapesState = FetchDataState<BioShape[], []>; export type ShapesState = { lineTypesState: LineTypesState; + arrowTypesState: ArrowTypesState; bioShapesState: BioShapesState; }; diff --git a/src/redux/store.ts b/src/redux/store.ts index 2df34675..f6b8ad65 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -12,6 +12,7 @@ import modalReducer from '@/redux/modal/modal.slice'; import modelsReducer from '@/redux/models/models.slice'; import shapesReducer from '@/redux/shapes/shapes.slice'; import modelElementsReducer from '@/redux/modelElements/modelElements.slice'; +import layersReducer from '@/redux/layers/layers.slice'; import oauthReducer from '@/redux/oauth/oauth.slice'; import overlayBioEntityReducer from '@/redux/overlayBioEntity/overlayBioEntity.slice'; import overlaysReducer from '@/redux/overlays/overlays.slice'; @@ -63,6 +64,7 @@ export const reducers = { models: modelsReducer, shapes: shapesReducer, modelElements: modelElementsReducer, + layers: layersReducer, reactions: reactionsReducer, contextMenu: contextMenuReducer, cookieBanner: cookieBannerReducer, diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index b7434131..b05d1f57 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -7,6 +7,7 @@ import { ChevronUpIcon } from '@/shared/Icon/Icons/ChevronUpIcon'; import { CloseIcon } from '@/shared/Icon/Icons/CloseIcon'; import { DotsIcon } from '@/shared/Icon/Icons/DotsIcon'; import { ExportIcon } from '@/shared/Icon/Icons/ExportIcon'; +import { LayersIcon } from '@/shared/Icon/Icons/LayersIcon'; import { InfoIcon } from '@/shared/Icon/Icons/InfoIcon'; import { LegendIcon } from '@/shared/Icon/Icons/LegendIcon'; import { PageIcon } from '@/shared/Icon/Icons/PageIcon'; @@ -40,6 +41,7 @@ const icons: Record<IconTypes, IconComponentType> = { dots: DotsIcon, admin: AdminIcon, export: ExportIcon, + layers: LayersIcon, info: InfoIcon, legend: LegendIcon, page: PageIcon, diff --git a/src/shared/Icon/Icons/LayersIcon.tsx b/src/shared/Icon/Icons/LayersIcon.tsx new file mode 100644 index 00000000..bebcbea3 --- /dev/null +++ b/src/shared/Icon/Icons/LayersIcon.tsx @@ -0,0 +1,39 @@ +interface LayersIconProps { + className?: string; +} + +export const LayersIcon = ({ className }: LayersIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="0 0 24 24" + fill="none" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 4L4 8.5L12 13L20 8.5L12 4Z" + stroke="black" + strokeWidth="1" + strokeLinecap="round" + strokeLinejoin="round" + fill="none" + /> + <path + d="M4 12.5L12 17L20 12.5" + stroke="black" + strokeWidth="1" + strokeLinecap="round" + strokeLinejoin="round" + fill="none" + /> + <path + d="M4 16.5L12 21L20 16.5" + stroke="black" + strokeWidth="1" + strokeLinecap="round" + strokeLinejoin="round" + fill="none" + /> + </svg> +); diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts index d6f79610..4a7f5114 100644 --- a/src/types/drawerName.ts +++ b/src/types/drawerName.ts @@ -10,4 +10,5 @@ export type DrawerName = | 'overlays' | 'bio-entity' | 'comment' - | 'available-plugins'; + | 'available-plugins' + | 'layers'; diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 07442161..6feeea09 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -8,6 +8,7 @@ export type IconTypes = | 'dots' | 'admin' | 'export' + | 'layers' | 'info' | 'legend' | 'page' diff --git a/src/types/models.ts b/src/types/models.ts index f9a2fe30..13dd6e28 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -67,9 +67,17 @@ import { javaStacktraceSchema } from '@/models/javaStacktraceSchema'; import { oauthSchema } from '@/models/oauthSchema'; import { bioShapeSchema } from '@/models/bioShapeSchema'; import { shapeSchema } from '@/models/shapeSchema'; -import { modelElementsSchema } from '@/models/modelElementsSchema'; import { modelElementModificationSchema } from '@/models/modelElementModificationSchema'; import { lineTypeSchema } from '@/models/lineTypeSchema'; +import { layerSchema } from '@/models/layerSchema'; +import { layerTextSchema } from '@/models/layerTextSchema'; +import { layerRectSchema } from '@/models/layerRectSchema'; +import { pageableSchema } from '@/models/pageableSchema'; +import { modelElementSchema } from '@/models/modelElementSchema'; +import { layerOvalSchema } from '@/models/layerOvalSchema'; +import { layerLineSchema } from '@/models/layerLineSchema'; +import { arrowTypeSchema } from '@/models/arrowTypeSchema'; +import { arrowSchema } from '@/models/arrowSchema'; export type Project = z.infer<typeof projectSchema>; export type OverviewImageView = z.infer<typeof overviewImageView>; @@ -79,7 +87,18 @@ export type OverviewImageLinkModel = z.infer<typeof overviewImageLinkModel>; export type MapModel = z.infer<typeof mapModelSchema>; export type BioShape = z.infer<typeof bioShapeSchema>; export type LineType = z.infer<typeof lineTypeSchema>; +export type ArrowType = z.infer<typeof arrowTypeSchema>; +const layersSchema = pageableSchema(layerSchema); +export type Layers = z.infer<typeof layersSchema>; +export type Layer = z.infer<typeof layerSchema>; +export type LayerText = z.infer<typeof layerTextSchema>; +export type LayerRect = z.infer<typeof layerRectSchema>; +export type LayerOval = z.infer<typeof layerOvalSchema>; +export type LayerLine = z.infer<typeof layerLineSchema>; +export type Arrow = z.infer<typeof arrowSchema>; +const modelElementsSchema = pageableSchema(modelElementSchema); export type ModelElements = z.infer<typeof modelElementsSchema>; +export type ModelElement = z.infer<typeof modelElementSchema>; export type Shape = z.infer<typeof shapeSchema>; export type Modification = z.infer<typeof modelElementModificationSchema>; export type MapOverlay = z.infer<typeof mapOverlay>; -- GitLab