MSU576 commited on
Commit
d0ee2d7
Β·
verified Β·
1 Parent(s): 4df803c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +620 -248
app.py CHANGED
@@ -596,49 +596,82 @@ def build_full_geotech_pdf(site: Dict[str, Any], filename: str, include_map_imag
596
  if "sites" not in st.session_state:
597
  # initialize with a default site
598
  st.session_state["sites"] = [{
599
- "Site Name":"home",
600
- "Project Name": "Project ",
601
- "Soil Class": None,
602
- "Soil Recognizer Confidence": None,
603
- "Site ID": None,
604
- "Coordinates": "",
605
- "lat": None,
606
- "lon": None,
607
- "Project Description": "",
608
- # Site Characterization
609
- "Topography": None, # manual / descriptive topo entry
610
- "Drainage": None, # manual drainage notes
611
- "Current Land Use": None, # can be linked to Environmental Data
612
- "Regional Geology": None, # manual geology notes
613
- # Investigations & Lab
614
- "Field Investigation": [],
615
- "Laboratory Results": [],
616
- "GSD": None,
617
- "USCS": None,
618
- "AASHTO": None,
619
- "GI": None,
620
- # Geotechnical Parameters
621
- "Load Bearing Capacity": None,
622
- "Skin Shear Strength": None,
623
- "Relative Compaction": None,
624
- "Rate of Consolidation": None,
625
- "Nature of Construction": None,
626
- # Earth Engine Data
627
- "Soil Profile": None, # SoilGrids (clay/sand/silt etc.)
628
- "Flood Data": None, # JRC Global Surface Water
629
- "Seismic Data": None, # GSHAP Seismic hazard
630
- "Environmental Data": None, # Landcover, forest loss, urban
631
- "Topo Data": None, # SRTM DEM elevation
632
- # Map & Visualization
633
- "map_snapshot": None,
634
- # AI / Reporting
635
- "chat_history": [],
636
- "classifier_inputs": {},
637
- "classifier_decision": None,
638
- "report_convo_state": 0,
639
- "report_missing_fields": [],
640
- "report_answers": {}
641
- }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
 
643
  if "active_site" not in st.session_state:
644
  st.session_state["active_site"] = 0
@@ -711,49 +744,82 @@ with st.sidebar:
711
  idx = len(st.session_state["sites"])
712
  #idx = len(st.session_state["sites"]) + 1
713
  st.session_state["sites"].append({
714
- "Site Name": new_site_name.strip(),
715
- "Project Name": "Project - " + new_site_name.strip(),
716
- "Site ID": idx,
717
- "Soil Class": None,
718
- "Soil Recognizer Confidence": None,
719
- "Coordinates": "",
720
- "lat": None,
721
- "lon": None,
722
- "Project Description": "",
723
- # Site Characterization
724
- "Topography": None, # manual / descriptive topo entry
725
- "Drainage": None, # manual drainage notes
726
- "Current Land Use": None, # can be linked to Environmental Data
727
- "Regional Geology": None, # manual geology notes
728
- # Investigations & Lab
729
- "Field Investigation": [],
730
- "Laboratory Results": [],
731
- "GSD": None,
732
- "USCS": None,
733
- "AASHTO": None,
734
- "GI": None,
735
- # Geotechnical Parameters
736
- "Load Bearing Capacity": None,
737
- "Skin Shear Strength": None,
738
- "Relative Compaction": None,
739
- "Rate of Consolidation": None,
740
- "Nature of Construction": None,
741
- # Earth Engine Data
742
- "Soil Profile": None, # SoilGrids (clay/sand/silt etc.)
743
- "Flood Data": None, # JRC Global Surface Water
744
- "Seismic Data": None, # GSHAP Seismic hazard
745
- "Environmental Data": None, # Landcover, forest loss, urban
746
- "Topo Data": None, # SRTM DEM elevation
747
- # Map & Visualization
748
- "map_snapshot": None,
749
- # AI / Reporting
750
- "chat_history": [],
751
- "classifier_inputs": {},
752
- "classifier_decision": None,
753
- "report_convo_state": 0,
754
- "report_missing_fields": [],
755
- "report_answers": {}
756
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
 
758
  st.success(f"Site '{new_site_name.strip()}' created.")
759
  st.session_state["active_site"] = idx
@@ -1006,18 +1072,35 @@ def ocr_page():
1006
  else:
1007
  st.warning("OCR not available in this deployment.")
1008
  # Locator Page (with Earth Engine auth at top)
 
1009
  import os
 
1010
  import streamlit as st
1011
  import geemap.foliumap as geemap
1012
  import ee
1013
  import matplotlib.pyplot as plt
 
 
 
1014
 
1015
- # ----------------------------
1016
- # ----------------------------
1017
  def locator_page():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1018
  # ----------------------------
1019
- # Earth Engine Initialization (service account or interactive)
1020
- # ----------------------------
1021
  EARTHENGINE_TOKEN = os.getenv("EARTHENGINE_TOKEN")
1022
  SERVICE_ACCOUNT = os.getenv("SERVICE_ACCOUNT") # optional: service account email
1023
 
@@ -1056,89 +1139,117 @@ def locator_page():
1056
  st.stop()
1057
 
1058
  # ----------------------------
1059
- # Helper functions
 
 
 
 
 
 
 
 
 
 
1060
  # ----------------------------
1061
- def safe_get_reduce(region, image, band: str, scale: int, default=None, max_pixels=1e9):
 
 
 
 
 
 
 
1062
  try:
1063
- res = image.reduceRegion(
1064
  reducer=ee.Reducer.mean(),
1065
  geometry=region,
1066
- scale=scale,
1067
  maxPixels=int(max_pixels)
1068
  )
1069
- val = res.get(band)
1070
  if val is None:
 
1071
  return default
1072
- return float(val.getInfo())
1073
- except Exception:
 
 
 
 
 
 
 
 
 
1074
  return default
1075
 
1076
- def safe_reduce_histogram(region, image, band: str, scale: int, max_pixels=1e9):
 
 
 
 
 
1077
  try:
1078
- hist = image.reduceRegion(
1079
  reducer=ee.Reducer.frequencyHistogram(),
1080
  geometry=region,
1081
- scale=scale,
1082
  maxPixels=int(max_pixels)
1083
- ).get(band)
 
 
 
 
 
1084
  if hist is None:
1085
- return None
1086
- return hist.getInfo()
1087
- except Exception:
1088
- return None
 
 
 
 
1089
 
1090
- def get_roi_from_map(m: geemap.Map):
1091
- """Try multiple ways to extract the ROI from the drawn shapes."""
1092
- candidates = [
1093
- getattr(m, "user_roi", None),
1094
- getattr(m, "last_drawn_geojson", None),
1095
- getattr(m, "draw_last_feature", None),
1096
- getattr(m, "draw_features", None),
1097
- getattr(m, "user_drawn_features", None),
1098
- getattr(m, "drawn_features", None),
1099
- ]
1100
- for c in candidates:
1101
- if c:
1102
- if isinstance(c, ee.Geometry):
1103
- return c
1104
- try:
1105
- if isinstance(c, dict):
1106
- return ee.Geometry(c)
1107
- if isinstance(c, list) and len(c) > 0 and isinstance(c[0], dict):
1108
- return ee.Geometry(c[0])
1109
- except Exception:
1110
- pass
1111
  try:
1112
- s = getattr(m, "draw_last_feature", None)
1113
- if s:
1114
- import json
1115
- if isinstance(s, str):
1116
- return ee.Geometry(json.loads(s))
1117
- except Exception:
1118
- pass
1119
- return None
1120
-
1121
- # ----------------------------
1122
- # Page title
1123
- # ----------------------------
1124
- st.title("🌍 GeoMate Interactive Earth Explorer")
1125
- st.markdown(
1126
- "Draw a polygon or rectangle on the map (use the drawing tool), then the app will compute "
1127
- "regional summaries (soil clay, elevation, seismic, flood occurrence, landcover distribution)."
1128
- )
 
 
 
 
 
1129
 
1130
  # ----------------------------
1131
  # Map setup
1132
  # ----------------------------
1133
- m = geemap.Map(center=[28.0, 72.0], zoom=4, draw_export=True)
1134
 
 
1135
  basemaps = [
1136
  "HYBRID", "ROADMAP", "TERRAIN", "SATELLITE",
1137
  "Esri.WorldImagery", "Esri.WorldTopoMap", "Esri.WorldShadedRelief",
1138
  "Esri.NatGeoWorldMap", "Esri.OceanBasemap",
1139
  "CartoDB.Positron", "CartoDB.DarkMatter",
1140
  "Stamen.Terrain", "Stamen.Watercolor",
1141
- "OpenStreetMap",
1142
  ]
1143
  for b in basemaps:
1144
  try:
@@ -1147,128 +1258,389 @@ def locator_page():
1147
  pass
1148
 
1149
  # ----------------------------
1150
- # Datasets
 
1151
  # ----------------------------
1152
- soil = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-4B1C_M/v01").select("b200")
1153
- soil_vis = {"min": 0, "max": 60, "palette": ["yellow", "brown", "red"]}
1154
- m.addLayer(soil, soil_vis, "Soil Clay (200cm)")
1155
-
1156
- dem = ee.Image("USGS/SRTMGL1_003")
1157
- dem_vis = {"min": 0, "max": 4000, "palette": ["blue", "green", "brown", "white"]}
1158
- m.addLayer(dem, dem_vis, "Topography (SRTM DEM)")
1159
-
1160
- seismic = ee.Image("GEM/2015/GlobalSeismicHazard").select("b0")
1161
- m.addLayer(seismic, {"min": 0, "max": 1, "palette": ["white", "red"]}, "Seismic Hazard")
1162
-
1163
- water = ee.Image("JRC/GSW1_4/GlobalSurfaceWater").select("occurrence")
1164
- flood_vis = {"min": 0, "max": 100, "palette": ["white", "blue"]}
1165
- m.addLayer(water, flood_vis, "Flood Hazard")
1166
-
1167
- landcover = ee.Image("ESA/WorldCover/v200/2021")
1168
- landcover_vis = {
1169
- "bands": ["Map"], "min": 10, "max": 100,
1170
- "palette": [
1171
- "006400", "ffbb22", "ffff4c", "f096ff", "fa0000",
1172
- "b4b4b4", "f0f0f0", "0064c8", "0096a0", "00cf75"
1173
- ]
1174
- }
1175
- m.addLayer(landcover, landcover_vis, "Landcover 2021")
1176
 
1177
- # Boundaries & grid
1178
- countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
1179
- m.addLayer(
1180
- countries.style(**{"color": "black", "fillColor": "00000000", "width": 1}),
1181
- {}, "Country Boundaries"
1182
- )
1183
- states = ee.FeatureCollection("FAO/GAUL_SIMPLIFIED_500m/2015/level1")
1184
- m.addLayer(
1185
- states.style(**{"color": "purple", "fillColor": "00000000", "width": 0.5}),
1186
- {}, "State/Province Boundaries"
1187
- )
1188
- graticule = geemap.latlon_grid(5.0, region=ee.Geometry.Rectangle([-180, -90, 180, 90]))
1189
- m.addLayer(graticule.style(**{"color": "gray", "width": 0.5}), {}, "Lat/Lon Grid")
 
 
 
 
1190
 
1191
- # Drawing tools
1192
- m.add_draw_control(polyline=False, circle=False, circlemarker=False, rectangle=True, polygon=True)
1193
- m.to_streamlit(height=700, responsive=True)
1194
- st.markdown("πŸ‘‰ Use the map drawing tool to select a polygon/rectangle.")
 
 
 
 
 
 
 
 
 
 
 
1195
 
1196
  # ----------------------------
1197
- # ROI extraction
1198
  # ----------------------------
1199
- roi = get_roi_from_map(m)
1200
- if roi is None:
1201
- st.warning("⚠️ No polygon selected yet.")
1202
- return
 
 
1203
 
 
 
 
1204
  try:
1205
- if not isinstance(roi, ee.Geometry):
1206
- roi = ee.Geometry(roi)
1207
  except Exception:
1208
- st.error("πŸ‘Ž Could not parse the drawn ROI into an Earth Engine geometry.")
1209
- return
1210
 
1211
- st.success("βœ… Polygon selected! Computing regional summaries...")
 
 
 
 
 
 
1212
 
1213
  # ----------------------------
1214
- # Compute stats
1215
  # ----------------------------
1216
- soil_val = safe_get_reduce(roi, soil, "b200", scale=1000, default=None)
1217
- elev_val = safe_get_reduce(roi, dem, "elevation", scale=1000, default=None)
1218
- seismic_val = safe_get_reduce(roi, seismic, "b0", scale=5000, default=None)
1219
- flood_val = safe_get_reduce(roi, water, "occurrence", scale=30, default=None)
1220
- lc_stats = safe_reduce_histogram(roi, landcover, "Map", scale=30)
1221
-
1222
- def safe_display(x):
1223
- return "N/A" if x is None else round(x, 2)
1224
-
1225
- st.subheader("πŸ“Š Regional Data Summary")
1226
- st.write(f"**Average Clay (200cm):** {safe_display(soil_val)} %")
1227
- st.write(f"**Average Elevation:** {safe_display(elev_val)} m")
1228
- st.write(f"**Seismic Hazard (PGA):** {safe_display(seismic_val)} g")
1229
- st.write(f"**Flood Occurrence Probability:** {safe_display(flood_val)} %")
1230
-
1231
- if lc_stats:
1232
- labels = [str(k) for k in lc_stats.keys()]
1233
- values = list(lc_stats.values())
1234
- fig1, ax1 = plt.subplots(figsize=(6, 4))
1235
- ax1.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
1236
- ax1.set_title("Landcover Distribution")
1237
- st.pyplot(fig1)
 
 
 
1238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1239
  try:
1240
- soil_hist = soil.reduceRegion(
1241
- reducer=ee.Reducer.histogram(maxBuckets=10),
1242
- geometry=roi,
1243
- scale=1000,
1244
- maxPixels=1e9
1245
- ).get("b200").getInfo()
1246
  except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1247
  soil_hist = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1248
 
1249
- if soil_hist and isinstance(soil_hist, dict) and "bucketMeans" in soil_hist:
1250
- fig2, ax2 = plt.subplots(figsize=(6, 4))
1251
- ax2.bar(soil_hist["bucketMeans"], soil_hist["histogram"],
1252
- width=2, color="brown", alpha=0.7)
1253
- ax2.set_xlabel("Clay fraction (%)")
1254
- ax2.set_ylabel("Pixel count")
1255
- ax2.set_title("Soil Clay Distribution")
1256
- st.pyplot(fig2)
1257
 
1258
- # ----------------------------
1259
- # Save for reports
1260
- # ----------------------------
1261
- if "soil_json" not in st.session_state:
1262
- st.session_state["soil_json"] = {}
1263
-
1264
- st.session_state["soil_json"].update({
1265
- "Soil": f"{safe_display(soil_val)} %",
1266
- "Elevation": f"{safe_display(elev_val)} m",
1267
- "Seismic Hazard": f"{safe_display(seismic_val)} g",
1268
- "Flood Probability": f"{safe_display(flood_val)} %",
1269
- "Landcover Stats": lc_stats if lc_stats else {}
1270
- })
1271
- st.success("πŸ“‘ Data saved to `soil_json` for report integration!")
1272
 
1273
  # GeoMate Ask (RAG) β€” simple chat with memory per site and auto-extract numeric values
1274
  def rag_page():
 
596
  if "sites" not in st.session_state:
597
  # initialize with a default site
598
  st.session_state["sites"] = [{
599
+ "Site Name": None,
600
+ "Project Name": "Project",
601
+ "Site ID": None,
602
+ "Soil Class": None,
603
+ "Soil Recognizer Confidence": None,
604
+ "Coordinates": "",
605
+ "lat": None,
606
+ "lon": None,
607
+ "Project Description": "",
608
+ # ---------------------------
609
+ # Site Characterization
610
+ # ---------------------------
611
+ "Topography": None, # manual topo entry
612
+ "Drainage": None, # manual drainage notes
613
+ "Current Land Use": None, # can be linked to Environmental Data
614
+ "Regional Geology": None, # manual geology notes
615
+ # ---------------------------
616
+ # Investigations & Lab
617
+ # ---------------------------
618
+ "Field Investigation": [],
619
+ "Laboratory Results": [],
620
+ "GSD": None,
621
+ "USCS": None,
622
+ "AASHTO": None,
623
+ "GI": None,
624
+ # ---------------------------
625
+ # Geotechnical Parameters
626
+ # ---------------------------
627
+ "Load Bearing Capacity": None,
628
+ "Skin Shear Strength": None,
629
+ "Relative Compaction": None,
630
+ "Rate of Consolidation": None,
631
+ "Nature of Construction": None,
632
+ # ---------------------------
633
+ # Earth Engine Data
634
+ # ---------------------------
635
+ "Soil Profile": { # SoilGrids (multi-parameter)
636
+ "Clay": None, # e.g. % clay at 200 cm
637
+ "Sand": None, # % sand
638
+ "Silt": None, # % silt
639
+ "OrganicCarbon": None, # % organic carbon
640
+ "pH": None # soil pH if available
641
+ },
642
+ "Topo Data": None, # Avg elevation (SRTM DEM)
643
+ "Seismic Data": None, # PGA/g (GEM hazard)
644
+ "Flood Data": None, # JRC Surface Water occurrence
645
+ "Environmental Data": { # Landcover, vegetation, urban, etc.
646
+ "Landcover Stats": None, # histogram by class
647
+ "Forest Loss": None, # future add: Hansen dataset
648
+ "Urban Fraction": None # optional calc from landcover
649
+ },
650
+ "Weather Data": { # daily/monthly climate summaries
651
+ "Rainfall": None,
652
+ "Temperature": None,
653
+ "Humidity": None
654
+ },
655
+ "Atmospheric Data": { # optional: pollution, aerosols
656
+ "AerosolOpticalDepth": None,
657
+ "NO2": None,
658
+ "CO": None
659
+ },
660
+ # ---------------------------
661
+ # Map & Visualization
662
+ # ---------------------------
663
+ "map_snapshot": None,
664
+ # ---------------------------
665
+ # AI / Reporting
666
+ # ---------------------------
667
+ "chat_history": [],
668
+ "classifier_inputs": {},
669
+ "classifier_decision": None,
670
+ "report_convo_state": 0,
671
+ "report_missing_fields": [],
672
+ "report_answers": {}
673
+ })
674
+
675
 
676
  if "active_site" not in st.session_state:
677
  st.session_state["active_site"] = 0
 
744
  idx = len(st.session_state["sites"])
745
  #idx = len(st.session_state["sites"]) + 1
746
  st.session_state["sites"].append({
747
+ "Site Name": new_site_name.strip(),
748
+ "Project Name": "Project - " + new_site_name.strip(),
749
+ "Site ID": idx,
750
+ "Soil Class": None,
751
+ "Soil Recognizer Confidence": None,
752
+ "Coordinates": "",
753
+ "lat": None,
754
+ "lon": None,
755
+ "Project Description": "",
756
+ # ---------------------------
757
+ # Site Characterization
758
+ # ---------------------------
759
+ "Topography": None, # manual topo entry
760
+ "Drainage": None, # manual drainage notes
761
+ "Current Land Use": None, # can be linked to Environmental Data
762
+ "Regional Geology": None, # manual geology notes
763
+ # ---------------------------
764
+ # Investigations & Lab
765
+ # ---------------------------
766
+ "Field Investigation": [],
767
+ "Laboratory Results": [],
768
+ "GSD": None,
769
+ "USCS": None,
770
+ "AASHTO": None,
771
+ "GI": None,
772
+ # ---------------------------
773
+ # Geotechnical Parameters
774
+ # ---------------------------
775
+ "Load Bearing Capacity": None,
776
+ "Skin Shear Strength": None,
777
+ "Relative Compaction": None,
778
+ "Rate of Consolidation": None,
779
+ "Nature of Construction": None,
780
+ # ---------------------------
781
+ # Earth Engine Data
782
+ # ---------------------------
783
+ "Soil Profile": { # SoilGrids (multi-parameter)
784
+ "Clay": None, # e.g. % clay at 200 cm
785
+ "Sand": None, # % sand
786
+ "Silt": None, # % silt
787
+ "OrganicCarbon": None, # % organic carbon
788
+ "pH": None # soil pH if available
789
+ },
790
+ "Topo Data": None, # Avg elevation (SRTM DEM)
791
+ "Seismic Data": None, # PGA/g (GEM hazard)
792
+ "Flood Data": None, # JRC Surface Water occurrence
793
+ "Environmental Data": { # Landcover, vegetation, urban, etc.
794
+ "Landcover Stats": None, # histogram by class
795
+ "Forest Loss": None, # future add: Hansen dataset
796
+ "Urban Fraction": None # optional calc from landcover
797
+ },
798
+ "Weather Data": { # daily/monthly climate summaries
799
+ "Rainfall": None,
800
+ "Temperature": None,
801
+ "Humidity": None
802
+ },
803
+ "Atmospheric Data": { # optional: pollution, aerosols
804
+ "AerosolOpticalDepth": None,
805
+ "NO2": None,
806
+ "CO": None
807
+ },
808
+ # ---------------------------
809
+ # Map & Visualization
810
+ # ---------------------------
811
+ "map_snapshot": None,
812
+ # ---------------------------
813
+ # AI / Reporting
814
+ # ---------------------------
815
+ "chat_history": [],
816
+ "classifier_inputs": {},
817
+ "classifier_decision": None,
818
+ "report_convo_state": 0,
819
+ "report_missing_fields": [],
820
+ "report_answers": {}
821
+ })
822
+
823
 
824
  st.success(f"Site '{new_site_name.strip()}' created.")
825
  st.session_state["active_site"] = idx
 
1072
  else:
1073
  st.warning("OCR not available in this deployment.")
1074
  # Locator Page (with Earth Engine auth at top)
1075
+
1076
  import os
1077
+ import json
1078
  import streamlit as st
1079
  import geemap.foliumap as geemap
1080
  import ee
1081
  import matplotlib.pyplot as plt
1082
+ from datetime import datetime
1083
+ from io import BytesIO
1084
+ import base64
1085
 
 
 
1086
  def locator_page():
1087
+ """
1088
+ Robust locator page:
1089
+ - Uses your initialize_ee() auth routine (expects EARTHENGINE_TOKEN / SERVICE_ACCOUNT in env)
1090
+ - Shows interactive map with many basemaps and overlays
1091
+ - Safe reducers with fallbacks and caching
1092
+ - Stores results in st.session_state['soil_json'] AND in the active site entry under Earth Engine fields
1093
+ """
1094
+
1095
+ st.title("🌍 GeoMate Interactive Earth Explorer")
1096
+ st.markdown(
1097
+ "Draw a polygon (or rectangle) on the map using the drawing tool. "
1098
+ "The app will compute regional summaries (soil clay, elevation, seismic, flood occurrence, landcover, NDVI) "
1099
+ "and save results for reports."
1100
+ )
1101
+
1102
  # ----------------------------
1103
+ # Use your existing EE init function if present
 
1104
  EARTHENGINE_TOKEN = os.getenv("EARTHENGINE_TOKEN")
1105
  SERVICE_ACCOUNT = os.getenv("SERVICE_ACCOUNT") # optional: service account email
1106
 
 
1139
  st.stop()
1140
 
1141
  # ----------------------------
1142
+ # I assume your file defines initialize_ee() exactly like your earlier message.
1143
+ try:
1144
+ init_ok = initialize_ee() # call your auth initializer
1145
+ except NameError:
1146
+ st.error("Auth initializer `initialize_ee()` not found. Ensure your auth code exists above this function.")
1147
+ return
1148
+ if not init_ok:
1149
+ return
1150
+
1151
+ # ----------------------------
1152
+ # Helper: safe reducers + caching
1153
  # ----------------------------
1154
+ def safe_get_reduce(region, image, band, scale=1000, default=None, max_pixels=int(1e7)):
1155
+ """Return float or None. Uses reduceRegion mean safely."""
1156
+ cache_key = f"reduce::{region.toGeoJSONString()[:200]}::{str(image)}::{band}::{scale}"
1157
+ # check cache
1158
+ cache = st.session_state.setdefault("_ee_cache", {})
1159
+ if cache_key in cache:
1160
+ return cache[cache_key]
1161
+
1162
  try:
1163
+ rr = image.reduceRegion(
1164
  reducer=ee.Reducer.mean(),
1165
  geometry=region,
1166
+ scale=int(scale),
1167
  maxPixels=int(max_pixels)
1168
  )
1169
+ val = rr.get(band)
1170
  if val is None:
1171
+ cache[cache_key] = default
1172
  return default
1173
+ v = val.getInfo()
1174
+ if v is None:
1175
+ cache[cache_key] = default
1176
+ return default
1177
+ got = float(v)
1178
+ cache[cache_key] = got
1179
+ return got
1180
+ except Exception as e:
1181
+ # log to session for debugging if desired
1182
+ st.session_state.setdefault("_ee_errors", []).append(str(e))
1183
+ cache[cache_key] = default
1184
  return default
1185
 
1186
+ def safe_reduce_histogram(region, image, band, scale=1000, max_pixels=int(1e7)):
1187
+ """Return a frequency histogram dict or {}."""
1188
+ cache_key = f"hist::{region.toGeoJSONString()[:200]}::{str(image)}::{band}::{scale}"
1189
+ cache = st.session_state.setdefault("_ee_cache", {})
1190
+ if cache_key in cache:
1191
+ return cache[cache_key]
1192
  try:
1193
+ rr = image.reduceRegion(
1194
  reducer=ee.Reducer.frequencyHistogram(),
1195
  geometry=region,
1196
+ scale=int(scale),
1197
  maxPixels=int(max_pixels)
1198
+ )
1199
+ val = rr.get(band)
1200
+ if val is None:
1201
+ cache[cache_key] = {}
1202
+ return {}
1203
+ hist = val.getInfo()
1204
  if hist is None:
1205
+ cache[cache_key] = {}
1206
+ return {}
1207
+ cache[cache_key] = hist
1208
+ return hist
1209
+ except Exception as e:
1210
+ st.session_state.setdefault("_ee_errors", []).append(str(e))
1211
+ cache[cache_key] = {}
1212
+ return {}
1213
 
1214
+ def safe_time_series(region, collection, band, start, end, reducer=ee.Reducer.mean(), scale=1000, max_pixels=int(1e7)):
1215
+ """Return simple timeseries list of (date, value) for a collection."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1216
  try:
1217
+ # reduce each image over region, map to list
1218
+ def per_image(img):
1219
+ date = img.date().format("YYYY-MM-dd")
1220
+ val = img.reduceRegion(reducer=reducer, geometry=region, scale=int(scale), maxPixels=int(max_pixels)).get(band)
1221
+ return ee.Feature(None, {"date": date, "val": val})
1222
+
1223
+ feats = collection.filterDate(start, end).map(per_image).filter(ee.Filter.notNull(["val"])).getInfo()
1224
+ # feats is a dict with 'features' list
1225
+ points = []
1226
+ for f in feats.get("features", []):
1227
+ props = f.get("properties", {})
1228
+ date = props.get("date")
1229
+ val = props.get("val")
1230
+ if val is not None:
1231
+ try:
1232
+ points.append((date, float(val)))
1233
+ except Exception:
1234
+ pass
1235
+ return points
1236
+ except Exception as e:
1237
+ st.session_state.setdefault("_ee_errors", []).append(str(e))
1238
+ return []
1239
 
1240
  # ----------------------------
1241
  # Map setup
1242
  # ----------------------------
1243
+ m = geemap.Map(center=[28.0, 72.0], zoom=5, draw_export=True)
1244
 
1245
+ # enriched basemaps list (many options; geemap will ignore unknown)
1246
  basemaps = [
1247
  "HYBRID", "ROADMAP", "TERRAIN", "SATELLITE",
1248
  "Esri.WorldImagery", "Esri.WorldTopoMap", "Esri.WorldShadedRelief",
1249
  "Esri.NatGeoWorldMap", "Esri.OceanBasemap",
1250
  "CartoDB.Positron", "CartoDB.DarkMatter",
1251
  "Stamen.Terrain", "Stamen.Watercolor",
1252
+ "OpenStreetMap", "Esri.WorldGrayCanvas", "Esri.WorldStreetMap"
1253
  ]
1254
  for b in basemaps:
1255
  try:
 
1258
  pass
1259
 
1260
  # ----------------------------
1261
+ # Datasets (choose stable EE catalog IDs)
1262
+ # Terrain (DEM) -> prefer NASADEM then SRTM fallback
1263
  # ----------------------------
1264
+ # Try NASADEM (higher quality), fallback to SRTM:
1265
+ try:
1266
+ dem = ee.Image("NASA/NASADEM_HGT/001") # NASADEM if available
1267
+ dem_band_name = "elevation"
1268
+ except Exception:
1269
+ dem = ee.Image("USGS/SRTMGL1_003")
1270
+ dem_band_name = "elevation"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1271
 
1272
+ # ----------------------------
1273
+ # Soil -> use OpenLandMap clay fraction (v02) which provides multiple bands like b0,b10,b30,... and is available
1274
+ # (fallback to SoilGrids if OpenLandMap missing)
1275
+ # ----------------------------
1276
+ soil_img = None
1277
+ soil_band = "b200" # default deep band
1278
+ try:
1279
+ soil_img = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-3A1A1A_M/v02")
1280
+ # pick default band b200 (100-200cm). Offer depth selection below in UI.
1281
+ except Exception:
1282
+ # fallback to SoilGrids if available
1283
+ try:
1284
+ soil_img = ee.Image("projects/soilgrids-isric/clay_mean")
1285
+ # SoilGrids band names like 'clay_0-5cm_mean' etc β€” we will pick a default after UI selection.
1286
+ soil_band = "clay_0-5cm_mean"
1287
+ except Exception:
1288
+ soil_img = None
1289
 
1290
+ # ----------------------------
1291
+ # Seismic -> attempt SEDAC GSHAP, then GEM
1292
+ # ----------------------------
1293
+ seismic_img = None
1294
+ try:
1295
+ seismic_img = ee.Image("SEDAC/GSHAPSeismicHazard") # band "gshap"
1296
+ seismic_band = "gshap"
1297
+ except Exception:
1298
+ try:
1299
+ seismic_img = ee.Image("GEM/2015/GlobalSeismicHazard") # may or may not exist
1300
+ # many GEM products use band b0 etc.
1301
+ seismic_band = "b0"
1302
+ except Exception:
1303
+ seismic_img = None
1304
+ seismic_band = None
1305
 
1306
  # ----------------------------
1307
+ # Flood -> JRC Global Surface Water
1308
  # ----------------------------
1309
+ try:
1310
+ water = ee.Image("JRC/GSW1_4/GlobalSurfaceWater")
1311
+ water_band = "occurrence"
1312
+ except Exception:
1313
+ water = None
1314
+ water_band = None
1315
 
1316
+ # ----------------------------
1317
+ # Landcover -> ESA WorldCover v200 (10 m) - "Map" band
1318
+ # ----------------------------
1319
  try:
1320
+ landcover = ee.Image("ESA/WorldCover/v200")
1321
+ lc_band = "Map"
1322
  except Exception:
1323
+ landcover = None
1324
+ lc_band = None
1325
 
1326
+ # ----------------------------
1327
+ # NDVI collection (MODIS 1km as robust global product)
1328
+ # ----------------------------
1329
+ try:
1330
+ ndvi_col = ee.ImageCollection("MODIS/061/MOD13A2").select("NDVI")
1331
+ except Exception:
1332
+ ndvi_col = None
1333
 
1334
  # ----------------------------
1335
+ # Add layers to map (visuals)
1336
  # ----------------------------
1337
+ # Add DEM visualization
1338
+ try:
1339
+ m.addLayer(dem, {"min": 0, "max": 4000, "palette": ["blue", "green", "brown", "white"]}, "DEM / Topography")
1340
+ except Exception:
1341
+ pass
1342
+
1343
+ # Add soil view (if available) β€” user will pick depth later
1344
+ if soil_img:
1345
+ try:
1346
+ # show the dataset (choose one band to display; if band missing, geemap will raise β€” catch it)
1347
+ # prefer b0 or b200 if available; if both missing, show first band
1348
+ available_bands = soil_img.bandNames().getInfo()
1349
+ # find a band to display
1350
+ display_band = soil_band if soil_band in available_bands else available_bands[0]
1351
+ m.addLayer(soil_img.select(display_band), {"min": 0.0, "max": 0.6, "palette": ["#ffffcc","#c2e699","#78c679","#31a354"]}, f"Soil Clay ({display_band})")
1352
+ except Exception:
1353
+ pass
1354
+
1355
+ # Seismic
1356
+ if seismic_img:
1357
+ try:
1358
+ # for SEDAC band "gshap" map 0-1
1359
+ m.addLayer(seismic_img, {"min": 0, "max": 1, "palette": ["white", "yellow", "red"]}, "Seismic Hazard")
1360
+ except Exception:
1361
+ pass
1362
 
1363
+ # Flood
1364
+ if water is not None:
1365
+ try:
1366
+ m.addLayer(water.select(water_band), {"min":0,"max":100,"palette":["white","blue"]}, "Water Occurrence (JRC)")
1367
+ except Exception:
1368
+ pass
1369
+
1370
+ # Landcover
1371
+ if landcover is not None:
1372
+ try:
1373
+ m.addLayer(landcover, {"min":10,"max":100,"palette":["#006400","#ffbb22","#ffff4c","#f096ff","#fa0000","#b4b4b4","#f0f0f0","#0064c8","#0096a0","#00cf75"]}, "Landcover (WorldCover)")
1374
+ except Exception:
1375
+ pass
1376
+
1377
+ # Add country boundaries & graticule
1378
  try:
1379
+ countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
1380
+ m.addLayer(countries.style(**{"color": "black", "fillColor": "00000000", "width": 1}), {}, "Country Boundaries")
 
 
 
 
1381
  except Exception:
1382
+ pass
1383
+ try:
1384
+ m.addLayer(geemap.latlon_grid(5.0, region=ee.Geometry.Rectangle([-180, -90, 180, 90])).style(**{"color":"gray","width":0.5}), {}, "Lat/Lon Grid")
1385
+ except Exception:
1386
+ pass
1387
+
1388
+ # Enable drawing
1389
+ m.add_draw_control(polyline=False, circle=False, circlemarker=False, rectangle=True, polygon=True)
1390
+ m.to_streamlit(height=700, responsive=True)
1391
+ st.markdown("πŸ‘‰ Draw a polygon/rectangle on the map (draw tool). After drawing click **Compute Summaries**.")
1392
+
1393
+ # Button to compute summaries (we'll fetch ROI from map object)
1394
+ if "compute_button" not in st.session_state:
1395
+ st.session_state["compute_button"] = False
1396
+ if st.button("Compute Summaries"):
1397
+ st.session_state["compute_button"] = True
1398
+
1399
+ # get ROI helper (try multiple attributes)
1400
+ def get_roi_from_map(m):
1401
+ # preferred property
1402
+ candidates = [
1403
+ getattr(m, "user_roi", None),
1404
+ getattr(m, "last_drawn_geojson", None),
1405
+ getattr(m, "draw_last_feature", None),
1406
+ getattr(m, "draw_features", None),
1407
+ getattr(m, "user_drawn_features", None),
1408
+ getattr(m, "drawn_features", None),
1409
+ ]
1410
+ for c in candidates:
1411
+ if c:
1412
+ # if it's already an ee.Geometry
1413
+ if isinstance(c, ee.Geometry):
1414
+ return c
1415
+ # if it's a GeoJSON dict
1416
+ try:
1417
+ if isinstance(c, dict):
1418
+ return ee.Geometry(c)
1419
+ # sometimes geemap keeps features list
1420
+ if isinstance(c, list) and len(c) > 0 and isinstance(c[0], dict):
1421
+ return ee.Geometry(c[0])
1422
+ # string that is GeoJSON
1423
+ if isinstance(c, str):
1424
+ import json
1425
+ js = json.loads(c)
1426
+ if isinstance(js, dict):
1427
+ return ee.Geometry(js)
1428
+ except Exception:
1429
+ pass
1430
+ # try Map.draw_last_feature property
1431
+ try:
1432
+ s = getattr(m, "draw_last_feature", None)
1433
+ if s:
1434
+ import json
1435
+ if isinstance(s, str):
1436
+ return ee.Geometry(json.loads(s))
1437
+ except Exception:
1438
+ pass
1439
+ return None
1440
+
1441
+ # If user pressed compute, extract ROI and compute
1442
+ if st.session_state.get("compute_button", False):
1443
+ roi = get_roi_from_map(m)
1444
+ if roi is None:
1445
+ st.error("No drawn ROI found. Please draw a polygon/rectangle on the map using the draw tool and press Compute Summaries again.")
1446
+ return
1447
+
1448
+ # ensure ROI is ee.Geometry
1449
+ try:
1450
+ if not isinstance(roi, ee.Geometry):
1451
+ roi = ee.Geometry(roi)
1452
+ except Exception:
1453
+ st.error("Failed to parse ROI as Earth Engine geometry.")
1454
+ return
1455
+
1456
+ st.success("Polygon found β€” computing (this may take a few seconds)...")
1457
+
1458
+ # choose soil depth via UI (0-200cm). If soil_img is OpenLandMap provide depth selection; if SoilGrids, show its bands
1459
+ chosen_soil_band = None
1460
+ if soil_img is not None:
1461
+ try:
1462
+ bands = soil_img.bandNames().getInfo()
1463
+ # if OpenLandMap bands like b0,b10,... show friendly selector
1464
+ depth_choice = st.selectbox("Soil depth / band to analyze", options=bands, index=bands.index(soil_band) if soil_band in bands else 0)
1465
+ chosen_soil_band = depth_choice
1466
+ except Exception:
1467
+ chosen_soil_band = None
1468
+
1469
+ # compute using safe helpers with conservative maxPixels and reasonable scales
1470
+ # scales: soil 250-1000m, dem 90-1000m, seismic coarse 5000m, flood 30-250m, landcover 30m
1471
+ soil_val = None
1472
+ if soil_img is not None and chosen_soil_band is not None:
1473
+ soil_val = safe_get_reduce(roi, soil_img.select(chosen_soil_band), chosen_soil_band, scale=1000, default=None, max_pixels=int(5e7))
1474
+
1475
+ elev_val = safe_get_reduce(roi, dem, dem_band_name if dem_band_name else "elevation", scale=1000, default=None, max_pixels=int(5e7))
1476
+
1477
+ seismic_val = None
1478
+ if seismic_img is not None and seismic_band is not None:
1479
+ seismic_val = safe_get_reduce(roi, seismic_img, seismic_band, scale=5000, default=None, max_pixels=int(5e7))
1480
+
1481
+ flood_val = None
1482
+ if water is not None and water_band is not None:
1483
+ flood_val = safe_get_reduce(roi, water.select(water_band), water_band, scale=30, default=None, max_pixels=int(5e7))
1484
+
1485
+ # landcover histogram
1486
+ lc_stats = {}
1487
+ if landcover is not None and lc_band is not None:
1488
+ lc_stats = safe_reduce_histogram(roi, landcover, lc_band, scale=30, max_pixels=int(5e7))
1489
+
1490
+ # NDVI timeseries (last 2 years) β€” provide small chart
1491
+ ndvi_ts = []
1492
+ if ndvi_col is not None:
1493
+ end = datetime.utcnow().strftime("%Y-%m-%d")
1494
+ start = (datetime.utcnow().replace(year=datetime.utcnow().year - 2)).strftime("%Y-%m-%d")
1495
+ ndvi_ts = safe_time_series(roi, ndvi_col, "NDVI", start, end, reducer=ee.Reducer.mean(), scale=1000, max_pixels=int(5e7))
1496
+
1497
+ # ----------------------------
1498
+ # UI: display numeric summary
1499
+ # ----------------------------
1500
+ def pretty(x, fmt="{:.2f}"):
1501
+ return "N/A" if x is None else fmt.format(x)
1502
+
1503
+ st.subheader("πŸ“Š Regional Data Summary")
1504
+ st.write(f"**Soil ({chosen_soil_band}):** {pretty(soil_val)}")
1505
+ st.write(f"**Average Elevation:** {pretty(elev_val, '{:.1f}')} m")
1506
+ st.write(f"**Seismic (mean):** {pretty(seismic_val)}")
1507
+ st.write(f"**Flood occurrence (mean %):** {pretty(flood_val)}")
1508
+
1509
+ # Landcover pie chart (colored)
1510
+ if lc_stats:
1511
+ # convert keys into ints if possible (WorldCover class codes)
1512
+ labels = []
1513
+ values = []
1514
+ for k, v in lc_stats.items():
1515
+ labels.append(str(k))
1516
+ values.append(v)
1517
+ fig1, ax1 = plt.subplots(figsize=(6,4))
1518
+ ax1.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
1519
+ ax1.set_title("Landcover Distribution (class codes)")
1520
+ st.pyplot(fig1)
1521
+ else:
1522
+ st.info("No landcover histogram available.")
1523
+
1524
+ # NDVI timeseries plot
1525
+ if ndvi_ts:
1526
+ dates = [d for d, v in ndvi_ts]
1527
+ vals = [v for d, v in ndvi_ts]
1528
+ fig2, ax2 = plt.subplots(figsize=(8,3))
1529
+ ax2.plot(dates, vals, marker="o")
1530
+ ax2.set_title("NDVI (mean) β€” last 2 years")
1531
+ ax2.set_xlabel("Date")
1532
+ ax2.set_ylabel("NDVI (scaled)")
1533
+ plt.xticks(rotation=45)
1534
+ st.pyplot(fig2)
1535
+ else:
1536
+ st.info("NDVI timeseries not available or too sparse.")
1537
+
1538
+ # Soil histogram (if available)
1539
  soil_hist = None
1540
+ try:
1541
+ soil_hist = soil_img.reduceRegion(
1542
+ reducer=ee.Reducer.histogram(maxBuckets=20),
1543
+ geometry=roi,
1544
+ scale=1000,
1545
+ maxPixels=int(5e7)
1546
+ ).get(chosen_soil_band).getInfo() if (soil_img is not None and chosen_soil_band) else None
1547
+ except Exception:
1548
+ soil_hist = None
1549
+
1550
+ if soil_hist and isinstance(soil_hist, dict) and "bucketMeans" in soil_hist:
1551
+ fig3, ax3 = plt.subplots(figsize=(6,4))
1552
+ ax3.bar(soil_hist["bucketMeans"], soil_hist["histogram"], width= (soil_hist["bucketMeans"][1]-soil_hist["bucketMeans"][0]) if len(soil_hist["bucketMeans"])>1 else 1, color="saddlebrown")
1553
+ ax3.set_title(f"Soil histogram ({chosen_soil_band})")
1554
+ ax3.set_xlabel("Clay fraction (kg/kg)")
1555
+ ax3.set_ylabel("Pixel count")
1556
+ st.pyplot(fig3)
1557
+
1558
+ # ----------------------------
1559
+ # Save results to session_state for reports
1560
+ # ----------------------------
1561
+ # Ensure sites and active site exist
1562
+ if "sites" not in st.session_state or "active_site" not in st.session_state:
1563
+ # just store soil_json if site structure not present
1564
+ st.session_state["soil_json"] = {
1565
+ "Soil": None if soil_val is None else float(soil_val),
1566
+ "Soil Band": chosen_soil_band,
1567
+ "Elevation": None if elev_val is None else float(elev_val),
1568
+ "Seismic": None if seismic_val is None else float(seismic_val),
1569
+ "Flood": None if flood_val is None else float(flood_val),
1570
+ "Landcover Stats": lc_stats or {},
1571
+ "NDVI TS": ndvi_ts or []
1572
+ }
1573
+ st.success("Saved results to st.session_state['soil_json']. (No active site present.)")
1574
+ else:
1575
+ # Save into active site JSON fields (keeping your field names unchanged)
1576
+ active = st.session_state["active_site"]
1577
+ try:
1578
+ site_obj = st.session_state["sites"][active]
1579
+ except Exception:
1580
+ # if your sites is a list of dicts with Site ID matching idx, try to match
1581
+ try:
1582
+ site_obj = st.session_state["sites"][int(active)]
1583
+ except Exception:
1584
+ site_obj = None
1585
+
1586
+ # fallback: if site_obj is None just write soil_json and exit
1587
+ if site_obj is None:
1588
+ st.session_state["soil_json"] = {
1589
+ "Soil": None if soil_val is None else float(soil_val),
1590
+ "Soil Band": chosen_soil_band,
1591
+ "Elevation": None if elev_val is None else float(elev_val),
1592
+ "Seismic": None if seismic_val is None else float(seismic_val),
1593
+ "Flood": None if flood_val is None else float(flood_val),
1594
+ "Landcover Stats": lc_stats or {},
1595
+ "NDVI TS": ndvi_ts or []
1596
+ }
1597
+ st.success("Saved results to st.session_state['soil_json']. (Could not find active site object.)")
1598
+ else:
1599
+ # update the exact fields you requested (names unchanged)
1600
+ site_obj["Soil Profile"] = f"{round(soil_val,3)} ({chosen_soil_band})" if soil_val is not None else "No data"
1601
+ site_obj["Topo Data"] = f"{round(elev_val,2)} m (mean)" if elev_val is not None else "No data"
1602
+ site_obj["Seismic Data"] = f"{round(seismic_val,4)}" if seismic_val is not None else "No data"
1603
+ site_obj["Flood Data"] = f"{round(flood_val,2)} %" if flood_val is not None else "No data"
1604
+ # Environmental Data: combine landcover summary + NDVI basic stats
1605
+ env_summary = {
1606
+ "Landcover Histogram": lc_stats or {},
1607
+ "NDVI_timeseries_points": ndvi_ts or []
1608
+ }
1609
+ site_obj["Environmental Data"] = env_summary
1610
+
1611
+ # Save drawn polygon GeoJSON for future map restore and report inclusion
1612
+ try:
1613
+ # fetch GeoJSON from ROI
1614
+ geojson = roi.toGeoJSON() if hasattr(roi, "toGeoJSON") else ee.Geometry(roi).getInfo()
1615
+ site_obj["drawn_polygon"] = geojson
1616
+ except Exception:
1617
+ site_obj["drawn_polygon"] = None
1618
+
1619
+ # Save to soil_json as well (for report block that expects it)
1620
+ st.session_state["soil_json"] = {
1621
+ "Soil": None if soil_val is None else float(soil_val),
1622
+ "Soil Band": chosen_soil_band,
1623
+ "Elevation": None if elev_val is None else float(elev_val),
1624
+ "Seismic": None if seismic_val is None else float(seismic_val),
1625
+ "Flood": None if flood_val is None else float(flood_val),
1626
+ "Landcover Stats": lc_stats or {},
1627
+ "NDVI TS": ndvi_ts or []
1628
+ }
1629
+ st.success("πŸ“‘ Results saved to active site and st.session_state['soil_json'] for report integration.")
1630
+
1631
+ # Snapshot map as HTML and save path into site object (map snapshot - small HTML content)
1632
+ try:
1633
+ snap_html = m.to_html(None) # returns HTML string if path None
1634
+ # store minimal snapshot content into site or soil_json (may be large; consider storing link)
1635
+ if "sites" in st.session_state and site_obj is not None:
1636
+ site_obj["map_snapshot"] = snap_html # caution: large string
1637
+ st.session_state["last_map_snapshot"] = snap_html
1638
+ except Exception:
1639
+ pass
1640
 
1641
+ # end of Compute Summaries block
 
 
 
 
 
 
 
1642
 
1643
+ # end locator_page()
 
 
 
 
 
 
 
 
 
 
 
 
 
1644
 
1645
  # GeoMate Ask (RAG) β€” simple chat with memory per site and auto-extract numeric values
1646
  def rag_page():