/* Visual Page Inspector — image viewer with SVG marker overlays */ let currentPageData = null; let currentZoom = 1.0; let currentVisualPage = 1; let tooltipDiv = null; function initializePageViewer(data) { if (!data.page_images || Object.keys(data.page_images).length === 0) return; document.getElementById('pageViewerCard').style.display = 'block'; currentPageData = data; const pageSelector = document.getElementById('pageSelector'); const pageNumbers = Object.keys(data.page_images).map(Number).sort((a, b) => a - b); pageSelector.innerHTML = pageNumbers.map(pn => { const pi = data.issues.filter(i => i.page_number === pn); let color = '#10b981'; if (pi.some(i => i.severity === 'CRITICAL')) color = '#dc2626'; else if (pi.some(i => i.severity === 'ERROR')) color = '#ef4444'; else if (pi.some(i => i.severity === 'WARNING')) color = '#f59e0b'; return ``; }).join(''); const firstWithIssues = pageNumbers.find(p => data.issues.some(i => i.page_number === p)); loadVisualPage(firstWithIssues || pageNumbers[0]); } function loadVisualPage(pageNum, highlightNum) { if (!currentPageData || !currentPageData.page_images[pageNum]) return; currentVisualPage = pageNum; document.getElementById('currentPageTitle').textContent = `Page ${pageNum}`; document.querySelectorAll('[id^="pageBtn"]').forEach(btn => { btn.style.background = 'var(--surface)'; btn.style.fontWeight = 'normal'; }); const sel = document.getElementById(`pageBtn${pageNum}`); if (sel) { sel.style.background = 'var(--accent-subtle)'; sel.style.fontWeight = '600'; } const img = document.getElementById('pageImage'); img.onload = () => { drawMarkers(pageNum); if (highlightNum !== undefined) { // Markers are drawn synchronously in drawMarkers — highlight immediately after setTimeout(() => highlightMarker(highlightNum), 50); } }; // Use GCS URL directly if available, otherwise fall back to api.php const imageUrl = currentPageData.page_images[pageNum]; if (imageUrl && (imageUrl.startsWith('http://') || imageUrl.startsWith('https://'))) { img.src = imageUrl; } else { img.src = `api.php?action=image&job_id=${currentJobId}&page=${pageNum}`; } } function drawMarkers(pageNum) { const svg = document.getElementById('markerOverlay'); const img = document.getElementById('pageImage'); svg.innerHTML = ''; const imgW = img.naturalWidth; const imgH = img.naturalHeight; const dispW = img.clientWidth; const dispH = img.clientHeight; const dpi = currentPageData.page_image_dpi || 150; const scale = dpi / 72.0; svg.setAttribute('viewBox', `0 0 ${imgW} ${imgH}`); svg.setAttribute('width', dispW); svg.setAttribute('height', dispH); const allWithCoords = currentPageData.issues.filter(i => i.coordinates && i.page_number); const pageIssues = allWithCoords.filter(i => i.page_number === pageNum); if (pageIssues.length === 0) return; // Group by coordinates const groups = {}; pageIssues.forEach(issue => { const gIdx = allWithCoords.indexOf(issue) + 1; const key = `${issue.coordinates.x0}-${issue.coordinates.y0}-${issue.coordinates.x1}-${issue.coordinates.y1}`; if (!groups[key]) groups[key] = { coords: issue.coordinates, issues: [], numbers: [], primary: issue }; groups[key].issues.push(issue); groups[key].numbers.push(gIdx); }); Object.values(groups).forEach(group => { const coords = group.coords; const nums = group.numbers; const cnt = group.issues.length; const x0 = coords.x0 * scale; const y0 = coords.y0 * scale; const x1 = coords.x1 * scale; const y1 = coords.y1 * scale; let stroke, fill; switch (group.primary.severity) { case 'CRITICAL': stroke = '#dc2626'; fill = 'rgba(220,38,38,0.2)'; break; case 'ERROR': stroke = '#ef4444'; fill = 'rgba(239,68,68,0.2)'; break; case 'WARNING': stroke = '#f59e0b'; fill = 'rgba(245,158,11,0.2)'; break; default: stroke = '#3b82f6'; fill = 'rgba(59,130,246,0.2)'; } const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('x', x0); rect.setAttribute('y', y0); rect.setAttribute('width', x1 - x0); rect.setAttribute('height', y1 - y0); rect.setAttribute('fill', fill); rect.setAttribute('stroke', stroke); rect.setAttribute('stroke-width', '3'); rect.setAttribute('stroke-dasharray', '5,5'); rect.setAttribute('rx', '4'); rect.style.cursor = 'pointer'; rect.style.pointerEvents = 'all'; rect.addEventListener('mouseenter', e => showIssueTooltip(e, group.issues)); rect.addEventListener('mouseleave', hideIssueTooltip); svg.appendChild(rect); const label = cnt > 1 ? `${nums[0]}+${cnt - 1}` : `${nums[0]}`; const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', x0 + 20); circle.setAttribute('cy', y0 + 20); circle.setAttribute('r', cnt > 1 ? '18' : '16'); circle.setAttribute('fill', stroke); circle.setAttribute('stroke', 'white'); circle.setAttribute('stroke-width', '2'); circle.setAttribute('id', `marker-${nums[0]}`); svg.appendChild(circle); const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); text.setAttribute('x', x0 + 20); text.setAttribute('y', y0 + 26); text.setAttribute('text-anchor', 'middle'); text.setAttribute('fill', 'white'); text.setAttribute('font-size', cnt > 1 ? '11' : '13'); text.setAttribute('font-weight', 'bold'); text.textContent = label; svg.appendChild(text); }); } function showIssueTooltip(event, issues) { if (!Array.isArray(issues)) issues = [issues]; if (!tooltipDiv) { tooltipDiv = document.createElement('div'); Object.assign(tooltipDiv.style, { position: 'fixed', background: 'rgba(0,0,0,0.95)', color: 'white', padding: '12px', borderRadius: '8px', maxWidth: '400px', maxHeight: '400px', overflowY: 'auto', zIndex: '10000', fontSize: '13px', pointerEvents: 'none' }); document.body.appendChild(tooltipDiv); } const html = issues.map((issue, idx) => `