Eishaan commited on
Commit
338ebb6
·
1 Parent(s): 3329d8f

Support polygon-only analysis flow

Browse files
app.py CHANGED
@@ -312,10 +312,28 @@ def generate_site_analysis(
312
  osm_context=osm_context,
313
  topography=topography,
314
  soil=soil,
 
 
315
  )
316
  evidence.extend(sample_evidence)
317
  warnings.extend(sample_warnings)
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  diagram_paths = create_diagrams(
320
  selection,
321
  climate,
@@ -571,6 +589,63 @@ def _current_wind_direction(climate: dict[str, Any]) -> float | None:
571
  return None
572
 
573
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  def _file_path(file_obj: Any) -> str:
575
  return str(getattr(file_obj, "name", file_obj))
576
 
@@ -624,10 +699,24 @@ def build_app() -> gr.Blocks:
624
  )
625
  with gr.Column(scale=5, elem_classes=["control-panel"]):
626
  gr.Markdown("### Project Setup")
627
- project_name = gr.Textbox(label="Project name", value="Architecture Site Analysis")
628
- site_name = gr.Textbox(label="Site name", value="Untitled site")
629
- project_type = gr.Textbox(label="Project type", placeholder="Thesis, resort, housing, institute...")
630
- boundary_source = gr.Textbox(label="Boundary source", placeholder="Faculty CAD, Google Earth/KML, rough map draw, manual pin...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  manual_location = gr.Textbox(
632
  label="Lat/lon or Google Maps URL",
633
  placeholder="21.002, 70.245 or a Google Maps URL with coordinates",
 
312
  osm_context=osm_context,
313
  topography=topography,
314
  soil=soil,
315
+ anchor_lat=selection.anchor_lat,
316
+ anchor_lon=selection.anchor_lon,
317
  )
318
  evidence.extend(sample_evidence)
319
  warnings.extend(sample_warnings)
320
 
321
+ (
322
+ project_name,
323
+ site_name,
324
+ project_type,
325
+ boundary_source,
326
+ auto_label_warnings,
327
+ ) = _resolve_project_fields(
328
+ project_name=project_name,
329
+ site_name=site_name,
330
+ project_type=project_type,
331
+ boundary_source=boundary_source,
332
+ selection=selection,
333
+ site_identity=site_identity,
334
+ )
335
+ warnings.extend(auto_label_warnings)
336
+
337
  diagram_paths = create_diagrams(
338
  selection,
339
  climate,
 
589
  return None
590
 
591
 
592
+ def _resolve_project_fields(
593
+ *,
594
+ project_name: str,
595
+ site_name: str,
596
+ project_type: str,
597
+ boundary_source: str,
598
+ selection: SiteSelection,
599
+ site_identity: dict[str, Any] | None,
600
+ ) -> tuple[str, str, str, str, list[str]]:
601
+ warnings: list[str] = []
602
+ project = (project_name or "").strip()
603
+ site = (site_name or "").strip()
604
+ ptype = (project_type or "").strip()
605
+ source = (boundary_source or "").strip()
606
+
607
+ if not project:
608
+ project = "Preliminary Site Analysis"
609
+ warnings.append("Project name was not provided; the app used a generic title.")
610
+ if not site or site.lower() in {"untitled site", "untitled", "site"}:
611
+ site = _auto_site_name(selection, site_identity)
612
+ warnings.append("Site name was not provided; the app generated a label from the selected area.")
613
+ if not ptype:
614
+ ptype = "Early-stage architecture site analysis"
615
+ if not source:
616
+ source = _auto_boundary_source(selection)
617
+ warnings.append("Boundary source was not provided; the report labels the selection from the input mode.")
618
+ return project, site, ptype, source, warnings
619
+
620
+
621
+ def _auto_site_name(selection: SiteSelection, site_identity: dict[str, Any] | None) -> str:
622
+ if site_identity:
623
+ for key in ("city", "town", "village", "district", "state"):
624
+ value = site_identity.get(key)
625
+ if value:
626
+ return f"{value} selected site"
627
+ display = site_identity.get("display_name")
628
+ if display:
629
+ return f"{str(display).split(',')[0]} selected site"
630
+ if selection.anchor_lat is not None and selection.anchor_lon is not None:
631
+ return f"Selected site {selection.anchor_lat:.5f}, {selection.anchor_lon:.5f}"
632
+ return "Selected site area"
633
+
634
+
635
+ def _auto_boundary_source(selection: SiteSelection) -> str:
636
+ if selection.selection_type in {"drawn_polygon", "rectangle"}:
637
+ return "User-drawn boundary on OpenStreetMap base map; approximate and not legal/cadastral."
638
+ if selection.selection_type == "pin_radius":
639
+ return "User-selected pin and radius; approximate context analysis, not exact boundary."
640
+ if selection.selection_type == "dxf_boundary":
641
+ return "Uploaded DXF boundary candidate selected by user."
642
+ if selection.selection_type == "geojson_boundary":
643
+ return "Uploaded GeoJSON boundary selected by user."
644
+ if selection.selection_type == "kml_boundary":
645
+ return "Uploaded KML/KMZ boundary selected by user."
646
+ return "User-provided site selection."
647
+
648
+
649
  def _file_path(file_obj: Any) -> str:
650
  return str(getattr(file_obj, "name", file_obj))
651
 
 
699
  )
700
  with gr.Column(scale=5, elem_classes=["control-panel"]):
701
  gr.Markdown("### Project Setup")
702
+ project_name = gr.Textbox(
703
+ label="Project name (optional)",
704
+ placeholder="Auto: Preliminary Site Analysis",
705
+ info="Leave blank if you only want to draw/select a site and generate analysis.",
706
+ )
707
+ site_name = gr.Textbox(
708
+ label="Site name (optional)",
709
+ placeholder="Auto-filled from approximate address or coordinates",
710
+ info="If blank, the app labels the selected polygon/pin from reverse geocoding or coordinates.",
711
+ )
712
+ project_type = gr.Textbox(
713
+ label="Project type (optional)",
714
+ placeholder="Thesis, resort, housing, institute...",
715
+ )
716
+ boundary_source = gr.Textbox(
717
+ label="Boundary source (optional)",
718
+ placeholder="Auto-labelled from map draw, pin radius, CAD, KML/KMZ, or GeoJSON",
719
+ )
720
  manual_location = gr.Textbox(
721
  label="Lat/lon or Google Maps URL",
722
  placeholder="21.002, 70.245 or a Google Maps URL with coordinates",
src/sample_data.py CHANGED
@@ -7,9 +7,19 @@ from .evidence import make_evidence
7
  from .models import EvidenceItem
8
 
9
 
10
- def is_sample_site(project_name: str, site_name: str, boundary_source: str) -> bool:
 
 
 
 
 
 
 
 
 
 
11
  text = " ".join([project_name or "", site_name or "", boundary_source or ""]).lower()
12
- return "chorwad" in text and "sample" in text
13
 
14
 
15
  def apply_chorwad_sample_fallbacks(
@@ -22,8 +32,10 @@ def apply_chorwad_sample_fallbacks(
22
  osm_context: dict[str, Any],
23
  topography: dict[str, Any],
24
  soil: dict[str, Any],
 
 
25
  ) -> tuple[dict[str, Any] | None, dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any], list[EvidenceItem], list[str]]:
26
- if not is_sample_site(project_name, site_name, boundary_source):
27
  return site_identity, climate, osm_context, topography, soil, [], []
28
 
29
  evidence: list[EvidenceItem] = []
@@ -70,6 +82,12 @@ def apply_chorwad_sample_fallbacks(
70
  return site_identity, climate, osm_context, topography, soil, evidence, warnings
71
 
72
 
 
 
 
 
 
 
73
  def _missing_climate(climate: dict[str, Any]) -> bool:
74
  for key in ("forecast", "recent_historical", "climate_normal"):
75
  value = climate.get(key)
 
7
  from .models import EvidenceItem
8
 
9
 
10
+ CHORWAD_SAMPLE_LAT = 21.00248
11
+ CHORWAD_SAMPLE_LON = 70.24537
12
+
13
+
14
+ def is_sample_site(
15
+ project_name: str,
16
+ site_name: str,
17
+ boundary_source: str,
18
+ anchor_lat: float | None = None,
19
+ anchor_lon: float | None = None,
20
+ ) -> bool:
21
  text = " ".join([project_name or "", site_name or "", boundary_source or ""]).lower()
22
+ return ("chorwad" in text and "sample" in text) or _near_chorwad_sample(anchor_lat, anchor_lon)
23
 
24
 
25
  def apply_chorwad_sample_fallbacks(
 
32
  osm_context: dict[str, Any],
33
  topography: dict[str, Any],
34
  soil: dict[str, Any],
35
+ anchor_lat: float | None = None,
36
+ anchor_lon: float | None = None,
37
  ) -> tuple[dict[str, Any] | None, dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any], list[EvidenceItem], list[str]]:
38
+ if not is_sample_site(project_name, site_name, boundary_source, anchor_lat, anchor_lon):
39
  return site_identity, climate, osm_context, topography, soil, [], []
40
 
41
  evidence: list[EvidenceItem] = []
 
82
  return site_identity, climate, osm_context, topography, soil, evidence, warnings
83
 
84
 
85
+ def _near_chorwad_sample(anchor_lat: float | None, anchor_lon: float | None) -> bool:
86
+ if anchor_lat is None or anchor_lon is None:
87
+ return False
88
+ return abs(anchor_lat - CHORWAD_SAMPLE_LAT) <= 0.01 and abs(anchor_lon - CHORWAD_SAMPLE_LON) <= 0.01
89
+
90
+
91
  def _missing_climate(climate: dict[str, Any]) -> bool:
92
  for key in ("forecast", "recent_historical", "climate_normal"):
93
  value = climate.get(key)
tests/test_app_defaults.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+
3
+ from app import _resolve_project_fields
4
+ from src.models import SiteSelection
5
+
6
+
7
+ class AppDefaultTests(unittest.TestCase):
8
+ def test_polygon_only_metadata_is_auto_labelled(self):
9
+ selection = SiteSelection(
10
+ id="S1",
11
+ selection_type="drawn_polygon",
12
+ coordinate_mode="wgs84",
13
+ geometry_geojson={"type": "Polygon", "coordinates": []},
14
+ local_geometry=None,
15
+ anchor_lat=21.00248,
16
+ anchor_lon=70.24537,
17
+ radius_m=None,
18
+ area_sqm=11978,
19
+ perimeter_m=439,
20
+ centroid=(21.00248, 70.24537),
21
+ bbox=(70.2447, 21.0019, 70.246, 21.003),
22
+ unit_source="WGS84 drawn map",
23
+ accuracy_label="user-drawn approximate boundary",
24
+ source_files=[],
25
+ selected_boundary_id=None,
26
+ limitations=["Drawn map boundaries are approximate."],
27
+ )
28
+
29
+ project, site, project_type, source, warnings = _resolve_project_fields(
30
+ project_name="",
31
+ site_name="",
32
+ project_type="",
33
+ boundary_source="",
34
+ selection=selection,
35
+ site_identity={"city": "Chorwad", "state": "Gujarat", "country": "India"},
36
+ )
37
+
38
+ self.assertEqual(project, "Preliminary Site Analysis")
39
+ self.assertEqual(site, "Chorwad selected site")
40
+ self.assertEqual(project_type, "Early-stage architecture site analysis")
41
+ self.assertIn("User-drawn boundary", source)
42
+ self.assertGreaterEqual(len(warnings), 3)
43
+
44
+ def test_default_untitled_site_is_replaced(self):
45
+ selection = SiteSelection(
46
+ id="S2",
47
+ selection_type="pin_radius",
48
+ coordinate_mode="wgs84",
49
+ geometry_geojson=None,
50
+ local_geometry=None,
51
+ anchor_lat=21.0,
52
+ anchor_lon=70.0,
53
+ radius_m=250,
54
+ area_sqm=196349.5,
55
+ perimeter_m=1570.8,
56
+ centroid=(21.0, 70.0),
57
+ bbox=None,
58
+ unit_source="meters",
59
+ accuracy_label="pin-radius approximation",
60
+ source_files=[],
61
+ selected_boundary_id=None,
62
+ limitations=["This is approximate context analysis."],
63
+ )
64
+
65
+ _, site, _, source, _ = _resolve_project_fields(
66
+ project_name="Architecture Site Analysis",
67
+ site_name="Untitled site",
68
+ project_type="",
69
+ boundary_source="",
70
+ selection=selection,
71
+ site_identity=None,
72
+ )
73
+
74
+ self.assertEqual(site, "Selected site 21.00000, 70.00000")
75
+ self.assertIn("pin and radius", source)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ unittest.main()
tests/test_sample_data.py CHANGED
@@ -8,6 +8,8 @@ class SampleDataTests(unittest.TestCase):
8
  self.assertTrue(is_sample_site("Chorwad Coastal Thesis Sample", "", "sample drawn boundary"))
9
  self.assertFalse(is_sample_site("Real thesis", "Chorwad site", "faculty CAD"))
10
  self.assertFalse(is_sample_site("Sample", "Mumbai site", "manual pin"))
 
 
11
 
12
  def test_sample_fallback_fills_missing_layers_and_warns(self):
13
  identity, climate, context, topo, soil, evidence, warnings = apply_chorwad_sample_fallbacks(
@@ -63,6 +65,28 @@ class SampleDataTests(unittest.TestCase):
63
  self.assertEqual(evidence, [])
64
  self.assertEqual(warnings, [])
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  if __name__ == "__main__":
68
  unittest.main()
 
8
  self.assertTrue(is_sample_site("Chorwad Coastal Thesis Sample", "", "sample drawn boundary"))
9
  self.assertFalse(is_sample_site("Real thesis", "Chorwad site", "faculty CAD"))
10
  self.assertFalse(is_sample_site("Sample", "Mumbai site", "manual pin"))
11
+ self.assertTrue(is_sample_site("", "", "", anchor_lat=21.00248, anchor_lon=70.24537))
12
+ self.assertFalse(is_sample_site("", "", "", anchor_lat=19.076, anchor_lon=72.8777))
13
 
14
  def test_sample_fallback_fills_missing_layers_and_warns(self):
15
  identity, climate, context, topo, soil, evidence, warnings = apply_chorwad_sample_fallbacks(
 
65
  self.assertEqual(evidence, [])
66
  self.assertEqual(warnings, [])
67
 
68
+ def test_coordinate_sample_fallback_supports_polygon_only_demo(self):
69
+ identity, climate, context, topo, soil, evidence, warnings = apply_chorwad_sample_fallbacks(
70
+ project_name="",
71
+ site_name="",
72
+ boundary_source="",
73
+ site_identity=None,
74
+ climate={"forecast": None, "recent_historical": None, "climate_normal": None},
75
+ osm_context={"counts": {}, "features": []},
76
+ topography={},
77
+ soil={},
78
+ anchor_lat=21.00248,
79
+ anchor_lon=70.24537,
80
+ )
81
+
82
+ self.assertEqual(identity["state"], "Gujarat")
83
+ self.assertIn("forecast", climate)
84
+ self.assertGreater(context["counts"]["water"], 0)
85
+ self.assertGreater(topo["mean_elevation_m"], 0)
86
+ self.assertIn("texture_signal", soil)
87
+ self.assertEqual(len(evidence), 1)
88
+ self.assertIn("demo/testing", warnings[0])
89
+
90
 
91
  if __name__ == "__main__":
92
  unittest.main()