Spaces:
Configuration error
Configuration error
Update app.py
Browse files
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
|
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 |
-
|
13 |
import json
|
14 |
-
import
|
15 |
-
|
16 |
-
|
|
|
|
|
17 |
|
18 |
-
#
|
19 |
st.set_page_config(
|
20 |
page_title="داشبورد مانیتورینگ مزارع نیشکر دهخدا",
|
21 |
-
page_icon="
|
22 |
-
layout="wide",
|
23 |
-
initial_sidebar_state="expanded"
|
24 |
)
|
25 |
|
26 |
-
#
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
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 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
except Exception as e:
|
90 |
-
st.error(f"
|
91 |
-
|
92 |
|
93 |
-
#
|
94 |
-
@st.cache_data
|
95 |
-
def
|
|
|
96 |
try:
|
97 |
-
df = pd.read_csv(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
return df
|
|
|
|
|
|
|
99 |
except Exception as e:
|
100 |
-
st.error(f"خطا در
|
101 |
-
|
|
|
|
|
102 |
|
103 |
-
|
104 |
-
|
105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
|
107 |
-
|
108 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
117 |
-
|
118 |
-
|
|
|
|
|
|
|
119 |
ndmi = image.normalizedDifference(['B8', 'B11']).rename('NDMI')
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
#
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
return
|
157 |
-
|
158 |
-
#
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
#
|
170 |
-
|
171 |
-
|
172 |
-
#
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
#
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
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 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
if not
|
283 |
-
st.
|
284 |
-
|
285 |
-
|
286 |
-
#
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
#
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
#
|
302 |
-
|
303 |
-
|
304 |
-
#
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
#
|
331 |
-
|
332 |
-
|
333 |
-
#
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
#
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
if
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
437 |
reducer=ee.Reducer.mean(),
|
438 |
geometry=aoi,
|
439 |
-
scale=
|
440 |
-
).
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
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 |
-
|
563 |
-
|
564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|