| 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) |
|
|