from __future__ import annotations from .evidence import evidence_to_markdown_table from .models import ReportBundle, SiteSelection from .safety import assert_safe_text, safety_block def build_board_bundle( project_name: str, site_name: str, selection: SiteSelection, climate: dict, osm_context: dict, sun_summary: dict[str, str], site_identity: dict | None, evidence_rows, diagram_paths: list[str], topography: dict | None = None, soil: dict | None = None, warnings: list[str] | None = None, ) -> ReportBundle: board = build_board_markdown( project_name, site_name, selection, climate, osm_context, sun_summary, site_identity, topography=topography, soil=soil, ) checklist = build_site_visit_checklist() bundle = ReportBundle( board_markdown=board, evidence_rows=list(evidence_rows), checklist_markdown=checklist, diagram_paths=diagram_paths, export_path=None, warnings=warnings or [], ) return bundle def build_board_markdown( project_name: str, site_name: str, selection: SiteSelection, climate: dict, osm_context: dict, sun_summary: dict[str, str], site_identity: dict | None = None, topography: dict | None = None, soil: dict | None = None, ) -> str: area = _fmt(selection.area_sqm, "sqm") perimeter = _fmt(selection.perimeter_m, "m") context_counts = osm_context.get("counts") or {} context_text = ", ".join(f"{k}: {v}" for k, v in list(context_counts.items())[:6]) or "OSM context unavailable or sparse." climate_text = _climate_summary_lines(climate) return f"""## Board Preview **Project:** {project_name or "Untitled project"} **Site:** {site_name or "Untitled site"} **Input mode:** {selection.selection_type} **Accuracy label:** {selection.accuracy_label} ### Site Identity {_site_identity_text(site_identity, selection)} ### Boundary Summary - Area: {area} - Perimeter: {perimeter} - Anchor / centroid: {_centroid_text(selection)} - Source / unit: {selection.unit_source} - Limitations: {"; ".join(selection.limitations)} ### Climate Captions {climate_text} ### Geographic Context {context_text} ### Terrain / Soil Signals {_topography_summary(topography)} {_soil_summary(soil)} ### Sun / Wind - {sun_summary.get("orientation_note", "Orientation summary unavailable.")} - {sun_summary.get("east_west_note", "")} - {sun_summary.get("wind_note", "")} - Limitation: {sun_summary.get("limitation", "No shadow simulation.")} ### Constraints / Opportunities - Treat west and afternoon exposure as a likely shading/glare item to verify. - Treat mapped roads/access as preliminary until road width and entry conditions are checked. - Treat water, green, built-up, and amenity context as source-backed only where evidence rows exist. - Keep culture/demographic context as editable field observations unless supported by reliable sources. {build_detailed_site_analysis(selection, climate, osm_context, sun_summary, site_identity, topography, soil)} """ def build_detailed_site_analysis( selection: SiteSelection, climate: dict, osm_context: dict, sun_summary: dict[str, str], site_identity: dict | None, topography: dict | None = None, soil: dict | None = None, ) -> str: context_counts = osm_context.get("counts") or {} current = climate.get("forecast") or {} recent = climate.get("recent_historical") or {} normal = climate.get("climate_normal") or {} return f""" ## Detailed Site Analysis Workbook This section is intentionally longer than the presentation board. It is meant to help a student understand what to draw, what to write, what is already supported by data, and what must still be checked on site. ### Reference Framework The report structure uses common architecture and urban-design site-analysis categories, rewritten into this app's own evidence-first format: - Urban Design Lab's site-analysis guide groups analysis around site location, neighbourhood context, site-specific conditions, natural features, man-made features, circulation, utilities, sensory conditions, human/cultural context, and climate. Source: https://urbandesignlab.in/urban-design-site-analysis/ - First In Architecture separates hard data and soft data and treats site analysis as a way to understand external conditions before design decisions. Source: https://www.firstinarchitecture.co.uk/architecture-site-analysis-guide/ - Archisoup emphasizes site visits, visual aids, GIS/CAD/environmental tools, and checklists for turning site information into design proposals. Source: https://www.archisoup.com/architecture-site-analysis-introduction - ProjectManager describes site analysis as combining social, historical, climatic, geographic, legal, and infrastructure aspects into maps, diagrams, and written analysis. Source: https://www.projectmanager.com/blog/site-analysis-in-architecture These references are used as learning/checklist references only. They are not treated as factual evidence about this specific site. ### 1. Location And Administrative Context **What the app found** {_site_identity_text(site_identity, selection)} **Why it matters** - Establishes the site in relation to city, region, access routes, climate zone, and surrounding settlement pattern. - Helps label studio sheets correctly and prevents analysis from becoming generic. - Supports later research into local regulations, heritage, materials, and culture. **What to draw or include** - Location map at regional scale. - City/town context map. - Immediate access map with road names and approach directions. - A short location note with latitude/longitude and approximate address. **Verify manually** - Final project address, plot name, survey number, ownership source, and administrative boundary. - Whether the site is part of a heritage, tourism, coastal, institutional, or special planning zone. ### 2. Boundary, Size, Scale And Input Reliability **What the app found** - Input mode: {selection.selection_type} - Accuracy label: {selection.accuracy_label} - Area: {_fmt(selection.area_sqm, "sqm")} - Perimeter: {_fmt(selection.perimeter_m, "m")} - Anchor / centroid: {_centroid_text(selection)} - Unit/source note: {selection.unit_source} - Known limits: {"; ".join(selection.limitations)} **Why it matters** - Area and perimeter affect massing, open-space ratios, circulation length, service access, and phasing. - Boundary reliability controls how strongly any plot-level conclusion can be trusted. **What to draw or include** - Boundary diagram with north arrow and scale. - Area/perimeter box. - Source label: faculty CAD, survey, KML/Google Earth, GeoJSON, drawn map, or pin-radius. - Accuracy note beside every map-based conclusion. **Verify manually** - Compare the drawn/uploaded boundary with CAD, KML/GeoJSON, faculty drawing, or survey documents. - Confirm whether any setbacks, easements, rights of way, coastal buffers, or restricted edges apply. ### 3. Neighbourhood Context **What the app found** {_context_count_lines(context_counts)} **Why it matters** - Surrounding land use, building heights, road hierarchy, public transport, activity nodes, and edges shape entry, privacy, visibility, noise, and frontage strategy. - Neighbourhood context also tells whether the project should blend, contrast, repair, or intensify existing patterns. **What to draw or include** - Figure-ground / built-void diagram. - Land-use or surrounding-use diagram. - Nearby landmarks and activity nodes. - Edge-condition diagram for each side of the site. - Positive and negative context arrows. **Verify manually** - Building uses, building heights, active frontages, parking behavior, pedestrian movement, informal activity, and public transport stops. - Any upcoming road widening, development, or local planning change. ### 4. Natural Features, Landscape And Drainage **What the app can support now** - The current prototype records mapped green/water features where OSM data is available. {_topography_summary(topography)} - It does not yet run contour extraction, flood-risk, NDVI, or tree-canopy analysis. **Why it matters** - Topography affects cut-fill, access gradients, views, drainage, retaining structures, and universal access. - Vegetation affects shade, microclimate, ecology, site identity, and construction constraints. - Water edges and drainage patterns affect risk, orientation, humidity, views, and setbacks. **What to draw or include** - Slope/contour diagram where contour or DEM data is available. - Drainage direction and low-point diagram. - Existing tree/vegetation map. - Water-body and buffer/edge diagram where relevant. - Landscape opportunities and constraints. **Verify manually** - Actual slope, waterlogging, erosion, soil dampness, tree species/health, drainage outlets, and seasonal water changes. ### 5. Man-Made Features, Built Context And Heritage **What the app can support now** - DXF layer scan can flag likely existing building, road, contour, water, vegetation, and built-up layers when layer names exist. - OSM context can count nearby mapped roads, amenities, land use, green/open space, and water features. **Why it matters** - Existing structures, heritage elements, damaged buildings, retaining walls, fences, utilities, and previous land use can become design anchors or constraints. - Heritage and adaptive reuse conditions are not just visual issues; they affect circulation, phasing, approval, and public meaning. **What to draw or include** - Existing built-form map. - Heritage/significant-structure note. - Material and architectural-character board. - Demolition/retain/reuse diagram if existing structures are part of the project. **Verify manually** - Building condition, ownership, heritage status, previous use, contamination risk, structural damage, and what must be retained. ### 6. Movement, Access And Circulation **What the app found** {_movement_summary(context_counts)} **Why it matters** - Access governs arrival sequence, service movement, emergency entry, pedestrian safety, parking, public transport connection, and construction logistics. - For resorts, campuses, institutions, housing, and public buildings, movement hierarchy often becomes the organizing diagram. **What to draw or include** - Vehicular approach diagram. - Pedestrian approach diagram. - Public transport / bus-stop / station proximity if relevant. - Service and emergency access assumptions. - Desire lines and conflict points. **Verify manually** - Road width, turning radius, traffic speed, peak hours, pedestrian crossings, bus stops, parking pressure, blocked entries, and construction access. ### 7. Utilities, Services And Infrastructure **What the app can support now** - The prototype does not claim utility locations from public data. - Utilities must remain a site-visit and document-verification section unless user/CAD evidence is uploaded. **Why it matters** - Water, drainage, sewer, electricity, fire access, telecom, transformers, poles, and service corridors influence site planning and feasibility. **What to draw or include** - Existing utilities map if provided by CAD/site survey. - Drainage outlet and service approach diagram. - Missing-services checklist. **Verify manually** - Electricity poles, substations, overhead lines, water supply, sewer/stormwater drains, manholes, fire access, service roads, and any underground services. ### 8. Sensory Analysis: Views, Noise, Odour, Pollution And Experience **What the app can support now** - The app can prompt this analysis but cannot verify sensory conditions remotely. **Why it matters** - Views shape orientation, openings, public/private zoning, and experience. - Noise, smell, glare, dust, traffic, and pollution can define buffers, planting, services, and facade strategy. **What to draw or include** - Positive views. - Negative views. - Noise source diagram. - Smell/dust/pollution edge diagram. - Arrival-sequence photos. **Verify manually** - Morning/evening light, glare, sea/road/industry smells, traffic noise, crowding, informal use, and seasonal changes. ### 9. Human, Cultural And Activity Context **What the app can support now** - User-written local/culture notes are included as user-provided evidence. - The app does not invent demographics, culture, crime, income, or social behavior. **Why it matters** - Architecture is used by people with routines, rituals, economies, seasonal patterns, and expectations. - For Indian sites, local climate, material culture, festival use, informal vendors, tourism, community memory, and daily movement can be decisive. **What to draw or include** - Activity mapping by time of day. - User group matrix. - Cultural/material reference board. - Local typology and streetscape study. - Stakeholder and conflict/opportunity notes. **Verify manually** - Local interviews, observation at different times, student/faculty/client brief, census/government sources if demographics are needed, and local planning documents. ### 10. Climate, Sun, Wind And Microclimate **What the app found** {_climate_summary_lines(climate)} - Sun/orientation note: {sun_summary.get("orientation_note", "not available")} - Wind note: {sun_summary.get("wind_note", "not available")} **Why it matters** - Climate affects shade, roof design, ventilation, material durability, outdoor comfort, drainage, landscape, and energy strategy. - Sun path and western exposure help decide massing, openings, shading, courtyards, and thermal buffers. - Wind direction is useful for ventilation concepts but needs local verification because public data is regional/modelled. **What to draw or include** - Sun-path / orientation diagram. - Wind direction or wind-rose diagram. - Monthly temperature-rainfall chart. - Shading and heat-gain notes for east, west, north, and south edges. - Outdoor comfort and rain-protection implications. **Verify manually** - Actual shade from adjacent buildings/trees, local wind channels, glare, humid pockets, drainage behavior after rain, and thermal comfort during site visit. ### 11. Soil, Ground And Foundation Caution **What the app says safely** {_soil_summary(soil)} - This prototype does not generate final soil or foundation recommendations. - Soil/foundation is treated as a professional-verification item. **Why it matters** - Soil texture, bearing capacity, water table, settlement, fill, erosion, and contamination can affect foundation design, excavation, retaining walls, drainage, and cost. **What to draw or include** - Soil information source note if available. - Ground-risk checklist. - Areas needing geotechnical confirmation. **Verify manually** - Geotechnical report, borehole data, soil test, local engineer input, water table, previous fill, nearby construction behavior, and drainage/settlement signs. ### 12. Regulation, Ownership And Legal Constraints **What the app can support now** - The app does not verify legal/cadastral boundaries, zoning, ownership, FSI/FAR, CRZ, heritage controls, setbacks, or approvals. **Why it matters** - Regulations define what can be built, where it can be built, how high it can be, how much open space is required, and what must be protected. **What to draw or include** - Regulation checklist. - Setback/easement/right-of-way diagram if documents are available. - Risk register for unknown approvals. **Verify manually** - Local development control regulations, coastal/heritage/environmental rules, ownership documents, survey/cadastral records, and authority approvals. ### 13. Issues, Constraints And Opportunities Matrix | Theme | Likely issue | Opportunity | Evidence status | Action | |---|---|---|---|---| | Boundary | Boundary may be approximate depending on source | Use CAD/GeoJSON/survey to improve precision | User input / computed | Verify source and redraw if needed | | Climate | Heat, rain, humidity, and wind are modelled/contextual | Use climate-responsive shading, ventilation, drainage | Public data / modelled | Confirm comfort and rain behavior on site | | Access | Road/access data may be incomplete | Organize entry, service, and pedestrian hierarchy | OSM / field needed | Measure road widths and observe peak movement | | Natural systems | Slope, trees, drainage, and water behavior need field evidence | Use landscape, shade, water edge, and drainage as design generators | Mostly field needed | Photograph and map on site | | Built context | Surrounding heights, materials, and uses need observation | Build context-responsive massing and edge conditions | OSM/CAD/user notes | Survey edges and nearby typologies | | Culture/activity | Social use cannot be inferred safely | Create locally grounded program and public/private transitions | User/site visit | Observe, interview, and document | | Soil/ground | Ground conditions are not confirmed | Plan professional tests early | Professional verification | Request geotechnical input | ### 14. Diagram And Sheet Production Checklist Use this as a board-making checklist: - Regional location map. - City/town context map. - Site boundary and dimensions. - Figure-ground / built-void. - Land use / surrounding-use map. - Road hierarchy and access. - Pedestrian movement / desire lines. - Public transport proximity. - Sun path and shade exposure. - Wind direction / ventilation cue. - Temperature-rainfall-humidity chart. - Slope/topography/drainage. - Vegetation/tree cover. - Water body / drainage edge. - Views: positive and negative. - Noise/odour/pollution sources. - Heritage/existing structures. - Materials, vernacular, typology. - Utilities/services. - Constraints-opportunities synthesis. - Site visit photo key plan. ### 15. Before, During And After Site Visit **Before site visit** - Print/export boundary, access map, and checklist. - Mark missing data: soil, utilities, road width, slope, tree condition, surrounding heights, noise, views, and local activity. - Prepare photo list: north, south, east, west, corners, road edge, water/vegetation, existing structures, utilities, and approach sequence. **During site visit** - Walk the boundary and record edge conditions. - Photograph all approach roads and entrances. - Record shade, wind, noise, smell, dust, traffic, slope, drainage, waterlogging, trees, and local activity. - Ask locals/security/faculty/client about seasonal flooding, traffic peaks, safety, ownership, and previous use. **After site visit** - Separate confirmed observations from assumptions. - Update diagrams with photos and manual notes. - Mark each report claim as public data, computed, user-observed, or professional-verification required. - Convert constraints and opportunities into design moves. """ def _context_count_lines(context_counts: dict) -> str: if not context_counts: return "- OSM context unavailable or sparse for this selected radius." lines = [] for key, value in list(context_counts.items())[:10]: label = str(key).replace("_", " ") try: count = int(value) except (TypeError, ValueError): count = value lines.append(f"- {label}: {count} mapped feature(s)") return "\n".join(lines) def _topography_summary(topography: dict | None) -> str: if not topography: return "- Topography/elevation unavailable in this run; verify slope, contours, drainage, and low points manually." mean_elev = topography.get("mean_elevation_m", "n/a") relief = topography.get("relief_m", "n/a") slope = topography.get("approx_slope_pct", "n/a") interpretation = topography.get("interpretation", "Use only as preliminary terrain context.") return ( f"- Public elevation signal: mean {mean_elev} m, sampled relief {relief} m, " f"approximate sampled slope {slope}%. {interpretation}" ) def _soil_summary(soil: dict | None) -> str: if not soil: return "- Soil signal unavailable in this run; use geotechnical report, local engineer input, or official soil maps." pieces = [soil.get("texture_signal", "soil texture signal unavailable")] if soil.get("clay_pct") is not None: pieces.append(f"clay {soil['clay_pct']}%") if soil.get("sand_pct") is not None: pieces.append(f"sand {soil['sand_pct']}%") if soil.get("silt_pct") is not None: pieces.append(f"silt {soil['silt_pct']}%") if soil.get("ph_h2o") is not None: pieces.append(f"pH {soil['ph_h2o']}") implication = soil.get("design_implication", "Use only as a preliminary prompt for soil verification.") return "- Preliminary SoilGrids signal: " + ", ".join(pieces) + f". {implication}" def _climate_summary_lines(climate: dict) -> str: forecast = climate.get("forecast") or {} recent = climate.get("recent_historical") or {} normal = climate.get("climate_normal") or {} lines = [] if _has_values(forecast): lines.append( "- **Forecast/current:** " f"temperature {forecast.get('current_temperature_c', 'n/a')} C, " f"humidity {forecast.get('current_humidity_pct', 'n/a')}%, " f"wind {forecast.get('current_wind_speed_kmh', 'n/a')} km/h. " "Use for site-visit timing only." ) else: lines.append( "- **Forecast/current:** unavailable in this run. Do not make immediate weather or site-visit-timing claims from this layer." ) if _has_values(recent): period = recent.get("period", "recent archive period") lines.append( "- **Recent historical:** retrieved from Open-Meteo historical archive " f"for {period}; use only as modelled recent context." ) else: lines.append( "- **Recent historical:** unavailable in this run. Verify recent heat, rainfall, humidity, and wind tendencies separately." ) if _has_values(normal): period = normal.get("period", "multi-year historical archive") total_rain = normal.get("total_precipitation_mm", "n/a") lines.append( "- **Climate-normal style:** computed from Open-Meteo historical archive " f"for {period}; approximate annual precipitation {total_rain} mm. " "This is not an official climatological normal." ) else: lines.append( "- **Climate-normal style:** unavailable in this run. Use a studio-approved climate source before making long-term climate claims." ) return "\n".join(lines) def _has_values(value: object) -> bool: if not isinstance(value, dict): return False for item in value.values(): if isinstance(item, list): if any(_has_values(child) if isinstance(child, dict) else child is not None for child in item): return True elif item is not None: return True return False def _movement_summary(context_counts: dict) -> str: if not context_counts: return "- OSM road/access data was unavailable or sparse for this selected radius." matches = [] for key, value in context_counts.items(): label = str(key).replace("_", " ") lower = label.lower() if any(token in lower for token in ["road", "access", "highway", "transport"]): matches.append(f"{label}: {value}") if matches: return "- Road/access mapped features: " + ", ".join(matches[:5]) + "." return "- No road/access-specific count was found in the retrieved OSM summary; verify approach roads manually." def build_site_visit_checklist() -> str: return """- Confirm actual plot edges and boundary source. - Measure or verify access road width and entry points. - Photograph road edge, pedestrian access, and service approach. - Check drainage, low points, waterlogging, and runoff direction. - Observe slope/topography and compare with any contour or DEM evidence. - Record major trees, shade, vegetation health, and removal constraints. - Check surrounding building heights and likely shadow/glare effects. - Note noise, dust, traffic, smell, and activity peaks. - Identify visible utilities/services and missing service information. - Record local activity, community use, culture, materials, and typology. - Ask for soil/geotechnical reports before any foundation decision. - Take photos in all cardinal directions plus key edges/corners.""" def build_markdown_report( project_name: str, site_name: str, selection: SiteSelection, bundle: ReportBundle, ) -> str: warnings = "\n".join(f"- {w}" for w in bundle.warnings) report = f"""# Site Intelligence Studio Report ## Safety And Scope {safety_block()} ## Site - Project: {project_name or "Untitled project"} - Site: {site_name or "Untitled site"} - Input mode: {selection.selection_type} - Accuracy: {selection.accuracy_label} {bundle.board_markdown} ## Site Visit Checklist {bundle.checklist_markdown} ## Evidence Table {evidence_to_markdown_table(bundle.evidence_rows)} ## Warnings {warnings if warnings else "- No additional warnings."} """ assert_safe_text(report) return report def _fmt(value: float | None, suffix: str) -> str: if value is None: return "not available" return f"{value:,.1f} {suffix}" def _centroid_text(selection: SiteSelection) -> str: if selection.anchor_lat is not None and selection.anchor_lon is not None: return f"{selection.anchor_lat:.6f}, {selection.anchor_lon:.6f}" if selection.centroid: return f"{selection.centroid[0]:.2f}, {selection.centroid[1]:.2f}" return "not available" def _site_identity_text(site_identity: dict | None, selection: SiteSelection) -> str: lines = [ f"- Latitude / longitude: {_centroid_text(selection)}", ] if not site_identity: lines.append("- Address context: not available.") return "\n".join(lines) ordered = [ ("Approximate address", site_identity.get("display_name")), ("Street / road", site_identity.get("road")), ("Neighbourhood", site_identity.get("neighbourhood")), ("City / town", site_identity.get("city")), ("District", site_identity.get("district")), ("State", site_identity.get("state")), ("Country", site_identity.get("country")), ("Postcode", site_identity.get("postcode")), ] for label, value in ordered: if value: lines.append(f"- {label}: {value}") if len(lines) == 1: lines.append("- Address context: reverse geocoding returned no usable address fields.") lines.append("- Address note: OSM-derived address context is approximate and must be verified.") return "\n".join(lines)