Esmaeilkianii commited on
Commit
86ae7f0
·
verified ·
1 Parent(s): 48886cb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +450 -539
app.py CHANGED
@@ -1,568 +1,479 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import numpy as np
4
- import geemap.foliumap as geemap
5
  import ee
6
- import os
7
- import datetime
8
- import plotly.express as px
9
- import plotly.graph_objects as go
10
- from streamlit_folium import folium_static
11
  import folium
12
- from folium.plugins import Draw, Fullscreen, MeasureControl
13
  import json
14
- import base64
15
- from PIL import Image
16
- import io
 
 
17
 
18
- # تنظیم عنوان صفحه
19
  st.set_page_config(
20
  page_title="داشبورد مانیتورینگ مزارع نیشکر دهخدا",
21
- page_icon="🌱",
22
- layout="wide",
23
- initial_sidebar_state="expanded",
24
  )
25
 
26
- # استایل‌های CSS
27
- st.markdown("""
28
- <style>
29
- .main {
30
- background-color: #f5f7f9;
31
- }
32
- .stButton button {
33
- background-color: #4CAF50;
34
- color: white;
35
- font-weight: bold;
36
- }
37
- .stSelectbox label, .stMultiselect label {
38
- font-weight: bold;
39
- color: #2c3e50;
40
- }
41
- .reportview-container .main .block-container {
42
- padding-top: 2rem;
43
- }
44
- h1, h2, h3 {
45
- color: #2c3e50;
46
- }
47
- .stAlert {
48
- background-color: #d4edda;
49
- color: #155724;
50
- }
51
- .css-1v3fvcr {
52
- background-color: #f5f7f9;
53
- }
54
- .css-18e3th9 {
55
- padding-top: 1rem;
56
- }
57
- .css-1kyxreq {
58
- justify-content: center;
59
- align-items: center;
60
- }
61
- .metric-card {
62
- background-color: white;
63
- border-radius: 5px;
64
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
65
- padding: 1rem;
66
- margin-bottom: 1rem;
67
- }
68
- .metric-value {
69
- font-size: 2rem;
70
- font-weight: bold;
71
- color: #2c3e50;
72
- }
73
- .metric-label {
74
- font-size: 1rem;
75
- color: #7f8c8d;
76
- }
77
- </style>
78
- """, unsafe_allow_html=True)
79
-
80
- # تابع برای احراز هویت GEE
81
- @st.cache_resource
82
- def initialize_gee():
83
  try:
84
- service_account = 'dehkhodamap-e9f0da4ce9f6514021@ee-esmaeilkiani13877.iam.gserviceaccount.com'
85
- credentials_file = 'ee-esmaeilkiani13877-cfdea6eaf411.json'
86
- credentials = ee.ServiceAccountCredentials(service_account, credentials_file)
87
- ee.Initialize(credentials)
88
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  except Exception as e:
90
- st.error(f"خطا در اتصال به Google Earth Engine: {e}")
91
- return False
92
 
93
- # خواندن داده‌های CSV
94
- @st.cache_data
95
- def load_data():
 
96
  try:
97
- df = pd.read_csv('output (1).csv')
 
 
 
 
 
 
 
 
 
 
 
 
98
  return df
 
 
 
99
  except Exception as e:
100
- st.error(f"خطا در خواندن فایل CSV: {e}")
101
- return None
 
 
102
 
103
- # تابع برای محاسبه شاخص‌های مختلف
104
- def calculate_indices(image):
105
- # NDVI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
107
-
108
- # EVI
 
 
 
 
109
  evi = image.expression(
110
- '2.5 * ((NIR - RED) / (NIR + 6 * RED - 7.5 * BLUE + 1))',
111
- {
112
  'NIR': image.select('B8'),
113
  'RED': image.select('B4'),
114
  'BLUE': image.select('B2')
115
- }
116
- ).rename('EVI')
117
-
118
- # NDMI (Normalized Difference Moisture Index)
 
 
 
119
  ndmi = image.normalizedDifference(['B8', 'B11']).rename('NDMI')
120
-
121
- # LAI (Leaf Area Index) - simplified model
122
- lai = image.expression(
123
- '3.618 * EVI - 0.118',
124
- {
125
- 'EVI': evi
126
- }
127
- ).rename('LAI')
128
-
129
- # Biomass - simplified model based on LAI
130
- biomass = image.expression(
131
- '0.8 * LAI + 0.2',
132
- {
133
- 'LAI': lai
134
- }
135
- ).rename('Biomass')
136
-
137
- # MSI (Moisture Stress Index)
138
- msi = image.expression(
139
- 'SWIR1 / NIR',
140
- {
141
- 'SWIR1': image.select('B11'),
142
- 'NIR': image.select('B8')
143
- }
144
- ).rename('MSI')
145
-
146
- # Chlorophyll Index
147
- chlorophyll = image.expression(
148
- 'NIR / RED - 1',
149
- {
150
- 'NIR': image.select('B8'),
151
- 'RED': image.select('B4')
152
- }
153
- ).rename('Chlorophyll')
154
-
155
- # Add all indices to the image
156
- return image.addBands([ndvi, evi, ndmi, lai, biomass, msi, chlorophyll])
157
-
158
- # تابع برای دریافت تصاویر Sentinel-2
159
- def get_sentinel_imagery(start_date, end_date, aoi):
160
- # فیلتر کردن مجموعه داده Sentinel-2
161
- s2 = ee.ImageCollection('COPERNICUS/S2_SR') \
162
- .filterDate(start_date, end_date) \
163
- .filterBounds(aoi) \
164
- .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
165
-
166
- if s2.size().getInfo() == 0:
167
- return None
168
-
169
- # محاسبه میانگین تصاویر
170
- s2_median = s2.median()
171
-
172
- # محاسبه شاخص‌ها
173
- s2_indices = calculate_indices(s2_median)
174
-
175
- return s2_indices
176
-
177
- # تابع برای ایجاد نقشه
178
- def create_map(center_lat, center_lon, zoom=13):
179
- m = geemap.Map()
180
- m.set_center(center_lon, center_lat, zoom)
181
- m.add_basemap('HYBRID')
182
- return m
183
-
184
- # تابع برای اضافه کردن لایه شاخص به نقشه
185
- def add_index_layer(m, image, index_name, vis_params, layer_name):
186
- if image is not None:
187
- m.add_layer(image.select(index_name), vis_params, layer_name)
188
- return m
189
-
190
- # تابع برای ایجاد نمودار زمانی
191
- def create_time_series(aoi, index_name, start_date, end_date, interval='day'):
192
- # تعریف بازه‌های زمانی
193
- if interval == 'day':
194
- step = 1
195
- unit = 'day'
196
- elif interval == 'week':
197
- step = 7
198
- unit = 'day'
199
- elif interval == 'month':
200
- step = 1
201
- unit = 'month'
202
-
203
- # ایجاد لیست تاریخ‌ها
204
- dates = []
205
- values = []
206
-
207
- current_date = start_date
208
- while current_date <= end_date:
209
- next_date = current_date + datetime.timedelta(days=step) if unit == 'day' else \
210
- datetime.date(current_date.year + (current_date.month + step - 1) // 12,
211
- (current_date.month + step - 1) % 12 + 1,
212
- 1)
213
-
214
- # دریافت تصویر برای این بازه زمانی
215
- image = get_sentinel_imagery(current_date.strftime('%Y-%m-%d'),
216
- next_date.strftime('%Y-%m-%d'),
217
- aoi)
218
-
219
- if image is not None:
220
- # محاسبه میانگین شاخص در منطقه مورد نظر
221
- mean_value = image.select(index_name).reduceRegion(
222
- reducer=ee.Reducer.mean(),
223
- geometry=aoi,
224
- scale=10
225
- ).get(index_name).getInfo()
226
-
227
- if mean_value is not None:
228
- dates.append(current_date.strftime('%Y-%m-%d'))
229
- values.append(mean_value)
230
-
231
- current_date = next_date
232
-
233
- # ایجاد دیتافریم برای نمودار
234
- if len(dates) > 0:
235
- df = pd.DataFrame({
236
- 'Date': dates,
237
- index_name: values
238
- })
239
- return df
240
- else:
241
- return None
242
-
243
- # تابع برای دانلود تصویر نقشه
244
- def get_map_download_link(m, filename="map.html"):
245
- """تولید لینک دانلود برای نقشه"""
246
- m.to_html(filename)
247
- with open(filename, 'rb') as f:
248
- html_data = f.read()
249
-
250
- b64 = base64.b64encode(html_data).decode()
251
- href = f'<a href="data:text/html;base64,{b64}" download="{filename}">دانلود نقشه</a>'
252
- return href
253
-
254
- # تابع برای رتبه‌بندی مزارع بر اساس شاخص‌ها
255
- def rank_farms(df, index_values):
256
- """رتبه‌بندی مزارع بر اساس مقادیر شاخص‌ها"""
257
- if df is None or index_values is None or len(index_values) == 0:
258
- return None
259
-
260
- # ترکیب داده‌های مزارع با مقادیر شاخص‌ها
261
- farm_ranks = df.copy()
262
-
263
- # اضافه کردن مقادیر شاخص‌ها (در اینجا فرض می‌کنیم که index_values یک دیکشنری است)
264
- for farm_id, values in index_values.items():
265
- for index_name, value in values.items():
266
- farm_ranks.loc[farm_ranks['مزرعه'] == farm_id, index_name] = value
267
-
268
- # رتبه‌بندی بر اساس NDVI (به عنوان مثال)
269
- if 'NDVI' in farm_ranks.columns:
270
- farm_ranks['رتبه NDVI'] = farm_ranks['NDVI'].rank(ascending=False)
271
-
272
- return farm_ranks
273
-
274
- # تابع اصلی برنامه
275
  def main():
276
- # عنوان اصلی
277
- st.title("داشبورد مانیتورینگ مزارع نیشکر دهخدا")
278
-
279
- # احراز هویت GEE
280
- gee_initialized = initialize_gee()
281
-
282
- if not gee_initialized:
283
- st.error("اتصال به Google Earth Engine برقرار نشد. لطفاً فایل احراز هویت را بررسی کنید.")
284
- return
285
-
286
- # خواندن داده‌ها
287
- df = load_data()
288
-
289
- if df is None:
290
- st.error("خواندن داده‌ها با مشکل مواجه شد. لطفاً فایل CSV را بررسی کنید.")
291
- return
292
-
293
- # تبدیل ستون‌های مختصات به عدد
294
- df['طول جغرافیایی'] = pd.to_numeric(df['طول جغرافیایی'], errors='coerce')
295
- df['عرض جغرافیایی'] = pd.to_numeric(df['عرض جغرافیایی'], errors='coerce')
296
-
297
- # حذف ردیف‌های با مختصات نامعتبر
298
- df = df[~((df['طول جغرافیایی'] == 0) | (df['عرض جغرافیایی'] == 0) |
299
- df['طول جغرافیایی'].isna() | df['عرض جغرافیایی'].isna())]
300
-
301
- # ایجاد ستون‌های جدید برای نمایش بهتر
302
- df['مزرعه_نمایشی'] = df['مزرعه'].astype(str)
303
-
304
- # ایجاد sidebar
305
- st.sidebar.title("فیلترها و تنظیمات")
306
-
307
- # فیلتر روز هفته
308
- days_of_week = df['روزهای هفته'].unique().tolist()
309
- selected_day = st.sidebar.selectbox("انتخاب روز هفته", days_of_week)
310
-
311
- # فیلتر مزرعه
312
- filtered_df = df[df['روزهای هفته'] == selected_day]
313
- farm_ids = filtered_df['مزرعه'].unique().tolist()
314
- selected_farm = st.sidebar.selectbox("انتخاب مزرعه", farm_ids)
315
-
316
- # فیلتر شاخص
317
- indices = ['NDVI', 'EVI', 'NDMI', 'LAI', 'Biomass', 'MSI', 'Chlorophyll']
318
- selected_index = st.sidebar.selectbox("انتخاب شاخص", indices)
319
-
320
- # تنظیمات تاریخ
321
- st.sidebar.subheader("بازه زمانی")
322
- today = datetime.date.today()
323
- start_date = st.sidebar.date_input("تاریخ شروع", today - datetime.timedelta(days=30))
324
- end_date = st.sidebar.date_input("تاریخ پایان", today)
325
-
326
- # تنظیمات نمایش نقشه
327
- st.sidebar.subheader("تنظیمات نقشه")
328
- map_zoom = st.sidebar.slider("بزرگنمایی نقشه", 10, 18, 13)
329
-
330
- # دکمه اعمال فیلترها
331
- apply_filters = st.sidebar.button("اعمال فیلترها")
332
-
333
- # نمایش اطلاعات مزرعه انتخاب شده
334
- if selected_farm:
335
- farm_data = filtered_df[filtered_df['مزرعه'] == selected_farm].iloc[0]
336
-
337
- st.subheader(f"اطلاعات مزرعه {selected_farm}")
338
-
339
- # نمایش اطلاعات در چند ستون
340
- col1, col2, col3 = st.columns(3)
341
-
342
- with col1:
343
- st.markdown(f"**کانال:** {farm_data['کانال']}")
344
- st.markdown(f"**اداره:** {farm_data['اداره']}")
345
- st.markdown(f"**مساحت داشت:** {farm_data['مساحت داشت']} هکتار")
346
-
347
- with col2:
348
- st.markdown(f"**واریته:** {farm_data['واریته']}")
349
- st.markdown(f"**سن:** {farm_data['سن']}")
350
- st.markdown(f"**روز هفته:** {farm_data['روزهای هفته']}")
351
-
352
- with col3:
353
- st.markdown(f"**طول جغرافیایی:** {farm_data['طول جغرافیایی']}")
354
- st.markdown(f"**عرض جغرافیایی:** {farm_data['عرض جغرافیایی']}")
355
-
356
- # ایجاد تب‌ها برای نمایش نقشه و نمودارها
357
- tab1, tab2, tab3 = st.tabs(["نقشه", "نمودارها", "جدول مقایسه"])
358
-
359
- with tab1:
360
- if apply_filters and selected_farm:
361
- # دریافت مختصات مزرعه انتخاب شده
362
- farm_data = filtered_df[filtered_df['مزرعه'] == selected_farm].iloc[0]
363
- center_lat = farm_data['عرض جغرافیایی']
364
- center_lon = farm_data['طول جغرافیایی']
365
-
366
- # ایجاد منطقه مورد نظر (AOI) - یک بافر 500 متری اطراف نقطه مرکزی مزرعه
367
- point = ee.Geometry.Point([center_lon, center_lat])
368
- aoi = point.buffer(500)
369
-
370
- # ایجاد نقشه
371
- m = create_map(center_lat, center_lon, map_zoom)
372
-
373
- # دریافت تصاویر Sentinel-2
374
- imagery = get_sentinel_imagery(start_date.strftime('%Y-%m-%d'),
375
- end_date.strftime('%Y-%m-%d'),
376
- aoi)
377
-
378
- if imagery is not None:
379
- # تنظیم پارامترهای نمایش برای شاخص انتخاب شده
380
- vis_params = {
381
- 'NDVI': {'min': -0.2, 'max': 0.8, 'palette': ['red', 'yellow', 'green']},
382
- 'EVI': {'min': -0.2, 'max': 1, 'palette': ['red', 'yellow', 'green']},
383
- 'NDMI': {'min': -0.2, 'max': 0.8, 'palette': ['red', 'yellow', 'blue']},
384
- 'LAI': {'min': 0, 'max': 5, 'palette': ['white', 'lightgreen', 'darkgreen']},
385
- 'Biomass': {'min': 0, 'max': 5, 'palette': ['white', 'lightgreen', 'darkgreen']},
386
- 'MSI': {'min': 0, 'max': 2, 'palette': ['blue', 'yellow', 'red']},
387
- 'Chlorophyll': {'min': 0, 'max': 5, 'palette': ['white', 'yellow', 'green']}
388
- }
389
-
390
- # اضافه کردن لایه شاخص به نقشه
391
- m = add_index_layer(m, imagery, selected_index, vis_params[selected_index], f"شاخص {selected_index}")
392
-
393
- # اضافه کردن نقاط مزارع به نقشه
394
- for _, row in filtered_df.iterrows():
395
- folium.Marker(
396
- location=[row['عرض جغرافیایی'], row['طول جغرافیایی']],
397
- popup=f"مزرعه: {row['مزرعه']}<br>واریته: {row['واریته']}<br>سن: {row['سن']}",
398
- tooltip=f"مزرعه {row['مزرعه']}",
399
- icon=folium.Icon(color='blue' if row['مزرعه'] != selected_farm else 'red')
400
- ).add_to(m)
401
-
402
- # اضافه کردن کنترل‌های نقشه
403
- m.add_control(Draw(export=True))
404
- m.add_control(Fullscreen())
405
- m.add_control(MeasureControl())
406
-
407
- # نمایش نقشه
408
- folium_static(m)
409
-
410
- # دکمه دانلود نقشه
411
- st.markdown(get_map_download_link(m), unsafe_allow_html=True)
412
-
413
- # نمایش راهنمای رنگ‌ها
414
- st.subheader("راهنمای رنگ‌ها")
415
-
416
- if selected_index in ['NDVI', 'EVI']:
417
- st.markdown("""
418
- - <span style='color:red'>قرمز</span>: وضعیت ضعیف (0.0-0.2)
419
- - <span style='color:yellow'>زرد</span>: وضعیت متوسط (0.2-0.5)
420
- - <span style='color:green'>سبز</span>: وضعیت خوب (0.5-0.8)
421
- """, unsafe_allow_html=True)
422
- elif selected_index == 'NDMI':
423
- st.markdown("""
424
- - <span style='color:red'>قرمز</span>: خشک (0.0-0.2)
425
- - <span style='color:yellow'>زرد</span>: رطوبت متوسط (0.2-0.5)
426
- - <span style='color:blue'>آبی</span>: رطوبت بالا (0.5-0.8)
427
- """, unsafe_allow_html=True)
428
- elif selected_index in ['LAI', 'Biomass']:
429
- st.markdown("""
430
- - <span style='color:white; background-color:black'>سفید</span>: مقدار کم (0-1)
431
- - <span style='color:lightgreen'>سبز روشن</span>: مقدار متوسط (1-3)
432
- - <span style='color:darkgreen'>سبز تیره</span>: مقدار زیاد (3-5)
433
- """, unsafe_allow_html=True)
434
-
435
- # محاسبه میانگین شاخص برای مزرعه انتخاب شده
436
- mean_value = imagery.select(selected_index).reduceRegion(
 
 
 
 
 
 
 
 
 
 
 
 
437
  reducer=ee.Reducer.mean(),
438
  geometry=aoi,
439
- scale=10
440
- ).get(selected_index).getInfo()
441
-
442
- if mean_value is not None:
443
- st.metric(f"میانگین {selected_index} مزرعه {selected_farm}", f"{mean_value:.4f}")
444
- else:
445
- st.warning("تصویر ماهواره‌ای مناسبی برای بازه زمانی انتخاب شده یافت نشد. لطفاً بازه زمانی دیگری را انتخاب کنید.")
446
-
447
- with tab2:
448
- if apply_filters and selected_farm:
449
- # دریافت مختصات مزرعه انتخاب شده
450
- farm_data = filtered_df[filtered_df['مزرعه'] == selected_farm].iloc[0]
451
- center_lat = farm_data['عرض جغرافیایی']
452
- center_lon = farm_data['طول جغرافیایی']
453
-
454
- # ایجاد منطقه مورد نظر (AOI)
455
- point = ee.Geometry.Point([center_lon, center_lat])
456
- aoi = point.buffer(500)
457
-
458
- # انتخاب بازه زمانی برای نمودار
459
- time_interval = st.radio("انتخاب بازه زمانی", ["روزانه", "هفتگی", "ماهانه"], horizontal=True)
460
- interval_map = {"روزانه": "day", "هفتگی": "week", "ماهانه": "month"}
461
-
462
- # ایجاد نمودار زمانی
463
- time_series_df = create_time_series(aoi, selected_index, start_date, end_date, interval_map[time_interval])
464
-
465
- if time_series_df is not None and not time_series_df.empty:
466
- fig = px.line(time_series_df, x='Date', y=selected_index,
467
- title=f"نمودار زمانی {selected_index} برای مزرعه {selected_farm}")
468
- fig.update_layout(xaxis_title="تاریخ", yaxis_title=selected_index)
469
- st.plotly_chart(fig, use_container_width=True)
470
-
471
- # نمایش داده‌های نمودار
472
- st.dataframe(time_series_df)
473
-
474
- # دکمه دانلود داده‌های نمودار
475
- csv = time_series_df.to_csv(index=False)
476
- b64 = base64.b64encode(csv.encode()).decode()
477
- href = f'<a href="data:file/csv;base64,{b64}" download="time_series_data.csv">دانلود داده‌های نمودار</a>'
478
- st.markdown(href, unsafe_allow_html=True)
479
- else:
480
- st.warning("داده‌ای برای نمایش نمودار زمانی یافت نشد. لطفاً بازه زمانی دیگری را انتخاب کنید.")
481
-
482
- with tab3:
483
- if apply_filters:
484
- st.subheader("مقایسه مزارع")
485
-
486
- # انتخاب مزارع برای مقایسه
487
- farms_to_compare = st.multiselect("انتخاب مزارع برای مقایسه", farm_ids, default=[selected_farm] if selected_farm else [])
488
-
489
- if farms_to_compare:
490
- # ایجاد دیکشنری برای ذخیره مقادیر شاخص‌ها
491
- index_values = {}
492
-
493
- # محاسبه مقادیر شاخص‌ها برای هر مزرعه
494
- for farm_id in farms_to_compare:
495
- farm_data = filtered_df[filtered_df['مزرعه'] == farm_id].iloc[0]
496
- center_lat = farm_data['عرض جغرافیایی']
497
- center_lon = farm_data['طول جغرافیایی']
498
-
499
- point = ee.Geometry.Point([center_lon, center_lat])
500
- aoi = point.buffer(500)
501
-
502
- imagery = get_sentinel_imagery(start_date.strftime('%Y-%m-%d'),
503
- end_date.strftime('%Y-%m-%d'),
504
- aoi)
505
-
506
- if imagery is not None:
507
- # محاسبه میانگین شاخص‌ها
508
- values = {}
509
- for index_name in indices:
510
- mean_value = imagery.select(index_name).reduceRegion(
511
- reducer=ee.Reducer.mean(),
512
- geometry=aoi,
513
- scale=10
514
- ).get(index_name).getInfo()
515
-
516
- if mean_value is not None:
517
- values[index_name] = mean_value
518
-
519
- index_values[farm_id] = values
520
-
521
- # ایجاد دیتافریم برای مقایسه
522
- comparison_data = []
523
- for farm_id in farms_to_compare:
524
- if farm_id in index_values:
525
- row = {'مزرعه': farm_id}
526
- row.update(index_values[farm_id])
527
- comparison_data.append(row)
528
-
529
- if comparison_data:
530
- comparison_df = pd.DataFrame(comparison_data)
531
-
532
- # نمایش جدول مقایسه
533
- st.dataframe(comparison_df)
534
-
535
- # ایجاد نمودار مقایسه‌ای
536
- if len(comparison_df) > 1: # اگر بیش از یک مزرعه انتخاب شده باشد
537
- fig = go.Figure()
538
-
539
- for index_name in indices:
540
- if index_name in comparison_df.columns:
541
- fig.add_trace(go.Bar(
542
- x=comparison_df['مزرعه'],
543
- y=comparison_df[index_name],
544
- name=index_name
545
- ))
546
-
547
- fig.update_layout(
548
- title="مقایسه شاخص‌ها بین مزارع",
549
- xaxis_title="مزرعه",
550
- yaxis_title="مقدار شاخص",
551
- barmode='group'
552
- )
553
-
554
- st.plotly_chart(fig, use_container_width=True)
555
-
556
- # دکمه دانلود داده‌های مقایسه
557
- csv = comparison_df.to_csv(index=False)
558
- b64 = base64.b64encode(csv.encode()).decode()
559
- href = f'<a href="data:file/csv;base64,{b64}" download="comparison_data.csv">دانلود داده‌های مقایسه</a>'
560
- st.markdown(href, unsafe_allow_html=True)
561
  else:
562
- st.warning("داده‌ای برای مقایسه مزارع یافت نشد.")
563
- else:
564
- st.info("لطفاً حداقل یک مزرعه را برای مقایسه انتخاب کنید.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
- # اجرای برنامه
 
 
567
  if __name__ == "__main__":
568
- main()
 
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()