'use client' import React, { useState, useRef, useCallback, useEffect } from 'react' import { useRouter } from 'next/navigation' import { createWorker, PSM } from 'tesseract.js' import Link from 'next/link' import Cookies from 'js-cookie' import { performOCR, validateAddress, getAddressSuggestions } from '@/lib/api' // Schweizer Adress-Schlüsselwörter (Deutsch, Französisch und Italienisch) const ADDRESS_KEYWORDS = [ // Deutsch 'strasse', 'str.', 'weg', 'platz', 'gasse', 'hausnummer', 'nr.', 'nummer', 'postfach', 'postleitzahl', 'plz', 'stadt', 'kanton', 'stockwerk', 'etage', // Französisch 'rue', 'avenue', 'ave.', 'boulevard', 'blvd.', 'place', 'chemin', 'route', 'numéro', 'no.', 'boîte postale', 'code postal', 'ville', 'canton', 'étage', // Italienisch 'via', 'viale', 'corso', 'piazza', 'numero', 'n.', 'casella postale', 'codice postale', 'città', 'cantone', 'piano', // Generell 'ch-', 'switzerland', 'schweiz', 'suisse', 'svizzera' ] // Eski tarayıcılar için tip tanımlamaları declare global { interface Navigator { getUserMedia?: (constraints: MediaStreamConstraints, successCallback: (stream: MediaStream) => void, errorCallback: (error: Error) => void) => void; webkitGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: (stream: MediaStream) => void, errorCallback: (error: Error) => void) => void; mozGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: (stream: MediaStream) => void, errorCallback: (error: Error) => void) => void; msGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: (stream: MediaStream) => void, errorCallback: (error: Error) => void) => void; } interface MediaDevices { getUserMedia(constraints: MediaStreamConstraints): Promise; } } // Window-Interface erweitern declare global { interface Window { handleAddressSelection?: (address: string | null) => void; } } interface AddressInput { street: string number: string city: string } const CACHE_DURATION = 120 * 60 * 60 * 1000; // 120 Stunden const RATE_LIMIT = 1000; // 1 Sekunde let lastRequestTime = 0; interface Coordinate { lat: number; lon: number; } interface RoutePoint { index: number; lat: number; lon: number; address: string; } interface OptimizationResult { success: boolean; data?: { route: RoutePoint[]; distance: number; duration: number; }; error?: string; } interface CachedAddress { data: string[]; timestamp: number; } interface GuideRect { x: number; y: number; width: number; height: number; } const addressCache = new Map(); function getCachedAddresses(query: string): string[] | null { const cached = addressCache.get(query); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return cached.data; } return null; } function cacheAddresses(query: string, addresses: string[]) { addressCache.set(query, { data: addresses, timestamp: Date.now() }); } async function makeRequest(query: string) { const now = Date.now(); if (now - lastRequestTime < RATE_LIMIT) { await new Promise(resolve => setTimeout(resolve, RATE_LIMIT)); } lastRequestTime = now; } // Adresse von Nominatim API suchen async function fetchFromNominatim(query: string): Promise { try { const response = await fetch( `/api/address-suggestions?q=${encodeURIComponent(query)}`, { headers: { 'Content-Type': 'application/json' } } ); if (!response.ok) { // Verwende messagesData für Fehlermeldung (wird später im Komponentenkontext übergeben oder behandelt) throw new Error('Adresssuchdienst antwortete nicht'); // Generischer Fallback } const results = await response.json(); return results; } catch (error) { console.error('Fehler bei der Adresssuche:', error); throw error; } } // Ersatzdienst mit lokalen Daten verwenden async function searchWithFallbackService(query: string): Promise { // Einfache lokale Suche const localAddresses = [ 'Bahnhofstrasse 1, 8001 Zürich', 'Bundesplatz 3, 3003 Bern', 'Rue du Mont-Blanc 18, 1201 Genève', 'Via Nassa 5, 6900 Lugano' ]; return localAddresses.filter(address => address.toLowerCase().includes(query.toLowerCase()) ); } // Hauptsuchfunktion async function searchAddress(query: string): Promise { try { // Zuerst Cache prüfen const cached = getCachedAddresses(query); if (cached) { console.log('Aus Cache geladen:', query); return cached; } // Ratenbegrenzung anwenden await makeRequest(query); // API-Anfrage stellen const results = await fetchFromNominatim(query); // Ergebnisse zwischenspeichern cacheAddresses(query, results); console.log('Von API geladen und im Cache gespeichert:', query); return results; } catch (error) { console.error('Nominatim API Fehler:', error); // Auf Ersatzdienst wechseln console.log('Wechsle zu Ersatzdienst'); return await searchWithFallbackService(query); } } // Update the validateWithAPI function to better handle OCR results const validateWithAPI = async ( text: string, setAddressSuggestions: React.Dispatch>, setShowSuggestionModal: React.Dispatch>, setCurrentAddress: React.Dispatch> ): Promise => { try { console.log("[VALIDATE] Starting address validation for:", text); // Split text into lines and clean them const lines = text .split('\n') .map(line => line.trim()) .filter(line => line.length > 0); // Quick check for empty text if (lines.length === 0) { console.log("[VALIDATE] No lines to validate"); return ''; } console.log("[VALIDATE] Working with cleaned lines:", lines); // Look for patterns that suggest a Swiss address: // 1. A line with "strasse" or similar street indicator // 2. A line with 4-digit postal code let streetLine = ''; let postalCodeLine = ''; for (const line of lines) { const lowerLine = line.toLowerCase(); // Check for street indicators if ( (lowerLine.includes('strasse') || lowerLine.includes('str.') || lowerLine.includes('weg') || lowerLine.includes('platz')) && /\d+/.test(lowerLine) // Street lines typically include a number ) { streetLine = line; } // Check for Swiss postal code format (4 digits followed by city name) if (/\b\d{4}\b/.test(lowerLine)) { postalCodeLine = line; } } // If we found both components of an address, format it if (streetLine && postalCodeLine) { console.log("[VALIDATE] Found address components:", { streetLine, postalCodeLine }); // Filter out likely person name (usually the first line) if present const nameLine = lines[0] !== streetLine && lines[0] !== postalCodeLine ? lines[0] : ''; // Construct properly formatted address let formattedAddress = ''; if (nameLine) { formattedAddress = `${nameLine}\n${streetLine}\n${postalCodeLine}`; } else { formattedAddress = `${streetLine}\n${postalCodeLine}`; } console.log("[VALIDATE] Created formatted address:", formattedAddress); // Try validating with API const validationResponse = await validateAddress(formattedAddress); if (validationResponse.success && validationResponse.data) { console.log("[VALIDATE] API validation successful:", validationResponse.data); return validationResponse.data; } // If API validation fails, use our formatted address anyway return formattedAddress; } // If we couldn't identify clear address components, proceed with API validation console.log("[VALIDATE] Could not identify clear address components, calling API"); const validationResponse = await validateAddress(text); if (validationResponse.success && validationResponse.data) { console.log("[VALIDATE] API validation successful:", validationResponse.data); return validationResponse.data; } // If API validation fails, try to get suggestions console.log("[VALIDATE] API validation failed, getting suggestions"); const suggestionsResponse = await getAddressSuggestions(text); if (suggestionsResponse.success && suggestionsResponse.data && suggestionsResponse.data.length > 0) { console.log("[VALIDATE] Found suggestions:", suggestionsResponse.data); setAddressSuggestions(suggestionsResponse.data); setShowSuggestionModal(true); return ''; } // If no suggestions, return the original text formatted console.log("[VALIDATE] No suggestions found, returning original text"); return lines.join('\n'); } catch (error) { console.error('[VALIDATE] Validation error:', error); return text; } }; // Adresse aus OCR-Text extrahieren (fokussiert auf Straße, Hausnummer, PLZ und Ort) const extractAddress = async (text: string): Promise => { console.log('[EXTRACT] Processing OCR text:', text); // Zeilen aufteilen, trimmen, Leerzeilen entfernen const lines = text .split('\n') .map(line => line.trim()) .filter(line => line.length > 0); if (lines.length === 0) return ''; // Nur nach Straße und PLZ/Ort suchen (ignoriere Namen/Anreden) let streetLine = ''; let postalCityLine = ''; // Durchsuche jede Zeile nach relevanten Mustern for (const line of lines) { const lowerLine = line.toLowerCase(); // Straße mit Hausnummer erkennen if ((/strasse|str\.|weg|platz|gasse/i.test(lowerLine)) && /\d+/.test(lowerLine)) { streetLine = line; console.log('[EXTRACT] Found street line:', streetLine); continue; } // PLZ (4-stellig in der Schweiz) mit Ort erkennen if (/\b\d{4}\b/.test(lowerLine)) { postalCityLine = line; console.log('[EXTRACT] Found postal/city line:', postalCityLine); continue; } } // Wenn keine strukturierte Adresse gefunden, nach Teilinformationen suchen if (!streetLine || !postalCityLine) { console.log('[EXTRACT] Searching for partial address information'); // Alle Zeilen zusammen durchsuchen const fullText = lines.join(' ').toLowerCase(); // Straße suchen (falls noch nicht gefunden) if (!streetLine) { const streetMatch = fullText.match(/([a-zäöüß]+(?:strasse|str\.|weg|platz|gasse))\s+(\d+)/i); if (streetMatch) { streetLine = streetMatch[0]; console.log('[EXTRACT] Extracted street from full text:', streetLine); } } // PLZ/Ort suchen (falls noch nicht gefunden) if (!postalCityLine) { const postalMatch = fullText.match(/\b(\d{4})\s+([a-zäöüß]+)\b/i); if (postalMatch) { postalCityLine = postalMatch[0]; console.log('[EXTRACT] Extracted postal/city from full text:', postalCityLine); } } } // Adresse formatieren let formattedAddress = ''; if (streetLine && postalCityLine) { // Beide Teile gefunden formattedAddress = `${streetLine}, ${postalCityLine}`; } else if (streetLine) { // Nur Straße gefunden formattedAddress = streetLine; } else if (postalCityLine) { // Nur PLZ/Ort gefunden formattedAddress = postalCityLine; } else { // Fallback: eventuell gibt es Teile der Adresse ohne die typischen Muster console.log('[EXTRACT] No standard address patterns found, attempting backup extraction'); // Nach Zahlen suchen, die wie Hausnummern oder PLZ aussehen const numberMatches = []; for (const line of lines) { const numbers = line.match(/\b\d+\b/g); if (numbers) { for (const num of numbers) { if (num.length === 4) { // Wahrscheinlich eine PLZ numberMatches.push({ type: 'postal', text: line }); } else if (num.length <= 3) { // Wahrscheinlich eine Hausnummer numberMatches.push({ type: 'house', text: line }); } } } } // Wenn Matches gefunden, diese verwenden if (numberMatches.length > 0) { formattedAddress = numberMatches.map(match => match.text).join(', '); } else { // Letzter Ausweg: OCR-Text ohne Name verwenden // Oft sind die ersten 1-2 Zeilen Namen, die wir ignorieren wollen if (lines.length > 2) { formattedAddress = lines.slice(1).join(', '); } else { formattedAddress = lines.join(', '); } } } console.log('[EXTRACT] Final extracted address:', formattedAddress); return formattedAddress; }; // Schweizer Adresse formatieren (optional, wenn spezifische Formatierung benötigt wird) const formatSwissAddress = (addressParts: string[]): string => { // Logik zur Formatierung, z.B. Strasse, PLZ Ort return addressParts.join(', '); }; // Hilfsfunktion zum Zeichnen des Hilfsrahmens const drawGuideLine = (canvasRef: React.RefObject): GuideRect | null => { const canvas = canvasRef.current; if (!canvas) { console.log("[GUIDE] Canvas reference not available"); return null; } const ctx = canvas.getContext('2d'); if (!ctx) { console.log("[GUIDE] Could not get canvas context"); return null; } // Nur zeichnen, wenn Canvas Dimensionen hat if (canvas.width === 0 || canvas.height === 0) { console.log("[GUIDE] Canvas has zero dimensions, cannot draw guide"); return null; } // Rahmenabmessungen berechnen - Adresse ist meist in einer Zeile // Daher ist der Rahmen breiter als hoch const frameWidth = canvas.width * 0.85; // 85% der Canvas-Breite const frameHeight = canvas.height * 0.25; // 25% der Canvas-Höhe für eine Textzeile const frameX = Math.floor((canvas.width - frameWidth) / 2); const frameY = Math.floor((canvas.height - frameHeight) / 2); // Rahmen in kontrastierender Farbe zeichnen ctx.strokeStyle = '#ff0000'; // Rot (gut sichtbar auf den meisten Hintergründen) ctx.lineWidth = 4; // Dickere Linie für bessere Sichtbarkeit ctx.setLineDash([10, 10]); // Gestrichelte Linie für bessere Sichtbarkeit ctx.strokeRect(frameX, frameY, frameWidth, frameHeight); // Text-Hinweis für den Benutzer ctx.font = 'bold 16px Arial'; ctx.fillStyle = '#ff0000'; ctx.textAlign = 'center'; ctx.fillText('Adresse hier platzieren', canvas.width / 2, frameY - 10); // Rahmenkoordinaten zurückgeben const guideRect = { x: frameX, y: frameY, width: frameWidth, height: frameHeight }; console.log("[GUIDE] Guide rectangle created:", guideRect); return guideRect; }; // Hauptkomponente der Seite export default function NewRoutePage() { // Zustandsvariablen (State Hooks) const [isManualEntry, setIsManualEntry] = useState(false) const [searchTerm, setSearchTerm] = useState('') const [suggestedAddresses, setSuggestedAddresses] = useState([]) const [addresses, setAddresses] = useState([]) const [currentAddress, setCurrentAddress] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [scanStatus, setScanStatus] = useState('') const [isOptimizing, setIsOptimizing] = useState(false) const [showSuggestionModal, setShowSuggestionModal] = useState(false) const [isAutoScanEnabled, setIsAutoScanEnabled] = useState(false) const [autoScanIntervalId, setAutoScanIntervalId] = useState(null) const [messagesData, setMessagesData] = useState(null) // Zustand für Übersetzungen const [isCameraViewVisible, setIsCameraViewVisible] = useState(false); // Steuert Sichtbarkeit des Video-Bereichs const [mediaStream, setMediaStream] = useState(null); // Hält das Stream-Objekt // Referenzen (Refs) const videoRef = useRef(null) const canvasRef = useRef(null) const workerRef = useRef(null) const router = useRouter() // Effect zum Laden der Übersetzungen useEffect(() => { const loadMessages = async () => { const defaultLocale = 'de'; // Standard wie in Middleware const lang = Cookies.get('NEXT_LOCALE') || defaultLocale; // Fallback auf 'de' try { const messagesModule = await import(`@/messages/${lang}.json`); setMessagesData(messagesModule.default); } catch (error) { console.error(`Nachrichtendatei konnte nicht geladen werden: ${lang}`, error); // Bei Fehler Standard (Deutsch) laden try { const defaultMessages = await import(`@/messages/${defaultLocale}.json`); setMessagesData(defaultMessages.default); } catch (defaultError) { console.error(`Standard-Nachrichtendatei (${defaultLocale}) konnte nicht geladen werden:`, defaultError); setMessagesData({}); // Leeres Objekt, wenn nichts geladen werden kann } } }; loadMessages(); }, []); // Effect zum Initialisieren und Aufräumen des Tesseract Workers useEffect(() => { const initializeWorker = async () => { try { console.log("[WORKER] Initializing Tesseract worker..."); const worker = await createWorker({ logger: (m: any) => console.log(m) }); await worker.loadLanguage('deu'); await worker.initialize('deu'); // Set parameters after initialization await worker.setParameters({ tessedit_pageseg_mode: PSM.SINGLE_BLOCK, // Treat image as a single uniform block of text // Add character whitelist for German addresses tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzÄÖÜäöüß0123456789.,- ', }); console.log("[WORKER] Tesseract parameters set."); workerRef.current = worker; console.log("[WORKER] Tesseract worker initialized and assigned to ref.", workerRef.current); } catch (err) { console.error("[WORKER] Tesseract Worker konnte nicht initialisiert werden:", err); setError(messagesData?.newRoute?.scanError || "Fehler beim Initialisieren des Scanners"); } }; initializeWorker(); return () => { // Worker beim Verlassen der Komponente beenden console.log("[WORKER] Terminating Tesseract worker..."); workerRef.current?.terminate() .then(() => console.log('[WORKER] Tesseract Worker beendet.')) .catch(err => console.error('[WORKER] Fehler beim Beenden des Tesseract Workers:', err)); workerRef.current = null; } }, []); // Empty dependency array - run only once on mount // Neuer useEffect zum Anhängen des Streams an das Video-Element useEffect(() => { const videoElement = videoRef.current; // Funktion zum Entfernen des Event Listeners const cleanupListener = () => { if (videoElement) { videoElement.removeEventListener('play', handleVideoPlay); console.log("[EFFECT] Removed play event listener"); } }; if (isCameraViewVisible && mediaStream && videoElement) { console.log("[EFFECT] Attaching stream to video element"); // Wichtig: Alle alten Tracks stoppen if (videoElement.srcObject) { const oldStream = videoElement.srcObject as MediaStream; oldStream.getTracks().forEach(track => { track.stop(); console.log("[EFFECT] Stopped old track:", track.kind); }); } videoElement.srcObject = mediaStream; console.log("[EFFECT] Stream attached to video element"); // Video autoplay attribute setzen und abspielen videoElement.autoplay = true; videoElement.muted = true; // Wichtig für mobile Browser videoElement.playsInline = true; // Wichtig für iOS // Timeout für Video-Start (gibt dem Browser etwas Zeit) setTimeout(() => { videoElement.play() .then(() => { console.log("[EFFECT] Video playback started successfully"); // Canvas initial einrichten if (canvasRef.current) { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (ctx) { // Canvas initial mit Video-Dimensionen einrichten setTimeout(() => { if (videoElement.videoWidth && videoElement.videoHeight) { canvas.width = videoElement.videoWidth; canvas.height = videoElement.videoHeight; console.log(`[EFFECT] Canvas dimensions set to: ${canvas.width}x${canvas.height}`); } }, 500); // Kurze Verzögerung um sicherzustellen, dass Metadaten geladen sind } } // Event Listener hinzufügen videoElement.addEventListener('play', handleVideoPlay); console.log("[EFFECT] Added play event listener"); }) .catch(err => { console.error("[EFFECT] Video play failed:", err); setError("Video konnte nicht abgespielt werden: " + (err.message || "Unbekannter Fehler")); setIsCameraViewVisible(false); setMediaStream(null); }); }, 100); // Cleanup-Funktion zurückgeben return cleanupListener; } else { // Log why the condition failed console.log("[EFFECT] Not attaching stream.", { isVisible: isCameraViewVisible, hasStream: !!mediaStream, hasVideoElement: !!videoElement }); // Stream und Tracks stoppen, wenn die Komponente nicht sichtbar ist if (videoElement && videoElement.srcObject) { const currentStream = videoElement.srcObject as MediaStream; currentStream.getTracks().forEach(track => { track.stop(); console.log("[EFFECT] Stopped track:", track.kind); }); videoElement.srcObject = null; console.log("[EFFECT] Stream stopped and detached"); } // Listener entfernen, falls er noch existiert cleanupListener(); } }, [isCameraViewVisible, mediaStream]); // Abhängigkeiten: Sichtbarkeit und Stream // Funktion zum Abrufen von Adressvorschlägen const fetchAddressSuggestions = async (term: string) => { if (term.length < 3) { setSuggestedAddresses([]); return; } try { setScanStatus(messagesData?.newRoute?.searchingAddresses || 'Adressen werden gesucht...'); const results = await searchAddress(term); setSuggestedAddresses(results); setScanStatus(''); } catch (error) { console.error('Fehler bei der Adresssuche:', error); setError(messagesData?.newRoute?.searchError || 'Fehler bei der Adresssuche'); setScanStatus(''); } }; // Callback für Video-Wiedergabe const handleVideoPlay = () => { // Startet das Zeichnen auf dem Canvas, wenn das Video abgespielt wird requestAnimationFrame(updateCanvas); }; // Funktion zum Aktualisieren des Canvas (für visuelles Feedback) const updateCanvas = (timestamp: number) => { if (!videoRef.current || !canvasRef.current) return; const video = videoRef.current; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; // Canvas-Größe an Video anpassen if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; } // Videoframe auf Canvas zeichnen ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // Hilfslinie zeichnen const guideRect = drawGuideLine(canvasRef); // Nächsten Frame anfordern requestAnimationFrame(updateCanvas); }; // Funktion zum Starten des automatischen Scannens const startAutoScan = () => { if (autoScanIntervalId) return; // Verhindern, dass mehrere Intervalle gestartet werden setIsAutoScanEnabled(true); const intervalId = setInterval(() => { scanDocument(true); // true übergeben, um im Auto-Scan-Modus zu kennzeichnen }, 5000); // Alle 5 Sekunden scannen setAutoScanIntervalId(intervalId); }; // Funktion zum Stoppen des automatischen Scannens const stopAutoScan = () => { if (autoScanIntervalId) { clearInterval(autoScanIntervalId); setAutoScanIntervalId(null); } setIsAutoScanEnabled(false); }; // Funktion zum Starten der Kamera (modifiziert) const startCamera = async () => { setLoading(true); setError(null); setCurrentAddress(''); // Aktuelle Adresse löschen setMediaStream(null); // Alten Stream zurücksetzen setIsCameraViewVisible(false); // Ansicht vorerst ausblenden // Vorhandene Tracks stoppen, falls vorhanden if (videoRef.current && videoRef.current.srcObject) { const oldStream = videoRef.current.srcObject as MediaStream; oldStream.getTracks().forEach(track => track.stop()); videoRef.current.srcObject = null; } try { // Zuerst prüfen, ob Browser getUserMedia unterstützt if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { console.error('Browser unterstützt getUserMedia nicht!'); throw new Error('Ihr Browser unterstützt die Kamerafunktion nicht. Bitte versuchen Sie es mit einem aktuellen Browser wie Chrome, Firefox oder Safari.'); } console.log("Requesting camera stream with environment facing mode..."); // Zuerst mit Rückkamera versuchen try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', // Bevorzugt Rückkamera für Mobilgeräte width: { ideal: 1280 }, height: { ideal: 720 } } }); console.log("Stream obtained with environment camera:", stream); setMediaStream(stream); setIsCameraViewVisible(true); } catch (envError) { console.warn("Couldn't access environment camera, trying fallback to any camera:", envError); // Fallback: Irgendeine verfügbare Kamera verwenden const stream = await navigator.mediaDevices.getUserMedia({ video: true }); console.log("Stream obtained with fallback camera:", stream); setMediaStream(stream); setIsCameraViewVisible(true); } } catch (err) { console.error('Kamerafehler:', err); if (err instanceof Error) { // Detaillierte Fehlermeldungen basierend auf Error.name if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') { setError('Kamerazugriff verweigert. Bitte erteilen Sie die Berechtigung und versuchen Sie es erneut.'); } else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') { setError('Keine Kamera gefunden. Bitte stellen Sie sicher, dass eine Kamera verbunden ist.'); } else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') { setError('Die Kamera ist möglicherweise bereits von einer anderen Anwendung in Gebrauch.'); } else if (err.name === 'OverconstrainedError') { setError('Die angeforderten Kameraeinstellungen werden nicht unterstützt.'); } else { setError(`Kamerafehler: ${err.message}`); } } else { setError(messagesData?.newRoute?.cameraError || 'Fehler beim Starten der Kamera'); } setMediaStream(null); setIsCameraViewVisible(false); } finally { setLoading(false); } }; // Funktion zum Scannen des Dokuments (manuell oder automatisch) const scanDocument = async (isAuto = false) => { // Entry log console.log(`[SCAN] scanDocument called. isAuto: ${isAuto}, loading: ${loading}, worker ready: ${!!workerRef.current}`); // Check prerequisites if (!videoRef.current || !canvasRef.current || !workerRef.current || loading) { console.log('[SCAN] Exiting: Scanner nicht bereit oder beschäftigt.', { hasVideo: !!videoRef.current, hasCanvas: !!canvasRef.current, hasWorker: !!workerRef.current, isLoading: loading }); if (!isAuto) { setError('Scanner ist nicht bereit. Bitte starten Sie die Kamera und versuchen Sie es erneut.'); } return; } const video = videoRef.current; // Check video readiness if (video.readyState < video.HAVE_METADATA || video.videoWidth === 0 || video.videoHeight === 0) { console.log('[SCAN] Exiting: Videostream noch nicht bereit oder Dimensionen ungültig.', { readyState: video.readyState, width: video.videoWidth, height: video.videoHeight }); if (!isAuto) { setError('Videostream nicht bereit. Bitte warten Sie einen Moment oder versuchen Sie, die Kamera neu zu starten.'); } return; } console.log("[SCAN] Proceeding with scan..."); setLoading(true); setError(null); setScanStatus(messagesData?.newRoute?.scanning || 'Scannen...'); try { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Canvas context nicht verfügbar'); } // Capture video frame to canvas canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); console.log('[SCAN] Video frame captured to canvas'); // Get guide rectangle for cropping const guideRect = drawGuideLine(canvasRef); // Initialize variables for OCR let imageDataUrl = ''; let rawOcrText = ''; // Process image with guide rectangle if available if (guideRect) { console.log("[SCAN] Processing cropped image with guide frame"); // Create temporary canvas for the cropped area const tempCanvas = document.createElement('canvas'); tempCanvas.width = guideRect.width; tempCanvas.height = guideRect.height; const tempCtx = tempCanvas.getContext('2d'); if (!tempCtx) { throw new Error('Temporary canvas context not available'); } // Apply image processing to improve OCR tempCtx.filter = 'contrast(150%) brightness(110%) grayscale(1)'; // Copy cropped area to temp canvas tempCtx.drawImage( canvas, guideRect.x, guideRect.y, guideRect.width, guideRect.height, 0, 0, guideRect.width, guideRect.height ); tempCtx.filter = 'none'; imageDataUrl = tempCanvas.toDataURL('image/png', 1.0); } else { // If no guide rectangle, use the full frame console.log("[SCAN] No guide rectangle, using full image"); imageDataUrl = canvas.toDataURL('image/png', 1.0); } // Perform OCR console.log("[SCAN] Starting OCR recognition..."); const worker = workerRef.current; const { data: { text } } = await worker.recognize(imageDataUrl); rawOcrText = text; console.log('[SCAN] OCR raw result:', rawOcrText); // Exit if OCR returned empty text if (!rawOcrText.trim()) { throw new Error('OCR returned empty text'); } // Pre-process the OCR text const processedText = await extractAddress(rawOcrText); console.log('[SCAN] Processed address:', processedText); if (!processedText) { throw new Error('Could not extract address from OCR result'); } // Validate address console.log("[SCAN] Validating extracted address..."); const validatedAddress = await validateWithAPI( processedText, setSuggestedAddresses, setShowSuggestionModal, setCurrentAddress ); // Handle validation result if (validatedAddress) { console.log("[SCAN] Successfully validated address:", validatedAddress); setCurrentAddress(validatedAddress); setScanStatus(messagesData?.newRoute?.addressFound || 'Adresse gefunden. Bitte überprüfen und "Hinzufügen" klicken.'); // Stop auto-scan if enabled if (isAutoScanEnabled) { stopAutoScan(); } } else if (showSuggestionModal) { // If showing suggestions modal, status is handled by the modal console.log("[SCAN] Showing address suggestions modal"); setScanStatus(messagesData?.newRoute?.chooseSuggestion || 'Bitte wählen Sie eine Adresse aus den Vorschlägen.'); } else { // If no valid address and no suggestions, let user edit the text console.log("[SCAN] No validated address, showing OCR text for editing"); setCurrentAddress(processedText); setScanStatus(messagesData?.newRoute?.checkAddress || 'Adresse erkannt, bitte überprüfen und bearbeiten.'); } } catch (error) { console.error('[SCAN] Error during scanning:', error); const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler'; console.log('[SCAN] Error details:', errorMessage); // Provide user-friendly error messages if (errorMessage.includes('empty text')) { setError('Keine Adresse im Bild erkannt. Bitte positionieren Sie die Adresse innerhalb des markierten Bereichs.'); } else { setError(messagesData?.newRoute?.scanError || 'Fehler beim Scannen des Dokuments'); } setScanStatus(messagesData?.newRoute?.scanErrorStatus || 'Fehler aufgetreten'); } finally { setLoading(false); } }; // Funktion zum Hinzufügen einer Adresse zur Liste const addAddress = () => { if (currentAddress.trim()) { // Extract only the essential address components const addressParts = currentAddress.split(/[\n,]/).map(part => part.trim()).filter(part => part.length > 0); // Identify street and postal/city parts let streetPart = ''; let postalCityPart = ''; // Look for street and postal/city in each part for (const part of addressParts) { const lowerPart = part.toLowerCase(); // Street with number if (/strasse|str\.|weg|platz|gasse/i.test(lowerPart) && /\d+/.test(part)) { streetPart = part.replace(/\s+/g, ' ').trim(); } // Postal code with city if (/\b\d{4}\b/.test(part)) { postalCityPart = part.replace(/\s+/g, ' ').trim(); } } // If we couldn't find both parts, look in the full text if (!streetPart || !postalCityPart) { const fullText = addressParts.join(' '); // Try to extract street if (!streetPart) { const streetMatch = fullText.match(/([a-zäöüß]+(?:strasse|str\.|weg|platz|gasse))\s+(\d+)/i); if (streetMatch) { streetPart = streetMatch[0].trim(); } } // Try to extract postal code if (!postalCityPart) { const postalMatch = fullText.match(/\b(\d{4})\s+([a-zäöüß]+)\b/i); if (postalMatch) { postalCityPart = postalMatch[0].trim(); } } } // Format address with only essential parts let formattedAddress = ''; if (streetPart && postalCityPart) { formattedAddress = `${streetPart}, ${postalCityPart}`; } else if (streetPart) { formattedAddress = streetPart; } else if (postalCityPart) { formattedAddress = postalCityPart; } else { // If we couldn't extract specific components, use the original cleaned text // But try to remove any name parts (typically in the first line) if (addressParts.length > 1) { // Check if first part looks like a name const firstPart = addressParts[0].toLowerCase(); if (!/\d/.test(firstPart) && !/strasse|str\.|weg|platz|gasse/i.test(firstPart)) { // Skip the first part which likely contains the name formattedAddress = addressParts.slice(1).join(', '); } else { formattedAddress = addressParts.join(', '); } } else { formattedAddress = currentAddress.trim(); } } // Final cleanup of formatted address formattedAddress = formattedAddress .replace(/,\s*,/g, ',') // Remove double commas .replace(/\s+,/g, ',') // Remove spaces before commas .replace(/,(\S)/g, ', $1') // Ensure space after commas .replace(/(\d{4})\s+([A-Za-zäöüÄÖÜ]+)/g, '$1 $2') // Fix spacing in postal code + city .replace(/l(\d+)/gi, '1$1') // Replace lowercase L with 1 when preceding digits .replace(/l\s+(\d+)/gi, '1 $1'); // Replace lowercase L with 1 when before digits console.log('[ADD] Adding essential address:', formattedAddress); // Check if this address already exists if (!addresses.includes(formattedAddress)) { // Add to addresses list setAddresses([...addresses, formattedAddress]); // Reset inputs setCurrentAddress(''); setSearchTerm(''); setSuggestedAddresses([]); setError(null); setScanStatus(messagesData?.newRoute?.addressAdded || 'Adresse hinzugefügt'); // Clear success message after a delay setTimeout(() => { if (scanStatus === messagesData?.newRoute?.addressAdded || scanStatus === 'Adresse hinzugefügt') { setScanStatus(''); } }, 2000); } else { // Address already exists in the list setError(messagesData?.newRoute?.duplicateAddress || 'Diese Adresse ist bereits in der Liste'); setTimeout(() => setError(null), 3000); } } }; // Funktion zur Optimierung und Speicherung der Route const optimizeAndSaveRoute = async () => { if (addresses.length < 2) { setError(messagesData?.newRoute?.minAddressError || 'Mindestens 2 Adressen erforderlich') return } try { setIsOptimizing(true) setScanStatus(messagesData?.newRoute?.optimizing || 'Route wird optimiert...') setError(null) // Route erstellen (API-Aufruf) const createResponse = await fetch('/api/routes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ addresses }) }); const createResult = await createResponse.json(); if (!createResponse.ok || !createResult.success || !createResult.data) { throw new Error(createResult.error || messagesData?.newRoute?.saveError || 'Route konnte nicht gespeichert werden'); } const routeId = createResult.data.id; setScanStatus(messagesData?.newRoute?.routeCreated || 'Route erstellt, wird optimiert...'); // Route optimieren (API-Aufruf) // Hinweis: Die Optimierung erfolgt serverseitig durch den vorherigen API-Aufruf `/api/routes/optimize`, // welcher innerhalb der `/api/routes` POST-Logik aufgerufen werden sollte ODER hier separat. // Aktuell scheint die Optimierung implizit beim Erstellen zu erfolgen oder der Client erwartet das Ergebnis. // Wir nehmen an, dass `createResult.data` die optimierte Route enthält oder wir auf die Route-Detailseite weiterleiten. // Annahme: Die API gibt die optimierten Details zurück oder setzt den Status. // Wir leiten zur Detailseite weiter, wo die optimierte Route angezeigt wird. console.log('Route erfolgreich erstellt/optimiert. Route ID:', routeId) setScanStatus(messagesData?.newRoute?.routeSaved || 'Route erfolgreich gespeichert'); // Zur Routen-Detailseite oder Dashboard weiterleiten router.push(`/dashboard/routes/${routeId}`); // Optional: Kurze Erfolgsmeldung anzeigen, bevor weitergeleitet wird // await new Promise(resolve => setTimeout(resolve, 1500)); } catch (error) { console.error('Routenoptimierungs-/Speicherfehler:', error) setError(error instanceof Error ? error.message : messagesData?.newRoute?.generalError || 'Fehler während der Routenverarbeitung') setScanStatus(messagesData?.newRoute?.scanErrorStatus || 'Fehler aufgetreten') } finally { setIsOptimizing(false) } }; // Ladezustand für Übersetzungen if (!messagesData) { return (
{/* Verwende Übersetzung für Ladeanzeige */}

{messagesData?.common?.loading || 'Laden...'}

); } // Haupt-JSX der Komponente return (
{/* Optimierungs-Overlay */} {isOptimizing && (

{messagesData?.newRoute?.optimizing}

{scanStatus}

)} {/* Adressvorschlagsmodal - neu gestaltet */} {showSuggestionModal && (

{messagesData?.newRoute?.addressSuggestions || 'Adressvorschläge'}

{messagesData?.newRoute?.chooseAddress || 'Bitte wählen Sie die korrekte Adresse aus:'}

{suggestedAddresses.map((addr, idx) => ( ))}
)}
{/* Seitentitel */}

{messagesData?.newRoute?.title}

{/* Zurück-Button */} {messagesData?.common?.back}
{/* Eingabemodus-Auswahl */}
{/* Kamera-Scan-Ansicht */} {!isManualEntry ? ( <> {/* Kamerabild & Canvas - Conditionally render this block */} {isCameraViewVisible && (
)} {/* Statusanzeige für Scan */} {scanStatus && (
{scanStatus}
)} {/* Kamerasteuerungsbuttons */}
{/* Bearbeitung des gescannten Adresstextes */}