MSU576 commited on
Commit
6486dee
·
verified ·
1 Parent(s): 71f36f6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -360
app.py CHANGED
@@ -743,123 +743,118 @@ if "active_site" not in st.session_state:
743
 
744
 
745
  # -------------------- LANDING PAGE --------------------
746
- def landing_page():
747
-
748
- # Background hero with placeholder image (replace BACKGROUND_URL with your image path or URL)
749
-
750
- BACKGROUND_URL = "/app/background_placeholder.jpg" # <- replace this (or provide URL)
751
-
752
- st.markdown(f"""
753
-
754
- <div style="
755
-
756
- background-image: url('{BACKGROUND_URL}');
757
-
758
- background-size: cover;
759
-
760
- background-position: center;
761
-
762
- padding: 48px 28px;
763
-
764
- border-radius: 12px;
765
-
766
- margin-bottom: 18px;
767
-
768
- position: relative;
769
-
770
- ">
771
-
772
- <div style="background: rgba(11,11,11,0.55); padding:22px; border-radius:10px; max-width:900px;">
773
-
774
- <h1 style='color:#FF8C00; margin:0'>GeoMate V2</h1>
775
-
776
- <p style='color:#e8eef6; margin:6px 0 0; font-size:16px'>
777
-
778
- AI geotechnical copilot — soil recognition, classification, locator (EE), RAG-powered Q&A, and dynamic reports.
779
-
780
- </p>
781
-
782
- <div style='margin-top:8px; color:#cfcfcf; font-size:13px'>
783
-
784
- Quick: Classifier • GSD • Locator • RAG • Reports
785
-
786
- </div>
787
-
788
- </div>
789
-
790
- </div>
791
-
792
- """, unsafe_allow_html=True)
793
-
794
-
795
-
796
- st.markdown("<div style='display:flex;align-items:center;gap:12px'>"
797
-
798
- "<div style='width:76px;height:76px;border-radius:14px;background:linear-gradient(135deg,#ff7a00,#ff3a3a);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 24px rgba(0,0,0,0.6)'>"
799
-
800
- "<span style='font-size:34px'>🛰️</span></div>"
801
-
802
- "<div><h1 style='margin:0;color:#FF8C00'>GeoMate V2</h1>"
803
-
804
- "<div class='small-muted'>AI geotechnical copilot — soil recognition, classification, locator, RAG, and reports</div></div></div>", unsafe_allow_html=True)
805
-
806
- st.markdown("---")
807
-
808
- col1, col2 = st.columns([2,1])
809
-
810
- with col1:
811
-
812
- st.markdown("<div class='gm-card'>", unsafe_allow_html=True)
813
-
814
- st.write("GeoMate is built to help geotechnical engineers: classify soils (USCS/AASHTO), plot GSD, fetch Earth Engine data, chat with a RAG-backed LLM, and generate professional geotechnical reports.")
815
-
816
- st.markdown("</div>", unsafe_allow_html=True)
817
-
818
- st.markdown("### Quick Actions")
819
-
820
- c1, c2, c3 = st.columns(3)
821
-
822
- if c1.button("🧪 Classifier"):
823
-
824
- st.session_state["page"] = "Classifier"; st.rerun()
825
-
826
- if c2.button("📈 GSD Curve"):
827
-
828
- st.session_state["page"] = "GSD"; st.rerun()
829
-
830
- if c3.button("🌍 Locator"):
831
-
832
- st.session_state["page"] = "Locator"; st.rerun()
833
-
834
- c4, c5, c6 = st.columns(3)
835
-
836
- if c4.button("🤖 GeoMate Ask"):
837
-
838
- st.session_state["page"] = "RAG"; st.rerun()
839
-
840
- if c5.button("📷 OCR"):
841
-
842
- st.session_state["page"] = "OCR"; st.rerun()
843
-
844
- if c6.button("📑 Reports"):
845
-
846
- st.session_state["page"] = "Reports"; st.rerun()
847
-
848
- with col2:
849
-
850
- st.markdown("<div class='gm-card' style='text-align:center'>", unsafe_allow_html=True)
851
-
852
- st.markdown("<h3 style='color:#FF8C00'>Live Site Summary</h3>", unsafe_allow_html=True)
853
-
854
- site = st.session_state["sites"][st.session_state["active_site"]]
855
-
856
- st.write(f"Site: **{site.get('Site Name')}**")
857
-
858
- st.write(f"USCS: {site.get('USCS')}, AASHTO: {site.get('AASHTO')}")
859
 
860
- st.write(f"GSD saved: {'Yes' if site.get('GSD') else 'No'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
 
862
- st.markdown("</div>", unsafe_allow_html=True)
863
 
864
  # Soil Classifier page (conversational, step-by-step)
865
  def soil_classifier_page():
@@ -1068,20 +1063,16 @@ import geemap.foliumap as geemap
1068
  import ee
1069
  import matplotlib.pyplot as plt
1070
  from datetime import datetime
1071
- from io import BytesIO
1072
- import base64
1073
- import folium
1074
-
1075
  import tempfile
 
 
1076
 
 
 
 
1077
  def export_map_snapshot(m, width=800, height=600):
1078
- """
1079
- Export geemap Map object to PNG snapshot (returns bytes).
1080
- - m: geemap.Map
1081
- - width, height: dimensions of snapshot
1082
- """
1083
  try:
1084
- # geemap has a built-in screenshot method
1085
  tmpfile = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
1086
  m.screenshot(filename=tmpfile.name, region=None, dimensions=(width, height))
1087
  with open(tmpfile.name, "rb") as f:
@@ -1090,16 +1081,10 @@ def export_map_snapshot(m, width=800, height=600):
1090
  st.warning(f"Map snapshot failed: {e}")
1091
  return None
1092
 
1093
-
 
 
1094
  def locator_page():
1095
- """
1096
- Robust locator page:
1097
- - Uses initialize_ee() auth routine (expects EARTHENGINE_TOKEN / SERVICE_ACCOUNT in env)
1098
- - Shows interactive map with basemaps and overlays
1099
- - Captures ROI drawn on the map
1100
- - Computes summaries and saves them into active site and soil_json
1101
- """
1102
-
1103
  st.title("🌍 GeoMate Interactive Earth Explorer")
1104
  st.markdown(
1105
  "Draw a polygon (or rectangle) on the map using the drawing tool. "
@@ -1180,191 +1165,71 @@ def locator_page():
1180
  # ----------------------------
1181
  m = geemap.Map(center=[28.0, 72.0], zoom=5, plugin_Draw=True, draw_export=True, locate_control=True)
1182
 
1183
- basemaps = [
1184
- "HYBRID", "ROADMAP", "TERRAIN", "SATELLITE",
1185
- "Esri.WorldImagery", "Esri.WorldTopoMap", "Esri.WorldShadedRelief",
1186
- "Esri.NatGeoWorldMap", "Esri.OceanBasemap",
1187
- "CartoDB.Positron", "CartoDB.DarkMatter",
1188
- "Stamen.Terrain", "Stamen.Watercolor",
1189
- "OpenStreetMap", "Esri.WorldGrayCanvas", "Esri.WorldStreetMap"
1190
- ]
1191
- for b in basemaps:
1192
  try:
1193
- m.add_basemap(b)
1194
- except Exception:
1195
- pass
 
 
1196
 
1197
  # ----------------------------
1198
  # Datasets (DEM, Soil, Seismic, Flood, Landcover, NDVI)
1199
  # ----------------------------
1200
-
1201
- # --- DEM ---
1202
  try:
1203
- dem = ee.Image("NASA/NASADEM_HGT/001") # NASADEM ~30m global
1204
- dem_band_name = "elevation"
1205
- st.info("Using NASADEM dataset for elevation (30m).")
1206
  except Exception:
1207
  try:
1208
- dem = ee.Image("USGS/SRTMGL1_003") # SRTM ~30m fallback
1209
- dem_band_name = "elevation"
1210
- st.warning("Fallback to SRTM DEM (30m).")
1211
  except Exception:
1212
- dem = None
1213
- dem_band_name = None
1214
- st.error("No DEM dataset available.")
1215
-
1216
- # --- Soil Clay Fraction ---
1217
- soil_img = None
1218
- soil_band = None
1219
- chosen_soil_band = None
1220
  try:
1221
  soil_img = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-3A1A1A_M/v02")
1222
  bands = soil_img.bandNames().getInfo()
1223
- # interactive soil band selector
1224
- chosen_soil_band = st.selectbox(
1225
- "Select soil depth / clay band",
1226
- options=bands,
1227
- index=bands.index("b200") if "b200" in bands else 0
1228
- )
1229
- st.info(f"Using OpenLandMap Clay Fraction — Band: {chosen_soil_band}")
1230
  except Exception:
1231
  try:
1232
  soil_img = ee.Image("projects/soilgrids-isric/clay_mean")
1233
  bands = soil_img.bandNames().getInfo()
1234
- chosen_soil_band = st.selectbox(
1235
- "Select soil depth (SoilGrids)",
1236
- options=bands,
1237
- index=0
1238
- )
1239
- st.warning(f"Fallback to SoilGrids Clay dataset — Band: {chosen_soil_band}")
1240
  except Exception:
1241
- soil_img = None
1242
- chosen_soil_band = None
1243
- st.error("No soil dataset available.")
1244
-
1245
- # --- Seismic Hazard ---
1246
  try:
1247
- seismic_img = ee.Image("SEDAC/GSHAPSeismicHazard")
1248
- seismic_band = "gshap"
1249
- st.info("Using SEDAC Global Seismic Hazard dataset.")
1250
  except Exception:
1251
- seismic_img = None
1252
- seismic_band = None
1253
- st.error("No seismic hazard dataset available.")
1254
-
1255
- # --- Flood Occurrence ---
1256
  try:
1257
- water = ee.Image("JRC/GSW1_4/GlobalSurfaceWater")
1258
- water_band = "occurrence"
1259
- st.info("Using JRC Global Surface Water Occurrence (1984–2020).")
1260
  except Exception:
1261
- water = None
1262
- water_band = None
1263
- st.error("No flood dataset available.")
1264
-
1265
- # --- Landcover ---
1266
  try:
1267
- landcover = ee.Image("ESA/WorldCover/v200")
1268
- lc_band = "Map"
1269
- st.info("Using ESA WorldCover v200 (2020, 10m).")
1270
  except Exception:
1271
- landcover = None
1272
- lc_band = None
1273
- st.error("No landcover dataset available.")
1274
-
1275
- # --- NDVI ---
1276
  try:
1277
  ndvi_col = ee.ImageCollection("MODIS/061/MOD13A2").select("NDVI")
1278
- st.info("Using MODIS NDVI (16-day, 1km, global).")
1279
  except Exception:
1280
  ndvi_col = None
1281
- st.error("No NDVI dataset available.")
1282
-
1283
- # ----------------------------
1284
- # Add Layers to Map
1285
  # ----------------------------
1286
- if dem:
1287
- try:
1288
- m.addLayer(
1289
- dem,
1290
- {"min": 0, "max": 4000, "palette": ["blue", "green", "brown", "white"]},
1291
- "DEM / Elevation"
1292
- )
1293
- except Exception:
1294
- pass
1295
-
1296
- if soil_img and chosen_soil_band:
1297
- try:
1298
- m.addLayer(
1299
- soil_img.select(chosen_soil_band),
1300
- {"min": 0.0, "max": 0.6, "palette": ["#ffffcc", "#c2e699", "#78c679", "#31a354"]},
1301
- f"Soil Clay Fraction ({chosen_soil_band})"
1302
- )
1303
- except Exception:
1304
- pass
1305
-
1306
- if seismic_img:
1307
- try:
1308
- m.addLayer(
1309
- seismic_img,
1310
- {"min": 0, "max": 1, "palette": ["white", "yellow", "red"]},
1311
- "Seismic Hazard"
1312
- )
1313
- except Exception:
1314
- pass
1315
-
1316
- if water:
1317
- try:
1318
- m.addLayer(
1319
- water.select(water_band),
1320
- {"min": 0, "max": 100, "palette": ["white", "blue"]},
1321
- "Flood Occurrence (%)"
1322
- )
1323
- except Exception:
1324
- pass
1325
-
1326
- if landcover:
1327
- try:
1328
- m.addLayer(
1329
- landcover,
1330
- {
1331
- "min": 10,
1332
- "max": 100,
1333
- "palette": [
1334
- "#006400", "#ffbb22", "#ffff4c", "#f096ff", "#fa0000",
1335
- "#b4b4b4", "#f0f0f0", "#0064c8", "#0096a0", "#00cf75"
1336
- ]
1337
- },
1338
- "Landcover (ESA WorldCover)"
1339
- )
1340
- except Exception:
1341
- pass
1342
-
1343
- try:
1344
- countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
1345
- m.addLayer(
1346
- countries.style(**{"color": "black", "fillColor": "00000000", "width": 1}),
1347
- {},
1348
- "Country Boundaries"
1349
- )
1350
- except Exception:
1351
- pass
1352
-
1353
  # ----------------------------
1354
- from streamlit_folium import st_folium
1355
- import json
1356
 
1357
- # --- Render geemap map ---
1358
- m.to_streamlit(height=600, responsive=True)
1359
-
1360
- # Capture drawn ROI
1361
- result = st_folium(m, width=700, height=600, returned_objects=["last_active_drawing"])
1362
-
1363
- roi = None
1364
- coords = None
1365
- flat_coords = None
1366
-
1367
- # --- Get ROI from drawn feature ---
1368
  if result and "last_active_drawing" in result and result["last_active_drawing"]:
1369
  feat = result["last_active_drawing"]
1370
  geom = feat.get("geometry")
@@ -1373,24 +1238,18 @@ def locator_page():
1373
  roi = ee.Geometry(geom)
1374
  coords = geom.get("coordinates", None)
1375
  st.session_state["roi_geojson"] = feat
1376
-
1377
- # Flatten coordinates
1378
  if coords:
1379
  if geom["type"] in ["Polygon", "MultiPolygon"]:
1380
  flat_coords = [(lat, lon) for ring in coords for lon, lat in ring]
1381
  elif geom["type"] == "Point":
1382
- lon, lat = coords
1383
- flat_coords = [(lat, lon)]
1384
  elif geom["type"] == "LineString":
1385
  flat_coords = [(lat, lon) for lon, lat in coords]
1386
-
1387
- if flat_coords:
1388
- st.session_state["roi_coords"] = flat_coords # ✅ Save for reuse
1389
  st.success("✅ ROI captured!")
1390
  except Exception as e:
1391
- st.error(f"Failed to convert drawn geometry to ee.Geometry: {e}")
1392
 
1393
- # --- Restore ROI from session if none drawn ---
1394
  if roi is None and "roi_geojson" in st.session_state:
1395
  saved = st.session_state["roi_geojson"]
1396
  try:
@@ -1402,81 +1261,81 @@ def locator_page():
1402
  if geom["type"] in ["Polygon", "MultiPolygon"]:
1403
  flat_coords = [(lat, lon) for ring in coords for lon, lat in ring]
1404
  elif geom["type"] == "Point":
1405
- lon, lat = coords
1406
- flat_coords = [(lat, lon)]
1407
  elif geom["type"] == "LineString":
1408
  flat_coords = [(lat, lon) for lon, lat in coords]
1409
-
1410
- if flat_coords:
1411
- st.session_state["roi_coords"] = flat_coords # ✅ Restore saved
1412
- st.info("♻️ ROI restored from earlier session")
1413
  except Exception as e:
1414
  st.warning(f"Could not restore ROI: {e}")
1415
 
1416
- # --- Show coordinates below the map ---
1417
  if "roi_coords" in st.session_state:
1418
  st.markdown("### 📍 ROI Coordinates (Lat, Lon)")
1419
  st.write(st.session_state["roi_coords"])
1420
 
1421
- # --- Compute summaries ---
 
 
1422
  if st.button("Compute Summaries"):
1423
  if roi is None:
1424
- st.error("⚠️ No ROI found. Please draw a polygon/rectangle/circle and try again.")
1425
  else:
1426
- st.success("ROI ready — performing computations...")
1427
- # Here you can now use st.session_state["roi_coords"] for summaries
1428
- chosen_soil_band = None
1429
- if soil_img:
1430
- bands = soil_img.bandNames().getInfo()
1431
- chosen_soil_band = st.selectbox("Soil band to analyze", bands, index=bands.index(soil_band) if soil_band in bands else 0)
1432
-
1433
- soil_val = safe_get_reduce(roi, soil_img.select(chosen_soil_band), chosen_soil_band, 1000) if soil_img and chosen_soil_band else None
1434
- elev_val = safe_get_reduce(roi, dem, dem_band_name, 1000)
1435
- seismic_val = safe_get_reduce(roi, seismic_img, seismic_band, 5000) if seismic_img else None
1436
- flood_val = safe_get_reduce(roi, water.select(water_band), water_band, 30) if water else None
1437
- lc_stats = safe_reduce_histogram(roi, landcover, lc_band, 30) if landcover else {}
1438
- ndvi_ts = []
1439
- if ndvi_col:
1440
- end = datetime.utcnow().strftime("%Y-%m-%d")
1441
- start = (datetime.utcnow().replace(year=datetime.utcnow().year-2)).strftime("%Y-%m-%d")
1442
- ndvi_ts = safe_time_series(roi, ndvi_col, "NDVI", start, end)
1443
-
1444
- # Save results
1445
- active = st.session_state.get("active_site", 0)
1446
- if "sites" in st.session_state:
1447
- site = st.session_state["sites"][active]
1448
- site["ROI"] = roi.getInfo()
1449
- site["Soil Profile"] = f"{soil_val} ({chosen_soil_band})" if soil_val else "N/A"
1450
- site["Topo Data"] = f"{elev_val} m" if elev_val else "N/A"
1451
- site["Seismic Data"] = seismic_val
1452
- site["Flood Data"] = flood_val
1453
- site["Environmental Data"] = {"Landcover": lc_stats, "NDVI": ndvi_ts}
1454
- st.session_state["soil_json"] = {
1455
- "Soil": soil_val, "Soil Band": chosen_soil_band,
1456
- "Elevation": elev_val, "Seismic": seismic_val,
1457
- "Flood": flood_val, "Landcover Stats": lc_stats,
1458
- "NDVI TS": ndvi_ts
1459
- }
1460
- # --- Save map snapshot ---
1461
- map_bytes = export_map_snapshot(m)
1462
- if map_bytes:
1463
- st.session_state["last_map_snapshot"] = map_bytes
1464
  if "sites" in st.session_state:
1465
- st.session_state["sites"][active]["map_snapshot"] = map_bytes
1466
- st.image(map_bytes, caption="Map Snapshot", use_column_width=True)
1467
-
1468
- # Display
1469
- st.subheader("📊 Summary")
1470
- st.write(f"**Soil:** {soil_val}")
1471
- st.write(f"**Elevation:** {elev_val}")
1472
- st.write(f"**Seismic:** {seismic_val}")
1473
- st.write(f"**Flood:** {flood_val}")
1474
- st.json(lc_stats)
1475
- if ndvi_ts:
1476
- d,v = zip(*ndvi_ts)
1477
- fig, ax = plt.subplots()
1478
- ax.plot(d, v, marker="o"); ax.set_title("NDVI"); ax.set_xlabel("Date")
1479
- st.pyplot(fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1480
 
1481
  # GeoMate Ask (RAG) — simple chat with memory per site and auto-extract numeric values
1482
  import re, json, pickle
 
743
 
744
 
745
  # -------------------- LANDING PAGE --------------------
746
+ import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747
 
748
+ def landing_page():
749
+ # Fixed gradient background (no slideshow)
750
+ st.markdown("""
751
+ <style>
752
+ .stApp {
753
+ background: linear-gradient(135deg, #000000 0%, #1c1c1c 50%, #2e004f 100%);
754
+ color: #ffffff;
755
+ font-family: 'Segoe UI', sans-serif;
756
+ }
757
+ .gm-hero {
758
+ padding: 48px 28px;
759
+ border-radius: 16px;
760
+ margin-bottom: 22px;
761
+ background: linear-gradient(135deg, rgba(11,11,11,0.8) 0%, rgba(20,0,40,0.75) 100%);
762
+ box-shadow: 0 8px 28px rgba(0,0,0,0.65);
763
+ text-align: center;
764
+ animation: fadeIn 1.8s ease-in-out;
765
+ }
766
+ .gm-hero h1 {
767
+ font-size: 3.5rem;
768
+ font-weight: 900;
769
+ margin: 0;
770
+ background: linear-gradient(90deg,#FF8C00,#a020f0);
771
+ -webkit-background-clip: text;
772
+ -webkit-text-fill-color: transparent;
773
+ }
774
+ .gm-hero p {
775
+ font-size: 1.2rem;
776
+ margin-top: 10px;
777
+ color: #e0e0e0;
778
+ }
779
+ .gm-card {
780
+ background: rgba(25,25,25,0.85);
781
+ padding: 18px;
782
+ border-radius: 12px;
783
+ box-shadow: 0 6px 18px rgba(0,0,0,0.5);
784
+ }
785
+ .quick-btn {
786
+ display: inline-block;
787
+ margin: 6px;
788
+ padding: 12px 24px;
789
+ border-radius: 50px;
790
+ background: linear-gradient(90deg, #ff4d00, #a020f0);
791
+ color: white !important;
792
+ font-weight: 600;
793
+ text-decoration: none;
794
+ transition: all 0.3s ease;
795
+ }
796
+ .quick-btn:hover {
797
+ background: linear-gradient(90deg, #a020f0, #ff4d00);
798
+ transform: translateY(-3px);
799
+ box-shadow: 0 6px 16px rgba(0,0,0,0.4);
800
+ }
801
+ @keyframes fadeIn {
802
+ from { opacity:0; transform: translateY(40px);}
803
+ to { opacity:1; transform: translateY(0);}
804
+ }
805
+ </style>
806
+ """, unsafe_allow_html=True)
807
+
808
+ # Hero Section
809
+ st.markdown("""
810
+ <div class="gm-hero">
811
+ <h1>GeoMate V2</h1>
812
+ <p>AI Geotechnical Copilot — soil recognition, classification,
813
+ Earth Engine locator, RAG-powered Q&A, OCR, and dynamic reports.</p>
814
+ </div>
815
+ """, unsafe_allow_html=True)
816
+
817
+ # Content layout
818
+ col1, col2 = st.columns([2, 1])
819
+
820
+ with col1:
821
+ st.markdown("<div class='gm-card'>", unsafe_allow_html=True)
822
+ st.write(
823
+ "GeoMate helps geotechnical engineers: classify soils (USCS/AASHTO), "
824
+ "plot grain size distributions (GSD), fetch Earth Engine data, chat with a RAG-backed LLM, "
825
+ "run OCR on site logs, and generate professional reports."
826
+ )
827
+ st.markdown("</div>", unsafe_allow_html=True)
828
+
829
+ st.markdown("### 🚀 Quick Actions")
830
+ c1, c2, c3 = st.columns(3)
831
+ if c1.button("🧪 Classifier"):
832
+ st.session_state["page"] = "Classifier"; st.rerun()
833
+ if c2.button("📈 Soil Recognizer"):
834
+ st.session_state["page"] = "Soil recognizer"; st.rerun()
835
+ if c3.button("🌍 Locator"):
836
+ st.session_state["page"] = "Locator"; st.rerun()
837
+
838
+ c4, c5, c6 = st.columns(3)
839
+ if c4.button("🤖 Ask GeoMate"):
840
+ st.session_state["page"] = "RAG"; st.rerun()
841
+ if c5.button("📷 OCR"):
842
+ st.session_state["page"] = "OCR"; st.rerun()
843
+ if c6.button("📑 Reports"):
844
+ st.session_state["page"] = "Reports"; st.rerun()
845
+
846
+ with col2:
847
+ st.markdown("<div class='gm-card' style='text-align:center'>", unsafe_allow_html=True)
848
+ st.markdown("<h3 style='color:#FF8C00'>📊 Live Site Summary</h3>", unsafe_allow_html=True)
849
+ if "sites" in st.session_state and st.session_state.get("active_site") is not None:
850
+ site = st.session_state["sites"][st.session_state["active_site"]]
851
+ st.write(f"**Site:** {site.get('Site Name','N/A')}")
852
+ st.write(f"USCS: {site.get('USCS','-')} | AASHTO: {site.get('AASHTO','-')}")
853
+ st.write(f"GSD Saved: {'✅' if site.get('GSD') else '❌'}")
854
+ else:
855
+ st.info("No active site selected.")
856
+ st.markdown("</div>", unsafe_allow_html=True)
857
 
 
858
 
859
  # Soil Classifier page (conversational, step-by-step)
860
  def soil_classifier_page():
 
1063
  import ee
1064
  import matplotlib.pyplot as plt
1065
  from datetime import datetime
 
 
 
 
1066
  import tempfile
1067
+ from streamlit_folium import st_folium
1068
+ import folium
1069
 
1070
+ # =====================================================
1071
+ # Map snapshot export
1072
+ # =====================================================
1073
  def export_map_snapshot(m, width=800, height=600):
1074
+ """Export geemap Map object to PNG snapshot (returns bytes)."""
 
 
 
 
1075
  try:
 
1076
  tmpfile = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
1077
  m.screenshot(filename=tmpfile.name, region=None, dimensions=(width, height))
1078
  with open(tmpfile.name, "rb") as f:
 
1081
  st.warning(f"Map snapshot failed: {e}")
1082
  return None
1083
 
1084
+ # =====================================================
1085
+ # Locator page
1086
+ # =====================================================
1087
  def locator_page():
 
 
 
 
 
 
 
 
1088
  st.title("🌍 GeoMate Interactive Earth Explorer")
1089
  st.markdown(
1090
  "Draw a polygon (or rectangle) on the map using the drawing tool. "
 
1165
  # ----------------------------
1166
  m = geemap.Map(center=[28.0, 72.0], zoom=5, plugin_Draw=True, draw_export=True, locate_control=True)
1167
 
1168
+ # Restore ROI (if available) as polygon on the map
1169
+ if "roi_geojson" in st.session_state:
 
 
 
 
 
 
 
1170
  try:
1171
+ saved = st.session_state["roi_geojson"]
1172
+ folium.GeoJson(saved, name="Saved ROI",
1173
+ style_function=lambda x: {"color": "red", "weight": 2, "fillOpacity": 0.1}).add_to(m)
1174
+ except Exception as e:
1175
+ st.warning(f"Could not re-add saved ROI: {e}")
1176
 
1177
  # ----------------------------
1178
  # Datasets (DEM, Soil, Seismic, Flood, Landcover, NDVI)
1179
  # ----------------------------
1180
+ # DEM
 
1181
  try:
1182
+ dem = ee.Image("NASA/NASADEM_HGT/001"); dem_band_name = "elevation"
 
 
1183
  except Exception:
1184
  try:
1185
+ dem = ee.Image("USGS/SRTMGL1_003"); dem_band_name = "elevation"
 
 
1186
  except Exception:
1187
+ dem = None; dem_band_name = None
1188
+
1189
+ # Soil
1190
+ soil_img = None; chosen_soil_band = None
 
 
 
 
1191
  try:
1192
  soil_img = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-3A1A1A_M/v02")
1193
  bands = soil_img.bandNames().getInfo()
1194
+ chosen_soil_band = st.selectbox("Select soil depth / clay band", options=bands, index=bands.index("b200") if "b200" in bands else 0)
 
 
 
 
 
 
1195
  except Exception:
1196
  try:
1197
  soil_img = ee.Image("projects/soilgrids-isric/clay_mean")
1198
  bands = soil_img.bandNames().getInfo()
1199
+ chosen_soil_band = st.selectbox("Select soil depth (SoilGrids)", options=bands, index=0)
 
 
 
 
 
1200
  except Exception:
1201
+ soil_img = None; chosen_soil_band = None
1202
+
1203
+ # Seismic
 
 
1204
  try:
1205
+ seismic_img = ee.Image("SEDAC/GSHAPSeismicHazard"); seismic_band = "gshap"
 
 
1206
  except Exception:
1207
+ seismic_img = None; seismic_band = None
1208
+
1209
+ # Flood
 
 
1210
  try:
1211
+ water = ee.Image("JRC/GSW1_4/GlobalSurfaceWater"); water_band = "occurrence"
 
 
1212
  except Exception:
1213
+ water = None; water_band = None
1214
+
1215
+ # Landcover
 
 
1216
  try:
1217
+ landcover = ee.Image("ESA/WorldCover/v200"); lc_band = "Map"
 
 
1218
  except Exception:
1219
+ landcover = None; lc_band = None
1220
+
1221
+ # NDVI
 
 
1222
  try:
1223
  ndvi_col = ee.ImageCollection("MODIS/061/MOD13A2").select("NDVI")
 
1224
  except Exception:
1225
  ndvi_col = None
1226
+
 
 
 
1227
  # ----------------------------
1228
+ # Render map + capture draw ROI (only once with st_folium)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1229
  # ----------------------------
1230
+ result = st_folium(m, width=800, height=600, returned_objects=["last_active_drawing"])
1231
+ roi, coords, flat_coords = None, None, None
1232
 
 
 
 
 
 
 
 
 
 
 
 
1233
  if result and "last_active_drawing" in result and result["last_active_drawing"]:
1234
  feat = result["last_active_drawing"]
1235
  geom = feat.get("geometry")
 
1238
  roi = ee.Geometry(geom)
1239
  coords = geom.get("coordinates", None)
1240
  st.session_state["roi_geojson"] = feat
 
 
1241
  if coords:
1242
  if geom["type"] in ["Polygon", "MultiPolygon"]:
1243
  flat_coords = [(lat, lon) for ring in coords for lon, lat in ring]
1244
  elif geom["type"] == "Point":
1245
+ lon, lat = coords; flat_coords = [(lat, lon)]
 
1246
  elif geom["type"] == "LineString":
1247
  flat_coords = [(lat, lon) for lon, lat in coords]
1248
+ if flat_coords: st.session_state["roi_coords"] = flat_coords
 
 
1249
  st.success("✅ ROI captured!")
1250
  except Exception as e:
1251
+ st.error(f"Failed to convert geometry: {e}")
1252
 
 
1253
  if roi is None and "roi_geojson" in st.session_state:
1254
  saved = st.session_state["roi_geojson"]
1255
  try:
 
1261
  if geom["type"] in ["Polygon", "MultiPolygon"]:
1262
  flat_coords = [(lat, lon) for ring in coords for lon, lat in ring]
1263
  elif geom["type"] == "Point":
1264
+ lon, lat = coords; flat_coords = [(lat, lon)]
 
1265
  elif geom["type"] == "LineString":
1266
  flat_coords = [(lat, lon) for lon, lat in coords]
1267
+ if flat_coords: st.session_state["roi_coords"] = flat_coords
1268
+ st.info("♻️ ROI restored from session")
 
 
1269
  except Exception as e:
1270
  st.warning(f"Could not restore ROI: {e}")
1271
 
1272
+ # Show coordinates
1273
  if "roi_coords" in st.session_state:
1274
  st.markdown("### 📍 ROI Coordinates (Lat, Lon)")
1275
  st.write(st.session_state["roi_coords"])
1276
 
1277
+ # ----------------------------
1278
+ # Compute summaries
1279
+ # ----------------------------
1280
  if st.button("Compute Summaries"):
1281
  if roi is None:
1282
+ st.error("⚠️ No ROI found. Please draw first.")
1283
  else:
1284
+ st.success("ROI ready — computing...")
1285
+
1286
+ soil_val = safe_get_reduce(roi, soil_img.select(chosen_soil_band), chosen_soil_band, 1000) if soil_img and chosen_soil_band else None
1287
+ elev_val = safe_get_reduce(roi, dem, dem_band_name, 1000) if dem else None
1288
+ seismic_val = safe_get_reduce(roi, seismic_img, seismic_band, 5000) if seismic_img else None
1289
+ flood_val = safe_get_reduce(roi, water.select(water_band), water_band, 30) if water else None
1290
+ lc_stats = safe_reduce_histogram(roi, landcover, lc_band, 30) if landcover else {}
1291
+ ndvi_ts = []
1292
+ if ndvi_col:
1293
+ end = datetime.utcnow().strftime("%Y-%m-%d")
1294
+ start = (datetime.utcnow().replace(year=datetime.utcnow().year-2)).strftime("%Y-%m-%d")
1295
+ ndvi_ts = safe_time_series(roi, ndvi_col, "NDVI", start, end)
1296
+
1297
+ # Save results
1298
+ active = st.session_state.get("active_site", 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1299
  if "sites" in st.session_state:
1300
+ site = st.session_state["sites"][active]
1301
+ if roi:
1302
+ try:
1303
+ site["ROI"] = roi.getInfo()
1304
+ except Exception:
1305
+ site["ROI"] = "Not available"
1306
+ site["Soil Profile"] = f"{soil_val} ({chosen_soil_band})" if soil_val else "N/A"
1307
+ site["Topo Data"] = f"{elev_val} m" if elev_val else "N/A"
1308
+ site["Seismic Data"] = seismic_val if seismic_val else "N/A"
1309
+ site["Flood Data"] = flood_val if flood_val else "N/A"
1310
+ site["Environmental Data"] = {"Landcover": lc_stats, "NDVI": ndvi_ts}
1311
+
1312
+ st.session_state["soil_json"] = {
1313
+ "Soil": soil_val, "Soil Band": chosen_soil_band,
1314
+ "Elevation": elev_val, "Seismic": seismic_val,
1315
+ "Flood": flood_val, "Landcover Stats": lc_stats,
1316
+ "NDVI TS": ndvi_ts
1317
+ }
1318
+
1319
+ # Map snapshot
1320
+ map_bytes = export_map_snapshot(m)
1321
+ if map_bytes:
1322
+ st.session_state["last_map_snapshot"] = map_bytes
1323
+ if "sites" in st.session_state:
1324
+ st.session_state["sites"][active]["map_snapshot"] = map_bytes
1325
+ st.image(map_bytes, caption="Map Snapshot", use_column_width=True)
1326
+
1327
+ # Display
1328
+ st.subheader("📊 Summary")
1329
+ st.write(f"**Soil:** {soil_val}")
1330
+ st.write(f"**Elevation:** {elev_val}")
1331
+ st.write(f"**Seismic:** {seismic_val}")
1332
+ st.write(f"**Flood:** {flood_val}")
1333
+ st.json(lc_stats)
1334
+ if ndvi_ts:
1335
+ d, v = zip(*ndvi_ts)
1336
+ fig, ax = plt.subplots()
1337
+ ax.plot(d, v, marker="o"); ax.set_title("NDVI"); ax.set_xlabel("Date")
1338
+ st.pyplot(fig)
1339
 
1340
  # GeoMate Ask (RAG) — simple chat with memory per site and auto-extract numeric values
1341
  import re, json, pickle