|
|
""" |
|
|
Export Utilities for Construction Plans |
|
|
Handles PDF and JSON export functionality |
|
|
""" |
|
|
|
|
|
import json |
|
|
import logging |
|
|
from typing import Dict, Any, Optional |
|
|
from datetime import datetime |
|
|
from pathlib import Path |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
def export_to_json(construction_plan: Any, output_path: Optional[str] = None) -> str: |
|
|
""" |
|
|
Export construction plan to JSON format |
|
|
|
|
|
Args: |
|
|
construction_plan: Construction plan object |
|
|
output_path: Optional output file path |
|
|
|
|
|
Returns: |
|
|
Path to exported JSON file |
|
|
""" |
|
|
try: |
|
|
|
|
|
plan_dict = _construction_plan_to_dict(construction_plan) |
|
|
|
|
|
|
|
|
if not output_path: |
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
|
output_path = f"construction_plan_{timestamp}.json" |
|
|
|
|
|
|
|
|
output_file = Path(output_path) |
|
|
output_file.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f: |
|
|
json.dump(plan_dict, f, indent=2, ensure_ascii=False) |
|
|
|
|
|
logger.info(f"Construction plan exported to JSON: {output_file}") |
|
|
return str(output_file) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to export to JSON: {str(e)}") |
|
|
raise |
|
|
|
|
|
|
|
|
def export_to_pdf(construction_plan: Any, output_path: Optional[str] = None) -> str: |
|
|
""" |
|
|
Export construction plan to PDF format |
|
|
|
|
|
Args: |
|
|
construction_plan: Construction plan object |
|
|
output_path: Optional output file path |
|
|
|
|
|
Returns: |
|
|
Path to exported PDF file |
|
|
""" |
|
|
try: |
|
|
|
|
|
if not output_path: |
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
|
output_path = f"construction_plan_{timestamp}.pdf" |
|
|
|
|
|
|
|
|
output_file = Path(output_path) |
|
|
output_file.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
|
|
|
pdf_content = _generate_pdf_content(construction_plan) |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
from weasyprint import HTML |
|
|
HTML(string=pdf_content).write_pdf(output_file) |
|
|
except (ImportError, OSError, Exception) as e: |
|
|
|
|
|
logger.warning(f"weasyprint not available or failed ({type(e).__name__}), saving as HTML instead") |
|
|
output_file = output_file.with_suffix('.html') |
|
|
with open(output_file, 'w', encoding='utf-8') as f: |
|
|
f.write(pdf_content) |
|
|
|
|
|
logger.info(f"Construction plan exported to PDF: {output_file}") |
|
|
return str(output_file) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to export to PDF: {str(e)}") |
|
|
raise |
|
|
|
|
|
|
|
|
def _construction_plan_to_dict(construction_plan: Any) -> Dict[str, Any]: |
|
|
"""Convert construction plan object to dictionary""" |
|
|
from dataclasses import asdict, is_dataclass |
|
|
|
|
|
|
|
|
if isinstance(construction_plan, dict): |
|
|
logger.info("Construction plan is already a dictionary") |
|
|
if 'visualization' in construction_plan and construction_plan['visualization']: |
|
|
logger.info("Including visualization data in JSON export") |
|
|
if 'house_specifications' in construction_plan and construction_plan['house_specifications']: |
|
|
logger.info("Including house specifications in JSON export") |
|
|
return construction_plan |
|
|
elif is_dataclass(construction_plan): |
|
|
plan_dict = asdict(construction_plan) |
|
|
|
|
|
if hasattr(construction_plan, 'visualization') and construction_plan.visualization: |
|
|
logger.info("Including visualization data in JSON export") |
|
|
|
|
|
if hasattr(construction_plan, 'house_specifications') and construction_plan.house_specifications: |
|
|
logger.info("Including house specifications in JSON export") |
|
|
return plan_dict |
|
|
elif hasattr(construction_plan, '__dict__'): |
|
|
return construction_plan.__dict__ |
|
|
else: |
|
|
return {'data': str(construction_plan)} |
|
|
|
|
|
|
|
|
def _generate_pdf_content(construction_plan: Any) -> str: |
|
|
"""Generate HTML content for PDF export""" |
|
|
|
|
|
|
|
|
if isinstance(construction_plan, dict): |
|
|
metadata = construction_plan.get('metadata', {}) |
|
|
summary = construction_plan.get('executive_summary', {}) |
|
|
risk = construction_plan.get('risk_assessment', {}) |
|
|
house_specs = construction_plan.get('house_specifications') |
|
|
recommendations = construction_plan.get('construction_recommendations', {}) |
|
|
costs = construction_plan.get('material_costs', {}) |
|
|
facilities = construction_plan.get('critical_facilities', {}) |
|
|
visualization = construction_plan.get('visualization') |
|
|
else: |
|
|
metadata = construction_plan.metadata |
|
|
summary = construction_plan.executive_summary |
|
|
risk = construction_plan.risk_assessment |
|
|
house_specs = construction_plan.house_specifications if hasattr(construction_plan, 'house_specifications') else None |
|
|
recommendations = construction_plan.construction_recommendations |
|
|
costs = construction_plan.material_costs |
|
|
facilities = construction_plan.critical_facilities |
|
|
visualization = construction_plan.visualization if hasattr(construction_plan, 'visualization') else None |
|
|
|
|
|
|
|
|
def get_value(obj, key, default=''): |
|
|
if isinstance(obj, dict): |
|
|
return obj.get(key, default) |
|
|
return getattr(obj, key, default) |
|
|
|
|
|
|
|
|
building_type = get_value(metadata, 'building_type', 'Unknown') |
|
|
location = get_value(metadata, 'location', {}) |
|
|
location_name = get_value(location, 'name', 'Unknown') |
|
|
coordinates = get_value(metadata, 'coordinates', {}) |
|
|
lat = get_value(coordinates, 'latitude', 0) |
|
|
lon = get_value(coordinates, 'longitude', 0) |
|
|
building_area = get_value(metadata, 'building_area') |
|
|
generated_at = get_value(metadata, 'generated_at', 'Unknown') |
|
|
overall_risk = get_value(summary, 'overall_risk', 'Unknown') |
|
|
|
|
|
html = f""" |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<title>Construction Plan Report</title> |
|
|
<style> |
|
|
body {{ |
|
|
font-family: Arial, sans-serif; |
|
|
line-height: 1.6; |
|
|
color: #333; |
|
|
max-width: 800px; |
|
|
margin: 0 auto; |
|
|
padding: 20px; |
|
|
}} |
|
|
h1 {{ |
|
|
color: #2563eb; |
|
|
border-bottom: 3px solid #2563eb; |
|
|
padding-bottom: 10px; |
|
|
}} |
|
|
h2 {{ |
|
|
color: #1e40af; |
|
|
margin-top: 30px; |
|
|
border-bottom: 2px solid #dbeafe; |
|
|
padding-bottom: 5px; |
|
|
}} |
|
|
h3 {{ |
|
|
color: #374151; |
|
|
margin-top: 20px; |
|
|
}} |
|
|
.section {{ |
|
|
margin-bottom: 30px; |
|
|
padding: 15px; |
|
|
background: #f9fafb; |
|
|
border-radius: 8px; |
|
|
}} |
|
|
.critical {{ |
|
|
background: #fef2f2; |
|
|
border-left: 5px solid #dc2626; |
|
|
padding: 15px; |
|
|
margin: 15px 0; |
|
|
}} |
|
|
.warning {{ |
|
|
background: #fef3c7; |
|
|
border-left: 5px solid #f59e0b; |
|
|
padding: 15px; |
|
|
margin: 15px 0; |
|
|
}} |
|
|
.info {{ |
|
|
background: #dbeafe; |
|
|
border-left: 5px solid #2563eb; |
|
|
padding: 15px; |
|
|
margin: 15px 0; |
|
|
}} |
|
|
table {{ |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
margin: 15px 0; |
|
|
}} |
|
|
th, td {{ |
|
|
padding: 10px; |
|
|
text-align: left; |
|
|
border-bottom: 1px solid #e5e7eb; |
|
|
}} |
|
|
th {{ |
|
|
background: #f3f4f6; |
|
|
font-weight: bold; |
|
|
}} |
|
|
ul, ol {{ |
|
|
margin: 10px 0; |
|
|
padding-left: 25px; |
|
|
}} |
|
|
li {{ |
|
|
margin: 5px 0; |
|
|
}} |
|
|
.footer {{ |
|
|
margin-top: 50px; |
|
|
padding-top: 20px; |
|
|
border-top: 2px solid #e5e7eb; |
|
|
font-size: 0.9em; |
|
|
color: #6b7280; |
|
|
}} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1>๐๏ธ Disaster Risk Construction Plan</h1> |
|
|
|
|
|
<div class="section"> |
|
|
<h2>Project Information</h2> |
|
|
<p><strong>Building Type:</strong> {building_type.replace('_', ' ').title()}</p> |
|
|
<p><strong>Location:</strong> {location_name}</p> |
|
|
<p><strong>Coordinates:</strong> {lat:.4f}ยฐN, {lon:.4f}ยฐE</p> |
|
|
{f"<p><strong>Building Area:</strong> {building_area:.2f} sq meters</p>" if building_area else ""} |
|
|
<p><strong>Generated:</strong> {generated_at}</p> |
|
|
</div> |
|
|
|
|
|
<div class="section"> |
|
|
<h2>Executive Summary</h2> |
|
|
<p><strong>Overall Risk:</strong> {overall_risk}</p> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
critical_concerns = get_value(summary, 'critical_concerns', []) |
|
|
if critical_concerns: |
|
|
html += """ |
|
|
<div class="critical"> |
|
|
<h3>โ ๏ธ Critical Concerns</h3> |
|
|
<ul> |
|
|
""" |
|
|
for concern in critical_concerns: |
|
|
html += f"<li>{concern}</li>" |
|
|
html += """ |
|
|
</ul> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
key_recommendations = get_value(summary, 'key_recommendations', []) |
|
|
if key_recommendations: |
|
|
html += """ |
|
|
<div class="warning"> |
|
|
<h3>๐ฏ Key Recommendations</h3> |
|
|
<ol> |
|
|
""" |
|
|
for rec in key_recommendations: |
|
|
html += f"<li>{rec}</li>" |
|
|
html += """ |
|
|
</ol> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
if visualization: |
|
|
viz_timestamp = get_value(visualization, 'generation_timestamp', 'Unknown') |
|
|
viz_model = get_value(visualization, 'model_version', 'Unknown') |
|
|
viz_format = get_value(visualization, 'image_format', 'png') |
|
|
viz_base64 = get_value(visualization, 'image_base64', '') |
|
|
viz_features = get_value(visualization, 'features_included', []) |
|
|
viz_prompt = get_value(visualization, 'prompt_used', '') |
|
|
|
|
|
html += f""" |
|
|
<div class="section"> |
|
|
<h2>๐จ Architectural Visualization</h2> |
|
|
<p><strong>Generated:</strong> {viz_timestamp}</p> |
|
|
<p><strong>Model:</strong> {viz_model}</p> |
|
|
""" |
|
|
|
|
|
|
|
|
if viz_base64: |
|
|
html += f""" |
|
|
<div style="text-align: center; margin: 20px 0;"> |
|
|
<img src="data:image/{viz_format.lower()};base64,{viz_base64}" |
|
|
alt="Architectural Visualization" |
|
|
style="max-width: 100%; height: auto; border: 2px solid #e5e7eb; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
if viz_features: |
|
|
html += """ |
|
|
<h3>Disaster-Resistant Features Shown</h3> |
|
|
<ul> |
|
|
""" |
|
|
for feature in viz_features: |
|
|
html += f"<li>{feature}</li>" |
|
|
html += """ |
|
|
</ul> |
|
|
""" |
|
|
|
|
|
|
|
|
if viz_prompt: |
|
|
html += f""" |
|
|
<p><strong>Design Prompt:</strong> {viz_prompt}</p> |
|
|
""" |
|
|
|
|
|
html += """ |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
html += f""" |
|
|
<div class="section"> |
|
|
<h2>Risk Assessment</h2> |
|
|
<p><strong>Overall Risk Level:</strong> {risk.summary.overall_risk_level}</p> |
|
|
<p><strong>Total Hazards Assessed:</strong> {risk.summary.total_hazards_assessed}</p> |
|
|
<p><strong>High Risk Hazards:</strong> {risk.summary.high_risk_count}</p> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
if house_specs: |
|
|
html += """ |
|
|
<div class="section"> |
|
|
<h2>๐๏ธ House Specifications</h2> |
|
|
<h3>Basic Specifications</h3> |
|
|
<table> |
|
|
<tr> |
|
|
<td><strong>Number of Floors</strong></td> |
|
|
<td>""" + str(house_specs.number_of_floors) + """</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td><strong>Primary Material</strong></td> |
|
|
<td>""" + str(house_specs.primary_material).replace('_', ' ').title() + """</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td><strong>Foundation Type</strong></td> |
|
|
<td>""" + str(house_specs.foundation_type).replace('_', ' ').title() + """</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td><strong>Foundation Depth</strong></td> |
|
|
<td>""" + f"{house_specs.foundation_depth_meters:.2f} meters" + """</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td><strong>Roof Type</strong></td> |
|
|
<td>""" + str(house_specs.roof_type).replace('_', ' ').title() + """</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td><strong>Wall Thickness</strong></td> |
|
|
<td>""" + f"{house_specs.wall_thickness_mm} mm" + """</td> |
|
|
</tr> |
|
|
</table> |
|
|
""" |
|
|
|
|
|
|
|
|
if house_specs.reinforcement_details: |
|
|
html += f""" |
|
|
<h3>Reinforcement Details</h3> |
|
|
<p>{house_specs.reinforcement_details}</p> |
|
|
""" |
|
|
|
|
|
|
|
|
if house_specs.structural_features: |
|
|
html += """ |
|
|
<h3>Structural Features</h3> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Feature</th> |
|
|
<th>Specification</th> |
|
|
<th>Justification</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
""" |
|
|
for feature in house_specs.structural_features: |
|
|
html += f""" |
|
|
<tr> |
|
|
<td><strong>{feature.feature_name}</strong></td> |
|
|
<td>{feature.specification}</td> |
|
|
<td>{feature.justification}</td> |
|
|
</tr> |
|
|
""" |
|
|
html += """ |
|
|
</tbody> |
|
|
</table> |
|
|
""" |
|
|
|
|
|
|
|
|
if house_specs.compliance_codes: |
|
|
html += """ |
|
|
<h3>Compliance Codes</h3> |
|
|
<ul> |
|
|
""" |
|
|
for code in house_specs.compliance_codes: |
|
|
html += f"<li>{code}</li>" |
|
|
html += """ |
|
|
</ul> |
|
|
""" |
|
|
|
|
|
|
|
|
if house_specs.decision_rationale: |
|
|
html += f""" |
|
|
<h3>Decision Rationale</h3> |
|
|
<p style="white-space: pre-wrap;">{house_specs.decision_rationale}</p> |
|
|
""" |
|
|
|
|
|
html += """ |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
if recommendations.general_guidelines: |
|
|
html += """ |
|
|
<div class="section"> |
|
|
<h2>Construction Recommendations</h2> |
|
|
<h3>General Guidelines</h3> |
|
|
<ul> |
|
|
""" |
|
|
for guideline in recommendations.general_guidelines: |
|
|
html += f"<li>{guideline}</li>" |
|
|
html += """ |
|
|
</ul> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
if costs.materials: |
|
|
html += """ |
|
|
<div class="section"> |
|
|
<h2>Material Cost Estimates</h2> |
|
|
""" |
|
|
|
|
|
if costs.total_estimate: |
|
|
html += f""" |
|
|
<p><strong>Total Estimate Range:</strong></p> |
|
|
<ul> |
|
|
<li>Low: {costs.total_estimate.currency} {costs.total_estimate.low:,.2f}</li> |
|
|
<li>Mid: {costs.total_estimate.currency} {costs.total_estimate.mid:,.2f}</li> |
|
|
<li>High: {costs.total_estimate.currency} {costs.total_estimate.high:,.2f}</li> |
|
|
</ul> |
|
|
""" |
|
|
|
|
|
html += """ |
|
|
<h3>Itemized Materials</h3> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Material</th> |
|
|
<th>Category</th> |
|
|
<th>Unit Price</th> |
|
|
<th>Quantity</th> |
|
|
<th>Total</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
""" |
|
|
|
|
|
for material in costs.materials[:20]: |
|
|
quantity_str = f"{material.quantity_needed:.2f} {material.unit}" if material.quantity_needed else "TBD" |
|
|
total_str = f"{material.currency} {material.total_cost:,.2f}" if material.total_cost else "TBD" |
|
|
|
|
|
html += f""" |
|
|
<tr> |
|
|
<td>{material.material_name}</td> |
|
|
<td>{material.category}</td> |
|
|
<td>{material.currency} {material.price_per_unit:,.2f}/{material.unit}</td> |
|
|
<td>{quantity_str}</td> |
|
|
<td>{total_str}</td> |
|
|
</tr> |
|
|
""" |
|
|
|
|
|
html += """ |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
if facilities.schools or facilities.hospitals: |
|
|
html += """ |
|
|
<div class="section"> |
|
|
<h2>Critical Facilities</h2> |
|
|
""" |
|
|
|
|
|
if facilities.schools: |
|
|
html += "<h3>Schools</h3><ul>" |
|
|
for school in facilities.schools[:5]: |
|
|
html += f"<li>{school.name} - {school.distance_meters/1000:.2f} km</li>" |
|
|
html += "</ul>" |
|
|
|
|
|
if facilities.hospitals: |
|
|
html += "<h3>Hospitals</h3><ul>" |
|
|
for hospital in facilities.hospitals[:5]: |
|
|
html += f"<li>{hospital.name} - {hospital.distance_meters/1000:.2f} km</li>" |
|
|
html += "</ul>" |
|
|
|
|
|
html += """ |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
html += """ |
|
|
<div class="footer"> |
|
|
<p><strong>Disclaimer:</strong> This construction plan is generated for guidance purposes only. |
|
|
Always consult with licensed engineers, architects, and local authorities for actual construction projects. |
|
|
Cost estimates are based on current market conditions and may vary.</p> |
|
|
<p>Generated by Disaster Risk Construction Planner</p> |
|
|
</div> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
def create_export_buttons(construction_plan: Any) -> tuple: |
|
|
""" |
|
|
Create export buttons and handlers for Gradio interface |
|
|
|
|
|
Args: |
|
|
construction_plan: Construction plan object |
|
|
|
|
|
Returns: |
|
|
Tuple of (json_button, pdf_button, json_file, pdf_file) |
|
|
""" |
|
|
import gradio as gr |
|
|
|
|
|
def export_json_handler(): |
|
|
try: |
|
|
file_path = export_to_json(construction_plan) |
|
|
return file_path |
|
|
except Exception as e: |
|
|
logger.error(f"JSON export failed: {str(e)}") |
|
|
return None |
|
|
|
|
|
def export_pdf_handler(): |
|
|
try: |
|
|
file_path = export_to_pdf(construction_plan) |
|
|
return file_path |
|
|
except Exception as e: |
|
|
logger.error(f"PDF export failed: {str(e)}") |
|
|
return None |
|
|
|
|
|
json_button = gr.Button("๐ฅ Export as JSON", variant="secondary") |
|
|
pdf_button = gr.Button("๐ Export as PDF", variant="secondary") |
|
|
|
|
|
json_file = gr.File(label="JSON Export", visible=False) |
|
|
pdf_file = gr.File(label="PDF Export", visible=False) |
|
|
|
|
|
json_button.click(fn=export_json_handler, outputs=json_file) |
|
|
pdf_button.click(fn=export_pdf_handler, outputs=pdf_file) |
|
|
|
|
|
return json_button, pdf_button, json_file, pdf_file |
|
|
|