diff --git a/client/public/template.docx b/client/public/template.docx new file mode 100644 index 0000000..dec638c Binary files /dev/null and b/client/public/template.docx differ diff --git a/client/public/template_table.docx b/client/public/template_table.docx new file mode 100644 index 0000000..b88ddad Binary files /dev/null and b/client/public/template_table.docx differ diff --git a/client/public/test.png b/client/public/test.png new file mode 100644 index 0000000..1b0be22 Binary files /dev/null and b/client/public/test.png differ diff --git a/client/src/components/map/MapComponent.tsx b/client/src/components/map/MapComponent.tsx index 9a539b5..45aa24a 100644 --- a/client/src/components/map/MapComponent.tsx +++ b/client/src/components/map/MapComponent.tsx @@ -14,13 +14,13 @@ import { addInteractions, handleImageDrop, loadFeatures, processFigure, processL import useSWR, { SWRConfiguration } from 'swr' import { fetcher } from '../../http/axiosInstance' import { BASE_URL } from '../../constants' -import { ActionIcon, Autocomplete, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay, Stack, Container } from '@mantine/core' +import { ActionIcon, Autocomplete, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay, Stack, Container, Modal, Transition } from '@mantine/core' import { IconBoxMultiple, IconBoxPadding, IconChevronDown, IconPlus, IconSearch, IconUpload } from '@tabler/icons-react' import { ICitySettings, IFigure, ILine } from '../../interfaces/gis' import axios from 'axios' import MapToolbar from './MapToolbar/MapToolbar' import MapStatusbar from './MapStatusbar/MapStatusbar' -import { setAlignMode, setSatMapsProvider, setTypeRoles, useMapStore, setMapLabel } from '../../store/map' +import { setAlignMode, setSatMapsProvider, setTypeRoles, useMapStore, setMapLabel, clearPrintArea } from '../../store/map' import { useThrottle } from '@uidotdev/usehooks' import ObjectTree from '../Tree/ObjectTree' import { setCurrentObjectId, setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects' @@ -73,6 +73,7 @@ const MapComponent = ({ nodeLayerSource, drawingLayerSource, satLayer, staticMapLayer, figuresLayer, linesLayer, regionsLayer, districtBoundLayer, baseLayer, + printArea, printSource, printAreaDraw } = useMapStore().id[id] // Tab settings @@ -383,6 +384,17 @@ const MapComponent = ({ // } // }, [searchParams, figuresData, linesData]) + useEffect(() => { + if (selectedDistrict === null) { + setSelectedYear(id, null) + } + + if (selectedRegion === null) { + setSelectedYear(id, null) + setSelectedDistrict(id, null) + } + }, [selectedDistrict, selectedRegion, id]) + useEffect(() => { const districtBoundSource = districtBoundLayer.getSource() @@ -505,8 +517,42 @@ const MapComponent = ({ } }, [colorScheme]) + useEffect(() => { + if (map) { + if (mode === 'print') { + map.addInteraction(printAreaDraw) + } else { + map.removeInteraction(printAreaDraw) + } + } + + }, [mode, map, printAreaDraw]) + + useEffect(() => { + if (printArea) { + map?.setTarget('print-portal') + // map?.setView(new View({ + // extent: printArea + // })) + printSource.clear() + map?.getView().setCenter(getCenter(printArea)) + map?.getView().fit(printArea, { + size: [640, 320] + }) + } + }, [printArea, map]) + return ( <> + { + clearPrintArea(id) + map?.setTarget(mapElement.current as HTMLDivElement) + }} title="Предпросмотр области"> + + + {active && @@ -636,22 +682,40 @@ const MapComponent = ({ } + + + {(styles) => + + + + } + - + - + {selectedRegion && selectedDistrict && selectedYear && mode === 'edit' && } - - {selectedRegion && selectedDistrict && selectedYear && - - } + + + {(styles) => } + diff --git a/client/src/components/map/MapLegend/MapLegend.tsx b/client/src/components/map/MapLegend/MapLegend.tsx index 135a7ff..cc5a05d 100644 --- a/client/src/components/map/MapLegend/MapLegend.tsx +++ b/client/src/components/map/MapLegend/MapLegend.tsx @@ -1,14 +1,16 @@ -import { Accordion, ColorSwatch, Flex, ScrollAreaAutosize, Stack, Text, useMantineColorScheme } from '@mantine/core' +import { Accordion, ColorSwatch, Flex, MantineStyleProp, ScrollAreaAutosize, Stack, Text, useMantineColorScheme } from '@mantine/core' import useSWR from 'swr'; import { fetcher } from '../../../http/axiosInstance'; import { BASE_URL } from '../../../constants'; const MapLegend = ({ selectedDistrict, - selectedYear + selectedYear, + style }: { selectedDistrict: number | null, - selectedYear: number | null + selectedYear: number | null, + style: MantineStyleProp }) => { const { colorScheme } = useMantineColorScheme(); @@ -29,7 +31,7 @@ const MapLegend = ({ ) return ( - + Легенда diff --git a/client/src/components/map/MapMode.tsx b/client/src/components/map/MapMode.tsx index a772ac4..6d8fc4a 100644 --- a/client/src/components/map/MapMode.tsx +++ b/client/src/components/map/MapMode.tsx @@ -1,6 +1,6 @@ import { Center, SegmentedControl } from '@mantine/core' import { getMode, Mode, setMode } from '../../store/map' -import { IconEdit, IconEye } from '@tabler/icons-react' +import { IconEdit, IconEye, IconPrinter } from '@tabler/icons-react' const MapMode = ({ map_id @@ -25,6 +25,15 @@ const MapMode = ({ ), }, + { + value: 'print', + label: ( +
+ + Печать +
+ ), + }, ]} /> ) } diff --git a/client/src/components/map/mapUtils.ts b/client/src/components/map/mapUtils.ts index 94ec564..f3c526c 100644 --- a/client/src/components/map/mapUtils.ts +++ b/client/src/components/map/mapUtils.ts @@ -17,6 +17,7 @@ import { fromCircle, fromExtent } from "ol/geom/Polygon"; import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles"; import { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSnap, getTipPoint, getTranslate, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate } from "../../store/map"; import Collection from "ol/Collection"; +import { GeometryFunction, SketchCoordType } from "ol/interaction/Draw"; const calculateAngle = (coords: [number, number][]) => { const [start, end] = coords; @@ -341,9 +342,47 @@ export const updateImageSource = ( } }; +export const fixedAspectRatioBox: GeometryFunction = ( + coordinates: SketchCoordType, + geometry: SimpleGeometry | undefined, +): SimpleGeometry => { + // Ensure coordinates is an array of at least two points + if (!Array.isArray(coordinates) || coordinates.length < 2) { + return geometry ?? new Polygon([]); + } + + const [start, end] = coordinates as Coordinate[]; // Ensure it's a Coordinate array + const minX = start[0]; + const minY = start[1]; + const maxX = end[0]; + let maxY = end[1]; + + const width = maxX - minX; + const height = width / 2; // Enforce 2:1 aspect ratio + maxY = minY + height; + + // Define the rectangle's coordinates + const boxCoords: Coordinate[][] = [[ + [minX, minY], + [maxX, minY], + [maxX, maxY], + [minX, maxY], + [minX, minY], // Close the polygon + ]]; + + if (geometry) { + geometry.setCoordinates(boxCoords); + return geometry; + } + return new Polygon(boxCoords); +}; + + + export const addInteractions = ( map_id: string ) => { + console.log("Adding interactions") const currentTool = getCurrentTool(map_id) const clearPrevious = getMeasureClearPrevious(map_id) const measureType = getMeasureType(map_id) diff --git a/client/src/constants/app.tsx b/client/src/constants/app.tsx index afd5e49..a880d7b 100644 --- a/client/src/constants/app.tsx +++ b/client/src/constants/app.tsx @@ -152,10 +152,10 @@ const pages = [ component: , drawer: true, dashboard: true, - enabled: true, + enabled: false, }, { - label: "DB Manager", + label: "Тест БД", path: "/db-manager", icon: , component: , diff --git a/client/src/layouts/DashboardLayout.tsx b/client/src/layouts/DashboardLayout.tsx index 1647bcb..7535dad 100644 --- a/client/src/layouts/DashboardLayout.tsx +++ b/client/src/layouts/DashboardLayout.tsx @@ -9,7 +9,7 @@ import { pages } from '../constants/app'; function DashboardLayout() { const [mobileOpened, { toggle: toggleMobile }] = useDisclosure() - const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true) + const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(false) const navigate = useNavigate() const getPageTitle = () => { diff --git a/client/src/pages/MapTest.tsx b/client/src/pages/MapTest.tsx index 6fa2862..e6d61a6 100644 --- a/client/src/pages/MapTest.tsx +++ b/client/src/pages/MapTest.tsx @@ -17,12 +17,12 @@ function MapTest() { region: 11, district: 145, }, - { - id: uuidv4(), - year: 2023, - region: 11, - district: 146, - }, + // { + // id: uuidv4(), + // year: 2023, + // region: 11, + // district: 146, + // }, ] useEffect(() => { diff --git a/client/src/pages/PrintReport.tsx b/client/src/pages/PrintReport.tsx index a64a68e..5b658e0 100644 --- a/client/src/pages/PrintReport.tsx +++ b/client/src/pages/PrintReport.tsx @@ -1,4 +1,6 @@ import { Button, Flex } from "@mantine/core"; +import { useState } from "react"; +import createReport from 'docx-templates' const xslTemplate = ` @@ -904,9 +906,109 @@ const xslTemplate = ` ` const PrintReport = () => { - const handleGenerateExcel = () => { - // Define the example XML data - const xmlData = ` + const [loading, setLoading] = useState(false); + + const generateDocx = async () => { + setLoading(true); + + try { + // Fetch the DOCX template from the public folder + const response = await fetch("/template.docx"); + const response_table = await fetch("/template_table.docx"); + const templateArrayBuffer = await response.arrayBuffer(); + const templateArrayBuffer_table = await response_table.arrayBuffer(); + + // Convert ArrayBuffer to Uint8Array (Fix TypeScript error) + const templateUint8Array = new Uint8Array(templateArrayBuffer); + const templateUint8Array_table = new Uint8Array(templateArrayBuffer_table); + + // Fetch the image (Example: Load from public folder) + const imageResponse = await fetch("/test.png"); // Change this to your image path + const imageBlob = await imageResponse.blob(); + const imageArrayBuffer = await imageBlob.arrayBuffer(); + const imageUint8Array = new Uint8Array(imageArrayBuffer); + + // Generate the DOCX file with the replacement + const report = await createReport({ + template: templateUint8Array, // Ensure it's Uint8Array + data: { + test: "Hello World", + myImage: { + width: 6, // Width in cm + height: 6, // Height in cm + data: imageUint8Array, // Image binary data + extension: ".png", // Specify the image format + }, + }, + }); + + const report_table = await createReport({ + template: templateUint8Array_table, // Ensure it's Uint8Array + data: { + test: "Hello World", + rows: [ + { + first: 'A', + second: 'B', + third: 'C', + fourth: 'D', + }, + { + first: 'E', + second: 'F', + third: 'G', + fourth: 'H', + }, + { + first: 'I', + second: 'J', + third: 'K', + fourth: 'L', + } + ] + }, + }); + + // Convert Uint8Array to a Blob + const blob = new Blob([report], { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }); + + const blob_table = new Blob([report_table], { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }); + + // Create a download link and trigger the download + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "report.docx"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Create a download link and trigger the download + const url_table = URL.createObjectURL(blob_table); + const a_table = document.createElement("a"); + a_table.href = url_table; + a_table.download = "report_table.docx"; + document.body.appendChild(a_table); + a_table.click(); + document.body.removeChild(a_table); + + // Revoke the object URL after download + URL.revokeObjectURL(url); + URL.revokeObjectURL(url_table); + } catch (error) { + console.error("Error generating DOCX:", error); + } finally { + setLoading(false); + } + } + + const handleGenerateExcel = () => { + // Define the example XML data + const xmlData = ` 1 @@ -928,40 +1030,41 @@ const PrintReport = () => { `; - // Parse the XSL template and XML data - const parser = new DOMParser(); - const xslDoc = parser.parseFromString(xslTemplate, "application/xml"); - const xmlDoc = parser.parseFromString(xmlData, "application/xml"); + // Parse the XSL template and XML data + const parser = new DOMParser(); + const xslDoc = parser.parseFromString(xslTemplate, "application/xml"); + const xmlDoc = parser.parseFromString(xmlData, "application/xml"); - // Apply the transformation - const xsltProcessor = new XSLTProcessor(); - xsltProcessor.importStylesheet(xslDoc); - const resultDocument = xsltProcessor.transformToDocument(xmlDoc); + // Apply the transformation + const xsltProcessor = new XSLTProcessor(); + xsltProcessor.importStylesheet(xslDoc); + const resultDocument = xsltProcessor.transformToDocument(xmlDoc); - // Serialize the result to a string - const serializer = new XMLSerializer(); - const resultXml = serializer.serializeToString(resultDocument); + // Serialize the result to a string + const serializer = new XMLSerializer(); + const resultXml = serializer.serializeToString(resultDocument); - // Add missing Excel-specific headers if needed - const correctedXml = `\n` + resultXml + // Add missing Excel-specific headers if needed + const correctedXml = `\n` + resultXml - // Convert to Blob and trigger download - const blob = new Blob([correctedXml], { type: "application/vnd.ms-excel" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = "template.xls"; - link.click(); + // Convert to Blob and trigger download + const blob = new Blob([correctedXml], { type: "application/vnd.ms-excel" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "template.xls"; + link.click(); - // Clean up - URL.revokeObjectURL(url); - } + // Clean up + URL.revokeObjectURL(url); + } - return ( - - - - ) + return ( + + + + + ) } export default PrintReport \ No newline at end of file diff --git a/client/src/store/map.ts b/client/src/store/map.ts index 927e2ab..07b21c8 100644 --- a/client/src/store/map.ts +++ b/client/src/store/map.ts @@ -22,11 +22,11 @@ import { click, pointerMove } from 'ol/events/condition'; import { measureStyleFunction, modifyStyle } from '../components/map/Measure/MeasureStyles'; import MapBrowserEvent from 'ol/MapBrowserEvent'; import { transform } from 'ol/proj'; -import { applyTransformations, calculateTransformations, zoomToFeature } from '../components/map/mapUtils'; +import { applyTransformations, calculateTransformations, fixedAspectRatioBox, zoomToFeature } from '../components/map/mapUtils'; import { setCurrentObjectId, setSelectedRegion } from './objects'; import View from 'ol/View'; -export type Mode = 'edit' | 'view' +export type Mode = 'edit' | 'view' | 'print' interface MapState { id: Record; } @@ -206,6 +211,24 @@ export const initializeMapState = ( const alignModeLayer = new VectorLayer({ source: new VectorSource(), properties: { id: uuidv4(), type: 'align', name: 'Подгонка' } }) + + const printSource = new VectorSource() + const printLayer = new VectorLayer({ + source: printSource + }) + const printAreaDraw = new Draw({ + source: printSource, + type: 'Circle', + geometryFunction: fixedAspectRatioBox + }) + + printAreaDraw.on('drawend', (e) => { + const extent = e.feature.getGeometry()?.getExtent() + if (extent) { + setPrintArea(id, extent) + } + }) + const map = new Map({ controls: [], layers: [ @@ -222,7 +245,8 @@ export const initializeMapState = ( overlayLayer, nodeLayer, measureLayer, - alignModeLayer + alignModeLayer, + printLayer ] }) @@ -364,13 +388,38 @@ export const initializeMapState = ( nodeLayer: nodeLayer, overlayLayer: overlayLayer, regionSelect: regionSelect, - lineSelect: lineSelect + lineSelect: lineSelect, + printArea: null, + printLayer: printLayer, + printSource: printSource, + printAreaDraw: printAreaDraw, + printPreviewSize: [640, 320] } } } }) } +export const clearPrintArea = (id: string) => useMapStore.setState((state) => { + state.id[id].printSource.clear() + + return { + id: { + ...state.id, + [id]: { ...state.id[id], printArea: null } + } + } +}) + +export const setPrintArea = (id: string, extent: Extent | null) => useMapStore.setState((state) => { + return { + id: { + ...state.id, + [id]: { ...state.id[id], printArea: extent } + } + } +}) + export const getFiguresLayer = (id: string) => useMapStore.getState().id[id].figuresLayer export const getLinesLayer = (id: string) => useMapStore.getState().id[id].linesLayer export const getMeasureModify = (id: string) => useMapStore.getState().id[id].measureModify