pass aspect ratio to fixedAspectRatio; remove printAreaDraw after printArea is defined

This commit is contained in:
2025-03-07 16:50:54 +09:00
parent 0ca6c136e3
commit ada3b63b8d
6 changed files with 485 additions and 201 deletions

View File

@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { jsPDF } from "jspdf"
import 'ol/ol.css'
import { Modify } from 'ol/interaction'
import { ImageStatic, Vector as VectorSource } from 'ol/source'
@ -14,13 +15,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, Modal, Transition } from '@mantine/core'
import { IconBoxMultiple, IconBoxPadding, IconChevronDown, IconPlus, IconSearch, IconUpload } from '@tabler/icons-react'
import { ActionIcon, Autocomplete, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay, Stack, Container, Modal, Transition, Text, Select, Switch, Checkbox, Radio, ScrollAreaAutosize } from '@mantine/core'
import { IconArrowsMaximize, IconArrowsMinimize, IconBoxMultiple, IconBoxPadding, IconChevronCompactLeft, IconChevronDown, IconChevronLeft, IconChevronsDown, IconHelp, IconPlus, IconSearch, IconUpload, IconWindowMaximize, IconWindowMinimize } 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, clearPrintArea } from '../../store/map'
import { setAlignMode, setSatMapsProvider, setTypeRoles, useMapStore, setMapLabel, clearPrintArea, setPreviousView, setMode, setPrintScale, PrintScale, setPrintScaleLine } from '../../store/map'
import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree'
import { setCurrentObjectId, setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects'
@ -32,13 +33,11 @@ import GeoJSON from 'ol/format/GeoJSON'
import MapLegend from './MapLegend/MapLegend'
import GisService from '../../services/GisService'
import MapMode from './MapMode'
const satMapsProviders = [
{ label: 'Google', value: 'google' },
{ label: 'Яндекс', value: 'yandex' },
{ label: 'Подложка', value: 'custom' },
{ label: 'Static', value: 'static' }
]
import ScaleLine from 'ol/control/ScaleLine'
import { getPointResolution } from 'ol/proj'
import { PrintFormat, PrintOrientation, printResolutions, setPrintOrientation, setPrintResolution, usePrintStore } from '../../store/print'
import { printDimensions, satMapsProviders, scaleOptions, schemas } from '../../constants/map'
import { modals } from "@mantine/modals";
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false
@ -73,9 +72,11 @@ const MapComponent = ({
nodeLayerSource, drawingLayerSource,
satLayer, staticMapLayer, figuresLayer, linesLayer,
regionsLayer, districtBoundLayer, baseLayer,
printArea, printSource, printAreaDraw
previousView, printArea, printSource, printAreaDraw, printScale, printScaleLine,
} = useMapStore().id[id]
const { printOrientation, printResolution, printFormat } = usePrintStore()
// Tab settings
const objectsPane: ITabsPane[] = [{ title: 'Объекты', value: 'objects', view: <ObjectTree map_id={id} /> }, { title: 'Неразмещенные', value: 'unplaced', view: <></> }, { title: 'Другие', value: 'other', view: <></> },]
const paramsPane: ITabsPane[] = [{ title: 'История изменений', value: 'history', view: <></> }, { title: 'Параметры', value: 'parameters', view: <ObjectParameters map_id={id} /> }, { title: 'Вычисляемые', value: 'calculated', view: <></> }]
@ -180,7 +181,6 @@ const MapComponent = ({
zIndex: '1',
backgroundColor: colorScheme === 'light' ? '#F0F0F0CC' : '#000000CC',
backdropFilter: 'blur(8px)',
border: '1px solid #00000022'
}
const { data: nodes } = useSWR('/nodes/all', () => fetcher('/nodes/all', BASE_URL.ems), { revalidateOnFocus: false })
@ -205,18 +205,18 @@ const MapComponent = ({
}, [nodes, nodeLayerSource])
const [searchObject, setSearchObject] = useState<string | undefined>("")
const throttledSearchObject = useThrottle(searchObject, 500);
const throttledSearchObject = useThrottle(searchObject, 500)
useEffect(() => {
if (!selectedObjectType || !map) return;
if (!selectedObjectType || !map) return
if (figuresLayer) {
// Reset styles and apply highlight to matching features
figuresLayer.getSource()?.getFeatures().forEach((feature) => {
if (selectedObjectType == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
feature.setStyle(highlightStyleYellow)
} else {
feature.setStyle(undefined); // Reset to default style
feature.setStyle(undefined) // Reset to default style
}
})
}
@ -225,9 +225,9 @@ const MapComponent = ({
// Reset styles and apply highlight to matching features
linesLayer.getSource()?.getFeatures().forEach((feature) => {
if (selectedObjectType == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
feature.setStyle(highlightStyleYellow)
} else {
feature.setStyle(undefined); // Reset to default style
feature.setStyle(undefined) // Reset to default style
}
})
}
@ -248,7 +248,7 @@ const MapComponent = ({
}
} else {
feature.setStyle(undefined); // Reset to default style
feature.setStyle(undefined) // Reset to default style
}
})
}
@ -257,14 +257,14 @@ const MapComponent = ({
// Reset styles and apply highlight to matching features
linesLayer.getSource()?.getFeatures().forEach((feature: Feature) => {
if (currentObjectId == feature.get('object_id')) {
feature.setStyle(highlightStyleRed);
feature.setStyle(highlightStyleRed)
const geometry = feature.getGeometry()
if (geometry) {
map?.getView().fit(geometry as SimpleGeometry, { duration: 500, maxZoom: 18 })
}
} else {
feature.setStyle(undefined); // Reset to default style
feature.setStyle(undefined) // Reset to default style
}
})
}
@ -311,79 +311,6 @@ const MapComponent = ({
}
}, [districtsData, id, selectedDistrict, selectedYear])
//const [searchParams, setSearchParams] = useSearchParams()
// useEffect(() => {
// if (selectedRegion) {
// setSearchParams((params) => {
// params.set('r', selectedRegion.toString());
// return params
// })
// }
// if (selectedDistrict) {
// setSearchParams((params) => {
// params.set('d', selectedDistrict?.toString());
// return params
// })
// }
// if (selectedYear) {
// setSearchParams((params) => {
// params.set('y', selectedYear?.toString());
// return params
// })
// }
// if (currentObjectId) {
// setSearchParams((params) => {
// params.set('o', currentObjectId?.toString());
// return params
// })
// }
// }, [selectedRegion, selectedDistrict, selectedYear, currentObjectId, setSearchParams])
// useEffect(() => {
// if (Array.isArray(regionsData)) {
// const region = searchParams.get('r')
// if (searchParams.get('r')) {
// setSelectedRegion(Number(region))
// }
// }
// }, [searchParams, regionsData])
// useEffect(() => {
// if (Array.isArray(regionsData)) {
// const district = searchParams.get('d')
// if (Array.isArray(districtsData)) {
// if (district) {
// setSelectedDistrict(Number(district))
// }
// }
// }
// }, [searchParams, regionsData, districtsData])
// useEffect(() => {
// if (Array.isArray(regionsData)) {
// const year = searchParams.get('y')
// if (year) {
// setSelectedYear(Number(year))
// }
// }
// }, [searchParams, regionsData])
// useEffect(() => {
// const object = searchParams.get('o')
// if (figuresData && linesData && object) {
// setCurrentObjectId(object)
// }
// }, [searchParams, figuresData, linesData])
useEffect(() => {
if (selectedDistrict === null) {
setSelectedYear(id, null)
@ -395,6 +322,8 @@ const MapComponent = ({
}
}, [selectedDistrict, selectedRegion, id])
const [leftPaneHidden, setLeftPaneHidden] = useState(false)
useEffect(() => {
const districtBoundSource = districtBoundLayer.getSource()
@ -456,9 +385,9 @@ const MapComponent = ({
if (selectedDistrict && districtData) {
const settings = getCitySettings()
const imageUrl = `${import.meta.env.VITE_API_EMS_URL}/static/${selectedDistrict}`;
const img = new Image();
img.src = imageUrl;
const imageUrl = `${import.meta.env.VITE_API_EMS_URL}/static/${selectedDistrict}`
const img = new Image()
img.src = imageUrl
img.onload = () => {
if (map) {
const width = img.naturalWidth
@ -469,25 +398,25 @@ const MapComponent = ({
const wk = width * k
const hk = height * k
const center = [settings.offset_x + (wk), settings.offset_y - (hk)];
const center = [settings.offset_x + (wk), settings.offset_y - (hk)]
const extent = [
center[0] - (wk),
center[1] - (hk),
center[0] + (wk),
center[1] + (hk),
];
]
// Set up the initial image layer with the extent
const imageSource = new ImageStatic({
url: imageUrl,
imageExtent: extent,
});
staticMapLayer.setSource(imageSource);
})
staticMapLayer.setSource(imageSource)
//map.current.addLayer(imageLayer.current);
//map.current.addLayer(imageLayer.current)
}
};
}
}
}, [selectedDistrict, districtData, staticMapLayer])
@ -496,27 +425,33 @@ const MapComponent = ({
baseLayer.on('prerender', function (e) {
if (colorScheme === 'dark') {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'grayscale(80%) invert(100%) hue-rotate(180deg) ';
context.globalCompositeOperation = 'source-over';
const context = e.context as CanvasRenderingContext2D
context.filter = 'grayscale(80%) invert(100%) hue-rotate(180deg) '
context.globalCompositeOperation = 'source-over'
}
} else {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'none';
const context = e.context as CanvasRenderingContext2D
context.filter = 'none'
}
}
})
baseLayer.on('postrender', function (e) {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'none';
const context = e.context as CanvasRenderingContext2D
context.filter = 'none'
}
})
}
}, [colorScheme])
const scaleLine = useRef(new ScaleLine({
bar: true,
text: true,
minWidth: 125
}))
useEffect(() => {
if (map) {
if (mode === 'print') {
@ -525,41 +460,199 @@ const MapComponent = ({
map.removeInteraction(printAreaDraw)
}
}
}, [mode, map, printAreaDraw])
useEffect(() => {
if (printArea) {
// backup view before entering print mode
setPreviousView(id, map?.getView())
map?.setTarget('print-portal')
// map?.setView(new View({
// extent: printArea
// }))
printSource.clear()
map?.getView().setCenter(getCenter(printArea))
map?.getView().fit(printArea, {
size: [640, 320]
size: printOrientation === 'horizontal' ? [594, 420] : [420, 594]
})
map?.removeInteraction(printAreaDraw)
}
}, [printArea, map])
const exportToPDF = (format: PrintFormat, resolution: number, orientation: PrintOrientation) => {
const dim = printDimensions[format]
const width = Math.round((dim[orientation === 'horizontal' ? 0 : 1] * resolution) / 25.4)
const height = Math.round((dim[orientation === 'horizontal' ? 1 : 0] * resolution) / 25.4)
if (!map) return
// Store original size and scale
const originalSize = map.getSize()
const originalResolution = map.getView().getResolution()
if (!originalSize || !originalResolution) return
// Calculate new resolution to fit high DPI
const scaleFactor = width / originalSize[0]
const newResolution = originalResolution / scaleFactor
// console.log(`New resolution: ${newResolution}`)
const center = map.getView().getCenter()
let scaleResolution
if (center) {
scaleResolution = Number(printScale) / getPointResolution(map.getView().getProjection(), Number(resolution) / 25.4, center)
// console.log(`Scaled resolution: ${scaleResolution}`)
}
console.log(width, height)
// Set new high-resolution rendering
map.setSize([width, height])
map.getView().setResolution(newResolution)
map.renderSync()
map.once("rendercomplete", function () {
const mapCanvas = document.createElement("canvas")
mapCanvas.width = width
mapCanvas.height = height
const mapContext = mapCanvas.getContext("2d")
if (!mapContext) return
const canvas = document.querySelector('canvas')
if (canvas) {
if (canvas.width > 0) {
const opacity = canvas.parentElement?.style.opacity || "1"
mapContext.globalAlpha = parseFloat(opacity)
const transform = canvas.style.transform
const matrixMatch = transform.match(/^matrix\(([^)]+)\)$/)
if (matrixMatch) {
const matrix = matrixMatch[1].split(",").map(Number)
mapContext.setTransform(...matrix as [number, number, number, number, number, number])
}
mapContext.drawImage(canvas, 0, 0)
}
}
mapContext.globalAlpha = 1
mapContext.setTransform(1, 0, 0, 1, 0, 0)
// Restore original map settings
map.setSize(originalSize)
map.getView().setResolution(originalResolution)
map.renderSync()
// Generate PDF
const pdf = new jsPDF(orientation === 'horizontal' ? "landscape" : 'portrait', undefined, format)
pdf.addImage(mapCanvas.toDataURL("image/jpeg"), "JPEG", 0, 0, orientation === 'horizontal' ? dim[0] : dim[1], orientation === 'horizontal' ? dim[1] : dim[0])
const filename = `${selectedYear}-${selectedRegion}-${selectedDistrict}-${new Date().toISOString()}.pdf`
pdf.save(filename, {
returnPromise: true
}).then(() => {
})
})
}
useEffect(() => {
if (printScaleLine) {
map?.addControl(scaleLine.current)
} else {
map?.removeControl(scaleLine.current)
}
}, [printScaleLine])
const [fullscreen, setFullscreen] = useState(false)
return (
<>
<Modal keepMounted size='auto' opened={!!printArea} onClose={() => {
<Modal.Root scrollAreaComponent={ScrollAreaAutosize} keepMounted size='auto' opened={!!printArea} onClose={() => {
clearPrintArea(id)
map?.setTarget(mapElement.current as HTMLDivElement)
}} title="Предпросмотр области">
<Stack>
<div id='print-portal' style={{ width: '640px', height: '320px' }}>
map?.addInteraction(printAreaDraw)
}} fullScreen={fullscreen}>
<Modal.Overlay />
<Modal.Content style={{ transition: 'all .3s ease' }}>
<Modal.Header>
<Modal.Title>
Предпросмотр области печати
</Modal.Title>
</div>
<Flex gap='sm'>
<Button>
Печать
</Button>
</Flex>
<Flex ml='auto' gap='md'>
<ActionIcon title='Помощь' ml='auto' variant='transparent'>
<IconHelp color='gray' />
</ActionIcon>
<ActionIcon title={fullscreen ? 'Свернуть' : 'Развернуть'} variant='transparent' onClick={() => setFullscreen(!fullscreen)}>
{fullscreen ? <IconWindowMinimize color='gray' /> : <IconWindowMaximize color='gray' />}
</ActionIcon>
<Modal.CloseButton title='Закрыть' />
</Flex>
</Modal.Header>
<Modal.Body>
<Stack align='center'>
<Text w='100%'>Область печати можно передвигать.</Text>
</Stack>
</Modal>
<div id='print-portal' style={{
width: printOrientation === 'horizontal' ? '594px' : '420px',
height: printOrientation === 'horizontal' ? '420px' : '594px'
}}>
</div>
<Flex w='100%' wrap='wrap' gap='lg' justify='space-between'>
<Radio.Group
label='Ориентация'
value={printOrientation}
onChange={(value) => setPrintOrientation(value as PrintOrientation)}
>
<Stack>
<Radio value='horizontal' label='Горизонтальная' />
<Radio value='vertical' label='Вертикальная' />
</Stack>
</Radio.Group>
<Select
allowDeselect={false}
label="Разрешение"
placeholder="Выберите разрешение"
data={printResolutions}
value={printResolution.toString()}
onChange={(value) => setPrintResolution(Number(value))}
/>
<Select
allowDeselect={false}
label="Масштаб"
placeholder="Выберите масштаб"
data={scaleOptions}
value={printScale}
onChange={(value) => setPrintScale(id, value as PrintScale)}
/>
<Checkbox
checked={printScaleLine}
label="Масштабная линия"
onChange={(event) => setPrintScaleLine(id, event.currentTarget.checked)}
/>
</Flex>
<Flex w='100%' gap='sm' align='center'>
<Button ml='auto' onClick={() => {
if (previousView) {
exportToPDF(printFormat, printResolution, printOrientation)
}
}}>
Печать
</Button>
</Flex>
</Stack>
</Modal.Body>
</Modal.Content>
</Modal.Root>
{active &&
<Portal target='#header-portal'>
@ -594,13 +687,7 @@ const MapComponent = ({
data={regionsData ? regionsData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []}
onChange={(value) => setSelectedRegion(id, Number(value))}
clearable
onClear={() => {
setSelectedRegion(id, null)
// setSearchParams((params) => {
// params.delete('r')
// return params
// })
}}
onClear={() => setSelectedRegion(id, null)}
searchable
value={selectedRegion ? selectedRegion.toString() : null}
/>
@ -611,19 +698,13 @@ const MapComponent = ({
data={districtsData ? districtsData.map((item: { name: string, id: number, district_name: string }) => ({ label: [item.name, item.district_name].join(' - '), value: item.id.toString() })) : []}
onChange={(value) => setSelectedDistrict(id, Number(value))}
clearable
onClear={() => {
setSelectedDistrict(id, null)
// setSearchParams((params) => {
// params.delete('d')
// return params
// })
}}
onClear={() => { setSelectedDistrict(id, null) }}
searchable
value={selectedDistrict ? selectedDistrict.toString() : null}
/>
<MantineSelect placeholder='Схема' w='92px'
data={['2018', '2019', '2020', '2021', '2022', '2023', '2024'].map(el => ({ label: el, value: el }))}
data={schemas.map(el => ({ label: el, value: el }))}
onChange={(e) => {
if (e) {
setSelectedYear(id, Number(e))
@ -631,13 +712,7 @@ const MapComponent = ({
setSelectedYear(id, null)
}
}}
onClear={() => {
setSelectedYear(id, null)
// setSearchParams((params) => {
// params.delete('y')
// return params
// })
}}
onClear={() => setSelectedYear(id, null)}
value={selectedYear ? selectedYear?.toString() : null}
clearable
/>
@ -682,19 +757,22 @@ const MapComponent = ({
<Container pos='absolute' w='100%' h='100%' p='0' fluid>
<Flex direction='column' w='100%' h='100%'>
<Flex w='100%' h='94%' p='xs' style={{ flexGrow: 1 }}>
<Stack w='100%' maw='340px'>
<Transition
mounted={!!selectedRegion && !!selectedDistrict && !!selectedYear}
transition="slide-right"
duration={200}
timingFunction="ease"
>
{(styles) => <Flex direction='column' h={'100%'} w={'100%'} style={{ ...mapControlsStyle, ...styles }}>
<TabsPane defaultTab='objects' tabs={objectsPane} />
<Divider />
<TabsPane defaultTab='parameters' tabs={paramsPane} />
</Flex>}
</Transition>
<Stack w='100%' maw='380px'>
<Flex w='100%' h='100%' gap='xs'>
{selectedRegion && selectedDistrict && selectedYear &&
<Flex direction='column' h={'100%'} w={leftPaneHidden ? '0px' : '100%'} style={{ ...mapControlsStyle, transition: 'width .3s ease' }}>
<TabsPane defaultTab='objects' tabs={objectsPane} />
<Divider />
<TabsPane defaultTab='parameters' tabs={paramsPane} />
</Flex>
}
{!!selectedRegion && !!selectedDistrict && !!selectedYear &&
<Button p='0' variant='subtle' w='32' style={{ zIndex: '1' }} onClick={() => setLeftPaneHidden(!leftPaneHidden)}>
<IconChevronLeft size={16} style={{ transform: `${leftPaneHidden ? 'rotate(180deg)' : ''}` }} />
</Button>
}
</Flex>
</Stack>
<Stack w='100%' align='center'>
@ -735,6 +813,6 @@ const MapComponent = ({
<LoadingOverlay visible={linesValidating || figuresValidating} />
</>
)
};
}
export default MapComponent

View File

@ -1,40 +1,106 @@
import { Center, SegmentedControl } from '@mantine/core'
import { getMode, Mode, setMode } from '../../store/map'
import { IconEdit, IconEye, IconPrinter } from '@tabler/icons-react'
import { Button, Flex, FloatingIndicator, Popover, SegmentedControl } from '@mantine/core'
import { Mode, setMode, useMapStore } from '../../store/map'
import { IconChevronDown, IconCropLandscape, IconCropPortrait, IconEdit, IconEye, IconPrinter } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
import { PrintOrientation, setPrintOrientation, usePrintStore } from '../../store/print'
const MapMode = ({
map_id
}: { map_id: string }) => {
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
const { mode } = useMapStore().id[map_id]
const setControlRef = (item: Mode) => (node: HTMLButtonElement) => {
controlsRefs[item] = node;
setControlsRefs(controlsRefs);
}
const { printOrientation } = usePrintStore()
useEffect(() => {
}, [printOrientation])
return (
<SegmentedControl value={getMode(map_id)} onChange={(value) => setMode(map_id, value as Mode)} color="blue" w='auto' data={[
{
value: 'view',
label: (
<Center style={{ gap: 10 }}>
<IconEye size={16} />
<span>Просмотр</span>
</Center>
),
},
{
value: 'edit',
label: (
<Center style={{ gap: 10 }}>
<IconEdit size={16} />
<span>Редактирование</span>
</Center>
),
},
{
value: 'print',
label: (
<Center style={{ gap: 10 }}>
<IconPrinter size={16} />
<span>Печать</span>
</Center>
),
},
]} />
<Flex ref={setRootRef} p={4} gap={4}>
<Button
variant={mode === 'view' ? 'filled' : 'subtle'}
key={'view'}
ref={setControlRef('view' as Mode)}
onClick={() => {
setMode(map_id, 'view' as Mode)
}}
leftSection={<IconEye size={16} />}
mod={{ active: mode === 'view' as Mode }}
>
Просмотр
</Button>
<Button
variant={mode === 'edit' ? 'filled' : 'subtle'}
key={'edit'}
ref={setControlRef('edit' as Mode)}
onClick={() => {
setMode(map_id, 'edit' as Mode)
}}
leftSection={<IconEdit size={16} />}
mod={{ active: mode === 'edit' as Mode }}
>
Редактирование
</Button>
<Popover width='auto' position='bottom-end' >
<Popover.Target>
<Button.Group>
<Button
variant={mode === 'print' ? 'filled' : 'subtle'}
key={'print'}
ref={setControlRef('print' as Mode)}
onClick={(e) => {
e.stopPropagation()
setMode(map_id, 'print' as Mode)
}}
leftSection={<IconPrinter size={16} />}
mod={{ active: mode === 'print' as Mode }}
>
Печать
</Button>
<Button variant={mode === 'print' ? 'filled' : 'subtle'} w='auto' p={8} title='Ориентация'>
<IconChevronDown size={16} />
</Button>
</Button.Group>
</Popover.Target>
<Popover.Dropdown p={0} style={{ display: 'flex' }}>
<SegmentedControl
color='blue'
value={printOrientation}
onChange={(value) => {
setPrintOrientation(value as PrintOrientation)
setMode(map_id, 'print' as Mode)
}}
data={[
{
value: 'horizontal',
label: (
<IconCropLandscape title='Горизонтальная' style={{ display: 'block' }} size={20} />
),
},
{
value: 'vertical',
label: (
<IconCropPortrait title='Вертикальная' style={{ display: 'block' }} size={20} />
),
},
]}
/>
</Popover.Dropdown>
</Popover>
<FloatingIndicator target={controlsRefs[mode]} parent={rootRef} />
</Flex >
)
}

View File

@ -15,9 +15,9 @@ import { ImageStatic } from "ol/source";
import { IFigure, ILine } from "../../interfaces/gis";
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 { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSnap, getTipPoint, getTranslate, PrintOrientation, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate } from "../../store/map";
import Collection from "ol/Collection";
import { GeometryFunction, SketchCoordType } from "ol/interaction/Draw";
import { SketchCoordType } from "ol/interaction/Draw";
const calculateAngle = (coords: [number, number][]) => {
const [start, end] = coords;
@ -342,9 +342,10 @@ export const updateImageSource = (
}
};
export const fixedAspectRatioBox: GeometryFunction = (
export const fixedAspectRatioBox = (
coordinates: SketchCoordType,
geometry: SimpleGeometry | undefined,
orientation?: PrintOrientation
): SimpleGeometry => {
if (!Array.isArray(coordinates) || coordinates.length < 2) {
return geometry ?? new Polygon([]);
@ -356,7 +357,7 @@ export const fixedAspectRatioBox: GeometryFunction = (
const maxX = end[0];
const width = maxX - minX;
const height = Math.abs(width / 2)
const height = orientation === 'horizontal' ? Math.abs(width / Math.sqrt(2)) : Math.abs(width * Math.sqrt(2)); // Maintain 1:√2 ratio
const maxY = end[1] > minY ? minY + height : minY - height;

View File

@ -0,0 +1,48 @@
export const schemas = [
'2018',
'2019',
'2020',
'2021',
'2022',
'2023',
'2024',
]
export const scaleOptions = [
{
label: '1:500000',
value: '500'
},
{
label: '1:100000',
value: '250'
},
{
label: '1:50000',
value: '50'
},
{
label: '1:25000',
value: '25'
},
{
label: '1:10000',
value: '10'
},
]
export const satMapsProviders = [
{ label: 'Google', value: 'google' },
{ label: 'Яндекс', value: 'yandex' },
{ label: 'Подложка', value: 'custom' },
{ label: 'Static', value: 'static' }
]
export const printDimensions = {
a0: [1189, 841],
a1: [841, 594],
a2: [594, 420],
a3: [420, 297],
a4: [297, 210],
a5: [210, 148],
}

View File

@ -25,9 +25,14 @@ import { transform } from 'ol/proj';
import { applyTransformations, calculateTransformations, fixedAspectRatioBox, zoomToFeature } from '../components/map/mapUtils';
import { setCurrentObjectId, setSelectedRegion } from './objects';
import View from 'ol/View';
import { getPrintOrientation } from './print';
export type Mode = 'edit' | 'view' | 'print'
export type PrintScale = '500' | '250' | '50' | '25' | '10'
export type PrintOrientation = 'horizontal' | 'vertical'
interface MapState {
id: Record<string, {
currentTool: ToolType;
@ -77,11 +82,17 @@ interface MapState {
overlayLayer: VectorLayer;
regionSelect: Select;
lineSelect: Select;
previousView: View | undefined | null;
printArea: Extent | null;
printLayer: VectorLayer;
printSource: VectorSource;
printAreaDraw: Draw;
printPreviewSize: number[];
printDim: 'a0' | 'a1' | 'a2' | 'a3' | 'a4' | 'a5';
printRes: number;
printOrientation: PrintOrientation;
printScale: PrintScale;
printScaleLine: boolean;
}>;
}
@ -224,7 +235,22 @@ export const initializeMapState = (
const printAreaDraw = new Draw({
source: printSource,
type: 'Circle',
geometryFunction: fixedAspectRatioBox
style: (feature) => {
return new Style({
fill: new Fill({
color: "rgba(0, 0, 255, 0.3)", // Semi-transparent blue fill
}),
stroke: new Stroke({
color: "blue",
width: 2,
}),
image: undefined, // 🚀 This removes the default point!
});
},
geometryFunction: function (coords, geom) {
const printOrientation = getPrintOrientation()
return fixedAspectRatioBox(coords, geom, printOrientation)
},
})
printAreaDraw.on('drawend', (e) => {
@ -396,17 +422,59 @@ export const initializeMapState = (
overlayLayer: overlayLayer,
regionSelect: regionSelect,
lineSelect: lineSelect,
previousView: null,
printArea: null,
printLayer: printLayer,
printSource: printSource,
printAreaDraw: printAreaDraw,
printPreviewSize: [640, 320]
printPreviewSize: [640, 320],
printDim: 'a4',
printOrientation: 'horizontal',
printRes: 72,
printScale: '250',
printScaleLine: true
}
}
}
})
}
export const setPrintOrientation = (id: string, orientation: PrintOrientation) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printOrientation: orientation }
}
}
})
export const setPrintScaleLine = (id: string, bool: boolean) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printScaleLine: bool }
}
}
})
export const setPrintScale = (id: string, printScale: PrintScale) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], printScale: printScale }
}
}
})
export const setPreviousView = (id: string, view: View | undefined | null) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], previousView: view }
}
}
})
export const clearPrintArea = (id: string) => useMapStore.setState((state) => {
state.id[id].printSource.clear()

23
client/src/store/print.ts Normal file
View File

@ -0,0 +1,23 @@
import { create } from 'zustand';
export type PrintOrientation = 'horizontal' | 'vertical'
export type PrintFormat = 'a0' | 'a1' | 'a2' | 'a3' | 'a4' | 'a5'
export const printResolutions = ['72', '150', '200', '300']
export interface PrintState {
printFormat: PrintFormat;
printOrientation: PrintOrientation;
printResolution: number;
}
export const usePrintStore = create<PrintState>(() => ({
printFormat: 'a4',
printOrientation: 'horizontal',
printResolution: 72
}))
export const getPrintOrientation = () => usePrintStore.getState().printOrientation
export const setPrintFormat = (format: PrintFormat) => usePrintStore.setState(() => ({ printFormat: format }))
export const setPrintOrientation = (orientation: PrintOrientation) => usePrintStore.setState(() => ({ printOrientation: orientation }))
export const setPrintResolution = (resolution: number) => usePrintStore.setState(() => ({ printResolution: resolution }))