MineWatchAI / src /report.py
Ashkan Taghipour (The University of Western Australia)
Initial commit
f5648f5
"""
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