""" 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: # Convert construction plan to dictionary plan_dict = _construction_plan_to_dict(construction_plan) # Generate filename if not provided if not output_path: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"construction_plan_{timestamp}.json" # Ensure output directory exists output_file = Path(output_path) output_file.parent.mkdir(parents=True, exist_ok=True) # Write JSON file 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: # Generate filename if not provided if not output_path: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"construction_plan_{timestamp}.pdf" # Ensure output directory exists output_file = Path(output_path) output_file.parent.mkdir(parents=True, exist_ok=True) # Generate PDF content pdf_content = _generate_pdf_content(construction_plan) # For now, create a simple HTML-to-PDF conversion # In production, use a proper PDF library like reportlab or weasyprint try: from weasyprint import HTML HTML(string=pdf_content).write_pdf(output_file) except (ImportError, OSError, Exception) as e: # Fallback: Save as HTML if weasyprint not available or fails 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 already a dict, return it 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) # Ensure visualization data is included if present if hasattr(construction_plan, 'visualization') and construction_plan.visualization: logger.info("Including visualization data in JSON export") # Ensure house specifications are included if present 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""" # Handle both dict and object formats 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 # Helper function to safely get nested values def get_value(obj, key, default=''): if isinstance(obj, dict): return obj.get(key, default) return getattr(obj, key, default) # Extract metadata values 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"""
Building Type: {building_type.replace('_', ' ').title()}
Location: {location_name}
Coordinates: {lat:.4f}°N, {lon:.4f}°E
{f"Building Area: {building_area:.2f} sq meters
" if building_area else ""}Generated: {generated_at}
Overall Risk: {overall_risk}
Generated: {viz_timestamp}
Model: {viz_model}
""" # Add the image if available if viz_base64: html += f"""Design Prompt: {viz_prompt}
""" html += """Overall Risk Level: {risk.summary.overall_risk_level}
Total Hazards Assessed: {risk.summary.total_hazards_assessed}
High Risk Hazards: {risk.summary.high_risk_count}
| Number of Floors | """ + str(house_specs.number_of_floors) + """ |
| Primary Material | """ + str(house_specs.primary_material).replace('_', ' ').title() + """ |
| Foundation Type | """ + str(house_specs.foundation_type).replace('_', ' ').title() + """ |
| Foundation Depth | """ + f"{house_specs.foundation_depth_meters:.2f} meters" + """ |
| Roof Type | """ + str(house_specs.roof_type).replace('_', ' ').title() + """ |
| Wall Thickness | """ + f"{house_specs.wall_thickness_mm} mm" + """ |
{house_specs.reinforcement_details}
""" # Structural features if house_specs.structural_features: html += """| Feature | Specification | Justification |
|---|---|---|
| {feature.feature_name} | {feature.specification} | {feature.justification} |
{house_specs.decision_rationale}
""" html += """Total Estimate Range:
| Material | Category | Unit Price | Quantity | Total |
|---|---|---|---|---|
| {material.material_name} | {material.category} | {material.currency} {material.price_per_unit:,.2f}/{material.unit} | {quantity_str} | {total_str} |