Spaces:
Running
Running
| """ | |
| 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 | |