""" Report generation module for RehabWatch. Creates PDF reports and CSV exports. """ from fpdf import FPDF from datetime import datetime from typing import Dict, Any, Optional import io class RehabWatchPDF(FPDF): """Custom PDF class for RehabWatch reports.""" def header(self): """Add header to each page.""" self.set_font('Helvetica', 'B', 16) self.set_text_color(46, 125, 50) # Green self.cell(0, 10, 'RehabWatch', 0, 0, 'L') self.set_font('Helvetica', '', 10) self.set_text_color(100, 100, 100) self.cell(0, 10, 'Mining Rehabilitation Assessment', 0, 1, 'R') self.line(10, 25, 200, 25) self.ln(10) def footer(self): """Add footer to each page.""" self.set_y(-15) self.set_font('Helvetica', 'I', 8) self.set_text_color(128, 128, 128) self.cell(0, 10, f'Page {self.page_no()}/{{nb}}', 0, 0, 'C') def chapter_title(self, title: str): """Add a section title.""" self.set_font('Helvetica', 'B', 14) self.set_text_color(33, 33, 33) self.cell(0, 10, title, 0, 1, 'L') self.ln(2) def chapter_body(self, body: str): """Add body text.""" self.set_font('Helvetica', '', 11) self.set_text_color(66, 66, 66) self.multi_cell(0, 6, body) self.ln(4) def generate_pdf_report( tenement_id: str, stats: Dict[str, float], rehab_score: int, interpretation: str, date_before: str, date_after: str, mine_name: Optional[str] = None ) -> bytes: """ Generate a PDF report of the rehabilitation assessment. Args: tenement_id: Mining tenement identifier stats: Statistics dictionary from analysis rehab_score: Rehabilitation score (0-100) interpretation: Plain-language interpretation date_before: Analysis start date date_after: Analysis end date mine_name: Optional name of the mine Returns: PDF as bytes for download """ pdf = RehabWatchPDF() pdf.alias_nb_pages() pdf.add_page() # Title pdf.set_font('Helvetica', 'B', 24) pdf.set_text_color(33, 33, 33) pdf.cell(0, 15, 'Rehabilitation Assessment Report', 0, 1, 'C') pdf.ln(5) # Site Information pdf.set_font('Helvetica', '', 12) pdf.set_text_color(66, 66, 66) if mine_name: pdf.cell(0, 8, f'Site: {mine_name}', 0, 1, 'C') pdf.cell(0, 8, f'Tenement ID: {tenement_id}', 0, 1, 'C') pdf.cell(0, 8, f'Analysis Period: {date_before} to {date_after}', 0, 1, 'C') pdf.cell(0, 8, f'Report Generated: {datetime.now().strftime("%Y-%m-%d %H:%M")}', 0, 1, 'C') pdf.ln(10) # Rehabilitation Score Box pdf.set_fill_color(240, 240, 240) pdf.rect(60, pdf.get_y(), 90, 35, 'F') pdf.set_font('Helvetica', 'B', 12) pdf.set_text_color(66, 66, 66) pdf.cell(0, 8, 'Rehabilitation Score', 0, 1, 'C') # Score color based on value if rehab_score >= 60: pdf.set_text_color(46, 125, 50) # Green elif rehab_score >= 40: pdf.set_text_color(255, 152, 0) # Orange else: pdf.set_text_color(183, 28, 28) # Red pdf.set_font('Helvetica', 'B', 36) pdf.cell(0, 15, f'{rehab_score}/100', 0, 1, 'C') pdf.ln(15) # Interpretation pdf.chapter_title('Summary') pdf.chapter_body(interpretation) # Statistics Table pdf.chapter_title('Detailed Statistics') # Table header pdf.set_font('Helvetica', 'B', 10) pdf.set_fill_color(46, 125, 50) pdf.set_text_color(255, 255, 255) pdf.cell(90, 8, 'Metric', 1, 0, 'C', True) pdf.cell(50, 8, 'Value', 1, 0, 'C', True) pdf.cell(50, 8, 'Unit', 1, 1, 'C', True) # Table data pdf.set_font('Helvetica', '', 10) pdf.set_text_color(66, 66, 66) table_data = [ ('NDVI Before (mean)', f"{stats['ndvi_before_mean']:.4f}", 'index'), ('NDVI After (mean)', f"{stats['ndvi_after_mean']:.4f}", 'index'), ('NDVI Change (mean)', f"{stats['ndvi_change_mean']:.4f}", 'index'), ('Relative Change', f"{stats['percent_change']:.2f}", '%'), ('SAVI (mean)', f"{stats.get('savi_after_mean', 0):.4f}", 'index'), ('EVI (mean)', f"{stats.get('evi_after_mean', 0):.4f}", 'index'), ('NDMI (moisture)', f"{stats.get('ndmi_after_mean', 0):.4f}", 'index'), ('BSI (bare soil)', f"{stats.get('bsi_after_mean', 0):.4f}", 'index'), ('Water Presence', f"{stats.get('percent_water', 0):.2f}", '%'), ('Bare Soil Extent', f"{stats.get('percent_bare_soil', 0):.2f}", '%'), ('Moisture Stressed', f"{stats.get('percent_moisture_stressed', 0):.2f}", '%'), ('Area Improved', f"{stats['area_improved_ha']:.2f}", 'hectares'), ('Area Stable', f"{stats['area_stable_ha']:.2f}", 'hectares'), ('Area Degraded', f"{stats['area_degraded_ha']:.2f}", 'hectares'), ('Total Area', f"{stats['total_area_ha']:.2f}", 'hectares'), ('Percentage Improved', f"{stats['percent_improved']:.2f}", '%'), ('Percentage Degraded', f"{stats['percent_degraded']:.2f}", '%'), ] fill = False for row in table_data: if fill: pdf.set_fill_color(245, 245, 245) else: pdf.set_fill_color(255, 255, 255) pdf.cell(90, 7, row[0], 1, 0, 'L', fill) pdf.cell(50, 7, row[1], 1, 0, 'C', fill) pdf.cell(50, 7, row[2], 1, 1, 'C', fill) fill = not fill pdf.ln(10) # Methodology pdf.chapter_title('Methodology') methodology_text = """This assessment uses multiple vegetation and soil indices derived from Sentinel-2 satellite imagery, Copernicus DEM, and IO-LULC land cover data. Vegetation Indices: - NDVI (Normalized Difference Vegetation Index): Overall vegetation health - SAVI (Soil Adjusted Vegetation Index): Better for sparse vegetation - EVI (Enhanced Vegetation Index): Better for dense vegetation Soil & Water Indices: - BSI (Bare Soil Index): Identifies exposed soil areas - NDWI (Normalized Difference Water Index): Water body detection - NDMI (Normalized Difference Moisture Index): Vegetation moisture content Terrain Analysis: - Slope and aspect from Copernicus DEM GLO-30 (30m resolution) - Erosion risk combining slope steepness and bare soil exposure Land Cover: - IO-LULC annual land cover classification (2017-2023) The Rehabilitation Score combines vegetation health, improvement trends, soil stability, and moisture status compared to reference conditions.""" pdf.chapter_body(methodology_text) # Data Sources and Disclaimers pdf.add_page() pdf.chapter_title('Data Sources') sources_text = """- Satellite Imagery: Copernicus Sentinel-2 L2A (Surface Reflectance) - Spatial Resolution: 10 meters - Temporal Resolution: ~5 days revisit - Cloud Masking: Applied using Scene Classification Layer (SCL) - Compositing Method: Median composite over 30-day windows - Digital Elevation: Copernicus DEM GLO-30 (30m resolution) - Land Cover: IO-LULC Annual v02 (10m resolution, 2017-2023) - Data Access: Microsoft Planetary Computer (free, open access)""" pdf.chapter_body(sources_text) pdf.chapter_title('Disclaimer') disclaimer_text = """This report is generated automatically using satellite remote sensing data and should be used for preliminary assessment purposes only. Results may be affected by: - Cloud cover and atmospheric conditions - Seasonal vegetation variations - Sensor calibration differences - Topographic effects For regulatory compliance or detailed rehabilitation assessment, ground-based verification is recommended. This analysis does not constitute professional advice and should be interpreted by qualified personnel. RehabWatch is a demonstration tool and the developers assume no liability for decisions made based on this analysis.""" pdf.chapter_body(disclaimer_text) # Output PDF bytes return bytes(pdf.output()) def stats_to_csv( stats: Dict[str, float], tenement_id: str, rehab_score: int, date_before: str, date_after: str, mine_name: Optional[str] = None ) -> str: """ Convert statistics to CSV format. Args: stats: Statistics dictionary tenement_id: Mining tenement identifier rehab_score: Rehabilitation score date_before: Analysis start date date_after: Analysis end date mine_name: Optional mine name Returns: CSV string for download """ lines = [] # Header lines.append("RehabWatch Rehabilitation Assessment Export") lines.append(f"Generated,{datetime.now().strftime('%Y-%m-%d %H:%M')}") if mine_name: lines.append(f"Site Name,{mine_name}") lines.append(f"Tenement ID,{tenement_id}") lines.append(f"Analysis Start Date,{date_before}") lines.append(f"Analysis End Date,{date_after}") lines.append("") # Statistics lines.append("Metric,Value,Unit") lines.append(f"Rehabilitation Score,{rehab_score},/100") lines.append(f"NDVI Before (mean),{stats['ndvi_before_mean']:.4f},index") lines.append(f"NDVI After (mean),{stats['ndvi_after_mean']:.4f},index") lines.append(f"NDVI Change (mean),{stats['ndvi_change_mean']:.4f},index") lines.append(f"NDVI Change (std dev),{stats['ndvi_change_std']:.4f},index") lines.append(f"Relative Change,{stats['percent_change']:.2f},%") lines.append(f"SAVI Before,{stats.get('savi_before_mean', 0):.4f},index") lines.append(f"SAVI After,{stats.get('savi_after_mean', 0):.4f},index") lines.append(f"EVI Before,{stats.get('evi_before_mean', 0):.4f},index") lines.append(f"EVI After,{stats.get('evi_after_mean', 0):.4f},index") lines.append(f"NDWI After,{stats.get('ndwi_after_mean', 0):.4f},index") lines.append(f"NDMI After,{stats.get('ndmi_after_mean', 0):.4f},index") lines.append(f"BSI After,{stats.get('bsi_after_mean', 0):.4f},index") lines.append(f"Water Presence,{stats.get('percent_water', 0):.2f},%") lines.append(f"Bare Soil Extent,{stats.get('percent_bare_soil', 0):.2f},%") lines.append(f"Moisture Stressed,{stats.get('percent_moisture_stressed', 0):.2f},%") lines.append(f"Sparse Vegetation,{stats.get('percent_sparse_veg', 0):.2f},%") lines.append(f"Low Vegetation,{stats.get('percent_low_veg', 0):.2f},%") lines.append(f"Moderate Vegetation,{stats.get('percent_moderate_veg', 0):.2f},%") lines.append(f"Dense Vegetation,{stats.get('percent_dense_veg', 0):.2f},%") lines.append(f"Area Improved,{stats['area_improved_ha']:.2f},hectares") lines.append(f"Area Stable,{stats['area_stable_ha']:.2f},hectares") lines.append(f"Area Degraded,{stats['area_degraded_ha']:.2f},hectares") lines.append(f"Total Area,{stats['total_area_ha']:.2f},hectares") lines.append(f"Percentage Improved,{stats['percent_improved']:.2f},%") lines.append(f"Percentage Stable,{stats['percent_stable']:.2f},%") lines.append(f"Percentage Degraded,{stats['percent_degraded']:.2f},%") return "\n".join(lines) def generate_summary_text( tenement_id: str, stats: Dict[str, float], rehab_score: int, interpretation: str, date_before: str, date_after: str ) -> str: """ Generate a plain text summary of the assessment. Args: tenement_id: Mining tenement identifier stats: Statistics dictionary rehab_score: Rehabilitation score interpretation: Interpretation text date_before: Analysis start date date_after: Analysis end date Returns: Formatted text summary """ summary = f""" ================================================================================ REHABWATCH REHABILITATION ASSESSMENT ================================================================================ Tenement: {tenement_id} Analysis Period: {date_before} to {date_after} Report Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')} -------------------------------------------------------------------------------- REHABILITATION SCORE -------------------------------------------------------------------------------- {rehab_score} / 100 -------------------------------------------------------------------------------- KEY FINDINGS -------------------------------------------------------------------------------- {interpretation} -------------------------------------------------------------------------------- DETAILED STATISTICS -------------------------------------------------------------------------------- Vegetation Index (NDVI): - Before: {stats['ndvi_before_mean']:.4f} - After: {stats['ndvi_after_mean']:.4f} - Change: {stats['ndvi_change_mean']:.4f} ({stats['percent_change']:.2f}%) Area Analysis: - Total Area: {stats['total_area_ha']:.2f} ha - Area Improved: {stats['area_improved_ha']:.2f} ha ({stats['percent_improved']:.2f}%) - Area Stable: {stats['area_stable_ha']:.2f} ha ({stats['percent_stable']:.2f}%) - Area Degraded: {stats['area_degraded_ha']:.2f} ha ({stats['percent_degraded']:.2f}%) ================================================================================ DATA SOURCES ================================================================================ Satellite: Copernicus Sentinel-2 L2A Resolution: 10 meters Analysis: NDVI vegetation index ================================================================================ """ return summary