Esmaeilkiani commited on
Commit
fa57651
·
verified ·
1 Parent(s): 6aba287

Update app.py

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