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