Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import pyproj # Added for coordinate transformation | |
| import base64 # For encoding logo image | |
| import os # For path joining | |
| # --- Theme Selection Logic --- | |
| # MUST BE VERY EARLY, ideally after imports and before page_config | |
| if 'selected_theme_name' not in st.session_state: | |
| st.session_state.selected_theme_name = "تم شیشهای (مینیال)" # Default theme | |
| # Define theme colors (CSS variables) | |
| # Each theme will override these variables | |
| THEMES = { | |
| "تم شیشهای (مینیال)": { | |
| "--primary-color": "#007bff", # A clean blue | |
| "--secondary-color": "#6c757d", # A neutral grey | |
| "--accent-color": "#17a2b8", # A calming teal/cyan | |
| "--background-color": "transparent", # Page background is now a gradient in body | |
| "--container-background-color": "rgba(255, 255, 255, 0.6)", # The glass itself | |
| "--container-border-color": "rgba(255, 255, 255, 0.25)", | |
| "--text-color": "#212529", # Dark text for readability | |
| "--header-text-color": "#004085", # Darker blue for headers | |
| "--button-bg-color": "rgba(0, 123, 255, 0.7)", | |
| "--button-hover-bg-color": "rgba(0, 123, 255, 0.9)", | |
| "--metric-border-accent": "var(--primary-color)", | |
| "--table-header-bg": "rgba(0, 123, 255, 0.1)", | |
| "--tab-active-bg": "rgba(255, 255, 255, 0.8)", | |
| "--tab-active-text": "var(--primary-color)", | |
| "--info-bg": "rgba(23, 162, 184, 0.1)", | |
| "--info-border": "var(--accent-color)", | |
| "--warning-bg": "rgba(255, 193, 7, 0.1)", | |
| "--warning-border": "#ffc107", | |
| "--success-bg": "rgba(40, 167, 69, 0.1)", | |
| "--success-border": "#28a745", | |
| }, | |
| "پیشفرض (آبی تیره)": { | |
| "--primary-color": "#1a535c", # Dark Teal | |
| "--secondary-color": "#4ecdc4", # Light Teal | |
| "--accent-color": "#e76f51", # Coral | |
| "--background-color": "#f0f2f6", # Light Grey Page BG | |
| "--container-background-color": "#ffffff", # White Container BG | |
| "--text-color": "#212529", # Dark Text | |
| "--header-text-color": "#1a535c", | |
| "--button-bg-color": "#264653", | |
| "--button-hover-bg-color": "#2a9d8f", | |
| "--metric-border-accent": "#4ecdc4", | |
| "--table-header-bg": "#2a9d8f", | |
| "--tab-active-bg": "#4ecdc4", | |
| "--tab-active-text": "white", | |
| "--info-bg": "#e6f7ff", # Light blue for info boxes | |
| "--info-border": "#007bff", | |
| "--warning-bg": "#fff3cd", # Light yellow for warning | |
| "--warning-border": "#ffc107", | |
| "--success-bg": "#f0fff0", # Light green for success | |
| "--success-border": "#28a745", | |
| }, | |
| "تم سبز (طبیعت)": { | |
| "--primary-color": "#2d6a4f", # Dark Green | |
| "--secondary-color": "#74c69d", # Medium Green | |
| "--accent-color": "#fca311", # Orange accent | |
| "--background-color": "#f4f9f4", | |
| "--container-background-color": "#ffffff", | |
| "--text-color": "#1b4332", | |
| "--header-text-color": "#2d6a4f", | |
| "--button-bg-color": "#40916c", | |
| "--button-hover-bg-color": "#52b788", | |
| "--metric-border-accent": "#74c69d", | |
| "--table-header-bg": "#40916c", | |
| "--tab-active-bg": "#74c69d", | |
| "--tab-active-text": "white", | |
| "--info-bg": "#e6fff0", | |
| "--info-border": "#2d6a4f", | |
| "--warning-bg": "#fff9e6", | |
| "--warning-border": "#fca311", | |
| "--success-bg": "#e6fff0", | |
| "--success-border": "#2d6a4f", | |
| }, | |
| "تم قرمز (هشدار)": { | |
| "--primary-color": "#9d0208", # Dark Red | |
| "--secondary-color": "#dc2f02", # Medium Red | |
| "--accent-color": "#ffba08", # Yellow accent | |
| "--background-color": "#fff5f5", | |
| "--container-background-color": "#ffffff", | |
| "--text-color": "#370617", | |
| "--header-text-color": "#9d0208", | |
| "--button-bg-color": "#ae2012", | |
| "--button-hover-bg-color": "#dc2f02", | |
| "--metric-border-accent": "#dc2f02", | |
| "--table-header-bg": "#ae2012", | |
| "--tab-active-bg": "#dc2f02", | |
| "--tab-active-text": "white", | |
| "--info-bg": "#ffeeee", | |
| "--info-border": "#9d0208", | |
| "--warning-bg": "#fff0e6", | |
| "--warning-border": "#ffba08", | |
| "--success-bg": "#eeffee", # Less prominent success | |
| "--success-border": "#555", | |
| }, | |
| "تم زرد/نارنجی (گرم)": { | |
| "--primary-color": "#e76f51", # Coral (Primary) | |
| "--secondary-color": "#f4a261", # Sandy Brown | |
| "--accent-color": "#2a9d8f", # Teal Accent | |
| "--background-color": "#fff8f0", | |
| "--container-background-color": "#ffffff", | |
| "--text-color": "#854d0e", # Brown text | |
| "--header-text-color": "#d95f02", # Dark Orange | |
| "--button-bg-color": "#e76f51", | |
| "--button-hover-bg-color": "#f4a261", | |
| "--metric-border-accent": "#f4a261", | |
| "--table-header-bg": "#e76f51", | |
| "--tab-active-bg": "#f4a261", | |
| "--tab-active-text": "white", | |
| "--info-bg": "#fff8e1", | |
| "--info-border": "#e76f51", | |
| "--warning-bg": "#fff3cd", | |
| "--warning-border": "#f4a261", | |
| "--success-bg": "#f0fff0", | |
| "--success-border": "#2a9d8f", | |
| }, | |
| "تم قهوهای (خاکی)": { | |
| "--primary-color": "#544741", # Dark Brown | |
| "--secondary-color": "#8a786f", # Medium Brown | |
| "--accent-color": "#c6ac8f", # Light Tan/Beige | |
| "--background-color": "#f5f2ef", | |
| "--container-background-color": "#ffffff", | |
| "--text-color": "#3d2c25", | |
| "--header-text-color": "#544741", | |
| "--button-bg-color": "#6f5f55", | |
| "--button-hover-bg-color": "#8a786f", | |
| "--metric-border-accent": "#8a786f", | |
| "--table-header-bg": "#6f5f55", | |
| "--tab-active-bg": "#8a786f", | |
| "--tab-active-text": "white", | |
| "--info-bg": "#f9f6f3", | |
| "--info-border": "#544741", | |
| "--warning-bg": "#fef7e0", # Light yellow | |
| "--warning-border": "#c6ac8f", | |
| "--success-bg": "#f3f9f3", | |
| "--success-border": "#777", | |
| }, | |
| "تم روشن (ساده)": { | |
| "--primary-color": "#4A5568", # Cool Gray | |
| "--secondary-color": "#718096", # Medium Gray | |
| "--accent-color": "#3182CE", # Blue Accent | |
| "--background-color": "#F7FAFC", | |
| "--container-background-color": "#FFFFFF", | |
| "--text-color": "#2D3748", | |
| "--header-text-color": "#2D3748", | |
| "--button-bg-color": "#4A5568", | |
| "--button-hover-bg-color": "#2D3748", | |
| "--metric-border-accent": "#718096", | |
| "--table-header-bg": "#E2E8F0", # Light gray, ensure good contrast with white text if used, or change text color | |
| "--tab-active-bg": "#4A5568", | |
| "--tab-active-text": "white", | |
| "--info-bg": "#EBF8FF", | |
| "--info-border": "#3182CE", | |
| "--warning-bg": "#FFFBEB", | |
| "--warning-border": "#ECC94B", | |
| "--success-bg": "#F0FFF4", | |
| "--success-border": "#48BB78", | |
| } | |
| } | |
| current_theme_colors = THEMES[st.session_state.selected_theme_name] | |
| # --- Page Config --- | |
| st.set_page_config( | |
| page_title="سامانه پایش هوشمند نیشکر", | |
| page_icon="🌾", | |
| layout="wide" | |
| ) | |
| # --- Custom Modern Cards CSS --- | |
| st.markdown(""" | |
| <style> | |
| @keyframes cardFadeIn { | |
| 0% { opacity: 0; transform: translateY(20px); } | |
| 100% { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Unified Glassmorphism Card Style for all tabs */ | |
| .modern-gradient-card, .glass-card, .ai-fadein-card { | |
| background: var(--container-background-color); | |
| backdrop-filter: blur(15px); | |
| -webkit-backdrop-filter: blur(15px); | |
| color: var(--text-color); | |
| border-radius: 18px; | |
| border: 1.5px solid var(--container-border-color); | |
| box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1); | |
| padding: 30px 24px; | |
| margin-bottom: 28px; | |
| display: flex; | |
| align-items: center; | |
| animation: cardFadeIn 1s cubic-bezier(.39,.575,.565,1) both; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .modern-gradient-card .icon, .glass-card .glass-icon, .ai-fadein-card .ai-icon { | |
| font-size: 2.8em; | |
| margin-left: 18px; | |
| opacity: 0.8; | |
| animation: none; /* Remove pulse/glow animations */ | |
| filter: none; | |
| } | |
| .glass-card .glass-icon { | |
| color: var(--primary-color); | |
| } | |
| .ai-fadein-card .ai-icon { | |
| color: var(--primary-color); | |
| } | |
| /* Remove FAB from tab 2 for minimalist look */ | |
| .fab-animated { | |
| display: none; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- Animated Logo Display --- | |
| def get_image_as_base64(path): | |
| if not os.path.exists(path): | |
| return None | |
| with open(path, "rb") as img_file: | |
| return base64.b64encode(img_file.read()).decode() | |
| logo_path = "logo (1).png" # Your logo file | |
| logo_base64 = get_image_as_base64(logo_path) | |
| if logo_base64: | |
| logo_html = f""" | |
| <style> | |
| .logo-container {{ | |
| display: flex; | |
| justify-content: center; /* Center the logo horizontally */ | |
| align-items: center; | |
| padding: 10px; /* Add some padding around the logo */ | |
| margin-bottom: 20px; /* Space below the logo */ | |
| background-color: var(--container-background-color); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--container-border-color); | |
| border-radius: 10px; /* Optional: rounded corners for the background container */ | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); /* Optional: subtle shadow */ | |
| }} | |
| .logo-container img {{ | |
| max-height: 100px; /* Adjust max height as needed */ | |
| max-width: 100%; /* Ensure logo is responsive within its container */ | |
| object-fit: contain; | |
| }} | |
| </style> | |
| <div class="logo-container"> | |
| <img src="data:image/png;base64,{logo_base64}" alt="Company Logo"> | |
| </div> | |
| """ | |
| st.markdown(logo_html, unsafe_allow_html=True) | |
| else: | |
| st.warning(f"لوگو در مسیر '{logo_path}' یافت نشد. لطفاً مسیر فایل را بررسی کنید.") | |
| # --- Imports --- (Keep after page_config if they don't cause issues) | |
| import pandas as pd | |
| import ee | |
| import geemap.foliumap as geemap | |
| import folium | |
| import json | |
| import datetime | |
| import plotly.express as px | |
| import os | |
| from io import BytesIO | |
| import requests | |
| import traceback | |
| from streamlit_folium import st_folium | |
| import google.generativeai as genai | |
| import time # For potential (not recommended) auto-rerun | |
| # --- Apply Dynamic CSS based on selected theme --- | |
| # This CSS block will use the variables defined in current_theme_colors | |
| st.markdown(f""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap'); | |
| :root {{ | |
| {"; ".join([f"{key}: {value}" for key, value in current_theme_colors.items()])}; | |
| }} | |
| body {{ | |
| font-family: 'Vazirmatn', sans-serif; | |
| background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%); | |
| background-attachment: fixed; /* Keep gradient fixed during scroll */ | |
| color: var(--text-color); | |
| }} | |
| /* Main container - not directly targetable, use for .main if Streamlit uses it */ | |
| .main {{ | |
| font-family: 'Vazirmatn', sans-serif; | |
| background-color: transparent; /* Main content area should be transparent to see body gradient */ | |
| }} | |
| /* Headers */ | |
| h1, h2, h3 {{ | |
| font-family: 'Vazirmatn', sans-serif; | |
| text-align: right; | |
| font-weight: 600; | |
| margin-bottom: 0.7em; | |
| }} | |
| h2 {{ | |
| color: var(--primary-color); | |
| }} | |
| h3 {{ | |
| color: var(--accent-color); | |
| font-weight: 500; | |
| }} | |
| /* Metrics - Enhanced Styling */ | |
| .stMetric {{ | |
| font-family: 'Vazirmatn', sans-serif; | |
| background: var(--container-background-color); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--container-border-color); | |
| border-left: 5px solid var(--metric-border-accent); | |
| border-radius: 12px; | |
| padding: 1.2rem; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.08); | |
| transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; | |
| }} | |
| .stMetric:hover {{ | |
| transform: translateY(-4px); | |
| box-shadow: 0 6px 20px rgba(0,0,0,0.12); | |
| }} | |
| .stMetric > label {{ | |
| font-weight: 500; | |
| color: var(--primary-color); | |
| }} | |
| .stMetric > div[data-testid="stMetricValue"] {{ | |
| font-size: 1.8em; | |
| font-weight: 600; | |
| color: var(--text-color); | |
| }} | |
| /* Tabs */ | |
| .stTabs [data-baseweb="tab-list"] {{ | |
| gap: 8px; | |
| direction: rtl; | |
| border-bottom: none; /* Remove bottom border for a cleaner look */ | |
| }} | |
| .stTabs [data-baseweb="tab"] {{ | |
| height: 55px; | |
| padding: 12px 25px; | |
| background-color: rgba(255, 255, 255, 0.2); /* Inactive tabs are slightly transparent */ | |
| border-radius: 10px 10px 0 0; | |
| font-family: 'Vazirmatn', sans-serif; | |
| font-weight: 600; | |
| color: var(--text-color); | |
| border: 1px solid var(--container-border-color); | |
| border-bottom: none; | |
| transition: background-color 0.3s, color 0.3s; | |
| }} | |
| .stTabs [data-baseweb="tab"][aria-selected="true"] {{ | |
| background-color: var(--tab-active-bg); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| color: var(--tab-active-text); | |
| border-color: var(--container-border-color); | |
| }} | |
| /* Tables */ | |
| .dataframe-container table {{ | |
| font-family: 'Vazirmatn', sans-serif; | |
| text-align: right; | |
| border-collapse: collapse; | |
| width: 100%; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.07); | |
| border-radius: 10px; | |
| overflow: hidden; | |
| background: transparent; | |
| }} | |
| .dataframe-container th {{ | |
| background-color: var(--table-header-bg); /* Use a semi-transparent variable */ | |
| color: var(--header-text-color); | |
| backdrop-filter: blur(5px); | |
| -webkit-backdrop-filter: blur(5px); | |
| padding: 12px 15px; | |
| font-weight: 600; | |
| text-align: right; | |
| }} | |
| .dataframe-container td {{ | |
| padding: 10px 15px; | |
| border-bottom: 1px solid var(--container-border-color); | |
| background-color: transparent; /* Cell background */ | |
| }} | |
| .dataframe-container tr:nth-child(even) td {{ | |
| background-color: rgba(255,255,255,0.05); /* very subtle alternating color */ | |
| }} | |
| .dataframe-container tr:hover td {{ | |
| background-color: rgba(255,255,255,0.15); | |
| }} | |
| /* Sidebar */ | |
| .css-1d391kg {{ /* Streamlit's default sidebar class */ | |
| font-family: 'Vazirmatn', sans-serif; | |
| direction: rtl; | |
| background-color: var(--container-background-color); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border-left: 1px solid var(--container-border-color); | |
| padding: 1.5rem; | |
| }} | |
| .css-1d391kg .stSelectbox label, .css-1d391kg .stTextInput label, .css-1d391kg .stButton > button {{ | |
| font-weight: 500; | |
| color: var(--text-color); | |
| }} | |
| /* Custom status badges */ | |
| .status-badge {{ padding: 5px 10px; border-radius: 15px; font-size: 0.85em; font-weight: 500; display: inline-block; }} | |
| .status-positive {{ background-color: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }} | |
| .status-neutral {{ background-color: #feF3c7; color: #92400e; border: 1px solid #fcd34d; }} | |
| .status-negative {{ background-color: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }} | |
| /* Custom containers for better visual grouping */ | |
| .section-container {{ | |
| background-color: var(--container-background-color); | |
| backdrop-filter: blur(15px); | |
| -webkit-backdrop-filter: blur(15px); | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| border: 1px solid var(--container-border-color); | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| margin-bottom: 2rem; | |
| }} | |
| /* Styling for buttons */ | |
| .stButton > button {{ | |
| font-family: 'Vazirmatn', sans-serif; | |
| background-color: var(--button-bg-color); | |
| color: white; | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| padding: 10px 20px; | |
| border-radius: 10px; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| backdrop-filter: blur(5px); | |
| -webkit-backdrop-filter: blur(5px); | |
| }} | |
| .stButton > button:before {{ | |
| display: none; /* Remove shine animation */ | |
| }} | |
| .stButton > button:hover {{ | |
| background-color: var(--button-hover-bg-color); | |
| transform: translateY(-3px); | |
| box-shadow: 0 7px 14px rgba(0,0,0,0.15); | |
| }} | |
| .stButton > button:active {{ | |
| transform: translateY(1px); | |
| box-shadow: 0 3px 8px rgba(0,0,0,0.15); | |
| }} | |
| /* Input fields */ | |
| .stTextInput input, .stSelectbox div[data-baseweb="select"] > div, .stDateInput input {{ | |
| border-radius: 8px !important; /* Ensure high specificity */ | |
| border: 1px solid #ced4da !important; | |
| background-color: var(--container-background-color) !important; | |
| color: var(--text-color) !important; | |
| }} | |
| .stTextInput input:focus, .stSelectbox div[data-baseweb="select"] > div:focus-within, .stDateInput input:focus {{ | |
| border-color: var(--accent-color) !important; | |
| box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--accent-color) 30%, transparent 70%) !important; | |
| }} | |
| /* Placeholder text color for inputs */ | |
| .stTextInput input::placeholder {{ color: color-mix(in srgb, var(--text-color) 60%, transparent 40%); }} | |
| /* Markdown links */ | |
| a {{ color: var(--accent-color); text-decoration: none; }} | |
| a:hover {{ text-decoration: underline; }} | |
| /* Custom Gemini response box styles */ | |
| .gemini-response-default, .gemini-response-report, .gemini-response-analysis {{ | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| padding: 20px; | |
| border-radius: 12px; | |
| margin-top: 20px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.08); | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| border: 1px solid var(--container-border-color); | |
| }} | |
| .gemini-response-default:before, .gemini-response-report:before, .gemini-response-analysis:before {{ | |
| content: '💡'; | |
| font-size: 1.2em; | |
| position: absolute; | |
| top: 10px; | |
| right: 15px; | |
| opacity: 0.5; | |
| }} | |
| .gemini-response-default:hover, .gemini-response-report:hover, .gemini-response-analysis:hover {{ | |
| box-shadow: 0 6px 16px rgba(0,0,0,0.12); | |
| transform: translateY(-3px); | |
| }} | |
| .gemini-response-report {{ | |
| background-color: var(--success-bg); | |
| border-left: 5px solid var(--success-border); | |
| }} | |
| .gemini-response-analysis {{ | |
| background-color: var(--warning-bg); | |
| border-left: 5px solid var(--warning-border); | |
| }} | |
| /* Animated Gemini AI Tab */ | |
| .gemini-header {{ | |
| background: var(--container-background-color); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--container-border-color); | |
| color: var(--header-text-color); | |
| padding: 15px 25px; | |
| border-radius: 12px; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| }} | |
| .pulse-animation {{ | |
| animation: none; | |
| }} | |
| .fade-in {{ | |
| opacity: 0; | |
| animation: fadeIn 1s forwards; | |
| }} | |
| @keyframes fadeIn {{ | |
| from {{ opacity: 0; transform: translateY(20px); }} | |
| to {{ opacity: 1; transform: translateY(0); }} | |
| }} | |
| /* Animated AI Icon */ | |
| .ai-icon {{ | |
| display: flex; | |
| justify-content: center; | |
| margin-bottom: 15px; | |
| }} | |
| .ai-icon-pulse {{ | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| background-color: var(--accent-color); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| font-size: 30px; | |
| animation: none; | |
| }} | |
| /* Advanced Card Design for Gemini Sections */ | |
| .gemini-card {{ | |
| background-color: var(--container-background-color); | |
| backdrop-filter: blur(15px); | |
| -webkit-backdrop-filter: blur(15px); | |
| border-radius: 12px; | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
| padding: 20px; | |
| margin-bottom: 25px; | |
| transition: all 0.3s ease; | |
| border: 1px solid var(--container-border-color); | |
| border-top: 4px solid var(--accent-color); | |
| position: relative; | |
| overflow: hidden; | |
| }} | |
| .gemini-card:hover {{ | |
| transform: translateY(-5px); | |
| box-shadow: 0 15px 30px rgba(0,0,0,0.15); | |
| }} | |
| .gemini-card::after {{ | |
| display: none; | |
| }} | |
| .gemini-card-header {{ | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| padding-bottom: 10px; | |
| border-bottom: 1px solid rgba(0,0,0,0.1); | |
| }} | |
| .gemini-card-icon {{ | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 8px; | |
| background-color: var(--accent-color); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| margin-left: 15px; | |
| color: white; | |
| font-weight: bold; | |
| font-size: 20px; | |
| }} | |
| .gemini-card-title {{ | |
| margin: 0; | |
| font-size: 18px; | |
| color: var(--primary-color); | |
| font-weight: 600; | |
| }} | |
| /* Floating Action Button */ | |
| .fab-button {{ | |
| display: none; | |
| }} | |
| /* Loading Animation */ | |
| .loading-animation {{ | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| }} | |
| .loading-dot {{ | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background-color: var(--accent-color); | |
| margin: 0 5px; | |
| animation: loading 1.4s infinite ease-in-out both; | |
| }} | |
| .loading-dot:nth-child(1) {{ animation-delay: -0.32s; }} | |
| .loading-dot:nth-child(2) {{ animation-delay: -0.16s; }} | |
| @keyframes loading {{ | |
| 0%, 80%, 100% {{ transform: scale(0); }} | |
| 40% {{ transform: scale(1); }} | |
| }} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- Configuration --- | |
| APP_TITLE = "سامانه پایش هوشمند نیشکر" | |
| APP_SUBTITLE = "مطالعات کاربردی شرکت کشت و صنعت دهخدا" | |
| INITIAL_LAT = 31.534442 | |
| INITIAL_LON = 48.724416 | |
| INITIAL_ZOOM = 12 | |
| # --- File Paths (Relative to the script location in Hugging Face) --- | |
| # CSV_FILE_PATH = 'cleaned_output.csv' # OLD | |
| CSV_FILE_PATH = 'merged_farm_data_renamed (1).csv' # NEW | |
| SERVICE_ACCOUNT_FILE = 'boreal-furnace-436321-f4-bac45f087928.json' | |
| # --- GEE Authentication --- | |
| def initialize_gee(): | |
| try: | |
| if not os.path.exists(SERVICE_ACCOUNT_FILE): | |
| st.error(f"خطا: فایل Service Account در مسیر '{SERVICE_ACCOUNT_FILE}' یافت نشد.") | |
| st.stop() | |
| credentials = ee.ServiceAccountCredentials(None, key_file=SERVICE_ACCOUNT_FILE) | |
| ee.Initialize(credentials=credentials, opt_url='https://earthengine-highvolume.googleapis.com') | |
| return True | |
| except Exception as e: | |
| st.error(f"خطا در اتصال به GEE: {e}") | |
| st.stop() | |
| # --- Load Farm Data from GEE FeatureCollection --- | |
| def load_farm_data_from_gee(): | |
| try: | |
| farms_fc = ee.FeatureCollection("projects/boreal-furnace-436321-f4/assets/Croplogging-Farm") | |
| features = farms_fc.getInfo()['features'] | |
| farm_records = [] | |
| for f in features: | |
| props = f['properties'] | |
| geom = f['geometry'] | |
| # Create EE geometry to calculate accurate area | |
| ee_geom = None | |
| if geom['type'] == 'Polygon': | |
| ee_geom = ee.Geometry.Polygon(geom['coordinates']) | |
| # محاسبه centroid | |
| if geom['type'] == 'Polygon': | |
| coords = geom['coordinates'][0] | |
| centroid_lon = sum([pt[0] for pt in coords]) / len(coords) | |
| centroid_lat = sum([pt[1] for pt in coords]) / len(coords) | |
| else: | |
| centroid_lon, centroid_lat = None, None | |
| # محاسبه مساحت دقیق بر اساس هندسه | |
| area_ha = None | |
| if ee_geom: | |
| try: | |
| area_m2 = ee_geom.area(maxError=1).getInfo() | |
| if area_m2 is not None: | |
| area_ha = area_m2 / 10000.0 # تبدیل به هکتار | |
| except Exception: | |
| area_ha = None | |
| farm_records.append({ | |
| 'مزرعه': props.get('farm', ''), | |
| 'گروه': props.get('group', ''), | |
| 'واریته': props.get('Variety', ''), | |
| 'سن': props.get('Age', ''), | |
| 'مساحت': area_ha if area_ha is not None else props.get('Area', ''), # استفاده از مساحت محاسبه شده دقیق | |
| 'روز ': props.get('Day', ''), | |
| 'Field': props.get('Field', ''), | |
| 'geometry': geom, | |
| 'centroid_lon': centroid_lon, | |
| 'centroid_lat': centroid_lat, | |
| 'calculated_area_ha': area_ha, # ذخیره مساحت محاسبه شده به عنوان ستون جداگانه | |
| }) | |
| df = pd.DataFrame(farm_records) | |
| st.success(f"✅ دادههای {len(df)} مزرعه از GEE بارگذاری شد.") | |
| return df | |
| except Exception as e: | |
| st.error(f"❌ خطا در بارگذاری داده از GEE: {e}") | |
| return None | |
| # --- Use GEE farm data instead of CSV --- | |
| if initialize_gee(): | |
| farm_data_df = load_farm_data_from_gee() | |
| else: | |
| st.error("❌ اتصال به GEE ناموفق بود.") | |
| st.stop() | |
| if farm_data_df is None: | |
| st.error("❌ بارگذاری داده مزارع از GEE ناموفق بود.") | |
| st.stop() | |
| # ============================================================================== | |
| # Gemini API Configuration | |
| # ============================================================================== | |
| # !!! هشدار امنیتی: قرار دادن مستقیم API Key در کد ریسک بالایی دارد !!! | |
| GEMINI_API_KEY = "AIzaSyDzirWUubBVyjF10_JZ8UVSd6c6nnTKpLw" # <<<<<<< جایگزین کنید >>>>>>>> | |
| gemini_model = None | |
| if GEMINI_API_KEY and GEMINI_API_KEY != "YOUR_GEMINI_API_KEY_HERE": | |
| try: | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| gemini_model = genai.GenerativeModel('gemini-1.5-flash-latest') | |
| # st.sidebar.success("✅ اتصال به Gemini برقرار شد.") # Sidebar not yet rendered | |
| except Exception as e: | |
| # st.sidebar.error(f"خطا در اتصال به Gemini: {e}") # Sidebar not yet rendered | |
| print(f"خطا در اتصال به Gemini: {e}") # Log to console instead | |
| gemini_model = None | |
| # else handled in sidebar display logic | |
| def ask_gemini(prompt_text, temperature=0.7, top_p=1.0, top_k=40): | |
| if not gemini_model: | |
| return "خطا: مدل Gemini مقداردهی اولیه نشده است. کلید API را بررسی کنید." | |
| try: | |
| generation_config = genai.types.GenerationConfig( | |
| temperature=temperature, top_p=top_p, top_k=top_k, max_output_tokens=3072 | |
| ) | |
| response = gemini_model.generate_content(prompt_text, generation_config=generation_config) | |
| return response.text | |
| except Exception as e: | |
| return f"خطا در ارتباط با Gemini API: {e}\n{traceback.format_exc()}" | |
| # ============================================================================== | |
| # Sidebar | |
| # ============================================================================== | |
| with st.sidebar: | |
| st.markdown("## 🎨 انتخاب تم") | |
| selected_theme_name_sidebar = st.selectbox( | |
| "تم رنگی برنامه را انتخاب کنید:", | |
| options=list(THEMES.keys()), | |
| index=list(THEMES.keys()).index(st.session_state.selected_theme_name), | |
| key="theme_selector_widget" | |
| ) | |
| if selected_theme_name_sidebar != st.session_state.selected_theme_name: | |
| st.session_state.selected_theme_name = selected_theme_name_sidebar | |
| st.rerun() # Rerun to apply new theme CSS | |
| st.markdown("---") | |
| st.header("⚙️ تنظیمات نمایش") | |
| if GEMINI_API_KEY == "YOUR_GEMINI_API_KEY_HERE": | |
| st.warning("⚠️ کلید API جمینای خود را مستقیماً در کد برنامه (متغیر GEMINI_API_KEY) وارد کنید تا قابلیتهای هوشمند فعال شوند.") | |
| elif not gemini_model: | |
| st.error("اتصال به Gemini ناموفق بود. کلید API را بررسی کنید.") | |
| else: | |
| st.success("✅ اتصال به Gemini برقرار است.") | |
| # available_days = sorted(farm_data_df['روزهای هفته'].unique()) # OLD | |
| available_days = sorted(farm_data_df['روز '].unique()) # NEW: Using 'روز ' (with space) | |
| selected_day = st.selectbox( | |
| "📅 روز هفته:", options=available_days, index=0, | |
| help="دادههای مزارع بر اساس این روز فیلتر میشوند." | |
| ) | |
| # filtered_farms_df = farm_data_df[farm_data_df['روزهای هفته'] == selected_day].copy() # OLD | |
| filtered_farms_df = farm_data_df[farm_data_df['روز '] == selected_day].copy() # NEW | |
| if filtered_farms_df.empty: | |
| st.warning(f"⚠️ هیچ مزرعهای برای روز '{selected_day}' یافت نشد.") | |
| st.stop() | |
| available_farms = sorted(filtered_farms_df['مزرعه'].unique()) | |
| farm_options = ["همه مزارع"] + available_farms | |
| selected_farm_name = st.selectbox( | |
| "🌾 انتخاب مزرعه:", options=farm_options, index=0, | |
| help="مزرعهای که میخواهید جزئیات آن را ببینید یا 'همه مزارع' برای نمایش کلی." | |
| ) | |
| index_options = { | |
| "NDVI": "پوشش گیاهی (NDVI)", "EVI": "پوشش گیاهی بهبودیافته (EVI)", | |
| "NDMI": "رطوبت گیاه (NDMI)", "LAI": "سطح برگ (LAI)", | |
| "MSI": "تنش رطوبتی (MSI)", "CVI": "کلروفیل (CVI)", | |
| } | |
| selected_index = st.selectbox( | |
| "📈 انتخاب شاخص:", options=list(index_options.keys()), | |
| format_func=lambda x: f"{x} - {index_options[x]}", index=0 | |
| ) | |
| today = datetime.date.today() | |
| persian_to_weekday = {"شنبه": 5, "یکشنبه": 6, "دوشنبه": 0, "سه شنبه": 1, "چهارشنبه": 2, "پنجشنبه": 3, "جمعه": 4} | |
| try: | |
| target_weekday = persian_to_weekday[selected_day] | |
| days_to_subtract = (today.weekday() - target_weekday + 7) % 7 | |
| end_date_current = today - datetime.timedelta(days=days_to_subtract if days_to_subtract != 0 else 0) | |
| if today.weekday() == target_weekday and days_to_subtract == 0: end_date_current = today | |
| elif days_to_subtract == 0 and today.weekday() != target_weekday: end_date_current = today - datetime.timedelta(days=7) | |
| start_date_current = end_date_current - datetime.timedelta(days=6) | |
| end_date_previous = start_date_current - datetime.timedelta(days=1) | |
| start_date_previous = end_date_previous - datetime.timedelta(days=6) | |
| start_date_current_str, end_date_current_str = start_date_current.strftime('%Y-%m-%d'), end_date_current.strftime('%Y-%m-%d') | |
| start_date_previous_str, end_date_previous_str = start_date_previous.strftime('%Y-%m-%d'), end_date_previous.strftime('%Y-%m-%d') | |
| st.markdown(f"<p style='font-size:0.9em;'>🗓️ <b>بازه فعلی:</b> {start_date_current_str} تا {end_date_current_str}</p>", unsafe_allow_html=True) | |
| st.markdown(f"<p style='font-size:0.9em;'>🗓️ <b>بازه قبلی:</b> {start_date_previous_str} تا {end_date_previous_str}</p>", unsafe_allow_html=True) | |
| except Exception as e: | |
| st.error(f"خطا در محاسبه بازه زمانی: {e}") | |
| st.stop() | |
| st.markdown("---") | |
| st.markdown("<div style='text-align:center; font-size:0.9em;'>Developed by Esmaeil Kiani<strong>اسماعیل کیانی</strong></div>", unsafe_allow_html=True) | |
| st.markdown("<div style='text-align:center; font-size:0.95em;'>🌾 شرکت کشت و صنعت دهخدا</div>", unsafe_allow_html=True) | |
| # ============================================================================== | |
| # GEE Functions (Copied from previous version - no changes needed for this request) | |
| # ============================================================================== | |
| def maskS2clouds(image): | |
| qa = image.select('QA60') | |
| cloudBitMask = 1 << 10 | |
| cirrusBitMask = 1 << 11 | |
| mask = qa.bitwiseAnd(cloudBitMask).eq(0).And(qa.bitwiseAnd(cirrusBitMask).eq(0)) | |
| scl = image.select('SCL') | |
| good_quality_scl = scl.remap([4, 5, 6], [1, 1, 1], 0) | |
| opticalBands = image.select('B.*').multiply(0.0001) | |
| return image.addBands(opticalBands, None, True).updateMask(mask).updateMask(good_quality_scl) | |
| def add_indices(image): | |
| ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI') | |
| evi = image.expression( | |
| '2.5 * (NIR - RED) / (NIR + 6 * RED - 7.5 * BLUE + 1)', | |
| {'NIR': image.select('B8'), 'RED': image.select('B4'), 'BLUE': image.select('B2')} | |
| ).rename('EVI') | |
| ndmi = image.normalizedDifference(['B8', 'B11']).rename('NDMI') | |
| msi = image.expression('SWIR1 / NIR', {'SWIR1': image.select('B11'), 'NIR': image.select('B8')}).rename('MSI') | |
| lai_expr = ndvi.multiply(3.5).clamp(0,8) | |
| lai = lai_expr.rename('LAI') | |
| green_safe = image.select('B3').max(ee.Image(0.0001)) | |
| red_safe = image.select('B4').max(ee.Image(0.0001)) | |
| cvi = image.expression('(NIR / GREEN) * (RED / GREEN)', | |
| {'NIR': image.select('B8'), 'GREEN': green_safe, 'RED': red_safe} | |
| ).rename('CVI') | |
| return image.addBands([ndvi, evi, ndmi, msi, lai, cvi]) | |
| def get_processed_image(_geometry, start_date, end_date, index_name): | |
| try: | |
| s2_sr_col = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED') | |
| .filterBounds(_geometry) | |
| .filterDate(start_date, end_date) | |
| .map(maskS2clouds)) | |
| count = s2_sr_col.size().getInfo() | |
| if count == 0: | |
| return None, f"تصویر بدون ابری در بازه {start_date} تا {end_date} یافت نشد." | |
| indexed_col = s2_sr_col.map(add_indices) | |
| median_image = indexed_col.median() | |
| if index_name not in median_image.bandNames().getInfo(): | |
| return None, f"شاخص '{index_name}' پس از پردازش در تصویر میانه یافت نشد." | |
| output_image = median_image.select(index_name) | |
| return output_image, None | |
| except ee.EEException as e: | |
| error_message = f"خطای Google Earth Engine: {e}" | |
| error_details = e.args[0] if e.args else str(e) | |
| if isinstance(error_details, str): | |
| if 'computation timed out' in error_details.lower(): | |
| error_message += "\n(احتمالاً به دلیل حجم بالای پردازش یا بازه زمانی طولانی)" | |
| elif 'user memory limit exceeded' in error_details.lower(): | |
| error_message += "\n(احتمالاً به دلیل پردازش منطقه بزرگ یا عملیات پیچیده)" | |
| return None, error_message | |
| except Exception as e: | |
| return None, f"خطای ناشناخته در پردازش GEE: {e}\n{traceback.format_exc()}" | |
| def get_index_time_series(_geometry, index_name, start_date_str, end_date_str): | |
| try: | |
| s2_sr_col = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED') | |
| .filterBounds(_geometry) | |
| .filterDate(start_date_str, end_date_str) | |
| .map(maskS2clouds) | |
| .map(add_indices)) | |
| def extract_value(image): | |
| value = ee.Algorithms.If( | |
| image.bandNames().contains(index_name), | |
| image.reduceRegion( | |
| reducer=ee.Reducer.mean(), | |
| geometry=_geometry, | |
| scale=10, | |
| maxPixels=1e9 | |
| ).get(index_name), | |
| None | |
| ) | |
| return ee.Feature(None, {'date': image.date().format('YYYY-MM-dd'), index_name: value}) | |
| ts_features = s2_sr_col.map(extract_value).filter(ee.Filter.notNull([index_name])) | |
| ts_info = ts_features.getInfo()['features'] | |
| if not ts_info: | |
| return pd.DataFrame(columns=['date', index_name]), "دادهای برای سری زمانی یافت نشد." | |
| ts_data = [{'date': f['properties']['date'], index_name: f['properties'][index_name]} for f in ts_info if f['properties'] and f['properties'][index_name] is not None] | |
| if not ts_data: | |
| return pd.DataFrame(columns=['date', index_name]), "داده معتبری برای سری زمانی یافت نشد." | |
| ts_df = pd.DataFrame(ts_data) | |
| ts_df['date'] = pd.to_datetime(ts_df['date']) | |
| ts_df = ts_df.sort_values('date').set_index('date') | |
| return ts_df, None | |
| except ee.EEException as e: | |
| return pd.DataFrame(columns=['date', index_name]), f"خطای GEE در دریافت سری زمانی: {e}" | |
| except Exception as e: | |
| return pd.DataFrame(columns=['date', index_name]), f"خطای ناشناخته در دریافت سری زمانی: {e}\n{traceback.format_exc()}" | |
| # ============================================================================== | |
| # Determine active farm geometry | |
| # ============================================================================== | |
| active_farm_geom = None | |
| active_farm_centroid_for_point_ops = None # For operations needing a point (e.g., time series) | |
| active_farm_name_display = selected_farm_name | |
| active_farm_area_ha_display = "N/A" # Default, as 'مساحت' might not be in CSV or calculated yet | |
| def get_farm_polygon_ee(farm_row): | |
| try: | |
| geom = farm_row['geometry'] | |
| if geom['type'] == 'Polygon': | |
| coords = geom['coordinates'] | |
| return ee.Geometry.Polygon(coords) | |
| return None | |
| except Exception as e: | |
| return None | |
| if selected_farm_name == "همه مزارع": | |
| if not filtered_farms_df.empty: | |
| # For "همه مزارع", use a bounding box of the centroids of all farms in the filtered list | |
| # These centroids ('centroid_lon', 'centroid_lat') were calculated in load_farm_data using WGS84 | |
| min_lon_df = filtered_farms_df['centroid_lon'].min() | |
| min_lat_df = filtered_farms_df['centroid_lat'].min() | |
| max_lon_df = filtered_farms_df['centroid_lon'].max() | |
| max_lat_df = filtered_farms_df['centroid_lat'].max() | |
| if pd.notna(min_lon_df) and pd.notna(min_lat_df) and pd.notna(max_lon_df) and pd.notna(max_lat_df): | |
| try: | |
| active_farm_geom = ee.Geometry.Rectangle([min_lon_df, min_lat_df, max_lon_df, max_lat_df]) | |
| active_farm_centroid_for_point_ops = active_farm_geom.centroid(maxError=1) | |
| except Exception as e_bbox: | |
| st.error(f"خطا در ایجاد محدوده کلی مزارع: {e_bbox}") | |
| active_farm_geom = None | |
| active_farm_centroid_for_point_ops = None | |
| else: # A single farm is selected | |
| selected_farm_details_active_df = filtered_farms_df[filtered_farms_df['مزرعه'] == selected_farm_name] | |
| if not selected_farm_details_active_df.empty: | |
| farm_row_active = selected_farm_details_active_df.iloc[0] | |
| active_farm_geom = get_farm_polygon_ee(farm_row_active) # This is now an ee.Geometry.Polygon | |
| if active_farm_geom: | |
| active_farm_centroid_for_point_ops = active_farm_geom.centroid(maxError=1) | |
| try: | |
| # Try to calculate area using GEE for the selected polygon | |
| area_m2 = active_farm_geom.area(maxError=1).getInfo() | |
| if area_m2 is not None: | |
| active_farm_area_ha_display = area_m2 / 10000.0 | |
| else: | |
| active_farm_area_ha_display = "محاسبه نشد" # GEE returned None for area | |
| except Exception as e_area: | |
| active_farm_area_ha_display = "خطا در محاسبه" # Error during GEE call | |
| else: | |
| active_farm_area_ha_display = "هندسه نامعتبر" | |
| else: # Should not happen if farm name is from dropdown | |
| st.warning(f"جزئیات مزرعه '{selected_farm_name}' در لیست فیلتر شده یافت نشد.") | |
| # ============================================================================== | |
| # Main Panel Display | |
| # ============================================================================== | |
| tab_titles = ["📊 داشبورد اصلی", "🗺️ نقشه و نمودارها", "💡 تحلیل هوشمند"] | |
| # Add icons to tab titles (experimental, might not work perfectly on all browsers/versions) | |
| # tab_icons = ["📊", "🗺️", "💡"] | |
| # tab_titles_with_icons = [f"{icon} {title}" for icon, title in zip(tab_icons, tab_titles)] | |
| # tab1, tab2, tab3 = st.tabs(tab_titles_with_icons) | |
| tab1, tab2, tab3 = st.tabs(tab_titles) | |
| with tab1: | |
| # Modern Gradient Card (Tab1) | |
| st.markdown(f""" | |
| <div class='modern-gradient-card'> | |
| <span class='icon'>🌱</span> | |
| <div> | |
| <div style='font-size:1.25em; font-weight:600;'>وضعیت کلی مزارع</div> | |
| <div style='font-size:1.05em;'>تعداد کل مزارع: <b>{len(filtered_farms_df)}</b></div> | |
| <div style='font-size:0.98em;'>روز انتخابی: <b>{selected_day}</b></div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with st.container(): | |
| st.markdown("<div class='section-container'>", unsafe_allow_html=True) | |
| if selected_farm_name == "همه مزارع": | |
| st.subheader(f"📋 نمایش کلی مزارع برای روز: {selected_day}") | |
| st.info(f"تعداد مزارع در این روز: {len(filtered_farms_df)}") | |
| else: | |
| selected_farm_details_tab1 = filtered_farms_df[filtered_farms_df['مزرعه'] == selected_farm_name].iloc[0] | |
| st.subheader(f"📋 جزئیات مزرعه: {selected_farm_name} (روز: {selected_day})") | |
| cols_details = st.columns([1,1,1]) | |
| with cols_details[0]: | |
| # استفاده از مساحت دقیق محاسبه شده در GEE | |
| farm_area_display = selected_farm_details_tab1.get('مساحت') | |
| if pd.notna(farm_area_display) and isinstance(farm_area_display, (int, float)): | |
| st.metric("مساحت (هکتار)", f"{farm_area_display:,.2f}") | |
| else: | |
| st.metric("مساحت (هکتار)", "N/A") | |
| with cols_details[1]: | |
| st.metric("واریته", f"{selected_farm_details_tab1.get('واریته', 'N/A')}") | |
| with cols_details[2]: | |
| # 'کانال' is not in new CSV. Using 'اداره' or 'گروه' if available. | |
| admin_val = selected_farm_details_tab1.get('اداره', 'N/A') | |
| group_val = selected_farm_details_tab1.get('گروه', 'N/A') | |
| st.metric("اداره/گروه", f"{admin_val} / {group_val}") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown("<div class='section-container'>", unsafe_allow_html=True) | |
| st.subheader(f"📈 جدول رتبهبندی مزارع بر اساس {index_options[selected_index]} (روز: {selected_day})") | |
| st.caption("مقایسه مقادیر متوسط شاخص (نقاط مرکزی CSV) در هفته جاری با هفته قبل.") | |
| def calculate_weekly_indices_for_ranking_table(_farms_df, index_name_calc, start_curr, end_curr, start_prev, end_prev): | |
| results = [] | |
| errors = [] | |
| total_farms = len(_farms_df) | |
| prog_bar = st.progress(0, text="شروع پردازش مزارع...") | |
| for i, (idx, farm) in enumerate(_farms_df.iterrows()): | |
| prog_bar.progress((i + 1) / total_farms, text=f"پردازش مزرعه {i+1}/{total_farms}: {farm['مزرعه']}") | |
| farm_name_calc = farm['مزرعه'] | |
| # Create polygon and then get centroid for point-based GEE analysis | |
| farm_polygon_for_calc = get_farm_polygon_ee(farm) | |
| if not farm_polygon_for_calc: | |
| errors.append(f"خطا در ایجاد هندسه برای {farm_name_calc} در جدول رتبهبندی.") | |
| results.append({ | |
| 'مزرعه': farm_name_calc, | |
| 'اداره': farm.get('اداره', 'N/A'), | |
| 'گروه': farm.get('گروه', 'N/A'), | |
| 'مساحت (هکتار)': farm.get('مساحت', 'N/A'), | |
| f'{index_name_calc} (هفته جاری)': None, | |
| f'{index_name_calc} (هفته قبل)': None, | |
| 'تغییر': None | |
| }) | |
| continue | |
| # استفاده از هندسه کامل مزرعه برای محاسبه شاخص دقیق بجای نقطه مرکزی | |
| farm_area_ha = farm.get('مساحت') | |
| def get_mean_value(start_dt, end_dt): | |
| try: | |
| image_calc, error_calc = get_processed_image(farm_polygon_for_calc, start_dt, end_dt, index_name_calc) | |
| if image_calc: | |
| # استفاده از کل پلیگون برای محاسبه میانگین بجای نقطه مرکزی | |
| mean_dict = image_calc.reduceRegion( | |
| reducer=ee.Reducer.mean(), | |
| geometry=farm_polygon_for_calc, | |
| scale=10, | |
| maxPixels=1e9 | |
| ).getInfo() | |
| return mean_dict.get(index_name_calc), None | |
| return None, error_calc | |
| except Exception as e_reduce: return None, f"خطا در {farm_name_calc} ({start_dt}-{end_dt}): {e_reduce}" | |
| current_val, err_curr = get_mean_value(start_curr, end_curr) | |
| if err_curr: errors.append(f"{farm_name_calc} (جاری): {err_curr}") | |
| previous_val, err_prev = get_mean_value(start_prev, end_prev) | |
| if err_prev: errors.append(f"{farm_name_calc} (قبلی): {err_prev}") | |
| change = float(current_val) - float(previous_val) if current_val is not None and previous_val is not None else None | |
| results.append({ | |
| 'مزرعه': farm_name_calc, | |
| 'اداره': farm.get('اداره', 'N/A'), # 'اداره' is in new CSV | |
| 'گروه': farm.get('گروه', 'N/A'), # 'گروه' is in new CSV | |
| 'مساحت (هکتار)': farm_area_ha, | |
| f'{index_name_calc} (هفته جاری)': current_val, | |
| f'{index_name_calc} (هفته قبل)': previous_val, | |
| 'تغییر': change | |
| }) | |
| prog_bar.empty() | |
| return pd.DataFrame(results), errors | |
| ranking_df, calculation_errors = calculate_weekly_indices_for_ranking_table( | |
| filtered_farms_df, selected_index, | |
| start_date_current_str, end_date_current_str, | |
| start_date_previous_str, end_date_previous_str | |
| ) | |
| if calculation_errors: | |
| with st.expander("⚠️ مشاهده خطاهای محاسبه شاخصها", expanded=False): | |
| for error_item in calculation_errors: st.caption(f"- {error_item}") | |
| ranking_df_sorted = pd.DataFrame() | |
| if not ranking_df.empty: | |
| ascending_sort = selected_index in ['MSI'] | |
| ranking_df_sorted = ranking_df.sort_values( | |
| by=f'{selected_index} (هفته جاری)', ascending=ascending_sort, na_position='last' | |
| ).reset_index(drop=True) | |
| ranking_df_sorted.index = ranking_df_sorted.index + 1 | |
| ranking_df_sorted.index.name = 'رتبه' | |
| def determine_status_html(row, index_name_col_status): | |
| # ... (function copied from previous, no changes) | |
| change_val_status = row['تغییر'] | |
| current_val_status = row[f'{index_name_col_status} (هفته جاری)'] | |
| prev_val_status = row[f'{index_name_col_status} (هفته قبل)'] | |
| if pd.isna(change_val_status) or pd.isna(current_val_status) or pd.isna(prev_val_status): | |
| return "<span class='status-badge status-neutral'>بدون داده</span>" | |
| try: change_val_status = float(change_val_status) | |
| except (ValueError, TypeError): return "<span class='status-badge status-neutral'>خطا در داده</span>" | |
| threshold_status = 0.05 | |
| if index_name_col_status in ['NDVI', 'EVI', 'LAI', 'CVI', 'NDMI']: | |
| if change_val_status > threshold_status: return "<span class='status-badge status-positive'>رشد/بهبود</span>" | |
| elif change_val_status < -threshold_status: return "<span class='status-badge status-negative'>تنش/کاهش</span>" | |
| else: return "<span class='status-badge status-neutral'>ثابت</span>" | |
| elif index_name_col_status in ['MSI']: | |
| if change_val_status < -threshold_status: return "<span class='status-badge status-positive'>بهبود (تنش کمتر)</span>" | |
| elif change_val_status > threshold_status: return "<span class='status-badge status-negative'>تنش بیشتر</span>" | |
| else: return "<span class='status-badge status-neutral'>ثابت</span>" | |
| return "<span class='status-badge status-neutral'>نامشخص</span>" | |
| ranking_df_sorted['وضعیت'] = ranking_df_sorted.apply(lambda row: determine_status_html(row, selected_index), axis=1) | |
| df_display = ranking_df_sorted.copy() | |
| cols_to_format_display = [f'{selected_index} (هفته جاری)', f'{selected_index} (هفته قبل)', 'تغییر', 'مساحت (هکتار)'] | |
| for col_fmt_dsp in cols_to_format_display: | |
| if col_fmt_dsp in df_display.columns: | |
| df_display[col_fmt_dsp] = df_display[col_fmt_dsp].apply(lambda x: f"{float(x):.3f}" if pd.notna(x) and isinstance(x, (int, float)) else ("N/A" if pd.isna(x) else str(x))) | |
| st.markdown(f"<div class='dataframe-container'>{df_display.to_html(escape=False, index=True, classes='styled-table')}</div>", unsafe_allow_html=True) | |
| st.subheader("📊 خلاصه وضعیت مزارع") | |
| count_positive_summary = sum(1 for s in ranking_df_sorted['وضعیت'] if 'status-positive' in s) | |
| count_neutral_summary = sum(1 for s in ranking_df_sorted['وضعیت'] if 'status-neutral' in s and 'بدون داده' not in s and 'خطا' not in s) | |
| count_negative_summary = sum(1 for s in ranking_df_sorted['وضعیت'] if 'status-negative' in s) | |
| count_nodata_summary = sum(1 for s in ranking_df_sorted['وضعیت'] if 'بدون داده' in s or 'خطا' in s or 'نامشخص' in s) | |
| col1_sum, col2_sum, col3_sum, col4_sum = st.columns(4) | |
| with col1_sum: st.metric("🟢 بهبود/رشد", count_positive_summary) | |
| with col2_sum: st.metric("⚪ ثابت", count_neutral_summary) | |
| with col3_sum: st.metric("🔴 تنش/کاهش", count_negative_summary) | |
| with col4_sum: st.metric("❔ بدون داده/خطا", count_nodata_summary) | |
| st.info("""**توضیحات وضعیت:** 🟢 بهبود/رشد ⚪ ثابت 🔴 تنش/کاهش ❔ بدون داده/خطا""") | |
| def extract_status_text(html_badge): | |
| # ... (function copied from previous, no changes) | |
| if 'رشد/بهبود' in html_badge: return 'رشد/بهبود' | |
| if 'تنش کمتر' in html_badge: return 'بهبود (تنش کمتر)' | |
| if 'ثابت' in html_badge: return 'ثابت' | |
| if 'تنش/کاهش' in html_badge: return 'تنش/کاهش' | |
| if 'تنش شدید' in html_badge: return 'تنش شدید' | |
| if 'بدون داده' in html_badge: return 'بدون داده' | |
| if 'خطا در داده' in html_badge: return 'خطا در داده' | |
| return 'نامشخص' | |
| csv_data_dl = ranking_df_sorted.copy() | |
| csv_data_dl['وضعیت'] = csv_data_dl['وضعیت'].apply(extract_status_text) | |
| csv_output = csv_data_dl.to_csv(index=True).encode('utf-8-sig') | |
| st.download_button( | |
| label="📥 دانلود جدول رتبهبندی (CSV)", data=csv_output, | |
| file_name=f'ranking_{selected_index}_{selected_day}_{end_date_current_str}.csv', mime='text/csv', | |
| ) | |
| else: | |
| st.info(f"دادهای برای جدول رتبهبندی بر اساس {selected_index} در این بازه زمانی یافت نشد.") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| with tab2: | |
| # Glassmorphism Card + Floating Action Button (Tab2) | |
| st.markdown(f""" | |
| <div class='glass-card'> | |
| <span class='glass-icon'>🗺️</span> | |
| <div> | |
| <div style='font-size:1.18em; font-weight:600;'>نمایش نقشه و نمودارها</div> | |
| <div style='font-size:1em;'>شاخص انتخابی: <b>{index_options[selected_index]}</b></div> | |
| <div style='font-size:0.95em;'>مزرعه: <b>{active_farm_name_display}</b></div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| vis_params_map = { # Same as before | |
| 'NDVI': {'min': 0.0, 'max': 0.9, 'palette': ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#d9ef8b', '#a6d96a', '#66bd63', '#1a9850', '#006837']}, | |
| 'EVI': {'min': 0.0, 'max': 0.9, 'palette': ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#d9ef8b', '#a6d96a', '#66bd63', '#1a9850', '#006837']}, | |
| 'NDMI': {'min': -0.5, 'max': 0.8, 'palette': ['#8c510a', '#bf812d', '#dfc27d', '#f6e8c3', '#f5f5f5', '#c7eae5', '#80cdc1', '#35978f', '#01665e']}, | |
| 'LAI': {'min': 0, 'max': 7, 'palette': ['#FFFFE5', '#FFF7BC', '#FEE391', '#FEC44F', '#FE9929', '#EC7014', '#CC4C02', '#993404', '#662506']}, | |
| 'MSI': {'min': 0.2, 'max': 3.0, 'palette': ['#01665e', '#35978f', '#80cdc1', '#c7eae5', '#f5f5f5', '#f6e8c3', '#dfc27d', '#bf812d', '#8c510a']}, | |
| 'CVI': {'min': 0, 'max': 25, 'palette': ['#FFFFE5', '#FFF7BC', '#FEE391', '#FEC44F', '#FE9929', '#EC7014', '#CC4C02', '#993404', '#662506']}, | |
| } | |
| map_center_lat_folium, map_center_lon_folium, initial_zoom_map_val_folium = INITIAL_LAT, INITIAL_LON, INITIAL_ZOOM | |
| if active_farm_geom: # This is now a polygon for single farm, or bounding box for all | |
| try: | |
| # Center map on the centroid of the active geometry (polygon or bounding box) | |
| if active_farm_geom.coordinates(): # Check if coordinates exist (it might be an empty geometry if creation failed) | |
| centroid_coords = active_farm_geom.centroid(maxError=1).coordinates().getInfo() | |
| map_center_lon_folium, map_center_lat_folium = centroid_coords[0], centroid_coords[1] | |
| if selected_farm_name != "همه مزارع": # Single farm selected (polygon) | |
| initial_zoom_map_val_folium = 15 # Zoom closer for a single farm polygon | |
| # else: "همه مزارع" (bounding box), use default INITIAL_ZOOM or adjust based on bounds | |
| except Exception: pass # Keep initial map center on error (e.g. if getInfo fails) | |
| m = geemap.Map(location=[map_center_lat_folium, map_center_lon_folium], zoom=initial_zoom_map_val_folium, add_google_map=True) | |
| m.add_basemap("HYBRID") | |
| m.add_basemap("SATELLITE") | |
| if active_farm_geom: | |
| gee_image_current_map, error_msg_current_map = get_processed_image( | |
| active_farm_geom, start_date_current_str, end_date_current_str, selected_index | |
| ) | |
| if gee_image_current_map: | |
| try: | |
| m.addLayer( | |
| gee_image_current_map, vis_params_map.get(selected_index, {}), | |
| f"{selected_index} ({start_date_current_str} to {end_date_current_str})" | |
| ) | |
| palette_map_lgd = vis_params_map.get(selected_index, {}).get('palette', []) # Legend logic same as before | |
| legend_html_content = "" | |
| if palette_map_lgd: | |
| if selected_index in ['NDVI', 'EVI', 'LAI', 'CVI']: | |
| legend_html_content = f'<p style="margin:0; background-color:{palette_map_lgd[-1]}; color:white; padding: 2px 5px; border-radius:3px;">بالا (مطلوب)</p>' \ | |
| f'<p style="margin:0; background-color:{palette_map_lgd[len(palette_map_lgd)//2]}; color:black; padding: 2px 5px; border-radius:3px;">متوسط</p>' \ | |
| f'<p style="margin:0; background-color:{palette_map_lgd[0]}; color:white; padding: 2px 5px; border-radius:3px;">پایین (نامطلوب)</p>' | |
| elif selected_index == 'NDMI': | |
| legend_html_content = f'<p style="margin:0; background-color:{palette_map_lgd[-1]}; color:white; padding: 2px 5px; border-radius:3px;">مرطوب</p>' \ | |
| f'<p style="margin:0; background-color:{palette_map_lgd[0]}; color:black; padding: 2px 5px; border-radius:3px;">خشک</p>' | |
| elif selected_index == 'MSI': | |
| legend_html_content = f'<p style="margin:0; background-color:{palette_map_lgd[0]}; color:white; padding: 2px 5px; border-radius:3px;">تنش کم (مرطوب)</p>' \ | |
| f'<p style="margin:0; background-color:{palette_map_lgd[-1]}; color:black; padding: 2px 5px; border-radius:3px;">تنش زیاد (خشک)</p>' | |
| if legend_html_content: | |
| legend_title_map = index_options[selected_index].split('(')[0].strip() | |
| legend_html = f''' | |
| <div style="position: fixed; bottom: 50px; left: 10px; width: auto; | |
| background-color: var(--container-background-color); opacity: 0.85; z-index:1000; padding: 10px; border-radius:8px; | |
| font-family: 'Vazirmatn', sans-serif; font-size: 0.9em; box-shadow: 0 2px 5px rgba(0,0,0,0.2); color: var(--text-color);"> | |
| <p style="margin:0 0 8px 0; font-weight:bold; color:var(--primary-color);">راهنمای {legend_title_map}</p> | |
| {legend_html_content} | |
| </div>''' | |
| m.get_root().html.add_child(folium.Element(legend_html)) | |
| if active_farm_name_display == "همه مزارع": | |
| for _, farm_row_map in filtered_farms_df.iterrows(): | |
| # Display marker at centroid for "همه مزارع" view | |
| # Centroids were pre-calculated in load_farm_data for pandas df (now WGS84) | |
| centroid_lon_map = farm_row_map.get('centroid_lon') | |
| centroid_lat_map = farm_row_map.get('centroid_lat') | |
| if pd.notna(centroid_lon_map) and pd.notna(centroid_lat_map): | |
| folium.Marker( | |
| location=[centroid_lat_map, centroid_lon_map], | |
| popup=f"<b>{farm_row_map['مزرعه']}</b><br>اداره: {farm_row_map.get('اداره', 'N/A')}<br>گروه: {farm_row_map.get('گروه', 'N/A')}", | |
| tooltip=farm_row_map['مزرعه'], icon=folium.Icon(color='royalblue', icon='leaf', prefix='fa') | |
| ).add_to(m) | |
| # For a single selected farm, its boundary is drawn. A marker at centroid can also be added if desired. | |
| elif selected_farm_name != "همه مزارع" and active_farm_centroid_for_point_ops: | |
| try: | |
| point_coords_map = active_farm_centroid_for_point_ops.coordinates().getInfo() | |
| folium.Marker( | |
| location=[point_coords_map[1], point_coords_map[0]], tooltip=f"مرکز مزرعه: {active_farm_name_display}", | |
| icon=folium.Icon(color='crimson', icon='map-marker', prefix='fa') | |
| ).add_to(m) | |
| except Exception as e_marker: | |
| st.caption(f"نکته: نتوانست نشانگر مرکز مزرعه را اضافه کند: {e_marker}") | |
| m.add_layer_control() | |
| except Exception as map_err: st.error(f"خطا در افزودن لایه به نقشه: {map_err}\n{traceback.format_exc()}") | |
| else: st.warning(f"تصویری برای نمایش روی نقشه یافت نشد. {error_msg_current_map}") | |
| st_folium(m, width=None, height=500, use_container_width=True, returned_objects=[]) | |
| else: st.warning("هندسه مزرعه برای نمایش نقشه انتخاب نشده است.") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown("<div class='section-container'>", unsafe_allow_html=True) | |
| st.subheader(f"📊 نمودار روند زمانی شاخص {index_options[selected_index]} برای '{active_farm_name_display}'") | |
| if active_farm_name_display == "همه مزارع": | |
| st.info("لطفاً یک مزرعه خاص را برای نمایش نمودار سری زمانی انتخاب کنید.") | |
| # Check if a single farm is selected AND its geometry is available for GEE operations | |
| elif selected_farm_name != "همه مزارع" and active_farm_geom: | |
| ts_end_date_chart = today.strftime('%Y-%m-%d') | |
| ts_start_date_chart_user = st.date_input("تاریخ شروع برای سری زمانی:", | |
| value=today - datetime.timedelta(days=365), | |
| min_value=datetime.date(2017,1,1), max_value=today - datetime.timedelta(days=30), | |
| key="ts_start_date_chart", help="بازه زمانی حداقل ۳۰ روز و حداکثر ۲ سال توصیه میشود." | |
| ) | |
| if st.button("📈 نمایش/بهروزرسانی نمودار سری زمانی", key="btn_ts_chart_show"): | |
| max_days_chart = 365 * 2 | |
| if (today - ts_start_date_chart_user).days > max_days_chart: | |
| st.warning(f"بازه زمانی به ۲ سال محدود شد.") | |
| ts_start_date_chart_user = today - datetime.timedelta(days=max_days_chart) | |
| with st.spinner(f"⏳ در حال دریافت و ترسیم سری زمانی..."): | |
| ts_df_chart, ts_error_chart = get_index_time_series( | |
| active_farm_geom, selected_index, # استفاده از کل پلیگون برای محاسبه دقیق | |
| start_date_str=ts_start_date_chart_user.strftime('%Y-%m-%d'), | |
| end_date_str=ts_end_date_chart | |
| ) | |
| if ts_error_chart: st.warning(f"خطا در دریافت دادههای سری زمانی: {ts_error_chart}") | |
| elif not ts_df_chart.empty: | |
| fig_chart = px.line(ts_df_chart, y=selected_index, markers=True, | |
| title=f"روند زمانی {index_options[selected_index]} برای '{active_farm_name_display}'", | |
| labels={'date': 'تاریخ', selected_index: index_options[selected_index]}) | |
| fig_chart.update_layout( | |
| font=dict(family="Vazirmatn", color="var(--text-color)"), | |
| xaxis_title="تاریخ", yaxis_title=index_options[selected_index], | |
| plot_bgcolor="var(--container-background-color)", | |
| paper_bgcolor="var(--container-background-color)", | |
| hovermode="x unified" | |
| ) | |
| fig_chart.update_traces(line=dict(color="var(--accent-color)", width=2.5), marker=dict(color="var(--primary-color)", size=6)) | |
| st.plotly_chart(fig_chart, use_container_width=True) | |
| else: st.info(f"دادهای برای نمایش نمودار سری زمانی {selected_index} یافت نشد.") | |
| else: # Handles "همه مزارع" or if single farm's geometry could not be determined | |
| st.warning("نمودار سری زمانی فقط برای مزارع منفرد (با هندسه معتبر) قابل نمایش است.") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| with tab3: | |
| # AI Fade-in Card (Tab3) | |
| st.markdown(""" | |
| <div class='ai-fadein-card'> | |
| <span class='ai-icon'>🤖</span> | |
| <div> | |
| <div style='font-size:1.18em; font-weight:600;'>تحلیل هوشمند با Gemini</div> | |
| <div style='font-size:1em;'>پاسخهای هوشمند و گزارشهای تحلیلی با هوش مصنوعی</div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if not gemini_model: | |
| st.warning("⚠️ قابلیتهای هوشمند Gemini با وارد کردن صحیح کلید API در کد فعال میشوند.") | |
| else: | |
| # --- Data Preparation for Tab 3 --- | |
| # Ensure ranking_df and its summaries are available for Gemini analyses in tab3 | |
| # It will use cache if already computed in tab1 | |
| # Active variables from sidebar: filtered_farms_df, selected_index, | |
| # start_date_current_str, end_date_current_str, start_date_previous_str, end_date_previous_str | |
| # Make sure all variables needed by calculate_weekly_indices_for_ranking_table are defined | |
| # These should be available from the sidebar scope | |
| # Example: filtered_farms_df, selected_index, start_date_current_str, etc. | |
| ranking_df_tab3, calculation_errors_tab3 = calculate_weekly_indices_for_ranking_table( | |
| filtered_farms_df, selected_index, | |
| start_date_current_str, end_date_current_str, | |
| start_date_previous_str, end_date_previous_str | |
| ) | |
| ranking_df_sorted_tab3 = pd.DataFrame() | |
| count_positive_summary_tab3 = 0 | |
| count_neutral_summary_tab3 = 0 | |
| count_negative_summary_tab3 = 0 | |
| count_nodata_summary_tab3 = 0 | |
| if not ranking_df_tab3.empty: | |
| ascending_sort_tab3 = selected_index in ['MSI'] # True for MSI (lower is better for change, but higher value is worse) | |
| # For ranking, we sort by current value. For 'critical', we might look at 'change'. | |
| # Create a temporary column for sorting by 'change' if it's a positive-is-good index, to get worst changes first | |
| # Or for negative-is-good index, to get worst (largest positive) changes first. | |
| # This is complex. Let's stick to sorting by current value for now, Gemini prompt will handle selection. | |
| ranking_df_sorted_tab3 = ranking_df_tab3.sort_values( | |
| by=f'{selected_index} (هفته جاری)', ascending=ascending_sort_tab3, na_position='last' | |
| ).reset_index(drop=True) | |
| ranking_df_sorted_tab3.index = ranking_df_sorted_tab3.index + 1 # Start ranking from 1 | |
| ranking_df_sorted_tab3.index.name = 'رتبه' | |
| # Add HTML status for display and text status for prompts | |
| ranking_df_sorted_tab3['وضعیت_html'] = ranking_df_sorted_tab3.apply(lambda row: determine_status_html(row, selected_index), axis=1) | |
| ranking_df_sorted_tab3['وضعیت'] = ranking_df_sorted_tab3['وضعیت_html'].apply(extract_status_text) | |
| # Recalculate summary counts for tab3 context | |
| count_positive_summary_tab3 = sum(1 for s in ranking_df_sorted_tab3['وضعیت_html'] if 'status-positive' in s) | |
| count_neutral_summary_tab3 = sum(1 for s in ranking_df_sorted_tab3['وضعیت_html'] if 'status-neutral' in s and 'بدون داده' not in s and 'خطا' not in s) | |
| count_negative_summary_tab3 = sum(1 for s in ranking_df_sorted_tab3['وضعیت_html'] if 'status-negative' in s) | |
| count_nodata_summary_tab3 = sum(1 for s in ranking_df_sorted_tab3['وضعیت_html'] if 'بدون داده' in s or 'خطا' in s or 'نامشخص' in s) | |
| else: | |
| # Ensure essential columns exist even if empty for downstream code | |
| essential_cols = ['مزرعه', 'وضعیت_html', 'وضعیت', f'{selected_index} (هفته جاری)', f'{selected_index} (هفته قبل)', 'تغییر'] | |
| ranking_df_sorted_tab3 = pd.DataFrame(columns=essential_cols) | |
| count_nodata_summary_tab3 = len(filtered_farms_df) if filtered_farms_df is not None else 0 | |
| # --- Shared Context Strings for Gemini in Tab 3 --- | |
| farm_details_for_gemini_tab3 = "" | |
| analysis_basis_str_gemini_tab3 = "تحلیل شاخصها بر اساس کل مساحت مزارع انجام میشود و از مرز دقیق هندسی هر مزرعه (چندضلعی) برای محاسبه استفاده میشود. این روش نسبت به استفاده از نقطه مرکزی، دقت بیشتری دارد." # Updated basis | |
| if active_farm_name_display != "همه مزارع": | |
| farm_details_for_gemini_tab3 = f"مزرعه مورد نظر: '{active_farm_name_display}'.\n" | |
| # active_farm_area_ha_display is now "N/A" or GEE calculated. | |
| if isinstance(active_farm_area_ha_display, (int, float)): | |
| farm_details_for_gemini_tab3 += f"مساحت محاسبهشده (تخمینی با GEE): {active_farm_area_ha_display:,.2f} هکتار.\n" | |
| else: # Could be "N/A", "خطا در محاسبه", etc. | |
| farm_details_for_gemini_tab3 += f"مساحت: {active_farm_area_ha_display}.\n" | |
| # Get other details like 'واریته', 'اداره', 'گروه', 'سن' if available from filtered_farms_df | |
| if filtered_farms_df is not None and not filtered_farms_df.empty: | |
| csv_farm_details_tab3_series_df = filtered_farms_df[filtered_farms_df['مزرعه'] == active_farm_name_display] | |
| if not csv_farm_details_tab3_series_df.empty: | |
| csv_farm_detail_row = csv_farm_details_tab3_series_df.iloc[0] | |
| farm_details_for_gemini_tab3 += f"واریته (از CSV): {csv_farm_detail_row.get('واریته', 'N/A')}.\n" | |
| farm_details_for_gemini_tab3 += f"اداره (از CSV): {csv_farm_detail_row.get('اداره', 'N/A')}.\n" | |
| farm_details_for_gemini_tab3 += f"گروه (از CSV): {csv_farm_detail_row.get('گروه', 'N/A')}.\n" | |
| farm_details_for_gemini_tab3 += f"سن (از CSV): {csv_farm_detail_row.get('سن', 'N/A')}.\n" | |
| # --- 1. Intelligent Q&A --- | |
| # Wrap this section in gemini-card and add a header | |
| st.markdown("<div class='gemini-card'>", unsafe_allow_html=True) # Start gemini-card | |
| with st.expander("💬 پرسش و پاسخ هوشمند", expanded=True): | |
| st.markdown(""" | |
| <div class='gemini-card-header'> | |
| <div class='gemini-card-icon'>💬</div> | |
| <h3 class='gemini-card-title'>پرسش و پاسخ هوشمند</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("##### سوال خود را در مورد وضعیت عمومی مزارع یا یک مزرعه خاص بپرسید.") # Keep descriptive text | |
| user_farm_q_gemini = st.text_area( | |
| f"سوال شما درباره '{active_farm_name_display}' یا مزارع روز '{selected_day}' (شاخص: {index_options[selected_index]}):", | |
| key="gemini_farm_q_text_tab3", | |
| height=100 | |
| ) | |
| if st.button("✉️ ارسال سوال به Gemini", key="btn_gemini_farm_q_send_tab3"): | |
| if not user_farm_q_gemini: | |
| st.info("لطفاً سوال خود را وارد کنید.") | |
| else: | |
| prompt_gemini_q = f"شما یک دستیار هوشمند برای تحلیل دادههای کشاورزی نیشکر هستید. {analysis_basis_str_gemini_tab3}\n" | |
| context_data_gemini_q = "" | |
| if active_farm_name_display != "همه مزارع": | |
| context_data_gemini_q += farm_details_for_gemini_tab3 | |
| farm_data_for_prompt_q = pd.DataFrame() | |
| if not ranking_df_sorted_tab3.empty: | |
| farm_data_for_prompt_q = ranking_df_sorted_tab3[ranking_df_sorted_tab3['مزرعه'] == active_farm_name_display] | |
| if not farm_data_for_prompt_q.empty: | |
| current_farm_data = farm_data_for_prompt_q.iloc[0] | |
| status_text_gemini_q = current_farm_data['وضعیت'] | |
| current_val_str_gemini_q = f"{current_farm_data[f'{selected_index} (هفته جاری)']:.3f}" if pd.notna(current_farm_data[f'{selected_index} (هفته جاری)']) else "N/A" | |
| prev_val_str_gemini_q = f"{current_farm_data[f'{selected_index} (هفته قبل)']:.3f}" if pd.notna(current_farm_data[f'{selected_index} (هفته قبل)']) else "N/A" | |
| change_str_gemini_q = f"{current_farm_data['تغییر']:.3f}" if pd.notna(current_farm_data['تغییر']) else "N/A" | |
| context_data_gemini_q += ( | |
| f"دادههای مزرعه '{active_farm_name_display}' برای شاخص {index_options[selected_index]} (هفته منتهی به {end_date_current_str}):\n" | |
| f"- مقدار هفته جاری: {current_val_str_gemini_q}\n" | |
| f"- مقدار هفته قبل: {prev_val_str_gemini_q}\n" | |
| f"- تغییر: {change_str_gemini_q}\n" | |
| f"- وضعیت کلی: {status_text_gemini_q}\n" | |
| ) | |
| else: | |
| context_data_gemini_q += f"دادههای عددی هفتگی برای شاخص '{selected_index}' جهت مزرعه '{active_farm_name_display}' در جدول رتبهبندی یافت نشد.\n" | |
| prompt_gemini_q += f"کاربر در مورد '{active_farm_name_display}' پرسیده: '{user_farm_q_gemini}'.\n{context_data_gemini_q}پاسخ جامع و مفید به فارسی ارائه دهید." | |
| else: # "همه مزارع" | |
| context_data_gemini_q = f"وضعیت کلی مزارع برای روز '{selected_day}' و شاخص '{index_options[selected_index]}'. تعداد {len(filtered_farms_df) if filtered_farms_df is not None else 0} مزرعه فیلتر شدهاند." | |
| if not ranking_df_sorted_tab3.empty: | |
| context_data_gemini_q += ( | |
| f"\nخلاصه وضعیت مزارع (نقاط مرکزی CSV) برای شاخص {selected_index}:\n" | |
| f"- بهبود/رشد: {count_positive_summary_tab3}\n" | |
| f"- ثابت: {count_neutral_summary_tab3}\n" | |
| f"- تنش/کاهش: {count_negative_summary_tab3}\n" | |
| f"- بدون داده/خطا: {count_nodata_summary_tab3}\n" | |
| ) | |
| prompt_gemini_q += f"کاربر در مورد وضعیت کلی مزارع پرسیده: '{user_farm_q_gemini}'.\n{context_data_gemini_q}پاسخ جامع و مفید به فارسی ارائه دهید." | |
| with st.spinner("⏳ در حال پردازش پاسخ با Gemini..."): | |
| response_gemini_q = ask_gemini(prompt_gemini_q) | |
| st.markdown(f"<div class='gemini-response-default'>{response_gemini_q}</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) # End gemini-card | |
| # --- 2. Automatic Weekly Report --- | |
| # Wrap this section in gemini-card and add a header | |
| st.markdown("<div class='gemini-card'>", unsafe_allow_html=True) # Start gemini-card | |
| with st.expander("📄 تولید گزارش خودکار هفتگی", expanded=False): | |
| st.markdown(""" | |
| <div class='gemini-card-header'> | |
| <div class='gemini-card-icon'>📄</div> | |
| <h3 class='gemini-card-title'>تولید گزارش خودکار هفتگی</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f"##### تولید گزارش هفتگی برای مزرعه '{active_farm_name_display}' بر اساس شاخص '{index_options[selected_index]}'.") # Keep descriptive text | |
| if active_farm_name_display == "همه مزارع": | |
| st.info("لطفاً یک مزرعه خاص را از سایدبار برای تولید گزارش انتخاب کنید.") | |
| else: | |
| farm_data_for_report_gemini = pd.DataFrame() | |
| if not ranking_df_sorted_tab3.empty: | |
| farm_data_for_report_gemini = ranking_df_sorted_tab3[ranking_df_sorted_tab3['مزرعه'] == active_farm_name_display] | |
| if farm_data_for_report_gemini.empty: | |
| st.info(f"دادههای رتبهبندی برای '{active_farm_name_display}' (شاخص: {selected_index}) جهت تولید گزارش موجود نیست.") | |
| elif st.button(f"📝 تولید گزارش برای '{active_farm_name_display}'", key="btn_gemini_report_gen_tab3"): | |
| report_context_gemini = farm_details_for_gemini_tab3 | |
| current_farm_report_data = farm_data_for_report_gemini.iloc[0] | |
| current_val_str_rep = f"{current_farm_report_data[f'{selected_index} (هفته جاری)']:.3f}" if pd.notna(current_farm_report_data[f'{selected_index} (هفته جاری)']) else "N/A" | |
| prev_val_str_rep = f"{current_farm_report_data[f'{selected_index} (هفته قبل)']:.3f}" if pd.notna(current_farm_report_data[f'{selected_index} (هفته قبل)']) else "N/A" | |
| change_str_rep = f"{current_farm_report_data['تغییر']:.3f}" if pd.notna(current_farm_report_data['تغییر']) else "N/A" | |
| status_text_rep = current_farm_report_data['وضعیت'] | |
| report_context_gemini += ( | |
| f"دادههای شاخص {index_options[selected_index]} برای '{active_farm_name_display}' (هفته منتهی به {end_date_current_str}):\n" | |
| f"- جاری: {current_val_str_rep}\n" | |
| f"- قبلی: {prev_val_str_rep}\n" | |
| f"- تغییر: {change_str_rep}\n" | |
| f"- وضعیت: {status_text_rep}\n" | |
| ) | |
| prompt_rep = ( | |
| f"شما یک دستیار هوشمند برای تهیه گزارشهای کشاورزی هستید. لطفاً یک گزارش توصیفی و ساختاریافته به زبان فارسی در مورد وضعیت '{active_farm_name_display}' برای هفته منتهی به {end_date_current_str} تهیه کنید.\n" | |
| f"اطلاعات موجود:\n{report_context_gemini}{analysis_basis_str_gemini_tab3}\n" | |
| f"در گزارش به موارد فوق اشاره کنید، تحلیل مختصری از وضعیت (با توجه به شاخص {selected_index}) ارائه دهید و در صورت امکان، پیشنهادهای کلی (نه تخصصی و قطعی) برای بهبود یا حفظ وضعیت مطلوب بیان کنید. گزارش باید رسمی، دارای عنوان، تاریخ، و بخشهای مشخص (مقدمه، وضعیت فعلی، تحلیل، پیشنهادات) و قابل فهم برای مدیران کشاورزی باشد." | |
| ) | |
| with st.spinner(f"⏳ در حال تولید گزارش برای '{active_farm_name_display}'..."): | |
| response_rep = ask_gemini(prompt_rep, temperature=0.6, top_p=0.9) | |
| st.markdown(f"### گزارش هفتگی '{active_farm_name_display}' (شاخص {index_options[selected_index]})") | |
| st.markdown(f"**تاریخ گزارش:** {datetime.date.today().strftime('%Y-%m-%d')}") | |
| st.markdown(f"**بازه زمانی:** {start_date_current_str} الی {end_date_current_str}") | |
| st.markdown(f"<div class='gemini-response-report'>{response_rep}</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) # End gemini-card | |
| # --- 3. Prioritization Assistant (NEW) --- | |
| # Wrap this section in gemini-card and add a header | |
| st.markdown("<div class='gemini-card'>", unsafe_allow_html=True) # Start gemini-card | |
| with st.expander("⚠️ دستیار اولویتبندی مزارع بحرانی", expanded=False): | |
| st.markdown(""" | |
| <div class='gemini-card-header'> | |
| <div class='gemini-card-icon'>🚨</div> | |
| <h3 class='gemini-card-title'>دستیار اولویتبندی مزارع بحرانی</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f"##### شناسایی مزارع نیازمند توجه فوری بر اساس شاخص '{index_options[selected_index]}'.") # Keep descriptive text | |
| if count_negative_summary_tab3 == 0 and (not ranking_df_sorted_tab3.empty): | |
| st.info(f"بر اساس شاخص '{index_options[selected_index]}'، هیچ مزرعهای در وضعیت 'تنش/کاهش' برای روز '{selected_day}' شناسایی نشد.") | |
| elif ranking_df_sorted_tab3.empty : | |
| st.info(f"دادهای برای رتبهبندی و اولویتبندی مزارع بر اساس شاخص '{index_options[selected_index]}' یافت نشد.") | |
| elif st.button(f"🔍 تحلیل و اولویتبندی مزارع بحرانی", key="btn_gemini_priority_assist_tab3"): | |
| # Prepare data for the prompt: farms with negative status | |
| # Sort by 'تغییر' to get the most negative changes first for positive-is-good indices | |
| # For MSI (stress index, higher is worse), a positive change is bad. | |
| # The existing 'وضعیت' text captures this logic. | |
| problematic_farms_df = ranking_df_sorted_tab3[ | |
| ranking_df_sorted_tab3['وضعیت'].str.contains('تنش|کاهش', case=False, na=False) | |
| ] | |
| # Sort by 'تغییر' column to highlight most significant changes for the prompt context | |
| # For NDVI, EVI, etc. (higher is better), a more negative 'تغییر' is worse. | |
| # For MSI (higher is worse), a more positive 'تغییر' is worse. | |
| # The 'ascending' parameter of sort_values handles this based on index nature. | |
| # However, 'تغییر' itself is just a difference. 'status_text' is more reliable for "bad". | |
| # Let's sort the problematic farms by the 'تغییر' to show Gemini the ones with biggest issues first. | |
| # If index is like NDVI (higher better), sort 'تغییر' ascending (most negative first) | |
| # If index is like MSI (higher worse), sort 'تغییر' descending (most positive first) | |
| sort_asc_for_change = selected_index not in ['MSI'] | |
| problematic_farms_for_prompt = problematic_farms_df.sort_values(by='تغییر', ascending=sort_asc_for_change) | |
| prompt_priority = f"""شما یک دستیار هوشمند برای اولویتبندی در مدیریت مزارع نیشکر هستید. | |
| روز مشاهده: {selected_day} | |
| شاخص مورد بررسی: {index_options[selected_index]} (ماهیت شاخص: {'مقدار بالاتر بهتر است' if selected_index not in ['MSI'] else 'مقدار بالاتر بدتر است (تنش بیشتر)'}) | |
| هفته منتهی به: {end_date_current_str} | |
| بر اساس جدول رتبهبندی هفتگی، {count_negative_summary_tab3} مزرعه وضعیت 'تنش/کاهش' یا تغییر منفی قابل توجهی را نشان میدهند. | |
| اطلاعات حداکثر ۵ مزرعه از این مزارع بحرانی (مرتب شده بر اساس شدت تغییر نامطلوب): | |
| {problematic_farms_for_prompt[['مزرعه', f'{selected_index} (هفته جاری)', 'تغییر', 'وضعیت']].head().to_string(index=False)} | |
| وظیفه شما: | |
| 1. از بین مزارع فوق، حداکثر ۳ مورد از بحرانیترینها را بر اساس شدت وضعیت نامطلوب (مقدار 'تغییر' و مقدار فعلی شاخص) انتخاب کنید. | |
| 2. برای هر مزرعه منتخب: | |
| الف. نام مزرعه و دادههای کلیدی آن (مقدار شاخص جاری، تغییر، وضعیت) را ذکر کنید. | |
| ب. دو یا سه دلیل احتمالی اولیه برای این وضعیت نامطلوب (با توجه به ماهیت شاخص {selected_index}) ارائه دهید. (مثال: برای NDVI پایین: تنش آبی، آفات، بیماری، برداشت اخیر. برای MSI بالا: خشکی، تنش آبی شدید). | |
| ج. یک یا دو اقدام اولیه پیشنهادی برای بررسی میدانی یا مدیریتی ارائه دهید. (مثال: بررسی سیستم آبیاری، پایش آفات، نمونه برداری خاک/گیاه). | |
| 3. اگر هیچ مزرعهای وضعیت بحرانی ندارد (که در اینجا قاعدتا نباید اینطور باشد چون دکمه فعال شده)، این موضوع را اعلام کنید. | |
| پاسخ باید به فارسی، ساختاریافته (مثلاً با استفاده از لیستها یا بخشبندی برای هر مزرعه)، و کاربردی باشد. | |
| {analysis_basis_str_gemini_tab3} | |
| """ | |
| with st.spinner("⏳ در حال تحلیل اولویتبندی با Gemini..."): | |
| response_priority = ask_gemini(prompt_priority, temperature=0.5) | |
| st.markdown(f"<div class='gemini-response-analysis'>{response_priority}</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) # End gemini-card | |
| # --- 4. Intelligent Timeseries Analysis --- | |
| # Wrap this section in gemini-card and add a header | |
| st.markdown("<div class='gemini-card'>", unsafe_allow_html=True) # Start gemini-card | |
| with st.expander(f"📉 تحلیل هوشمند روند زمانی شاخص {index_options[selected_index]}", expanded=False): | |
| st.markdown(""" | |
| <div class='gemini-card-header'> | |
| <div class='gemini-card-icon'>📉</div> | |
| <h3 class='gemini-card-title'>تحلیل هوشمند روند زمانی</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f"##### تحلیل روند زمانی شاخص '{index_options[selected_index]}' برای مزرعه '{active_farm_name_display}'.") # Keep descriptive text | |
| if active_farm_name_display == "همه مزارع": | |
| st.info("لطفاً یک مزرعه خاص را از سایدبار برای تحلیل سری زمانی انتخاب کنید.") | |
| elif active_farm_geom: | |
| if st.button(f"🔍 تحلیل روند زمانی {selected_index} برای '{active_farm_name_display}' با Gemini", key="btn_gemini_timeseries_an_tab3"): | |
| ts_end_date_gemini_ts = today.strftime('%Y-%m-%d') | |
| ts_start_date_gemini_ts = (today - datetime.timedelta(days=180)).strftime('%Y-%m-%d') # 6 months | |
| with st.spinner(f"⏳ در حال دریافت دادههای سری زمانی برای Gemini..."): | |
| # get_index_time_series is cached | |
| ts_df_gemini_ts, ts_error_gemini_ts = get_index_time_series( | |
| active_farm_geom, selected_index, # Use entire farm polygon | |
| start_date_str=ts_start_date_gemini_ts, end_date_str=ts_end_date_gemini_ts | |
| ) | |
| if ts_error_gemini_ts: | |
| st.error(f"خطا در دریافت دادههای سری زمانی برای Gemini: {ts_error_gemini_ts}") | |
| elif not ts_df_gemini_ts.empty: | |
| ts_summary_gemini = f"دادههای سری زمانی شاخص {index_options[selected_index]} برای '{active_farm_name_display}' در 6 ماه گذشته ({ts_start_date_gemini_ts} تا {ts_end_date_gemini_ts}):\n" | |
| # Sample data for conciseness in prompt, but provide key stats | |
| sample_freq_gemini = max(1, len(ts_df_gemini_ts) // 10) # Max 10 samples + ends | |
| ts_sampled_data_str = ts_df_gemini_ts.iloc[::sample_freq_gemini][selected_index].to_string(header=True, index=True) | |
| if len(ts_df_gemini_ts) > 1: | |
| ts_sampled_data_str += f"\n...\n{ts_df_gemini_ts[[selected_index]].iloc[-1].to_string(header=False)}" # Ensure last point is included | |
| ts_summary_gemini += ts_sampled_data_str | |
| ts_summary_gemini += f"\nمقدار اولیه حدود {ts_df_gemini_ts[selected_index].iloc[0]:.3f} و نهایی حدود {ts_df_gemini_ts[selected_index].iloc[-1]:.3f}." | |
| ts_summary_gemini += f"\n میانگین: {ts_df_gemini_ts[selected_index].mean():.3f}, کمترین: {ts_df_gemini_ts[selected_index].min():.3f}, بیشترین: {ts_df_gemini_ts[selected_index].max():.3f}." | |
| prompt_ts_an = ( | |
| f"شما یک تحلیلگر دادههای کشاورزی خبره هستید. {analysis_basis_str_gemini_tab3}\n" | |
| f" بر اساس دادههای سری زمانی زیر برای شاخص {index_options[selected_index]} مزرعه '{active_farm_name_display}' طی 6 ماه گذشته:\n{ts_summary_gemini}\n" | |
| f"وظایف تحلیلگر:\n" | |
| f"۱. روند کلی تغییرات شاخص را توصیف کنید (مثلاً صعودی، نزولی، نوسانی، ثابت).\n" | |
| f"۲. آیا دورههای خاصی از رشد قابل توجه، کاهش شدید یا ثبات طولانی مدت مشاهده میشود؟ اگر بله، به تاریخهای تقریبی اشاره کنید.\n" | |
| f"۳. با توجه به ماهیت شاخص '{selected_index}' ({'مقدار بالاتر بهتر است' if selected_index not in ['MSI'] else 'مقدار بالاتر بدتر است (تنش بیشتر)'}) و روند مشاهده شده، چه تفسیرهای اولیهای در مورد سلامت و وضعیت گیاه میتوان داشت؟\n" | |
| f"۴. چه نوع مشاهدات میدانی یا اطلاعات تکمیلی میتواند به درک بهتر این روند و تأیید تحلیل شما کمک کند?\n" | |
| f"پاسخ به فارسی، ساختاریافته، تحلیلی و کاربردی باشد." | |
| ) | |
| with st.spinner(f"⏳ در حال تحلیل روند زمانی {selected_index} با Gemini..."): | |
| response_ts_an = ask_gemini(prompt_ts_an, temperature=0.5) | |
| st.markdown(f"<div class='gemini-response-analysis'>{response_ts_an}</div>", unsafe_allow_html=True) | |
| else: | |
| st.info(f"دادهای برای تحلیل سری زمانی {selected_index} برای '{active_farm_name_display}' در 6 ماه گذشته یافت نشد.") | |
| else: # Not a single farm or no geometry | |
| st.info("تحلیل روند زمانی فقط برای یک مزرعه منفرد با مختصات مشخص قابل انجام است.") | |
| # --- 5. General Q&A --- | |
| # Wrap this section in gemini-card and add a header | |
| st.markdown("<div class='gemini-card'>", unsafe_allow_html=True) # Start gemini-card | |
| with st.expander("🗣️ پرسش و پاسخ عمومی", expanded=False): | |
| st.markdown(""" | |
| <div class='gemini-card-header'> | |
| <div class='gemini-card-icon'>❓</div> | |
| <h3 class='gemini-card-title'>پرسش و پاسخ عمومی</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("##### سوالات عمومی خود را در مورد مفاهیم کشاورزی، شاخصهای سنجش از دور، نیشکر یا عملکرد این سامانه بپرسید.") # Keep descriptive text | |
| user_general_q_gemini = st.text_area( | |
| "سوال عمومی شما:", | |
| key="gemini_general_q_text_tab3", | |
| height=100 | |
| ) | |
| if st.button("❓ پرسیدن سوال عمومی از Gemini", key="btn_gemini_general_q_send_tab3"): | |
| if not user_general_q_gemini: | |
| st.info("لطفاً سوال خود را وارد کنید.") | |
| else: | |
| prompt_gen_q = ( | |
| f"شما یک دانشنامه هوشمند در زمینه کشاورزی (با تمرکز بر نیشکر) و سنجش از دور هستید. " | |
| f"لطفاً به سوال زیر که توسط یک کاربر سامانه پایش نیشکر پرسیده شده است، به زبان فارسی پاسخ دهید. " | |
| f"سعی کنید پاسخ شما ساده، قابل فهم، دقیق و در حد امکان جامع باشد.\n" | |
| f"سوال کاربر: '{user_general_q_gemini}'" | |
| ) | |
| with st.spinner("⏳ در حال جستجو برای پاسخ با Gemini..."): | |
| response_gen_q = ask_gemini(prompt_gen_q, temperature=0.4) | |
| st.markdown(f"<div class='gemini-response-default'>{response_gen_q}</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) # End gemini-card | |
| # st.markdown("</div>", unsafe_allow_html=True) # End of section-container for tab3 - This outer div seems unnecessary now, removing. |