Files
universal_is/client/src/components/map/MapPrint/MapPrint.tsx

234 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { IconHelp, IconWindowMaximize, IconWindowMinimize } from '@tabler/icons-react'
import React, { useEffect, useRef, useState } from 'react'
import { clearPrintArea, PrintScale, setPreviousView, setPrintScale, setPrintScaleLine, useMapStore } from '../../../store/map'
import { PrintFormat, PrintOrientation, printResolutions, setPrintOrientation, setPrintResolution, usePrintStore } from '../../../store/print'
import { printDimensions, scaleOptions } from '../../../constants/map'
import { useObjectsStore } from '../../../store/objects'
import jsPDF from 'jspdf'
import { getCenter } from 'ol/extent'
import ScaleLine from 'ol/control/ScaleLine'
import { Button, Checkbox, Dialog, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, Dropdown, Field, Option, Radio, RadioGroup } from '@fluentui/react-components'
import { Dismiss24Regular } from '@fluentui/react-icons'
const MapPrint = ({
id,
mapElement
}: {
id: string
mapElement: React.MutableRefObject<HTMLDivElement | null>
}) => {
const [fullscreen, setFullscreen] = useState(false)
const { printOrientation, printResolution, printFormat } = usePrintStore()
const { selectedYear, selectedRegion, selectedDistrict } = useObjectsStore().id[id]
const {
map,
mode,
previousView, printArea, printSource, printAreaDraw, printScale, printScaleLine,
} = useMapStore().id[id]
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
// 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()
const dimensions = {
w: orientation === 'horizontal' ? dim[0] : dim[1],
h: orientation === 'horizontal' ? dim[1] : dim[0]
}
// Generate PDF
const pdf = new jsPDF(orientation === 'horizontal' ? "landscape" : 'portrait', undefined, format)
pdf.addImage(mapCanvas.toDataURL("image/jpeg"), "JPEG", 0, 0, dimensions.w, dimensions.h)
const filename = `${selectedYear}-${selectedRegion}-${selectedDistrict}-${new Date().toISOString()}.pdf`
pdf.save(filename, {
returnPromise: true
}).then(() => {
})
})
}
const scaleLine = useRef(new ScaleLine({
bar: true,
text: true,
minWidth: 125
}))
const [opened, setOpened] = useState(false)
useEffect(() => {
if (printArea && opened) {
// backup view before entering print mode
setPreviousView(id, map?.getView())
map?.setTarget('print-portal')
printSource.clear()
map?.getView().setCenter(getCenter(printArea))
map?.getView().fit(printArea, {
size: printOrientation === 'horizontal' ? [594, 420] : [420, 594]
})
map?.removeInteraction(printAreaDraw)
}
}, [printArea, map, opened])
useEffect(() => {
if (printScaleLine && printArea) {
map?.addControl(scaleLine.current)
} else {
map?.removeControl(scaleLine.current)
}
}, [printScaleLine, printArea])
useEffect(() => {
if (!!printArea) {
setOpened(true)
}
}, [printArea])
useEffect(() => {
if (!opened && mode === 'print') {
clearPrintArea(id)
map?.setTarget(mapElement.current as HTMLDivElement)
map?.addInteraction(printAreaDraw)
}
}, [opened])
return (
<Dialog open={opened}>
<DialogSurface style={{ maxWidth: fullscreen ? '100%' : 'fit-content', maxHeight: fullscreen ? '100%' : 'fit-content' }}>
<DialogBody>
<DialogTitle action={
<div style={{ display: 'flex', marginLeft: 'auto', gap: '1.5rem' }}>
<Button appearance='subtle' title='Помощь' style={{ marginLeft: 'auto' }} icon={<IconHelp color='gray' />} />
<Button appearance='subtle' title={fullscreen ? 'Свернуть' : 'Развернуть'} style={{ marginLeft: 'auto' }} icon={fullscreen ? <IconWindowMinimize color='gray' /> : <IconWindowMaximize color='gray' />} onClick={() => setFullscreen(!fullscreen)} />
<Button appearance='subtle' title='Закрыть' icon={<Dismiss24Regular />} onClick={() => setOpened(false)} />
</div>
}>Предпросмотр области печати</DialogTitle>
<DialogContent style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ display: 'flex', width: 'fit-content', flexDirection: 'row', alignItems: 'flex-start', height: 'fit-content', overflowY: 'auto' }}>
<div id='print-portal' style={{
width: printOrientation === 'horizontal' ? '594px' : '420px',
height: printOrientation === 'horizontal' ? '420px' : '594px',
flexShrink: '0'
}}>
</div>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', flexWrap: 'wrap', gap: '1rem', padding: '1rem', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Field label={'Ориентация'}>
<RadioGroup value={printOrientation} onChange={(_, data) => setPrintOrientation(data.value as PrintOrientation)}>
<Radio value='horizontal' label='Горизонтальная' />
<Radio value='vertical' label='Вертикальная' />
</RadioGroup>
</Field>
<Field label="Разрешение">
<Dropdown
value={printResolution.toString()}
selectedOptions={[printResolution.toString()]}
onOptionSelect={(_, data) => setPrintResolution(Number(data.optionValue))}
>
{printResolutions.map((res) => (
<Option key={res} text={res} value={res}>
{res}
</Option>
))}
</Dropdown>
</Field>
<Field label="Масштаб">
<Dropdown
value={printScale.toString()}
selectedOptions={[printScale]}
onOptionSelect={(_, data) => setPrintScale(id, data.optionValue as PrintScale)}
>
{scaleOptions.map((opt) => (
<Option key={opt.value} text={opt.label} value={opt.value}>
{opt.label}
</Option>
))}
</Dropdown>
</Field>
</div>
</div>
</DialogContent>
<DialogActions>
<Checkbox
checked={printScaleLine}
label="Масштабная линия"
onChange={(event) => setPrintScaleLine(id, event.currentTarget.checked)}
/>
<Button style={{ marginLeft: 'auto' }} onClick={() => {
if (previousView) {
exportToPDF(printFormat, printResolution, printOrientation)
}
}}>
Печать
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
)
}
export default MapPrint