YashMK89 commited on
Commit
f757fb8
·
verified ·
1 Parent(s): e810f2a

update app.py

Browse files
Files changed (1) hide show
  1. app.py +513 -100
app.py CHANGED
@@ -7,18 +7,15 @@ import geopandas as gpd
7
  from datetime import datetime
8
  import leafmap.foliumap as leafmap
9
  import re
10
- from shapely.geometry import base, Polygon
11
  from xml.etree import ElementTree as XET
12
- from concurrent.futures import ThreadPoolExecutor
13
- import zipfile
14
- import io
15
- import time
16
 
17
  # Set up the page layout
18
  st.set_page_config(layout="wide")
19
 
20
  # Custom button styling
21
- st.markdown(
22
  """
23
  <style>
24
  div.stButton > button:first-child {
@@ -57,120 +54,536 @@ with open(os.path.expanduser("~/.config/earthengine/credentials"), "w") as f:
57
  f.write(earthengine_credentials)
58
  ee.Initialize(project='ee-yashsacisro24')
59
 
60
- # User Inputs
61
- st.markdown("<h4 style='text-align: center;'>User Inputs</h4>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- # Select Imagery Base
 
 
 
64
  imagery_base = st.selectbox("Select Imagery Base", ["Sentinel", "Landsat", "MODIS", "Custom Input"], index=0)
65
- dataset_file = None
66
 
67
- if imagery_base == "Custom Input":
 
 
 
 
 
 
68
  custom_dataset_id = st.text_input("Enter Custom Earth Engine Dataset ID (e.g., AHN/AHN4)", value="")
69
  if custom_dataset_id:
70
  try:
 
 
71
  collection = ee.ImageCollection(custom_dataset_id)
72
  band_names = collection.first().bandNames().getInfo()
73
- dataset_file = {custom_dataset_id: {"bands": band_names}}
 
 
 
 
 
74
  st.write(f"Fetched bands for {custom_dataset_id}: {', '.join(band_names)}")
75
  except Exception as e:
76
- st.error(f"Error fetching dataset: {str(e)}. Please check the dataset ID.")
 
 
 
 
 
 
 
 
 
77
  else:
78
- dataset_file = f"{imagery_base.lower()}_datasets.json"
79
- with open(dataset_file) as f:
80
- dataset_file = json.load(f)
81
-
82
- # Select Dataset
83
- main_selection = st.selectbox(f"Select {imagery_base} Dataset Category", list(dataset_file.keys()))
84
- sub_selection = st.selectbox("Select Specific Dataset", list(dataset_file[main_selection].keys()))
85
- selected_bands = st.multiselect("Select Bands", dataset_file[main_selection][sub_selection]["bands"])
86
-
87
- # Custom Formula
88
- custom_formula = st.text_input("Enter Custom Formula (e.g., (B8 - B4) / (B8 + B4))", value="(B8 - B4) / (B8 + B4)")
89
-
90
- # Reducer and Aggregation Period
91
- reducer_choice = st.selectbox("Select Reducer", ["mean", "sum", "median", "min", "max", "count"], index=0)
92
- aggregation_period = st.selectbox("Select Aggregation Period", ["Custom", "Weekly", "Monthly", "Yearly", "Biweekly"], index=0)
93
-
94
- # Geometry Upload
95
- shape_type = st.selectbox("Point or Polygon?", ["Point", "Polygon"])
96
- file_upload = st.file_uploader("Upload Geometry (CSV, GeoJSON, KML, ZIP)", type=["csv", "geojson", "kml", "zip"])
97
-
98
- # Parse Uploaded File
99
- locations_df = None
100
- if file_upload:
101
- if file_upload.name.endswith(".zip"):
102
- with zipfile.ZipFile(file_upload) as z:
103
- for filename in z.namelist():
104
- if filename.endswith((".csv", ".geojson", ".kml")):
105
- file_upload = z.open(filename)
106
- break
107
- if file_upload.name.endswith(".csv"):
108
- locations_df = pd.read_csv(file_upload)
109
- elif file_upload.name.endswith(".geojson"):
110
- locations_df = gpd.read_file(file_upload)
111
- elif file_upload.name.endswith(".kml"):
112
- kml_string = file_upload.read().decode("utf-8")
113
- root = XET.fromstring(kml_string)
114
- ns = {"kml": "http://www.opengis.net/kml/2.2"}
115
- geometries = []
116
- for placemark in root.findall(".//kml:Placemark", ns):
117
- coords_elem = placemark.find(".//kml:coordinates", ns)
118
- if coords_elem is not None:
119
- coords_text = coords_elem.text.strip()
120
- coords = [tuple(map(float, coord.split(","))) for coord in coords_text.split()]
121
- geometries.append(Polygon(coords))
122
- locations_df = gpd.GeoDataFrame(geometry=geometries, crs="EPSG:4326")
123
-
124
- # Process Aggregation
125
- def process_aggregation_parallel(locations_df, start_date, end_date, dataset_id, selected_bands, reducer_choice, shape_type, aggregation_period, custom_formula):
126
- results = []
127
- total_steps = len(locations_df)
128
- progress_bar = st.progress(0)
129
- progress_text = st.empty()
130
 
131
- def process_location(row):
132
- try:
133
- roi = convert_to_ee_geometry(row.geometry)
134
- collection = ee.ImageCollection(dataset_id).filterDate(start_date, end_date).filterBounds(roi)
135
- aggregated_collection = aggregate_data(collection, aggregation_period)
136
- calculated_values = []
137
- for image in aggregated_collection.toList(aggregated_collection.size()).getInfo():
138
- index_image = calculate_custom_formula(image, roi, selected_bands, custom_formula, reducer_choice)
139
- calculated_values.append(index_image.getInfo())
140
- return calculated_values
141
- except Exception as e:
142
- st.error(f"Error processing location: {e}")
143
- return []
144
-
145
- with ThreadPoolExecutor() as executor:
146
- futures = [executor.submit(process_location, row) for _, row in locations_df.iterrows()]
147
- for idx, future in enumerate(futures):
148
- results.extend(future.result())
149
- progress_percentage = (idx + 1) / total_steps
150
- progress_bar.progress(progress_percentage)
151
- progress_text.markdown(f"Processing: {int(progress_percentage * 100)}%")
152
- return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- # Button to Trigger Calculation
155
- if st.button("Calculate"):
156
- if locations_df is not None:
157
- results = process_aggregation_parallel(
158
  locations_df,
159
- start_date.strftime("%Y-%m-%d"),
160
- end_date.strftime("%Y-%m-%d"),
161
- sub_selection,
162
  selected_bands,
163
  reducer_choice,
164
  shape_type,
165
  aggregation_period,
166
- custom_formula
 
 
167
  )
168
  if results:
169
  result_df = pd.DataFrame(results)
 
170
  st.dataframe(result_df)
 
171
  st.download_button(
172
- label="Download Results as CSV",
173
- data=result_df.to_csv(index=False).encode("utf-8"),
174
- file_name=f"results_{datetime.now().strftime('%Y%m%d')}.csv",
175
- mime="text/csv"
176
- )
 
 
 
 
 
 
7
  from datetime import datetime
8
  import leafmap.foliumap as leafmap
9
  import re
10
+ from shapely.geometry import base
11
  from xml.etree import ElementTree as XET
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
 
 
 
13
 
14
  # Set up the page layout
15
  st.set_page_config(layout="wide")
16
 
17
  # Custom button styling
18
+ m = st.markdown(
19
  """
20
  <style>
21
  div.stButton > button:first-child {
 
54
  f.write(earthengine_credentials)
55
  ee.Initialize(project='ee-yashsacisro24')
56
 
57
+ # Helper function to get reducer
58
+ def get_reducer(reducer_name):
59
+ reducers = {
60
+ 'mean': ee.Reducer.mean(),
61
+ 'sum': ee.Reducer.sum(),
62
+ 'median': ee.Reducer.median(),
63
+ 'min': ee.Reducer.min(),
64
+ 'max': ee.Reducer.max(),
65
+ 'count': ee.Reducer.count(),
66
+ }
67
+ return reducers.get(reducer_name.lower(), ee.Reducer.mean())
68
+
69
+ # Function to convert geometry to Earth Engine format
70
+ def convert_to_ee_geometry(geometry):
71
+ if isinstance(geometry, base.BaseGeometry):
72
+ if geometry.is_valid:
73
+ geojson = geometry.__geo_interface__
74
+ return ee.Geometry(geojson)
75
+ else:
76
+ raise ValueError("Invalid geometry: The polygon geometry is not valid.")
77
+ elif isinstance(geometry, dict) or isinstance(geometry, str):
78
+ try:
79
+ if isinstance(geometry, str):
80
+ geometry = json.loads(geometry)
81
+ if 'type' in geometry and 'coordinates' in geometry:
82
+ return ee.Geometry(geometry)
83
+ else:
84
+ raise ValueError("GeoJSON format is invalid.")
85
+ except Exception as e:
86
+ raise ValueError(f"Error parsing GeoJSON: {e}")
87
+ elif isinstance(geometry, str) and geometry.lower().endswith(".kml"):
88
+ try:
89
+ tree = XET.parse(geometry)
90
+ kml_root = tree.getroot()
91
+ kml_namespace = {'kml': 'http://www.opengis.net/kml/2.2'}
92
+ coordinates = kml_root.findall(".//kml:coordinates", kml_namespace)
93
+ if coordinates:
94
+ coords_text = coordinates[0].text.strip()
95
+ coords = coords_text.split()
96
+ coords = [tuple(map(float, coord.split(','))) for coord in coords]
97
+ geojson = {"type": "Polygon", "coordinates": [coords]}
98
+ return ee.Geometry(geojson)
99
+ else:
100
+ raise ValueError("KML does not contain valid coordinates.")
101
+ except Exception as e:
102
+ raise ValueError(f"Error parsing KML: {e}")
103
+ else:
104
+ raise ValueError("Unsupported geometry input type. Supported types are Shapely, GeoJSON, and KML.")
105
+
106
+ # Function to calculate custom formula
107
+ def calculate_custom_formula(image, geometry, selected_bands, custom_formula, reducer_choice, scale=30):
108
+ try:
109
+ band_values = {}
110
+ band_names = image.bandNames().getInfo()
111
+ for band in selected_bands:
112
+ if band not in band_names:
113
+ raise ValueError(f"Band '{band}' not found in the dataset.")
114
+ band_values[band] = image.select(band)
115
+ reducer = get_reducer(reducer_choice)
116
+ reduced_values = {}
117
+ for band in selected_bands:
118
+ value = band_values[band].reduceRegion(
119
+ reducer=reducer,
120
+ geometry=geometry,
121
+ scale=scale
122
+ ).get(band).getInfo()
123
+ reduced_values[band] = float(value if value is not None else 0)
124
+ formula = custom_formula
125
+ for band in selected_bands:
126
+ formula = formula.replace(band, str(reduced_values[band]))
127
+ result = eval(formula, {"__builtins__": {}}, reduced_values)
128
+ if not isinstance(result, (int, float)):
129
+ raise ValueError("Formula did not result in a numeric value.")
130
+ return ee.Image.constant(result).rename('custom_result')
131
+ except ZeroDivisionError:
132
+ st.error("Error: Division by zero in the formula.")
133
+ return ee.Image(0).rename('custom_result').set('error', 'Division by zero')
134
+ except SyntaxError:
135
+ st.error(f"Error: Invalid syntax in formula '{custom_formula}'.")
136
+ return ee.Image(0).rename('custom_result').set('error', 'Invalid syntax')
137
+ except ValueError as e:
138
+ st.error(f"Error: {str(e)}")
139
+ return ee.Image(0).rename('custom_result').set('error', str(e))
140
+ except Exception as e:
141
+ st.error(f"Unexpected error: {e}")
142
+ return ee.Image(0).rename('custom_result').set('error', str(e))
143
+
144
+ # Aggregation functions
145
+ def aggregate_data_custom(collection):
146
+ collection = collection.map(lambda image: image.set('day', ee.Date(image.get('system:time_start')).format('YYYY-MM-dd')))
147
+ grouped_by_day = collection.aggregate_array('day').distinct()
148
+ def calculate_daily_mean(day):
149
+ daily_collection = collection.filter(ee.Filter.eq('day', day))
150
+ daily_mean = daily_collection.mean()
151
+ return daily_mean.set('day', day)
152
+ daily_images = ee.List(grouped_by_day.map(calculate_daily_mean))
153
+ return ee.ImageCollection(daily_images)
154
+
155
+ def aggregate_data_weekly(collection):
156
+ def set_week_start(image):
157
+ date = ee.Date(image.get('system:time_start'))
158
+ days_since_week_start = date.getRelative('day', 'week')
159
+ offset = ee.Number(days_since_week_start).multiply(-1)
160
+ week_start = date.advance(offset, 'day')
161
+ return image.set('week_start', week_start.format('YYYY-MM-dd'))
162
+ collection = collection.map(set_week_start)
163
+ grouped_by_week = collection.aggregate_array('week_start').distinct()
164
+ def calculate_weekly_mean(week_start):
165
+ weekly_collection = collection.filter(ee.Filter.eq('week_start', week_start))
166
+ weekly_mean = weekly_collection.mean()
167
+ return weekly_mean.set('week_start', week_start)
168
+ weekly_images = ee.List(grouped_by_week.map(calculate_weekly_mean))
169
+ return ee.ImageCollection(weekly_images)
170
+
171
+ def aggregate_data_monthly(collection, start_date, end_date):
172
+ collection = collection.filterDate(start_date, end_date)
173
+ collection = collection.map(lambda image: image.set('month', ee.Date(image.get('system:time_start')).format('YYYY-MM')))
174
+ grouped_by_month = collection.aggregate_array('month').distinct()
175
+ def calculate_monthly_mean(month):
176
+ monthly_collection = collection.filter(ee.Filter.eq('month', month))
177
+ monthly_mean = monthly_collection.mean()
178
+ return monthly_mean.set('month', month)
179
+ monthly_images = ee.List(grouped_by_month.map(calculate_monthly_mean))
180
+ return ee.ImageCollection(monthly_images)
181
+
182
+ def aggregate_data_yearly(collection):
183
+ collection = collection.map(lambda image: image.set('year', ee.Date(image.get('system:time_start')).format('YYYY')))
184
+ grouped_by_year = collection.aggregate_array('year').distinct()
185
+ def calculate_yearly_mean(year):
186
+ yearly_collection = collection.filter(ee.Filter.eq('year', year))
187
+ yearly_mean = yearly_collection.mean()
188
+ return yearly_mean.set('year', year)
189
+ yearly_images = ee.List(grouped_by_year.map(calculate_yearly_mean))
190
+ return ee.ImageCollection(yearly_images)
191
+
192
+ # Worker function for processing a single geometry
193
+ def process_single_geometry(row, start_date_str, end_date_str, dataset_id, selected_bands, reducer_choice, shape_type, aggregation_period, custom_formula, kernel_size=None, include_boundary=None):
194
+ if shape_type.lower() == "point":
195
+ latitude = row.get('latitude')
196
+ longitude = row.get('longitude')
197
+ if pd.isna(latitude) or pd.isna(longitude):
198
+ return None # Skip invalid points
199
+ location_name = row.get('name', f"Location_{row.name}")
200
+ if kernel_size == "3x3 Kernel":
201
+ buffer_size = 45 # 90m x 90m
202
+ roi = ee.Geometry.Point([longitude, latitude]).buffer(buffer_size).bounds()
203
+ elif kernel_size == "5x5 Kernel":
204
+ buffer_size = 75 # 150m x 150m
205
+ roi = ee.Geometry.Point([longitude, latitude]).buffer(buffer_size).bounds()
206
+ else: # Point
207
+ roi = ee.Geometry.Point([longitude, latitude])
208
+ elif shape_type.lower() == "polygon":
209
+ polygon_geometry = row.get('geometry')
210
+ location_name = row.get('name', f"Polygon_{row.name}")
211
+ try:
212
+ roi = convert_to_ee_geometry(polygon_geometry)
213
+ if not include_boundary:
214
+ roi = roi.buffer(-30).bounds()
215
+ except ValueError:
216
+ return None # Skip invalid polygons
217
+
218
+ # Filter and aggregate the image collection
219
+ collection = ee.ImageCollection(dataset_id) \
220
+ .filterDate(ee.Date(start_date_str), ee.Date(end_date_str)) \
221
+ .filterBounds(roi)
222
+
223
+ if aggregation_period.lower() == 'custom (start date to end date)':
224
+ collection = aggregate_data_custom(collection)
225
+ elif aggregation_period.lower() == 'weekly':
226
+ collection = aggregate_data_weekly(collection)
227
+ elif aggregation_period.lower() == 'monthly':
228
+ collection = aggregate_data_monthly(collection, start_date_str, end_date_str)
229
+ elif aggregation_period.lower() == 'yearly':
230
+ collection = aggregate_data_yearly(collection)
231
+
232
+ # Process each image in the collection
233
+ image_list = collection.toList(collection.size())
234
+ processed_weeks = set()
235
+ aggregated_results = []
236
+
237
+ for i in range(image_list.size().getInfo()):
238
+ image = ee.Image(image_list.get(i))
239
+ if aggregation_period.lower() == 'custom (start date to end date)':
240
+ timestamp = image.get('day')
241
+ period_label = 'Date'
242
+ date = ee.Date(timestamp).format('YYYY-MM-dd').getInfo()
243
+ elif aggregation_period.lower() == 'weekly':
244
+ timestamp = image.get('week_start')
245
+ period_label = 'Week'
246
+ date = ee.String(timestamp).getInfo()
247
+ if (pd.to_datetime(date) < pd.to_datetime(start_date_str) or
248
+ pd.to_datetime(date) > pd.to_datetime(end_date_str) or
249
+ date in processed_weeks):
250
+ continue
251
+ processed_weeks.add(date)
252
+ elif aggregation_period.lower() == 'monthly':
253
+ timestamp = image.get('month')
254
+ period_label = 'Month'
255
+ date = ee.Date(timestamp).format('YYYY-MM').getInfo()
256
+ elif aggregation_period.lower() == 'yearly':
257
+ timestamp = image.get('year')
258
+ period_label = 'Year'
259
+ date = ee.Date(timestamp).format('YYYY').getInfo()
260
+
261
+ index_image = calculate_custom_formula(image, roi, selected_bands, custom_formula, reducer_choice, scale=30)
262
+ try:
263
+ index_value = index_image.reduceRegion(
264
+ reducer=get_reducer(reducer_choice),
265
+ geometry=roi,
266
+ scale=30
267
+ ).get('custom_result')
268
+ calculated_value = index_value.getInfo()
269
+ if isinstance(calculated_value, (int, float)):
270
+ result = {
271
+ 'Location Name': location_name,
272
+ period_label: date,
273
+ 'Start Date': start_date_str,
274
+ 'End Date': end_date_str,
275
+ 'Calculated Value': calculated_value
276
+ }
277
+ if shape_type.lower() == 'point':
278
+ result['Latitude'] = latitude
279
+ result['Longitude'] = longitude
280
+ aggregated_results.append(result)
281
+ except Exception as e:
282
+ st.error(f"Error retrieving value for {location_name}: {e}")
283
+
284
+ return aggregated_results
285
+
286
+ # Main processing function
287
+ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id, selected_bands, reducer_choice, shape_type, aggregation_period, custom_formula="", kernel_size=None, include_boundary=None):
288
+ aggregated_results = []
289
+ total_steps = len(locations_df)
290
+ progress_bar = st.progress(0)
291
+ progress_text = st.empty()
292
+
293
+ with ThreadPoolExecutor(max_workers=10) as executor:
294
+ futures = []
295
+ for idx, row in locations_df.iterrows():
296
+ future = executor.submit(
297
+ process_single_geometry,
298
+ row,
299
+ start_date_str,
300
+ end_date_str,
301
+ dataset_id,
302
+ selected_bands,
303
+ reducer_choice,
304
+ shape_type,
305
+ aggregation_period,
306
+ custom_formula,
307
+ kernel_size,
308
+ include_boundary
309
+ )
310
+ futures.append(future)
311
+
312
+ completed = 0
313
+ for future in as_completed(futures):
314
+ result = future.result()
315
+ if result:
316
+ aggregated_results.extend(result)
317
+ completed += 1
318
+ progress_percentage = completed / total_steps
319
+ progress_bar.progress(progress_percentage)
320
+ progress_text.markdown(f"Processing: {int(progress_percentage * 100)}%")
321
+
322
+ if aggregated_results:
323
+ result_df = pd.DataFrame(aggregated_results)
324
+ if aggregation_period.lower() == 'custom (start date to end date)':
325
+ agg_dict = {
326
+ 'Start Date': 'first',
327
+ 'End Date': 'first',
328
+ 'Calculated Value': 'mean'
329
+ }
330
+ if shape_type.lower() == 'point':
331
+ agg_dict['Latitude'] = 'first'
332
+ agg_dict['Longitude'] = 'first'
333
+ aggregated_output = result_df.groupby('Location Name').agg(agg_dict).reset_index()
334
+ aggregated_output.rename(columns={'Calculated Value': 'Aggregated Value'}, inplace=True)
335
+ return aggregated_output.to_dict(orient='records')
336
+ else:
337
+ return result_df.to_dict(orient='records')
338
 
339
+ return []
340
+
341
+ # Streamlit App Logic
342
+ st.markdown("<h5>Image Collection</h5>", unsafe_allow_html=True)
343
  imagery_base = st.selectbox("Select Imagery Base", ["Sentinel", "Landsat", "MODIS", "Custom Input"], index=0)
 
344
 
345
+ if imagery_base == "Sentinel":
346
+ dataset_file = "sentinel_datasets.json"
347
+ elif imagery_base == "Landsat":
348
+ dataset_file = "landsat_datasets.json"
349
+ elif imagery_base == "MODIS":
350
+ dataset_file = "modis_datasets.json"
351
+ elif imagery_base == "Custom Input":
352
  custom_dataset_id = st.text_input("Enter Custom Earth Engine Dataset ID (e.g., AHN/AHN4)", value="")
353
  if custom_dataset_id:
354
  try:
355
+ if custom_dataset_id.startswith("ee.ImageCollection("):
356
+ custom_dataset_id = custom_dataset_id.replace("ee.ImageCollection('", "").replace("')", "")
357
  collection = ee.ImageCollection(custom_dataset_id)
358
  band_names = collection.first().bandNames().getInfo()
359
+ data = {
360
+ f"Custom Dataset: {custom_dataset_id}": {
361
+ "sub_options": {custom_dataset_id: f"Custom Dataset ({custom_dataset_id})"},
362
+ "bands": {custom_dataset_id: band_names}
363
+ }
364
+ }
365
  st.write(f"Fetched bands for {custom_dataset_id}: {', '.join(band_names)}")
366
  except Exception as e:
367
+ st.error(f"Error fetching dataset: {str(e)}. Please check the dataset ID and ensure it's valid in Google Earth Engine.")
368
+ data = {}
369
+ else:
370
+ st.warning("Please enter a custom dataset ID to proceed.")
371
+ data = {}
372
+
373
+ st.markdown("<hr><h5><b>{}</b></h5>".format(imagery_base), unsafe_allow_html=True)
374
+
375
+ if data:
376
+ main_selection = st.selectbox(f"Select {imagery_base} Dataset Category", list(data.keys()))
377
  else:
378
+ main_selection = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
+ sub_selection = None
381
+ dataset_id = None
382
+
383
+ if main_selection:
384
+ sub_options = data[main_selection]["sub_options"]
385
+ sub_selection = st.selectbox(f"Select Specific {imagery_base} Dataset ID", list(sub_options.keys()))
386
+ if sub_selection:
387
+ st.write(f"You selected: {main_selection} -> {sub_options[sub_selection]}")
388
+ st.write(f"Dataset ID: {sub_selection}")
389
+ dataset_id = sub_selection
390
+
391
+ st.markdown("<hr><h5><b>Earth Engine Index Calculator</b></h5>", unsafe_allow_html=True)
392
+
393
+ if main_selection and sub_selection:
394
+ dataset_bands = data[main_selection]["bands"].get(sub_selection, [])
395
+ st.write(f"Available Bands for {sub_options[sub_selection]}: {', '.join(dataset_bands)}")
396
+ selected_bands = st.multiselect(
397
+ "Select 1 or 2 Bands for Calculation",
398
+ options=dataset_bands,
399
+ default=[dataset_bands[0]] if dataset_bands else [],
400
+ help=f"Select 1 or 2 bands from: {', '.join(dataset_bands)}"
401
+ )
402
+
403
+ if len(selected_bands) < 1:
404
+ st.warning("Please select at least one band.")
405
+ st.stop()
406
+
407
+ if selected_bands:
408
+ if len(selected_bands) == 1:
409
+ default_formula = f"{selected_bands[0]}"
410
+ example = f"'{selected_bands[0]} * 2' or '{selected_bands[0]} + 1'"
411
+ else:
412
+ default_formula = f"({selected_bands[0]} - {selected_bands[1]}) / ({selected_bands[0]} + {selected_bands[1]})"
413
+ example = f"'{selected_bands[0]} * {selected_bands[1]} / 2' or '({selected_bands[0]} - {selected_bands[1]}) / ({selected_bands[0]} + {selected_bands[1]})'"
414
+ custom_formula = st.text_input(
415
+ "Enter Custom Formula (e.g (B8 - B4) / (B8 + B4) , B4*B3/2)",
416
+ value=default_formula,
417
+ help=f"Use only these bands: {', '.join(selected_bands)}. Examples: {example}"
418
+ )
419
+
420
+ def validate_formula(formula, selected_bands):
421
+ allowed_chars = set(" +-*/()0123456789.")
422
+ terms = re.findall(r'[a-zA-Z][a-zA-Z0-9_]*', formula)
423
+ invalid_terms = [term for term in terms if term not in selected_bands]
424
+ if invalid_terms:
425
+ return False, f"Invalid terms in formula: {', '.join(invalid_terms)}. Use only {', '.join(selected_bands)}."
426
+ if not all(char in allowed_chars or char in ''.join(selected_bands) for char in formula):
427
+ return False, "Formula contains invalid characters. Use only bands, numbers, and operators (+, -, *, /, ())"
428
+ return True, ""
429
+
430
+ is_valid, error_message = validate_formula(custom_formula, selected_bands)
431
+ if not is_valid:
432
+ st.error(error_message)
433
+ st.stop()
434
+ elif not custom_formula:
435
+ st.warning("Please enter a custom formula to proceed.")
436
+ st.stop()
437
+
438
+ st.write(f"Custom Formula: {custom_formula}")
439
+
440
+ reducer_choice = st.selectbox(
441
+ "Select Reducer (e.g, mean , sum , median , min , max , count)",
442
+ ['mean', 'sum', 'median', 'min', 'max', 'count'],
443
+ index=0
444
+ )
445
+
446
+ start_date = st.date_input("Start Date", value=pd.to_datetime('2024-11-01'))
447
+ end_date = st.date_input("End Date", value=pd.to_datetime('2024-12-01'))
448
+ start_date_str = start_date.strftime('%Y-%m-%d')
449
+ end_date_str = end_date.strftime('%Y-%m-%d')
450
+
451
+ aggregation_period = st.selectbox(
452
+ "Select Aggregation Period (e.g, Custom(Start Date to End Date) , Weekly , Monthly , Yearly)",
453
+ ["Custom (Start Date to End Date)", "Weekly", "Monthly", "Yearly"],
454
+ index=0
455
+ )
456
+
457
+ shape_type = st.selectbox("Do you want to process 'Point' or 'Polygon' data?", ["Point", "Polygon"])
458
+
459
+ kernel_size = None
460
+ include_boundary = None
461
+
462
+ if shape_type.lower() == "point":
463
+ kernel_size = st.selectbox(
464
+ "Select Calculation Area(e.g, Point , 3x3 Kernel , 5x5 Kernel)",
465
+ ["Point", "3x3 Kernel", "5x5 Kernel"],
466
+ index=0,
467
+ help="Choose 'Point' for exact point calculation, or a kernel size for area averaging."
468
+ )
469
+ elif shape_type.lower() == "polygon":
470
+ include_boundary = st.checkbox(
471
+ "Include Boundary Pixels",
472
+ value=True,
473
+ help="Check to include pixels on the polygon boundary; uncheck to exclude them."
474
+ )
475
+
476
+ file_upload = st.file_uploader(f"Upload your {shape_type} data (CSV, GeoJSON, KML)", type=["csv", "geojson", "kml"])
477
+ locations_df = pd.DataFrame()
478
+
479
+ if file_upload is not None:
480
+ if shape_type.lower() == "point":
481
+ if file_upload.name.endswith('.csv'):
482
+ locations_df = pd.read_csv(file_upload)
483
+ elif file_upload.name.endswith('.geojson'):
484
+ locations_df = gpd.read_file(file_upload)
485
+ elif file_upload.name.endswith('.kml'):
486
+ kml_string = file_upload.read().decode('utf-8')
487
+ try:
488
+ root = XET.fromstring(kml_string)
489
+ ns = {'kml': 'http://www.opengis.net/kml/2.2'}
490
+ points = []
491
+ for placemark in root.findall('.//kml:Placemark', ns):
492
+ name = placemark.findtext('kml:name', default=f"Point_{len(points)}", namespaces=ns)
493
+ coords_elem = placemark.find('.//kml:Point/kml:coordinates', ns)
494
+ if coords_elem is not None:
495
+ coords_text = coords_elem.text.strip()
496
+ coords = [c.strip() for c in coords_text.split(',')]
497
+ if len(coords) >= 2:
498
+ lon, lat = float(coords[0]), float(coords[1])
499
+ points.append({'name': name, 'geometry': f"POINT ({lon} {lat})"})
500
+ if not points:
501
+ st.error("No valid Point data found in the KML file.")
502
+ else:
503
+ locations_df = gpd.GeoDataFrame(points, geometry=gpd.GeoSeries.from_wkt([p['geometry'] for p in points]), crs="EPSG:4326")
504
+ except Exception as e:
505
+ st.error(f"Error parsing KML file: {str(e)}")
506
+ elif shape_type.lower() == "polygon":
507
+ if file_upload.name.endswith('.csv'):
508
+ locations_df = pd.read_csv(file_upload)
509
+ elif file_upload.name.endswith('.geojson'):
510
+ locations_df = gpd.read_file(file_upload)
511
+ elif file_upload.name.endswith('.kml'):
512
+ kml_string = file_upload.read().decode('utf-8')
513
+ try:
514
+ root = XET.fromstring(kml_string)
515
+ ns = {'kml': 'http://www.opengis.net/kml/2.2'}
516
+ polygons = []
517
+ for placemark in root.findall('.//kml:Placemark', ns):
518
+ name = placemark.findtext('kml:name', default=f"Polygon_{len(polygons)}", namespaces=ns)
519
+ coords_elem = placemark.find('.//kml:Polygon//kml:coordinates', ns)
520
+ if coords_elem is not None:
521
+ coords_text = ' '.join(coords_elem.text.split())
522
+ coord_pairs = [pair.split(',')[:2] for pair in coords_text.split() if pair]
523
+ if len(coord_pairs) >= 4:
524
+ coords_str = " ".join([f"{float(lon)} {float(lat)}" for lon, lat in coord_pairs])
525
+ polygons.append({'name': name, 'geometry': f"POLYGON (({coords_str}))"})
526
+ if not polygons:
527
+ st.error("No valid Polygon data found in the KML file.")
528
+ else:
529
+ locations_df = gpd.GeoDataFrame(polygons, geometry=gpd.GeoSeries.from_wkt([p['geometry'] for p in polygons]), crs="EPSG:4326")
530
+ except Exception as e:
531
+ st.error(f"Error parsing KML file: {str(e)}")
532
+
533
+ if not locations_df.empty:
534
+ if 'geometry' in locations_df.columns:
535
+ if shape_type.lower() == "point":
536
+ locations_df['latitude'] = locations_df['geometry'].y
537
+ locations_df['longitude'] = locations_df['geometry'].x
538
+ m = leafmap.Map(center=[locations_df['latitude'].mean(), locations_df['longitude'].mean()], zoom=10)
539
+ for _, row in locations_df.iterrows():
540
+ latitude = row['latitude']
541
+ longitude = row['longitude']
542
+ if pd.isna(latitude) or pd.isna(longitude):
543
+ continue
544
+ m.add_marker(location=[latitude, longitude], popup=row.get('name', 'No Name'))
545
+ st.write("Map of Uploaded Points:")
546
+ m.to_streamlit()
547
+ elif shape_type.lower() == "polygon":
548
+ centroid_lat = locations_df.geometry.centroid.y.mean()
549
+ centroid_lon = locations_df.geometry.centroid.x.mean()
550
+ m = leafmap.Map(center=[centroid_lat, centroid_lon], zoom=10)
551
+ for _, row in locations_df.iterrows():
552
+ polygon = row['geometry']
553
+ if polygon.is_valid:
554
+ gdf = gpd.GeoDataFrame([row], geometry=[polygon], crs=locations_df.crs)
555
+ m.add_gdf(gdf=gdf, layer_name=row.get('name', 'Unnamed Polygon'))
556
+ st.write("Map of Uploaded Polygons:")
557
+ m.to_streamlit()
558
 
559
+ if st.button(f"Calculate {custom_formula}"):
560
+ if not locations_df.empty:
561
+ results = process_aggregation(
 
562
  locations_df,
563
+ start_date_str,
564
+ end_date_str,
565
+ dataset_id,
566
  selected_bands,
567
  reducer_choice,
568
  shape_type,
569
  aggregation_period,
570
+ custom_formula,
571
+ kernel_size,
572
+ include_boundary
573
  )
574
  if results:
575
  result_df = pd.DataFrame(results)
576
+ st.write(f"Processed Results Table ({aggregation_period}) for Formula: {custom_formula}")
577
  st.dataframe(result_df)
578
+ filename = f"{main_selection}_{dataset_id}_{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}_{aggregation_period.lower()}.csv"
579
  st.download_button(
580
+ label="Download results as CSV",
581
+ data=result_df.to_csv(index=False).encode('utf-8'),
582
+ file_name=filename,
583
+ mime='text/csv'
584
+ )
585
+ st.success('Processing complete!')
586
+ else:
587
+ st.warning("No results were generated. Check your inputs or formula.")
588
+ else:
589
+ st.warning("Please upload a file to proceed.")