v1.0.3: Fix PDF footer overlap, arrow alignment, screenshot quality, arrowheads in export
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lageplan",
|
"name": "lageplan",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1130,6 +1130,32 @@ export default function AppPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw arrowheads for arrow features
|
||||||
|
for (const f of currentFeatures.filter(f => f.type === 'arrow')) {
|
||||||
|
if (f.geometry.type !== 'LineString') continue
|
||||||
|
const lineCoords = f.geometry.coordinates as number[][]
|
||||||
|
if (lineCoords.length < 2) continue
|
||||||
|
const p1 = lineCoords[lineCoords.length - 2]
|
||||||
|
const p2 = lineCoords[lineCoords.length - 1]
|
||||||
|
const px1 = mapInstance.project(p1 as [number, number])
|
||||||
|
const px2 = mapInstance.project(p2 as [number, number])
|
||||||
|
const angle = Math.atan2(px2.y - px1.y, px2.x - px1.x)
|
||||||
|
const color = (f.properties.color as string) || '#000000'
|
||||||
|
const arrowSize = 14 * dpr
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(px2.x * dpr, px2.y * dpr)
|
||||||
|
ctx.rotate(angle + Math.PI / 2)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(0, -arrowSize)
|
||||||
|
ctx.lineTo(-arrowSize * 0.7, arrowSize * 0.3)
|
||||||
|
ctx.lineTo(arrowSize * 0.7, arrowSize * 0.3)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.fill()
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
// Draw line/polygon label markers at midpoints
|
// Draw line/polygon label markers at midpoints
|
||||||
for (const f of currentFeatures.filter(f => f.properties.label && (f.geometry.type === 'LineString' || f.geometry.type === 'Polygon'))) {
|
for (const f of currentFeatures.filter(f => f.properties.label && (f.geometry.type === 'LineString' || f.geometry.type === 'Polygon'))) {
|
||||||
const label = f.properties.label as string
|
const label = f.properties.label as string
|
||||||
|
|||||||
@@ -1030,8 +1030,8 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
|
|||||||
if (mapRef?.current) {
|
if (mapRef?.current) {
|
||||||
const canvas = mapRef.current.getCanvas()
|
const canvas = mapRef.current.getCanvas()
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
// Resize to max 1600px wide and convert to JPEG
|
// Resize to max 2400px wide and convert to JPEG
|
||||||
const maxW = 1600
|
const maxW = 2400
|
||||||
const ratio = Math.min(1, maxW / canvas.width)
|
const ratio = Math.min(1, maxW / canvas.width)
|
||||||
const offscreen = document.createElement('canvas')
|
const offscreen = document.createElement('canvas')
|
||||||
offscreen.width = Math.round(canvas.width * ratio)
|
offscreen.width = Math.round(canvas.width * ratio)
|
||||||
@@ -1039,18 +1039,18 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
|
|||||||
const ctx = offscreen.getContext('2d')
|
const ctx = offscreen.getContext('2d')
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.drawImage(canvas, 0, 0, offscreen.width, offscreen.height)
|
ctx.drawImage(canvas, 0, 0, offscreen.width, offscreen.height)
|
||||||
mapScreenshot = offscreen.toDataURL('image/jpeg', 0.75)
|
mapScreenshot = offscreen.toDataURL('image/jpeg', 0.85)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) { console.warn('Map screenshot failed:', e) }
|
} catch (e) { console.warn('Map screenshot failed:', e) }
|
||||||
} else if (rawScreenshot.length > 500000) {
|
} else if (rawScreenshot.length > 800000) {
|
||||||
// Compress pre-captured screenshot if too large
|
// Compress pre-captured screenshot if too large
|
||||||
try {
|
try {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.src = rawScreenshot
|
img.src = rawScreenshot
|
||||||
await new Promise(r => { img.onload = r; img.onerror = r })
|
await new Promise(r => { img.onload = r; img.onerror = r })
|
||||||
const maxW = 1600
|
const maxW = 2400
|
||||||
const ratio = Math.min(1, maxW / img.naturalWidth)
|
const ratio = Math.min(1, maxW / img.naturalWidth)
|
||||||
const offscreen = document.createElement('canvas')
|
const offscreen = document.createElement('canvas')
|
||||||
offscreen.width = Math.round(img.naturalWidth * ratio)
|
offscreen.width = Math.round(img.naturalWidth * ratio)
|
||||||
@@ -1058,7 +1058,7 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
|
|||||||
const ctx = offscreen.getContext('2d')
|
const ctx = offscreen.getContext('2d')
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.drawImage(img, 0, 0, offscreen.width, offscreen.height)
|
ctx.drawImage(img, 0, 0, offscreen.width, offscreen.height)
|
||||||
mapScreenshot = offscreen.toDataURL('image/jpeg', 0.75)
|
mapScreenshot = offscreen.toDataURL('image/jpeg', 0.85)
|
||||||
}
|
}
|
||||||
} catch { mapScreenshot = rawScreenshot }
|
} catch { mapScreenshot = rawScreenshot }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1377,12 +1377,12 @@ export function MapView({
|
|||||||
const lineCoords = f.geometry.coordinates as number[][]
|
const lineCoords = f.geometry.coordinates as number[][]
|
||||||
if (lineCoords.length < 2) return
|
if (lineCoords.length < 2) return
|
||||||
|
|
||||||
// Get last two points to calculate arrow direction
|
// Get last two points to calculate arrow direction using screen-projected coords
|
||||||
const p1 = lineCoords[lineCoords.length - 2]
|
const p1 = lineCoords[lineCoords.length - 2]
|
||||||
const p2 = lineCoords[lineCoords.length - 1]
|
const p2 = lineCoords[lineCoords.length - 1]
|
||||||
const angle = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * (180 / Math.PI)
|
const px1 = map.current.project(p1 as [number, number])
|
||||||
// MapLibre uses screen coords where Y is inverted, so negate the angle
|
const px2 = map.current.project(p2 as [number, number])
|
||||||
const screenAngle = -angle + 90
|
const screenAngle = Math.atan2(px2.y - px1.y, px2.x - px1.x) * (180 / Math.PI) + 90
|
||||||
|
|
||||||
const color = (f.properties.color as string) || '#000000'
|
const color = (f.properties.color as string) || '#000000'
|
||||||
const arrowEl = document.createElement('div')
|
const arrowEl = document.createElement('div')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Document, Page, Text, View, StyleSheet, Font, Image } from '@react-pdf/
|
|||||||
// Register default font (Helvetica is built-in)
|
// Register default font (Helvetica is built-in)
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
padding: '12mm 15mm 15mm',
|
padding: '12mm 15mm 32mm',
|
||||||
fontFamily: 'Helvetica',
|
fontFamily: 'Helvetica',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
|
|||||||
Reference in New Issue
Block a user