Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,103 +1,411 @@
|
|
1 |
-
import streamlit as st
|
2 |
-
import
|
3 |
-
import pandas as pd
|
4 |
-
import
|
5 |
-
import
|
6 |
-
from
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
def
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
#
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import ee
|
3 |
+
import pandas as pd
|
4 |
+
import numpy as np
|
5 |
+
import folium
|
6 |
+
from streamlit_folium import st_folium
|
7 |
+
import json # Added to read the service account file
|
8 |
+
# from google.oauth2 import service_account # ee.ServiceAccountCredentials handles this
|
9 |
+
|
10 |
+
# --- Configuration & GEE Initialization ---
|
11 |
+
st.set_page_config(layout="wide")
|
12 |
+
st.title("سامانه پایش و پشتیبانی تصمیم آبیاری نیشکر")
|
13 |
+
st.subheader("Sugarcane Irrigation Monitoring & Decision Support")
|
14 |
+
|
15 |
+
SERVICE_ACCOUNT_FILE = 'ee-esmaeilkiani13877-cfdea6eaf411 (4).json'
|
16 |
+
GEE_PROJECT_ID = "ee-esmaeilkiani13877" # Extract from JSON or set manually
|
17 |
+
FEATURE_COLLECTION_ID = "projects/ee-esmaeilkiani13877/assets/Croplogging-Farm"
|
18 |
+
|
19 |
+
@st.cache_resource
|
20 |
+
def initialize_gee():
|
21 |
+
"""Initializes GEE with service account credentials."""
|
22 |
+
try:
|
23 |
+
# Read client_email from the service account file
|
24 |
+
with open(SERVICE_ACCOUNT_FILE, 'r') as f:
|
25 |
+
sa_info = json.load(f)
|
26 |
+
client_email = sa_info.get('client_email')
|
27 |
+
if not client_email:
|
28 |
+
st.error("Client email not found in service account file.")
|
29 |
+
st.stop()
|
30 |
+
|
31 |
+
credentials = ee.ServiceAccountCredentials(client_email, SERVICE_ACCOUNT_FILE)
|
32 |
+
ee.Initialize(credentials=credentials, project=GEE_PROJECT_ID, opt_url='https://earthengine-highvolume.googleapis.com')
|
33 |
+
st.success("GEE Authenticated Successfully!")
|
34 |
+
return True
|
35 |
+
except Exception as e:
|
36 |
+
st.error(f"GEE Authentication Failed: {e}")
|
37 |
+
st.stop()
|
38 |
+
|
39 |
+
if not initialize_gee():
|
40 |
+
st.stop()
|
41 |
+
|
42 |
+
# --- Load Field Geometry and Attributes ---
|
43 |
+
@st.cache_data
|
44 |
+
def load_farm_data():
|
45 |
+
"""Loads farm data from GEE Feature Collection."""
|
46 |
+
try:
|
47 |
+
fc = ee.FeatureCollection(FEATURE_COLLECTION_ID)
|
48 |
+
# Convert to Pandas DataFrame for easier manipulation in Streamlit
|
49 |
+
# Need to get properties. This can be slow for large FCs.
|
50 |
+
# For performance, it's often better to do this server-side or get only needed props.
|
51 |
+
props = fc.aggregate_array('.all') # Gets all properties, might be slow
|
52 |
+
|
53 |
+
# A more efficient way to get specific properties if fc.getInfo() is too large
|
54 |
+
def get_fc_properties(feature):
|
55 |
+
return ee.Feature(feature).toDictionary()
|
56 |
+
|
57 |
+
fc_list = fc.toList(fc.size())
|
58 |
+
|
59 |
+
data = []
|
60 |
+
# We need to execute .getInfo() to bring data client-side for Streamlit
|
61 |
+
# For very large feature collections, consider alternatives or pagination
|
62 |
+
# For now, let's assume the number of farms is manageable
|
63 |
+
|
64 |
+
# Efficiently get a list of dictionaries
|
65 |
+
farm_features = fc.toList(fc.size()).map(lambda f: ee.Feature(f).toDictionary(['farm', 'group', 'Variety', 'Age', 'Area', 'calculated_area_ha', 'Field', 'Day', 'centroid_lon', 'centroid_lat']))
|
66 |
+
farm_data_list = farm_features.getInfo() # This is the server call
|
67 |
+
|
68 |
+
df = pd.DataFrame(farm_data_list)
|
69 |
+
|
70 |
+
# Ensure required columns are present, fill with None if not
|
71 |
+
required_cols = ['farm', 'group', 'Variety', 'Age', 'Area', 'calculated_area_ha', 'Field', 'Day', 'centroid_lon', 'centroid_lat']
|
72 |
+
for col in required_cols:
|
73 |
+
if col not in df.columns:
|
74 |
+
df[col] = None
|
75 |
+
|
76 |
+
# Use 'calculated_area_ha' if 'Area' is missing or prefer 'calculated_area_ha'
|
77 |
+
df['display_area'] = df['calculated_area_ha'].fillna(df['Area'])
|
78 |
+
|
79 |
+
return df, fc # Return both DataFrame for UI and GEE FC for spatial operations
|
80 |
+
except Exception as e:
|
81 |
+
st.error(f"Error loading farm data: {e}")
|
82 |
+
return pd.DataFrame(), None
|
83 |
+
|
84 |
+
farm_df, farm_fc_gee = load_farm_data()
|
85 |
+
|
86 |
+
if farm_df.empty or farm_fc_gee is None:
|
87 |
+
st.warning("Farm data could not be loaded. Please check the Feature Collection ID and GEE permissions.")
|
88 |
+
st.stop()
|
89 |
+
|
90 |
+
farm_names = sorted(farm_df['farm'].astype(str).unique().tolist())
|
91 |
+
|
92 |
+
# --- Sidebar for User Inputs ---
|
93 |
+
st.sidebar.header("⚙️ User Inputs")
|
94 |
+
|
95 |
+
selected_farm_name = st.sidebar.selectbox("Select Farm (انتخاب مزرعه):", farm_names)
|
96 |
+
|
97 |
+
# Auto-fill farm data
|
98 |
+
selected_farm_data = farm_df[farm_df['farm'] == selected_farm_name].iloc[0] if selected_farm_name else None
|
99 |
+
|
100 |
+
if selected_farm_data is not None:
|
101 |
+
st.sidebar.subheader("Farm Details (مشخصات مزرعه)")
|
102 |
+
st.sidebar.text(f"Group (گروه): {selected_farm_data.get('group', 'N/A')}")
|
103 |
+
st.sidebar.text(f"Variety (واریته): {selected_farm_data.get('Variety', 'N/A')}")
|
104 |
+
st.sidebar.text(f"Age (سن): {selected_farm_data.get('Age', 'N/A')} months")
|
105 |
+
st.sidebar.text(f"Area (مساحت): {selected_farm_data.get('display_area', 0):.2f} ha")
|
106 |
+
# Get the GEE geometry for the selected farm
|
107 |
+
selected_farm_geometry_gee = farm_fc_gee.filter(ee.Filter.eq('farm', selected_farm_name)).first().geometry()
|
108 |
+
else:
|
109 |
+
selected_farm_geometry_gee = None # Default to some full region or handle error
|
110 |
+
|
111 |
+
# Irrigation parameters
|
112 |
+
st.sidebar.subheader("Irrigation Parameters (پارامترهای آبیاری)")
|
113 |
+
Q = st.sidebar.number_input("Q: Irrigation flow rate (نرخ جریان آبیاری) (liters/second)", value=25.0, min_value=0.1, step=0.5)
|
114 |
+
t = st.sidebar.number_input("t: Irrigation time (زمان آبیاری) (hours)", value=12.0, min_value=0.1, step=0.5)
|
115 |
+
efficiency = st.sidebar.number_input("Efficiency (ضریب راندمان آبیاری)", value=1.05, min_value=0.1, max_value=2.0, step=0.01)
|
116 |
+
hydromodule = st.sidebar.number_input("Hydromodule (هیدرومدول) (m³/hour/ha)", value=3.6, min_value=0.1, step=0.1)
|
117 |
+
days_in_month = st.sidebar.number_input("Days in current month (تعداد روزهای ماه)", value=30, min_value=1, max_value=31, step=1)
|
118 |
+
|
119 |
+
# --- Irrigation Calculations ---
|
120 |
+
st.sidebar.subheader("Irrigation Calculations (محاسبات آبیاری)")
|
121 |
+
calculated_area_ha = selected_farm_data['display_area'] if selected_farm_data is not None else 0
|
122 |
+
|
123 |
+
if calculated_area_ha > 0:
|
124 |
+
volume_per_hectare = (Q * t * 3.6) / calculated_area_ha
|
125 |
+
interval_target = 1450 / (efficiency * 24 * hydromodule) if (efficiency * hydromodule) > 0 else 0
|
126 |
+
rounds_target = days_in_month / interval_target if interval_target > 0 else 0
|
127 |
+
area_month_target = 511.3 * rounds_target
|
128 |
+
area_day_target = area_month_target / days_in_month if days_in_month > 0 else 0
|
129 |
+
|
130 |
+
st.sidebar.metric("Volume per Hectare (حجم در هکتار)", f"{volume_per_hectare:.2f} m³/ha")
|
131 |
+
st.sidebar.metric("Interval Target (تناوب هدف)", f"{interval_target:.2f} days")
|
132 |
+
st.sidebar.metric("Rounds Target (دور هدف)", f"{rounds_target:.2f}")
|
133 |
+
st.sidebar.metric("Area Month Target (سطح زیر کشت ماهانه هدف)", f"{area_month_target:.2f} ha")
|
134 |
+
st.sidebar.metric("Area Day Target (سطح زیر کشت روزانه هدف)", f"{area_day_target:.2f} ha/day")
|
135 |
+
else:
|
136 |
+
st.sidebar.warning("Select a farm with valid area to see calculations.")
|
137 |
+
|
138 |
+
|
139 |
+
# --- Main Panel: Map and Indices ---
|
140 |
+
col1, col2 = st.columns([3, 1]) # Map column, Output panel column
|
141 |
+
|
142 |
+
with col1:
|
143 |
+
st.header("🗺️ Interactive Map & Indices")
|
144 |
|
145 |
+
# Default map center (e.g., first farm's centroid or a general area)
|
146 |
+
if selected_farm_data is not None and pd.notna(selected_farm_data.get('centroid_lon')) and pd.notna(selected_farm_data.get('centroid_lat')):
|
147 |
+
map_center = [selected_farm_data['centroid_lat'], selected_farm_data['centroid_lon']]
|
148 |
+
zoom_start = 13
|
149 |
+
else: # Fallback if no farm selected or centroid missing
|
150 |
+
map_center = [20, 0] # Default to a global view or a known region
|
151 |
+
zoom_start = 2
|
152 |
+
if not farm_df.empty and pd.notna(farm_df['centroid_lat'].iloc[0]) and pd.notna(farm_df['centroid_lon'].iloc[0]):
|
153 |
+
map_center = [farm_df['centroid_lat'].iloc[0], farm_df['centroid_lon'].iloc[0]] # Center on first farm
|
154 |
+
zoom_start = 10
|
155 |
+
|
156 |
+
|
157 |
+
m = folium.Map(location=map_center, zoom_start=zoom_start, tiles="OpenStreetMap")
|
158 |
+
|
159 |
+
# Add all farm boundaries for context (optional, can be slow)
|
160 |
+
# To make it lighter, consider simplifying geometries or showing only nearby farms
|
161 |
+
# For now, let's try adding all. If slow, this needs optimization.
|
162 |
+
# farm_boundaries_geojson = farm_fc_gee.geometry().getInfo() # This gets ALL geometries combined
|
163 |
+
# folium.GeoJson(farm_boundaries_geojson, name="All Farm Boundaries").add_to(m)
|
164 |
+
|
165 |
+
if selected_farm_geometry_gee and selected_farm_data is not None:
|
166 |
+
# Highlight selected farm
|
167 |
+
try:
|
168 |
+
selected_farm_geojson = selected_farm_geometry_gee.getInfo() # Get GeoJSON for the specific geometry
|
169 |
+
folium.GeoJson(
|
170 |
+
selected_farm_geojson,
|
171 |
+
name=f"Selected Farm: {selected_farm_name}",
|
172 |
+
style_function=lambda x: {'fillColor': 'yellow', 'color': 'orange', 'weight': 2.5, 'fillOpacity': 0.3}
|
173 |
+
).add_to(m)
|
174 |
+
|
175 |
+
# Fit map to selected farm bounds
|
176 |
+
bounds = selected_farm_geometry_gee.bounds().getInfo()['coordinates'][0]
|
177 |
+
# bounds is like [[min_lon, min_lat], [max_lon, min_lat], [max_lon, max_lat], [min_lon, max_lat], [min_lon, min_lat]]
|
178 |
+
# folium needs [[min_lat, min_lon], [max_lat, max_lon]]
|
179 |
+
map_bounds = [[min(p[1] for p in bounds), min(p[0] for p in bounds)],
|
180 |
+
[max(p[1] for p in bounds), max(p[0] for p in bounds)]]
|
181 |
+
m.fit_bounds(map_bounds)
|
182 |
+
|
183 |
+
except Exception as e:
|
184 |
+
st.error(f"Error adding selected farm geometry to map: {e}")
|
185 |
+
# Fallback to centroid if bounds fail
|
186 |
+
m.location = map_center
|
187 |
+
m.zoom_start = zoom_start
|
188 |
+
|
189 |
+
|
190 |
+
# --- Placeholder for GEE Indices ---
|
191 |
+
st.subheader("🛰️ Remote Sensing Indices")
|
192 |
+
index_options = ["NDVI", "NDWI", "LSWI", "NDMI"] # Add "Soil Moisture" later
|
193 |
+
selected_indices_display = st.multiselect("Select Indices to Display on Map:", index_options, default=["NDVI"])
|
194 |
+
|
195 |
+
# Date for indices
|
196 |
+
from datetime import datetime, timedelta
|
197 |
+
# Default to most recent data, e.g., last 30 days for Sentinel-2
|
198 |
+
end_date = datetime.now()
|
199 |
+
start_date_s2 = end_date - timedelta(days=30)
|
200 |
+
start_date_modis = end_date - timedelta(days=8) # Modis has more frequent data
|
201 |
+
|
202 |
+
def get_sentinel2_sr_collection(aoi, start_date, end_date):
|
203 |
+
s2_sr_col = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
|
204 |
+
.filterBounds(aoi)
|
205 |
+
.filterDate(ee.Date(start_date.strftime('%Y-%m-%d')), ee.Date(end_date.strftime('%Y-%m-%d')))
|
206 |
+
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))) # Basic cloud filter
|
207 |
+
return s2_sr_col
|
208 |
+
|
209 |
+
def calculate_ndvi(image):
|
210 |
+
return image.normalizedDifference(['B8', 'B4']).rename('NDVI')
|
211 |
+
|
212 |
+
def calculate_ndwi(image): # Using Green and NIR (McFeeters)
|
213 |
+
return image.normalizedDifference(['B3', 'B8']).rename('NDWI')
|
214 |
+
|
215 |
+
def calculate_lswi(image): # Using NIR and SWIR1
|
216 |
+
return image.normalizedDifference(['B8', 'B11']).rename('LSWI') # B8A might be better (narrow NIR) but B8 is common
|
217 |
+
|
218 |
+
def calculate_ndmi(image): # Normalized Difference Moisture Index (uses NIR and SWIR1)
|
219 |
+
return image.normalizedDifference(['B8', 'B11']).rename('NDMI') # Same as LSWI for Sentinel-2
|
220 |
+
|
221 |
+
# Visualization parameters
|
222 |
+
ndvi_vis = {'min': -0.2, 'max': 0.9, 'palette': ['red', 'yellow', 'green']}
|
223 |
+
water_vis = {'min': -0.5, 'max': 0.5, 'palette': ['brown', 'tan', 'lightblue', 'blue']}
|
224 |
+
|
225 |
+
|
226 |
+
if selected_farm_geometry_gee and selected_farm_data is not None:
|
227 |
+
s2_collection = get_sentinel2_sr_collection(selected_farm_geometry_gee, start_date_s2, end_date)
|
228 |
+
latest_s2_image = s2_collection.mosaic().clip(selected_farm_geometry_gee) # Use mosaic of recent images
|
229 |
+
|
230 |
+
if "NDVI" in selected_indices_display:
|
231 |
+
ndvi_image = calculate_ndvi(latest_s2_image)
|
232 |
+
try:
|
233 |
+
map_id_dict = ndvi_image.getMapId(ndvi_vis)
|
234 |
+
folium.TileLayer(
|
235 |
+
tiles=map_id_dict['tile_fetcher'].url_format,
|
236 |
+
attr='Google Earth Engine (NDVI)',
|
237 |
+
name='NDVI',
|
238 |
+
overlay=True,
|
239 |
+
control=True,
|
240 |
+
show=("NDVI" == selected_indices_display[0] if selected_indices_display else False) # Show first selected by default
|
241 |
+
).add_to(m)
|
242 |
+
except Exception as e:
|
243 |
+
st.warning(f"Could not display NDVI: {e}")
|
244 |
+
|
245 |
+
if "NDWI" in selected_indices_display:
|
246 |
+
ndwi_image = calculate_ndwi(latest_s2_image)
|
247 |
+
try:
|
248 |
+
map_id_dict = ndwi_image.getMapId(water_vis)
|
249 |
+
folium.TileLayer(
|
250 |
+
tiles=map_id_dict['tile_fetcher'].url_format,
|
251 |
+
attr='Google Earth Engine (NDWI)',
|
252 |
+
name='NDWI',
|
253 |
+
overlay=True,
|
254 |
+
control=True,
|
255 |
+
show=("NDWI" == selected_indices_display[0] if selected_indices_display else False)
|
256 |
+
).add_to(m)
|
257 |
+
except Exception as e:
|
258 |
+
st.warning(f"Could not display NDWI: {e}")
|
259 |
+
|
260 |
+
if "LSWI" in selected_indices_display:
|
261 |
+
lswi_image = calculate_lswi(latest_s2_image) # Often similar to NDMI with S2 bands
|
262 |
+
try:
|
263 |
+
map_id_dict = lswi_image.getMapId(water_vis)
|
264 |
+
folium.TileLayer(
|
265 |
+
tiles=map_id_dict['tile_fetcher'].url_format,
|
266 |
+
attr='Google Earth Engine (LSWI)',
|
267 |
+
name='LSWI',
|
268 |
+
overlay=True,
|
269 |
+
control=True,
|
270 |
+
show=("LSWI" == selected_indices_display[0] if selected_indices_display else False)
|
271 |
+
).add_to(m)
|
272 |
+
except Exception as e:
|
273 |
+
st.warning(f"Could not display LSWI: {e}")
|
274 |
+
|
275 |
+
if "NDMI" in selected_indices_display: # Note: For S2, NDMI with B8 & B11 is same as LSWI used
|
276 |
+
ndmi_image = calculate_ndmi(latest_s2_image)
|
277 |
+
try:
|
278 |
+
map_id_dict = ndmi_image.getMapId(water_vis)
|
279 |
+
folium.TileLayer(
|
280 |
+
tiles=map_id_dict['tile_fetcher'].url_format,
|
281 |
+
attr='Google Earth Engine (NDMI)',
|
282 |
+
name='NDMI',
|
283 |
+
overlay=True,
|
284 |
+
control=True,
|
285 |
+
show=("NDMI" == selected_indices_display[0] if selected_indices_display else False)
|
286 |
+
).add_to(m)
|
287 |
+
except Exception as e:
|
288 |
+
st.warning(f"Could not display NDMI: {e}")
|
289 |
+
|
290 |
+
folium.LayerControl().add_to(m)
|
291 |
+
st_folium(m, width=None, height=600) # Use st_folium
|
292 |
+
|
293 |
+
with col2:
|
294 |
+
st.header("📊 Output Panel (خروجی)")
|
295 |
+
if selected_farm_data is not None:
|
296 |
+
st.subheader("مشخصات مزرعه:")
|
297 |
+
st.write(f"**نام مزرعه (Farm Name):** {selected_farm_data.get('farm', 'N/A')}")
|
298 |
+
st.write(f"**سن (Age):** {selected_farm_data.get('Age', 'N/A')} ماه (months)")
|
299 |
+
st.write(f"**واریته (Variety):** {selected_farm_data.get('Variety', 'N/A')}")
|
300 |
+
st.write(f"**مساحت (Area):** {selected_farm_data.get('display_area', 0):.2f} هکتار (ha)")
|
301 |
+
st.markdown("---")
|
302 |
+
st.subheader("نتایج محاسبات آبیاری:")
|
303 |
+
if calculated_area_ha > 0:
|
304 |
+
st.write(f"**مقدار آب به ازای هر هکتار (Volume per Hectare):** {volume_per_hectare:.2f} m³/ha")
|
305 |
+
st.write(f"**تناوب آبیاری (Irrigation Interval):** {interval_target:.2f} روز (days)")
|
306 |
+
st.write(f"**مساحت قابل آبیاری ماهانه (Target Monthly Area):** {area_month_target:.2f} هکتار (ha)")
|
307 |
+
st.write(f"**مساحت قابل آبیاری روزانه (Target Daily Area):** {area_day_target:.2f} هکتار/روز (ha/day)")
|
308 |
+
else:
|
309 |
+
st.warning("اطلاعات آبیاری برای نمایش در دسترس نیست (مساحت نامعتبر است).")
|
310 |
+
else:
|
311 |
+
st.info("یک مزرعه را از نوار کناری انتخاب کنید تا اطلاعات آن نمایش داده شود.")
|
312 |
+
|
313 |
+
# --- Time Series Charts ---
|
314 |
+
st.markdown("---")
|
315 |
+
st.header("📈 Time Series Charts (نمودارهای سری زمانی)")
|
316 |
+
|
317 |
+
@st.cache_data(ttl=3600) # Cache for 1 hour
|
318 |
+
def get_indices_time_series(farm_geometry, farm_name_for_cache_key):
|
319 |
+
# farm_name_for_cache_key is to help streamlit differentiate cache if farm_geometry object changes subtly
|
320 |
+
if farm_geometry is None:
|
321 |
+
return pd.DataFrame()
|
322 |
+
|
323 |
+
end_date = ee.Date(datetime.now())
|
324 |
+
start_date = end_date.advance(-6, 'month') # Last 6 months
|
325 |
+
|
326 |
+
def GEE_s2_proc(img):
|
327 |
+
ndvi = calculate_ndvi(img).select('NDVI')
|
328 |
+
ndwi = calculate_ndwi(img).select('NDWI')
|
329 |
+
lswi = calculate_lswi(img).select('LSWI')
|
330 |
+
# For S2, NDMI (B8, B11) is the same as LSWI. If different bands are intended, adjust.
|
331 |
+
ndmi = calculate_ndmi(img).select('NDMI')
|
332 |
+
return img.addBands([ndvi, ndwi, lswi, ndmi]) \
|
333 |
+
.set('system:time_start', img.get('system:time_start'))
|
334 |
+
|
335 |
+
s2_col = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
|
336 |
+
.filterBounds(farm_geometry)
|
337 |
+
.filterDate(start_date, end_date)
|
338 |
+
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 30)) # Looser cloud filter for time series
|
339 |
+
.map(GEE_s2_proc))
|
340 |
+
|
341 |
+
def create_time_series(image_collection, band_name):
|
342 |
+
def reduce_region_function(image):
|
343 |
+
median_val = image.reduceRegion(
|
344 |
+
reducer=ee.Reducer.median(),
|
345 |
+
geometry=farm_geometry,
|
346 |
+
scale=20, # Scale for Sentinel-2 bands used in indices
|
347 |
+
maxPixels=1e9,
|
348 |
+
bestEffort=True # Use bestEffort for large geometries or complex reductions
|
349 |
+
).get(band_name)
|
350 |
+
return ee.Feature(None, {'date': ee.Date(image.get('system:time_start')).format('YYYY-MM-dd'), band_name: median_val})
|
351 |
+
|
352 |
+
ts = image_collection.select(band_name).map(reduce_region_function).filter(ee.Filter.NotNull([band_name]))
|
353 |
+
|
354 |
+
try:
|
355 |
+
ts_info = ts.getInfo()['features']
|
356 |
+
df_list = []
|
357 |
+
for f in ts_info:
|
358 |
+
props = f['properties']
|
359 |
+
if props[band_name] is not None: # Ensure value is not None
|
360 |
+
df_list.append({'date': props['date'], band_name: props[band_name]})
|
361 |
+
df = pd.DataFrame(df_list)
|
362 |
+
if not df.empty:
|
363 |
+
df['date'] = pd.to_datetime(df['date'])
|
364 |
+
df = df.set_index('date').sort_index()
|
365 |
+
# Resample to weekly median if enough data, otherwise just plot available
|
366 |
+
# df = df.resample('W').median() # This might lead to many NaNs if data is sparse
|
367 |
+
return df
|
368 |
+
except Exception as e:
|
369 |
+
st.warning(f"Could not generate time series for {band_name}: {e}")
|
370 |
+
return pd.DataFrame(columns=['date', band_name]).set_index('date')
|
371 |
+
|
372 |
+
|
373 |
+
if selected_farm_geometry_gee and selected_farm_data is not None:
|
374 |
+
st.subheader(f"Indices for {selected_farm_name} (Last 6 Months)")
|
375 |
+
|
376 |
+
chart_data_ndvi = get_indices_time_series(selected_farm_geometry_gee, f"{selected_farm_name}_NDVI")
|
377 |
+
if not chart_data_ndvi.empty:
|
378 |
+
st.line_chart(chart_data_ndvi['NDVI'], use_container_width=True)
|
379 |
+
st.caption("NDVI Time Series (Median over farm, weekly resample if dense)")
|
380 |
+
else:
|
381 |
+
st.write(f"No NDVI data found for {selected_farm_name} in the last 6 months.")
|
382 |
+
|
383 |
+
# Add other indices similarly
|
384 |
+
index_to_chart = st.selectbox("Select index for time series chart:", index_options, index=0)
|
385 |
+
|
386 |
+
if index_to_chart == "NDVI": # Already displayed above
|
387 |
+
pass
|
388 |
+
elif index_to_chart: # For NDWI, LSWI, NDMI
|
389 |
+
chart_data_other = get_indices_time_series(selected_farm_geometry_gee, f"{selected_farm_name}_{index_to_chart}")
|
390 |
+
if not chart_data_other.empty:
|
391 |
+
st.line_chart(chart_data_other[index_to_chart], use_container_width=True)
|
392 |
+
st.caption(f"{index_to_chart} Time Series (Median over farm)")
|
393 |
+
else:
|
394 |
+
st.write(f"No {index_to_chart} data found for {selected_farm_name} in the last 6 months.")
|
395 |
+
else:
|
396 |
+
st.info("Select a farm to view time series charts.")
|
397 |
+
|
398 |
+
st.sidebar.markdown("---")
|
399 |
+
st.sidebar.info("Developed with GEE & Streamlit.")
|
400 |
+
|
401 |
+
# --- Future improvements: ---
|
402 |
+
# 1. Soil Moisture (SMAP or ERA5-Land) - requires different collections and processing.
|
403 |
+
# 2. More robust cloud masking for Sentinel-2.
|
404 |
+
# 3. Option for user to select date range for indices on map.
|
405 |
+
# 4. Progress indicators for GEE computations.
|
406 |
+
# 5. Error handling for GEE calls (e.g., no image found).
|
407 |
+
# 6. Optimization for loading farm data if FeatureCollection is very large.
|
408 |
+
# 7. Better map layer control (e.g., radio buttons for exclusive display of one index at a time).
|
409 |
+
# 8. Allow drawing AOI if farm not in list.
|
410 |
+
# 9. Caching of GEE map tiles if possible or GEE results.
|
411 |
+
# 10. More sophisticated time series analysis (e.g. smoothing, trend lines).
|