diff --git a/src/components/FunctionalArea/Modal/EditOverlayGroupModal/EditOverlayGroupModal.component.test.tsx b/src/components/FunctionalArea/Modal/EditOverlayGroupModal/EditOverlayGroupModal.component.test.tsx index c96ff7ee872ec2911fd37fd8f1f921aadb310f3f..65884f0964738cd84d099bf4111fec7fcae71e1b 100644 --- a/src/components/FunctionalArea/Modal/EditOverlayGroupModal/EditOverlayGroupModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/EditOverlayGroupModal/EditOverlayGroupModal.component.test.tsx @@ -55,6 +55,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -76,6 +77,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -110,6 +112,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -156,6 +159,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -196,6 +200,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -242,6 +247,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -274,6 +280,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); diff --git a/src/components/FunctionalArea/Modal/EditOverlayGroupModal/hooks/useEditOverlayGroup.test.ts b/src/components/FunctionalArea/Modal/EditOverlayGroupModal/hooks/useEditOverlayGroup.test.ts index 8f627f2cf21b441b9010cb1b1c0cabb98af9b387..8662528c237966f09cdeb5338b2084a223a34e07 100644 --- a/src/components/FunctionalArea/Modal/EditOverlayGroupModal/hooks/useEditOverlayGroup.test.ts +++ b/src/components/FunctionalArea/Modal/EditOverlayGroupModal/hooks/useEditOverlayGroup.test.ts @@ -30,6 +30,7 @@ describe('useEditOverlayGroup', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -70,6 +71,7 @@ describe('useEditOverlayGroup', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -113,6 +115,7 @@ describe('useEditOverlayGroup', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -152,6 +155,7 @@ describe('useEditOverlayGroup', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -192,6 +196,7 @@ describe('useEditOverlayGroup', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx index 484ce41b9ca6a374e0c5d10042134a29e388c302..f56bc6a710618fd795b84bfcb8bc0494259bbe80 100644 --- a/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/EditOverlayModal/EditOverlayModal.component.test.tsx @@ -57,6 +57,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -78,6 +79,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -112,6 +114,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -151,6 +154,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -191,6 +195,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -237,6 +242,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, overlays: OVERLAYS_INITIAL_STATE_MOCK, }); @@ -267,6 +273,7 @@ describe('EditOverlayModal - component', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); diff --git a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts index a0f6504ad1c95272b279f4341f2a90cd0c978649..f0a3fd8473d20a9b61213da2fc000cbe8f2aac06 100644 --- a/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts +++ b/src/components/FunctionalArea/Modal/EditOverlayModal/hooks/useEditOverlay.test.ts @@ -29,6 +29,7 @@ describe('useEditOverlay', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -69,6 +70,7 @@ describe('useEditOverlay', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -112,6 +114,7 @@ describe('useEditOverlay', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -152,6 +155,7 @@ describe('useEditOverlay', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); @@ -192,6 +196,7 @@ describe('useEditOverlay', () => { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }, }); diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx index 7bfed87c90040deabb9981e9480bc7fd5539d52a..3cf8590754fc1b8a41dac5e15f48b1da52c7c7c4 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx @@ -64,6 +64,7 @@ const renderComponent = ( width: 1, height: 1, }, + layerLineFactoryState: undefined, }, models: { ...MODELS_DATA_MOCK_WITH_MAIN_MAP, @@ -88,6 +89,7 @@ describe('LayerImageObjectEditFactoryModal - component', () => { ...layerImageFixture, glyph: null, }, + layerLine: null, }); expect(screen.getByText(/Glyph:/i)).toBeInTheDocument(); @@ -103,6 +105,7 @@ describe('LayerImageObjectEditFactoryModal - component', () => { ...layerImageFixture, glyph: null, }, + layerLine: null, }); const dropdown = screen.getByTestId('autocomplete'); @@ -121,6 +124,7 @@ describe('LayerImageObjectEditFactoryModal - component', () => { ...layerImageFixture, glyph: null, }, + layerLine: null, }); const dropdown = screen.getByTestId('autocomplete'); @@ -168,6 +172,7 @@ describe('LayerImageObjectEditFactoryModal - component', () => { renderComponent({ activeAction: MAP_EDIT_ACTIONS.TRANSFORM_IMAGE, layerObject: glyphData, + layerLine: null, }); const submitButton = screen.getByText(/Submit/i); @@ -189,6 +194,7 @@ describe('LayerImageObjectEditFactoryModal - component', () => { ...layerImageFixture, glyph: null, }, + layerLine: null, }); store.dispatch({ diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx index b14ea618617053ad92f36e12f3470c836ad7f9fc..f2d498e0b5345787ecd18faee8e1d0a787c05a32 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx @@ -58,6 +58,7 @@ const renderComponent = (): { store: StoreType } => { width: 1, height: 1, }, + layerLineFactoryState: undefined, }, models: { ...MODELS_DATA_MOCK_WITH_MAIN_MAP, diff --git a/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineEditFactoryModal.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6924c0382e9cadf0ff906da90b5474141f5a0421 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineEditFactoryModal.component.tsx @@ -0,0 +1,154 @@ +/* eslint-disable no-magic-numbers */ +import React, { useState } from 'react'; +import './LayerLineFactoryModal.styles.css'; +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { Button } from '@/shared/Button'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { Color } from '@/types/models'; +import { showToast } from '@/utils/showToast'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { mapEditToolsSetLayerLine } from '@/redux/mapEditTools/mapEditTools.slice'; +import { SerializedError } from '@reduxjs/toolkit'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useMapInstance } from '@/utils/context/mapInstanceContext'; +import { mapEditToolsLayerLineSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; +import { updateLayerLine } from '@/redux/layers/layers.thunks'; +import { layerUpdateLine } from '@/redux/layers/layers.slice'; +import { + LayerLineFactoryForm, + LayerLineFactoryPayload, +} from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types'; +import { + DEFAULT_ARROW_ANGLE, + DEFAULT_ARROW_LENGTH, + DEFAULT_ARROW_LINE_TYPE, +} from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.constants'; +import { LayerLineForm } from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineForm.component'; +import { arrowTypesKeysSelector, lineTypesKeysSelector } from '@/redux/shapes/shapes.selectors'; + +export const LayerLineEditFactoryModal: React.FC = () => { + const layerLine = useAppSelector(mapEditToolsLayerLineSelector); + const currentModelId = useAppSelector(currentModelIdSelector); + const dispatch = useAppDispatch(); + const { mapInstance } = useMapInstance(); + const lineTypes = useAppSelector(lineTypesKeysSelector).map(lineType => ({ + id: lineType, + name: lineType, + })); + const arrowTypes = useAppSelector(arrowTypesKeysSelector).map(arrowType => ({ + id: arrowType, + name: arrowType, + })); + + if (!layerLine) { + throw new Error('No layer line object'); + } + + const [isSending, setIsSending] = useState<boolean>(false); + const [data, setData] = useState<LayerLineFactoryForm>({ + color: layerLine.color, + lineType: layerLine.lineType, + width: layerLine.width, + startArrow: layerLine.startArrow.arrowType, + endArrow: layerLine.endArrow.arrowType, + }); + + const getDataToSend = (): LayerLineFactoryPayload => { + return { + color: data.color, + lineType: data.lineType, + width: data.width, + startArrow: { + arrowType: data.startArrow, + angle: DEFAULT_ARROW_ANGLE, + lineType: DEFAULT_ARROW_LINE_TYPE, + length: DEFAULT_ARROW_LENGTH, + }, + endArrow: { + arrowType: data.endArrow, + angle: DEFAULT_ARROW_ANGLE, + lineType: DEFAULT_ARROW_LINE_TYPE, + length: DEFAULT_ARROW_LENGTH, + }, + segments: layerLine.segments, + z: layerLine.z, + }; + }; + + const handleSubmit = async (): Promise<void> => { + if (!layerLine) { + return; + } + try { + const updatedLayerLine = await dispatch( + updateLayerLine({ + modelId: currentModelId, + layerId: layerLine.layer, + lineId: layerLine.id, + payload: getDataToSend(), + }), + ).unwrap(); + + if (!updatedLayerLine) { + showToast({ + type: 'error', + message: 'An error occurred while editing the line.', + }); + return; + } + + dispatch( + layerUpdateLine({ + modelId: currentModelId, + layerId: updatedLayerLine.layer, + layerLine: updatedLayerLine, + }), + ); + dispatch(mapEditToolsSetLayerLine(updatedLayerLine)); + updateElement(mapInstance, updatedLayerLine.layer, updatedLayerLine); + showToast({ + type: 'success', + message: 'The line has been successfully updated.', + }); + dispatch(closeModal()); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while editing the line.', + }); + } finally { + setIsSending(false); + } + }; + + const changeValues = (value: string | number | Color, key: string): void => { + setData(prevData => ({ ...prevData, [key]: value })); + }; + + return ( + <div className="relative flex w-[550px] flex-col gap-4 rounded-b-lg border border-t-[#E1E0E6] bg-white p-[24px]"> + {isSending && ( + <div className="c-layer-line-factory-modal-loader"> + <LoadingIndicator width={44} height={44} /> + </div> + )} + <LayerLineForm + onChange={changeValues} + data={data} + lineTypes={lineTypes} + arrowTypes={arrowTypes} + /> + <hr /> + <Button + type="button" + onClick={handleSubmit} + className="justify-center self-end justify-self-end text-base font-medium" + > + Submit + </Button> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.constants.ts b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..451a70008369003755c2d67f9a67b1a28b08e640 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.constants.ts @@ -0,0 +1,13 @@ +export const MAX_LINE_WIDTH = 48; + +export const MIN_LINE_WIDTH = 1; + +export const DEFAULT_LINE_TYPE = 'SOLID'; + +export const DEFAULT_ARROW = 'NONE'; + +export const DEFAULT_ARROW_LENGTH = 15; + +export const DEFAULT_ARROW_ANGLE = 2.748893571891069; + +export const DEFAULT_ARROW_LINE_TYPE = 'SOLID'; diff --git a/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types.ts b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..1282cbbace98d9e43afa96d67ecad57ef526e08f --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types.ts @@ -0,0 +1,29 @@ +import { Arrow, Color, Segment } from '@/types/models'; + +export type LayerLineFactoryForm = { + color: Color; + lineType: string; + width: number; + startArrow: string; + endArrow: string; +}; + +export type LayerLineFactoryPayload = { + color: Color; + lineType: string; + width: number; + startArrow: Arrow; + endArrow: Arrow; + segments: Array<Segment>; + z: number; +}; + +export type LayerLineEditFactoryPayload = { + color: Color; + lineType: string; + width: number; + startArrow: Arrow; + endArrow: Arrow; + segments: Array<Segment>; + z: number; +}; diff --git a/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactoryModal.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aa68b84675a190c9659fbd54cf2b8c90f6514a0e --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactoryModal.component.tsx @@ -0,0 +1,156 @@ +/* eslint-disable no-magic-numbers */ +import React, { useState } from 'react'; +import './LayerLineFactoryModal.styles.css'; +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { Button } from '@/shared/Button'; +import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; +import { LayerLineForm } from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineForm.component'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { + layersDrawLayerSelector, + maxObjectZIndexForLayerSelector, +} from '@/redux/layers/layers.selectors'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { Color } from '@/types/models'; +import { layerLineFactoryStateSelector } from '@/redux/modal/modal.selector'; +import { addLayerLine } from '@/redux/layers/layers.thunks'; +import { showToast } from '@/utils/showToast'; +import { layerAddLine } from '@/redux/layers/layers.slice'; +import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; +import { SerializedError } from '@reduxjs/toolkit'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useMapInstance } from '@/utils/context/mapInstanceContext'; +import { + LayerLineFactoryForm, + LayerLineFactoryPayload, +} from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types'; +import { arrowTypesKeysSelector, lineTypesKeysSelector } from '@/redux/shapes/shapes.selectors'; +import { + DEFAULT_ARROW, + DEFAULT_ARROW_ANGLE, + DEFAULT_ARROW_LENGTH, + DEFAULT_ARROW_LINE_TYPE, + DEFAULT_LINE_TYPE, + MIN_LINE_WIDTH, +} from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.constants'; + +export const LayerLineFactoryModal: React.FC = () => { + const drawLayer = useAppSelector(layersDrawLayerSelector); + const currentModelId = useAppSelector(currentModelIdSelector); + const layerLineFactoryState = useAppSelector(layerLineFactoryStateSelector); + const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, drawLayer)); + const dispatch = useAppDispatch(); + const lineTypes = useAppSelector(lineTypesKeysSelector).map(lineType => ({ + id: lineType, + name: lineType, + })); + const arrowTypes = useAppSelector(arrowTypesKeysSelector).map(arrowType => ({ + id: arrowType, + name: arrowType, + })); + const { mapInstance } = useMapInstance(); + + const [isSending, setIsSending] = useState<boolean>(false); + const [data, setData] = useState<LayerLineFactoryForm>({ + color: BLACK_COLOR, + lineType: DEFAULT_LINE_TYPE, + width: MIN_LINE_WIDTH, + startArrow: DEFAULT_ARROW, + endArrow: DEFAULT_ARROW, + }); + + const getDataToSend = (): LayerLineFactoryPayload => { + return { + color: data.color, + lineType: data.lineType, + width: data.width, + startArrow: { + arrowType: data.startArrow, + angle: DEFAULT_ARROW_ANGLE, + lineType: DEFAULT_ARROW_LINE_TYPE, + length: DEFAULT_ARROW_LENGTH, + }, + endArrow: { + arrowType: data.endArrow, + angle: DEFAULT_ARROW_ANGLE, + lineType: DEFAULT_ARROW_LINE_TYPE, + length: DEFAULT_ARROW_LENGTH, + }, + segments: layerLineFactoryState || [], + z: maxZIndex + 1, + }; + }; + + const handleSubmit = async (): Promise<void> => { + if (!layerLineFactoryState || !drawLayer) { + return; + } + try { + const layerLine = await dispatch( + addLayerLine({ + modelId: currentModelId, + layerId: drawLayer, + payload: getDataToSend(), + }), + ).unwrap(); + if (!layerLine) { + showToast({ + type: 'error', + message: 'An error occurred while adding a new line.', + }); + return; + } + dispatch(layerAddLine({ modelId: currentModelId, layerId: drawLayer, layerLine })); + drawElementOnLayer({ + mapInstance, + activeLayer: drawLayer, + object: layerLine, + drawFunctionKey: 'drawLine', + }); + showToast({ + type: 'success', + message: 'A new line has been successfully added.', + }); + dispatch(closeModal()); + dispatch(mapEditToolsSetActiveAction(null)); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while adding a new line.', + }); + } finally { + setIsSending(false); + } + }; + + const changeValues = (value: string | number | Color, key: string): void => { + setData(prevData => ({ ...prevData, [key]: value })); + }; + + return ( + <div className="relative flex w-[550px] flex-col gap-4 rounded-b-lg border border-t-[#E1E0E6] bg-white p-[24px]"> + {isSending && ( + <div className="c-layer-line-factory-modal-loader"> + <LoadingIndicator width={44} height={44} /> + </div> + )} + <LayerLineForm + onChange={changeValues} + data={data} + lineTypes={lineTypes} + arrowTypes={arrowTypes} + /> + <hr /> + <Button + type="button" + onClick={handleSubmit} + className="justify-center self-end justify-self-end text-base font-medium" + > + Submit + </Button> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactoryModal.styles.css b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactoryModal.styles.css new file mode 100644 index 0000000000000000000000000000000000000000..e6bb96a9607fbb5a71e5053fd9ef3b478d9cb15c --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactoryModal.styles.css @@ -0,0 +1,12 @@ +.c-layer-line-factory-modal-loader { + width: 100%; + height: 100%; + margin-left: -24px; + margin-top: -24px; + background: #f9f9f980; + z-index: 1; + position: absolute; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineForm.component.tsx b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineForm.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f77f23c92a1b9091e3d2eed2383870d1e2af919f --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineForm.component.tsx @@ -0,0 +1,95 @@ +/* eslint-disable no-magic-numbers */ +import { ColorTilePicker } from '@/shared/ColorPicker/ColorTilePicker.component'; +import hexToRgbIntAlpha from '@/utils/convert/hexToRgbIntAlpha'; +import { Color } from '@/types/models'; +import { LayerLineFactoryForm } from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types'; +import { Select } from '@/shared/Select'; +import React from 'react'; +import { Input } from '@/shared/Input'; +import { + MAX_LINE_WIDTH, + MIN_LINE_WIDTH, +} from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.constants'; + +type LayerLineFormProps = { + data: LayerLineFactoryForm; + lineTypes: Array<{ id: string; name: string }>; + arrowTypes: Array<{ id: string; name: string }>; + onChange: (value: number | string | Color, key: string) => void; +}; + +export const LayerLineForm = ({ + data, + onChange, + lineTypes, + arrowTypes, +}: LayerLineFormProps): React.JSX.Element => { + const onLineWidthChange = (event: React.ChangeEvent<HTMLInputElement>): void => { + const value = Number(event.target.value); + let resultValue = value; + if (value < MIN_LINE_WIDTH) { + resultValue = MIN_LINE_WIDTH; + } + if (value > MAX_LINE_WIDTH) { + resultValue = MAX_LINE_WIDTH; + } + onChange(resultValue, 'width'); + }; + + return ( + <div className="grid grid-cols-3 gap-2"> + <div> + <span>Color:</span> + <ColorTilePicker + initialColor={data.color} + colorChange={color => onChange(hexToRgbIntAlpha(color), 'color')} + /> + </div> + <div> + <span>Line type:</span> + <Select + options={lineTypes} + selectedId={data.lineType} + listClassName="max-h-48" + testId="font-size-select" + onChange={value => onChange(value, 'lineType')} + /> + </div> + <div> + <span>Line width:</span> + <Input + type="number" + name="line-width" + min={MIN_LINE_WIDTH} + max={MAX_LINE_WIDTH} + id="line-width" + value={data.width} + onChange={onLineWidthChange} + className="text-sm font-medium text-font-400" + /> + </div> + + <div> + <span>Start arrow:</span> + <Select + options={arrowTypes} + selectedId={data.startArrow} + listClassName="max-h-48" + testId="font-size-select" + onChange={value => onChange(value, 'startArrow')} + /> + </div> + + <div> + <span>End arrow:</span> + <Select + options={arrowTypes} + selectedId={data.endArrow} + listClassName="max-h-48" + testId="font-size-select" + onChange={value => onChange(value, 'endArrow')} + /> + </div> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/LayerOvalFactoryModal/LayerOvalFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerOvalFactoryModal/LayerOvalFactoryModal.component.test.tsx index 3fdb2ca207a8e35065ec7f31d146502759b3254d..c5ea45971b16b1b3c2d4ca4fcae92dcb901faec6 100644 --- a/src/components/FunctionalArea/Modal/LayerOvalFactoryModal/LayerOvalFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerOvalFactoryModal/LayerOvalFactoryModal.component.test.tsx @@ -58,6 +58,7 @@ const renderComponent = (): { store: StoreType } => { width: 1, height: 1, }, + layerLineFactoryState: undefined, }, models: { ...MODELS_DATA_MOCK_WITH_MAIN_MAP, diff --git a/src/components/FunctionalArea/Modal/LayerRectFactoryModal/LayerRectFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerRectFactoryModal/LayerRectFactoryModal.component.test.tsx index 2e14dc505c4e18528e22c63192b2f3f12cfb2387..e06693abb844a3f7810a29e204926eef07d786c8 100644 --- a/src/components/FunctionalArea/Modal/LayerRectFactoryModal/LayerRectFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerRectFactoryModal/LayerRectFactoryModal.component.test.tsx @@ -58,6 +58,7 @@ const renderComponent = (): { store: StoreType } => { width: 1, height: 1, }, + layerLineFactoryState: undefined, }, models: { ...MODELS_DATA_MOCK_WITH_MAIN_MAP, diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx index a88c47d443f1da4db79b67ec19435314b3a27911..22d090f531916f40fae7ded0b5f87b241977aa7b 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.test.tsx @@ -63,6 +63,7 @@ const renderComponent = (): { store: StoreType } => { width: 1, height: 1, }, + layerLineFactoryState: undefined, }, models: { ...MODELS_DATA_MOCK_WITH_MAIN_MAP, diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index 38a0141484f1cff230bdb4b206351ad05a121e8a..7a12dfea3e2161579b60d8b9426f46ee89665c6b 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -16,6 +16,8 @@ import { LayerRectFactoryModal } from '@/components/FunctionalArea/Modal/LayerRe import { LayerRectEditFactoryModal } from '@/components/FunctionalArea/Modal/LayerRectFactoryModal/LayerRectEditFactoryModal.component'; import { LayerOvalFactoryModal } from '@/components/FunctionalArea/Modal/LayerOvalFactoryModal/LayerOvalFactoryModal.component'; import { LayerOvalEditFactoryModal } from '@/components/FunctionalArea/Modal/LayerOvalFactoryModal/LayerOvalEditFactoryModal.component'; +import { LayerLineFactoryModal } from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactoryModal.component'; +import { LayerLineEditFactoryModal } from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineEditFactoryModal.component'; import { EditOverlayModal } from './EditOverlayModal'; import { LoginModal } from './LoginModal'; import { ErrorReportModal } from './ErrorReportModal'; @@ -141,6 +143,16 @@ export const Modal = (): React.ReactNode => { <LayerOvalEditFactoryModal /> </ModalLayout> )} + {isOpen && modalName === 'layer-line-factory' && ( + <ModalLayout> + <LayerLineFactoryModal /> + </ModalLayout> + )} + {isOpen && modalName === 'layer-line-edit-factory' && ( + <ModalLayout> + <LayerLineEditFactoryModal /> + </ModalLayout> + )} </> ); }; diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx index 41ceda61f5115424eb965c61c72649bf8cdddb7d..c17ccf0c6f8262eb7e6b9fc73432edadc2ac2183 100644 --- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx +++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx @@ -34,6 +34,8 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => { modalName === 'add-comment' && 'h-auto w-[400px]', modalName === 'error-report' && 'h-auto w-[800px]', modalName === 'layer-factory' && 'h-auto w-[400px]', + ['layer-line-factory', 'layer-line-edit-factory'].includes(modalName) && + 'h-auto w-[550] overflow-visible', ['layer-oval-factory', 'layer-oval-edit-factory'].includes(modalName) && 'h-auto w-[300px] overflow-visible', ['layer-rect-factory', 'layer-rect-edit-factory'].includes(modalName) && @@ -53,6 +55,8 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => { 'layer-rect-edit-factory', 'layer-oval-factory', 'layer-oval-edit-factory', + 'layer-line-factory', + 'layer-line-edit-factory', ].includes(modalName) && 'rounded-t-lg', )} > diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component.tsx index ce8ca33db689e416cd95a0a280bbab3d16f3b011..70d06f0f016136e0ab8248a8f4d52a70c69b352f 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component.tsx @@ -9,6 +9,7 @@ type LayerDrawerLayerContextMenuProps = { addText: () => void; addRect: () => void; addOval: () => void; + addLine: () => void; }; export const LayerDrawerLayerContextMenu = ({ @@ -18,6 +19,7 @@ export const LayerDrawerLayerContextMenu = ({ addText, addRect, addOval, + addLine, }: LayerDrawerLayerContextMenuProps): JSX.Element => { const [menuVisible, setMenuVisible] = useState(false); const menuRef = useRef<HTMLUListElement>(null); @@ -78,6 +80,10 @@ export const LayerDrawerLayerContextMenu = ({ setMenuVisible(false); addOval(); }} + addLine={() => { + setMenuVisible(false); + addLine(); + }} /> )} </div> diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index 6ae1ccf145b49caee0717a02fb7cf9c07a36bf47..cdd055f9b81e20e10e0d42d68c94b9f829b083a2 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -44,7 +44,8 @@ export const LayersDrawer = (): JSX.Element => { document.getElementById(`layer-image-item-${mapEditToolsLayerObject.id}`) || document.getElementById(`layer-text-item-${mapEditToolsLayerObject.id}`) || document.getElementById(`layer-rect-item-${mapEditToolsLayerObject.id}`) || - document.getElementById(`layer-oval-item-${mapEditToolsLayerObject.id}`); + document.getElementById(`layer-oval-item-${mapEditToolsLayerObject.id}`) || + document.getElementById(`layer-line-item-${mapEditToolsLayerObject.id}`); if (!layerObjectElement) { return; } diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx index be3043a3f9f7102480e9f851213cd1c8a515d042..ab63317bff62adde5d1771982e99631fb5e1af94 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx @@ -75,6 +75,11 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.ADD_OVAL)); }; + const addLine = (): void => { + dispatch(setDrawLayer({ modelId: currentModelId, layerId: layerDetails.id })); + dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.ADD_LINE)); + }; + const rejectRemove = (): void => { setIsModalOpen(false); }; @@ -194,6 +199,7 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX addText={addText} addRect={addRect} addOval={addOval} + addLine={addLine} moveToFront={moveToFront} moveToBack={moveToBack} moveAboveDiagram={moveAboveDiagram} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx index f97407df423724c26c77a449d3a1d137a9a98ea6..42c670fe49da2d2d95864e78c4dca53c41973f01 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx @@ -17,6 +17,7 @@ type LayersDrawerLayerActionsProps = { addText: () => void; addRect: () => void; addOval: () => void; + addLine: () => void; moveToFront: () => void; moveToBack: () => void; moveAboveDiagram: () => void; @@ -35,6 +36,7 @@ export const LayersDrawerLayerActions = ({ addText, addRect, addOval, + addLine, moveToFront, moveToBack, moveAboveDiagram, @@ -95,6 +97,7 @@ export const LayersDrawerLayerActions = ({ addText={addText} addRect={addRect} addOval={addOval} + addLine={addLine} /> </> )} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component.tsx index 4871ce703503751caa289fab31874da465ec162c..af6fa9a756ff186142ece11ac27bf0b6f415c99a 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerContextMenuItems.component.tsx @@ -8,6 +8,7 @@ type LayerDrawerLayerContextMenuProps = { addText: () => void; addRect: () => void; addOval: () => void; + addLine: () => void; }; export const LayerDrawerLayerContextMenuItems = forwardRef< @@ -22,6 +23,7 @@ export const LayerDrawerLayerContextMenuItems = forwardRef< addText, addRect, addOval, + addLine, }: LayerDrawerLayerContextMenuProps, ref, ): JSX.Element => { @@ -72,6 +74,16 @@ export const LayerDrawerLayerContextMenuItems = forwardRef< <Icon name="oval" /> <span>Add oval</span> </li> + <li + className="flex min-h-[24px] cursor-pointer gap-3 px-4 py-1 hover:bg-gray-200" + tabIndex={0} + onClick={addLine} + onKeyDown={handleKeyPress} + role="menuitem" + > + <Icon name="line" /> + <span>Add line</span> + </li> <li className="flex min-h-[24px] cursor-pointer gap-3 px-4 py-1 hover:bg-gray-200" tabIndex={0} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLineItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLineItem.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9751eab9d01b24e2edb10860d27f95ea7b6896bb --- /dev/null +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLineItem.component.tsx @@ -0,0 +1,80 @@ +import React, { JSX, useMemo } from 'react'; +import { LayerLine } from '@/types/models'; +import { Icon } from '@/shared/Icon'; +import { LayersDrawerObjectActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; +import { mapEditToolsSetLayerLine } from '@/redux/mapEditTools/mapEditTools.slice'; + +interface LayersDrawerOvalItemProps { + layerLine: LayerLine; + moveToFront: () => void; + moveToBack: () => void; + removeObject: () => void; + centerObject: () => void; + editObject: () => void; + isLayerVisible: boolean; + isLayerActive: boolean; +} + +export const LayersDrawerLineItem = ({ + layerLine, + moveToFront, + moveToBack, + removeObject, + centerObject, + editObject, + isLayerVisible, + isLayerActive, +}: LayersDrawerOvalItemProps): JSX.Element | null => { + const dispatch = useAppDispatch(); + const activeLayerObject = useAppSelector(mapEditToolsLayerObjectSelector); + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); + + const showActions = useMemo(() => { + return activeLayerObject?.id === layerLine.id; + }, [activeLayerObject?.id, layerLine.id]); + + const canSelectItem = useMemo(() => { + return isLayerVisible && isLayerActive && hasPrivilegeToWriteProject; + }, [isLayerVisible, isLayerActive, hasPrivilegeToWriteProject]); + + const selectItem = useMemo(() => { + return (): void => { + if (canSelectItem) { + dispatch(mapEditToolsSetLayerLine(layerLine)); + } + }; + }, [canSelectItem, dispatch, layerLine]); + + const handleKeyPress = (): void => {}; + + return ( + <div + className="flex min-h-[24px] items-center justify-between gap-2" + id={`layer-line-item-${layerLine.id}`} + > + <div + className={`flex gap-2 ${canSelectItem ? 'cursor-pointer' : 'cursor-default'}`} + onClick={selectItem} + tabIndex={0} + onKeyDown={handleKeyPress} + role="button" + > + <Icon name="line" className="shrink-0" /> + <span className="truncate">line - {layerLine.id}</span> + </div> + {showActions && ( + <LayersDrawerObjectActions + moveToFront={moveToFront} + moveToBack={moveToBack} + removeObject={removeObject} + centerObject={centerObject} + editObject={editObject} + /> + )} + </div> + ); +}; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx index c5386ec4820f047075c05a02782737ff444a5a19..494aac6c9bc12bd542976282eb64e5ecb827f35b 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -11,20 +11,24 @@ import { LayersDrawerTextItem } from '@/components/Map/Drawer/LayersDrawer/Layer import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; import { removeLayerImage, + removeLayerLine, removeLayerOval, removeLayerRect, removeLayerText, updateLayerImageObject, + updateLayerLine, updateLayerOval, updateLayerRect, updateLayerText, } from '@/redux/layers/layers.thunks'; import { layerDeleteImage, + layerDeleteLine, layerDeleteOval, layerDeleteRect, layerDeleteText, layerUpdateImage, + layerUpdateLine, layerUpdateOval, layerUpdateRect, layerUpdateText, @@ -32,11 +36,14 @@ import { import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/removeElementFromLayer'; import { showToast } from '@/utils/showToast'; import { SerializedError } from '@reduxjs/toolkit'; -import { LayerImage, LayerOval, LayerRect, LayerText } from '@/types/models'; +import { LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useMapInstance } from '@/utils/context/mapInstanceContext'; import { mapModelIdSelector } from '@/redux/map/map.selectors'; -import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { + mapEditToolsSetLayerLine, + mapEditToolsSetLayerObject, +} from '@/redux/mapEditTools/mapEditTools.slice'; import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; import { useSetBounds } from '@/utils/map/useSetBounds'; import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; @@ -44,12 +51,16 @@ import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { Coordinate } from 'ol/coordinate'; import { openLayerImageObjectEditFactoryModal, + openLayerLineEditFactoryModal, openLayerOvalEditFactoryModal, openLayerRectEditFactoryModal, openLayerTextEditFactoryModal, } from '@/redux/modal/modal.slice'; import { LayersDrawerRectItem } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerRectItem.component'; import { LayersDrawerOvalItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerOvalItem.component'; +import { LayersDrawerLineItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLineItem.component'; +import { LayerLineEditFactoryPayload } from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types'; +import getLayerLineBoundingBoxCoords from '@/components/Map/MapViewer/utils/shapes/layer/utils/getLayerLineBoundingBoxCoords'; interface LayersDrawerObjectsListProps { layerId: number; @@ -78,6 +89,11 @@ const removeObjectConfig = { successMessage: 'The layer oval has been successfully removed.', errorMessage: 'An error occurred while removing the layer oval.', }, + line: { + question: 'Are you sure you want to remove the line?', + successMessage: 'The layer line has been successfully removed.', + errorMessage: 'An error occurred while removing the layer line.', + }, }; export const LayersDrawerObjectsList = ({ @@ -89,19 +105,21 @@ export const LayersDrawerObjectsList = ({ const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, layerId)); const minZIndex = useAppSelector(state => minObjectZIndexForLayerSelector(state, layerId)); const layer = useAppSelector(state => layerByIdSelector(state, layerId)); - const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); + const mapEditToolsLayerObject = useAppSelector(mapEditToolsLayerObjectSelector); const [removeModalState, setRemoveModalState] = useState< - undefined | 'text' | 'image' | 'rect' | 'oval' + undefined | 'text' | 'image' | 'rect' | 'oval' | 'line' >(undefined); const [layerObjectToRemove, setLayerObjectToRemove] = useState< - LayerImage | LayerText | LayerRect | LayerOval | null + LayerImage | LayerText | LayerRect | LayerOval | LayerLine | null >(null); const dispatch = useAppDispatch(); const setBounds = useSetBounds(); const pointToProjection = usePointToProjection(); const { mapInstance } = useMapInstance(); - const removeObject = (layerObject: LayerImage | LayerText | LayerRect | LayerOval): void => { + const removeObject = ( + layerObject: LayerImage | LayerText | LayerRect | LayerOval | LayerLine, + ): void => { setLayerObjectToRemove(layerObject); if ('glyph' in layerObject) { setRemoveModalState('image'); @@ -109,6 +127,8 @@ export const LayersDrawerObjectsList = ({ setRemoveModalState('text'); } else if ('fillColor' in layerObject && 'borderColor' in layerObject) { setRemoveModalState('rect'); + } else if ('segments' in layerObject) { + setRemoveModalState('line'); } else { setRemoveModalState('oval'); } @@ -169,7 +189,7 @@ export const LayersDrawerObjectsList = ({ rectId: layerObjectToRemove.id, }), ); - } else { + } else if (removeModalState === 'oval') { await dispatch( removeLayerOval({ modelId: currentModelId, @@ -184,6 +204,21 @@ export const LayersDrawerObjectsList = ({ ovalId: layerObjectToRemove.id, }), ); + } else { + await dispatch( + removeLayerLine({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + lineId: layerObjectToRemove.id, + }), + ).unwrap(); + dispatch( + layerDeleteLine({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + lineId: layerObjectToRemove.id, + }), + ); } removeElementFromLayer({ mapInstance, @@ -359,8 +394,53 @@ export const LayersDrawerObjectsList = ({ await updateOvalZIndex({ zIndex: minZIndex - 1, layerOval }); }; + const updateLineZIndex = async ({ + zIndex, + layerLine, + }: { + zIndex: number; + layerLine: LayerLine; + }): Promise<void> => { + const payload = { + color: layerLine.color, + lineType: layerLine.lineType, + width: layerLine.width, + startArrow: layerLine.startArrow, + endArrow: layerLine.endArrow, + segments: layerLine.segments, + z: zIndex, + } as LayerLineEditFactoryPayload; + const newLayerLine = await dispatch( + updateLayerLine({ + modelId: currentModelId, + layerId: layerLine.layer, + lineId: layerLine.id, + payload, + }), + ).unwrap(); + if (newLayerLine) { + dispatch( + layerUpdateLine({ + modelId: currentModelId, + layerId: newLayerLine.layer, + layerLine: newLayerLine, + }), + ); + dispatch(mapEditToolsSetLayerLine(newLayerLine)); + updateElement(mapInstance, newLayerLine.layer, newLayerLine); + } + }; + + const moveLineToFront = async (layerLine: LayerLine): Promise<void> => { + await updateLineZIndex({ zIndex: maxZIndex + 1, layerLine }); + }; + + const moveLineToBack = async (layerLine: LayerLine): Promise<void> => { + await updateLineZIndex({ zIndex: minZIndex - 1, layerLine }); + }; + const centerObject = (layerObject: LayerImage | LayerText | LayerRect | LayerOval): void => { - if (mapEditToolsLayerImageObject && mapEditToolsLayerImageObject.id === layerObject.id) { + if (mapEditToolsLayerObject && mapEditToolsLayerObject.id === layerObject.id) { const point1 = pointToProjection({ x: layerObject.x, y: layerObject.y }); const point2 = pointToProjection({ x: layerObject.x + layerObject.width, @@ -370,6 +450,16 @@ export const LayersDrawerObjectsList = ({ } }; + const centerLayerLineObject = (layerLine: LayerLine): void => { + if (mapEditToolsLayerObject && mapEditToolsLayerObject.id === layerLine.id) { + const coordinates = getLayerLineBoundingBoxCoords({ + segments: layerLine.segments, + pointToProjection, + }); + setBounds(coordinates); + } + }; + const editImage = (): void => { dispatch(openLayerImageObjectEditFactoryModal()); }; @@ -386,6 +476,10 @@ export const LayersDrawerObjectsList = ({ dispatch(openLayerOvalEditFactoryModal()); }; + const editLine = (): void => { + dispatch(openLayerLineEditFactoryModal()); + }; + if (!layer) { return null; } @@ -454,6 +548,19 @@ export const LayersDrawerObjectsList = ({ isLayerActive={isLayerActive} /> ))} + {Object.values(layer.lines).map(layerLine => ( + <LayersDrawerLineItem + layerLine={layerLine} + key={layerLine.id} + moveToFront={() => moveLineToFront(layerLine)} + moveToBack={() => moveLineToBack(layerLine)} + removeObject={() => removeObject(layerLine)} + centerObject={() => centerLayerLineObject(layerLine)} + editObject={() => editLine()} + isLayerVisible={isLayerVisible} + isLayerActive={isLayerActive} + /> + ))} </div> ); }; diff --git a/src/components/Map/MapViewer/MapViewer.constants.ts b/src/components/Map/MapViewer/MapViewer.constants.ts index 7334a70e8c4d09aba29843987f42c7f9fbd2b992..d3b61ac55070d8def3759b93918e262aad6a9b7e 100644 --- a/src/components/Map/MapViewer/MapViewer.constants.ts +++ b/src/components/Map/MapViewer/MapViewer.constants.ts @@ -22,13 +22,13 @@ export const COMPARTMENT_SBO_TERM = 'SBO:0000290'; export const ION_CHANNEL_SBO_TERM = 'SBO:0000284'; -export const HOMODIMER_INFO_BOX_WIDTH = 25; +export const HOMODIMER_INFO_BOX_WIDTH = 40; -export const HOMODIMER_INFO_BOX_HEIGHT = 15; +export const HOMODIMER_INFO_BOX_HEIGHT = 18; -export const ACTIVITY_INFO_BOX_WIDTH = 35; +export const ACTIVITY_INFO_BOX_WIDTH = 40; -export const ACTIVITY_INFO_BOX_HEIGHT = 15; +export const ACTIVITY_INFO_BOX_HEIGHT = 18; export const TEXT_CUTOFF_SCALE = 0.34; export const OUTLINE_CUTOFF_SCALE = 0.18; @@ -73,6 +73,7 @@ export const LAYER_ELEMENT_TYPES = { RECT: 'RECT', LINE: 'LINE', ARROW: 'ARROW', + IMAGE: 'IMAGE', }; export const COMPARTMENT_SQUARE_POINTS: Array<ShapeRelAbs | ShapeRelAbsBezierPoint> = [ diff --git a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 0d31cdb6452766c70efae6c4459a5d0b38d6446f..3e0e2dc6b350f98933f8e0eca1ca95fc017803a0 100644 --- a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -24,7 +24,8 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { mapDataSizeSelector } from '@/redux/map/map.selectors'; import { mapEditToolsActiveActionSelector, - mapEditToolsLayerObjectSelector, + mapEditToolsLayerLineSelector, + mapEditToolsLayerNonLineObjectSelector, } from '@/redux/mapEditTools/mapEditTools.selectors'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; import getDrawBoundingBoxInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawBoundingBoxInteraction'; @@ -36,6 +37,7 @@ import { import { Extent } from 'ol/extent'; import { mapEditToolsSetActiveAction, + mapEditToolsSetLayerLine, mapEditToolsSetLayerObject, } from '@/redux/mapEditTools/mapEditTools.slice'; import getTransformInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction'; @@ -43,6 +45,9 @@ import { useWebSocketEntityUpdatesContext } from '@/utils/websocket-entity-updat import processMessage from '@/components/Map/MapViewer/utils/websocket/processMessage'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; import getDrawOvalInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawOvalInteraction'; +import getDrawLineInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawLineInteraction'; +import getModifyLineInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getModifyLineInteraction'; +import { LAYER_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewer.constants'; export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { const activeAction = useAppSelector(mapEditToolsActiveActionSelector); @@ -57,7 +62,8 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { const prevActiveLayersRef = useRef<number[]>([]); const drawLayer = useAppSelector(layersDrawLayerSelector); const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); - const mapEditToolsLayerObject = useAppSelector(mapEditToolsLayerObjectSelector); + const mapEditToolsLayerNonLineObject = useAppSelector(mapEditToolsLayerNonLineObjectSelector); + const mapEditToolsLayerLine = useAppSelector(mapEditToolsLayerLineSelector); const lineTypes = useSelector(lineTypesSelector); const arrowTypes = useSelector(arrowTypesSelector); @@ -70,8 +76,10 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { >(), ); const featuresToTransformRef = useRef<Collection<Feature<Geometry>>>(new Collection()); + const linesFeaturesToSelectRef = useRef<Collection<Feature<Geometry>>>(new Collection()); + const modifyFeaturesRef = useRef<Collection<Feature<Geometry>>>(new Collection()); - const getLayerFeatures = (vectorLayer: VectorLayer): Feature<Geometry>[] => { + const getLayerFeaturesToTransform = (vectorLayer: VectorLayer): Feature<Geometry>[] => { const features: Array<Feature<Geometry>> = []; features.push(...vectorLayer.get('imagesFeatures')); features.push(...vectorLayer.get('rectsFeatures')); @@ -79,6 +87,11 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { features.push(...vectorLayer.get('ovalsFeatures')); return features; }; + const getLayerFeaturesToModify = (vectorLayer: VectorLayer): Feature<Geometry>[] => { + const features: Array<Feature<Geometry>> = []; + features.push(...vectorLayer.get('linesFeatures')); + return features; + }; const { lastJsonMessage } = useWebSocketEntityUpdatesContext(); @@ -132,6 +145,10 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { return getDrawOvalInteraction(mapSize, dispatch, restrictionExtent); }, [mapSize, dispatch, restrictionExtent]); + const drawLineInteraction = useMemo(() => { + return getDrawLineInteraction({ mapSize, dispatch, restrictionExtent }); + }, [mapSize, dispatch, restrictionExtent]); + const drawRectInteraction = useMemo(() => { if (!mapSize || !dispatch) { return null; @@ -174,8 +191,10 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { }); const { vectorLayer } = additionalLayer; if (!layerState.details.locked) { - const features = getLayerFeatures(vectorLayer); + const features = getLayerFeaturesToTransform(vectorLayer); featuresToTransformRef.current.extend(features); + const modifyFeatures = getLayerFeaturesToModify(vectorLayer); + linesFeaturesToSelectRef.current.extend(modifyFeatures); } vectorLayersRef.current.set(layerId, vectorLayer); mapInstance?.addLayer(vectorLayer); @@ -207,6 +226,20 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { transformRef.current = transformInteraction; }, [transformInteraction]); + const modifyLineInteraction = useMemo(() => { + if (!currentModelId) { + return null; + } + return getModifyLineInteraction({ + mapSize, + dispatch, + modelId: currentModelId, + featuresToSelectCollection: linesFeaturesToSelectRef.current, + modifyFeatures: modifyFeaturesRef.current, + restrictionExtent, + }); + }, [currentModelId, mapSize, dispatch, restrictionExtent]); + useEffect(() => { vectorLayersRef.current.forEach(layer => { const layerId = layer.get('id'); @@ -226,18 +259,34 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { } }, [layersVisibilityForCurrentModel, transformInteraction]); - // Selecting feature using the layers panel + useEffect(() => { + if (!modifyLineInteraction) { + return; + } + const { select } = modifyLineInteraction; + const selectedFeature = select.getFeatures().item(0); + if (!selectedFeature) { + return; + } + if (!layersVisibilityForCurrentModel[selectedFeature.get('layer')]) { + modifyFeaturesRef.current.clear(); + select.getFeatures().clear(); + dispatch(mapEditToolsSetLayerLine(null)); + } + }, [dispatch, layersVisibilityForCurrentModel, modifyLineInteraction]); + + // Selecting non line feature using the layers panel useEffect(() => { if (!transformRef.current) { return; } const transformFeatures = transformRef.current.getFeatures(); if ( - mapEditToolsLayerObject && + mapEditToolsLayerNonLineObject && (!transformFeatures.getLength() || - transformFeatures.item(0).getId() !== mapEditToolsLayerObject.id) + transformFeatures.item(0).getId() !== mapEditToolsLayerNonLineObject.id) ) { - const layer = vectorLayersRef.current.get(mapEditToolsLayerObject.layer); + const layer = vectorLayersRef.current.get(mapEditToolsLayerNonLineObject.layer); if (!layer) { return; } @@ -245,13 +294,42 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { if (!source) { return; } - const feature = source.getFeatureById(mapEditToolsLayerObject.id); + const feature = source.getFeatureById(mapEditToolsLayerNonLineObject.id); if (!feature) { return; } + modifyFeaturesRef.current.clear(); + modifyLineInteraction?.select.getFeatures().clear(); transformRef.current.setSelection(new Collection<Feature>([feature])); } - }, [mapEditToolsLayerObject]); + }, [mapEditToolsLayerNonLineObject, modifyLineInteraction]); + + // Selecting line feature using the layers panel + useEffect(() => { + const modifyFeatures = modifyFeaturesRef.current; + if ( + mapEditToolsLayerLine && + (!modifyFeatures.getLength() || modifyFeatures.item(0).getId() !== mapEditToolsLayerLine.id) + ) { + const layer = vectorLayersRef.current.get(mapEditToolsLayerLine.layer); + if (!layer) { + return; + } + const source = layer.getSource(); + if (!source) { + return; + } + const feature = source.getFeatureById(mapEditToolsLayerLine.id); + if (!feature) { + return; + } + transformRef.current?.setSelection(new Collection<Feature>()); + modifyFeatures.clear(); + modifyFeatures.push(feature); + modifyLineInteraction?.select.getFeatures().clear(); + modifyLineInteraction?.select.getFeatures().push(feature); + } + }, [mapEditToolsLayerLine, modifyLineInteraction]); useEffect(() => { const activeVectorLayers = [...vectorLayersRef.current.values()].filter(layer => { @@ -263,10 +341,26 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { } const removeFeatureHandler = (): void => { transformInteraction?.setSelection(new Collection<Feature>()); + modifyFeaturesRef.current.clear(); + modifyLineInteraction?.select.getFeatures().clear(); + dispatch(mapEditToolsSetLayerLine(null)); }; const addFeatureHandler = (event: VectorSourceEvent): void => { const newFeature = event.feature; - if (newFeature) { + if (!newFeature) { + return; + } + const elementType = newFeature.get('elementType'); + if (elementType === LAYER_ELEMENT_TYPES.LINE) { + linesFeaturesToSelectRef.current.push(newFeature); + } else if ( + [ + LAYER_ELEMENT_TYPES.RECT, + LAYER_ELEMENT_TYPES.TEXT, + LAYER_ELEMENT_TYPES.OVAL, + LAYER_ELEMENT_TYPES.IMAGE, + ].includes(elementType) + ) { featuresToTransformRef.current.push(newFeature); } }; @@ -284,7 +378,7 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { source?.un('addfeature', addFeatureHandler); }); }; - }, [activeLayers, transformInteraction]); + }, [activeLayers, transformInteraction, modifyLineInteraction, dispatch]); // update transform features after change active layers (lock) useEffect(() => { @@ -295,7 +389,7 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { removedLayers.forEach(layer => { const removedLayer = vectorLayersRef.current.get(layer); if (removedLayer) { - const features = getLayerFeatures(removedLayer); + const features = getLayerFeaturesToTransform(removedLayer); features.forEach((feature: Feature) => { featuresToTransformRef.current.remove(feature); }); @@ -306,7 +400,7 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { addedLayers.forEach(layer => { const addedLayer = vectorLayersRef.current.get(layer); if (addedLayer) { - const features = getLayerFeatures(addedLayer); + const features = getLayerFeaturesToTransform(addedLayer); featuresToTransformRef.current.extend(features); } }); @@ -337,6 +431,34 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { transformInteraction, ]); + useEffect(() => { + if ( + !modifyLineInteraction || + !activeLayers.length || + !hasPrivilegeToWriteProject || + activeAction + ) { + return () => {}; + } + const { modify, snap, select } = modifyLineInteraction; + mapInstance?.addInteraction(modify); + mapInstance?.addInteraction(snap); + mapInstance?.addInteraction(select); + return () => { + dispatch(mapEditToolsSetLayerObject(null)); + mapInstance?.removeInteraction(modify); + mapInstance?.removeInteraction(snap); + mapInstance?.removeInteraction(select); + }; + }, [ + activeAction, + activeLayers, + dispatch, + hasPrivilegeToWriteProject, + mapInstance, + modifyLineInteraction, + ]); + useEffect(() => { if (!drawImageInteraction || !hasPrivilegeToWriteProject) { return; @@ -418,4 +540,22 @@ export const useOlMapAdditionalLayers = (mapInstance: MapInstance): void => { mapInstance, hasPrivilegeToWriteProject, ]); + + useEffect(() => { + if (!drawLineInteraction || !hasPrivilegeToWriteProject) { + return; + } + mapInstance?.removeInteraction(drawLineInteraction); + if (!drawLayer || activeAction !== MAP_EDIT_ACTIONS.ADD_LINE) { + return; + } + mapInstance?.addInteraction(drawLineInteraction); + }, [ + activeAction, + drawLayer, + currentModelId, + drawLineInteraction, + mapInstance, + hasPrivilegeToWriteProject, + ]); }; diff --git a/src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts b/src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts index db8737b1a2cb77c716a60f8cb3a2754949ef843b..b0882e7fc5602b988fdc451fefabd185b2c6e9ff 100644 --- a/src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/onPointerMove.test.ts @@ -18,6 +18,10 @@ const MAP_INSTANCE_BASE_MOCK = { forEachFeatureAtPixel: (): void => {}, }; +const showPointerMoveOnLine = (): boolean => { + return false; +}; + describe('onPointerMove - util', () => { describe('when event dragging', () => { const target = document.createElement('div'); @@ -27,7 +31,9 @@ describe('onPointerMove - util', () => { } as unknown as Map; it('should return nothing and not modify target', () => { - expect(onPointerMove(mapInstance, EVENT_DRAGGING_MOCK)).toBe(undefined); + expect(onPointerMove(mapInstance, EVENT_DRAGGING_MOCK, showPointerMoveOnLine)).toBe( + undefined, + ); expect((mapInstance as any).getTarget().style.cursor).toBe(''); }); }); @@ -41,7 +47,7 @@ describe('onPointerMove - util', () => { } as unknown as Map; it('should return nothing and modify target', () => { - expect(onPointerMove(mapInstance, EVENT_MOCK)).toBe(undefined); + expect(onPointerMove(mapInstance, EVENT_MOCK, showPointerMoveOnLine)).toBe(undefined); expect((mapInstance as any).getTarget().style.cursor).toBe('pointer'); }); }); @@ -54,7 +60,7 @@ describe('onPointerMove - util', () => { } as unknown as Map; it('should return nothing and not modify target', () => { - expect(onPointerMove(mapInstance, EVENT_MOCK)).toBe(undefined); + expect(onPointerMove(mapInstance, EVENT_MOCK, showPointerMoveOnLine)).toBe(undefined); expect((mapInstance as any).getTarget()).toBe(TARGET_STRING); }); }); @@ -68,7 +74,7 @@ describe('onPointerMove - util', () => { } as unknown as Map; it('should return nothing and not modify target', () => { - expect(onPointerMove(mapInstance, EVENT_MOCK)).toBe(undefined); + expect(onPointerMove(mapInstance, EVENT_MOCK, showPointerMoveOnLine)).toBe(undefined); expect((mapInstance as any).getTarget().style.cursor).toBe(''); }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts b/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts index 868c3f3359df5e457e793f4ae6e676d6adf16442..bdaca7f2055da1d68a8871954dbd28a5285854bc 100644 --- a/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts +++ b/src/components/Map/MapViewer/utils/listeners/onPointerMove.ts @@ -1,13 +1,14 @@ import { PIN_ICON_ANY } from '@/constants/features'; import { Map } from 'ol'; import MapBrowserEvent from 'ol/MapBrowserEvent'; +import { LAYER_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewer.constants'; const isTargetHTMLElement = (target: string | HTMLElement | undefined): target is HTMLElement => !!target && typeof target !== 'string' && 'style' in target; /* prettier-ignore */ export const onPointerMove = - (mapInstance: Map, event: MapBrowserEvent<PointerEvent>): void => { + (mapInstance: Map, event: MapBrowserEvent<PointerEvent>, showPointerMoveOnLine: (layerId: number) => boolean): void => { if (event.dragging) { return; } @@ -15,12 +16,16 @@ export const onPointerMove = const pixel = mapInstance.getEventPixel(event.originalEvent); const feature = mapInstance.forEachFeatureAtPixel(pixel, firstFeature => { const isPinIcon = PIN_ICON_ANY.includes(firstFeature.get('type')); - if (!isPinIcon) { - return undefined; + const isLayerLine = LAYER_ELEMENT_TYPES.LINE=== firstFeature.get('elementType'); + if (isPinIcon) { + return firstFeature; } - return firstFeature; - }); + if(isLayerLine && showPointerMoveOnLine(firstFeature.get('layer'))) { + return firstFeature; + } + return undefined; + }, {hitTolerance: 3}); const target = mapInstance.getTarget(); if (isTargetHTMLElement(target)) { diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts index d231ca5b1c74e872a5a7f67cf976b1b2d367cfea..a4f17310c74258efe744d1e077b63f2efa55ed3d 100644 --- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts +++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts @@ -21,6 +21,11 @@ import { onPointerMove } from '@/components/Map/MapViewer/utils/listeners/onPoin import { View } from 'ol'; import { isMapEditToolsActiveSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { + layersActiveLayersSelector, + layersVisibilityForCurrentModelSelector, +} from '@/redux/layers/layers.selectors'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; interface UseOlMapListenersInput { view: View; @@ -34,11 +39,24 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) const modelElementsForCurrentModel = useSelector(modelElementsForCurrentModelSelector); const newReactionsForCurrentModel = useSelector(newReactionsForCurrentModelSelector); const isMapEditToolsActive = useSelector(isMapEditToolsActiveSelector); + const activeLayers = useSelector(layersActiveLayersSelector); + const layersVisibilityForCurrentModel = useSelector(layersVisibilityForCurrentModelSelector); + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); const openedDrawer = useAppSelector(openedDrawerSelector); const isLayersDrawerOpen = useMemo(() => { return openedDrawer === 'layers'; }, [openedDrawer]); + const showPointerMoveOnLine = useMemo(() => { + return (layerId: number): boolean => { + return ( + hasPrivilegeToWriteProject && + activeLayers.includes(layerId) && + layersVisibilityForCurrentModel[layerId] + ); + }; + }, [activeLayers, hasPrivilegeToWriteProject, layersVisibilityForCurrentModel]); + const dispatch = useAppDispatch(); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); @@ -91,11 +109,13 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput) return; } - const key = mapInstance.on('pointermove', event => onPointerMove(mapInstance, event)); + const key = mapInstance.on('pointermove', event => + onPointerMove(mapInstance, event, showPointerMoveOnLine), + ); // eslint-disable-next-line consistent-return return () => unByKey(key); - }, [mapInstance]); + }, [mapInstance, showPointerMoveOnLine]); useEffect(() => { if (!mapInstance || isMapEditToolsActive || isLayersDrawerOpen) { diff --git a/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts index 6a7691b280d423b7af4c7e7916d71fd0d31da7f6..f67101decaafc394965ed2124770f03ae56f1dee 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/BaseMultiPolygon.ts @@ -213,7 +213,7 @@ export default abstract class BaseMultiPolygon { text: this.text, fontSize: this.fontSize, color: rgbToHex(this.fontColor), - zIndex: this.zIndex, + zIndex: this.overlaysVisible ? this.zIndex + 1 : this.zIndex, horizontalAlign: this.nameHorizontalAlign, }); textStyle.setGeometry(textPolygon); diff --git a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts index c2365a6c59d75412e6a31dfc46aa583578fdaed2..c8c2b071a800efa8ec4638a4793df12afa95c897 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/CompartmentPathway.ts @@ -5,7 +5,6 @@ import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapVi import { BLACK_COLOR, MAP_ELEMENT_TYPES, - TRANSPARENT_COLOR, WHITE_COLOR, } from '@/components/Map/MapViewer/MapViewer.constants'; import Polygon from 'ol/geom/Polygon'; @@ -176,7 +175,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { getStyle({ geometry: compartmentPolygon, borderColor: this.borderColor, - fillColor: this.overlaysVisible ? TRANSPARENT_COLOR : { ...this.fillColor, alpha: 9 }, + fillColor: this.overlaysVisible ? WHITE_COLOR : { ...this.fillColor, alpha: 9 }, lineWidth: this.outerWidth, zIndex: this.zIndex, }), diff --git a/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts b/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts index 0de94d27ba3aa9c63eab9a97df2fda6ffd2a3b07..33ccf1c171ae700b9b2b01bf69e1f2dea6aa1e4d 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/MapElement.ts @@ -432,7 +432,7 @@ export default class MapElement extends BaseMultiPolygon { const elementStyle = getStyle({ geometry: elementPolygon, borderColor: this.borderColor, - fillColor: this.overlaysVisible ? TRANSPARENT_COLOR : this.fillColor, + fillColor: this.overlaysVisible ? WHITE_COLOR : this.fillColor, lineWidth: this.lineWidth, lineDash: this.lineDash, zIndex: this.zIndex, @@ -482,7 +482,7 @@ export default class MapElement extends BaseMultiPolygon { geometry: polygon, borderColor: color, fillColor: color, - zIndex: this.zIndex, + zIndex: this.zIndex + 1, }); polygon.set( 'strokeStyle', diff --git a/src/components/Map/MapViewer/utils/shapes/elements/getArrowFeature.ts b/src/components/Map/MapViewer/utils/shapes/elements/getArrowFeature.ts index 0ccc40a259d2f788024db99b370e3fccbb0447e0..5bc1afe6eb274f2351f18240c197e6dba85eef23 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/getArrowFeature.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/getArrowFeature.ts @@ -5,12 +5,13 @@ import { MultiPolygon } from 'ol/geom'; import { Arrow, Color } from '@/types/models'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import Polygon from 'ol/geom/Polygon'; -import { WHITE_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; +import { LAYER_ELEMENT_TYPES, WHITE_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; import { ArrowTypeDict } from '@/redux/shapes/shapes.types'; import getShapePolygon from '@/components/Map/MapViewer/utils/shapes/elements/getShapePolygon'; import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; +import { Stroke } from 'ol/style'; export default function getArrowFeature({ arrowTypes, @@ -32,12 +33,14 @@ export default function getArrowFeature({ lineWidth: number; color: Color; pointToProjection: UsePointToProjectionResult; -}): undefined | Feature<MultiPolygon> { +}): + | undefined + | { feature: Feature<MultiPolygon>; styles: Array<{ style: Style; strokeStyle: Stroke }> } { const arrowShapes = arrowTypes[arrow.arrowType]; if (!arrowShapes) { return undefined; } - const arrowStyles: Array<Style> = []; + const styles: Array<{ style: Style; strokeStyle: Stroke }> = []; const arrowPolygons: Array<Polygon> = []; arrowShapes.forEach(shape => { const arrowPolygon = getShapePolygon({ @@ -55,22 +58,22 @@ export default function getArrowFeature({ fillColor: shape.fill === false ? WHITE_COLOR : color, lineWidth, }); - arrowPolygon.set( - 'strokeStyle', - getStroke({ - color: rgbToHex(color), - width: lineWidth, - }), - ); + const strokeStyle = getStroke({ + color: rgbToHex(color), + width: lineWidth, + }); arrowPolygon.rotate(rotation, pointToProjection({ x, y })); - arrowStyles.push(style); + styles.push({ + style, + strokeStyle, + }); arrowPolygons.push(arrowPolygon); }); + const arrowFeature = new Feature({ geometry: new MultiPolygon(arrowPolygons), - style: arrowStyles, + elementType: LAYER_ELEMENT_TYPES.ARROW, zIndex, }); - arrowFeature.setStyle(arrowStyles); - return arrowFeature; + return { feature: arrowFeature, styles }; } diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts index ac3a96791e3e7b7d220dec4035bca46cc87ceab5..16d29e66a2910aab2020e82db93b2413694ab66e 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts @@ -83,9 +83,9 @@ describe('Layer', () => { borderColor: BLACK_COLOR, }, }, - lines: [ - { - id: 120899, + lines: { + 1: { + id: 1, width: 5.0, color: { alpha: 255, @@ -113,8 +113,9 @@ describe('Layer', () => { length: 15.0, }, lineType: 'SOLID', + layer: 1, }, - ], + }, images: { 1: { id: 1, diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts index 77075f320769409ee5dd79f52aff2dcfe6556d3e..3ce823db113da7a3fae7f96b0ace94f4f69f5cc9 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts @@ -1,7 +1,7 @@ /* eslint-disable no-magic-numbers */ import { LayerImage as LayerImageModel, - LayerLine, + LayerLine as LayerLineModel, LayerOval as LayerOvalModel, LayerRect as LayerRectModel, LayerText as LayerTextModel, @@ -14,31 +14,21 @@ import Polygon from 'ol/geom/Polygon'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types'; -import { FeatureLike } from 'ol/Feature'; -import Style from 'ol/style/Style'; import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; -import { - LAYER_ELEMENT_TYPES, - LAYER_TYPE, - REACTION_ELEMENT_CUTOFF_SCALE, -} from '@/components/Map/MapViewer/MapViewer.constants'; -import { Stroke } from 'ol/style'; +import { LAYER_TYPE } from '@/components/Map/MapViewer/MapViewer.constants'; import { MapSize } from '@/redux/map/map.types'; -import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; -import getArrowFeature from '@/components/Map/MapViewer/utils/shapes/elements/getArrowFeature'; -import getRotation from '@/components/Map/MapViewer/utils/shapes/coords/getRotation'; -import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import LayerText from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerText'; import LayerImage from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage'; import LayerRect from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerRect'; import LayerOval from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerOval'; +import LayerLine from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerLine'; export interface LayerProps { zIndex: number; texts: { [key: string]: LayerTextModel }; rects: { [key: string]: LayerRectModel }; ovals: { [key: string]: LayerOvalModel }; - lines: Array<LayerLine>; + lines: { [key: string]: LayerLineModel }; images: { [key: string]: LayerImageModel }; visible: boolean; layerId: number; @@ -58,7 +48,7 @@ export default class Layer { ovals: { [key: string]: LayerOvalModel }; - lines: Array<LayerLine>; + lines: { [key: string]: LayerLineModel }; images: { [key: string]: LayerImageModel }; @@ -94,6 +84,12 @@ export default class Layer { pointToProjection, }: LayerProps) { this.vectorSource = new VectorSource({}); + this.vectorLayer = new VectorLayer({ + zIndex, + visible, + updateWhileAnimating: true, + updateWhileInteracting: true, + }); this.texts = texts; this.rects = rects; @@ -123,22 +119,17 @@ export default class Layer { this.vectorSource.addFeatures(linesFeatures); this.vectorSource.addFeatures(arrowsFeatures); - this.vectorLayer = new VectorLayer({ - zIndex, - source: this.vectorSource, - visible, - updateWhileAnimating: true, - updateWhileInteracting: true, - }); + this.vectorLayer.setSource(this.vectorSource); this.vectorLayer.set('type', LAYER_TYPE.ADDITIONAL_LAYER); - this.vectorLayer.set('id', layerId); this.vectorLayer.set('imagesFeatures', imagesFeatures); this.vectorLayer.set('textsFeatures', textsFeatures); this.vectorLayer.set('rectsFeatures', rectsFeatures); this.vectorLayer.set('ovalsFeatures', ovalsFeatures); + this.vectorLayer.set('linesFeatures', linesFeatures); this.vectorLayer.set('drawImage', this.drawImage.bind(this)); this.vectorLayer.set('drawText', this.drawText.bind(this)); + this.vectorLayer.set('drawLine', this.drawLine.bind(this)); this.vectorLayer.set('drawOval', this.drawOval.bind(this)); this.vectorLayer.set('drawRect', this.drawRect.bind(this)); } @@ -244,100 +235,53 @@ export default class Layer { 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 [ - this.pointToProjection({ x: segment.x1, y: segment.y1 }), - this.pointToProjection({ x: segment.x2, y: segment.y2 }), - ]; - } - return [this.pointToProjection({ x: segment.x2, y: segment.y2 })]; - }) - .flat(); - - if (line.startArrow.arrowType !== 'NONE') { - const firstSegment = line.segments[0]; - const startArrowRotation = getRotation( - [firstSegment.x1, firstSegment.y1], - [firstSegment.x2, firstSegment.y2], - ); - const shortenedX1 = firstSegment.x1 + line.startArrow.length * Math.cos(startArrowRotation); - const shortenedY1 = firstSegment.y1 - line.startArrow.length * Math.sin(startArrowRotation); - points[0] = this.pointToProjection({ x: shortenedX1, y: shortenedY1 }); - - const startArrowFeature = getArrowFeature({ - arrowTypes: this.arrowTypes, - arrow: line.startArrow, - x: shortenedX1, - y: shortenedY1, - zIndex: line.z, - rotation: startArrowRotation, - lineWidth: line.width, - color: line.color, - pointToProjection: this.pointToProjection, - }); - if (startArrowFeature) { - startArrowFeature.set('elementType', LAYER_ELEMENT_TYPES.ARROW); - startArrowFeature.set('lineWidth', line.width); - startArrowFeature.setStyle(this.getStyle.bind(this)); - arrowsFeatures.push(startArrowFeature); - } - } - - if (line.endArrow.arrowType !== 'NONE') { - const lastSegment = line.segments[line.segments.length - 1]; - const endArrowRotation = getRotation( - [lastSegment.x1, lastSegment.y1], - [lastSegment.x2, lastSegment.y2], - ); - const shortenedX2 = lastSegment.x2 - line.endArrow.length * Math.cos(endArrowRotation); - const shortenedY2 = lastSegment.y2 - line.endArrow.length * Math.sin(endArrowRotation); - points[points.length - 1] = this.pointToProjection({ x: shortenedX2, y: shortenedY2 }); - - const endArrowFeature = getArrowFeature({ - arrowTypes: this.arrowTypes, - arrow: line.endArrow, - x: shortenedX2, - y: shortenedY2, - zIndex: line.z, - rotation: endArrowRotation, - lineWidth: line.width, - color: line.color, - pointToProjection: this.pointToProjection, - }); - if (endArrowFeature) { - endArrowFeature.set('elementType', LAYER_ELEMENT_TYPES.ARROW); - endArrowFeature.setStyle(this.getStyle.bind(this)); - arrowsFeatures.push(endArrowFeature); - } - } - - const lineString = new LineString(points); - - const lineDash = this.lineTypes[line.lineType] || []; - const lineStyle = getStyle({ - geometry: lineString, - borderColor: line.color, - lineWidth: line.width, - lineDash, - zIndex: line.z, - }); - const lineFeature = new Feature<LineString>({ - geometry: lineString, - style: lineStyle, - lineWidth: line.width, - elementType: LAYER_ELEMENT_TYPES.LINE, - }); - lineFeature.setStyle(this.getStyle.bind(this)); + const linesArrowsFeatures: Array<Feature<MultiPolygon>> = []; + Object.values(this.lines).forEach(line => { + const { lineFeature, arrowsFeatures } = this.getLineFeature(line); linesFeatures.push(lineFeature); + linesArrowsFeatures.push(...arrowsFeatures); + }); + return { + linesFeatures, + arrowsFeatures: linesArrowsFeatures, + }; + }; + + private getLineFeature = ( + line: LayerLineModel, + ): { + lineFeature: Feature<LineString>; + arrowsFeatures: Array<Feature<MultiPolygon>>; + } => { + const lineObject = new LayerLine({ + layerLine: line, + layer: this.layerId, + lineTypes: this.lineTypes, + arrowTypes: this.arrowTypes, + pointToProjection: this.pointToProjection, + vectorSource: this.vectorSource, + mapInstance: this.mapInstance, + mapSize: this.mapSize, }); - return { linesFeatures, arrowsFeatures }; + const arrowsFeatures: Array<Feature<MultiPolygon>> = []; + if (lineObject.startArrowFeature) { + arrowsFeatures.push(lineObject.startArrowFeature); + } + if (lineObject.endArrowFeature) { + arrowsFeatures.push(lineObject.endArrowFeature); + } + return { + lineFeature: lineObject.lineFeature, + arrowsFeatures, + }; }; + private drawLine(line: LayerLineModel): void { + const { lineFeature, arrowsFeatures } = this.getLineFeature(line); + this.vectorSource.addFeature(lineFeature); + this.vectorSource.addFeatures(arrowsFeatures); + } + private getImagesFeatures(): Feature<Polygon>[] { return Object.values(this.images).map(image => { return this.getGlyphFeature(image); @@ -369,38 +313,4 @@ export default class Layer { }); return glyph.feature; } - - protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { - const styles: Array<Style> = []; - const maxZoom = this.mapInstance?.getView().get('originalMaxZoom'); - const minResolution = this.mapInstance?.getView().getResolutionForZoom(maxZoom); - const style = feature.get('style'); - if (!minResolution || !style) { - return []; - } - - const scale = minResolution / resolution; - let strokeStyle: Stroke | undefined; - const type = feature.get('elementType'); - - if (type === LAYER_ELEMENT_TYPES.ARROW && scale <= REACTION_ELEMENT_CUTOFF_SCALE) { - return []; - } - - const stylesToProcess: Array<Style> = []; - if (style instanceof Style) { - stylesToProcess.push(style); - } else if (Array.isArray(style)) { - stylesToProcess.push(...style); - } - stylesToProcess.forEach(singleStyle => { - const styleGeometry = singleStyle.getGeometry(); - if (styleGeometry instanceof Polygon || styleGeometry instanceof LineString) { - strokeStyle = styleGeometry.get('strokeStyle'); - } - styles.push(getScaledElementStyle(singleStyle, strokeStyle, scale)); - }); - - return styles; - } } diff --git a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts index b7d306bb6f8f1456d337080ab1b9a3f4a5db62bc..b7656701f72f80ed954d56a25bba16a7585aa714 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts @@ -8,6 +8,7 @@ import { updateLayerImageObject } from '@/redux/layers/layers.thunks'; import { layerUpdateImage } from '@/redux/layers/layers.slice'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; import { BoundingBox } from '@/components/Map/MapViewer/MapViewer.types'; +import { LAYER_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewer.constants'; export type LayerImageProps = { elementId: number; @@ -55,6 +56,7 @@ export default class LayerImage extends Glyph { this.feature.set('getObjectData', this.getData.bind(this)); this.feature.set('save', this.save.bind(this)); this.feature.set('layer', layer); + this.feature.set('elementType', LAYER_ELEMENT_TYPES.IMAGE); } private async save({ diff --git a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerLine.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerLine.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7278a9415e63b1fe48df6b7a43b795e84f52239 --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerLine.ts @@ -0,0 +1,358 @@ +/* eslint-disable no-magic-numbers */ +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import Style from 'ol/style/Style'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import { MapInstance } from '@/types/map'; +import { Arrow, Color, LayerLine as LayerLineModel, Segment } from '@/types/models'; +import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; +import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; +import { Stroke } from 'ol/style'; +import { MapSize } from '@/redux/map/map.types'; +import { Coordinate } from 'ol/coordinate'; +import { LAYER_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewer.constants'; +import { LineString, MultiPolygon } from 'ol/geom'; +import getRotation from '@/components/Map/MapViewer/utils/shapes/coords/getRotation'; +import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; +import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; +import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; +import getArrowFeature from '@/components/Map/MapViewer/utils/shapes/elements/getArrowFeature'; +import { updateLayerLine } from '@/redux/layers/layers.thunks'; +import { store } from '@/redux/store'; +import { mapEditToolsSetLayerLine } from '@/redux/mapEditTools/mapEditTools.slice'; +import { layerUpdateLine } from '@/redux/layers/layers.slice'; +import VectorSource from 'ol/source/Vector'; +import Polygon from 'ol/geom/Polygon'; + +export interface LayerLineProps { + layerLine: LayerLineModel; + layer: number; + lineTypes: LineTypeDict; + arrowTypes: ArrowTypeDict; + pointToProjection: UsePointToProjectionResult; + vectorSource: VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>; + mapInstance: MapInstance; + mapSize: MapSize; +} + +export default class LayerLine { + elementId: number; + + segments: Array<Segment>; + + startArrow: Arrow; + + endArrow: Arrow; + + lineType: string; + + layer: number; + + zIndex: number; + + color: Color; + + lineWidth: number; + + lineTypes: LineTypeDict; + + arrowTypes: ArrowTypeDict; + + points: Coordinate[] = []; + + styles: Array<{ style: Style; strokeStyle: Stroke }> = []; + + strokeStyle: Stroke = new Stroke(); + + lineString: LineString = new LineString([]); + + lineFeature: Feature<LineString> = new Feature(); + + startArrowFeature: Feature<MultiPolygon> | undefined; + + endArrowFeature: Feature<MultiPolygon> | undefined; + + vectorSource: VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>; + + mapSize: MapSize; + + pointToProjection: UsePointToProjectionResult; + + minResolution: number; + + constructor({ + layerLine, + layer, + lineTypes, + arrowTypes, + pointToProjection, + vectorSource, + mapInstance, + mapSize, + }: LayerLineProps) { + this.elementId = layerLine.id; + this.segments = layerLine.segments; + this.startArrow = layerLine.startArrow; + this.endArrow = layerLine.endArrow; + this.lineType = layerLine.lineType; + this.layer = layer; + this.zIndex = layerLine.z; + this.color = layerLine.color; + this.lineWidth = layerLine.width; + this.lineTypes = lineTypes; + this.arrowTypes = arrowTypes; + this.vectorSource = vectorSource; + this.pointToProjection = pointToProjection; + this.mapSize = mapSize; + + const maxZoom = mapInstance?.getView().get('originalMaxZoom'); + this.minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom) || 1; + + this.drawLayerLine(); + + this.lineFeature = new Feature<LineString>({ + geometry: this.lineString, + elementType: LAYER_ELEMENT_TYPES.LINE, + layer: this.layer, + }); + + this.lineFeature.setId(this.elementId); + this.lineFeature.set('getObjectData', this.getData.bind(this)); + this.lineFeature.set('setCoordinates', this.setCoordinates.bind(this)); + this.lineFeature.set('updateElement', this.updateElement.bind(this)); + this.lineFeature.set('drawLayerLine', this.drawLayerLine.bind(this)); + this.lineFeature.set('save', this.save.bind(this)); + this.lineFeature.setStyle(this.getStyle.bind(this)); + } + + private getData(): LayerLineModel { + return { + id: this.elementId, + startArrow: this.startArrow, + endArrow: this.endArrow, + z: this.zIndex, + width: this.lineWidth, + lineType: this.lineType, + color: this.color, + segments: this.segments, + layer: this.layer, + }; + } + + private setLinePoints(): void { + this.points = this.segments + .map((segment, index) => { + if (index === 0) { + return [ + this.pointToProjection({ x: segment.x1, y: segment.y1 }), + this.pointToProjection({ x: segment.x2, y: segment.y2 }), + ]; + } + return [this.pointToProjection({ x: segment.x2, y: segment.y2 })]; + }) + .flat(); + } + + private drawStartArrow(): void { + if (this.startArrow.arrowType !== 'NONE') { + const firstSegment = this.segments[0]; + const startArrowRotation = getRotation( + [firstSegment.x2, firstSegment.y2], + [firstSegment.x1, firstSegment.y1], + ); + const shortenedX1 = firstSegment.x1 - this.startArrow.length * Math.cos(startArrowRotation); + const shortenedY1 = firstSegment.y1 + this.startArrow.length * Math.sin(startArrowRotation); + this.points[0] = this.pointToProjection({ x: shortenedX1, y: shortenedY1 }); + + const arrowFeature = getArrowFeature({ + arrowTypes: this.arrowTypes, + arrow: this.startArrow, + x: shortenedX1, + y: shortenedY1, + zIndex: this.zIndex, + rotation: startArrowRotation, + lineWidth: this.lineWidth, + color: this.color, + pointToProjection: this.pointToProjection, + }); + if (arrowFeature) { + const { feature, styles } = arrowFeature; + feature.setId(`start_arrow_${this.elementId}`); + feature.set('type', LAYER_ELEMENT_TYPES.ARROW); + feature.set('lineWidth', this.lineWidth); + feature.setStyle(this.getStyle.bind(this)); + this.styles.push(...styles); + this.startArrowFeature = feature; + } + } else { + this.startArrowFeature = undefined; + } + } + + private drawEndArrow(): void { + if (this.endArrow.arrowType !== 'NONE') { + const lastSegment = this.segments.at(-1); + if (!lastSegment) { + return; + } + const endArrowRotation = getRotation( + [lastSegment.x1, lastSegment.y1], + [lastSegment.x2, lastSegment.y2], + ); + const shortenedX2 = lastSegment.x2 - this.endArrow.length * Math.cos(endArrowRotation); + const shortenedY2 = lastSegment.y2 + this.endArrow.length * Math.sin(endArrowRotation); + this.points[this.points.length - 1] = this.pointToProjection({ + x: shortenedX2, + y: shortenedY2, + }); + + const arrowFeature = getArrowFeature({ + arrowTypes: this.arrowTypes, + arrow: this.endArrow, + x: shortenedX2, + y: shortenedY2, + zIndex: this.zIndex, + rotation: endArrowRotation, + lineWidth: this.lineWidth, + color: this.color, + pointToProjection: this.pointToProjection, + }); + if (arrowFeature) { + const { feature, styles } = arrowFeature; + feature.setId(`end_arrow_${this.elementId}`); + feature.set('type', LAYER_ELEMENT_TYPES.ARROW); + feature.setStyle(this.getStyle.bind(this)); + this.styles.push(...styles); + this.endArrowFeature = feature; + } + } else { + this.endArrowFeature = undefined; + } + } + + private drawLineString(): void { + this.lineString = new LineString(this.points); + } + + private setLineStyles(): void { + const lineDash = this.lineTypes[this.lineType] || []; + const lineStrokeStyle = getStroke({ + color: rgbToHex(this.color), + width: this.lineWidth, + lineDash, + }); + const lineStyle = getStyle({ + geometry: this.lineString, + borderColor: this.color, + lineWidth: this.lineWidth, + lineDash, + zIndex: this.zIndex, + }); + this.styles.push({ + style: lineStyle, + strokeStyle: lineStrokeStyle, + }); + } + + private drawLayerLine(): void { + this.startArrowFeature = undefined; + this.endArrowFeature = undefined; + this.styles = []; + this.setLinePoints(); + this.drawStartArrow(); + this.drawEndArrow(); + this.drawLineString(); + this.setLineStyles(); + this.lineFeature.setGeometry(this.lineString); + this.lineFeature.changed(); + } + + private updateElement(layerLine: LayerLineModel): void { + this.elementId = layerLine.id; + this.startArrow = layerLine.startArrow; + this.endArrow = layerLine.endArrow; + this.zIndex = layerLine.z; + this.lineWidth = layerLine.width; + this.lineType = layerLine.lineType; + this.color = layerLine.color; + this.segments = layerLine.segments; + + this.drawLayerLine(); + if ( + this.startArrowFeature && + !this.vectorSource.getFeatureById(`start_arrow_${this.elementId}`) + ) { + this.vectorSource.addFeature(this.startArrowFeature); + } + if (this.endArrowFeature && !this.vectorSource.getFeatureById(`end_arrow_${this.elementId}`)) { + this.vectorSource.addFeature(this.endArrowFeature); + } + } + + private async save({ + modelId, + segments, + firstPoint, + lastPoint, + }: { + modelId: number; + segments: Array<Segment>; + firstPoint: Coordinate; + lastPoint: Coordinate; + }): Promise<void> { + const firstSegment = segments.at(0); + const lastSegment = segments.at(-1); + const firstCurrentSegment = this.segments.at(0); + const lastCurrentSegment = this.segments.at(-1); + const firstCurrentPoint = this.points.at(0); + const lastCurrentPoint = this.points.at(-1); + if ( + firstCurrentPoint && + firstSegment && + firstCurrentSegment && + firstPoint[0] === firstCurrentPoint[0] && + firstPoint[1] === firstCurrentPoint[1] + ) { + firstSegment.x1 = firstCurrentSegment.x1; + firstSegment.y1 = firstCurrentSegment.y1; + } + if ( + lastCurrentPoint && + lastSegment && + lastCurrentSegment && + lastPoint[0] === lastCurrentPoint[0] && + lastPoint[1] === lastCurrentPoint[1] + ) { + lastSegment.x2 = lastCurrentSegment.x2; + lastSegment.y2 = lastCurrentSegment.y2; + } + const { dispatch } = store; + const layerLine = await dispatch( + updateLayerLine({ + modelId, + layerId: this.layer, + lineId: this.elementId, + payload: { ...this.getData(), segments }, + }), + ).unwrap(); + if (layerLine) { + dispatch(layerUpdateLine({ modelId, layerId: this.layer, layerLine })); + dispatch(mapEditToolsSetLayerLine(layerLine)); + this.updateElement(layerLine); + } + } + + private setCoordinates(coords: Coordinate[]): void { + const lineString = this.lineFeature.getGeometry(); + if (lineString) { + lineString.setCoordinates(coords); + } + } + + protected getStyle(_: FeatureLike, resolution: number): Style | Array<Style> | void { + const scale = this.minResolution / resolution; + return this.styles.map(({ style, strokeStyle }) => { + return getScaledElementStyle(style, strokeStyle, scale); + }); + } +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerRect.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerRect.ts index 8796afc221851b9a7a8b2065261d00a388f9527c..09bcb573b208cc4d8edb46820722a59b02de8504 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerRect.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerRect.ts @@ -19,6 +19,7 @@ import { updateLayerRect } from '@/redux/layers/layers.thunks'; import { layerUpdateRect } from '@/redux/layers/layers.slice'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; import getBoundingBoxPolygon from '@/components/Map/MapViewer/utils/shapes/elements/getBoundingBoxPolygon'; +import { LAYER_ELEMENT_TYPES } from '@/components/Map/MapViewer/MapViewer.constants'; export interface LayerRectProps { elementId: number; @@ -115,6 +116,7 @@ export default class LayerRect { this.feature = new Feature({ geometry: this.polygon, layer, + elementType: LAYER_ELEMENT_TYPES.RECT, }); this.feature.setId(this.elementId); this.feature.set('getObjectData', this.getData.bind(this)); diff --git a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts index 41662941aa751c9470a216be0987f7db566cd385..d831c7dabb687f5aed711526ccf0f2709b0d2aae 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts @@ -6,7 +6,10 @@ import { Feature } from 'ol'; import { FeatureLike } from 'ol/Feature'; import { MapInstance } from '@/types/map'; import { Color, LayerText as LayerTextModel } from '@/types/models'; -import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewer.constants'; +import { + LAYER_ELEMENT_TYPES, + TEXT_CUTOFF_SCALE, +} from '@/components/Map/MapViewer/MapViewer.constants'; import { BoundingBox, HorizontalAlign, @@ -165,6 +168,7 @@ export default class LayerText { } return 1; }, + elementType: LAYER_ELEMENT_TYPES.TEXT, layer, }); this.feature.setId(this.elementId); diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawLineInteraction.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawLineInteraction.ts new file mode 100644 index 0000000000000000000000000000000000000000..cfc0679d491e7ef96303a61538371acb5b4c2e62 --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawLineInteraction.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-magic-numbers */ +import Draw from 'ol/interaction/Draw'; +import { LineString } from 'ol/geom'; +import { MapSize } from '@/redux/map/map.types'; +import { AppDispatch } from '@/redux/store'; +import { openLayerLineFactoryModal } from '@/redux/modal/modal.slice'; +import SimpleGeometry from 'ol/geom/SimpleGeometry'; +import { Coordinate } from 'ol/coordinate'; +import { Extent } from 'ol/extent'; +import getLineSegmentsFromCoords from '@/components/Map/MapViewer/utils/shapes/layer/utils/getLineSegmentsFromCoords'; +import { never } from 'ol/events/condition'; + +export default function getDrawLineInteraction({ + mapSize, + dispatch, + restrictionExtent, +}: { + mapSize: MapSize; + dispatch: AppDispatch; + restrictionExtent: Extent; +}): Draw { + const drawLineInteraction = new Draw({ + type: 'LineString', + freehand: false, + condition: (mapBrowserEvent): boolean => { + const coords = mapBrowserEvent.coordinate; + return ( + coords[0] >= restrictionExtent[0] && + coords[0] <= restrictionExtent[2] && + coords[1] >= restrictionExtent[1] && + coords[1] <= restrictionExtent[3] + ); + }, + freehandCondition: never, + geometryFunction: (coordinates, geom): SimpleGeometry => { + let geometry = geom; + if (!geom) { + geometry = new LineString([]); + } + if (!coordinates.length) { + geometry.setCoordinates(coordinates); + return geometry; + } + const lastCoordinate = coordinates.at(-1) as Coordinate; + const lastCoordinateX = Math.min( + restrictionExtent[2], + Math.max(restrictionExtent[0], lastCoordinate[0]), + ); + const lastCoordinateY = Math.min( + restrictionExtent[3], + Math.max(restrictionExtent[1], lastCoordinate[1]), + ); + coordinates.splice(coordinates.length - 1, 1, [lastCoordinateX, lastCoordinateY]); + geometry.setCoordinates(coordinates); + return geometry; + }, + }); + + drawLineInteraction.on('drawend', event => { + const coords = (event.feature.getGeometry() as LineString).getCoordinates(); + const segments = getLineSegmentsFromCoords({ mapSize, coords }); + dispatch(openLayerLineFactoryModal(segments)); + }); + + return drawLineInteraction; +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getModifyLineInteraction.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getModifyLineInteraction.ts new file mode 100644 index 0000000000000000000000000000000000000000..70aea4f95719527bd849778e5eeaa3ab48cdc200 --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getModifyLineInteraction.ts @@ -0,0 +1,140 @@ +/* eslint-disable no-magic-numbers */ +import { Select, Snap } from 'ol/interaction'; +import { Collection, Feature } from 'ol'; +import { Geometry, LineString } from 'ol/geom'; +import getLineSegmentsFromCoords from '@/components/Map/MapViewer/utils/shapes/layer/utils/getLineSegmentsFromCoords'; +import { MapSize } from '@/redux/map/map.types'; +import { SelectEvent } from 'ol/interaction/Select'; +import { AppDispatch } from '@/redux/store'; +import { mapEditToolsSetLayerLine } from '@/redux/mapEditTools/mapEditTools.slice'; +import { openDrawer } from '@/redux/drawer/drawer.slice'; +import { Extent } from 'ol/extent'; +import ModifyFeature from 'ol-ext/interaction/ModifyFeature'; +import BaseEvent from 'ol/events/Event'; +import { Coordinate } from 'ol/coordinate'; + +function includesFeature(featuresCollection: Collection<Feature>, searchFeature: Feature): boolean { + const foundFeature = featuresCollection.getArray().find(feature => { + return feature.getId() === searchFeature.getId(); + }); + return Boolean(foundFeature); +} + +export default function getModifyLineInteraction({ + mapSize, + dispatch, + modelId, + featuresToSelectCollection, + modifyFeatures, + restrictionExtent, +}: { + mapSize: MapSize; + modelId: number; + dispatch: AppDispatch; + featuresToSelectCollection: Collection<Feature<Geometry>>; + modifyFeatures: Collection<Feature<Geometry>>; + restrictionExtent: Extent; +}): { modify: ModifyFeature; snap: Snap; select: Select } { + const snap = new Snap({ + features: featuresToSelectCollection, + pixelTolerance: 5, + edge: false, + }); + const modify = new ModifyFeature({ + features: modifyFeatures, + pixelTolerance: 10, + }); + const select = new Select({ + hitTolerance: 3, + filter: (feature): boolean => { + return includesFeature(featuresToSelectCollection, feature); + }, + }); + + select.on('select', (event: SelectEvent) => { + modifyFeatures.clear(); + if (!event.selected.length) { + dispatch(mapEditToolsSetLayerLine(null)); + return; + } + const selected = event.selected[0]; + modifyFeatures.push(selected); + const getObjectData = selected.get('getObjectData'); + if (getObjectData && getObjectData instanceof Function) { + const objectData = getObjectData(); + dispatch(mapEditToolsSetLayerLine(objectData)); + dispatch(openDrawer('layers')); + } + }); + + modify.on('modifying', (event: Event | BaseEvent) => { + const transformEvent = event as unknown as { features: Array<Feature>; coordinate: Coordinate }; + const { features, coordinate } = transformEvent; + if (!features.length) { + return; + } + const feature = features[0]; + const geometry = feature.getGeometry(); + if (!geometry || !(geometry instanceof LineString)) { + return; + } + + const [x, y] = coordinate; + const coords = geometry.getCoordinates(); + let vertexIndex = -1; + coords.forEach((pt: number[], index: number) => { + if (pt[0] === x || pt[1] === y) { + vertexIndex = index; + } + }); + if (vertexIndex === -1) { + return; + } + const [minX, minY, maxX, maxY] = restrictionExtent; + const correctedX = Math.max(minX, Math.min(maxX, x)); + const correctedY = Math.max(minY, Math.min(maxY, y)); + if (correctedX !== x || correctedY !== y) { + coords[vertexIndex] = [correctedX, correctedY]; + const setCoordinates = feature.get('setCoordinates'); + if (setCoordinates instanceof Function) { + setCoordinates(coords); + } + } + }); + + modify.on('modifyend', (event: Event | BaseEvent) => { + const transformEvent = event as unknown as { features: Array<Feature> }; + if (!transformEvent.features.length) { + return; + } + const feature = transformEvent.features[0]; + if (!feature) { + return; + } + const geometry = feature.getGeometry(); + if (!geometry) { + return; + } + const coords = (geometry as LineString).getCoordinates(); + const firstPoint = coords.at(0); + const lastPoint = coords.at(-1); + if (!firstPoint || !lastPoint) { + return; + } + const segments = getLineSegmentsFromCoords({ mapSize, coords }); + + const save = feature.get('save'); + const drawLayerLine = feature.get('drawLayerLine'); + if (save instanceof Function) { + try { + save({ modelId, segments, firstPoint, lastPoint }); + } catch { + if (drawLayerLine instanceof Function) { + drawLayerLine(); + } + } + } + }); + + return { modify, snap, select }; +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer.ts b/src/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer.ts index 8bd57e6404619a9cfbbe717ab729ab05c737300d..7cde56cb0d77a28d1c192230772b14f0d333b87b 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer.ts @@ -1,4 +1,4 @@ -import { LayerImage, LayerOval, LayerRect, LayerText } from '@/types/models'; +import { LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; import { MapInstance } from '@/types/map'; export default function drawElementOnLayer({ @@ -9,7 +9,7 @@ export default function drawElementOnLayer({ }: { mapInstance: MapInstance; activeLayer: number; - object: LayerImage | LayerText | LayerRect | LayerOval; + object: LayerImage | LayerText | LayerRect | LayerOval | LayerLine; drawFunctionKey: string; }): void { mapInstance?.getAllLayers().forEach(layer => { diff --git a/src/components/Map/MapViewer/utils/shapes/layer/utils/getLayerLineBoundingBoxCoords.ts b/src/components/Map/MapViewer/utils/shapes/layer/utils/getLayerLineBoundingBoxCoords.ts new file mode 100644 index 0000000000000000000000000000000000000000..9bffaf638a1e711e457d3c32d5c172dedaa35316 --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/layer/utils/getLayerLineBoundingBoxCoords.ts @@ -0,0 +1,48 @@ +import { Coordinate } from 'ol/coordinate'; +import { Segment } from '@/types/models'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; + +export default function getLayerLineBoundingBoxCoords({ + segments, + pointToProjection, +}: { + segments: Array<Segment>; + pointToProjection: UsePointToProjectionResult; +}): Coordinate[] { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + segments.forEach(segment => { + if (segment.x1 < minX) { + minX = segment.x1; + } + if (segment.x2 < minX) { + minX = segment.x2; + } + if (segment.y1 < minY) { + minY = segment.y1; + } + if (segment.y2 < minY) { + minY = segment.y2; + } + if (segment.x1 > maxX) { + maxX = segment.x1; + } + if (segment.x2 > maxX) { + maxX = segment.x2; + } + if (segment.y1 > maxY) { + maxY = segment.y1; + } + if (segment.y2 > maxY) { + maxY = segment.y2; + } + }); + const point1 = pointToProjection({ x: minX, y: minY }); + const point2 = pointToProjection({ + x: maxX, + y: maxY, + }); + return [point1, point2]; +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/utils/getLineSegmentsFromCoords.ts b/src/components/Map/MapViewer/utils/shapes/layer/utils/getLineSegmentsFromCoords.ts new file mode 100644 index 0000000000000000000000000000000000000000..410e376e675d8ca2b4bf0810f2b3f4571a8294dd --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/layer/utils/getLineSegmentsFromCoords.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-magic-numbers */ +import { MapSize } from '@/redux/map/map.types'; +import { Coordinate } from 'ol/coordinate'; +import { Segment } from '@/types/models'; +import { toLonLat } from 'ol/proj'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; + +export default function getLineSegmentsFromCoords({ + mapSize, + coords, +}: { + mapSize: MapSize; + coords: Coordinate[]; +}): Array<Segment> { + const segments: Array<Segment> = []; + let x1: number | undefined; + let y1: number | undefined; + coords.forEach(pointCoords => { + const [startLng, startLat] = toLonLat([pointCoords[0], pointCoords[1]]); + const point = latLngToPoint([startLat, startLng], mapSize); + if (x1 && y1) { + if (x1 === point.x && y1 === point.y) { + return; + } + segments.push({ + x1, + y1, + x2: point.x, + y2: point.y, + }); + } + x1 = point.x; + y1 = point.y; + }); + return segments; +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts b/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts index 0e8e010c5e021b026b563566aeab872279d678b9..855568436aee6e164c1dc192e02c61647686e090 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts @@ -1,11 +1,11 @@ import VectorSource from 'ol/source/Vector'; -import { LayerImage, LayerOval, LayerRect, LayerText } from '@/types/models'; +import { LayerImage, LayerOval, LayerRect, LayerText, LayerLine } from '@/types/models'; import { MapInstance } from '@/types/map'; export default function updateElement( mapInstance: MapInstance, layerId: number, - layerObject: LayerImage | LayerText | LayerRect | LayerOval, + layerObject: LayerImage | LayerText | LayerRect | LayerOval | LayerLine, ): void { mapInstance?.getAllLayers().forEach(layer => { if (layer.get('id') === layerId) { diff --git a/src/components/Map/MapViewer/utils/shapes/reaction/Reaction.ts b/src/components/Map/MapViewer/utils/shapes/reaction/Reaction.ts index e868e8a6bf0479ba05984fa1788deca3717854c0..cffa68c0ab6281b8665ba74c402f19976fb680d3 100644 --- a/src/components/Map/MapViewer/utils/shapes/reaction/Reaction.ts +++ b/src/components/Map/MapViewer/utils/shapes/reaction/Reaction.ts @@ -23,9 +23,9 @@ import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import getLineSegments from '@/components/Map/MapViewer/utils/shapes/coords/getLineSegments'; import getRotation from '@/components/Map/MapViewer/utils/shapes/coords/getRotation'; -import getArrowFeature from '@/components/Map/MapViewer/utils/shapes/elements/getArrowFeature'; import getShapePolygon from '@/components/Map/MapViewer/utils/shapes/elements/getShapePolygon'; import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; +import getReactionArrowFeature from '@/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature'; export interface ReactionProps { id: number; @@ -163,7 +163,7 @@ export default class Reaction { const shortenedX1 = firstSegment.x1 - line.startArrow.length * Math.cos(startArrowRotation); const shortenedY1 = firstSegment.y1 + line.startArrow.length * Math.sin(startArrowRotation); points[0] = this.pointToProjection({ x: shortenedX1, y: shortenedY1 }); - const startArrowFeature = getArrowFeature({ + const startArrowFeature = getReactionArrowFeature({ arrowTypes: this.arrowTypes, arrow: line.startArrow, x: shortenedX1, @@ -190,7 +190,7 @@ export default class Reaction { const shortenedX2 = lastSegment.x2 - line.endArrow.length * Math.cos(endArrowRotation); const shortenedY2 = lastSegment.y2 + line.endArrow.length * Math.sin(endArrowRotation); points[points.length - 1] = this.pointToProjection({ x: shortenedX2, y: shortenedY2 }); - const endArrowFeature = getArrowFeature({ + const endArrowFeature = getReactionArrowFeature({ arrowTypes: this.arrowTypes, arrow: line.endArrow, x: shortenedX2, diff --git a/src/components/Map/MapViewer/utils/shapes/elements/getArrowFeature.test.ts b/src/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature.test.ts similarity index 89% rename from src/components/Map/MapViewer/utils/shapes/elements/getArrowFeature.test.ts rename to src/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature.test.ts index 970241b9fde38f5d87c9e4b511e890e7f3663938..5bf581e179017ed919a8e03fd32aa0f4e2414c39 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/getArrowFeature.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature.test.ts @@ -6,12 +6,12 @@ import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; import { ArrowTypeDict } from '@/redux/shapes/shapes.types'; import getShapePolygon from '@/components/Map/MapViewer/utils/shapes/elements/getShapePolygon'; import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; -import getArrowFeature from '@/components/Map/MapViewer/utils/shapes/elements/getArrowFeature'; +import getReactionArrowFeature from '@/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature'; jest.mock('../style/getStyle'); -jest.mock('./getShapePolygon'); +jest.mock('../elements/getShapePolygon'); -describe('getArrowFeature', () => { +describe('getReactionArrowFeature', () => { const props = { arrowTypes: { FULL: [ @@ -105,18 +105,18 @@ describe('getArrowFeature', () => { }); it('should return arrow feature', () => { - const arrowFeature = getArrowFeature(props); + const arrowFeature = getReactionArrowFeature(props); expect(arrowFeature).toBeInstanceOf(Feature<MultiPolygon>); }); it('should handle missing arrowType in props', () => { const invalidProps = { ...props, arrowTypes: {} }; - expect(() => getArrowFeature(invalidProps)).not.toThrow(); + expect(() => getReactionArrowFeature(invalidProps)).not.toThrow(); }); it('should call getStyle', () => { - getArrowFeature(props); + getReactionArrowFeature(props); expect(getStyle).toBeCalled(); }); }); diff --git a/src/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature.ts b/src/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature.ts new file mode 100644 index 0000000000000000000000000000000000000000..f34a2261a580c493a4b388ef77ff30c08926d9bb --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/reaction/getReactionArrowFeature.ts @@ -0,0 +1,76 @@ +/* eslint-disable no-magic-numbers */ +import Style from 'ol/style/Style'; +import { Feature } from 'ol'; +import { MultiPolygon } from 'ol/geom'; +import { Arrow, Color } from '@/types/models'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import Polygon from 'ol/geom/Polygon'; +import { WHITE_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; +import { ArrowTypeDict } from '@/redux/shapes/shapes.types'; +import getShapePolygon from '@/components/Map/MapViewer/utils/shapes/elements/getShapePolygon'; +import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; +import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; +import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; + +export default function getReactionArrowFeature({ + arrowTypes, + arrow, + x, + y, + zIndex, + rotation, + lineWidth, + color, + pointToProjection, +}: { + arrowTypes: ArrowTypeDict; + arrow: Arrow; + x: number; + y: number; + zIndex: number; + rotation: number; + lineWidth: number; + color: Color; + pointToProjection: UsePointToProjectionResult; +}): undefined | Feature<MultiPolygon> { + const arrowShapes = arrowTypes[arrow.arrowType]; + if (!arrowShapes) { + return undefined; + } + const arrowStyles: Array<Style> = []; + const arrowPolygons: Array<Polygon> = []; + arrowShapes.forEach(shape => { + const arrowPolygon = getShapePolygon({ + shape, + x, + y: y - arrow.length / 2, + width: arrow.length, + height: arrow.length, + pointToProjection, + }); + const style = getStyle({ + geometry: arrowPolygon, + zIndex, + borderColor: color, + fillColor: shape.fill === false ? WHITE_COLOR : color, + lineWidth, + }); + arrowPolygon.set( + 'strokeStyle', + getStroke({ + color: rgbToHex(color), + width: lineWidth, + }), + ); + arrowPolygon.rotate(rotation, pointToProjection({ x, y })); + arrowStyles.push(style); + arrowPolygons.push(arrowPolygon); + }); + const arrowFeature = new Feature({ + geometry: new MultiPolygon(arrowPolygons), + style: arrowStyles, + zIndex, + }); + arrowFeature.setStyle(arrowStyles); + return arrowFeature; +} diff --git a/src/components/Map/MapViewer/utils/websocket/processLayerLine.ts b/src/components/Map/MapViewer/utils/websocket/processLayerLine.ts new file mode 100644 index 0000000000000000000000000000000000000000..76c07f8ba88b6016db56b26fe406d4ee7342c9aa --- /dev/null +++ b/src/components/Map/MapViewer/utils/websocket/processLayerLine.ts @@ -0,0 +1,53 @@ +import { WebSocketEntityUpdateInterface } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.types'; +import { store } from '@/redux/store'; +import { ENTITY_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; +import { MapInstance } from '@/types/map'; +import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; +import { getLayerLine } from '@/redux/layers/layers.thunks'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; +import { layerDeleteLine } from '@/redux/layers/layers.slice'; +import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/removeElementFromLayer'; + +export default async function processLayerLine({ + data, + mapInstance, +}: { + data: WebSocketEntityUpdateInterface; + mapInstance: MapInstance; +}): Promise<void> { + const { dispatch } = store; + if ( + data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED || + data.type === ENTITY_OPERATION_TYPES.ENTITY_UPDATED + ) { + const resultLine = await dispatch( + getLayerLine({ + modelId: data.mapId, + layerId: data.layerId, + lineId: data.entityId, + }), + ).unwrap(); + if (!resultLine) { + return; + } + if (data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED) { + drawElementOnLayer({ + mapInstance, + activeLayer: data.layerId, + object: resultLine, + drawFunctionKey: 'drawLine', + }); + } else { + updateElement(mapInstance, data.layerId, resultLine); + } + } else if (data.type === ENTITY_OPERATION_TYPES.ENTITY_DELETED) { + dispatch( + layerDeleteLine({ + modelId: data.mapId, + layerId: data.layerId, + lineId: data.entityId, + }), + ); + removeElementFromLayer({ mapInstance, layerId: data.layerId, featureId: data.entityId }); + } +} diff --git a/src/components/Map/MapViewer/utils/websocket/processMessage.ts b/src/components/Map/MapViewer/utils/websocket/processMessage.ts index 6d1396031b1407fb1701695ab630da0505076428..cac113c5bf80f97e63f0735244c2d633c99f02b0 100644 --- a/src/components/Map/MapViewer/utils/websocket/processMessage.ts +++ b/src/components/Map/MapViewer/utils/websocket/processMessage.ts @@ -6,6 +6,7 @@ import processLayerText from '@/components/Map/MapViewer/utils/websocket/process import processLayerOval from '@/components/Map/MapViewer/utils/websocket/processLayerOval'; import processLayer from '@/components/Map/MapViewer/utils/websocket/processLayer'; import processLayerRect from '@/components/Map/MapViewer/utils/websocket/processLayerRect'; +import processLayerLine from '@/components/Map/MapViewer/utils/websocket/processLayerLine'; export default async function processMessage({ jsonMessage, @@ -24,6 +25,8 @@ export default async function processMessage({ await processLayerOval({ data: jsonMessage, mapInstance }); } else if (jsonMessage.entityType === ENTITY_TYPES.LAYER_RECTANGLE) { await processLayerRect({ data: jsonMessage, mapInstance }); + } else if (jsonMessage.entityType === ENTITY_TYPES.LAYER_LINE) { + await processLayerLine({ data: jsonMessage, mapInstance }); } else if (jsonMessage.entityType === ENTITY_TYPES.LAYER) { await processLayer({ data: jsonMessage, mapInstance }); } diff --git a/src/models/fixtures/layerLinesFixture.ts b/src/models/fixtures/layerLinesFixture.ts index fc15d3c2d364031d896d8803f30cad37d997f8fd..45e9645bf39552aad1eaf2cccc72b82b2cd23d3d 100644 --- a/src/models/fixtures/layerLinesFixture.ts +++ b/src/models/fixtures/layerLinesFixture.ts @@ -6,5 +6,5 @@ import { layerLineSchema } from '@/models/layerLineSchema'; export const layerLinesFixture = createFixture(pageableSchema(layerLineSchema), { seed: ZOD_SEED, - array: { min: 3, max: 3 }, + array: { min: 1, max: 1 }, }); diff --git a/src/models/layerLineSchema.ts b/src/models/layerLineSchema.ts index e454f9302cae6a7f0ccec85aa087a0bf8413aa43..94401eeb1ecf231ab22a901317fb703da92dbc12 100644 --- a/src/models/layerLineSchema.ts +++ b/src/models/layerLineSchema.ts @@ -12,4 +12,5 @@ export const layerLineSchema = z.object({ startArrow: arrowSchema, endArrow: arrowSchema, lineType: z.string(), + layer: z.number(), }); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index b9c58c3cbb653e0d60e53b047597c273542f715b..6700df2845933450cc09f188b4616c32a00894c1 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -84,6 +84,14 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/${textId}`, removeLayerText: (modelId: number, layerId: number, textId: number | string): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/${textId}`, + addLayerLine: (modelId: number, layerId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`, + getLayerLine: (modelId: number, layerId: number, lineId: number | string): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/${lineId}`, + updateLayerLine: (modelId: number, layerId: number, lineId: number | string): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/${lineId}`, + removeLayerLine: (modelId: number, layerId: number, lineId: number | string): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/${lineId}`, getLayer: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`, getGlyphImage: (glyphId: number): string => diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts index d72e8c4ff1ade3c5b038045f146c07342647f254..2621883394cd95cc9ce62952c3b1d0bef4c42bb2 100644 --- a/src/redux/layers/layers.reducers.test.ts +++ b/src/redux/layers/layers.reducers.test.ts @@ -48,7 +48,7 @@ const layersState: LayersState = { texts: {}, rects: {}, ovals: {}, - lines: [], + lines: {}, images: {}, }, }, @@ -104,7 +104,7 @@ describe('layers reducer', () => { texts: { [layerTextsFixture.content[0].id]: layerTextsFixture.content[0] }, rects: { [layerRectsFixture.content[0].id]: layerRectsFixture.content[0] }, ovals: { [layerOvalsFixture.content[0].id]: layerOvalsFixture.content[0] }, - lines: layerLinesFixture.content, + lines: { [layerLinesFixture.content[0].id]: layerLinesFixture.content[0] }, images: { [layerImagesFixture.content[0].id]: layerImagesFixture.content[0] }, }, }, @@ -170,7 +170,7 @@ describe('layers reducer', () => { texts: { [layerTextsFixture.content[0].id]: layerTextsFixture.content[0] }, rects: { [layerRectsFixture.content[0].id]: layerRectsFixture.content[0] }, ovals: { [layerOvalsFixture.content[0].id]: layerOvalsFixture.content[0] }, - lines: layerLinesFixture.content, + lines: { [layerLinesFixture.content[0].id]: layerLinesFixture.content[0] }, images: { [layerImagesFixture.content[0].id]: layerImagesFixture.content[0] }, }, }, diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts index e8ba229c4c781bc2ed7650313f91cf1fc7813723..5518433130fb3122d6ddeb78d1836be587bc3682 100644 --- a/src/redux/layers/layers.reducers.ts +++ b/src/redux/layers/layers.reducers.ts @@ -12,9 +12,10 @@ import { LAYERS_STATE_INITIAL_LAYER_MOCK, } from '@/redux/layers/layers.mock'; import { DEFAULT_ERROR } from '@/constants/errors'; -import { LayerImage, LayerOval, LayerRect, LayerText } from '@/types/models'; +import { LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; import setLayerRect from '@/redux/layers/utils/setLayerRect'; import setLayerOval from '@/redux/layers/utils/setLayerOval'; +import setLayerLine from '@/redux/layers/utils/setLayerLine'; export const getLayersForModelReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { builder.addCase(getLayersForModel.pending, (state, action) => { @@ -68,7 +69,7 @@ export const getLayerReducer = (builder: ActionReducerMapBuilder<LayersState>): images: {}, ovals: {}, rects: {}, - lines: [], + lines: {}, }; data.layersVisibility[layerId] = updatedLayerDetails.visible; if (!updatedLayerDetails.locked) { @@ -366,3 +367,43 @@ export const layerDeleteOvalReducer = ( } delete layer.ovals[ovalId]; }; + +export const layerAddLineReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; layerLine: LayerLine }>, +): void => { + const { modelId, layerId, layerLine } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + setLayerLine({ layerId, layers: data.layers, layerLine }); +}; + +export const layerUpdateLineReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; layerLine: LayerLine }>, +): void => { + const { modelId, layerId, layerLine } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + setLayerLine({ layerId, layers: data.layers, layerLine }); +}; + +export const layerDeleteLineReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; lineId: number }>, +): void => { + const { modelId, layerId, lineId } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + const layer = data.layers[layerId]; + if (!layer) { + return; + } + delete layer.lines[lineId]; +}; diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 45b91bd9c11d458852bc6b900260272a29e1799c..ca915e9ff6b05116867d9e508be442eb1dd8e68a 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -136,7 +136,7 @@ export const maxObjectZIndexForLayerSelector = createSelector( const textsMaxZ = getMaxZFromItems(Object.values(foundLayer.texts)); const rectsMaxZ = getMaxZFromItems(Object.values(foundLayer.rects)); const ovalsMaxZ = getMaxZFromItems(Object.values(foundLayer.ovals)); - const linesMaxZ = getMaxZFromItems(foundLayer.lines); + const linesMaxZ = getMaxZFromItems(Object.values(foundLayer.lines)); const imagesMaxZ = getMaxZFromItems(Object.values(foundLayer.images)); return Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ); @@ -159,7 +159,7 @@ export const minObjectZIndexForLayerSelector = createSelector( const textsMinZ = getMinZFromItems(Object.values(foundLayer.texts)); const rectsMinZ = getMinZFromItems(Object.values(foundLayer.rects)); const ovalsMinZ = getMinZFromItems(Object.values(foundLayer.ovals)); - const linesMinZ = getMinZFromItems(foundLayer.lines); + const linesMinZ = getMinZFromItems(Object.values(foundLayer.lines)); const imagesMinZ = getMinZFromItems(Object.values(foundLayer.images)); return Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts index 243b6ab7dfb6d9da15e3a50d80c0fba6ad24e012..9eb890b0ba9b97f4ae1a5fe66588a90a6ec72697 100644 --- a/src/redux/layers/layers.slice.ts +++ b/src/redux/layers/layers.slice.ts @@ -22,6 +22,9 @@ import { layerDeleteReducer, layerUpdateOvalReducer, layerDeleteOvalReducer, + layerAddLineReducer, + layerUpdateLineReducer, + layerDeleteLineReducer, } from '@/redux/layers/layers.reducers'; export const layersSlice = createSlice({ @@ -45,6 +48,9 @@ export const layersSlice = createSlice({ layerAddOval: layerAddOvalReducer, layerUpdateOval: layerUpdateOvalReducer, layerDeleteOval: layerDeleteOvalReducer, + layerAddLine: layerAddLineReducer, + layerUpdateLine: layerUpdateLineReducer, + layerDeleteLine: layerDeleteLineReducer, }, extraReducers: builder => { getLayersForModelReducer(builder); @@ -72,6 +78,9 @@ export const { layerAddOval, layerUpdateOval, layerDeleteOval, + layerAddLine, + layerUpdateLine, + layerDeleteLine, } = layersSlice.actions; export default layersSlice.reducer; diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts index 3894b5a635979f7e922d3732e3ac12fd27be9ffe..5d2154d2ffa1c27a5ea383bb9803a2c319a1728b 100644 --- a/src/redux/layers/layers.thunks.test.ts +++ b/src/redux/layers/layers.thunks.test.ts @@ -76,7 +76,9 @@ describe('layers thunks', () => { ovals: { [layerOvalsFixture.content[0].id]: layerOvalsFixture.content[0], }, - lines: layerLinesFixture.content, + lines: { + [layerLinesFixture.content[0].id]: layerLinesFixture.content[0], + }, images: { [layerImagesFixture.content[0].id]: layerImagesFixture.content[0], }, diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index d26c624deb8f7d5089fcbb2ce160920ce10fda29..62ec14324368aeacc5ef1523a479f43f71ef29b1 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -1,7 +1,16 @@ /* eslint-disable no-magic-numbers */ import { z as zod } from 'zod'; import { apiPath } from '@/redux/apiPath'; -import { Color, Layer, LayerImage, LayerOval, LayerRect, Layers, LayerText } from '@/types/models'; +import { + Color, + Layer, + LayerImage, + LayerLine, + LayerOval, + LayerRect, + Layers, + LayerText, +} from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; @@ -30,6 +39,10 @@ import { } from '@/components/Map/MapViewer/MapViewer.types'; import { LayerRectFactoryForm } from '@/components/FunctionalArea/Modal/LayerRectFactoryModal/LayerRectFactory.types'; import { LayerOvalFactoryForm } from '@/components/FunctionalArea/Modal/LayerOvalFactoryModal/LayerOvalFactory.types'; +import { + LayerLineEditFactoryPayload, + LayerLineFactoryPayload, +} from '@/components/FunctionalArea/Modal/LayerLineFactoryModal/LayerLineFactory.types'; export const getLayer = createAsyncThunk< Layer | null, @@ -73,7 +86,7 @@ export const getLayersForModel = createAsyncThunk< texts: arrayToKeyValue(textsResponse.data.content as Array<LayerText>, 'id'), rects: arrayToKeyValue(rectsResponse.data.content as Array<LayerRect>, 'id'), ovals: arrayToKeyValue(ovalsResponse.data.content as Array<LayerOval>, 'id'), - lines: linesResponse.data.content, + lines: arrayToKeyValue(linesResponse.data.content as Array<LayerLine>, 'id'), images: arrayToKeyValue(imagesResponse.data.content as Array<LayerImage>, 'id'), }; }), @@ -83,7 +96,7 @@ export const getLayersForModel = createAsyncThunk< zod.array(layerTextSchema).safeParse(Object.values(layer.texts)).success && zod.array(layerRectSchema).safeParse(Object.values(layer.rects)).success && zod.array(layerOvalSchema).safeParse(Object.values(layer.ovals)).success && - zod.array(layerLineSchema).safeParse(layer.lines).success && + zod.array(layerLineSchema).safeParse(Object.values(layer.lines)).success && zod.array(layerImageSchema).safeParse(Object.values(layer.images)).success ); }); @@ -600,3 +613,84 @@ export const removeLayerOval = createAsyncThunk< return Promise.reject(getError({ error })); } }); + +export const getLayerLine = createAsyncThunk< + LayerLine | null, + { + modelId: number; + layerId: number; + lineId: number; + }, + ThunkConfig +>('layers/getLayerLine', async ({ modelId, layerId, lineId }) => { + try { + const { data } = await axiosInstanceNewAPI.get<LayerLine>( + apiPath.getLayerLine(modelId, layerId, lineId), + ); + const isDataValid = validateDataUsingZodSchema(data, layerLineSchema); + + return isDataValid ? data : null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); + +export const addLayerLine = createAsyncThunk< + LayerLine | null, + { + modelId: number; + layerId: number; + payload: LayerLineFactoryPayload; + }, + ThunkConfig +>('layers/addLayerLine', async ({ modelId, layerId, payload }) => { + try { + const { data } = await axiosInstanceNewAPI.post<LayerLine>( + apiPath.addLayerLine(modelId, layerId), + payload, + ); + const isDataValid = validateDataUsingZodSchema(data, layerLineSchema); + + return isDataValid ? data : null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); + +export const updateLayerLine = createAsyncThunk< + LayerLine | null, + { + modelId: number; + layerId: number; + lineId: number; + payload: LayerLineEditFactoryPayload; + }, + ThunkConfig +>('layers/updateLayerLine', async ({ modelId, layerId, lineId, payload }) => { + try { + const { data } = await axiosInstanceNewAPI.put<LayerLine>( + apiPath.updateLayerLine(modelId, layerId, lineId), + payload, + ); + const isDataValid = validateDataUsingZodSchema(data, layerLineSchema); + if (isDataValid) { + return data; + } + return null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); + +export const removeLayerLine = createAsyncThunk< + null, + { modelId: number; layerId: number; lineId: number }, + ThunkConfig +>('layers/removeLayerLine', async ({ modelId, layerId, lineId }) => { + try { + await axiosInstanceNewAPI.delete<void>(apiPath.removeLayerLine(modelId, layerId, lineId)); + return null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts index fef1836e434a5a3ef451a162d508c844e99f5eb5..dfa1cb464656af14981b772796645ce179275241 100644 --- a/src/redux/layers/layers.types.ts +++ b/src/redux/layers/layers.types.ts @@ -23,7 +23,7 @@ export type LayerState = { texts: { [key: string]: LayerText }; rects: { [key: string]: LayerRect }; ovals: { [key: string]: LayerOval }; - lines: LayerLine[]; + lines: { [key: string]: LayerLine }; images: { [key: string]: LayerImage }; }; diff --git a/src/redux/layers/utils/setLayerLine.ts b/src/redux/layers/utils/setLayerLine.ts new file mode 100644 index 0000000000000000000000000000000000000000..983f0b1e79041d4f6392ac002fc39e139d3cab1e --- /dev/null +++ b/src/redux/layers/utils/setLayerLine.ts @@ -0,0 +1,18 @@ +import { LayerLine } from '@/types/models'; +import { LayersDictState } from '@/redux/layers/layers.types'; + +export default function setLayerLine({ + layerId, + layers, + layerLine, +}: { + layerId: number; + layers: LayersDictState; + layerLine: LayerLine; +}): void { + const layer = layers[layerId]; + if (!layer) { + return; + } + layer.lines[layerLine.id] = layerLine; +} diff --git a/src/redux/mapEditTools/mapEditTools.constants.ts b/src/redux/mapEditTools/mapEditTools.constants.ts index d94ae83d709e3a7004adf7e7fbdccdf70dcb3a42..8f9fe3293873500d0fb593e88a1c63930b203d47 100644 --- a/src/redux/mapEditTools/mapEditTools.constants.ts +++ b/src/redux/mapEditTools/mapEditTools.constants.ts @@ -3,5 +3,6 @@ export const MAP_EDIT_ACTIONS = { ADD_TEXT: 'ADD_TEXT', ADD_RECT: 'ADD_RECT', ADD_OVAL: 'ADD_OVAL', + ADD_LINE: 'ADD_LINE', TRANSFORM_IMAGE: 'TRANSFORM_IMAGE', } as const; diff --git a/src/redux/mapEditTools/mapEditTools.mock.ts b/src/redux/mapEditTools/mapEditTools.mock.ts index fbbacec71b385a8f9ee7060ddce4c290a1ba4cd0..86269f24977aff0a8bcae7f264f2144058afaa73 100644 --- a/src/redux/mapEditTools/mapEditTools.mock.ts +++ b/src/redux/mapEditTools/mapEditTools.mock.ts @@ -3,4 +3,5 @@ import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types'; export const MAP_EDIT_TOOLS_STATE_INITIAL_MOCK: MapEditToolsState = { activeAction: null, layerObject: null, + layerLine: null, }; diff --git a/src/redux/mapEditTools/mapEditTools.reducers.ts b/src/redux/mapEditTools/mapEditTools.reducers.ts index 98c871a5bcfa1b5eb99ced56a21a7236d6eaf620..5f4429d5ce95441b05d55cebc5af66f361b67702 100644 --- a/src/redux/mapEditTools/mapEditTools.reducers.ts +++ b/src/redux/mapEditTools/mapEditTools.reducers.ts @@ -2,7 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types'; -import { LayerImage, LayerOval, LayerRect, LayerText } from '@/types/models'; +import { LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; export const mapEditToolsSetActiveActionReducer = ( state: MapEditToolsState, @@ -17,3 +17,10 @@ export const mapEditToolsSetLayerObjectReducer = ( ): void => { state.layerObject = action.payload; }; + +export const mapEditToolsSetLayerLineReducer = ( + state: MapEditToolsState, + action: PayloadAction<LayerLine | null>, +): void => { + state.layerLine = action.payload; +}; diff --git a/src/redux/mapEditTools/mapEditTools.selectors.ts b/src/redux/mapEditTools/mapEditTools.selectors.ts index d3ea7d128e91a479e9cdbf8311d0d7892da37a84..b6f278a2bd8937da73ee0255df6962f83ef8633a 100644 --- a/src/redux/mapEditTools/mapEditTools.selectors.ts +++ b/src/redux/mapEditTools/mapEditTools.selectors.ts @@ -10,6 +10,16 @@ export const mapEditToolsActiveActionSelector = createSelector( ); export const mapEditToolsLayerObjectSelector = createSelector( + mapEditToolsSelector, + state => state.layerObject || state.layerLine, +); + +export const mapEditToolsLayerLineSelector = createSelector( + mapEditToolsSelector, + state => state.layerLine, +); + +export const mapEditToolsLayerNonLineObjectSelector = createSelector( mapEditToolsSelector, state => state.layerObject, ); diff --git a/src/redux/mapEditTools/mapEditTools.slice.ts b/src/redux/mapEditTools/mapEditTools.slice.ts index 2db0001c938bc11f5469fb5addfc167d639680c3..243ad763e6db68619d9d82f399b6421a9a94ad7d 100644 --- a/src/redux/mapEditTools/mapEditTools.slice.ts +++ b/src/redux/mapEditTools/mapEditTools.slice.ts @@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock'; import { mapEditToolsSetActiveActionReducer, + mapEditToolsSetLayerLineReducer, mapEditToolsSetLayerObjectReducer, } from '@/redux/mapEditTools/mapEditTools.reducers'; @@ -11,9 +12,11 @@ export const layersSlice = createSlice({ reducers: { mapEditToolsSetActiveAction: mapEditToolsSetActiveActionReducer, mapEditToolsSetLayerObject: mapEditToolsSetLayerObjectReducer, + mapEditToolsSetLayerLine: mapEditToolsSetLayerLineReducer, }, }); -export const { mapEditToolsSetActiveAction, mapEditToolsSetLayerObject } = layersSlice.actions; +export const { mapEditToolsSetActiveAction, mapEditToolsSetLayerObject, mapEditToolsSetLayerLine } = + layersSlice.actions; export default layersSlice.reducer; diff --git a/src/redux/mapEditTools/mapEditTools.types.ts b/src/redux/mapEditTools/mapEditTools.types.ts index 50f2b0f4273929e3e465af2935babd5f10bf9a9a..1455362209ab9a5dd425150d53de397e997bf54b 100644 --- a/src/redux/mapEditTools/mapEditTools.types.ts +++ b/src/redux/mapEditTools/mapEditTools.types.ts @@ -1,7 +1,8 @@ import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; -import { LayerImage, LayerOval, LayerRect, LayerText } from '@/types/models'; +import { LayerImage, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; export type MapEditToolsState = { activeAction: keyof typeof MAP_EDIT_ACTIONS | null; layerObject: LayerImage | LayerText | LayerRect | LayerOval | null; + layerLine: LayerLine | null; }; diff --git a/src/redux/modal/modal.constants.ts b/src/redux/modal/modal.constants.ts index e1bd16d6813dfa4e230cf419468277e7fbb82f15..f7c1f14675c999e764d2e293f12fca2428a96857 100644 --- a/src/redux/modal/modal.constants.ts +++ b/src/redux/modal/modal.constants.ts @@ -16,4 +16,5 @@ export const MODAL_INITIAL_STATE: ModalState = { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }; diff --git a/src/redux/modal/modal.mock.ts b/src/redux/modal/modal.mock.ts index 26872d50cd57e6ff2f045d743f768115f47dfcd9..fef3c594d63b0dc5a34277acac5c0e58ce374e05 100644 --- a/src/redux/modal/modal.mock.ts +++ b/src/redux/modal/modal.mock.ts @@ -16,4 +16,5 @@ export const MODAL_INITIAL_STATE_MOCK: ModalState = { errorReportState: {}, layerFactoryState: { id: undefined }, layerObjectFactoryState: undefined, + layerLineFactoryState: undefined, }; diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index 0dda3081ad656ab53db3a1e41c87abd34d21a89b..fd22c266bb0f2021959184df4e12664082fd6f74 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -2,6 +2,7 @@ import { ModalName } from '@/types/modal'; import { PayloadAction } from '@reduxjs/toolkit'; import { ErrorData } from '@/utils/error-report/ErrorData'; import { BoundingBox } from '@/components/Map/MapViewer/MapViewer.types'; +import { Segment } from '@/types/models'; import { ModalState, OpenEditOverlayGroupModalAction, @@ -217,3 +218,19 @@ export const openLayerOvalEditFactoryModalReducer = (state: ModalState): void => state.modalName = 'layer-oval-edit-factory'; state.modalTitle = 'Edit oval'; }; + +export const openLayerLineFactoryModalReducer = ( + state: ModalState, + action: PayloadAction<Array<Segment>>, +): void => { + state.layerLineFactoryState = action.payload; + state.modalName = 'layer-line-factory'; + state.modalTitle = 'Add line'; + state.isOpen = true; +}; + +export const openLayerLineEditFactoryModalReducer = (state: ModalState): void => { + state.isOpen = true; + state.modalName = 'layer-line-edit-factory'; + state.modalTitle = 'Edit line'; +}; diff --git a/src/redux/modal/modal.selector.ts b/src/redux/modal/modal.selector.ts index 7c5224193cac110b864fd44f76095ee0511086c9..4265eca2f381d72f3c17ea44dc2b7a66eab38fdc 100644 --- a/src/redux/modal/modal.selector.ts +++ b/src/redux/modal/modal.selector.ts @@ -40,3 +40,8 @@ export const layerObjectFactoryStateSelector = createSelector( modalSelector, modal => modal.layerObjectFactoryState, ); + +export const layerLineFactoryStateSelector = createSelector( + modalSelector, + modal => modal.layerLineFactoryState, +); diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts index 1618ce0d702553a83f77530b49a29503d12df4d7..13fa58579a233896fdd865910086fd6cbc249de0 100644 --- a/src/redux/modal/modal.slice.ts +++ b/src/redux/modal/modal.slice.ts @@ -26,6 +26,8 @@ import { openLayerOvalFactoryModalReducer, openEditOverlayGroupModalReducer, openLayerOvalEditFactoryModalReducer, + openLayerLineFactoryModalReducer, + openLayerLineEditFactoryModalReducer, } from './modal.reducers'; const modalSlice = createSlice({ @@ -57,6 +59,8 @@ const modalSlice = createSlice({ openLayerRectEditFactoryModal: openLayerRectEditFactoryModalReducer, openLayerOvalFactoryModal: openLayerOvalFactoryModalReducer, openLayerOvalEditFactoryModal: openLayerOvalEditFactoryModalReducer, + openLayerLineFactoryModal: openLayerLineFactoryModalReducer, + openLayerLineEditFactoryModal: openLayerLineEditFactoryModalReducer, }, }); @@ -86,6 +90,8 @@ export const { openLayerRectEditFactoryModal, openLayerOvalFactoryModal, openLayerOvalEditFactoryModal, + openLayerLineFactoryModal, + openLayerLineEditFactoryModal, } = modalSlice.actions; export default modalSlice.reducer; diff --git a/src/redux/modal/modal.types.ts b/src/redux/modal/modal.types.ts index a558bfaba7b998c90a0eb8a26f48eb394ef8fb8e..ab98a156feb3c3caf7e0b7086f85beb59ff13005 100644 --- a/src/redux/modal/modal.types.ts +++ b/src/redux/modal/modal.types.ts @@ -1,5 +1,5 @@ import { ModalName } from '@/types/modal'; -import { MapOverlay, OverlayGroup } from '@/types/models'; +import { MapOverlay, OverlayGroup, Segment } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; import { ErrorData } from '@/utils/error-report/ErrorData'; import { BoundingBox } from '@/components/Map/MapViewer/MapViewer.types'; @@ -25,6 +25,8 @@ export type LayerFactoryState = { export type LayerObjectFactoryBoundingBoxState = BoundingBox | undefined; +export type LayerLineFactoryState = Array<Segment> | undefined; + export interface ModalState { isOpen: boolean; modalName: ModalName; @@ -36,6 +38,7 @@ export interface ModalState { editOverlayGroupState: EditOverlayGroupState; layerFactoryState: LayerFactoryState; layerObjectFactoryState: LayerObjectFactoryBoundingBoxState; + layerLineFactoryState: LayerLineFactoryState; } export type OpenEditOverlayModalPayload = MapOverlay; diff --git a/src/redux/shapes/shapes.selectors.ts b/src/redux/shapes/shapes.selectors.ts index d23c9a97d97a744f6150d30757d2706dbeaa6a96..9768c2a555b8038eb264f0c61dcfda8b1626cf6d 100644 --- a/src/redux/shapes/shapes.selectors.ts +++ b/src/redux/shapes/shapes.selectors.ts @@ -18,6 +18,10 @@ export const lineTypesSelector = createSelector( shapes => shapes.lineTypesState.data || {}, ); +export const lineTypesKeysSelector = createSelector(shapesSelector, shapes => + Object.keys(shapes.lineTypesState?.data || {}), +); + export const lineTypesLoadingSelector = createSelector( shapesSelector, shapes => shapes.lineTypesState.loading, @@ -28,6 +32,10 @@ export const arrowTypesSelector = createSelector( shapes => shapes.arrowTypesState.data || {}, ); +export const arrowTypesKeysSelector = createSelector(shapesSelector, shapes => + Object.keys(shapes.arrowTypesState?.data || {}), +); + export const arrowTypesLoadingSelector = createSelector( shapesSelector, shapes => shapes.arrowTypesState.loading, diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index 8f8459af7ccb83a7f3acc3ee2b741e3cd2ac5157..a4c7753ebdd4b362037c2d6f8ebbc7bb279c3a6c 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -37,6 +37,7 @@ import { LayerArrowUpIcon } from '@/shared/Icon/Icons/LayerArrowUpIcon'; import { LayerArrowDownIcon } from '@/shared/Icon/Icons/LayerArrowDownIcon'; import { RectangleIcon } from '@/shared/Icon/Icons/RectangleIcon'; import { OvalIcon } from '@/shared/Icon/Icons/OvalIcon'; +import { LineIcon } from '@/shared/Icon/Icons/LineIcon'; import { LocationIcon } from './Icons/LocationIcon'; import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn'; import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut'; @@ -97,6 +98,7 @@ const icons: Record<IconTypes, IconComponentType> = { 'layer-arrow-down': LayerArrowDownIcon, rectangle: RectangleIcon, oval: OvalIcon, + line: LineIcon, } as const; export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => { diff --git a/src/shared/Icon/Icons/LineIcon.tsx b/src/shared/Icon/Icons/LineIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6d535b99ce7e462bb7628d4db20e1a064683a9fa --- /dev/null +++ b/src/shared/Icon/Icons/LineIcon.tsx @@ -0,0 +1,35 @@ +interface LineIconProps { + className?: string; +} + +export const LineIcon = ({ className }: LineIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="0 0 200 200" + xmlns="http://www.w3.org/2000/svg" + className={className} + > + <defs> + <marker + id="arrow" + markerWidth="10" + markerHeight="10" + refX="5" + refY="5" + orient="auto" + markerUnits="strokeWidth" + > + <path d="M 0 0 L 10 5 L 0 10 z" fill="black" /> + </marker> + </defs> + <polyline + points="20,180 80,100 140,140 180,40" + stroke="black" + strokeWidth="8" + fill="none" + strokeLinejoin="round" + markerEnd="url(#arrow)" + /> + </svg> +); diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 57cd7d668c407da83bb3851a4037784862eddff0..0e587285da682a4b306bd0fdb97ff1a677b85cb3 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -43,6 +43,7 @@ export type IconTypes = | 'layer-arrow-up' | 'layer-arrow-down' | 'rectangle' - | 'oval'; + | 'oval' + | 'line'; export type IconComponentType = ({ className }: { className: string }) => JSX.Element; diff --git a/src/types/modal.ts b/src/types/modal.ts index 5efc956488c8a9902f2f2b1a9c66b477942d27ec..0a98faa94d02f5687a4a12d9305561c05a0c748f 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -21,4 +21,6 @@ export type ModalName = | 'layer-rect-factory' | 'layer-rect-edit-factory' | 'layer-oval-factory' - | 'layer-oval-edit-factory'; + | 'layer-oval-edit-factory' + | 'layer-line-factory' + | 'layer-line-edit-factory';