Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • minerva/frontend
1 result
Show changes
Commits on Source (4)
Showing
with 837 additions and 181 deletions
......@@ -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',
......
......@@ -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',
......@@ -68,7 +69,7 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => {
const typedError = error as SerializedError;
showToast({
type: 'error',
message: typedError.message || 'An error occurred while adding a new image',
message: typedError.message || 'An error occurred while editing the layer image',
});
} finally {
setIsSending(false);
......
/* eslint-disable no-magic-numbers */
import React, { useState } from 'react';
import './LayerTextFactoryModal.styles.css';
import { LayerTextForm } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component';
import { LoadingIndicator } from '@/shared/LoadingIndicator';
import { Button } from '@/shared/Button';
import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types';
import { Color } from '@/types/models';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { currentModelIdSelector } from '@/redux/models/models.selectors';
import { showToast } from '@/utils/showToast';
import { closeModal } from '@/redux/modal/modal.slice';
import { SerializedError } from '@reduxjs/toolkit';
import { updateLayerText } from '@/redux/layers/layers.thunks';
import { layerUpdateText } from '@/redux/layers/layers.slice';
import { useMapInstance } from '@/utils/context/mapInstanceContext';
import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice';
import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors';
import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement';
export const LayerTextEditFactoryModal: React.FC = () => {
const layerObject = useAppSelector(mapEditToolsLayerObjectSelector);
const currentModelId = useAppSelector(currentModelIdSelector);
const dispatch = useAppDispatch();
const { mapInstance } = useMapInstance();
if (!layerObject || !('notes' in layerObject)) {
throw new Error('Invalid layer text object');
}
const [isSending, setIsSending] = useState<boolean>(false);
const [data, setData] = useState<LayerTextFactoryForm>({
notes: layerObject.notes,
fontSize: layerObject.fontSize,
horizontalAlign: layerObject.horizontalAlign,
verticalAlign: layerObject.verticalAlign,
color: layerObject.color,
borderColor: layerObject.borderColor,
});
const handleSubmit = async (): Promise<void> => {
if (!layerObject) {
return;
}
try {
const layerText = await dispatch(
updateLayerText({
modelId: currentModelId,
layerId: layerObject.layer,
id: layerObject.id,
x: layerObject.x,
y: layerObject.y,
z: layerObject.z,
width: layerObject.width,
height: layerObject.height,
notes: data.notes,
fontSize: data.fontSize,
horizontalAlign: data.horizontalAlign,
verticalAlign: data.verticalAlign,
color: data.color,
borderColor: data.borderColor,
}),
).unwrap();
if (layerText) {
dispatch(layerUpdateText({ modelId: currentModelId, layerId: layerText.layer, layerText }));
dispatch(mapEditToolsSetLayerObject(layerText));
updateElement(mapInstance, layerText.layer, layerText);
}
showToast({
type: 'success',
message: 'The text 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 layer text',
});
} finally {
setIsSending(false);
}
};
const changeValues = (value: string | number | Color, key: string): void => {
setData(prevData => ({ ...prevData, [key]: value }));
};
return (
<div className="relative w-[900px] border border-t-[#E1E0E6] bg-white p-[24px]">
{isSending && (
<div className="c-layer-text-factory-modal-loader">
<LoadingIndicator width={44} height={44} />
</div>
)}
<LayerTextForm onChange={changeValues} data={data} />
<hr className="py-2" />
<Button
type="button"
onClick={handleSubmit}
className="justify-center self-end justify-self-end text-base font-medium"
>
Submit
</Button>
</div>
);
};
/* eslint-disable no-magic-numbers */
import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types';
export const TEXT_FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96];
export const TEXT_HORIZONTAL_ALIGNMENTS = [
{ id: 'LEFT', name: 'left' },
{ id: 'RIGHT', name: 'right' },
{ id: 'CENTER', name: 'center' },
{ id: 'END', name: 'end' },
{ id: 'START', name: 'start' },
];
] as const;
export const TEXT_VERTICAL_ALIGNMENTS = [
{ id: 'TOP', name: 'top' },
{ id: 'MIDDLE', name: 'middle' },
{ id: 'BOTTOM', name: 'bottom' },
];
] as const;
export const DEFAULT_TEXT_FONT_SIZE = 12;
export const DEFAULT_HORIZONTAL_ALIGNMENT = TEXT_HORIZONTAL_ALIGNMENTS[0].id;
export const DEFAULT_VERTICAL_ALIGNMENT = TEXT_VERTICAL_ALIGNMENTS[0].id;
export const DEFAULT_HORIZONTAL_ALIGNMENT: HorizontalAlign = TEXT_HORIZONTAL_ALIGNMENTS[0].id;
export const DEFAULT_VERTICAL_ALIGNMENT: VerticalAlign = TEXT_VERTICAL_ALIGNMENTS[0].id;
import { Color } from '@/types/models';
import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types';
export type LayerTextFactoryForm = {
notes: string;
fontSize: number;
horizontalAlign: string;
verticalAlign: string;
horizontalAlign: HorizontalAlign;
verticalAlign: VerticalAlign;
color: Color;
borderColor: Color;
};
......@@ -41,7 +41,7 @@ export const LayerTextForm = ({ data, onChange }: LayerTextFormProps): React.JSX
<div>
<span>Horizontal alignment:</span>
<Select
options={TEXT_HORIZONTAL_ALIGNMENTS}
options={[...TEXT_HORIZONTAL_ALIGNMENTS]}
selectedId={data.horizontalAlign}
testId="horizontal-alignment-select"
onChange={value => onChange(value, 'horizontalAlign')}
......@@ -50,7 +50,7 @@ export const LayerTextForm = ({ data, onChange }: LayerTextFormProps): React.JSX
<div>
<span>Vertical alignment:</span>
<Select
options={TEXT_VERTICAL_ALIGNMENTS}
options={[...TEXT_VERTICAL_ALIGNMENTS]}
selectedId={data.verticalAlign}
testId="vertical-alignment-select"
onChange={value => onChange(value, 'verticalAlign')}
......
......@@ -10,6 +10,7 @@ import {
LayerImageObjectFactoryModal,
} from '@/components/FunctionalArea/Modal/LayerImageObjectModal';
import { LayerTextFactoryModal } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component';
import { LayerTextEditFactoryModal } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextEditFactoryModal.component';
import { EditOverlayModal } from './EditOverlayModal';
import { LoginModal } from './LoginModal';
import { ErrorReportModal } from './ErrorReportModal';
......@@ -105,6 +106,11 @@ export const Modal = (): React.ReactNode => {
<LayerTextFactoryModal />
</ModalLayout>
)}
{isOpen && modalName === 'layer-text-edit-factory' && (
<ModalLayout>
<LayerTextEditFactoryModal />
</ModalLayout>
)}
</>
);
};
......@@ -34,7 +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]',
modalName === 'layer-text-factory' && 'h-auto w-[900px]',
['layer-text-factory', 'layer-text-edit-factory'].includes(modalName) &&
'h-auto w-[900px]',
['layer-image-object-factory', 'layer-image-object-edit-factory'].includes(modalName) &&
'h-auto w-[800px]',
['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]',
......
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>
);
};
......@@ -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());
......
......@@ -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);
......
......@@ -9,22 +9,35 @@ 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,
updateLayerText,
} from '@/redux/layers/layers.thunks';
import {
layerDeleteImage,
layerDeleteText,
layerUpdateImage,
layerUpdateText,
} 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';
import {
openLayerImageObjectEditFactoryModal,
openLayerTextEditFactoryModal,
} from '@/redux/modal/modal.slice';
interface LayersDrawerObjectsListProps {
layerId: number;
......@@ -32,6 +45,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 +67,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 +170,7 @@ export const LayersDrawerObjectsList = ({
}),
);
dispatch(mapEditToolsSetLayerObject(newLayerImage));
updateGlyph(mapInstance, newLayerImage.layer, newLayerImage);
updateElement(mapInstance, newLayerImage.layer, newLayerImage);
}
};
......@@ -132,12 +182,48 @@ 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 updateTextZIndex = async ({
zIndex,
layerText,
}: {
zIndex: number;
layerText: LayerText;
}): Promise<void> => {
const newLayerText = await dispatch(
updateLayerText({
modelId: currentModelId,
layerId: layerText.layer,
...layerText,
z: zIndex,
}),
).unwrap();
if (newLayerText) {
dispatch(
layerUpdateText({
modelId: currentModelId,
layerId: newLayerText.layer,
layerText: newLayerText,
}),
);
dispatch(mapEditToolsSetLayerObject(newLayerText));
updateElement(mapInstance, newLayerText.layer, newLayerText);
}
};
const bringTextToFront = async (layerText: LayerText): Promise<void> => {
await updateTextZIndex({ zIndex: highestZIndex + 1, layerText });
};
const bringTextToBack = async (layerText: LayerText): Promise<void> => {
await updateTextZIndex({ zIndex: lowestZIndex - 1, layerText });
};
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[]);
}
......@@ -147,6 +233,10 @@ export const LayersDrawerObjectsList = ({
dispatch(openLayerImageObjectEditFactoryModal());
};
const editText = (): void => {
dispatch(openLayerTextEditFactoryModal());
};
if (!layer) {
return null;
}
......@@ -154,13 +244,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={() => bringTextToFront(layerText)}
bringToBack={() => bringTextToBack(layerText)}
removeObject={() => removeObject(layerText)}
centerObject={() => centerObject(layerText)}
editObject={() => editText()}
isLayerVisible={isLayerVisible}
isLayerActive={isLayerActive}
/>
))}
{Object.values(layer.images).map(layerImage => (
<LayersDrawerImageItem
......@@ -168,7 +272,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}
......
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',
)}
/>
);
......
......@@ -8,7 +8,7 @@ export type MapConfig = {
};
export type VerticalAlign = 'TOP' | 'MIDDLE' | 'BOTTOM';
export type HorizontalAlign = 'LEFT' | 'RIGHT' | 'CENTER' | 'END' | 'START';
export type HorizontalAlign = 'LEFT' | 'RIGHT' | 'CENTER';
export type OverlayBioEntityGroupedElementsType = {
[id: string]: Array<OverlayBioEntityRender & { amount: number }>;
......
......@@ -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 => {
......
......@@ -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;
......@@ -35,13 +34,13 @@ export type GlyphProps = {
export default class Glyph {
feature: Feature<Polygon>;
style: Style = new Style({});
style: Style = new Style();
noGlyphStyle: Style;
noGlyphStyle: Style = new Style();
imageScale: number = 1;
polygonStyle: Style;
polygonStyle: Style = new Style();
polygon: Polygon = new Polygon([]);
......@@ -114,26 +113,7 @@ export default class Glyph {
this.drawPolygon();
this.polygonStyle = getStyle({
geometry: this.polygon,
zIndex: this.zIndex,
borderColor: { ...WHITE_COLOR, alpha: 0 },
fillColor: { ...WHITE_COLOR, alpha: 0 },
});
this.noGlyphStyle = getStyle({
geometry: this.polygon,
zIndex: this.zIndex,
fillColor: '#E7E7E7',
});
this.noGlyphStyle.setText(
new Text({
text: 'No image',
font: '12pt Arial',
fill: getFill({ color: '#000' }),
overflow: true,
}),
);
this.setStyles();
this.feature = new Feature({
geometry: this.polygon,
......@@ -154,7 +134,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));
......@@ -173,6 +153,29 @@ export default class Glyph {
]);
}
private setStyles(): void {
this.polygonStyle = getStyle({
geometry: this.polygon,
zIndex: this.zIndex,
borderColor: { ...WHITE_COLOR, alpha: 0 },
fillColor: { ...WHITE_COLOR, alpha: 0 },
});
this.noGlyphStyle = getStyle({
geometry: this.polygon,
zIndex: this.zIndex,
fillColor: '#E7E7E7',
});
this.noGlyphStyle.setText(
new Text({
text: 'No image',
font: '12pt Arial',
fill: getFill({ color: '#000' }),
overflow: true,
}),
);
}
private refreshPolygon(): void {
this.drawPolygon();
this.polygonStyle.setGeometry(this.polygon);
......@@ -186,7 +189,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;
......@@ -196,7 +199,7 @@ export default class Glyph {
this.glyphId = imageObject.glyph;
this.refreshPolygon();
this.refreshZIndex();
this.setStyles();
this.drawImage();
this.feature.changed();
}
......@@ -219,11 +222,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;
}
}
......
......@@ -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;
}
......
......@@ -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,
......
......@@ -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);
......
......@@ -5,9 +5,13 @@ 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 { Color, LayerText as LayerTextModel } from '@/types/models';
import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewer.constants';
import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types';
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';
......@@ -16,8 +20,16 @@ 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 TextProps {
export interface LayerTextProps {
elementId: number;
x: number;
y: number;
width: number;
......@@ -26,30 +38,63 @@ export interface TextProps {
zIndex: number;
text: string;
fontSize: number;
fontColor: Color;
color: Color;
borderColor: Color;
backgroundColor: Color;
verticalAlign: VerticalAlign;
horizontalAlign: HorizontalAlign;
pointToProjection: UsePointToProjectionResult;
mapInstance: MapInstance;
mapSize: MapSize;
}
export default class Text {
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;
style: Style = new Style();
polygonStyle: Style = new Style();
polygonStyle: Style;
polygon: Polygon = new Polygon([]);
strokeStyle: Stroke;
strokeStyle: Stroke = new Stroke();
point: Point;
feature: Feature<Point>;
feature: Feature<Polygon>;
mapSize: MapSize;
pointToProjection: UsePointToProjectionResult;
constructor({
elementId,
x,
y,
width,
......@@ -58,15 +103,32 @@ export default class Text {
zIndex,
text,
fontSize,
fontColor,
color,
borderColor,
backgroundColor,
verticalAlign,
horizontalAlign,
pointToProjection,
mapInstance,
}: TextProps) {
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,
......@@ -78,40 +140,14 @@ export default class Text {
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.drawPolygon();
this.setStyles();
this.feature = new Feature({
geometry: this.point,
geometry: this.polygon,
getScale: (resolution: number): number => {
const maxZoom = mapInstance?.getView().get('originalMaxZoom');
if (maxZoom) {
......@@ -124,16 +160,154 @@ export default class Text {
},
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 setStyles(): void {
this.strokeStyle = getStroke({
color: rgbToHex(this.borderColor),
width: 1,
});
this.polygonStyle = getStyle({
geometry: this.polygon,
borderColor: this.borderColor,
fillColor: { rgb: 0, alpha: 0 },
lineWidth: 1,
zIndex: this.zIndex,
});
this.style = getTextStyle({
text: this.text,
fontSize: this.fontSize,
color: rgbToHex(this.color),
zIndex: this.zIndex,
horizontalAlign: this.horizontalAlign,
});
this.style.setGeometry(this.point);
}
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.setStyles();
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;
}
......