MSU576 commited on
Commit
0ca193d
·
verified ·
1 Parent(s): 73ab862

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +220 -426
app.py CHANGED
@@ -985,6 +985,7 @@ def ocr_page():
985
  else:
986
  st.warning("OCR not available in this deployment.")
987
  # Locator Page (with Earth Engine auth at top)
 
988
 
989
  import os
990
  import json
@@ -995,46 +996,59 @@ import matplotlib.pyplot as plt
995
  from datetime import datetime
996
  from io import BytesIO
997
  import base64
998
- from streamlit_folium import st_folium
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
999
 
1000
  def locator_page():
1001
  """
1002
  Robust locator page:
1003
- - Uses your initialize_ee() auth routine (expects EARTHENGINE_TOKEN / SERVICE_ACCOUNT in env)
1004
- - Shows interactive map with many basemaps and overlays
1005
- - Safe reducers with fallbacks and caching
1006
- - Stores results in st.session_state['soil_json'] AND in the active site entry under Earth Engine fields
1007
  """
1008
 
1009
  st.title("🌍 GeoMate Interactive Earth Explorer")
1010
  st.markdown(
1011
  "Draw a polygon (or rectangle) on the map using the drawing tool. "
1012
- "The app will compute regional summaries (soil clay, elevation, seismic, flood occurrence, landcover, NDVI) "
1013
- "and save results for reports."
1014
  )
1015
 
1016
  # ----------------------------
1017
- # Use your existing EE init function if present
 
1018
  EARTHENGINE_TOKEN = os.getenv("EARTHENGINE_TOKEN")
1019
- SERVICE_ACCOUNT = os.getenv("SERVICE_ACCOUNT") # optional: service account email
1020
 
1021
  def initialize_ee():
1022
- """Initialize Earth Engine with multiple fallbacks."""
1023
  if "ee_initialized" in st.session_state and st.session_state["ee_initialized"]:
1024
  return True
1025
-
1026
  if EARTHENGINE_TOKEN and SERVICE_ACCOUNT:
1027
  try:
1028
- creds = ee.ServiceAccountCredentials(
1029
- email=SERVICE_ACCOUNT,
1030
- key_data=EARTHENGINE_TOKEN
1031
- )
1032
  ee.Initialize(creds)
1033
  st.session_state["ee_initialized"] = True
1034
  return True
1035
  except Exception as e:
1036
- st.warning(f"Service account init failed: {e} trying default/interactive auth...")
1037
-
1038
  try:
1039
  ee.Initialize()
1040
  st.session_state["ee_initialized"] = True
@@ -1046,119 +1060,52 @@ def locator_page():
1046
  st.session_state["ee_initialized"] = True
1047
  return True
1048
  except Exception as e:
1049
- st.error(f"Earth Engine authentication failed: {e}")
1050
  return False
1051
 
1052
  if not initialize_ee():
1053
  st.stop()
1054
 
1055
  # ----------------------------
1056
- # I assume your file defines initialize_ee() exactly like your earlier message.
1057
- try:
1058
- init_ok = initialize_ee() # call your auth initializer
1059
- except NameError:
1060
- st.error("Auth initializer `initialize_ee()` not found. Ensure your auth code exists above this function.")
1061
- return
1062
- if not init_ok:
1063
- return
1064
-
1065
  # ----------------------------
1066
- # Helper: safe reducers + caching
1067
- # ----------------------------
1068
- # --- Ensure roi exists in the local scope to avoid UnboundLocalError ---
1069
- roi = None
1070
  def safe_get_reduce(region, image, band, scale=1000, default=None, max_pixels=int(1e7)):
1071
- """Return float or None. Uses reduceRegion mean safely."""
1072
- cache_key = f"reduce::{region.toGeoJSONString()[:200]}::{str(image)}::{band}::{scale}"
1073
- # check cache
1074
- cache = st.session_state.setdefault("_ee_cache", {})
1075
- if cache_key in cache:
1076
- return cache[cache_key]
1077
-
1078
  try:
1079
- rr = image.reduceRegion(
1080
- reducer=ee.Reducer.mean(),
1081
- geometry=region,
1082
- scale=int(scale),
1083
- maxPixels=int(max_pixels)
1084
- )
1085
  val = rr.get(band)
1086
- if val is None:
1087
- cache[cache_key] = default
1088
- return default
1089
- v = val.getInfo()
1090
- if v is None:
1091
- cache[cache_key] = default
1092
- return default
1093
- got = float(v)
1094
- cache[cache_key] = got
1095
- return got
1096
- except Exception as e:
1097
- # log to session for debugging if desired
1098
- st.session_state.setdefault("_ee_errors", []).append(str(e))
1099
- cache[cache_key] = default
1100
  return default
1101
 
1102
  def safe_reduce_histogram(region, image, band, scale=1000, max_pixels=int(1e7)):
1103
- """Return a frequency histogram dict or {}."""
1104
- cache_key = f"hist::{region.toGeoJSONString()[:200]}::{str(image)}::{band}::{scale}"
1105
- cache = st.session_state.setdefault("_ee_cache", {})
1106
- if cache_key in cache:
1107
- return cache[cache_key]
1108
  try:
1109
- rr = image.reduceRegion(
1110
- reducer=ee.Reducer.frequencyHistogram(),
1111
- geometry=region,
1112
- scale=int(scale),
1113
- maxPixels=int(max_pixels)
1114
- )
1115
- val = rr.get(band)
1116
- if val is None:
1117
- cache[cache_key] = {}
1118
- return {}
1119
- hist = val.getInfo()
1120
- if hist is None:
1121
- cache[cache_key] = {}
1122
- return {}
1123
- cache[cache_key] = hist
1124
- return hist
1125
- except Exception as e:
1126
- st.session_state.setdefault("_ee_errors", []).append(str(e))
1127
- cache[cache_key] = {}
1128
  return {}
1129
 
1130
  def safe_time_series(region, collection, band, start, end, reducer=ee.Reducer.mean(), scale=1000, max_pixels=int(1e7)):
1131
- """Return simple timeseries list of (date, value) for a collection."""
1132
  try:
1133
- # reduce each image over region, map to list
1134
  def per_image(img):
1135
  date = img.date().format("YYYY-MM-dd")
1136
- val = img.reduceRegion(reducer=reducer, geometry=region, scale=int(scale), maxPixels=int(max_pixels)).get(band)
1137
  return ee.Feature(None, {"date": date, "val": val})
1138
-
1139
  feats = collection.filterDate(start, end).map(per_image).filter(ee.Filter.notNull(["val"])).getInfo()
1140
- # feats is a dict with 'features' list
1141
- points = []
1142
  for f in feats.get("features", []):
1143
- props = f.get("properties", {})
1144
- date = props.get("date")
1145
- val = props.get("val")
1146
- if val is not None:
1147
- try:
1148
- points.append((date, float(val)))
1149
- except Exception:
1150
- pass
1151
- return points
1152
- except Exception as e:
1153
- st.session_state.setdefault("_ee_errors", []).append(str(e))
1154
  return []
1155
 
1156
  # ----------------------------
1157
  # Map setup
1158
  # ----------------------------
1159
- m = geemap.Map(center=[28.0, 72.0], zoom=5, draw_export=True)
1160
 
1161
- # enriched basemaps list (many options; geemap will ignore unknown)
1162
  basemaps = [
1163
  "HYBRID", "ROADMAP", "TERRAIN", "SATELLITE",
1164
  "Esri.WorldImagery", "Esri.WorldTopoMap", "Esri.WorldShadedRelief",
@@ -1174,391 +1121,238 @@ def locator_page():
1174
  pass
1175
 
1176
  # ----------------------------
1177
- # Datasets (choose stable EE catalog IDs)
1178
- # Terrain (DEM) -> prefer NASADEM then SRTM fallback
1179
  # ----------------------------
1180
- # Try NASADEM (higher quality), fallback to SRTM:
 
1181
  try:
1182
- dem = ee.Image("NASA/NASADEM_HGT/001") # NASADEM if available
1183
  dem_band_name = "elevation"
 
1184
  except Exception:
1185
- dem = ee.Image("USGS/SRTMGL1_003")
1186
- dem_band_name = "elevation"
1187
-
1188
- # ----------------------------
1189
- # Soil -> use OpenLandMap clay fraction (v02) which provides multiple bands like b0,b10,b30,... and is available
1190
- # (fallback to SoilGrids if OpenLandMap missing)
1191
- # ----------------------------
 
 
 
1192
  soil_img = None
1193
- soil_band = "b200" # default deep band
 
1194
  try:
1195
  soil_img = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-3A1A1A_M/v02")
1196
- # pick default band b200 (100-200cm). Offer depth selection below in UI.
 
 
 
 
 
 
 
1197
  except Exception:
1198
- # fallback to SoilGrids if available
1199
  try:
1200
  soil_img = ee.Image("projects/soilgrids-isric/clay_mean")
1201
- # SoilGrids band names like 'clay_0-5cm_mean' etc — we will pick a default after UI selection.
1202
- soil_band = "clay_0-5cm_mean"
 
 
 
 
 
1203
  except Exception:
1204
  soil_img = None
1205
-
1206
- # ----------------------------
1207
- # Seismic -> attempt SEDAC GSHAP, then GEM
1208
- # ----------------------------
1209
- seismic_img = None
1210
  try:
1211
- seismic_img = ee.Image("SEDAC/GSHAPSeismicHazard") # band "gshap"
1212
  seismic_band = "gshap"
 
1213
  except Exception:
1214
- try:
1215
- seismic_img = ee.Image("GEM/2015/GlobalSeismicHazard") # may or may not exist
1216
- # many GEM products use band b0 etc.
1217
- seismic_band = "b0"
1218
- except Exception:
1219
- seismic_img = None
1220
- seismic_band = None
1221
-
1222
- # ----------------------------
1223
- # Flood -> JRC Global Surface Water
1224
- # ----------------------------
1225
  try:
1226
  water = ee.Image("JRC/GSW1_4/GlobalSurfaceWater")
1227
  water_band = "occurrence"
 
1228
  except Exception:
1229
  water = None
1230
  water_band = None
1231
-
1232
- # ----------------------------
1233
- # Landcover -> ESA WorldCover v200 (10 m) - "Map" band
1234
- # ----------------------------
1235
  try:
1236
  landcover = ee.Image("ESA/WorldCover/v200")
1237
  lc_band = "Map"
 
1238
  except Exception:
1239
  landcover = None
1240
  lc_band = None
1241
-
1242
- # ----------------------------
1243
- # NDVI collection (MODIS 1km as robust global product)
1244
- # ----------------------------
1245
  try:
1246
  ndvi_col = ee.ImageCollection("MODIS/061/MOD13A2").select("NDVI")
 
1247
  except Exception:
1248
  ndvi_col = None
1249
-
 
1250
  # ----------------------------
1251
- # Add layers to map (visuals)
1252
  # ----------------------------
1253
- # Add DEM visualization
1254
- try:
1255
- m.addLayer(dem, {"min": 0, "max": 4000, "palette": ["blue", "green", "brown", "white"]}, "DEM / Topography")
1256
- except Exception:
1257
- pass
1258
-
1259
- # Add soil view (if available) — user will pick depth later
1260
- if soil_img:
1261
  try:
1262
- # show the dataset (choose one band to display; if band missing, geemap will raise — catch it)
1263
- # prefer b0 or b200 if available; if both missing, show first band
1264
- available_bands = soil_img.bandNames().getInfo()
1265
- # find a band to display
1266
- display_band = soil_band if soil_band in available_bands else available_bands[0]
1267
- m.addLayer(soil_img.select(display_band), {"min": 0.0, "max": 0.6, "palette": ["#ffffcc","#c2e699","#78c679","#31a354"]}, f"Soil Clay ({display_band})")
1268
  except Exception:
1269
  pass
1270
-
1271
- # Seismic
 
 
 
 
 
 
 
 
 
1272
  if seismic_img:
1273
  try:
1274
- # for SEDAC band "gshap" map 0-1
1275
- m.addLayer(seismic_img, {"min": 0, "max": 1, "palette": ["white", "yellow", "red"]}, "Seismic Hazard")
 
 
 
1276
  except Exception:
1277
  pass
1278
-
1279
- # Flood
1280
- if water is not None:
1281
  try:
1282
- m.addLayer(water.select(water_band), {"min":0,"max":100,"palette":["white","blue"]}, "Water Occurrence (JRC)")
 
 
 
 
1283
  except Exception:
1284
  pass
1285
-
1286
- # Landcover
1287
- if landcover is not None:
1288
  try:
1289
- m.addLayer(landcover, {"min":10,"max":100,"palette":["#006400","#ffbb22","#ffff4c","#f096ff","#fa0000","#b4b4b4","#f0f0f0","#0064c8","#0096a0","#00cf75"]}, "Landcover (WorldCover)")
 
 
 
 
 
 
 
 
 
 
 
1290
  except Exception:
1291
  pass
1292
-
1293
- # Add country boundaries & graticule
1294
  try:
1295
  countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
1296
- m.addLayer(countries.style(**{"color": "black", "fillColor": "00000000", "width": 1}), {}, "Country Boundaries")
1297
- except Exception:
1298
- pass
1299
- try:
1300
- m.addLayer(geemap.latlon_grid(5.0, region=ee.Geometry.Rectangle([-180, -90, 180, 90])).style(**{"color":"gray","width":0.5}), {}, "Lat/Lon Grid")
1301
  except Exception:
1302
  pass
 
 
 
 
 
 
1303
 
1304
- # --------------------------------------------------------------------------
1305
- # Section: Display Map, Handle Drawings, and Trigger Computation
1306
- # --------------------------------------------------------------------------
1307
- st.markdown("👉 Draw a polygon/rectangle/circle on the map (use draw tool). After drawing, click **Compute Summaries**.")
1308
-
1309
- # create the map (basemaps already defined earlier)
1310
- m = geemap.Map(plugin_Draw=True, draw_export=True, locate_control=True)
1311
-
1312
- # --- Lat/Lon grid ---
1313
- try:
1314
- grid = geemap.latlon_grid(lat_step=1, lon_step=1, west=-180, east=180, south=-85, north=85)
1315
- m.addLayer(grid, {"color": "gray"}, "Lat/Lon Grid")
1316
- except Exception as e:
1317
- import folium
1318
- for lat in range(-90, 91, 5):
1319
- folium.PolyLine([[lat, -180], [lat, 180]], weight=0.5, opacity=0.6).add_to(m)
1320
- for lon in range(-180, 181, 5):
1321
- folium.PolyLine([[-90, lon], [90, lon]], weight=0.5, opacity=0.6).add_to(m)
1322
- st.info(f"Lat/Lon grid fallback used (error: {e})")
1323
-
1324
- # --- Render the map ---
1325
- m.to_streamlit(height=700, responsive=True)
1326
-
1327
- # --- Capture draw events ---
1328
- if m.user_roi:
1329
- try:
1330
- st.session_state["roi_geojson"] = m.user_roi.toGeoJSONString()
1331
- except Exception:
1332
- pass
1333
- elif m.draw_features:
1334
- st.session_state["roi_geojson"] = json.dumps(m.draw_features[-1])
1335
-
1336
- # --- Helper: parse stored ROI into ee.Geometry ---
1337
  def get_roi_from_map():
1338
- if "roi_geojson" in st.session_state and st.session_state["roi_geojson"]:
 
 
1339
  try:
1340
- gj = st.session_state["roi_geojson"]
1341
- if isinstance(gj, str):
1342
- gj = json.loads(gj)
1343
- geom = gj.get("geometry") if isinstance(gj, dict) else gj
1344
- return ee.Geometry(geom)
1345
- except Exception as e:
1346
- st.warning(f"Could not parse the stored ROI. Please redraw. Error: {e}")
1347
- st.session_state.pop("roi_geojson", None)
1348
  return None
1349
  return None
1350
 
1351
- # --- Compute summaries ---
1352
  if st.button("Compute Summaries"):
1353
  roi = get_roi_from_map()
1354
  if roi is None:
1355
- st.error("⚠️ No drawn ROI found. Please draw a polygon/rectangle/circle and press 'Compute Summaries' again.")
1356
- else:
1357
- st.success("✅ Polygon found — computing (this may take a few seconds)...")
1358
- st.session_state["roi_final"] = roi # persist globally
1359
-
1360
- # --- Soil band selection ---
1361
- chosen_soil_band = None
1362
- if soil_img is not None:
1363
- try:
1364
- bands = soil_img.bandNames().getInfo()
1365
- depth_choice = st.selectbox(
1366
- "Soil depth / band to analyze",
1367
- options=bands,
1368
- index=bands.index(soil_band) if soil_band in bands else 0
1369
- )
1370
- chosen_soil_band = depth_choice
1371
- except Exception:
1372
- chosen_soil_band = None
1373
-
1374
- # --- Run computations ---
1375
- soil_val = safe_get_reduce(roi, soil_img.select(chosen_soil_band), chosen_soil_band, scale=1000, default=None, max_pixels=int(5e7)) if soil_img and chosen_soil_band else None
1376
- elev_val = safe_get_reduce(roi, dem, dem_band_name if dem_band_name else "elevation", scale=1000, default=None, max_pixels=int(5e7))
1377
- seismic_val = safe_get_reduce(roi, seismic_img, seismic_band, scale=5000, default=None, max_pixels=int(5e7)) if seismic_img and seismic_band else None
1378
- flood_val = safe_get_reduce(roi, water.select(water_band), water_band, scale=30, default=None, max_pixels=int(5e7)) if water and water_band else None
1379
- lc_stats = safe_reduce_histogram(roi, landcover, lc_band, scale=30, max_pixels=int(5e7)) if landcover and lc_band else {}
1380
- ndvi_ts = []
1381
- if ndvi_col is not None:
1382
- end = datetime.utcnow().strftime("%Y-%m-%d")
1383
- start = (datetime.utcnow().replace(year=datetime.utcnow().year - 2)).strftime("%Y-%m-%d")
1384
- ndvi_ts = safe_time_series(roi, ndvi_col, "NDVI", start, end, reducer=ee.Reducer.mean(), scale=1000, max_pixels=int(5e7))
1385
-
1386
- # --- Save results into active site ---
1387
- active_site = st.session_state.get("active_site", 0)
1388
- site = st.session_state["sites"][active_site]
1389
- site["ROI"] = st.session_state.get("roi_geojson")
1390
- site["Soil Clay"] = f"{soil_val} (band {chosen_soil_band})" if soil_val is not None else "Not available"
1391
- site["Elevation (avg)"] = elev_val
1392
- site["Seismic Hazard"] = seismic_val
1393
- site["Flood Occurrence"] = flood_val
1394
- site["Landcover Histogram"] = lc_stats
1395
- site["NDVI Timeseries"] = ndvi_ts
1396
-
1397
- # --- Display results ---
1398
- st.subheader("📊 Regional Data Summary")
1399
- st.write(f"**Soil ({chosen_soil_band}):** {soil_val}")
1400
- st.write(f"**Average Elevation:** {elev_val} m")
1401
- st.write(f"**Seismic Hazard (mean):** {seismic_val}")
1402
- st.write(f"**Flood occurrence (mean %):** {flood_val}")
1403
- st.json(lc_stats)
1404
-
1405
- if ndvi_ts:
1406
- import matplotlib.pyplot as plt
1407
- dates, values = zip(*ndvi_ts)
1408
- fig, ax = plt.subplots()
1409
- ax.plot(dates, values, marker="o")
1410
- ax.set_title("NDVI (last 2 years)")
1411
- ax.set_xlabel("Date")
1412
- ax.set_ylabel("NDVI")
1413
- st.pyplot(fig)
1414
-
1415
- # ----------------------------
1416
- # UI: display numeric summary
1417
- # ----------------------------
1418
- def pretty(x, fmt="{:.2f}"):
1419
- return "N/A" if x is None else fmt.format(x)
1420
-
1421
- st.subheader("📊 Regional Data Summary")
1422
- st.write(f"**Soil ({chosen_soil_band}):** {pretty(soil_val)}")
1423
- st.write(f"**Average Elevation:** {pretty(elev_val, '{:.1f}')} m")
1424
- st.write(f"**Seismic (mean):** {pretty(seismic_val)}")
1425
- st.write(f"**Flood occurrence (mean %):** {pretty(flood_val)}")
1426
-
1427
- # Landcover pie chart (colored)
1428
- if lc_stats:
1429
- # convert keys into ints if possible (WorldCover class codes)
1430
- labels = []
1431
- values = []
1432
- for k, v in lc_stats.items():
1433
- labels.append(str(k))
1434
- values.append(v)
1435
- fig1, ax1 = plt.subplots(figsize=(6,4))
1436
- ax1.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
1437
- ax1.set_title("Landcover Distribution (class codes)")
1438
- st.pyplot(fig1)
1439
- else:
1440
- st.info("No landcover histogram available.")
1441
-
1442
- # NDVI timeseries plot
1443
  if ndvi_ts:
1444
- dates = [d for d, v in ndvi_ts]
1445
- vals = [v for d, v in ndvi_ts]
1446
- fig2, ax2 = plt.subplots(figsize=(8,3))
1447
- ax2.plot(dates, vals, marker="o")
1448
- ax2.set_title("NDVI (mean) — last 2 years")
1449
- ax2.set_xlabel("Date")
1450
- ax2.set_ylabel("NDVI (scaled)")
1451
- plt.xticks(rotation=45)
1452
- st.pyplot(fig2)
1453
- else:
1454
- st.info("NDVI timeseries not available or too sparse.")
1455
-
1456
- # Soil histogram (if available)
1457
- soil_hist = None
1458
- try:
1459
- soil_hist = soil_img.reduceRegion(
1460
- reducer=ee.Reducer.histogram(maxBuckets=20),
1461
- geometry=roi,
1462
- scale=1000,
1463
- maxPixels=int(5e7)
1464
- ).get(chosen_soil_band).getInfo() if (soil_img is not None and chosen_soil_band) else None
1465
- except Exception:
1466
- soil_hist = None
1467
-
1468
- if soil_hist and isinstance(soil_hist, dict) and "bucketMeans" in soil_hist:
1469
- fig3, ax3 = plt.subplots(figsize=(6,4))
1470
- 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")
1471
- ax3.set_title(f"Soil histogram ({chosen_soil_band})")
1472
- ax3.set_xlabel("Clay fraction (kg/kg)")
1473
- ax3.set_ylabel("Pixel count")
1474
- st.pyplot(fig3)
1475
-
1476
- # ----------------------------
1477
- # Save results to session_state for reports
1478
- # ----------------------------
1479
- # Ensure sites and active site exist
1480
- if "sites" not in st.session_state or "active_site" not in st.session_state:
1481
- # just store soil_json if site structure not present
1482
- st.session_state["soil_json"] = {
1483
- "Soil": None if soil_val is None else float(soil_val),
1484
- "Soil Band": chosen_soil_band,
1485
- "Elevation": None if elev_val is None else float(elev_val),
1486
- "Seismic": None if seismic_val is None else float(seismic_val),
1487
- "Flood": None if flood_val is None else float(flood_val),
1488
- "Landcover Stats": lc_stats or {},
1489
- "NDVI TS": ndvi_ts or []
1490
- }
1491
- st.success("Saved results to st.session_state['soil_json']. (No active site present.)")
1492
- else:
1493
- # Save into active site JSON fields (keeping your field names unchanged)
1494
- active = st.session_state["active_site"]
1495
- try:
1496
- site_obj = st.session_state["sites"][active]
1497
- except Exception:
1498
- # if your sites is a list of dicts with Site ID matching idx, try to match
1499
- try:
1500
- site_obj = st.session_state["sites"][int(active)]
1501
- except Exception:
1502
- site_obj = None
1503
-
1504
- # fallback: if site_obj is None just write soil_json and exit
1505
- if site_obj is None:
1506
- st.session_state["soil_json"] = {
1507
- "Soil": None if soil_val is None else float(soil_val),
1508
- "Soil Band": chosen_soil_band,
1509
- "Elevation": None if elev_val is None else float(elev_val),
1510
- "Seismic": None if seismic_val is None else float(seismic_val),
1511
- "Flood": None if flood_val is None else float(flood_val),
1512
- "Landcover Stats": lc_stats or {},
1513
- "NDVI TS": ndvi_ts or []
1514
- }
1515
- st.success("Saved results to st.session_state['soil_json']. (Could not find active site object.)")
1516
- else:
1517
- # update the exact fields you requested (names unchanged)
1518
- site_obj["Soil Profile"] = f"{round(soil_val,3)} ({chosen_soil_band})" if soil_val is not None else "No data"
1519
- site_obj["Topo Data"] = f"{round(elev_val,2)} m (mean)" if elev_val is not None else "No data"
1520
- site_obj["Seismic Data"] = f"{round(seismic_val,4)}" if seismic_val is not None else "No data"
1521
- site_obj["Flood Data"] = f"{round(flood_val,2)} %" if flood_val is not None else "No data"
1522
- # Environmental Data: combine landcover summary + NDVI basic stats
1523
- env_summary = {
1524
- "Landcover Histogram": lc_stats or {},
1525
- "NDVI_timeseries_points": ndvi_ts or []
1526
- }
1527
- site_obj["Environmental Data"] = env_summary
1528
-
1529
- # Save drawn polygon GeoJSON for future map restore and report inclusion
1530
- try:
1531
- # fetch GeoJSON from ROI
1532
- geojson = roi.toGeoJSON() if hasattr(roi, "toGeoJSON") else ee.Geometry(roi).getInfo()
1533
- site_obj["drawn_polygon"] = geojson
1534
- except Exception:
1535
- site_obj["drawn_polygon"] = None
1536
-
1537
- # Save to soil_json as well (for report block that expects it)
1538
- st.session_state["soil_json"] = {
1539
- "Soil": None if soil_val is None else float(soil_val),
1540
- "Soil Band": chosen_soil_band,
1541
- "Elevation": None if elev_val is None else float(elev_val),
1542
- "Seismic": None if seismic_val is None else float(seismic_val),
1543
- "Flood": None if flood_val is None else float(flood_val),
1544
- "Landcover Stats": lc_stats or {},
1545
- "NDVI TS": ndvi_ts or []
1546
- }
1547
- st.success("📑 Results saved to active site and st.session_state['soil_json'] for report integration.")
1548
-
1549
- # Snapshot map as HTML and save path into site object (map snapshot - small HTML content)
1550
- try:
1551
- snap_html = m.to_html(None) # returns HTML string if path None
1552
- # store minimal snapshot content into site or soil_json (may be large; consider storing link)
1553
- if "sites" in st.session_state and site_obj is not None:
1554
- site_obj["map_snapshot"] = snap_html # caution: large string
1555
- st.session_state["last_map_snapshot"] = snap_html
1556
- except Exception:
1557
- pass
1558
-
1559
- # end of Compute Summaries block
1560
-
1561
- # end locator_page()
1562
 
1563
  # GeoMate Ask (RAG) — simple chat with memory per site and auto-extract numeric values
1564
  import re, json, pickle
 
985
  else:
986
  st.warning("OCR not available in this deployment.")
987
  # Locator Page (with Earth Engine auth at top)
988
+ # Locator Page (with Earth Engine auth at top)
989
 
990
  import os
991
  import json
 
996
  from datetime import datetime
997
  from io import BytesIO
998
  import base64
999
+ import folium
1000
+
1001
+ import tempfile
1002
+
1003
+ def export_map_snapshot(m, width=800, height=600):
1004
+ """
1005
+ Export geemap Map object to PNG snapshot (returns bytes).
1006
+ - m: geemap.Map
1007
+ - width, height: dimensions of snapshot
1008
+ """
1009
+ try:
1010
+ # geemap has a built-in screenshot method
1011
+ tmpfile = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
1012
+ m.screenshot(filename=tmpfile.name, region=None, dimensions=(width, height))
1013
+ with open(tmpfile.name, "rb") as f:
1014
+ return f.read()
1015
+ except Exception as e:
1016
+ st.warning(f"Map snapshot failed: {e}")
1017
+ return None
1018
+
1019
 
1020
  def locator_page():
1021
  """
1022
  Robust locator page:
1023
+ - Uses initialize_ee() auth routine (expects EARTHENGINE_TOKEN / SERVICE_ACCOUNT in env)
1024
+ - Shows interactive map with basemaps and overlays
1025
+ - Captures ROI drawn on the map
1026
+ - Computes summaries and saves them into active site and soil_json
1027
  """
1028
 
1029
  st.title("🌍 GeoMate Interactive Earth Explorer")
1030
  st.markdown(
1031
  "Draw a polygon (or rectangle) on the map using the drawing tool. "
1032
+ "Then press **Compute Summaries** to compute soil clay, elevation, seismic, flood occurrence, landcover, and NDVI."
 
1033
  )
1034
 
1035
  # ----------------------------
1036
+ # EE Auth
1037
+ # ----------------------------
1038
  EARTHENGINE_TOKEN = os.getenv("EARTHENGINE_TOKEN")
1039
+ SERVICE_ACCOUNT = os.getenv("SERVICE_ACCOUNT")
1040
 
1041
  def initialize_ee():
 
1042
  if "ee_initialized" in st.session_state and st.session_state["ee_initialized"]:
1043
  return True
 
1044
  if EARTHENGINE_TOKEN and SERVICE_ACCOUNT:
1045
  try:
1046
+ creds = ee.ServiceAccountCredentials(email=SERVICE_ACCOUNT, key_data=EARTHENGINE_TOKEN)
 
 
 
1047
  ee.Initialize(creds)
1048
  st.session_state["ee_initialized"] = True
1049
  return True
1050
  except Exception as e:
1051
+ st.warning(f"Service account init failed: {e}, falling back...")
 
1052
  try:
1053
  ee.Initialize()
1054
  st.session_state["ee_initialized"] = True
 
1060
  st.session_state["ee_initialized"] = True
1061
  return True
1062
  except Exception as e:
1063
+ st.error(f"Earth Engine auth failed: {e}")
1064
  return False
1065
 
1066
  if not initialize_ee():
1067
  st.stop()
1068
 
1069
  # ----------------------------
1070
+ # Safe reducers
 
 
 
 
 
 
 
 
1071
  # ----------------------------
 
 
 
 
1072
  def safe_get_reduce(region, image, band, scale=1000, default=None, max_pixels=int(1e7)):
 
 
 
 
 
 
 
1073
  try:
1074
+ rr = image.reduceRegion(ee.Reducer.mean(), region, scale=scale, maxPixels=max_pixels)
 
 
 
 
 
1075
  val = rr.get(band)
1076
+ return float(val.getInfo()) if val else default
1077
+ except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
1078
  return default
1079
 
1080
  def safe_reduce_histogram(region, image, band, scale=1000, max_pixels=int(1e7)):
 
 
 
 
 
1081
  try:
1082
+ rr = image.reduceRegion(ee.Reducer.frequencyHistogram(), region, scale=scale, maxPixels=max_pixels)
1083
+ hist = rr.get(band)
1084
+ return hist.getInfo() if hist else {}
1085
+ except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  return {}
1087
 
1088
  def safe_time_series(region, collection, band, start, end, reducer=ee.Reducer.mean(), scale=1000, max_pixels=int(1e7)):
 
1089
  try:
 
1090
  def per_image(img):
1091
  date = img.date().format("YYYY-MM-dd")
1092
+ val = img.reduceRegion(reducer, region, scale=scale, maxPixels=max_pixels).get(band)
1093
  return ee.Feature(None, {"date": date, "val": val})
 
1094
  feats = collection.filterDate(start, end).map(per_image).filter(ee.Filter.notNull(["val"])).getInfo()
1095
+ pts = []
 
1096
  for f in feats.get("features", []):
1097
+ p = f.get("properties", {})
1098
+ if p.get("val") is not None:
1099
+ pts.append((p.get("date"), float(p.get("val"))))
1100
+ return pts
1101
+ except Exception:
 
 
 
 
 
 
1102
  return []
1103
 
1104
  # ----------------------------
1105
  # Map setup
1106
  # ----------------------------
1107
+ m = geemap.Map(center=[28.0, 72.0], zoom=5, plugin_Draw=True, draw_export=True, locate_control=True)
1108
 
 
1109
  basemaps = [
1110
  "HYBRID", "ROADMAP", "TERRAIN", "SATELLITE",
1111
  "Esri.WorldImagery", "Esri.WorldTopoMap", "Esri.WorldShadedRelief",
 
1121
  pass
1122
 
1123
  # ----------------------------
1124
+ # Datasets (DEM, Soil, Seismic, Flood, Landcover, NDVI)
 
1125
  # ----------------------------
1126
+
1127
+ # --- DEM ---
1128
  try:
1129
+ dem = ee.Image("NASA/NASADEM_HGT/001") # NASADEM ~30m global
1130
  dem_band_name = "elevation"
1131
+ st.info("Using NASADEM dataset for elevation (30m).")
1132
  except Exception:
1133
+ try:
1134
+ dem = ee.Image("USGS/SRTMGL1_003") # SRTM ~30m fallback
1135
+ dem_band_name = "elevation"
1136
+ st.warning("Fallback to SRTM DEM (30m).")
1137
+ except Exception:
1138
+ dem = None
1139
+ dem_band_name = None
1140
+ st.error("No DEM dataset available.")
1141
+
1142
+ # --- Soil Clay Fraction ---
1143
  soil_img = None
1144
+ soil_band = None
1145
+ chosen_soil_band = None
1146
  try:
1147
  soil_img = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-3A1A1A_M/v02")
1148
+ bands = soil_img.bandNames().getInfo()
1149
+ # interactive soil band selector
1150
+ chosen_soil_band = st.selectbox(
1151
+ "Select soil depth / clay band",
1152
+ options=bands,
1153
+ index=bands.index("b200") if "b200" in bands else 0
1154
+ )
1155
+ st.info(f"Using OpenLandMap Clay Fraction — Band: {chosen_soil_band}")
1156
  except Exception:
 
1157
  try:
1158
  soil_img = ee.Image("projects/soilgrids-isric/clay_mean")
1159
+ bands = soil_img.bandNames().getInfo()
1160
+ chosen_soil_band = st.selectbox(
1161
+ "Select soil depth (SoilGrids)",
1162
+ options=bands,
1163
+ index=0
1164
+ )
1165
+ st.warning(f"Fallback to SoilGrids Clay dataset — Band: {chosen_soil_band}")
1166
  except Exception:
1167
  soil_img = None
1168
+ chosen_soil_band = None
1169
+ st.error("No soil dataset available.")
1170
+
1171
+ # --- Seismic Hazard ---
 
1172
  try:
1173
+ seismic_img = ee.Image("SEDAC/GSHAPSeismicHazard")
1174
  seismic_band = "gshap"
1175
+ st.info("Using SEDAC Global Seismic Hazard dataset.")
1176
  except Exception:
1177
+ seismic_img = None
1178
+ seismic_band = None
1179
+ st.error("No seismic hazard dataset available.")
1180
+
1181
+ # --- Flood Occurrence ---
 
 
 
 
 
 
1182
  try:
1183
  water = ee.Image("JRC/GSW1_4/GlobalSurfaceWater")
1184
  water_band = "occurrence"
1185
+ st.info("Using JRC Global Surface Water Occurrence (1984–2020).")
1186
  except Exception:
1187
  water = None
1188
  water_band = None
1189
+ st.error("No flood dataset available.")
1190
+
1191
+ # --- Landcover ---
 
1192
  try:
1193
  landcover = ee.Image("ESA/WorldCover/v200")
1194
  lc_band = "Map"
1195
+ st.info("Using ESA WorldCover v200 (2020, 10m).")
1196
  except Exception:
1197
  landcover = None
1198
  lc_band = None
1199
+ st.error("No landcover dataset available.")
1200
+
1201
+ # --- NDVI ---
 
1202
  try:
1203
  ndvi_col = ee.ImageCollection("MODIS/061/MOD13A2").select("NDVI")
1204
+ st.info("Using MODIS NDVI (16-day, 1km, global).")
1205
  except Exception:
1206
  ndvi_col = None
1207
+ st.error("No NDVI dataset available.")
1208
+
1209
  # ----------------------------
1210
+ # Add Layers to Map
1211
  # ----------------------------
1212
+ if dem:
 
 
 
 
 
 
 
1213
  try:
1214
+ m.addLayer(
1215
+ dem,
1216
+ {"min": 0, "max": 4000, "palette": ["blue", "green", "brown", "white"]},
1217
+ "DEM / Elevation"
1218
+ )
 
1219
  except Exception:
1220
  pass
1221
+
1222
+ if soil_img and chosen_soil_band:
1223
+ try:
1224
+ m.addLayer(
1225
+ soil_img.select(chosen_soil_band),
1226
+ {"min": 0.0, "max": 0.6, "palette": ["#ffffcc", "#c2e699", "#78c679", "#31a354"]},
1227
+ f"Soil Clay Fraction ({chosen_soil_band})"
1228
+ )
1229
+ except Exception:
1230
+ pass
1231
+
1232
  if seismic_img:
1233
  try:
1234
+ m.addLayer(
1235
+ seismic_img,
1236
+ {"min": 0, "max": 1, "palette": ["white", "yellow", "red"]},
1237
+ "Seismic Hazard"
1238
+ )
1239
  except Exception:
1240
  pass
1241
+
1242
+ if water:
 
1243
  try:
1244
+ m.addLayer(
1245
+ water.select(water_band),
1246
+ {"min": 0, "max": 100, "palette": ["white", "blue"]},
1247
+ "Flood Occurrence (%)"
1248
+ )
1249
  except Exception:
1250
  pass
1251
+
1252
+ if landcover:
 
1253
  try:
1254
+ m.addLayer(
1255
+ landcover,
1256
+ {
1257
+ "min": 10,
1258
+ "max": 100,
1259
+ "palette": [
1260
+ "#006400", "#ffbb22", "#ffff4c", "#f096ff", "#fa0000",
1261
+ "#b4b4b4", "#f0f0f0", "#0064c8", "#0096a0", "#00cf75"
1262
+ ]
1263
+ },
1264
+ "Landcover (ESA WorldCover)"
1265
+ )
1266
  except Exception:
1267
  pass
1268
+
 
1269
  try:
1270
  countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
1271
+ m.addLayer(
1272
+ countries.style(**{"color": "black", "fillColor": "00000000", "width": 1}),
1273
+ {},
1274
+ "Country Boundaries"
1275
+ )
1276
  except Exception:
1277
  pass
1278
+
1279
+ # ----------------------------
1280
+ # Show map + draw tool
1281
+ # ----------------------------
1282
+ st.markdown("👉 Draw a polygon/rectangle and press **Compute Summaries**")
1283
+ m.to_streamlit(height=650, responsive=True)
1284
 
1285
+ # --- Capture ROI ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1286
  def get_roi_from_map():
1287
+ if m.draw_features:
1288
+ feat = m.draw_features[-1]
1289
+ geom = feat.get("geometry") if "geometry" in feat else feat
1290
  try:
1291
+ return ee.Geometry(geom)
1292
+ except Exception:
 
 
 
 
 
 
1293
  return None
1294
  return None
1295
 
1296
+ # --- Compute Summaries ---
1297
  if st.button("Compute Summaries"):
1298
  roi = get_roi_from_map()
1299
  if roi is None:
1300
+ st.error("⚠️ No drawn ROI found. Please draw and try again.")
1301
+ return
1302
+ st.success("✅ ROI found — computing...")
1303
+
1304
+ chosen_soil_band = None
1305
+ if soil_img:
1306
+ bands = soil_img.bandNames().getInfo()
1307
+ chosen_soil_band = st.selectbox("Soil band to analyze", bands, index=bands.index(soil_band) if soil_band in bands else 0)
1308
+
1309
+ 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
1310
+ elev_val = safe_get_reduce(roi, dem, dem_band_name, 1000)
1311
+ seismic_val = safe_get_reduce(roi, seismic_img, seismic_band, 5000) if seismic_img else None
1312
+ flood_val = safe_get_reduce(roi, water.select(water_band), water_band, 30) if water else None
1313
+ lc_stats = safe_reduce_histogram(roi, landcover, lc_band, 30) if landcover else {}
1314
+ ndvi_ts = []
1315
+ if ndvi_col:
1316
+ end = datetime.utcnow().strftime("%Y-%m-%d")
1317
+ start = (datetime.utcnow().replace(year=datetime.utcnow().year-2)).strftime("%Y-%m-%d")
1318
+ ndvi_ts = safe_time_series(roi, ndvi_col, "NDVI", start, end)
1319
+
1320
+ # Save results
1321
+ active = st.session_state.get("active_site", 0)
1322
+ if "sites" in st.session_state:
1323
+ site = st.session_state["sites"][active]
1324
+ site["ROI"] = roi.getInfo()
1325
+ site["Soil Profile"] = f"{soil_val} ({chosen_soil_band})" if soil_val else "N/A"
1326
+ site["Topo Data"] = f"{elev_val} m" if elev_val else "N/A"
1327
+ site["Seismic Data"] = seismic_val
1328
+ site["Flood Data"] = flood_val
1329
+ site["Environmental Data"] = {"Landcover": lc_stats, "NDVI": ndvi_ts}
1330
+ st.session_state["soil_json"] = {
1331
+ "Soil": soil_val, "Soil Band": chosen_soil_band,
1332
+ "Elevation": elev_val, "Seismic": seismic_val,
1333
+ "Flood": flood_val, "Landcover Stats": lc_stats,
1334
+ "NDVI TS": ndvi_ts
1335
+ }
1336
+ # --- Save map snapshot ---
1337
+ map_bytes = export_map_snapshot(m)
1338
+ if map_bytes:
1339
+ st.session_state["last_map_snapshot"] = map_bytes
1340
+ if "sites" in st.session_state:
1341
+ st.session_state["sites"][active]["map_snapshot"] = map_bytes
1342
+ st.image(map_bytes, caption="Map Snapshot", use_column_width=True)
1343
+
1344
+ # Display
1345
+ st.subheader("📊 Summary")
1346
+ st.write(f"**Soil:** {soil_val}")
1347
+ st.write(f"**Elevation:** {elev_val}")
1348
+ st.write(f"**Seismic:** {seismic_val}")
1349
+ st.write(f"**Flood:** {flood_val}")
1350
+ st.json(lc_stats)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1351
  if ndvi_ts:
1352
+ d,v = zip(*ndvi_ts)
1353
+ fig, ax = plt.subplots()
1354
+ ax.plot(d, v, marker="o"); ax.set_title("NDVI"); ax.set_xlabel("Date")
1355
+ st.pyplot(fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1356
 
1357
  # GeoMate Ask (RAG) — simple chat with memory per site and auto-extract numeric values
1358
  import re, json, pickle