Spaces:
Configuration error
Configuration error
Update app.py
Browse files
app.py
CHANGED
@@ -1,479 +1,373 @@
|
|
1 |
import streamlit as st
|
2 |
import pandas as pd
|
3 |
import ee
|
4 |
-
import
|
5 |
import folium
|
6 |
-
import
|
7 |
-
import
|
8 |
-
|
9 |
-
|
10 |
-
#
|
11 |
-
#
|
12 |
-
#
|
13 |
-
|
14 |
-
#
|
15 |
-
|
16 |
-
page_title="داشبورد مانیتورینگ مزارع نیشکر دهخدا",
|
17 |
-
page_icon="🌾",
|
18 |
-
layout="wide", # Use wide layout for better map display
|
19 |
-
initial_sidebar_state="expanded" # Keep sidebar open initially
|
20 |
-
)
|
21 |
-
|
22 |
-
# --- Constants ---
|
23 |
-
CSV_FILE_PATH = 'output (1).csv'
|
24 |
-
SERVICE_ACCOUNT_KEY_PATH = 'ee-esmaeilkiani13877-cfdea6eaf411.json'
|
25 |
-
SERVICE_ACCOUNT_EMAIL = 'dehkhodamap-e9f0da4ce9f6514021@ee-esmaeilkiani13877.iam.gserviceaccount.com'
|
26 |
-
DEFAULT_LATITUDE = 31.534442
|
27 |
-
DEFAULT_LONGITUDE = 48.724416
|
28 |
-
DEFAULT_ZOOM = 13
|
29 |
-
AOI_BUFFER_METERS = 500 # Buffer radius around the farm point for analysis
|
30 |
-
DATE_RANGE_MONTHS = 3 # Analyze data for the last 3 months
|
31 |
-
|
32 |
-
# --- GEE Authentication ---
|
33 |
-
@st.cache_resource(show_spinner="در حال اتصال به Google Earth Engine...")
|
34 |
-
def authenticate_gee(service_account_key_path, service_account_email):
|
35 |
-
"""Authenticates Google Earth Engine using a Service Account."""
|
36 |
try:
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
# Load credentials from the file
|
43 |
-
with open(service_account_key_path) as f:
|
44 |
-
credentials_dict = json.load(f)
|
45 |
-
|
46 |
-
credentials = ee.ServiceAccountCredentials(service_account_email, service_account_key_path)
|
47 |
-
ee.Initialize(credentials=credentials, opt_url='https://earthengine-highvolume.googleapis.com')
|
48 |
-
print("GEE Authenticated Successfully using Service Account.")
|
49 |
-
return True # Indicate successful authentication
|
50 |
-
except ee.EEException as e:
|
51 |
-
st.error(f"خطا در احراز هویت Google Earth Engine: {e}")
|
52 |
-
st.error("لطفاً مطمئن شوید فایل کلید سرویس معتبر است و دسترسیهای لازم را دارد.")
|
53 |
-
st.stop() # Stop execution if authentication fails
|
54 |
-
except FileNotFoundError:
|
55 |
-
st.error(f"خطا: فایل کلید سرویس در مسیر '{service_account_key_path}' یافت نشد.")
|
56 |
-
st.stop()
|
57 |
except Exception as e:
|
58 |
-
st.error(f"
|
59 |
-
st.
|
|
|
60 |
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
|
|
65 |
try:
|
66 |
-
df = pd.read_csv(csv_path
|
67 |
-
|
68 |
-
required_cols = ['مزرعه', 'طول جغرافیایی', 'عرض جغرافیایی', 'روزهای هفته']
|
69 |
-
if not all(col in df.columns for col in required_cols):
|
70 |
-
st.error(f"خطا: فایل CSV باید شامل ستونهای {required_cols} باشد.")
|
71 |
-
st.stop()
|
72 |
-
# Convert coordinate columns to numeric, coercing errors
|
73 |
-
df['طول جغرافیایی'] = pd.to_numeric(df['طول جغرافیایی'], errors='coerce')
|
74 |
-
df['عرض جغرافیایی'] = pd.to_numeric(df['عرض جغرافیایی'], errors='coerce')
|
75 |
-
# Handle potential missing coordinates indicated by the flag or NaN values
|
76 |
-
df['coordinates_missing'] = df['coordinates_missing'].fillna(False).astype(bool) | df['طول جغرافیایی'].isna() | df['عرض جغرافیایی'].isna()
|
77 |
-
# Fill NaN in 'روزهای هفته' with a placeholder if necessary, or handle appropriately
|
78 |
-
df['روزهای هفته'] = df['روزهای هفته'].fillna('نامشخص') # Or drop rows: df.dropna(subset=['روزهای هفته'])
|
79 |
return df
|
80 |
except FileNotFoundError:
|
81 |
-
st.error(f"
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
#
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
#
|
99 |
-
# mask = qa.bitwiseAnd(cloud_bit_mask).eq(0).And(qa.bitwiseAnd(cirrus_bit_mask).eq(0))
|
100 |
-
return image.updateMask(mask).divide(10000).copyProperties(image, ["system:time_start"]) # Scale factor for SR
|
101 |
-
|
102 |
-
def calculate_ndvi(image):
|
103 |
-
"""Calculates NDVI."""
|
104 |
-
# NDVI = (NIR - Red) / (NIR + Red)
|
105 |
-
# Sentinel-2 Bands: NIR=B8, Red=B4
|
106 |
-
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
|
107 |
-
return image.addBands(ndvi)
|
108 |
-
|
109 |
-
def calculate_evi(image):
|
110 |
-
"""Calculates EVI."""
|
111 |
-
# EVI = 2.5 * (NIR - Red) / (NIR + 6 * Red - 7.5 * Blue + 1)
|
112 |
-
# Sentinel-2 Bands: NIR=B8, Red=B4, Blue=B2
|
113 |
-
evi = image.expression(
|
114 |
-
'2.5 * ((NIR - RED) / (NIR + 6 * RED - 7.5 * BLUE + 1))', {
|
115 |
-
'NIR': image.select('B8'),
|
116 |
-
'RED': image.select('B4'),
|
117 |
-
'BLUE': image.select('B2')
|
118 |
-
}).rename('EVI')
|
119 |
-
return image.addBands(evi)
|
120 |
-
|
121 |
-
def calculate_ndmi(image):
|
122 |
-
"""Calculates NDMI (Normalized Difference Moisture Index)."""
|
123 |
-
# NDMI = (NIR - SWIR1) / (NIR + SWIR1)
|
124 |
-
# Sentinel-2 Bands: NIR=B8, SWIR1=B11
|
125 |
-
ndmi = image.normalizedDifference(['B8', 'B11']).rename('NDMI')
|
126 |
-
return image.addBands(ndmi)
|
127 |
-
|
128 |
-
def estimate_lai(image):
|
129 |
-
"""Estimates LAI using a simple NDVI-based formula (requires calibration)."""
|
130 |
-
# Example formula: LAI = sqrt(NDVI * (1 + NDVI)) - This is highly empirical!
|
131 |
-
# A more common simple approach might be linear or exponential based on NDVI
|
132 |
-
# LAI = a * NDVI + b OR LAI = exp(c * NDVI + d)
|
133 |
-
# Using a simple placeholder: LAI directly proportional to NDVI (for demonstration)
|
134 |
-
# For a slightly more standard empirical approach (e.g., based on SNAP toolbox relations):
|
135 |
-
# lai = image.expression('3.618 * EVI - 0.118', {'EVI': image.select('EVI')}).rename('LAI_EVI') # If EVI is calculated
|
136 |
-
# Or based on NDVI:
|
137 |
-
lai_ndvi = image.expression('sqrt(NDVI * (1 + NDVI))', {'NDVI': image.select('NDVI')}).rename('LAI') # Placeholder
|
138 |
-
# Ensure LAI is not negative
|
139 |
-
lai_ndvi = lai_ndvi.where(lai_ndvi.gt(0), 0)
|
140 |
-
return image.addBands(lai_ndvi)
|
141 |
-
|
142 |
-
def estimate_biomass(image):
|
143 |
-
"""Estimates Biomass using NDVI as a proxy (requires calibration)."""
|
144 |
-
# Biomass is often correlated with NDVI or LAI.
|
145 |
-
# Using NDVI directly as a proxy indicator.
|
146 |
-
biomass_proxy = image.select('NDVI').rename('Biomass_Proxy')
|
147 |
-
return image.addBands(biomass_proxy)
|
148 |
-
|
149 |
-
def get_image_collection(aoi, start_date, end_date):
|
150 |
-
"""Gets, filters, masks, and processes Sentinel-2 image collection."""
|
151 |
-
s2_sr_col = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED') # Use Harmonized SR
|
152 |
-
.filterBounds(aoi)
|
153 |
-
.filterDate(start_date, end_date)
|
154 |
-
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 30)) # Pre-filter by metadata
|
155 |
-
.map(mask_s2_clouds) # Apply cloud masking
|
156 |
-
.map(calculate_ndvi)
|
157 |
-
.map(calculate_evi)
|
158 |
-
.map(calculate_ndmi)
|
159 |
-
.map(estimate_lai) # Add estimated LAI
|
160 |
-
.map(estimate_biomass) # Add Biomass proxy
|
161 |
-
)
|
162 |
-
return s2_sr_col
|
163 |
-
|
164 |
-
# --- Visualization Parameters ---
|
165 |
-
ndvi_vis = {
|
166 |
-
'min': 0.0, 'max': 1.0,
|
167 |
-
'palette': ['#FF0000', '#FFA500', '#FFFF00', '#ADFF2F', '#008000'] # Red -> Orange -> Yellow -> GreenYellow -> Green
|
168 |
-
}
|
169 |
-
evi_vis = {
|
170 |
-
'min': 0.0, 'max': 1.0,
|
171 |
-
'palette': ['#FF0000', '#FFA500', '#FFFF00', '#ADFF2F', '#008000'] # Similar palette for EVI
|
172 |
}
|
173 |
-
ndmi_vis = {
|
174 |
-
'min': -0.5, 'max': 0.8, # Typical range for NDMI
|
175 |
-
'palette': ['#FF0000', '#FFA500', '#FFFF00', '#ADD8E6', '#0000FF'] # Red -> Orange -> Yellow -> LightBlue -> Blue
|
176 |
-
}
|
177 |
-
lai_vis = {
|
178 |
-
'min': 0.0, 'max': 6.0, # Typical LAI range
|
179 |
-
'palette': ['#FFFFFF', '#CE7E45', '#DF923D', '#F1B555', '#FCD163', '#99B718', '#74A901', '#66A000', '#529400', '#3E8601', '#207401', '#056201', '#004C00', '#023B01', '#012E01', '#011D01', '#011301'] # Common LAI palette
|
180 |
-
}
|
181 |
-
biomass_proxy_vis = {
|
182 |
-
'min': 0.0, 'max': 1.0, # Same range as NDVI proxy
|
183 |
-
'palette': ['#FDE725', '#7AD151', '#22A884', '#2A788E', '#414487', '#440154'] # Viridis palette often used for biomass/productivity
|
184 |
-
}
|
185 |
-
rgb_vis = {
|
186 |
-
'min': 0.0, 'max': 0.3, # Max value for SR reflectance (adjust as needed)
|
187 |
-
'bands': ['B4', 'B3', 'B2'] # Red, Green, Blue
|
188 |
-
}
|
189 |
-
|
190 |
-
# --- Main Application Logic ---
|
191 |
-
def main():
|
192 |
-
"""Main function to run the Streamlit application."""
|
193 |
-
|
194 |
-
# --- Authentication ---
|
195 |
-
if 'gee_authenticated' not in st.session_state:
|
196 |
-
st.session_state.gee_authenticated = authenticate_gee(SERVICE_ACCOUNT_KEY_PATH, SERVICE_ACCOUNT_EMAIL)
|
197 |
-
|
198 |
-
if not st.session_state.gee_authenticated:
|
199 |
-
st.warning("اتصال به Google Earth Engine برقرار نشد. لطفاً صفحه را رفرش کنید یا تنظیمات را بررسی نمایید.")
|
200 |
-
st.stop()
|
201 |
-
|
202 |
-
# --- Load Data ---
|
203 |
-
df_farms = load_farm_data(CSV_FILE_PATH)
|
204 |
-
|
205 |
-
# --- Sidebar ---
|
206 |
-
st.sidebar.title("تنظیمات نمایش")
|
207 |
-
st.sidebar.header("فیلتر مزارع")
|
208 |
-
|
209 |
-
# -- Day of the Week Filter --
|
210 |
-
available_days = sorted(df_farms['روزهای هفته'].unique())
|
211 |
-
selected_day = st.sidebar.selectbox(
|
212 |
-
"انتخاب روز هفته:",
|
213 |
-
options=available_days,
|
214 |
-
index=0 # Default to the first day
|
215 |
-
)
|
216 |
-
|
217 |
-
# Filter farms based on selected day
|
218 |
-
df_filtered_by_day = df_farms[df_farms['روزهای هفته'] == selected_day].copy()
|
219 |
-
|
220 |
-
# Check if any farms are available for the selected day
|
221 |
-
if df_filtered_by_day.empty:
|
222 |
-
st.sidebar.warning(f"هیچ مزرعهای برای روز '{selected_day}' یافت نشد.")
|
223 |
-
st.warning(f"هیچ مزرعهای برای روز '{selected_day}' در فایل CSV تعریف نشده است. لطفاً روز دیگری را انتخاب کنید یا فایل داده را بررسی نمایید.")
|
224 |
-
st.stop() # Stop if no farms match the day
|
225 |
-
|
226 |
-
# Remove farms with missing coordinates from selection
|
227 |
-
df_valid_farms = df_filtered_by_day[~df_filtered_by_day['coordinates_missing']].copy()
|
228 |
-
if df_valid_farms.empty:
|
229 |
-
st.sidebar.warning(f"تمام مزارع برای روز '{selected_day}' فاقد مختصات معتبر هستند.")
|
230 |
-
st.warning(f"تمام مزارع برای روز '{selected_day}' فاقد مختصات معتبر در فایل CSV هستند.")
|
231 |
-
st.stop()
|
232 |
-
|
233 |
-
# -- Farm Selection Dropdown --
|
234 |
-
available_farms = sorted(df_valid_farms['مزرعه'].unique())
|
235 |
-
selected_farm_name = st.sidebar.selectbox(
|
236 |
-
"انتخاب مزرعه:",
|
237 |
-
options=available_farms,
|
238 |
-
index=0 # Default to the first farm in the filtered list
|
239 |
-
)
|
240 |
-
|
241 |
-
# Get selected farm details
|
242 |
-
selected_farm_data = df_valid_farms[df_valid_farms['مزرعه'] == selected_farm_name].iloc[0]
|
243 |
-
farm_lat = selected_farm_data['عرض جغرافیایی']
|
244 |
-
farm_lon = selected_farm_data['طول جغرافیایی']
|
245 |
-
|
246 |
-
# --- Display Selected Farm Info ---
|
247 |
-
st.sidebar.header("اطلاعات مزرعه انتخاب شده")
|
248 |
-
st.sidebar.markdown(f"**نام مزرعه:** {selected_farm_data['مزرعه']}")
|
249 |
-
st.sidebar.markdown(f"**کانال:** {selected_farm_data.get('کانال', 'N/A')}") # Use .get for optional columns
|
250 |
-
st.sidebar.markdown(f"**اداره:** {selected_farm_data.get('اداره', 'N/A')}")
|
251 |
-
st.sidebar.markdown(f"**مساحت داشت:** {selected_farm_data.get('مساحت داش��', 'N/A')}")
|
252 |
-
st.sidebar.markdown(f"**واریته:** {selected_farm_data.get('واریته', 'N/A')}")
|
253 |
-
st.sidebar.markdown(f"**سن:** {selected_farm_data.get('سن', 'N/A')}")
|
254 |
-
st.sidebar.markdown(f"**روز هفته:** {selected_farm_data['روزهای هفته']}")
|
255 |
-
st.sidebar.markdown(f"**مختصات:** ({farm_lat:.6f}, {farm_lon:.6f})")
|
256 |
-
|
257 |
-
# --- Map Section ---
|
258 |
-
st.header(f"نقشه و شاخصهای مزرعه: {selected_farm_name}")
|
259 |
-
|
260 |
-
# Create AOI (Area of Interest) point and buffer
|
261 |
-
farm_point = ee.Geometry.Point([farm_lon, farm_lat])
|
262 |
-
aoi = farm_point.buffer(AOI_BUFFER_METERS)
|
263 |
-
|
264 |
-
# Define date range for analysis
|
265 |
-
end_date = datetime.now()
|
266 |
-
start_date = end_date - timedelta(days=DATE_RANGE_MONTHS * 30) # Approximate months
|
267 |
-
start_date_str = start_date.strftime('%Y-%m-%d')
|
268 |
-
end_date_str = end_date.strftime('%Y-%m-%d')
|
269 |
-
|
270 |
-
st.info(f"دوره زمانی تحلیل: {start_date_str} تا {end_date_str}")
|
271 |
-
|
272 |
-
# Get processed image collection
|
273 |
-
with st.spinner("در حال پردازش تصاویر ماهوارهای... لطفاً منتظر بمانید."):
|
274 |
-
image_collection = get_image_collection(aoi, start_date_str, end_date_str)
|
275 |
-
|
276 |
-
# Check if the collection is empty
|
277 |
-
collection_size = image_collection.size().getInfo()
|
278 |
-
if collection_size == 0:
|
279 |
-
st.warning("هیچ تصویر ماهوارهای مناسبی (بدون ابر) در محدوده زمانی و مکانی انتخاب شده یافت نشد.")
|
280 |
-
st.warning("لطفاً دوره زمانی را تغییر دهید یا منتظر تصاویر جدید بمانید.")
|
281 |
-
# Display a basic map without GEE layers if no images found
|
282 |
-
Map = geemap.Map(location=[farm_lat, farm_lon], zoom=DEFAULT_ZOOM, add_google_map=False)
|
283 |
-
Map.add_basemap("HYBRID")
|
284 |
-
# Add marker for the farm
|
285 |
-
folium.Marker(
|
286 |
-
location=[farm_lat, farm_lon],
|
287 |
-
popup=f"مزرعه: {selected_farm_name}\nLat: {farm_lat:.4f}, Lon: {farm_lon:.4f}",
|
288 |
-
tooltip=selected_farm_name,
|
289 |
-
icon=folium.Icon(color='green')
|
290 |
-
).add_to(Map)
|
291 |
-
Map.add_layer_control()
|
292 |
-
Map.to_streamlit(height=600)
|
293 |
-
st.stop() # Stop further processing if no images
|
294 |
-
|
295 |
-
# Create a median composite image for visualization
|
296 |
-
median_image = image_collection.median().clip(aoi) # Clip to AOI for cleaner display
|
297 |
-
|
298 |
-
# --- Initialize Map ---
|
299 |
-
Map = geemap.Map(location=[farm_lat, farm_lon], zoom=DEFAULT_ZOOM, add_google_map=False)
|
300 |
-
Map.add_basemap("HYBRID") # Use Satellite Hybrid basemap
|
301 |
-
|
302 |
-
# --- Add Layers to Map ---
|
303 |
-
try:
|
304 |
-
# Add RGB Layer
|
305 |
-
Map.addLayer(median_image, rgb_vis, 'تصویر واقعی (RGB)')
|
306 |
-
|
307 |
-
# Add Index Layers
|
308 |
-
Map.addLayer(median_image.select('NDVI'), ndvi_vis, 'شاخص NDVI', True) # Show NDVI by default
|
309 |
-
Map.addLayer(median_image.select('EVI'), evi_vis, 'شاخص EVI', False)
|
310 |
-
Map.addLayer(median_image.select('NDMI'), ndmi_vis, 'شاخص رطوبت NDMI', False)
|
311 |
-
Map.addLayer(median_image.select('LAI'), lai_vis, 'شاخص سطح برگ (LAI تخمینی)', False)
|
312 |
-
Map.addLayer(median_image.select('Biomass_Proxy'), biomass_proxy_vis, 'پروکسی بیوماس (مبتنی بر NDVI)', False)
|
313 |
-
|
314 |
-
# Add marker for the selected farm
|
315 |
-
folium.Marker(
|
316 |
-
location=[farm_lat, farm_lon],
|
317 |
-
popup=f"مزرعه: {selected_farm_name}\nLat: {farm_lat:.4f}, Lon: {farm_lon:.4f}",
|
318 |
-
tooltip=selected_farm_name,
|
319 |
-
icon=folium.Icon(color='red', icon='info-sign')
|
320 |
-
).add_to(Map)
|
321 |
-
|
322 |
-
# Add AOI boundary (optional)
|
323 |
-
Map.add_geojson(aoi.getInfo(), layer_name="محدوده تحلیل (AOI)", style={'color': 'yellow', 'fillOpacity': 0.0})
|
324 |
-
|
325 |
-
# Add Layer Control
|
326 |
-
Map.add_layer_control()
|
327 |
-
|
328 |
-
# Add Legends
|
329 |
-
Map.add_legend(title="NDVI", builtin_legend='NDVI', palette=ndvi_vis['palette'])
|
330 |
-
# Add other legends if needed, position them carefully
|
331 |
-
# Map.add_legend(title="EVI", palette=evi_vis['palette'], min=evi_vis['min'], max=evi_vis['max'], position='bottomright')
|
332 |
-
# Map.add_legend(title="NDMI", palette=ndmi_vis['palette'], min=ndmi_vis['min'], max=ndmi_vis['max'], position='bottomright')
|
333 |
-
|
334 |
-
# --- Display Map ---
|
335 |
-
Map.to_streamlit(height=600) # Adjust height as needed
|
336 |
-
|
337 |
-
except ee.EEException as e:
|
338 |
-
st.error(f"خطا در پردازش یا نمایش لایههای نقشه: {e}")
|
339 |
-
st.error("ممکن است مشکلی در دادههای GEE یا محاسبه شاخصها وجود داشته باشد.")
|
340 |
-
except Exception as e:
|
341 |
-
st.error(f"یک خطای غیرمنتظره در نمایش نقشه رخ داد: {e}")
|
342 |
-
|
343 |
-
|
344 |
-
# --- Time Series Charts Section ---
|
345 |
-
st.header("نمودارهای زمانی شاخصها")
|
346 |
-
st.markdown(f"روند تغییرات شاخصها برای مزرعه **{selected_farm_name}** در {DATE_RANGE_MONTHS} ماه گذشته")
|
347 |
-
|
348 |
-
# Select indices for charting
|
349 |
-
indices_to_chart = ['NDVI', 'EVI', 'NDMI', 'LAI', 'Biomass_Proxy']
|
350 |
-
selected_indices = st.multiselect(
|
351 |
-
"انتخاب شاخصها برای نمایش در نمودار:",
|
352 |
-
options=indices_to_chart,
|
353 |
-
default=['NDVI', 'EVI'] # Default selections
|
354 |
-
)
|
355 |
-
|
356 |
-
if selected_indices:
|
357 |
-
with st.spinner("در حال تولید نمودارهای زمانی..."):
|
358 |
-
try:
|
359 |
-
# Use geemap's built-in charting capabilities if possible, or extract data
|
360 |
-
# geemap's chart functions might require different setup for streamlit
|
361 |
-
# Alternative: Extract data and plot with Streamlit/Altair/Plotly
|
362 |
-
|
363 |
-
# Extract time series data
|
364 |
-
ts_data = image_collection.select(selected_indices).map(lambda image: image.reduceRegion(
|
365 |
-
reducer=ee.Reducer.mean(),
|
366 |
-
geometry=aoi,
|
367 |
-
scale=30 # Adjust scale based on data resolution (10m for Sentinel-2 relevant bands)
|
368 |
-
).set('system:time_start', image.get('system:time_start')))
|
369 |
-
|
370 |
-
# Filter out null results
|
371 |
-
ts_data_filtered = ts_data.filter(ee.Filter.notNull(ts_data.first().keys()))
|
372 |
-
|
373 |
-
# Get data to client-side (can be slow for long series/many indices)
|
374 |
-
ts_list = ts_data_filtered.getInfo()['features']
|
375 |
-
|
376 |
-
if not ts_list:
|
377 |
-
st.warning("دادهای برای رسم نمودار در این دوره یافت نشد.")
|
378 |
-
else:
|
379 |
-
# Convert to Pandas DataFrame for easier plotting
|
380 |
-
data_for_df = []
|
381 |
-
for feature in ts_list:
|
382 |
-
props = feature['properties']
|
383 |
-
row = {'date': datetime.fromtimestamp(props['system:time_start'] / 1000.0)}
|
384 |
-
for index_name in selected_indices:
|
385 |
-
# Check if index exists in properties (might be null if calculation failed for that image)
|
386 |
-
row[index_name] = props.get(index_name)
|
387 |
-
data_for_df.append(row)
|
388 |
-
|
389 |
-
df_chart = pd.DataFrame(data_for_df)
|
390 |
-
df_chart = df_chart.set_index('date')
|
391 |
-
df_chart = df_chart.dropna(axis=1, how='all') # Drop columns if all values are NaN
|
392 |
-
df_chart = df_chart.dropna(axis=0, how='any') # Drop rows with any NaN for cleaner plot
|
393 |
-
|
394 |
-
if not df_chart.empty and not df_chart.columns.intersection(selected_indices).empty:
|
395 |
-
# Melt DataFrame for Altair/Streamlit native charts
|
396 |
-
df_melt = df_chart.reset_index().melt('date', var_name='شاخص', value_name='مقدار')
|
397 |
-
|
398 |
-
# Display line chart using Streamlit's native charting
|
399 |
-
st.line_chart(df_chart[selected_indices])
|
400 |
-
|
401 |
-
# Or use Altair for more customization (optional)
|
402 |
-
# import altair as alt
|
403 |
-
# chart = alt.Chart(df_melt).mark_line(point=True).encode(
|
404 |
-
# x='date:T',
|
405 |
-
# y='مقدار:Q',
|
406 |
-
# color='شاخص:N',
|
407 |
-
# tooltip=['date:T', 'شاخص:N', 'مقدار:Q']
|
408 |
-
# ).interactive()
|
409 |
-
# st.altair_chart(chart, use_container_width=True)
|
410 |
-
|
411 |
-
# Display data table
|
412 |
-
st.subheader("دادههای نمودار")
|
413 |
-
st.dataframe(df_chart.style.format("{:.3f}"))
|
414 |
-
else:
|
415 |
-
st.warning("داده معتبری برای رسم نمودار پس از پردازش یافت نشد.")
|
416 |
-
|
417 |
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
422 |
else:
|
423 |
-
st.
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
"
|
442 |
-
"
|
443 |
-
|
444 |
-
|
445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
446 |
else:
|
447 |
-
st.
|
448 |
-
|
449 |
-
except ee.EEException as e:
|
450 |
-
st.error(f"خطا در محاسبه مقادیر میانگین برای جدول: {e}")
|
451 |
-
except Exception as e:
|
452 |
-
st.error(f"خطای غیرمنتظره در بخش جدول مقایسه: {e}")
|
453 |
-
|
454 |
-
# --- Download Map (Placeholder) ---
|
455 |
-
# Note: Downloading the current map view from geemap/folium within Streamlit
|
456 |
-
# can be complex. Offering download of the composite image might be more feasible.
|
457 |
-
# st.header("دانلود نقشه")
|
458 |
-
# st.info("قابلیت دانلود مستقیم نقشه در حال توسعه است.")
|
459 |
-
# Add a button to download the mean values data as CSV
|
460 |
-
try:
|
461 |
-
if 'df_summary' in locals() and not df_summary.empty:
|
462 |
-
csv_summary = df_summary.to_csv(index=False).encode('utf-8')
|
463 |
-
st.download_button(
|
464 |
-
label="دانلود خلاصه شاخصها (CSV)",
|
465 |
-
data=csv_summary,
|
466 |
-
file_name=f'summary_{selected_farm_name}_{selected_day}.csv',
|
467 |
-
mime='text/csv',
|
468 |
-
)
|
469 |
-
except NameError: # df_summary might not exist if calculation failed
|
470 |
-
pass
|
471 |
-
except Exception as e:
|
472 |
-
st.error(f"خطا در ایجاد دکمه دانلود CSV: {e}")
|
473 |
-
|
474 |
|
475 |
-
|
476 |
-
|
477 |
-
# ==============================================================================
|
478 |
-
if __name__ == "__main__":
|
479 |
-
main()
|
|
|
1 |
import streamlit as st
|
2 |
import pandas as pd
|
3 |
import ee
|
4 |
+
import datetime
|
5 |
import folium
|
6 |
+
from streamlit_folium import folium_static
|
7 |
+
import matplotlib.pyplot as plt
|
8 |
+
import matplotlib.dates as mdates
|
9 |
+
|
10 |
+
# --- 0. تنظیمات اولیه و احراز هویت GEE ---
|
11 |
+
SERVICE_ACCOUNT_FILE = 'ee-esmaeilkiani13877-cfdea6eaf411.json' # نام فایل کلید خود را اینجا قرار دهید
|
12 |
+
EE_SERVICE_ACCOUNT_EMAIL = 'dehkhodamap-e9f0da4ce9f6514021@ee-esmaeilkiani13877.iam.gserviceaccount.com' # ایمیل سرویس اکانت خود
|
13 |
+
|
14 |
+
@st.cache_resource # برای جلوگیری از initialize شدن مکرر
|
15 |
+
def initialize_gee():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
try:
|
17 |
+
credentials = ee.ServiceAccountCredentials(email=EE_SERVICE_ACCOUNT_EMAIL, key_file=SERVICE_ACCOUNT_FILE)
|
18 |
+
ee.Initialize(credentials)
|
19 |
+
st.success("Google Earth Engine با موفقیت مقداردهی اولیه شد.")
|
20 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
except Exception as e:
|
22 |
+
st.error(f"خطا در مقداردهی اولیه Google Earth Engine: {e}")
|
23 |
+
st.error("لطفاً از معتبر بودن فایل Service Account و دسترسیهای لازم اطمینان حاصل کنید.")
|
24 |
+
return False
|
25 |
|
26 |
+
gee_initialized = initialize_gee()
|
27 |
+
|
28 |
+
# --- 1. بارگذاری دادههای مزارع ---
|
29 |
+
@st.cache_data # برای جلوگیری از بارگذاری مکرر فایل CSV
|
30 |
+
def load_farm_data(csv_path=""):
|
31 |
try:
|
32 |
+
df = pd.read_csv(csv_path)
|
33 |
+
df['planting_date'] = pd.to_datetime(df['planting_date'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
return df
|
35 |
except FileNotFoundError:
|
36 |
+
st.error(f"فایل {csv_path} یافت نشد. لطفاً فایل CSV مشخصات مزارع را در کنار برنامه قرار دهید.")
|
37 |
+
return pd.DataFrame() # برگرداندن DataFrame خالی در صورت خطا
|
38 |
+
|
39 |
+
farms_df = load_farm_data()
|
40 |
+
|
41 |
+
# --- 2. جدول Kc بر اساس سن گیاه (روز) ---
|
42 |
+
# این مقادیر مثال هستند و باید بر اساس تحقیقات معتبر برای نیشکر تنظیم شوند
|
43 |
+
# منابع: FAO-56 یا تحقیقات محلی
|
44 |
+
KC_STAGES = {
|
45 |
+
# (شروع دوره بر حسب روز, پایان دوره بر حسب روز, مقدار Kc)
|
46 |
+
"initial": (0, 30, 0.35), # مرحله اولیه
|
47 |
+
"development_1": (31, 60, 0.55), # توسعه اول
|
48 |
+
"development_2": (61, 90, 0.80), # توسعه دوم
|
49 |
+
"mid_season_1": (91, 150, 1.15), # اواسط فصل اول
|
50 |
+
"mid_season_2": (151, 210, 1.25), # اواسط فصل دوم (اوج نیاز آبی)
|
51 |
+
"mid_season_3": (211, 270, 1.20), # اواسط فصل سوم
|
52 |
+
"late_season_1": (271, 330, 0.90), # اواخر فصل اول
|
53 |
+
"late_season_2": (331, 365, 0.70) # اواخر فصل دوم (نزدیک به برداشت)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
+
def get_kc(plant_age_days):
|
57 |
+
"""محاسبه ضریب گیاهی (Kc) بر اساس سن گیاه."""
|
58 |
+
for stage, (start_day, end_day, kc_value) in KC_STAGES.items():
|
59 |
+
if start_day <= plant_age_days <= end_day:
|
60 |
+
return kc_value
|
61 |
+
# اگر سن گیاه خارج از بازههای تعریف شده بود (مثلا خیلی پیر)
|
62 |
+
# میتوان یک مقدار پیشفرض یا Kc مرحله آخر را برگرداند
|
63 |
+
if plant_age_days > 365:
|
64 |
+
return KC_STAGES["late_season_2"][2]
|
65 |
+
return 0.2 # مقدار پیشفرض برای سنین بسیار کم یا خطا
|
66 |
+
|
67 |
+
# --- 3. دریافت دادههای هواشناسی از GEE ---
|
68 |
+
@st.cache_data(ttl=3600) # کش کردن دادهها برای یک ساعت
|
69 |
+
def get_weather_data_gee(latitude, longitude, target_datetime):
|
70 |
+
"""
|
71 |
+
دریافت دادههای هواشناسی ساعتی از ERA5-Land برای یک نقطه و زمان مشخص.
|
72 |
+
ET0 از 'potential_evaporation' (pev) بدست میآید.
|
73 |
+
"""
|
74 |
+
if not gee_initialized:
|
75 |
+
return None
|
76 |
+
|
77 |
+
point = ee.Geometry.Point(longitude, latitude)
|
78 |
+
start_date_gee = ee.Date(target_datetime.strftime('%Y-%m-%dT%H:%M:%S'))
|
79 |
+
# برای دریافت داده دقیق ساعتی، end_date را یک ساعت بعد تنظیم میکنیم
|
80 |
+
end_date_gee = start_date_gee.advance(1, 'hour')
|
81 |
+
|
82 |
+
era5_land = ee.ImageCollection('ECMWF/ERA5_LAND/HOURLY') \
|
83 |
+
.filterBounds(point) \
|
84 |
+
.filterDate(start_date_gee, end_date_gee) \
|
85 |
+
.select(['temperature_2m', 'dewpoint_temperature_2m',
|
86 |
+
'surface_solar_radiation_downwards_hourly',
|
87 |
+
'u_component_of_wind_10m', 'v_component_of_wind_10m',
|
88 |
+
'potential_evaporation']) # pev
|
89 |
+
|
90 |
+
if era5_land.size().getInfo() == 0:
|
91 |
+
st.warning(f"دادهای از ERA5-Land برای تاریخ و ساعت {target_datetime.strftime('%Y-%m-%d %H:00')} در موقعیت ({latitude}, {longitude}) یافت نشد.")
|
92 |
+
return None
|
93 |
+
|
94 |
+
image = era5_land.first() # باید فقط یک تصویر در این بازه ساعتی وجود داشته باشد
|
95 |
+
|
96 |
+
# استخراج مقادیر در نقطه مورد نظر
|
97 |
+
# از scale پیشفرض ERA5-Land که حدود 11km است استفاده میکنیم
|
98 |
+
data = image.reduceRegion(reducer=ee.Reducer.first(), geometry=point, scale=11132).getInfo()
|
99 |
+
|
100 |
+
if not data or 'potential_evaporation' not in data or data['potential_evaporation'] is None:
|
101 |
+
st.warning(f"مقدار 'potential_evaporation' برای ساعت مورد نظر یافت نشد.")
|
102 |
+
return None
|
103 |
+
|
104 |
+
# تبدیل واحدها
|
105 |
+
# pev (potential_evaporation) در واحد متر آب در ساعت است. برای mm/hour ضرب در 1000 میکنیم.
|
106 |
+
et0_mm_per_hour = data.get('potential_evaporation', 0) * 1000 # ET₀
|
107 |
+
temp_c = data.get('temperature_2m', 273.15) - 273.15 # کلوین به سلسیوس
|
108 |
+
dewpoint_c = data.get('dewpoint_temperature_2m', 273.15) - 273.15 # کلوین به سلسیوس
|
109 |
+
# محاسبه سرعت باد از مولفهها
|
110 |
+
u_wind = data.get('u_component_of_wind_10m', 0)
|
111 |
+
v_wind = data.get('v_component_of_wind_10m', 0)
|
112 |
+
wind_speed_mps = (u_wind**2 + v_wind**2)**0.5
|
113 |
+
# تابش خورشیدی (J/m^2 در ساعت) - برای نمایش
|
114 |
+
solar_radiation_j_m2_h = data.get('surface_solar_radiation_downwards_hourly', 0)
|
115 |
+
|
116 |
+
# محاسبه رطوبت نسبی تقریبی از دمای هوا و نقطه شبنم
|
117 |
+
# فرمول Magnus-Tetens (simplified)
|
118 |
+
# e_s = 6.112 * exp((17.67 * T) / (T + 243.5))
|
119 |
+
# e_d = 6.112 * exp((17.67 * Td) / (Td + 243.5))
|
120 |
+
# RH = (e_d / e_s) * 100
|
121 |
+
import math
|
122 |
+
e_s = 6.112 * math.exp((17.67 * temp_c) / (temp_c + 243.5))
|
123 |
+
e_d = 6.112 * math.exp((17.67 * dewpoint_c) / (dewpoint_c + 243.5))
|
124 |
+
rh_percent = (e_d / e_s) * 100 if e_s > 0 else 0
|
125 |
+
rh_percent = min(max(rh_percent, 0), 100) # اطمینان از بازه 0-100
|
126 |
+
|
127 |
+
return {
|
128 |
+
"et0_mm_per_hour": et0_mm_per_hour,
|
129 |
+
"temperature_c": temp_c,
|
130 |
+
"relative_humidity_percent": rh_percent,
|
131 |
+
"wind_speed_mps": wind_speed_mps,
|
132 |
+
"solar_radiation_j_m2_h": solar_radiation_j_m2_h
|
133 |
+
}
|
134 |
+
|
135 |
+
# --- 4. محاسبه نیاز آبی (CWR) ---
|
136 |
+
def calculate_cwr(et0_mm_per_hour, kc, area_hectare):
|
137 |
+
"""محاسبه نیاز آبی محصول (CWR) بر حسب لیتر و متر مکعب در ساعت."""
|
138 |
+
area_m2 = area_hectare * 10000 # تبدیل هکتار به متر مربع
|
139 |
+
cwr_mm_per_hour = et0_mm_per_hour * kc # نیاز آبی به mm در ساعت
|
140 |
+
cwr_m3_per_hour = (cwr_mm_per_hour / 1000) * area_m2 # تبدیل mm به متر و ضرب در مساحت
|
141 |
+
cwr_liters_per_hour = cwr_m3_per_hour * 1000 # تبدیل متر مکعب به لیتر
|
142 |
+
return cwr_liters_per_hour, cwr_m3_per_hour, cwr_mm_per_hour
|
143 |
+
|
144 |
+
# --- 5. رابط کاربری Streamlit ---
|
145 |
+
st.set_page_config(layout="wide", page_title="محاسبه نیاز آبی نیشکر")
|
146 |
+
st.title("📊 محاسبه نیاز آبی مزارع نیشکر")
|
147 |
+
|
148 |
+
if not gee_initialized:
|
149 |
+
st.stop()
|
150 |
+
|
151 |
+
if farms_df.empty:
|
152 |
+
st.warning("دادههای مزارع بارگذاری نشد. لطفاً فایل CSV را بررسی کنید.")
|
153 |
+
st.stop()
|
154 |
+
|
155 |
+
# --- ستونبندی برای ورودیها و نقشه ---
|
156 |
+
col_input, col_map = st.columns([1, 1])
|
157 |
+
|
158 |
+
with col_input:
|
159 |
+
st.header("ورودیهای کاربر")
|
160 |
+
farm_names = farms_df['farm_name'].tolist()
|
161 |
+
selected_farm_name = st.selectbox("انتخاب مزرعه:", farm_names)
|
162 |
+
|
163 |
+
selected_farm_info = farms_df[farms_df['farm_name'] == selected_farm_name].iloc[0]
|
164 |
+
|
165 |
+
# نمایش اطلاعات مزرعه انتخاب شده
|
166 |
+
st.subheader(f"مشخصات مزرعه: {selected_farm_name}")
|
167 |
+
st.markdown(f"""
|
168 |
+
- **مساحت:** {selected_farm_info['area_hectare']} هکتار
|
169 |
+
- **تاریخ کاشت:** {selected_farm_info['planting_date'].strftime('%Y-%m-%d')}
|
170 |
+
- **واریته:** {selected_farm_info['variety']}
|
171 |
+
- **نوع خاک:** {selected_farm_info['soil_type']}
|
172 |
+
- **مختصات:** ({selected_farm_info['latitude']:.4f}, {selected_farm_info['longitude']:.4f})
|
173 |
+
""")
|
174 |
+
|
175 |
+
target_date = st.date_input("تاریخ مورد نظر برای آبیاری:", datetime.date.today())
|
176 |
+
target_hour = st.slider("ساعت مورد نظر (0-23):", 0, 23, datetime.datetime.now().hour)
|
177 |
+
|
178 |
+
# ترکیب تاریخ و ساعت
|
179 |
+
target_datetime = datetime.datetime.combine(target_date, datetime.time(hour=target_hour))
|
180 |
+
|
181 |
+
# محاسبه سن گیاه
|
182 |
+
plant_age_days = (target_datetime.date() - selected_farm_info['planting_date'].date()).days
|
183 |
+
st.info(f"سن گیاه در تاریخ {target_date.strftime('%Y-%m-%d')}: **{plant_age_days} روز**")
|
184 |
+
|
185 |
+
# دریافت Kc
|
186 |
+
kc_value = get_kc(plant_age_days)
|
187 |
+
st.info(f"ضریب گیاهی (Kc) محاسبه شده: **{kc_value:.2f}**")
|
188 |
+
|
189 |
+
with col_map:
|
190 |
+
st.header("موقعیت مزرعه روی نقشه")
|
191 |
+
map_center = [selected_farm_info['latitude'], selected_farm_info['longitude']]
|
192 |
+
m = folium.Map(location=map_center, zoom_start=12)
|
193 |
+
folium.Marker(
|
194 |
+
location=map_center,
|
195 |
+
popup=f"{selected_farm_info['farm_name']}\nمساحت: {selected_farm_info['area_hectare']} هکتار",
|
196 |
+
tooltip=selected_farm_info['farm_name']
|
197 |
+
).add_to(m)
|
198 |
+
# نمایش سایر مزارع
|
199 |
+
for idx, row in farms_df.iterrows():
|
200 |
+
if row['farm_name'] != selected_farm_name:
|
201 |
+
folium.CircleMarker(
|
202 |
+
location=[row['latitude'], row['longitude']],
|
203 |
+
radius=5,
|
204 |
+
popup=f"{row['farm_name']}",
|
205 |
+
tooltip=row['farm_name'],
|
206 |
+
color='blue',
|
207 |
+
fill=True,
|
208 |
+
fill_color='blue'
|
209 |
+
).add_to(m)
|
210 |
+
folium_static(m, width=600, height=400)
|
211 |
+
|
212 |
+
|
213 |
+
# --- محاسبه و نمایش نتایج ---
|
214 |
+
if st.button("محاسبه نیاز آبی", type="primary"):
|
215 |
+
if plant_age_days < 0:
|
216 |
+
st.error("تاریخ مورد نظر نمیتواند قبل از تاریخ کاشت باشد.")
|
217 |
else:
|
218 |
+
st.header(f"نتایج برای مزرعه '{selected_farm_name}' در تاریخ {target_datetime.strftime('%Y-%m-%d %H:00')}")
|
219 |
+
|
220 |
+
with st.spinner("در حال دریافت دادههای هواشناسی از GEE و محاسبه..."):
|
221 |
+
weather_data = get_weather_data_gee(
|
222 |
+
selected_farm_info['latitude'],
|
223 |
+
selected_farm_info['longitude'],
|
224 |
+
target_datetime
|
225 |
+
)
|
226 |
+
|
227 |
+
if weather_data:
|
228 |
+
et0 = weather_data["et0_mm_per_hour"]
|
229 |
+
|
230 |
+
col_weather, col_cwr = st.columns(2)
|
231 |
+
with col_weather:
|
232 |
+
st.subheader("دادههای هواشناسی (ساعتی):")
|
233 |
+
st.metric(label="تبخیر و تعرق مرجع (ET₀)", value=f"{et0:.3f} mm/hour")
|
234 |
+
st.metric(label="دما", value=f"{weather_data['temperature_c']:.1f} °C")
|
235 |
+
st.metric(label="رطوبت نسبی", value=f"{weather_data['relative_humidity_percent']:.1f} %")
|
236 |
+
st.metric(label="سرعت باد", value=f"{weather_data['wind_speed_mps']:.1f} m/s")
|
237 |
+
st.metric(label="تابش خورشیدی", value=f"{weather_data['solar_radiation_j_m2_h'] / 3600000:.2f} MJ/m²/hour")
|
238 |
+
|
239 |
+
|
240 |
+
cwr_liters, cwr_m3, cwr_mm = calculate_cwr(et0, kc_value, selected_farm_info['area_hectare'])
|
241 |
+
|
242 |
+
with col_cwr:
|
243 |
+
st.subheader("نیاز آبی محاسبه شده:")
|
244 |
+
st.metric(label="نیاز آبی گیاه (ETc)", value=f"{cwr_mm:.3f} mm/hour")
|
245 |
+
st.success(f"**میزان آب مورد نیاز:**")
|
246 |
+
st.markdown(f"### **{cwr_m3:,.2f} متر مکعب در ساعت**")
|
247 |
+
st.markdown(f"### **{cwr_liters:,.0f} لیتر در ساعت**")
|
248 |
+
|
249 |
+
# --- نمودار تغییرات نیاز آبی در طول روز / هفته ---
|
250 |
+
st.subheader("نمودار تغییرات نیاز آبی (تخمینی)")
|
251 |
+
|
252 |
+
tab_daily, tab_weekly = st.tabs(["تغییرات روزانه", "تغییرات هفتگی (میانگین روزانه)"])
|
253 |
+
|
254 |
+
with tab_daily:
|
255 |
+
with st.spinner("در حال محاسبه نمودار روزانه... (ممکن است کمی طول بکشد)"):
|
256 |
+
hourly_et0 = []
|
257 |
+
hours_of_day = []
|
258 |
+
hourly_cwr_m3 = []
|
259 |
+
|
260 |
+
current_day_start = datetime.datetime(target_date.year, target_date.month, target_date.day, 0, 0, 0)
|
261 |
+
|
262 |
+
for hour_offset in range(24):
|
263 |
+
dt_hourly = current_day_start + datetime.timedelta(hours=hour_offset)
|
264 |
+
weather_hourly = get_weather_data_gee(
|
265 |
+
selected_farm_info['latitude'],
|
266 |
+
selected_farm_info['longitude'],
|
267 |
+
dt_hourly
|
268 |
+
)
|
269 |
+
if weather_hourly and weather_hourly["et0_mm_per_hour"] is not None:
|
270 |
+
et0_val = weather_hourly["et0_mm_per_hour"]
|
271 |
+
_, cwr_m3_val, _ = calculate_cwr(et0_val, kc_value, selected_farm_info['area_hectare'])
|
272 |
+
hourly_et0.append(et0_val)
|
273 |
+
hourly_cwr_m3.append(cwr_m3_val)
|
274 |
+
else: # اگر دادهای نبود، صفر در نظر میگیریم
|
275 |
+
hourly_et0.append(0)
|
276 |
+
hourly_cwr_m3.append(0)
|
277 |
+
hours_of_day.append(dt_hourly)
|
278 |
+
|
279 |
+
if hours_of_day:
|
280 |
+
fig_daily, ax1 = plt.subplots(figsize=(12, 6))
|
281 |
+
|
282 |
+
color = 'tab:red'
|
283 |
+
ax1.set_xlabel(f'ساعت در تاریخ {target_date.strftime("%Y-%m-%d")}')
|
284 |
+
ax1.set_ylabel('نیاز آبی (m³/hour)', color=color)
|
285 |
+
ax1.plot(hours_of_day, hourly_cwr_m3, color=color, marker='o', linestyle='-')
|
286 |
+
ax1.tick_params(axis='y', labelcolor=color)
|
287 |
+
ax1.grid(True, linestyle='--', alpha=0.7)
|
288 |
+
|
289 |
+
ax2 = ax1.twinx() # ایجاد محور y دوم
|
290 |
+
color = 'tab:blue'
|
291 |
+
ax2.set_ylabel('ET₀ (mm/hour)', color=color)
|
292 |
+
ax2.plot(hours_of_day, hourly_et0, color=color, marker='x', linestyle='--')
|
293 |
+
ax2.tick_params(axis='y', labelcolor=color)
|
294 |
+
|
295 |
+
fig_daily.tight_layout() # برای جلوگیری از همپوشانی لیبلها
|
296 |
+
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
297 |
+
plt.title(f'تغییرات ساعتی نیاز آبی و ET₀ برای {selected_farm_name}')
|
298 |
+
st.pyplot(fig_daily)
|
299 |
+
else:
|
300 |
+
st.warning("داده کافی برای رسم نمودار روزانه یافت نشد.")
|
301 |
+
|
302 |
+
with tab_weekly:
|
303 |
+
with st.spinner("در حال محاسبه نمودار هفتگی... (ممکن است کمی طول بکشد)"):
|
304 |
+
daily_avg_et0 = []
|
305 |
+
days_of_week = []
|
306 |
+
daily_total_cwr_m3 = [] # نیاز آبی کل روزانه
|
307 |
+
|
308 |
+
# شروع از 3 روز قبل تا 3 روز بعد از تاریخ انتخاب شده
|
309 |
+
for day_offset in range(-3, 4): # 7 days total
|
310 |
+
current_date = target_date + datetime.timedelta(days=day_offset)
|
311 |
+
daily_et0_sum = 0
|
312 |
+
hourly_data_points = 0
|
313 |
+
|
314 |
+
# محاسبه ET0 کل روزانه با جمع مقادیر ساعتی
|
315 |
+
# (برای دقت بیشتر، باید ET0 ساعتی را جمع کرد، اما برای سادگی فرض میکنیم ET0 روزانه موجود است)
|
316 |
+
# در اینجا، میانگین ET0 ساعتی را برای روز محاسبه میکنیم
|
317 |
+
temp_daily_et0_values = []
|
318 |
+
for h_offset in range(24): # 24 hours
|
319 |
+
dt_hourly_for_week = datetime.datetime.combine(current_date, datetime.time(hour=h_offset))
|
320 |
+
weather_h_week = get_weather_data_gee(
|
321 |
+
selected_farm_info['latitude'],
|
322 |
+
selected_farm_info['longitude'],
|
323 |
+
dt_hourly_for_week
|
324 |
+
)
|
325 |
+
if weather_h_week and weather_h_week["et0_mm_per_hour"] is not None:
|
326 |
+
temp_daily_et0_values.append(weather_h_week["et0_mm_per_hour"])
|
327 |
+
|
328 |
+
if temp_daily_et0_values:
|
329 |
+
avg_daily_et0_mm_per_hour = sum(temp_daily_et0_values) / len(temp_daily_et0_values)
|
330 |
+
# سن گیاه برای هر روز هفته مجددا محاسبه می شود
|
331 |
+
plant_age_current_day = (current_date - selected_farm_info['planting_date'].date()).days
|
332 |
+
kc_current_day = get_kc(plant_age_current_day)
|
333 |
+
|
334 |
+
# CWR روزانه = ET0 روزانه (mm/day) * Kc
|
335 |
+
# ET0 روزانه (mm/day) = میانگین ET0 ساعتی (mm/hour) * 24 hours
|
336 |
+
et0_mm_per_day = avg_daily_et0_mm_per_hour * 24
|
337 |
+
_, cwr_m3_per_day, _ = calculate_cwr(et0_mm_per_day, kc_current_day, selected_farm_info['area_hectare'])
|
338 |
+
|
339 |
+
daily_avg_et0.append(et0_mm_per_day) # ET0 روزانه
|
340 |
+
daily_total_cwr_m3.append(cwr_m3_per_day) # CWR کل روزانه
|
341 |
+
else:
|
342 |
+
daily_avg_et0.append(0)
|
343 |
+
daily_total_cwr_m3.append(0)
|
344 |
+
days_of_week.append(current_date)
|
345 |
+
|
346 |
+
if days_of_week:
|
347 |
+
fig_weekly, ax1_w = plt.subplots(figsize=(12, 6))
|
348 |
+
|
349 |
+
color = 'tab:green'
|
350 |
+
ax1_w.set_xlabel('تاریخ')
|
351 |
+
ax1_w.set_ylabel('نیاز آبی روزانه (m³/day)', color=color)
|
352 |
+
ax1_w.plot(days_of_week, daily_total_cwr_m3, color=color, marker='o', linestyle='-')
|
353 |
+
ax1_w.tick_params(axis='y', labelcolor=color)
|
354 |
+
ax1_w.grid(True, linestyle='--', alpha=0.7)
|
355 |
+
|
356 |
+
ax2_w = ax1_w.twinx()
|
357 |
+
color = 'tab:purple'
|
358 |
+
ax2_w.set_ylabel('ET₀ روزانه (mm/day)', color=color)
|
359 |
+
ax2_w.plot(days_of_week, daily_avg_et0, color=color, marker='x', linestyle='--')
|
360 |
+
ax2_w.tick_params(axis='y', labelcolor=color)
|
361 |
+
|
362 |
+
fig_weekly.tight_layout()
|
363 |
+
ax1_w.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
|
364 |
+
plt.xticks(rotation=45)
|
365 |
+
plt.title(f'تغییرات هفتگی نیاز آبی و ET₀ (روزانه) برای {selected_farm_name}')
|
366 |
+
st.pyplot(fig_weekly)
|
367 |
+
else:
|
368 |
+
st.warning("داده کافی برای رسم نمودار هفتگی یافت نشد.")
|
369 |
else:
|
370 |
+
st.error("خطا در دریافت دادههای هواشناسی. لطفاً ورودیها و اتصال اینترنت را بررسی کنید.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
371 |
|
372 |
+
st.markdown("---")
|
373 |
+
st.caption("طراحی شده برای محاسبه نیاز آبی نیشکر با استفاده از GEE و Streamlit")
|
|
|
|
|
|