Update app.py
Browse files
app.py
CHANGED
|
@@ -1005,7 +1005,270 @@ def ocr_page():
|
|
| 1005 |
st.error(f"OCR failed: {e}")
|
| 1006 |
else:
|
| 1007 |
st.warning("OCR not available in this deployment.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1009 |
|
| 1010 |
# GeoMate Ask (RAG) β simple chat with memory per site and auto-extract numeric values
|
| 1011 |
def rag_page():
|
|
|
|
| 1005 |
st.error(f"OCR failed: {e}")
|
| 1006 |
else:
|
| 1007 |
st.warning("OCR not available in this deployment.")
|
| 1008 |
+
# Locator Page (with Earth Engine auth at top)
|
| 1009 |
+
import os
|
| 1010 |
+
import streamlit as st
|
| 1011 |
+
import geemap.foliumap as geemap
|
| 1012 |
+
import ee
|
| 1013 |
+
import matplotlib.pyplot as plt
|
| 1014 |
+
|
| 1015 |
+
# ----------------------------
|
| 1016 |
+
# ----------------------------
|
| 1017 |
+
def locator_page():
|
| 1018 |
+
# ----------------------------
|
| 1019 |
+
# Earth Engine Initialization (service account or interactive)
|
| 1020 |
+
# ----------------------------
|
| 1021 |
+
EARTHENGINE_TOKEN = os.getenv("EARTHENGINE_TOKEN")
|
| 1022 |
+
SERVICE_ACCOUNT = os.getenv("SERVICE_ACCOUNT") # optional: service account email
|
| 1023 |
+
|
| 1024 |
+
def initialize_ee():
|
| 1025 |
+
"""Initialize Earth Engine with multiple fallbacks."""
|
| 1026 |
+
if "ee_initialized" in st.session_state and st.session_state["ee_initialized"]:
|
| 1027 |
+
return True
|
| 1028 |
+
|
| 1029 |
+
if EARTHENGINE_TOKEN and SERVICE_ACCOUNT:
|
| 1030 |
+
try:
|
| 1031 |
+
creds = ee.ServiceAccountCredentials(
|
| 1032 |
+
email=SERVICE_ACCOUNT,
|
| 1033 |
+
key_data=EARTHENGINE_TOKEN
|
| 1034 |
+
)
|
| 1035 |
+
ee.Initialize(creds)
|
| 1036 |
+
st.session_state["ee_initialized"] = True
|
| 1037 |
+
return True
|
| 1038 |
+
except Exception as e:
|
| 1039 |
+
st.warning(f"Service account init failed: {e} β trying default/interactive auth...")
|
| 1040 |
+
|
| 1041 |
+
try:
|
| 1042 |
+
ee.Initialize()
|
| 1043 |
+
st.session_state["ee_initialized"] = True
|
| 1044 |
+
return True
|
| 1045 |
+
except Exception:
|
| 1046 |
+
try:
|
| 1047 |
+
ee.Authenticate()
|
| 1048 |
+
ee.Initialize()
|
| 1049 |
+
st.session_state["ee_initialized"] = True
|
| 1050 |
+
return True
|
| 1051 |
+
except Exception as e:
|
| 1052 |
+
st.error(f"Earth Engine authentication failed: {e}")
|
| 1053 |
+
return False
|
| 1054 |
+
|
| 1055 |
+
if not initialize_ee():
|
| 1056 |
+
st.stop()
|
| 1057 |
|
| 1058 |
+
# ----------------------------
|
| 1059 |
+
# Helper functions
|
| 1060 |
+
# ----------------------------
|
| 1061 |
+
def safe_get_reduce(region, image, band: str, scale: int, default=None, max_pixels=1e9):
|
| 1062 |
+
try:
|
| 1063 |
+
res = image.reduceRegion(
|
| 1064 |
+
reducer=ee.Reducer.mean(),
|
| 1065 |
+
geometry=region,
|
| 1066 |
+
scale=scale,
|
| 1067 |
+
maxPixels=int(max_pixels)
|
| 1068 |
+
)
|
| 1069 |
+
val = res.get(band)
|
| 1070 |
+
if val is None:
|
| 1071 |
+
return default
|
| 1072 |
+
return float(val.getInfo())
|
| 1073 |
+
except Exception:
|
| 1074 |
+
return default
|
| 1075 |
+
|
| 1076 |
+
def safe_reduce_histogram(region, image, band: str, scale: int, max_pixels=1e9):
|
| 1077 |
+
try:
|
| 1078 |
+
hist = image.reduceRegion(
|
| 1079 |
+
reducer=ee.Reducer.frequencyHistogram(),
|
| 1080 |
+
geometry=region,
|
| 1081 |
+
scale=scale,
|
| 1082 |
+
maxPixels=int(max_pixels)
|
| 1083 |
+
).get(band)
|
| 1084 |
+
if hist is None:
|
| 1085 |
+
return None
|
| 1086 |
+
return hist.getInfo()
|
| 1087 |
+
except Exception:
|
| 1088 |
+
return None
|
| 1089 |
+
|
| 1090 |
+
def get_roi_from_map(m: geemap.Map):
|
| 1091 |
+
"""Try multiple ways to extract the ROI from the drawn shapes."""
|
| 1092 |
+
candidates = [
|
| 1093 |
+
getattr(m, "user_roi", None),
|
| 1094 |
+
getattr(m, "last_drawn_geojson", None),
|
| 1095 |
+
getattr(m, "draw_last_feature", None),
|
| 1096 |
+
getattr(m, "draw_features", None),
|
| 1097 |
+
getattr(m, "user_drawn_features", None),
|
| 1098 |
+
getattr(m, "drawn_features", None),
|
| 1099 |
+
]
|
| 1100 |
+
for c in candidates:
|
| 1101 |
+
if c:
|
| 1102 |
+
if isinstance(c, ee.Geometry):
|
| 1103 |
+
return c
|
| 1104 |
+
try:
|
| 1105 |
+
if isinstance(c, dict):
|
| 1106 |
+
return ee.Geometry(c)
|
| 1107 |
+
if isinstance(c, list) and len(c) > 0 and isinstance(c[0], dict):
|
| 1108 |
+
return ee.Geometry(c[0])
|
| 1109 |
+
except Exception:
|
| 1110 |
+
pass
|
| 1111 |
+
try:
|
| 1112 |
+
s = getattr(m, "draw_last_feature", None)
|
| 1113 |
+
if s:
|
| 1114 |
+
import json
|
| 1115 |
+
if isinstance(s, str):
|
| 1116 |
+
return ee.Geometry(json.loads(s))
|
| 1117 |
+
except Exception:
|
| 1118 |
+
pass
|
| 1119 |
+
return None
|
| 1120 |
+
|
| 1121 |
+
# ----------------------------
|
| 1122 |
+
# Page title
|
| 1123 |
+
# ----------------------------
|
| 1124 |
+
st.title("π GeoMate Interactive Earth Explorer")
|
| 1125 |
+
st.markdown(
|
| 1126 |
+
"Draw a polygon or rectangle on the map (use the drawing tool), then the app will compute "
|
| 1127 |
+
"regional summaries (soil clay, elevation, seismic, flood occurrence, landcover distribution)."
|
| 1128 |
+
)
|
| 1129 |
+
|
| 1130 |
+
# ----------------------------
|
| 1131 |
+
# Map setup
|
| 1132 |
+
# ----------------------------
|
| 1133 |
+
m = geemap.Map(center=[28.0, 72.0], zoom=4, draw_export=True)
|
| 1134 |
+
|
| 1135 |
+
basemaps = [
|
| 1136 |
+
"HYBRID", "ROADMAP", "TERRAIN", "SATELLITE",
|
| 1137 |
+
"Esri.WorldImagery", "Esri.WorldTopoMap", "Esri.WorldShadedRelief",
|
| 1138 |
+
"Esri.NatGeoWorldMap", "Esri.OceanBasemap",
|
| 1139 |
+
"CartoDB.Positron", "CartoDB.DarkMatter",
|
| 1140 |
+
"Stamen.Terrain", "Stamen.Watercolor",
|
| 1141 |
+
"OpenStreetMap",
|
| 1142 |
+
]
|
| 1143 |
+
for b in basemaps:
|
| 1144 |
+
try:
|
| 1145 |
+
m.add_basemap(b)
|
| 1146 |
+
except Exception:
|
| 1147 |
+
pass
|
| 1148 |
+
|
| 1149 |
+
# ----------------------------
|
| 1150 |
+
# Datasets
|
| 1151 |
+
# ----------------------------
|
| 1152 |
+
soil = ee.Image("OpenLandMap/SOL/SOL_CLAY-WFRACTION_USDA-4B1C_M/v01").select("b200")
|
| 1153 |
+
soil_vis = {"min": 0, "max": 60, "palette": ["yellow", "brown", "red"]}
|
| 1154 |
+
m.addLayer(soil, soil_vis, "Soil Clay (200cm)")
|
| 1155 |
+
|
| 1156 |
+
dem = ee.Image("USGS/SRTMGL1_003")
|
| 1157 |
+
dem_vis = {"min": 0, "max": 4000, "palette": ["blue", "green", "brown", "white"]}
|
| 1158 |
+
m.addLayer(dem, dem_vis, "Topography (SRTM DEM)")
|
| 1159 |
+
|
| 1160 |
+
seismic = ee.Image("GEM/2015/GlobalSeismicHazard").select("b0")
|
| 1161 |
+
m.addLayer(seismic, {"min": 0, "max": 1, "palette": ["white", "red"]}, "Seismic Hazard")
|
| 1162 |
+
|
| 1163 |
+
water = ee.Image("JRC/GSW1_4/GlobalSurfaceWater").select("occurrence")
|
| 1164 |
+
flood_vis = {"min": 0, "max": 100, "palette": ["white", "blue"]}
|
| 1165 |
+
m.addLayer(water, flood_vis, "Flood Hazard")
|
| 1166 |
+
|
| 1167 |
+
landcover = ee.Image("ESA/WorldCover/v200/2021")
|
| 1168 |
+
landcover_vis = {
|
| 1169 |
+
"bands": ["Map"], "min": 10, "max": 100,
|
| 1170 |
+
"palette": [
|
| 1171 |
+
"006400", "ffbb22", "ffff4c", "f096ff", "fa0000",
|
| 1172 |
+
"b4b4b4", "f0f0f0", "0064c8", "0096a0", "00cf75"
|
| 1173 |
+
]
|
| 1174 |
+
}
|
| 1175 |
+
m.addLayer(landcover, landcover_vis, "Landcover 2021")
|
| 1176 |
+
|
| 1177 |
+
# Boundaries & grid
|
| 1178 |
+
countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
|
| 1179 |
+
m.addLayer(
|
| 1180 |
+
countries.style(**{"color": "black", "fillColor": "00000000", "width": 1}),
|
| 1181 |
+
{}, "Country Boundaries"
|
| 1182 |
+
)
|
| 1183 |
+
states = ee.FeatureCollection("FAO/GAUL_SIMPLIFIED_500m/2015/level1")
|
| 1184 |
+
m.addLayer(
|
| 1185 |
+
states.style(**{"color": "purple", "fillColor": "00000000", "width": 0.5}),
|
| 1186 |
+
{}, "State/Province Boundaries"
|
| 1187 |
+
)
|
| 1188 |
+
graticule = geemap.latlon_grid(5.0, region=ee.Geometry.Rectangle([-180, -90, 180, 90]))
|
| 1189 |
+
m.addLayer(graticule.style(**{"color": "gray", "width": 0.5}), {}, "Lat/Lon Grid")
|
| 1190 |
+
|
| 1191 |
+
# Drawing tools
|
| 1192 |
+
m.add_draw_control(polyline=False, circle=False, circlemarker=False, rectangle=True, polygon=True)
|
| 1193 |
+
m.to_streamlit(height=700, responsive=True)
|
| 1194 |
+
st.markdown("π Use the map drawing tool to select a polygon/rectangle.")
|
| 1195 |
+
|
| 1196 |
+
# ----------------------------
|
| 1197 |
+
# ROI extraction
|
| 1198 |
+
# ----------------------------
|
| 1199 |
+
roi = get_roi_from_map(m)
|
| 1200 |
+
if roi is None:
|
| 1201 |
+
st.warning("β οΈ No polygon selected yet.")
|
| 1202 |
+
return
|
| 1203 |
+
|
| 1204 |
+
try:
|
| 1205 |
+
if not isinstance(roi, ee.Geometry):
|
| 1206 |
+
roi = ee.Geometry(roi)
|
| 1207 |
+
except Exception:
|
| 1208 |
+
st.error("π Could not parse the drawn ROI into an Earth Engine geometry.")
|
| 1209 |
+
return
|
| 1210 |
+
|
| 1211 |
+
st.success("β
Polygon selected! Computing regional summaries...")
|
| 1212 |
+
|
| 1213 |
+
# ----------------------------
|
| 1214 |
+
# Compute stats
|
| 1215 |
+
# ----------------------------
|
| 1216 |
+
soil_val = safe_get_reduce(roi, soil, "b200", scale=1000, default=None)
|
| 1217 |
+
elev_val = safe_get_reduce(roi, dem, "elevation", scale=1000, default=None)
|
| 1218 |
+
seismic_val = safe_get_reduce(roi, seismic, "b0", scale=5000, default=None)
|
| 1219 |
+
flood_val = safe_get_reduce(roi, water, "occurrence", scale=30, default=None)
|
| 1220 |
+
lc_stats = safe_reduce_histogram(roi, landcover, "Map", scale=30)
|
| 1221 |
+
|
| 1222 |
+
def safe_display(x):
|
| 1223 |
+
return "N/A" if x is None else round(x, 2)
|
| 1224 |
+
|
| 1225 |
+
st.subheader("π Regional Data Summary")
|
| 1226 |
+
st.write(f"**Average Clay (200cm):** {safe_display(soil_val)} %")
|
| 1227 |
+
st.write(f"**Average Elevation:** {safe_display(elev_val)} m")
|
| 1228 |
+
st.write(f"**Seismic Hazard (PGA):** {safe_display(seismic_val)} g")
|
| 1229 |
+
st.write(f"**Flood Occurrence Probability:** {safe_display(flood_val)} %")
|
| 1230 |
+
|
| 1231 |
+
if lc_stats:
|
| 1232 |
+
labels = [str(k) for k in lc_stats.keys()]
|
| 1233 |
+
values = list(lc_stats.values())
|
| 1234 |
+
fig1, ax1 = plt.subplots(figsize=(6, 4))
|
| 1235 |
+
ax1.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
|
| 1236 |
+
ax1.set_title("Landcover Distribution")
|
| 1237 |
+
st.pyplot(fig1)
|
| 1238 |
+
|
| 1239 |
+
try:
|
| 1240 |
+
soil_hist = soil.reduceRegion(
|
| 1241 |
+
reducer=ee.Reducer.histogram(maxBuckets=10),
|
| 1242 |
+
geometry=roi,
|
| 1243 |
+
scale=1000,
|
| 1244 |
+
maxPixels=1e9
|
| 1245 |
+
).get("b200").getInfo()
|
| 1246 |
+
except Exception:
|
| 1247 |
+
soil_hist = None
|
| 1248 |
+
|
| 1249 |
+
if soil_hist and isinstance(soil_hist, dict) and "bucketMeans" in soil_hist:
|
| 1250 |
+
fig2, ax2 = plt.subplots(figsize=(6, 4))
|
| 1251 |
+
ax2.bar(soil_hist["bucketMeans"], soil_hist["histogram"],
|
| 1252 |
+
width=2, color="brown", alpha=0.7)
|
| 1253 |
+
ax2.set_xlabel("Clay fraction (%)")
|
| 1254 |
+
ax2.set_ylabel("Pixel count")
|
| 1255 |
+
ax2.set_title("Soil Clay Distribution")
|
| 1256 |
+
st.pyplot(fig2)
|
| 1257 |
+
|
| 1258 |
+
# ----------------------------
|
| 1259 |
+
# Save for reports
|
| 1260 |
+
# ----------------------------
|
| 1261 |
+
if "soil_json" not in st.session_state:
|
| 1262 |
+
st.session_state["soil_json"] = {}
|
| 1263 |
+
|
| 1264 |
+
st.session_state["soil_json"].update({
|
| 1265 |
+
"Soil": f"{safe_display(soil_val)} %",
|
| 1266 |
+
"Elevation": f"{safe_display(elev_val)} m",
|
| 1267 |
+
"Seismic Hazard": f"{safe_display(seismic_val)} g",
|
| 1268 |
+
"Flood Probability": f"{safe_display(flood_val)} %",
|
| 1269 |
+
"Landcover Stats": lc_stats if lc_stats else {}
|
| 1270 |
+
})
|
| 1271 |
+
st.success("π Data saved to `soil_json` for report integration!")
|
| 1272 |
|
| 1273 |
# GeoMate Ask (RAG) β simple chat with memory per site and auto-extract numeric values
|
| 1274 |
def rag_page():
|