Esmaeilkianii commited on
Commit
fc8e5af
·
verified ·
1 Parent(s): 2a62584

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +358 -464
app.py CHANGED
@@ -1,479 +1,373 @@
1
  import streamlit as st
2
  import pandas as pd
3
  import ee
4
- import geemap.foliumap as geemap # Using foliumap backend for Streamlit compatibility
5
  import folium
6
- import os
7
- import json
8
- from datetime import datetime, timedelta
9
-
10
- # ==============================================================================
11
- # Configuration and Initialization
12
- # ==============================================================================
13
-
14
- # --- Page Configuration ---
15
- st.set_page_config(
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
- # Check if the key file exists
38
- if not os.path.exists(service_account_key_path):
39
- st.error(f"خطا: فایل کلید سرویس در مسیر '{service_account_key_path}' یافت نشد.")
40
- st.stop()
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"یک خطای غیرمنتظره در هنگام احراز هویت رخ داد: {e}")
59
- st.stop()
 
60
 
61
- # --- Data Loading ---
62
- @st.cache_data(show_spinner="در حال بارگذاری داده‌های مزارع...")
63
- def load_farm_data(csv_path):
64
- """Loads farm data from the CSV file."""
 
65
  try:
66
- df = pd.read_csv(csv_path, encoding='utf-8') # Specify UTF-8 encoding for Persian characters
67
- # Basic data cleaning/validation
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"خطا: فایل CSV در مسیر '{csv_path}' یافت نشد.")
82
- st.stop()
83
- except Exception as e:
84
- st.error(f"خطا در بارگذاری یا پردازش فایل CSV: {e}")
85
- st.stop()
86
-
87
- # --- GEE Image Processing Functions ---
88
-
89
- def mask_s2_clouds(image):
90
- """Masks clouds in Sentinel-2 SR images using the SCL band."""
91
- scl = image.select('SCL')
92
- # Select clear (4), vegetation (5), and non-vegetated (6) pixels. Also include water (7).
93
- # Avoid cloud shadows (3), clouds medium probability (8), clouds high probability (9), cirrus (10).
94
- mask = scl.eq(4).Or(scl.eq(5)).Or(scl.eq(6)).Or(scl.eq(7))
95
- # Also mask based on QA60 band if needed (though SCL is generally better for SR)
96
- # qa = image.select('QA60')
97
- # cloud_bit_mask = 1 << 10
98
- # cirrus_bit_mask = 1 << 11
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
- except ee.EEException as e:
419
- st.error(f"خطا در دریافت داده‌های سری زمانی از GEE: {e}")
420
- except Exception as e:
421
- st.error(f"خطا در پردازش یا نمایش نمودار: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  else:
423
- st.info("لطفاً حداقل یک شاخص را برای نمایش نمودار انتخاب کنید.")
424
-
425
- # --- Farm Ranking Table (Placeholder/Simplified) ---
426
- # Note: Calculating indices for ALL farms dynamically can be very slow.
427
- # This section shows data for the SELECTED farm as an example.
428
- # A full ranking would require pre-calculation or a different architecture.
429
- st.header("مقایسه شاخص‌ها (مزرعه انتخاب شده)")
430
- try:
431
- # Calculate average values for the selected farm over the period
432
- mean_values = median_image.reduceRegion(
433
- reducer=ee.Reducer.mean(),
434
- geometry=aoi,
435
- scale=30 # Match chart scale
436
- ).getInfo() # GetInfo fetches the result
437
-
438
- if mean_values:
439
- # Prepare data for table display
440
- farm_summary_data = {
441
- "شاخص": list(mean_values.keys()),
442
- "مقدار میانگین (در دوره)": [f"{v:.3f}" if isinstance(v, (int, float)) else v for v in mean_values.values()]
443
- }
444
- df_summary = pd.DataFrame(farm_summary_data)
445
- st.dataframe(df_summary)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  else:
447
- st.warning("مقادیر میانگین برای مزرعه انتخاب شده قابل محاسبه نبود.")
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
- # Run the App
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")