pass aspect ratio to fixedAspectRatio; remove printAreaDraw after printArea is defined
This commit is contained in:
@ -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
|
||||
|
@ -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 >
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
48
client/src/constants/map.ts
Normal file
48
client/src/constants/map.ts
Normal 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],
|
||||
}
|
@ -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
23
client/src/store/print.ts
Normal 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 }))
|
Reference in New Issue
Block a user