File size: 6,533 Bytes
70d9e6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669f7f8
 
 
70d9e6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec7397
70d9e6c
 
 
 
 
 
 
 
 
 
 
bec7397
70d9e6c
 
 
 
 
 
 
 
bec7397
 
 
 
70d9e6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bec7397
70d9e6c
bec7397
70d9e6c
 
 
 
 
 
 
 
bec7397
70d9e6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""PDF export for DDA detection reports (FR-05)."""
from __future__ import annotations

import json
from io import BytesIO
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..database import DATA_DIR
from ..models import DetectionRun
from .detect_service import _isoformat_ist


def _safe_filename(title: str, run_id: int) -> str:
    base = "".join(c if c.isalnum() or c in " -_" else "_" for c in (title or "report"))
    base = base.strip().replace(" ", "_")[:60] or "report"
    return f"DDA_Report_{run_id}_{base}.pdf"


def build_report_dict(run: DetectionRun, *, include_overlay_b64: bool = False, regions: Optional[List[dict]] = None) -> Dict[str, Any]:
    if regions is None:
        regions = json.loads(run.regions_json or "[]")
    payload: Dict[str, Any] = {
        "id": run.id,
        "title": run.title,
        "method": run.method,
        "zone": run.zone or "",
        "village": run.village or "",
        "statistics": {
            "totalPixels": run.total_pixels,
            "changedPixels": run.changed_pixels,
            "unchangedPixels": run.total_pixels - run.changed_pixels,
            "changePercentage": run.change_percentage,
        },
        "regions": regions,
        "regionsCount": run.regions_count,
        "overlayUrl": f"/api/overlay/{run.overlay_path}" if run.overlay_path else None,
        "beforeFullUrl": f"/api/overlay/{run.before_full_path}" if run.before_full_path else None,
        "beforeThumbUrl": f"/api/overlay/{run.before_thumb_path}" if run.before_thumb_path else None,
        "afterThumbUrl": f"/api/overlay/{run.after_thumb_path}" if run.after_thumb_path else None,
        "afterFullUrl": f"/api/overlay/{run.after_full_path}" if getattr(run, "after_full_path", None) else None,
        "createdAt": _isoformat_ist(run.created_at),
        "pdfUrl": f"/api/dda/reports/{run.id}/pdf",
    }
    if include_overlay_b64 and run.overlay_path:
        overlay_file = DATA_DIR / run.overlay_path
        if overlay_file.exists():
            import base64
            payload["overlayBase64Png"] = base64.b64encode(overlay_file.read_bytes()).decode("utf-8")
    return payload


def generate_report_pdf(run: DetectionRun, *, regions: Optional[List[dict]] = None) -> tuple[bytes, str]:
    """Build PDF bytes and suggested download filename."""
    from reportlab.lib import colors
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
    from reportlab.lib.units import mm
    from reportlab.platypus import Image as RLImage
    from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle

    from .geo_regions import region_lat_lng

    if regions is None:
        regions = json.loads(run.regions_json or "[]")
    buf = BytesIO()
    doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=18 * mm, rightMargin=18 * mm, topMargin=16 * mm, bottomMargin=16 * mm)
    styles = getSampleStyleSheet()
    title_style = ParagraphStyle("ReportTitle", parent=styles["Heading1"], fontSize=16, spaceAfter=8)
    sub_style = ParagraphStyle("ReportSub", parent=styles["Normal"], fontSize=10, textColor=colors.grey)
    body = styles["Normal"]

    location = ", ".join(filter(None, [run.village, run.zone])) or "—"
    story = [
        Paragraph("DDA Change Detection Report", title_style),
        Paragraph(run.title or f"Run #{run.id}", styles["Heading2"]),
        Paragraph(f"Generated {_isoformat_ist(run.created_at)} IST", sub_style),
        Spacer(1, 8),
        Paragraph(f"<b>Method:</b> {run.method or '—'}", body),
        Paragraph(f"<b>Location:</b> {location}", body),
        Paragraph(f"<b>Change:</b> {run.change_percentage:.2f}% ({run.changed_pixels:,} / {run.total_pixels:,} px)", body),
        Paragraph(f"<b>Regions detected:</b> {run.regions_count}", body),
        Spacer(1, 10),
    ]

    overlay_path = run.overlay_path
    if overlay_path:
        overlay_file = DATA_DIR / overlay_path
        if overlay_file.exists():
            try:
                from PIL import Image as PILImage
                with PILImage.open(overlay_file) as im:
                    im = im.convert("RGB")
                    max_w = 160 * mm
                    ratio = min(1.0, max_w / im.width)
                    w, h = int(im.width * ratio), int(im.height * ratio)
                    thumb = im.resize((w, h), PILImage.Resampling.LANCZOS)
                    img_buf = BytesIO()
                    thumb.save(img_buf, format="JPEG", quality=85)
                    img_buf.seek(0)
                    story.append(Paragraph("Change overlay", styles["Heading3"]))
                    story.append(RLImage(img_buf, width=w, height=h))
                    story.append(Spacer(1, 10))
            except Exception:
                pass

    story.append(Paragraph("Detected regions", styles["Heading3"]))
    if regions:
        table_data = [["#", "DDA type", "Internal type", "Conf.", "Area (px)", "Lat", "Lng", "Review"]]
        for r in regions[:50]:
            lat, lng = region_lat_lng(r)
            table_data.append([
                str(r.get("id", "")),
                r.get("ddaChangeType") or r.get("objectType") or "—",
                r.get("internalObjectType") or r.get("objectType") or "—",
                f'{(r.get("confidence", 0) or 0) * 100:.0f}%',
                f'{r.get("area", 0):,}',
                f"{lat:.5f}" if lat is not None else "—",
                f"{lng:.5f}" if lng is not None else "—",
                r.get("reviewStatus") or "pending",
            ])
        tbl = Table(table_data, repeatRows=1, colWidths=[22, 72, 72, 36, 52, 48, 48])
        tbl.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#2e33c5")),
            ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
            ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
            ("FONTSIZE", (0, 0), (-1, -1), 8),
            ("GRID", (0, 0), (-1, -1), 0.25, colors.lightgrey),
            ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f9f9fb")]),
            ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ]))
        story.append(tbl)
        if len(regions) > 50:
            story.append(Spacer(1, 6))
            story.append(Paragraph(f"Showing first 50 of {len(regions)} regions.", sub_style))
    else:
        story.append(Paragraph("No change regions detected.", body))

    doc.build(story)
    return buf.getvalue(), _safe_filename(run.title or "", run.id)