diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx index 0c0973f2bb3ea4843ff5788e9c6e36863681c94d..29a26c8bd3180a02f7414476e1ed00f05d92bd8a 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx @@ -81,7 +81,13 @@ const renderComponent = ( describe('LayerImageObjectEditFactoryModal - component', () => { it('should render LayerImageObjectEditFactoryModal component with initial state', () => { - renderComponent(); + renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); expect(screen.getByText(/Glyph:/i)).toBeInTheDocument(); expect(screen.getByText(/File:/i)).toBeInTheDocument(); @@ -90,7 +96,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }); it('should display a list of glyphs in the dropdown', async () => { - renderComponent(); + renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); const dropdown = screen.getByTestId('autocomplete'); if (!dropdown.firstChild) { @@ -102,7 +114,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }); it('should update the selected glyph on dropdown change', async () => { - renderComponent(); + renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); const dropdown = screen.getByTestId('autocomplete'); if (!dropdown.firstChild) { @@ -142,13 +160,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }; const getGlyphDataMock = jest.fn(() => glyphData); jest.spyOn(layerObjectFeature, 'get').mockImplementation(key => { - if (key === 'update') return (): void => {}; - if (key === 'getGlyphData') return getGlyphDataMock; + if (key === 'updateElement') return (): void => {}; + if (key === 'getObjectData') return getGlyphDataMock; return undefined; }); renderComponent({ activeAction: MAP_EDIT_ACTIONS.TRANSFORM_IMAGE, - layerImageObject: glyphData, + layerObject: glyphData, }); const submitButton = screen.getByText(/Submit/i); @@ -164,7 +182,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }); it('should display "No Image" when there is no image file', () => { - const { store } = renderComponent(); + const { store } = renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); store.dispatch({ type: 'glyphs/clearGlyphData', diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx index e4dfcdd01a2d83fe449f515152ec47c68b592ce1..dc2808459cdd66ba4f4c85b000a1d7f30cc55d94 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; @@ -13,24 +13,25 @@ import { showToast } from '@/utils/showToast'; import { closeModal } from '@/redux/modal/modal.slice'; import { SerializedError } from '@reduxjs/toolkit'; import { useMapInstance } from '@/utils/context/mapInstanceContext'; -import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; export const LayerImageObjectEditFactoryModal: React.FC = () => { - const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const layerObject = useAppSelector(mapEditToolsLayerObjectSelector); const { mapInstance } = useMapInstance(); + if (!layerObject || !('glyph' in layerObject)) { + throw new Error('Invalid layer image object'); + } const currentModelId = useAppSelector(currentModelIdSelector); const dispatch = useAppDispatch(); - const [selectedGlyph, setSelectedGlyph] = useState<number | null>( - layerImageObject?.glyph || null, - ); + const [selectedGlyph, setSelectedGlyph] = useState<number | null>(layerObject?.glyph || null); const [file, setFile] = useState<File | null>(null); const [isSending, setIsSending] = useState<boolean>(false); const handleSubmit = async (): Promise<void> => { - if (!layerImageObject) { + if (!layerObject) { return; } setIsSending(true); @@ -47,8 +48,8 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { const layerImage = await dispatch( updateLayerImageObject({ modelId: currentModelId, - layerId: layerImageObject.layer, - ...layerImageObject, + layerId: layerObject.layer, + ...layerObject, glyph: glyphId, }), ).unwrap(); @@ -57,7 +58,7 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { layerUpdateImage({ modelId: currentModelId, layerId: layerImage.layer, layerImage }), ); dispatch(mapEditToolsSetLayerObject(layerImage)); - updateGlyph(mapInstance, layerImage.layer, layerImage); + updateElement(mapInstance, layerImage.layer, layerImage); } showToast({ type: 'success', diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx index 12a7959c686d19332358bbb886f6dd9bba463aa3..6f57c76263e042ba0009c55879578125496cdaae 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx @@ -1,18 +1,80 @@ -import { JSX } from 'react'; +import React, { JSX, useMemo } from 'react'; import { LayerText } from '@/types/models'; import { Icon } from '@/shared/Icon'; +import { LayersDrawerObjectActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; interface LayersDrawerTextItemProps { layerText: LayerText; + bringToFront: () => void; + bringToBack: () => void; + removeObject: () => void; + centerObject: () => void; + editObject: () => void; + isLayerVisible: boolean; + isLayerActive: boolean; } export const LayersDrawerTextItem = ({ layerText, + bringToFront, + bringToBack, + removeObject, + centerObject, + editObject, + isLayerVisible, + isLayerActive, }: LayersDrawerTextItemProps): JSX.Element | null => { + const dispatch = useAppDispatch(); + const activeLayerObject = useAppSelector(mapEditToolsLayerObjectSelector); + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); + + const showActions = useMemo(() => { + return activeLayerObject?.id === layerText.id; + }, [activeLayerObject?.id, layerText.id]); + + const canSelectItem = useMemo(() => { + return isLayerVisible && isLayerActive && hasPrivilegeToWriteProject; + }, [isLayerVisible, isLayerActive, hasPrivilegeToWriteProject]); + + const selectItem = useMemo(() => { + return (): void => { + if (canSelectItem) { + dispatch(mapEditToolsSetLayerObject(layerText)); + } + }; + }, [canSelectItem, dispatch, layerText]); + + const handleKeyPress = (): void => {}; + return ( - <div className="flex min-h-[24px] gap-2"> - <Icon name="text" className="shrink-0" /> - <span className="truncate">{layerText.notes}</span> + <div + className="flex min-h-[24px] items-center justify-between gap-2" + id={`layer-text-item-${layerText.id}`} + > + <div + className={`flex gap-2 ${canSelectItem ? 'cursor-pointer' : 'cursor-default'}`} + onClick={selectItem} + tabIndex={0} + onKeyDown={handleKeyPress} + role="button" + > + <Icon name="text" className="shrink-0" /> + <span className="truncate">{layerText.notes}</span> + </div> + {showActions && ( + <LayersDrawerObjectActions + bringToFront={bringToFront} + bringToBack={bringToBack} + removeObject={removeObject} + centerObject={centerObject} + editObject={editObject} + /> + )} </div> ); }; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index df941aca918455a3b099da92866c2e5d9748f7a5..12bef081cb69be66edc08ac8bdf2eb448540ce31 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -8,14 +8,14 @@ import { JSX, useEffect, useRef } from 'react'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; import { LayersDrawerLayer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; export const LayersDrawer = (): JSX.Element => { const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); const dispatch = useAppDispatch(); const layersDrawerRef = useRef<HTMLDivElement>(null); - const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); const addNewLayer = (): void => { dispatch(openLayerFactoryModal()); diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx index d2884a5bb104b64d6d5f30105d5f3afc84a13bec..0622f1416bb018d4192399beb33bf26b4f0bf0fe 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx @@ -4,7 +4,7 @@ import { Icon } from '@/shared/Icon'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { glyphFileNameByIdSelector } from '@/redux/glyphs/glyphs.selectors'; import { LayersDrawerObjectActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; @@ -31,7 +31,7 @@ export const LayersDrawerImageItem = ({ isLayerActive, }: LayersDrawerImageItemProps): JSX.Element | null => { const dispatch = useAppDispatch(); - const activeLayerImage = useAppSelector(mapEditToolsLayerImageObjectSelector); + const activeLayerImage = useAppSelector(mapEditToolsLayerObjectSelector); const fileName = useAppSelector(state => glyphFileNameByIdSelector(state, layerImage.glyph)); const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx index fca7a858abf3d7658a6b63415d8eeea9bcf46636..fa98e051392758b8139aa334988c37a98a8f0aef 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -9,19 +9,23 @@ import { JSX, useState } from 'react'; import { LayersDrawerImageItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component'; import { LayersDrawerTextItem } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component'; import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; -import { removeLayerImage, updateLayerImageObject } from '@/redux/layers/layers.thunks'; -import { layerDeleteImage, layerUpdateImage } from '@/redux/layers/layers.slice'; +import { + removeLayerImage, + removeLayerText, + updateLayerImageObject, +} from '@/redux/layers/layers.thunks'; +import { layerDeleteImage, layerDeleteText, layerUpdateImage } from '@/redux/layers/layers.slice'; import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; import { showToast } from '@/utils/showToast'; import { SerializedError } from '@reduxjs/toolkit'; -import { LayerImage } from '@/types/models'; +import { LayerImage, 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 updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; import { useSetBounds } from '@/utils/map/useSetBounds'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { Coordinate } from 'ol/coordinate'; import { openLayerImageObjectEditFactoryModal } from '@/redux/modal/modal.slice'; @@ -32,6 +36,19 @@ interface LayersDrawerObjectsListProps { isLayerActive: boolean; } +const removeObjectConfig = { + image: { + question: 'Are you sure you want to remove the image?', + successMessage: 'The layer image has been successfully removed', + errorMessage: 'An error occurred while removing the layer text', + }, + text: { + question: 'Are you sure you want to remove the text?', + successMessage: 'The layer text has been successfully removed', + errorMessage: 'An error occurred while removing the layer text', + }, +}; + export const LayersDrawerObjectsList = ({ layerId, isLayerVisible, @@ -41,57 +58,81 @@ export const LayersDrawerObjectsList = ({ const highestZIndex = useAppSelector(highestZIndexSelector); const lowestZIndex = useAppSelector(lowestZIndexSelector); const layer = useAppSelector(state => layerByIdSelector(state, layerId)); - const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); - const [isImageRemoveModalOpen, setIsImageRemoveModalOpen] = useState(false); - const [layerImageToRemove, setLayerImageToRemove] = useState<LayerImage | null>(null); + const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); + const [removeModalState, setRemoveModalState] = useState<undefined | 'text' | 'image'>(undefined); + const [layerObjectToRemove, setLayerObjectToRemove] = useState<LayerImage | LayerText | null>( + null, + ); const dispatch = useAppDispatch(); const setBounds = useSetBounds(); const pointToProjection = usePointToProjection(); const { mapInstance } = useMapInstance(); - const removeImage = (layerImage: LayerImage): void => { - setLayerImageToRemove(layerImage); - setIsImageRemoveModalOpen(true); + const removeObject = (layerObject: LayerImage | LayerText): void => { + setLayerObjectToRemove(layerObject); + if ('glyph' in layerObject) { + setRemoveModalState('image'); + } else { + setRemoveModalState('text'); + } }; - const rejectImageRemove = (): void => { - setIsImageRemoveModalOpen(false); + const rejectRemove = (): void => { + setRemoveModalState(undefined); }; - const confirmImageRemove = async (): Promise<void> => { - if (!layerImageToRemove) { + const confirmRemove = async (): Promise<void> => { + if (!layerObjectToRemove || !removeModalState) { return; } + try { - await dispatch( - removeLayerImage({ - modelId: currentModelId, - layerId: layerImageToRemove.layer, - imageId: layerImageToRemove.id, - }), - ).unwrap(); - dispatch( - layerDeleteImage({ - modelId: currentModelId, - layerId: layerImageToRemove.layer, - imageId: layerImageToRemove.id, - }), - ); + if (removeModalState === 'text') { + await dispatch( + removeLayerText({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + textId: layerObjectToRemove.id, + }), + ).unwrap(); + dispatch( + layerDeleteText({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + textId: layerObjectToRemove.id, + }), + ); + } else { + await dispatch( + removeLayerImage({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + imageId: layerObjectToRemove.id, + }), + ).unwrap(); + dispatch( + layerDeleteImage({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + imageId: layerObjectToRemove.id, + }), + ); + } removeElementFromLayer({ mapInstance, - layerId: layerImageToRemove.layer, - featureId: layerImageToRemove.id, + layerId: layerObjectToRemove.layer, + featureId: layerObjectToRemove.id, }); showToast({ type: 'success', - message: 'The layer image has been successfully removed', + message: removeObjectConfig[removeModalState].successMessage, }); - setIsImageRemoveModalOpen(false); + setRemoveModalState(undefined); } catch (error) { const typedError = error as SerializedError; showToast({ type: 'error', - message: typedError.message || 'An error occurred while removing the layer image', + message: typedError.message || removeObjectConfig[removeModalState].errorMessage, }); } }; @@ -120,7 +161,7 @@ export const LayersDrawerObjectsList = ({ }), ); dispatch(mapEditToolsSetLayerObject(newLayerImage)); - updateGlyph(mapInstance, newLayerImage.layer, newLayerImage); + updateElement(mapInstance, newLayerImage.layer, newLayerImage); } }; @@ -132,12 +173,12 @@ export const LayersDrawerObjectsList = ({ await updateImageZIndex({ zIndex: lowestZIndex - 1, layerImage }); }; - const centerObject = (layerImage: LayerImage): void => { - if (mapEditToolsLayerImageObject && mapEditToolsLayerImageObject.id === layerImage.id) { - const point1 = pointToProjection({ x: layerImage.x, y: layerImage.y }); + const centerObject = (layerObject: LayerImage | LayerText): void => { + if (mapEditToolsLayerImageObject && mapEditToolsLayerImageObject.id === layerObject.id) { + const point1 = pointToProjection({ x: layerObject.x, y: layerObject.y }); const point2 = pointToProjection({ - x: layerImage.x + layerImage.width, - y: layerImage.y + layerImage.height, + x: layerObject.x + layerObject.width, + y: layerObject.y + layerObject.height, }); setBounds([point1, point2] as Coordinate[]); } @@ -154,13 +195,27 @@ export const LayersDrawerObjectsList = ({ return ( <div className={`${isLayerVisible ? 'opacity-100' : 'opacity-40'} flex flex-col gap-1 ps-3`}> <QuestionModal - isOpen={isImageRemoveModalOpen} - onClose={rejectImageRemove} - onConfirm={confirmImageRemove} - question="Are you sure you want to remove the image?" + isOpen={Boolean(removeModalState)} + onClose={rejectRemove} + onConfirm={confirmRemove} + question={ + removeModalState + ? removeObjectConfig[removeModalState].question + : 'Are you sure you want to remove the object' + } /> {Object.values(layer.texts).map(layerText => ( - <LayersDrawerTextItem layerText={layerText} key={layerText.id} /> + <LayersDrawerTextItem + layerText={layerText} + key={layerText.id} + bringToFront={() => {}} + bringToBack={() => {}} + removeObject={() => removeObject(layerText)} + centerObject={() => centerObject(layerText)} + editObject={() => {}} + isLayerVisible={isLayerVisible} + isLayerActive={isLayerActive} + /> ))} {Object.values(layer.images).map(layerImage => ( <LayersDrawerImageItem @@ -168,7 +223,7 @@ export const LayersDrawerObjectsList = ({ key={layerImage.id} bringToFront={() => bringImageToFront(layerImage)} bringToBack={() => bringImageToBack(layerImage)} - removeObject={() => removeImage(layerImage)} + removeObject={() => removeObject(layerImage)} centerObject={() => centerObject(layerImage)} editObject={() => editImage()} isLayerVisible={isLayerVisible} diff --git a/src/components/Map/MapViewer/MapViewer.component.tsx b/src/components/Map/MapViewer/MapViewer.component.tsx index 0307cf080469c999aa576b3b7710fed9c1fffc67..faf874c1f4c1e4802f5c9f37511723e6d1e05840 100644 --- a/src/components/Map/MapViewer/MapViewer.component.tsx +++ b/src/components/Map/MapViewer/MapViewer.component.tsx @@ -1,13 +1,17 @@ import 'ol/ol.css'; import { twMerge } from 'tailwind-merge'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { isMapEditToolsActiveSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { + isMapEditToolsActiveSelector, + mapEditToolsLayerObjectSelector, +} from '@/redux/mapEditTools/mapEditTools.selectors'; import { useOlMap } from './utils/useOlMap'; import { MAP_VIEWER_ROLE } from './MapViewer.constants'; export const MapViewer = (): JSX.Element => { const { mapRef } = useOlMap(); const isMapEditToolsActive = useAppSelector(isMapEditToolsActiveSelector); + const layerObject = useAppSelector(mapEditToolsLayerObjectSelector); return ( <div @@ -15,7 +19,7 @@ export const MapViewer = (): JSX.Element => { role={MAP_VIEWER_ROLE} className={twMerge( 'absolute left-[88px] top-[104px] h-[calc(100%-104px)] w-[calc(100%-88px)] bg-white', - isMapEditToolsActive ? 'bg-[#e4e2de]' : 'bg-white', + isMapEditToolsActive || layerObject ? 'bg-[#e4e2de]' : 'bg-white', )} /> ); diff --git a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 162d345c015bf0d698b35bb57413bdefc0ed61ea..5e3782938c6cc08f2bc06c077bfe514d06c02201 100644 --- a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -16,7 +16,7 @@ import { } from '@/redux/layers/layers.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { MapInstance } from '@/types/map'; -import { Geometry, LineString, MultiPolygon, Point } from 'ol/geom'; +import { Geometry, LineString, MultiPolygon } from 'ol/geom'; import Polygon from 'ol/geom/Polygon'; import Layer from '@/components/Map/MapViewer/utils/shapes/layer/Layer'; import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors'; @@ -25,7 +25,7 @@ import { mapDataSizeSelector } from '@/redux/map/map.selectors'; import { LayerState } from '@/redux/layers/layers.types'; import { mapEditToolsActiveActionSelector, - mapEditToolsLayerImageObjectSelector, + mapEditToolsLayerObjectSelector, } 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'; @@ -38,7 +38,7 @@ import { mapEditToolsSetActiveAction, mapEditToolsSetLayerObject, } from '@/redux/mapEditTools/mapEditTools.slice'; -import getTransformImageInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction'; +import getTransformInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction'; import { useWebSocketEntityUpdatesContext } from '@/utils/websocket-entity-updates/webSocketEntityUpdatesProvider'; import processMessage from '@/components/Map/MapViewer/utils/websocket/processMessage'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; @@ -46,9 +46,7 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' export const useOlMapAdditionalLayers = ( mapInstance: MapInstance, ): Array< - VectorLayer< - VectorSource<Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>> - > + VectorLayer<VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>> > => { const activeAction = useAppSelector(mapEditToolsActiveActionSelector); const dispatch = useAppDispatch(); @@ -61,7 +59,7 @@ export const useOlMapAdditionalLayers = ( const activeLayers = useAppSelector(layersActiveLayersSelector); const drawLayer = useAppSelector(layersDrawLayerSelector); const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); - const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const mapEditToolsLayerObject = useAppSelector(mapEditToolsLayerObjectSelector); const [layersState, setLayersState] = useState<Array<LayerState>>([]); const [layersLoadingState, setLayersLoadingState] = useState(false); @@ -157,21 +155,23 @@ export const useOlMapAdditionalLayers = ( if (!dispatch || !currentModelId || !activeLayers.length) { return null; } - const imagesFeatures: Array<Feature<Geometry>> = []; + const features: Array<Feature<Geometry>> = []; const activeVectorLayers = vectorLayers.filter(layer => activeLayers.includes(layer.get('id'))); activeVectorLayers.forEach(vectorLayer => { - imagesFeatures.push(...vectorLayer.get('imagesFeatures')); + features.push(...vectorLayer.get('imagesFeatures')); + features.push(...vectorLayer.get('textsFeatures')); }); - const imagesFeaturesCollection = new Collection(imagesFeatures); - return getTransformImageInteraction( + const featuresCollection = new Collection(features); + return getTransformInteraction( dispatch, mapSize, currentModelId, - imagesFeaturesCollection, + featuresCollection, restrictionExtent, ); }, [dispatch, mapSize, currentModelId, restrictionExtent, activeLayers, vectorLayers]); const transformRef = useRef(transformInteraction); + useEffect(() => { transformRef.current = transformInteraction; }, [transformInteraction]); @@ -202,13 +202,13 @@ export const useOlMapAdditionalLayers = ( } const transformFeatures = transformRef.current.getFeatures(); if ( - mapEditToolsLayerImageObject && + mapEditToolsLayerObject && (!transformFeatures.getLength() || - transformFeatures.item(0).getId() !== mapEditToolsLayerImageObject.id) + transformFeatures.item(0).getId() !== mapEditToolsLayerObject.id) ) { const layer = vectorLayers.find(vectorLayer => { const layerId = vectorLayer.get('id'); - return layerId === mapEditToolsLayerImageObject.layer; + return layerId === mapEditToolsLayerObject.layer; }); if (!layer) { return; @@ -217,13 +217,13 @@ export const useOlMapAdditionalLayers = ( if (!source) { return; } - const feature = source.getFeatureById(mapEditToolsLayerImageObject.id); + const feature = source.getFeatureById(mapEditToolsLayerObject.id); if (!feature) { return; } transformRef.current.setSelection(new Collection<Feature>([feature])); } - }, [mapEditToolsLayerImageObject, vectorLayers]); + }, [mapEditToolsLayerObject, vectorLayers]); useEffect(() => { const activeVectorLayers = vectorLayers.filter(layer => { diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts index 0dca36a15704c5ca55e6f188fe4d7ec0adcee5e9..342cdfacf7beb2b3b2f46b8494777f88223ca774 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts @@ -17,7 +17,6 @@ import { LayerImage } from '@/types/models'; import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import getFill from '@/components/Map/MapViewer/utils/shapes/style/getFill'; import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; -import getBoundingBoxFromExtent from '@/components/Map/MapViewer/utils/shapes/coords/getBoundingBoxFromExtent'; export type GlyphProps = { elementId: number; @@ -154,7 +153,7 @@ export default class Glyph { this.feature.set('setCoordinates', this.setCoordinates.bind(this)); this.feature.set('refreshPolygon', this.refreshPolygon.bind(this)); - this.feature.set('update', this.update.bind(this)); + this.feature.set('updateElement', this.updateElement.bind(this)); this.feature.setId(this.elementId); this.feature.setStyle(this.getStyle.bind(this)); @@ -186,7 +185,7 @@ export default class Glyph { this.feature.changed(); } - private update(imageObject: LayerImage): void { + protected updateElement(imageObject: LayerImage): void { this.elementId = imageObject.id; this.x = imageObject.x; this.y = imageObject.y; @@ -219,11 +218,6 @@ export default class Glyph { const geometry = this.polygonStyle.getGeometry(); if (geometry && geometry instanceof Polygon) { geometry.setCoordinates(coords); - const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), this.mapSize); - this.x = boundingBox.x; - this.y = boundingBox.y; - this.width = boundingBox.width; - this.height = boundingBox.height; } } diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts index 2149e63b551b9d2ec7a5e42561a2d805b5f8165f..7f697a728a0d69c560730771d4ae182ea95c9ceb 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts @@ -4,12 +4,12 @@ import { LayerLine, LayerOval, LayerRect, - LayerText, + LayerText as LayerTextModel, } from '@/types/models'; import { MapInstance } from '@/types/map'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; -import { LineString, MultiPolygon, Point } from 'ol/geom'; +import { LineString, MultiPolygon } from 'ol/geom'; import Polygon from 'ol/geom/Polygon'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; @@ -30,11 +30,11 @@ import getArrowFeature from '@/components/Map/MapViewer/utils/shapes/elements/ge import getRotation from '@/components/Map/MapViewer/utils/shapes/coords/getRotation'; import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import getEllipseCoords from '@/components/Map/MapViewer/utils/shapes/coords/getEllipseCoords'; -import Text from '@/components/Map/MapViewer/utils/shapes/text/Text'; -import LayerImage from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage'; +import LayerText from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerText'; +import LayerImage from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage'; export interface LayerProps { - texts: { [key: string]: LayerText }; + texts: { [key: string]: LayerTextModel }; rects: Array<LayerRect>; ovals: Array<LayerOval>; lines: Array<LayerLine>; @@ -51,7 +51,7 @@ export interface LayerProps { export default class Layer { layerId: number; - texts: { [key: string]: LayerText }; + texts: { [key: string]: LayerTextModel }; rects: Array<LayerRect>; @@ -71,12 +71,10 @@ export default class Layer { mapSize: MapSize; - vectorSource: VectorSource< - Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon> - >; + vectorSource: VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>; vectorLayer: VectorLayer< - VectorSource<Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>> + VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>> >; constructor({ @@ -107,7 +105,9 @@ export default class Layer { this.mapSize = mapSize; this.layerId = layerId; - this.vectorSource.addFeatures(this.getTextsFeatures()); + const textsFeatures = this.getTextsFeatures(); + this.vectorSource.addFeatures(textsFeatures); + this.vectorSource.addFeatures(this.getRectsFeatures()); this.vectorSource.addFeatures(this.getOvalsFeatures()); const imagesFeatures = this.getImagesFeatures(); @@ -127,53 +127,39 @@ export default class Layer { this.vectorLayer.set('id', layerId); this.vectorLayer.set('imagesFeatures', imagesFeatures); + this.vectorLayer.set('textsFeatures', textsFeatures); this.vectorLayer.set('drawImage', this.drawImage.bind(this)); this.vectorLayer.set('drawText', this.drawText.bind(this)); } - private getTextsFeatures = (): Array<Feature<Point>> => { - const textObjects = Object.values(this.texts).map(text => { - return new Text({ - x: text.x, - y: text.y, - zIndex: text.z, - width: text.width, - height: text.height, - layer: text.layer, - fontColor: text.color, - borderColor: text.borderColor, - fontSize: text.fontSize, - text: text.notes, - verticalAlign: text.verticalAlign as VerticalAlign, - horizontalAlign: text.horizontalAlign as HorizontalAlign, - pointToProjection: this.pointToProjection, - mapInstance: this.mapInstance, - }); - }); - return textObjects.map(text => text.feature); + private getTextsFeatures = (): Array<Feature<Polygon>> => { + return Object.values(this.texts).map(text => this.getTextFeature(text)); }; - private drawText(text: LayerText): void { + private drawText(text: LayerTextModel): void { const textFeature = this.getTextFeature(text); this.vectorSource.addFeature(textFeature); } - private getTextFeature(text: LayerText): Feature<Point> { - const textObject = new Text({ + private getTextFeature(text: LayerTextModel): Feature<Polygon> { + const textObject = new LayerText({ + elementId: text.id, x: text.x, y: text.y, zIndex: text.z, width: text.width, height: text.height, layer: text.layer, - fontColor: text.color, + color: text.color, borderColor: text.borderColor, + backgroundColor: text.backgroundColor, fontSize: text.fontSize, text: text.notes, verticalAlign: text.verticalAlign as VerticalAlign, horizontalAlign: text.horizontalAlign as HorizontalAlign, pointToProjection: this.pointToProjection, mapInstance: this.mapInstance, + mapSize: this.mapSize, }); return textObject.feature; } diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts similarity index 54% rename from src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts rename to src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts index ff893bdf860f74ce2a2d0c69e9aa6fce96400419..b7d306bb6f8f1456d337080ab1b9a3f4a5db62bc 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts @@ -3,6 +3,11 @@ import { MapInstance } from '@/types/map'; import { MapSize } from '@/redux/map/map.types'; import { LayerImage as LayerImageModel } from '@/types/models'; import Glyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph'; +import { store } from '@/redux/store'; +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'; export type LayerImageProps = { elementId: number; @@ -47,11 +52,35 @@ export default class LayerImage extends Glyph { mapSize, }); this.layer = layer; - this.feature.set('getGlyphData', this.getGlyphData.bind(this)); + this.feature.set('getObjectData', this.getData.bind(this)); + this.feature.set('save', this.save.bind(this)); this.feature.set('layer', layer); } - private getGlyphData(): LayerImageModel { + private async save({ + modelId, + boundingBox, + }: { + modelId: number; + boundingBox: BoundingBox; + }): Promise<void> { + const { dispatch } = store; + const layerImage = await dispatch( + updateLayerImageObject({ + modelId, + layerId: this.layer, + ...this.getData(), + ...boundingBox, + }), + ).unwrap(); + if (layerImage) { + dispatch(layerUpdateImage({ modelId, layerId: layerImage.layer, layerImage })); + dispatch(mapEditToolsSetLayerObject(layerImage)); + this.updateElement(layerImage); + } + } + + private getData(): LayerImageModel { return { id: this.elementId, x: this.x, diff --git a/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.test.ts similarity index 75% rename from src/components/Map/MapViewer/utils/shapes/text/Text.test.ts rename to src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.test.ts index 2f2e145da280fd69de1abcadbef8d3e97c5ce839..7db76f34349d662a45cccf9d5d0e529e9ad22870 100644 --- a/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.test.ts @@ -3,17 +3,20 @@ import { Map } from 'ol'; import { Style } from 'ol/style'; import View from 'ol/View'; import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; -import Text, { TextProps } from '@/components/Map/MapViewer/utils/shapes/text/Text'; +import LayerText, { + LayerTextProps, +} from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerText'; import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; import getTextCoords from '@/components/Map/MapViewer/utils/shapes/text/getTextCoords'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; -jest.mock('./getTextCoords'); -jest.mock('./getTextStyle'); -jest.mock('../style/rgbToHex'); +jest.mock('../../text/getTextCoords'); +jest.mock('../../text/getTextStyle'); +jest.mock('../../style/rgbToHex'); describe('Text', () => { - let props: TextProps; + let props: LayerTextProps; beforeEach(() => { const dummyElement = document.createElement('div'); @@ -26,6 +29,7 @@ describe('Text', () => { }), }); props = { + elementId: 1, x: 0, y: 0, width: 100, @@ -34,12 +38,20 @@ describe('Text', () => { layer: 1, text: 'Test', fontSize: 12, - fontColor: BLACK_COLOR, + color: BLACK_COLOR, borderColor: BLACK_COLOR, + backgroundColor: BLACK_COLOR, verticalAlign: 'MIDDLE', horizontalAlign: 'CENTER', pointToProjection: jest.fn(({ x, y }) => [x, y]), mapInstance, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue(new Style()); @@ -48,7 +60,7 @@ describe('Text', () => { }); it('should apply correct styles to the feature', () => { - const text = new Text(props); + const text = new LayerText(props); const { feature } = text; const style = feature.getStyleFunction()?.call(text, feature, 1); @@ -61,7 +73,7 @@ describe('Text', () => { }); it('should hide text when the scaled font size is too small', () => { - const text = new Text(props); + const text = new LayerText(props); const { feature } = text; const style = feature.getStyleFunction()?.call(text, feature, 20); diff --git a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts new file mode 100644 index 0000000000000000000000000000000000000000..133034cf4bf778c89f6d46b8d18a86f0a5473527 --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts @@ -0,0 +1,315 @@ +/* eslint-disable no-magic-numbers */ +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import Style from 'ol/style/Style'; +import { Point } from 'ol/geom'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import { MapInstance } from '@/types/map'; +import { LayerText as LayerTextModel, Color } from '@/types/models'; +import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewer.constants'; +import { + BoundingBox, + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewer.types'; +import getTextCoords from '@/components/Map/MapViewer/utils/shapes/text/getTextCoords'; +import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; +import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; +import Polygon from 'ol/geom/Polygon'; +import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; +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 getBoundingBoxFromExtent from '@/components/Map/MapViewer/utils/shapes/coords/getBoundingBoxFromExtent'; +import { Coordinate } from 'ol/coordinate'; +import { store } from '@/redux/store'; +import { updateLayerText } from '@/redux/layers/layers.thunks'; +import { layerUpdateText } from '@/redux/layers/layers.slice'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; + +export interface LayerTextProps { + elementId: number; + x: number; + y: number; + width: number; + height: number; + layer: number; + zIndex: number; + text: string; + fontSize: number; + color: Color; + borderColor: Color; + backgroundColor: Color; + verticalAlign: VerticalAlign; + horizontalAlign: HorizontalAlign; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; + mapSize: MapSize; +} + +export default class LayerText { + elementId: number; + + x: number; + + y: number; + + zIndex: number; + + width: number; + + height: number; + + layer: number; + + text: string; + + verticalAlign: VerticalAlign; + + horizontalAlign: HorizontalAlign; + + backgroundColor: Color; + + borderColor: Color; + + color: Color; + + fontSize: number; + + style: Style; + + polygonStyle: Style; + + polygon: Polygon = new Polygon([]); + + strokeStyle: Stroke; + + point: Point; + + feature: Feature<Polygon>; + + mapSize: MapSize; + + pointToProjection: UsePointToProjectionResult; + + constructor({ + elementId, + x, + y, + width, + height, + layer, + zIndex, + text, + fontSize, + color, + borderColor, + backgroundColor, + verticalAlign, + horizontalAlign, + pointToProjection, + mapInstance, + mapSize, + }: LayerTextProps) { + this.text = text; + this.fontSize = fontSize; + this.elementId = elementId; + this.x = x; + this.y = y; + this.zIndex = zIndex; + this.width = width; + this.height = height; + this.layer = layer; + this.text = text; + this.verticalAlign = verticalAlign; + this.horizontalAlign = horizontalAlign; + this.backgroundColor = backgroundColor; + this.borderColor = borderColor; + this.color = color; + this.mapSize = mapSize; + this.pointToProjection = pointToProjection; + + const textCoords = getTextCoords({ + x, + y, + height, + width, + fontSize, + verticalAlign, + horizontalAlign, + pointToProjection, + }); + this.drawPolygon(); + this.strokeStyle = getStroke({ + color: rgbToHex(borderColor), + width: 1, + }); + this.polygonStyle = getStyle({ + geometry: this.polygon, + borderColor, + fillColor: { rgb: 0, alpha: 0 }, + lineWidth: 1, + zIndex, + }); + + const textStyle = getTextStyle({ + text, + fontSize, + color: rgbToHex(color), + zIndex, + horizontalAlign, + }); + this.point = new Point(textCoords); + this.style = textStyle; + this.style.setGeometry(this.point); + + this.feature = new Feature({ + geometry: this.polygon, + getScale: (resolution: number): number => { + const maxZoom = mapInstance?.getView().get('originalMaxZoom'); + if (maxZoom) { + const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); + if (minResolution) { + return Math.round((minResolution / resolution) * 100) / 100; + } + } + return 1; + }, + layer, + }); + this.feature.setId(this.elementId); + this.feature.set('getObjectData', this.getData.bind(this)); + this.feature.set('setCoordinates', this.setCoordinates.bind(this)); + this.feature.set('refreshPolygon', this.refreshPolygon.bind(this)); + this.feature.set('save', this.save.bind(this)); + this.feature.set('updateElement', this.updateElement.bind(this)); + this.feature.setStyle(this.getStyle.bind(this)); + } + + private getData(): LayerTextModel { + return { + id: this.elementId, + x: this.x, + y: this.y, + z: this.zIndex, + width: this.width, + height: this.height, + layer: this.layer, + fontSize: this.fontSize, + notes: this.text, + verticalAlign: this.verticalAlign, + horizontalAlign: this.horizontalAlign, + backgroundColor: this.backgroundColor, + borderColor: this.borderColor, + color: this.color, + }; + } + + private drawPolygon(): void { + this.polygon = new Polygon([ + [ + this.pointToProjection({ x: this.x, y: this.y }), + this.pointToProjection({ x: this.x + this.width, y: this.y }), + this.pointToProjection({ x: this.x + this.width, y: this.y + this.height }), + this.pointToProjection({ x: this.x, y: this.y + this.height }), + this.pointToProjection({ x: this.x, y: this.y }), + ], + ]); + } + + private async save({ + modelId, + boundingBox, + }: { + modelId: number; + boundingBox: BoundingBox; + }): Promise<void> { + const { dispatch } = store; + const layerText = await dispatch( + updateLayerText({ + modelId, + layerId: this.layer, + ...this.getData(), + ...boundingBox, + }), + ).unwrap(); + if (layerText) { + dispatch(layerUpdateText({ modelId, layerId: layerText.layer, layerText })); + dispatch(mapEditToolsSetLayerObject(layerText)); + this.updateElement(layerText); + } + } + + private refreshPolygon(): void { + this.drawPolygon(); + this.polygonStyle.setGeometry(this.polygon); + this.feature.setGeometry(this.polygon); + this.feature.changed(); + } + + private refreshZIndex(): void { + this.polygonStyle.setZIndex(this.zIndex); + this.style.setZIndex(this.zIndex); + this.feature.changed(); + } + + private updateElement(layerText: LayerTextModel): void { + this.elementId = layerText.id; + this.x = layerText.x; + this.y = layerText.y; + this.zIndex = layerText.z; + this.width = layerText.width; + this.height = layerText.height; + this.text = layerText.notes; + this.fontSize = layerText.fontSize; + this.color = layerText.color; + this.borderColor = layerText.borderColor; + this.verticalAlign = layerText.verticalAlign; + this.horizontalAlign = layerText.horizontalAlign; + + this.refreshPolygon(); + this.refreshZIndex(); + this.feature.changed(); + } + + private setCoordinates(coords: Coordinate[][]): void { + const geometry = this.polygonStyle.getGeometry(); + if (geometry && geometry instanceof Polygon) { + geometry.setCoordinates(coords); + } + } + + protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const getScale = feature.get('getScale'); + let scale = 1; + if (getScale instanceof Function) { + scale = getScale(resolution); + } + const geometry = feature.getGeometry(); + if (geometry && geometry instanceof Polygon) { + const polygonExtent = geometry.getExtent(); + if (polygonExtent) { + const boundingBox = getBoundingBoxFromExtent(polygonExtent, this.mapSize); + const textCoords = getTextCoords({ + x: boundingBox.x, + y: boundingBox.y, + height: boundingBox.height, + width: boundingBox.width, + fontSize: this.fontSize, + verticalAlign: this.verticalAlign, + horizontalAlign: this.horizontalAlign, + pointToProjection: this.pointToProjection, + }); + this.point.setCoordinates(textCoords); + } + } + if (scale < TEXT_CUTOFF_SCALE) { + return undefined; + } + return [ + getScaledElementStyle(this.polygonStyle, this.strokeStyle, scale), + getScaledElementStyle(this.style, undefined, scale), + ]; + } +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.test.ts similarity index 85% rename from src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts rename to src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.test.ts index c2f77a2985e3e4a4e7bcf3b10c4add2ba9085751..1cb1fec97800d98c08ca67e1467c3da685d0cded 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.test.ts @@ -10,13 +10,13 @@ import { DEFAULT_TILE_SIZE } from '@/constants/map'; import { Collection, Feature } from 'ol'; import Transform from 'ol-ext/interaction/Transform'; import { Geometry } from 'ol/geom'; -import getTransformImageInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction'; +import getTransformInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction'; jest.mock('../../../../../../../utils/map/latLngToPoint', () => ({ latLngToPoint: jest.fn(latLng => ({ x: latLng[0], y: latLng[1] })), })); -describe('getTransformImageInteraction', () => { +describe('getTransformInteraction', () => { let store = {} as ToolkitStoreWithSingleSlice<ModalState>; let modelIdMock: number; let featuresCollectionMock: Collection<Feature<Geometry>>; @@ -39,7 +39,7 @@ describe('getTransformImageInteraction', () => { }); it('returns a Transform interaction', () => { - const transformInteraction = getTransformImageInteraction( + const transformInteraction = getTransformInteraction( store.dispatch, mapSize, modelIdMock, diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.ts similarity index 76% rename from src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts rename to src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.ts index 3a1435fd1a8db4b9dc6b2d9d669f9ddc799e0412..c1c8565c2fedda6278186bda9ec2a28c5996105f 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.ts @@ -5,15 +5,13 @@ import Transform from 'ol-ext/interaction/Transform'; import { Geometry } from 'ol/geom'; import { Collection, Feature } from 'ol'; import BaseEvent from 'ol/events/Event'; -import { updateLayerImageObject } from '@/redux/layers/layers.thunks'; -import { layerUpdateImage } from '@/redux/layers/layers.slice'; import getBoundingBoxFromExtent from '@/components/Map/MapViewer/utils/shapes/coords/getBoundingBoxFromExtent'; import { MapSize } from '@/redux/map/map.types'; import { Extent } from 'ol/extent'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; import { openDrawer } from '@/redux/drawer/drawer.slice'; -export default function getTransformImageInteraction( +export default function getTransformInteraction( dispatch: AppDispatch, mapSize: MapSize, modelId: number, @@ -86,6 +84,11 @@ export default function getTransformImageInteraction( newGeometry.scale(1, -1); feature.setGeometry(newGeometry); transform.setSelection(new Collection([feature])); + return; + } + const setCoordinates = feature.get('setCoordinates'); + if (geometry instanceof Polygon && setCoordinates instanceof Function) { + setCoordinates(geometry.getCoordinates()); } }); @@ -96,10 +99,10 @@ export default function getTransformImageInteraction( dispatch(mapEditToolsSetLayerObject(null)); return; } - const getGlyphData = features.item(0).get('getGlyphData'); - if (getGlyphData && getGlyphData instanceof Function) { - const glyphData = getGlyphData(); - dispatch(mapEditToolsSetLayerObject(glyphData)); + const getObjectData = features.item(0).get('getObjectData'); + if (getObjectData && getObjectData instanceof Function) { + const objectData = getObjectData(); + dispatch(mapEditToolsSetLayerObject(objectData)); dispatch(openDrawer('layers')); } }); @@ -107,30 +110,13 @@ export default function getTransformImageInteraction( transform.on(['scaleend', 'translateend'], async (event: BaseEvent | Event): Promise<void> => { const transformEvent = event as unknown as { feature: Feature }; const { feature } = transformEvent; - const setCoordinates = feature.get('setCoordinates'); - const getGlyphData = feature.get('getGlyphData'); const refreshPolygon = feature.get('refreshPolygon'); + const save = feature.get('save'); const geometry = feature.getGeometry(); - if (geometry && getGlyphData instanceof Function) { - const glyphData = getGlyphData(); + if (geometry && save instanceof Function) { try { const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), mapSize); - const layerImage = await dispatch( - updateLayerImageObject({ - modelId, - layerId: glyphData.layer, - ...glyphData, - ...boundingBox, - }), - ).unwrap(); - if (layerImage) { - dispatch(layerUpdateImage({ modelId, layerId: layerImage.layer, layerImage })); - dispatch(mapEditToolsSetLayerObject(layerImage)); - } - if (geometry instanceof Polygon && setCoordinates instanceof Function) { - setCoordinates(geometry.getCoordinates()); - geometry.changed(); - } + save({ modelId, boundingBox }); } catch { if (refreshPolygon instanceof Function) { refreshPolygon(); diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph.ts b/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts similarity index 60% rename from src/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph.ts rename to src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts index 054189738b3ace4ef4fb68d64a3113500496ce43..88f50d44622b43bebd15476b09d4543ee634143e 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts @@ -1,20 +1,20 @@ import VectorSource from 'ol/source/Vector'; -import { LayerImage } from '@/types/models'; +import { LayerImage, LayerText } from '@/types/models'; import { MapInstance } from '@/types/map'; -export default function updateGlyph( +export default function updateElement( mapInstance: MapInstance, layerId: number, - layerImage: LayerImage, + layerObject: LayerImage | LayerText, ): void { mapInstance?.getAllLayers().forEach(layer => { if (layer.get('id') === layerId) { const source = layer.getSource(); if (source instanceof VectorSource) { - const feature = source.getFeatureById(layerImage.id); - const update = feature?.get('update'); + const feature = source.getFeatureById(layerObject.id); + const update = feature?.get('updateElement'); if (update && update instanceof Function) { - update(layerImage); + update(layerObject); feature.changed(); } } diff --git a/src/components/Map/MapViewer/utils/shapes/text/Text.ts b/src/components/Map/MapViewer/utils/shapes/text/Text.ts deleted file mode 100644 index c1bc5799c7d386b1abde85d309bdfde5254ebc62..0000000000000000000000000000000000000000 --- a/src/components/Map/MapViewer/utils/shapes/text/Text.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; -import Style from 'ol/style/Style'; -import { Point } from 'ol/geom'; -import { Feature } from 'ol'; -import { FeatureLike } from 'ol/Feature'; -import { MapInstance } from '@/types/map'; -import { Color } from '@/types/models'; -import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewer.constants'; -import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types'; -import getTextCoords from '@/components/Map/MapViewer/utils/shapes/text/getTextCoords'; -import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; -import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; -import Polygon from 'ol/geom/Polygon'; -import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; -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'; - -export interface TextProps { - x: number; - y: number; - width: number; - height: number; - layer: number; - zIndex: number; - text: string; - fontSize: number; - fontColor: Color; - borderColor: Color; - verticalAlign: VerticalAlign; - horizontalAlign: HorizontalAlign; - pointToProjection: UsePointToProjectionResult; - mapInstance: MapInstance; -} - -export default class Text { - text: string; - - fontSize: number; - - style: Style; - - polygonStyle: Style; - - strokeStyle: Stroke; - - point: Point; - - feature: Feature<Point>; - - constructor({ - x, - y, - width, - height, - layer, - zIndex, - text, - fontSize, - fontColor, - borderColor, - verticalAlign, - horizontalAlign, - pointToProjection, - mapInstance, - }: TextProps) { - this.text = text; - this.fontSize = fontSize; - - const textCoords = getTextCoords({ - x, - y, - height, - width, - fontSize, - verticalAlign, - horizontalAlign, - pointToProjection, - }); - const borderPolygon = new Polygon([ - [ - pointToProjection({ x, y }), - pointToProjection({ x: x + width, y }), - pointToProjection({ x: x + width, y: y + height }), - pointToProjection({ x, y: y + height }), - pointToProjection({ x, y }), - ], - ]); - this.strokeStyle = getStroke({ - color: rgbToHex(borderColor), - width: 1, - }); - this.polygonStyle = getStyle({ - geometry: borderPolygon, - borderColor, - fillColor: { rgb: 0, alpha: 0 }, - lineWidth: 1, - zIndex, - }); - - const textStyle = getTextStyle({ - text, - fontSize, - color: rgbToHex(fontColor), - zIndex, - horizontalAlign, - }); - this.point = new Point(textCoords); - this.style = textStyle; - this.style.setGeometry(this.point); - - this.feature = new Feature({ - geometry: this.point, - getScale: (resolution: number): number => { - const maxZoom = mapInstance?.getView().get('originalMaxZoom'); - if (maxZoom) { - const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); - if (minResolution) { - return Math.round((minResolution / resolution) * 100) / 100; - } - } - return 1; - }, - layer, - }); - - this.feature.setStyle(this.getStyle.bind(this)); - } - - protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { - const getScale = feature.get('getScale'); - let scale = 1; - if (getScale instanceof Function) { - scale = getScale(resolution); - } - if (scale < TEXT_CUTOFF_SCALE) { - return undefined; - } - return [ - getScaledElementStyle(this.polygonStyle, this.strokeStyle, scale), - getScaledElementStyle(this.style, undefined, scale), - ]; - } -} diff --git a/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts b/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts index e3224fc9f5272d288c53b55a82f0656f81f09a64..26acfb6b179599a520f69bd5b570e5444fd0ad6d 100644 --- a/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts +++ b/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts @@ -12,6 +12,7 @@ export default function getTextCoords({ verticalAlign, horizontalAlign, pointToProjection, + useProjection = true, }: { x: number; y: number; @@ -20,7 +21,8 @@ export default function getTextCoords({ fontSize: number; verticalAlign: VerticalAlign; horizontalAlign: HorizontalAlign; - pointToProjection: UsePointToProjectionResult; + pointToProjection?: UsePointToProjectionResult; + useProjection?: boolean; }): Coordinate { const minX = x; const maxX = x + width; @@ -41,5 +43,8 @@ export default function getTextCoords({ textX = maxX; } - return pointToProjection({ x: textX, y: textY }); + if (useProjection && pointToProjection) { + return pointToProjection({ x: textX, y: textY }); + } + return [textX, textY]; } diff --git a/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts b/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts index 84018d80755cc437d318b11683ae4ac5815aa7ed..6fd6ec3a4000b3d439178eb8a10a001dc5d86e65 100644 --- a/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts +++ b/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts @@ -2,7 +2,7 @@ import { WebSocketEntityUpdateInterface } from '@/utils/websocket-entity-updates import { store } from '@/redux/store'; import { ENTITY_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; import { getLayerImage } from '@/redux/layers/layers.thunks'; -import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; import { MapInstance } from '@/types/map'; import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; import { layerDeleteImage } from '@/redux/layers/layers.slice'; @@ -38,7 +38,7 @@ export default async function processLayerImage({ drawFunctionKey: 'drawImage', }); } else { - updateGlyph(mapInstance, data.layerId, resultImage); + updateElement(mapInstance, data.layerId, resultImage); } } else if (data.type === ENTITY_OPERATION_TYPES.ENTITY_DELETED) { dispatch( diff --git a/src/components/Map/MapViewer/utils/websocket/processLayerText.ts b/src/components/Map/MapViewer/utils/websocket/processLayerText.ts index f31697c4ea5ebbb4e8337d7df876bbb04fbf8bdc..e1740c979958dd74f31ef3c13cf8a96b562bc9ba 100644 --- a/src/components/Map/MapViewer/utils/websocket/processLayerText.ts +++ b/src/components/Map/MapViewer/utils/websocket/processLayerText.ts @@ -4,6 +4,9 @@ import { ENTITY_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSock import { getLayerText } from '@/redux/layers/layers.thunks'; import { MapInstance } from '@/types/map'; import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; +import { layerDeleteText } from '@/redux/layers/layers.slice'; +import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; export default async function processLayerText({ data, @@ -13,7 +16,10 @@ export default async function processLayerText({ mapInstance: MapInstance; }): Promise<void> { const { dispatch } = store; - if (data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED) { + if ( + data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED || + data.type === ENTITY_OPERATION_TYPES.ENTITY_UPDATED + ) { const resultText = await dispatch( getLayerText({ modelId: data.mapId, @@ -24,11 +30,24 @@ export default async function processLayerText({ if (!resultText) { return; } - drawElementOnLayer({ - mapInstance, - activeLayer: data.layerId, - object: resultText, - drawFunctionKey: 'drawText', - }); + if (data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED) { + drawElementOnLayer({ + mapInstance, + activeLayer: data.layerId, + object: resultText, + drawFunctionKey: 'drawText', + }); + } else { + updateElement(mapInstance, data.layerId, resultText); + } + } else if (data.type === ENTITY_OPERATION_TYPES.ENTITY_DELETED) { + dispatch( + layerDeleteText({ + modelId: data.mapId, + layerId: data.layerId, + textId: data.entityId, + }), + ); + removeElementFromLayer({ mapInstance, layerId: data.layerId, featureId: data.entityId }); } } diff --git a/src/models/layerTextSchema.ts b/src/models/layerTextSchema.ts index 0ec90665b7532e11a8e2782cf28c9e9ef3164fe4..a575b1390ef46ba1b67509f90b0ba9a48a5eefee 100644 --- a/src/models/layerTextSchema.ts +++ b/src/models/layerTextSchema.ts @@ -11,8 +11,8 @@ export const layerTextSchema = z.object({ layer: z.number(), fontSize: z.number(), notes: z.string(), - verticalAlign: z.string(), - horizontalAlign: z.string(), + verticalAlign: z.enum(['TOP', 'MIDDLE', 'BOTTOM']), + horizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER', 'END', 'START']), backgroundColor: colorSchema, borderColor: colorSchema, color: colorSchema, diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 9eb33c02a449c4d869e659f88bcfe8e9522e53b0..18186d6f706cc1a2db43a31d0d30990c871fa4b0 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -64,6 +64,10 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/`, getLayerText: (modelId: number, layerId: number, imageId: number | string): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/${imageId}`, + updateLayerText: (modelId: number, layerId: number, textId: number | string): string => + `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}`, 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.ts b/src/redux/layers/layers.reducers.ts index 0213d7145366de6126d4855bab3665d5de0c3c66..238a3441a2e05c7f6d21a499a10985bbe7eed09e 100644 --- a/src/redux/layers/layers.reducers.ts +++ b/src/redux/layers/layers.reducers.ts @@ -198,3 +198,35 @@ export const layerAddTextReducer = ( } layer.texts[layerText.id] = layerText; }; + +export const layerUpdateTextReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; layerText: LayerText }>, +): void => { + const { modelId, layerId, layerText } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + const layer = data.layers.find(layerState => layerState.details.id === layerId); + if (!layer) { + return; + } + layer.texts[layerText.id] = layerText; +}; + +export const layerDeleteTextReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; textId: number }>, +): void => { + const { modelId, layerId, textId } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + const layer = data.layers.find(layerState => layerState.details.id === layerId); + if (!layer) { + return; + } + delete layer.texts[textId]; +}; diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts index dcd20ceba8302ea711ac5f583abffbf65e051dac..871012fcfb77c30fee1490cf6e1c50aaee65d624 100644 --- a/src/redux/layers/layers.slice.ts +++ b/src/redux/layers/layers.slice.ts @@ -12,6 +12,8 @@ import { setDrawLayerReducer, setLayerToInactiveReducer, setLayerToActiveReducer, + layerDeleteTextReducer, + layerUpdateTextReducer, } from '@/redux/layers/layers.reducers'; export const layersSlice = createSlice({ @@ -25,6 +27,8 @@ export const layersSlice = createSlice({ layerUpdateImage: layerUpdateImageReducer, layerDeleteImage: layerDeleteImageReducer, layerAddText: layerAddTextReducer, + layerUpdateText: layerUpdateTextReducer, + layerDeleteText: layerDeleteTextReducer, setDrawLayer: setDrawLayerReducer, }, extraReducers: builder => { @@ -42,6 +46,8 @@ export const { layerUpdateImage, layerDeleteImage, layerAddText, + layerUpdateText, + layerDeleteText, setDrawLayer, } = layersSlice.actions; diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 847d84d22a8a47c26f12b1d3be6beea86e0c3ab3..66035fd8362c4d412df45932263d47761a022a55 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -1,7 +1,7 @@ /* eslint-disable no-magic-numbers */ import { z as zod } from 'zod'; import { apiPath } from '@/redux/apiPath'; -import { Layer, LayerImage, Layers, LayerText } from '@/types/models'; +import { Color, Layer, LayerImage, Layers, LayerText } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; @@ -22,7 +22,11 @@ import { layerLineSchema } from '@/models/layerLineSchema'; import { layerImageSchema } from '@/models/layerImageSchema'; import arrayToKeyValue from '@/utils/array/arrayToKeyValue'; import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types'; -import { BoundingBox } from '@/components/Map/MapViewer/MapViewer.types'; +import { + BoundingBox, + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewer.types'; export const getLayer = createAsyncThunk< Layer | null, @@ -307,3 +311,81 @@ export const getLayerText = createAsyncThunk< return Promise.reject(getError({ error })); } }); + +export const removeLayerText = createAsyncThunk< + null, + { modelId: number; layerId: number; textId: number }, + ThunkConfig +>('layers/removeLayerText', async ({ modelId, layerId, textId }) => { + try { + await axiosInstanceNewAPI.delete<void>(apiPath.removeLayerText(modelId, layerId, textId)); + return null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); + +export const updateLayerText = createAsyncThunk< + LayerText | null, + { + modelId: number; + layerId: number; + id: number; + x: number; + y: number; + z: number; + width: number; + height: number; + fontSize: number; + notes: string; + verticalAlign: VerticalAlign; + horizontalAlign: HorizontalAlign; + color: Color; + borderColor: Color; + }, + ThunkConfig +>( + 'layers/updateLayerText', + async ({ + modelId, + layerId, + id, + x, + y, + z, + width, + height, + fontSize, + notes, + verticalAlign, + horizontalAlign, + color, + borderColor, + }) => { + try { + const { data } = await axiosInstanceNewAPI.put<LayerText>( + apiPath.updateLayerText(modelId, layerId, id), + { + x, + y, + z, + width, + height, + fontSize, + notes, + verticalAlign, + horizontalAlign, + color, + borderColor, + }, + ); + const isDataValid = validateDataUsingZodSchema(data, layerTextSchema); + if (isDataValid) { + return data; + } + return null; + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); diff --git a/src/redux/mapEditTools/mapEditTools.mock.ts b/src/redux/mapEditTools/mapEditTools.mock.ts index d6fe529ca573712859930af2879cf0591ddb7b50..fbbacec71b385a8f9ee7060ddce4c290a1ba4cd0 100644 --- a/src/redux/mapEditTools/mapEditTools.mock.ts +++ b/src/redux/mapEditTools/mapEditTools.mock.ts @@ -2,5 +2,5 @@ import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types'; export const MAP_EDIT_TOOLS_STATE_INITIAL_MOCK: MapEditToolsState = { activeAction: null, - layerImageObject: null, + layerObject: null, }; diff --git a/src/redux/mapEditTools/mapEditTools.reducers.ts b/src/redux/mapEditTools/mapEditTools.reducers.ts index da3f5f7f7d57126be4f93d49166881a093f58a47..d45e719f8a892cd828dc650ae0d03ea9dfb808e7 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 } from '@/types/models'; +import { LayerImage, LayerText } from '@/types/models'; export const mapEditToolsSetActiveActionReducer = ( state: MapEditToolsState, @@ -13,7 +13,7 @@ export const mapEditToolsSetActiveActionReducer = ( export const mapEditToolsSetLayerObjectReducer = ( state: MapEditToolsState, - action: PayloadAction<LayerImage | null>, + action: PayloadAction<LayerImage | LayerText | null>, ): void => { - state.layerImageObject = action.payload; + state.layerObject = action.payload; }; diff --git a/src/redux/mapEditTools/mapEditTools.selectors.ts b/src/redux/mapEditTools/mapEditTools.selectors.ts index 2b29bd35c204f8f06d6459d2fb7c89429e53a82a..d3ea7d128e91a479e9cdbf8311d0d7892da37a84 100644 --- a/src/redux/mapEditTools/mapEditTools.selectors.ts +++ b/src/redux/mapEditTools/mapEditTools.selectors.ts @@ -9,9 +9,9 @@ export const mapEditToolsActiveActionSelector = createSelector( state => state.activeAction, ); -export const mapEditToolsLayerImageObjectSelector = createSelector( +export const mapEditToolsLayerObjectSelector = createSelector( mapEditToolsSelector, - state => state.layerImageObject, + state => state.layerObject, ); export const isMapEditToolsActiveSelector = createSelector(mapEditToolsSelector, state => diff --git a/src/redux/mapEditTools/mapEditTools.types.ts b/src/redux/mapEditTools/mapEditTools.types.ts index e141e6b86f35356c891bf47320a1784b945ee525..4c216c406c2b890a4fbc6fe97701928f0f751828 100644 --- a/src/redux/mapEditTools/mapEditTools.types.ts +++ b/src/redux/mapEditTools/mapEditTools.types.ts @@ -1,7 +1,7 @@ import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; -import { LayerImage } from '@/types/models'; +import { LayerImage, LayerText } from '@/types/models'; export type MapEditToolsState = { activeAction: keyof typeof MAP_EDIT_ACTIONS | null; - layerImageObject: LayerImage | null; + layerObject: LayerImage | LayerText | null; };