Spaces:
Sleeping
Sleeping
| import os | |
| import math | |
| import requests | |
| import pandas as pd | |
| import numpy as np | |
| import joblib | |
| import streamlit as st | |
| import folium | |
| import tempfile | |
| import time | |
| from math import sqrt | |
| from dotenv import load_dotenv | |
| from pyproj import Transformer | |
| from streamlit_folium import st_folium | |
| from streamlit.runtime.scriptrunner import RerunException | |
| from geopy.geocoders import Nominatim | |
| from geopy.distance import geodesic | |
| from geopy.exc import GeocoderTimedOut | |
| # ======= CONFIG ======= | |
| st.set_page_config(page_title="Flood Guardian", layout="wide") | |
| FAVORITES_FILE = "/tmp/favorites.json" | |
| DATA_URL = "https://huggingface.co/datasets/Nuttanitranslate/flood-dataset/resolve/main/Result_145.csv" | |
| MODEL_PATH = os.path.join(tempfile.gettempdir(), "velocity_model.joblib") | |
| MODEL_URL = "https://huggingface.co/spaces/Nuttanitranslate/Flood/resolve/main/src/velocity_model" | |
| # --- กำหนดค่าเริ่มต้นของพิกัด ถ้ายังไม่มีใน session_state --- | |
| if "lat" not in st.session_state or "lon" not in st.session_state: | |
| st.session_state["lat"] = 48.69 # ตัวอย่างค่า Default | |
| st.session_state["lon"] = 8.15 | |
| # --- ดึง lat/lon จาก session_state --- | |
| lat = st.session_state["lat"] | |
| lon = st.session_state["lon"] | |
| dataset = "aster30m" | |
| OPENTOPO_API = f"https://api.opentopodata.org/v1/{dataset}?locations={lat},{lon}" # ✅ ถูกต้อง | |
| # ====== Load API Key ====== | |
| load_dotenv() | |
| API_KEY = os.getenv("OPENWEATHER_API_KEY") | |
| OPENWEATHER_API_KEY = API_KEY | |
| if not API_KEY: | |
| st.error("❌ ไม่พบ API KEY สำหรับ OpenWeatherMap ใน .env") | |
| st.stop() | |
| # ตั้งค่า ENV สำหรับ Streamlit | |
| os.environ["STREAMLIT_HOME"] = os.getcwd() # ✅ ได้ | |
| os.environ["STREAMLIT_BROWSER_GATHERUSAGESTATS"] = "false" # ✅ ได้ | |
| geolocator = Nominatim(user_agent="flood-guardian") | |
| # ======= LOAD ASSETS ======= | |
| def load_css(url): | |
| try: | |
| r = requests.get(url, timeout=10) | |
| if r.status_code == 200: | |
| st.markdown(f"<style>{r.text}</style>", unsafe_allow_html=True) | |
| except: | |
| pass | |
| def load_ascii(url): | |
| try: | |
| r = requests.get(url, timeout=10) | |
| if r.status_code == 200: | |
| st.markdown(f"<pre>{r.text}</pre>", unsafe_allow_html=True) | |
| except: | |
| pass | |
| load_css("https://huggingface.co/spaces/Nuttanitranslate/Flood/resolve/main/src/lenny_style.css") | |
| load_ascii("https://huggingface.co/spaces/Nuttanitranslate/Flood/resolve/main/ascii_warning.txt") | |
| # ======= BANNER ======= | |
| st.markdown('<div class="lenny-banner">( ͡° ͜ʖ ͡°) つ──☆*:・.• Welcome to FloodX</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="hud-overlay">SENSOR: ONLINE | STATUS: STABLE</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="dialog-box">⚠️ Flood risk in your area. Stay alert!</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="weather-rain"></div>', unsafe_allow_html=True) | |
| # ======= LOAD DATA ======= | |
| MODEL_PATH = "src/velocity_model" | |
| try: | |
| model = joblib.load(MODEL_PATH) | |
| except Exception as e: | |
| st.error(f"❌ โหลดโมเดล velocity_model ไม่สำเร็จ: {e}") | |
| st.stop() | |
| def load_flood_data(): | |
| df = pd.read_csv( | |
| "https://huggingface.co/spaces/Nuttanitranslate/Flood/resolve/main/src/Result_145.csv", | |
| skiprows=2, | |
| ) | |
| transformer = Transformer.from_crs("epsg:32632", "epsg:4326", always_xy=True) | |
| df[["lon", "lat"]] = df.apply( | |
| lambda row: pd.Series(transformer.transform(row["X"], row["Y"])), axis=1 | |
| ) | |
| return df | |
| df = load_flood_data() | |
| required_columns = [ | |
| "Rain(mmh-1)", | |
| "Depth(Max)", | |
| "Depth", | |
| "Elevation", | |
| "WaterSurfaceElevation", | |
| ] | |
| if any(col not in df.columns for col in required_columns): | |
| st.error("❌ ข้อมูล CSV ขาดคอลัมน์ที่จำเป็น") | |
| st.stop() | |
| X_pred = df[required_columns] | |
| df["velocity_pred"] = model.predict(X_pred) | |
| velocity_levels = [ | |
| (0.0, "💚 เบา/ไม่มีกระแสน้ำ"), | |
| (0.5, "💧 กระแสน้ำเบามาก"), | |
| (1.0, "💧 กระแสน้ำเบา"), | |
| (1.5, "💧 กระแสน้ำค่อนข้างแรง"), | |
| (2.0, "🌊 กระแสน้ำแรง"), | |
| ] | |
| def add_favorite(lat, lon, place_name): | |
| favorite = { | |
| "place": place_name, | |
| "lat": lat, | |
| "lon": lon | |
| } | |
| if not os.path.exists(FAVORITES_FILE): | |
| with open(FAVORITES_FILE, "w") as f: | |
| json.dump([favorite], f, ensure_ascii=False, indent=2) | |
| else: | |
| with open(FAVORITES_FILE, "r") as f: | |
| favorites = json.load(f) | |
| favorites.append(favorite) | |
| with open(FAVORITES_FILE, "w") as f: | |
| json.dump(favorites, f, ensure_ascii=False, indent=2) | |
| def get_risk_from_velocity(velocity): | |
| for v, desc in reversed(velocity_levels): | |
| if velocity >= v: | |
| return desc | |
| return "ไม่สามารถระบุความเสี่ยงได้" | |
| def generate_ascii_velocity(velocity): | |
| min_v = 0.0 | |
| max_v = 8.0 | |
| step = 0.05 | |
| max_bar_length = 50 | |
| if velocity < step: | |
| return "Velocity: 0.00 m/s [💧 กระแสน้ำเบาบาง/ไม่มีกระแสน้ำ]\n[ ]" | |
| steps_count = int((max_v - min_v) / step) | |
| index = int((velocity - min_v) / step) | |
| index = min(index, steps_count) | |
| bar_length = int(index / steps_count * max_bar_length) | |
| bar = "█" * bar_length | |
| for v, d in reversed(velocity_levels): | |
| if velocity >= v: | |
| desc = d | |
| break | |
| return f"Velocity: {velocity:.2f} m/s [{desc}]\n[{bar}]" | |
| def geocode(name): | |
| try: | |
| location = geolocator.geocode(name, timeout=10) | |
| if location: | |
| return location.latitude, location.longitude | |
| except GeocoderTimedOut: | |
| st.warning("Geocoding timeout. Please try again.") | |
| return None, None | |
| def analyze_risk(name, weather_data, df, model, show_ascii=True): | |
| if name: | |
| lat, lon = geocode(name) | |
| if lat is None or lon is None: | |
| st.warning("ไม่พบสถานที่ที่คุณค้นหา") | |
| return None | |
| else: | |
| lat = weather_data["coord"]["lat"] | |
| lon = weather_data["coord"]["lon"] | |
| def get_weather(lat, lon, API_KEY): | |
| url = f"http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API_KEY}&units=metric" | |
| try: | |
| res = requests.get(url).json() | |
| return { | |
| "temp": res["main"]["temp"], | |
| "humidity": res["main"]["humidity"], | |
| "pressure": res["main"]["pressure"], | |
| "wind_speed": res["wind"]["speed"], | |
| "wind_deg": res["wind"].get("deg", 0), | |
| "rain": res.get("rain", {}).get("1h", 0), | |
| "desc": res["weather"][0]["description"], | |
| "clouds": res["clouds"]["all"], | |
| } | |
| except: | |
| return None | |
| # --- Haversine distance (km) --- | |
| def haversine(lat1, lon1, lat2, lon2): | |
| R = 6371 # Earth radius km | |
| phi1 = math.radians(lat1) | |
| phi2 = math.radians(lat2) | |
| delta_phi = math.radians(lat2 - lat1) | |
| delta_lambda = math.radians(lon2 - lon1) | |
| a = math.sin(delta_phi / 2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(delta_lambda / 2)**2 | |
| c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) | |
| return R * c | |
| def get_elevation_api(lat, lon, verbose=True): | |
| datasets = ["aster30m", "srtm90m", "etopo1"] | |
| elevations = [] | |
| for dataset in datasets: | |
| url = f"https://api.opentopodata.org/v1/{dataset}?locations={lat},{lon}" | |
| try: | |
| r = requests.get(url, timeout=10) | |
| if r.status_code == 200: | |
| data = r.json() | |
| elevation = data["results"][0].get("elevation") | |
| if elevation is not None: | |
| elevations.append(elevation) | |
| elif r.status_code == 429 and verbose: | |
| st.markdown(f"<div class='weather-box'>⏳ โดนจำกัดการใช้งาน ({dataset}): HTTP 429</div>", unsafe_allow_html=True) | |
| elif verbose: | |
| st.markdown(f"<div class='weather-box'>⚠️ API error ({dataset}): HTTP {r.status_code}</div>", unsafe_allow_html=True) | |
| except Exception as e: | |
| if verbose: | |
| st.markdown(f"<div class='weather-box'>⚠️ Exception ({dataset}): {e}</div>", unsafe_allow_html=True) | |
| time.sleep(1.2) | |
| if elevations: | |
| average_elevation = round(sum(elevations) / len(elevations), 2) | |
| if verbose: | |
| st.markdown(f""" | |
| <div class="weather-box"> | |
| ✅ ค่าเฉลี่ยระดับความสูงจาก {len(elevations)} แหล่ง: <strong>{average_elevation} m</strong> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| return average_elevation | |
| else: | |
| if verbose: | |
| st.error("❌ ไม่สามารถดึงข้อมูลระดับความสูงจากทุกแหล่งได้") | |
| return None | |
| def get_slope_from_elevation(lat, lon, delta=0.0005): | |
| if lat is None or lon is None: | |
| return None | |
| coords = { | |
| "north": (lat + delta, lon), | |
| "south": (lat - delta, lon), | |
| "east": (lat, lon + delta), | |
| "west": (lat, lon - delta), | |
| "center": (lat, lon), | |
| } | |
| # ดึงค่าระดับความสูงจาก API | |
| elevations = {} | |
| for key, (lat_, lon_) in coords.items(): | |
| elevations[key] = get_elevation_api(lat_, lon_, verbose=False) | |
| # ถ้า elevation บางจุดดึงไม่สำเร็จ ให้ return None | |
| if any(v is None for v in elevations.values()): | |
| return None | |
| # คำนวณระยะห่างเป็นเมตร (โดยใช้ Haversine) | |
| dist_ns = haversine(*coords["north"], *coords["south"]) * 1000 | |
| dist_ew = haversine(*coords["east"], *coords["west"]) * 1000 | |
| # คำนวณความชันตามแนว N-S และ E-W | |
| slope_lat = (elevations["north"] - elevations["south"]) / dist_ns | |
| slope_lon = (elevations["east"] - elevations["west"]) / dist_ew | |
| # รวมความชันทั้ง 2 แนว | |
| slope_m = math.sqrt(slope_lat**2 + slope_lon**2) | |
| slope_deg = math.degrees(math.atan(slope_m)) | |
| return { | |
| "slope_m": round(abs(slope_m), 4), | |
| "slope_deg": round(abs(slope_deg), 2) | |
| } | |
| # --- โหลดข้อมูลจากฟังก์ชัน (มีการแปลงพิกัด UTM เป็น lat/lon) --- | |
| df = load_flood_data() | |
| # --- คำนวณระยะห่างจากจุดของผู้ใช้กับแต่ละจุดในข้อมูล --- | |
| df["distance"] = np.sqrt((df["lat"] - lat)**2 + (df["lon"] - lon)**2) | |
| # --- ส่วนแสดงแผนที่และช่องค้นหาสถานที่ --- | |
| col_map, col_input = st.columns([3, 1]) | |
| # --- แสดงผลพิกัดสุดท้าย --- | |
| # ฟังก์ชันหลักสำหรับการพยากรณ์ | |
| def predict_velocity(model, rain, depth_max, depth, elevation, slope_m): | |
| try: | |
| features = [[rain, depth_max, depth, elevation, slope_m]] | |
| return model.predict(features)[0] | |
| except Exception as e: | |
| st.error(f"❌ เกิดข้อผิดพลาดในการพยากรณ์: {e}") | |
| return None | |
| # -------------------- เริ่มต้น -------------------- | |
| # ตัวแปรจาก session | |
| lat = st.session_state.get("lat") | |
| lon = st.session_state.get("lon") | |
| # -------------------- แผนที่โต้ตอบ -------------------- | |
| m = folium.Map(location=[lat, lon], zoom_start=6) | |
| with col_map: | |
| map_data = st_folium(m, height=450, width=700) | |
| # ---------------- ค่าเริ่มต้น ---------------- | |
| if "lat" not in st.session_state: | |
| st.session_state["lat"] = 48.69 | |
| if "lon" not in st.session_state: | |
| st.session_state["lon"] = 8.15 | |
| if "place" not in st.session_state: | |
| st.session_state["place"] = "" | |
| if "last_clicked" not in st.session_state: | |
| st.session_state["last_clicked"] = None | |
| # ---------------- ตรวจสอบการคลิก ---------------- | |
| if map_data and map_data.get("last_clicked"): | |
| clicked_lat = map_data["last_clicked"]["lat"] | |
| clicked_lon = map_data["last_clicked"]["lng"] | |
| # เงื่อนไขอัปเดตเมื่อคลิกใหม่ | |
| if ( | |
| st.session_state["last_clicked"] is None or | |
| clicked_lat != st.session_state["last_clicked"]["lat"] or | |
| clicked_lon != st.session_state["last_clicked"]["lon"] | |
| ): | |
| st.session_state["last_clicked"] = {"lat": clicked_lat, "lon": clicked_lon} | |
| st.session_state["lat"] = clicked_lat | |
| st.session_state["lon"] = clicked_lon | |
| st.session_state["place"] = "" # reset การค้นหา | |
| # ---------------- ช่องค้นหาสถานที่ ---------------- | |
| # ทำงานต่อเมื่อยังไม่มีการคลิก | |
| if st.session_state["last_clicked"] is None: | |
| place = st.text_input("🔍 Typing", value=st.session_state["place"]) | |
| if place and place != st.session_state["place"]: | |
| lat, lon = geocode(place) | |
| if lat is not None and lon is not None: | |
| st.session_state["lat"] = lat | |
| st.session_state["lon"] = lon | |
| st.session_state["place"] = place | |
| st.success(f"📍 Lat: {lat:.5f} | Long: {lon:.5f}") | |
| else: | |
| st.warning("⚠️ ไม่สามารถระบุตำแหน่งพิกัดจากชื่อสถานที่ได้") | |
| # ---------------- แสดงพิกัด ---------------- | |
| with col_input: | |
| st.subheader("Coordinates") | |
| st.write(f"Latitude: `{st.session_state['lat']:.5f}`") | |
| st.write(f"Longitude: `{st.session_state['lon']:.5f}`") | |
| if st.session_state["place"]: | |
| st.caption(f"🔎 ค้นหาจาก: **{st.session_state['place']}**") | |
| elif st.session_state["last_clicked"]: | |
| st.caption("🖱️ Click on The map") | |
| # ---------------- Current Info ---------------- | |
| # -------------------- การวิเคราะห์ข้อมูล -------------------- | |
| nearest_row = df.loc[df["distance"].idxmin()] | |
| weather = get_weather(lat, lon, API_KEY) | |
| rain = weather.get("rain", 0.0) if weather else 0.0 | |
| depth_max = nearest_row["Depth(Max)"] | |
| depth = nearest_row["Depth"] | |
| elevation_use = nearest_row["Elevation"] | |
| elevation = get_elevation_api(lat, lon, verbose=False) | |
| coord_user = (lat, lon) | |
| coord_file = (nearest_row["lat"], nearest_row["lon"]) | |
| distance = geodesic(coord_user, coord_file).km | |
| slope_data = get_slope_from_elevation(lat, lon) | |
| slope_m = slope_data["slope_m"] if slope_data else None | |
| if slope_m is not None and elevation is not None: | |
| velocity_pred = predict_velocity(model, rain, depth_max, depth, elevation, slope_m) | |
| if velocity_pred is not None: | |
| st.metric("Velocity (m/s)", f"{velocity_pred:.2f}") | |
| st.metric("Slope (m/m)", f"{slope_data['slope_m']:.4f}") | |
| st.metric("Slope (°)", f"{slope_data['slope_deg']:.2f}") | |
| else: | |
| st.warning("ไม่สามารถพยากรณ์ค่าความเร็วได้") | |
| else: | |
| st.warning("⚠️ ไม่สามารถคำนวณความชันหรือระดับความสูงได้") | |
| # -------------------- ข้อมูลอากาศ -------------------- | |
| if weather: | |
| st.markdown(f""" | |
| <div class="weather-box"> | |
| 🌡️ อุณหภูมิ: <code>{weather['temp']} °C</code><br> | |
| 💧 ความชื้น: <code>{weather['humidity']} %</code><br> | |
| 💨 ลม: <code>{weather['wind_speed']} m/s</code> ทิศ <code>{weather['wind_deg']}°</code><br> | |
| ☁️ เมฆปกคลุม: <code>{weather['clouds']} %</code><br> | |
| 📈 ความกดอากาศ: <code>{weather['pressure']} hPa</code><br> | |
| ☔ ปริมาณฝน: <code>{weather['rain']} mm/hr</code><br> | |
| 📝 สภาพอากาศ: <code>{weather['desc']}</code> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # -------------------- Sidebar: พิกัดปัจจุบัน -------------------- | |
| if lat is not None and lon is not None: | |
| st.sidebar.subheader("🗺️ Current Location") | |
| st.sidebar.text(f"Lat: {lat:.5f}") | |
| st.sidebar.text(f"Lon: {lon:.5f}") | |
| # -------------------- เพิ่มจุดจำลองในแผนที่ -------------------- | |
| if "df" in st.session_state and not st.session_state.df.empty: | |
| df = st.session_state.df | |
| for _, row in df.iterrows(): | |
| velocity = row.get("velocity_pred", 0.0) | |
| popup_text = ( | |
| f"<b>Depth(Max):</b> {row.get('Depth(Max)', 0):.2f} m<br>" | |
| f"<b>Depth:</b> {row.get('Depth', 0):.2f} m<br>" | |
| f"<b>Elevation:</b> {row.get('Elevation', 0):.2f} m<br>" | |
| f"<b>WaterSurfaceElevation:</b> {row.get('WaterSurfaceElevation', 0):.2f} m<br>" | |
| f"<b>Rain:</b> {row.get('Rain(mmh-1)', 0):.2f} mm/h<br>" | |
| f"<b>Predicted Velocity:</b> {velocity:.2f} m/s<br>" | |
| f"<b>Risk:</b> {get_risk_from_velocity(velocity)}" | |
| ) | |
| folium.CircleMarker( | |
| location=[row["lat"], row["lon"]], | |
| radius=3, | |
| fill=True, | |
| fill_opacity=0.7, | |
| color="blue", | |
| popup=popup_text | |
| ).add_to(m) | |
| # -------------------- Sidebar: รายการโปรด -------------------- | |
| st.sidebar.header("Favorites") | |
| if "favorites" not in st.session_state: | |
| st.session_state.favorites = [] | |
| if st.sidebar.button("Clear Favorites"): | |
| st.session_state.favorites = [] | |
| for idx, fav in enumerate(st.session_state.favorites): | |
| st.sidebar.write(f"{idx+1}. {fav['name']} ({fav['lat']:.4f}, {fav['lon']:.4f})") | |
| # ปุ่มบันทึกสถานที่โปรด | |
| if st.button("Save to Favorites", key="save_fav_btn"): | |
| add_favorite(lat, lon, st.session_state.get("place", "Unknown")) | |
| st.success(f"📌 Saved {st.session_state.get('place', 'Unnamed Location')} to favorites!") | |
| # -------------------- ปุ่มควบคุมอื่น ๆ -------------------- | |
| if st.button("Start Analysis", key="analyze_btn"): | |
| try: | |
| result = analyze_risk(lat, lon, df, model, API_KEY) | |
| st.success(f"✅ Analysis Result for {st.session_state.get('place', 'Unknown')}: {result}") | |
| except Exception as e: | |
| st.error(f"❌ Error during analysis: {e}") | |
| if st.button("Restart", key="restart_btn"): | |
| for key in ["clicked_location", "velocity_pred", "risk_level", "historical_data", "search_location"]: | |
| if key in st.session_state: | |
| del st.session_state[key] | |
| raise RerunException(get_script_run_ctx()) | |
| # ข้อมูลเกี่ยวกับระบบ | |
| with st.expander("ℹ️ เกี่ยวกับ Flood Guardian"): | |
| st.markdown(""" | |
| 🌊 Machine Learning: Random Forest Regression สำหรับทำนายความเร็วกระแสน้ำ | |
| Random Forest เป็นหนึ่งในอัลกอริทึมสำคัญของกลุ่ม Ensemble Learning ซึ่งมีหลักการสร้างโมเดลจากการรวมผลลัพธ์ของหลาย ๆ โมเดลต้นไม้ตัดสินใจ (Decision Trees) เพื่อเพิ่มความแม่นยำและลดปัญหา overfitting... | |
| --- | |
| หลักการทำงานของ Random Forest | |
| 1. การสร้างหลายต้นไม้ตัดสินใจ (Decision Trees) | |
| ใช้เทคนิค Bootstrap Aggregation (Bagging) โดยการสุ่มข้อมูลฝึกและฟีเจอร์บางส่วนในแต่ละต้น | |
| 2. การทำนายแบบ Regression | |
| ค่าที่ได้คือค่าเฉลี่ยของค่าที่แต่ละต้นไม้ทำนายออกมา | |
| 3. ข้อดีของ Random Forest | |
| - ลด overfitting | |
| - จัดการกับข้อมูลหลายมิติได้ดี | |
| - ไม่ต้องตั้งสมมุติฐานความสัมพันธ์เชิงฟังก์ชัน | |
| --- | |
| การประยุกต์ใช้กับ Flood Guardian | |
| ใช้ข้อมูลจาก: | |
| - ปริมาณฝน (Rain) | |
| - ความลึกน้ำ (Depth) | |
| - ความสูงพื้นที่ (Elevation) | |
| - ความเร็วกระแสน้ำจริง (Velocity) | |
| Random Forest Regression สามารถเรียนรู้ความสัมพันธ์ซับซ้อนแบบไม่เชิงเส้นจากข้อมูลพวกนี้ได้อย่างแม่นยำ | |
| --- | |
| แหล่งข้อมูล | |
| - 🌐 [Nays2Dfloods](https://i-ric.org/en/solvers/nays2dh/) | |
| - ☁️ [OpenWeatherMap](https://openweathermap.org/api) | |
| - 🗺️ [OpenStreetMap](https://www.openstreetmap.org) | |
| - 🏔️ [OpenTopoData](https://www.opentopodata.org/) | |
| <p align="center"> | |
| <img src="https://huggingface.co/spaces/Nuttanitranslate/Flood/resolve/main/IMG_9449.webp" width="90%"> | |
| <br><em>การทำงานของ Random Forest</em> | |
| </p> | |
| 💡 **หมายเหตุ**: ข้อมูลนี้เป็นการประเมินเบื้องต้น ไม่ใช่คำเตือนทางการจากหน่วยงานรัฐ | |
| """, unsafe_allow_html=True) |