/* 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) => `
${issue.severity}: ${issue.category}
${issue.description}
${issue.recommendation ? `
Tip: ${issue.recommendation}
` : ''}
`).join(''); tooltipDiv.innerHTML = issues.length > 1 ? `
${issues.length} issues at this location:
` + html : html; tooltipDiv.style.display = 'block'; tooltipDiv.style.left = (event.clientX + 15) + 'px'; tooltipDiv.style.top = (event.clientY + 15) + 'px'; } function hideIssueTooltip() { if (tooltipDiv) tooltipDiv.style.display = 'none'; } function zoomIn() { currentZoom = Math.min(currentZoom + 0.25, 3.0); applyZoom(); } function zoomOut() { currentZoom = Math.max(currentZoom - 0.25, 0.5); applyZoom(); } function resetZoom() { currentZoom = 1.0; applyZoom(); } function applyZoom() { document.getElementById('zoomContainer').style.transform = `scale(${currentZoom})`; document.getElementById('zoomLevel').textContent = `${Math.round(currentZoom * 100)}%`; } function highlightMarker(issueNumber) { const marker = document.getElementById(`marker-${issueNumber}`); if (marker) { const r = marker.getAttribute('r'); marker.setAttribute('r', parseFloat(r) * 1.5); setTimeout(() => marker.setAttribute('r', r), 300); marker.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }