Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 999 |
|
| 1000 |
def locator_page():
|
| 1001 |
"""
|
| 1002 |
Robust locator page:
|
| 1003 |
-
- Uses
|
| 1004 |
-
- Shows interactive map with
|
| 1005 |
-
-
|
| 1006 |
-
-
|
| 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 |
-
"
|
| 1013 |
-
"and save results for reports."
|
| 1014 |
)
|
| 1015 |
|
| 1016 |
# ----------------------------
|
| 1017 |
-
#
|
|
|
|
| 1018 |
EARTHENGINE_TOKEN = os.getenv("EARTHENGINE_TOKEN")
|
| 1019 |
-
SERVICE_ACCOUNT = os.getenv("SERVICE_ACCOUNT")
|
| 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}
|
| 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
|
| 1050 |
return False
|
| 1051 |
|
| 1052 |
if not initialize_ee():
|
| 1053 |
st.stop()
|
| 1054 |
|
| 1055 |
# ----------------------------
|
| 1056 |
-
#
|
| 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
|
| 1087 |
-
|
| 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 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 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
|
| 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 |
-
|
| 1141 |
-
points = []
|
| 1142 |
for f in feats.get("features", []):
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 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 (
|
| 1178 |
-
# Terrain (DEM) -> prefer NASADEM then SRTM fallback
|
| 1179 |
# ----------------------------
|
| 1180 |
-
|
|
|
|
| 1181 |
try:
|
| 1182 |
-
dem = ee.Image("NASA/NASADEM_HGT/001") # NASADEM
|
| 1183 |
dem_band_name = "elevation"
|
|
|
|
| 1184 |
except Exception:
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
|
|
|
|
|
|
|
|
|
| 1192 |
soil_img = None
|
| 1193 |
-
soil_band =
|
|
|
|
| 1194 |
try:
|
| 1195 |
soil_img = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-3A1A1A_M/v02")
|
| 1196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
except Exception:
|
| 1198 |
-
# fallback to SoilGrids if available
|
| 1199 |
try:
|
| 1200 |
soil_img = ee.Image("projects/soilgrids-isric/clay_mean")
|
| 1201 |
-
|
| 1202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
except Exception:
|
| 1204 |
soil_img = None
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
#
|
| 1209 |
-
seismic_img = None
|
| 1210 |
try:
|
| 1211 |
-
seismic_img = ee.Image("SEDAC/GSHAPSeismicHazard")
|
| 1212 |
seismic_band = "gshap"
|
|
|
|
| 1213 |
except Exception:
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 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
|
| 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
|
| 1244 |
-
# ----------------------------
|
| 1245 |
try:
|
| 1246 |
ndvi_col = ee.ImageCollection("MODIS/061/MOD13A2").select("NDVI")
|
|
|
|
| 1247 |
except Exception:
|
| 1248 |
ndvi_col = None
|
| 1249 |
-
|
|
|
|
| 1250 |
# ----------------------------
|
| 1251 |
-
# Add
|
| 1252 |
# ----------------------------
|
| 1253 |
-
|
| 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 |
-
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1272 |
if seismic_img:
|
| 1273 |
try:
|
| 1274 |
-
|
| 1275 |
-
|
|
|
|
|
|
|
|
|
|
| 1276 |
except Exception:
|
| 1277 |
pass
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
if water is not None:
|
| 1281 |
try:
|
| 1282 |
-
m.addLayer(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1283 |
except Exception:
|
| 1284 |
pass
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
if landcover is not None:
|
| 1288 |
try:
|
| 1289 |
-
m.addLayer(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
except Exception:
|
| 1291 |
pass
|
| 1292 |
-
|
| 1293 |
-
# Add country boundaries & graticule
|
| 1294 |
try:
|
| 1295 |
countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
|
| 1296 |
-
m.addLayer(
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
| 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
|
|
|
|
|
|
|
| 1339 |
try:
|
| 1340 |
-
|
| 1341 |
-
|
| 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
|
| 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
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
if
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
if
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 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 |
-
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
-
|
| 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
|