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