YashMK89 commited on
Commit
1994f45
·
verified ·
1 Parent(s): 129b8c8

update app.py

Browse files
Files changed (1) hide show
  1. app.py +256 -313
app.py CHANGED
@@ -30,8 +30,8 @@ m = st.markdown(
30
  st.write(
31
  f"""
32
  <div style="display: flex; justify-content: space-between; align-items: center;">
33
- <img src="https://huggingface.co/spaces/YashMK89/GEE_Calculator/resolve/main/ISRO_Logo.png" style="width: 20%; margin-right: auto;">
34
- <img src="https://huggingface.co/spaces/YashMK89/GEE_Calculator/resolve/main/SAC_Logo.png" style="width: 20%; margin-left: auto;">
35
  </div>
36
  """,
37
  unsafe_allow_html=True,
@@ -44,7 +44,7 @@ st.markdown(
44
  """,
45
  unsafe_allow_html=True,
46
  )
47
- st.write("<h2><div style='text-align: center;'>User Inputs</div></h2>", unsafe_allow_html=True)
48
 
49
  # Authenticate and initialize Earth Engine
50
  earthengine_credentials = os.environ.get("EE_Authentication")
@@ -55,66 +55,105 @@ with open(os.path.expanduser("~/.config/earthengine/credentials"), "w") as f:
55
  f.write(earthengine_credentials)
56
 
57
  ee.Initialize(project='ee-yashsacisro24')
 
 
58
 
59
- # Load the Sentinel dataset options from JSON file
60
- with open("sentinel_datasets.json") as f:
 
 
 
 
 
61
  data = json.load(f)
62
 
63
  # Display the title for the Streamlit app
64
- st.title("Sentinel Dataset")
65
 
66
  # Select dataset category (main selection)
67
- main_selection = st.selectbox("Select Sentinel Dataset Category", list(data.keys()))
 
 
 
 
68
 
69
  # If a category is selected, display the sub-options (specific datasets)
70
  if main_selection:
71
  sub_options = data[main_selection]["sub_options"]
72
- sub_selection = st.selectbox("Select Specific Dataset ID", list(sub_options.keys()))
73
 
74
  # Display the selected dataset ID based on user input
75
  if sub_selection:
76
- st.write(f"You selected: {main_selection} -> {sub_selection}")
77
- st.write(f"Dataset ID: {sub_options[sub_selection]}")
78
-
79
- # Fetch the correct dataset ID from the sub-selection
80
- dataset_id = sub_options[sub_selection]
81
 
82
  # Earth Engine Index Calculator Section
83
  st.header("Earth Engine Index Calculator")
84
 
85
- index_choice = st.selectbox("Select an Index or Enter Custom Formula", ['NDVI', 'NDWI', 'Average NO₂', 'Custom Formula'])
86
-
87
- # Initialize custom_formula variable
88
- custom_formula = ""
89
-
90
- # Display corresponding formula based on the index selected (case-insensitive)
91
- if index_choice.lower() == 'ndvi':
92
- st.write("Formula for NDVI: NDVI = (B8 - B4) / (B8 + B4)")
93
- elif index_choice.lower() == 'ndwi':
94
- st.write("Formula for NDWI: NDWI = (B3 - B8) / (B3 + B8)")
95
- elif index_choice.lower() == 'average no₂':
96
- st.write("Formula for Average NO₂: Average NO₂ = Mean(NO2 band)")
97
- elif index_choice.lower() == 'custom formula':
98
- custom_formula = st.text_input("Enter Custom Formula (e.g., B5,B4 for two bands or B3 for one band)")
99
- # Check if custom formula is empty and show warning
100
- if not custom_formula:
101
- st.warning("Please enter a custom formula before proceeding.")
102
- else:
103
- # Check if the input contains a comma (indicating two bands)
104
- if ',' in custom_formula:
105
- # Split the input into two bands and strip whitespace
106
- band1, band2 = [b.strip() for b in custom_formula.split(',', 1)]
107
- st.write(f"Custom Formula: ({band1} - {band2}) / ({band1} + {band2})")
108
- else:
109
- # Single band case
110
- band = custom_formula.strip()
111
- st.write(f"Custom Formula: {band}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  # Function to get the corresponding reducer based on user input
114
  def get_reducer(reducer_name):
115
- """
116
- Map user-friendly reducer names to Earth Engine reducer objects.
117
- """
118
  reducers = {
119
  'mean': ee.Reducer.mean(),
120
  'sum': ee.Reducer.sum(),
@@ -123,91 +162,52 @@ def get_reducer(reducer_name):
123
  'max': ee.Reducer.max(),
124
  'count': ee.Reducer.count(),
125
  }
126
-
127
- # Default to 'mean' if the reducer_name is not recognized
128
  return reducers.get(reducer_name.lower(), ee.Reducer.mean())
129
 
130
  # Streamlit selectbox for reducer choice
131
  reducer_choice = st.selectbox(
132
- "Select Reducer",
133
  ['mean', 'sum', 'median', 'min', 'max', 'count'],
134
  index=0 # Default to 'mean'
135
  )
136
 
 
137
  def convert_to_ee_geometry(geometry):
138
- # Handle Shapely geometry
139
  if isinstance(geometry, base.BaseGeometry):
140
  if geometry.is_valid:
141
  geojson = geometry.__geo_interface__
142
- print("Shapely GeoJSON:", geojson) # Debugging: Inspect the GeoJSON structure
143
  return ee.Geometry(geojson)
144
  else:
145
  raise ValueError("Invalid geometry: The polygon geometry is not valid.")
146
-
147
- # Handle GeoJSON input (string or dictionary)
148
  elif isinstance(geometry, dict) or isinstance(geometry, str):
149
  try:
150
  if isinstance(geometry, str):
151
  geometry = json.loads(geometry)
152
  if 'type' in geometry and 'coordinates' in geometry:
153
- print("GeoJSON Geometry:", geometry) # Debugging: Inspect the GeoJSON structure
154
  return ee.Geometry(geometry)
155
  else:
156
  raise ValueError("GeoJSON format is invalid.")
157
  except Exception as e:
158
  raise ValueError(f"Error parsing GeoJSON: {e}")
159
-
160
- # Handle KML input (string or file path)
161
  elif isinstance(geometry, str) and geometry.lower().endswith(".kml"):
162
  try:
163
- # Parse the KML file
164
  tree = ET.parse(geometry)
165
  kml_root = tree.getroot()
166
-
167
- # Extract coordinates from KML geometry (assuming it's a Polygon or MultiPolygon)
168
- # KML coordinates are usually within the <coordinates> tag
169
  kml_namespace = {'kml': 'http://www.opengis.net/kml/2.2'}
170
  coordinates = kml_root.findall(".//kml:coordinates", kml_namespace)
171
-
172
  if coordinates:
173
- # Extract and format coordinates
174
  coords_text = coordinates[0].text.strip()
175
  coords = coords_text.split()
176
- # Convert KML coordinates (comma-separated) into a list of tuples
177
  coords = [tuple(map(float, coord.split(','))) for coord in coords]
178
- geojson = {
179
- "type": "Polygon", # Make sure the GeoJSON type is Polygon
180
- "coordinates": [coords] # Wrap the coordinates in a list (required by GeoJSON format)
181
- }
182
-
183
- # Debugging: Inspect the KML-to-GeoJSON structure
184
- print("KML GeoJSON:", geojson)
185
-
186
  return ee.Geometry(geojson)
187
  else:
188
  raise ValueError("KML does not contain valid coordinates.")
189
  except Exception as e:
190
  raise ValueError(f"Error parsing KML: {e}")
191
-
192
  else:
193
  raise ValueError("Unsupported geometry input type. Supported types are Shapely, GeoJSON, and KML.")
194
 
195
- # Function to read points from CSV
196
- def read_csv(file_path):
197
- df = pd.read_csv(file_path)
198
- return df
199
-
200
- # Function to read points from GeoJSON
201
- def read_geojson(file_path):
202
- gdf = gpd.read_file(file_path)
203
- return gdf
204
-
205
- # Function to read points from KML
206
- def read_kml(file_path):
207
- gdf = gpd.read_file(file_path, driver='KML')
208
- return gdf
209
-
210
-
211
  # Date Input for Start and End Dates
212
  start_date = st.date_input("Start Date", value=pd.to_datetime('2024-11-01'))
213
  end_date = st.date_input("End Date", value=pd.to_datetime('2024-12-01'))
@@ -217,18 +217,38 @@ start_date_str = start_date.strftime('%Y-%m-%d')
217
  end_date_str = end_date.strftime('%Y-%m-%d')
218
 
219
  # Aggregation period selection
220
- aggregation_period = st.selectbox("Select Aggregation Period", ["Daily", "Weekly", "Monthly", "Yearly"], index=0)
 
 
 
 
221
 
222
- # Ask user whether they want to process 'Point' or 'Polygon' data (case-insensitive)
223
  shape_type = st.selectbox("Do you want to process 'Point' or 'Polygon' data?", ["Point", "Polygon"])
224
 
225
- # Ask user to upload a file based on shape type (case-insensitive)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  file_upload = st.file_uploader(f"Upload your {shape_type} data (CSV, GeoJSON, KML)", type=["csv", "geojson", "kml"])
227
 
228
  if file_upload is not None:
229
  # Read the user-uploaded file
230
  if shape_type.lower() == "point":
231
- # Handle different file types for Point data
232
  if file_upload.name.endswith('.csv'):
233
  locations_df = pd.read_csv(file_upload)
234
  elif file_upload.name.endswith('.geojson'):
@@ -239,53 +259,34 @@ if file_upload is not None:
239
  st.error("Unsupported file format. Please upload CSV, GeoJSON, or KML.")
240
  locations_df = pd.DataFrame()
241
 
242
- # Check if the file contains polygons when the user selected "Point"
243
  if 'geometry' in locations_df.columns:
244
- # Check if the geometry type is Polygon or MultiPolygon
245
  if locations_df.geometry.geom_type.isin(['Polygon', 'MultiPolygon']).any():
246
  st.warning("The uploaded file contains polygon data. Please select 'Polygon' for processing.")
247
- st.stop() # Stop further processing if polygons are detected
248
 
249
- # Processing the point data
250
  with st.spinner('Processing Map...'):
251
  if locations_df is not None and not locations_df.empty:
252
- # For GeoJSON data, the coordinates are in the geometry column
253
  if 'geometry' in locations_df.columns:
254
- # Extract latitude and longitude from the geometry column
255
  locations_df['latitude'] = locations_df['geometry'].y
256
  locations_df['longitude'] = locations_df['geometry'].x
257
 
258
- # Ensure the necessary columns exist in the dataframe
259
  if 'latitude' not in locations_df.columns or 'longitude' not in locations_df.columns:
260
  st.error("Uploaded file is missing required 'latitude' or 'longitude' columns.")
261
  else:
262
- # Display a preview of the points data
263
  st.write("Preview of the uploaded points data:")
264
  st.dataframe(locations_df.head())
265
-
266
- # Create a LeafMap object to display the points
267
  m = leafmap.Map(center=[locations_df['latitude'].mean(), locations_df['longitude'].mean()], zoom=10)
268
-
269
- # Add points to the map using a loop
270
  for _, row in locations_df.iterrows():
271
  latitude = row['latitude']
272
  longitude = row['longitude']
273
-
274
- # Check if latitude or longitude are NaN and skip if they are
275
  if pd.isna(latitude) or pd.isna(longitude):
276
- continue # Skip this row and move to the next one
277
-
278
  m.add_marker(location=[latitude, longitude], popup=row.get('name', 'No Name'))
279
-
280
- # Display map
281
  st.write("Map of Uploaded Points:")
282
  m.to_streamlit()
283
-
284
- # Store the map in session_state
285
  st.session_state.map_data = m
286
 
287
  elif shape_type.lower() == "polygon":
288
- # Handle different file types for Polygon data:
289
  if file_upload.name.endswith('.csv'):
290
  locations_df = pd.read_csv(file_upload)
291
  elif file_upload.name.endswith('.geojson'):
@@ -296,229 +297,172 @@ if file_upload is not None:
296
  st.error("Unsupported file format. Please upload CSV, GeoJSON, or KML.")
297
  locations_df = pd.DataFrame()
298
 
299
- # Check if the file contains points when the user selected "Polygon"
300
  if 'geometry' in locations_df.columns:
301
- # Check if the geometry type is Point or MultiPoint
302
  if locations_df.geometry.geom_type.isin(['Point', 'MultiPoint']).any():
303
  st.warning("The uploaded file contains point data. Please select 'Point' for processing.")
304
- st.stop() # Stop further processing if point data is detected
305
 
306
- # Processing the polygon data
307
  with st.spinner('Processing Map...'):
308
  if locations_df is not None and not locations_df.empty:
309
- # Ensure the 'geometry' column exists in the dataframe
310
  if 'geometry' not in locations_df.columns:
311
  st.error("Uploaded file is missing required 'geometry' column.")
312
  else:
313
- # Display a preview of the polygons data
314
  st.write("Preview of the uploaded polygons data:")
315
  st.dataframe(locations_df.head())
316
-
317
- # Create a LeafMap object to display the polygons
318
- # Calculate the centroid of the polygons for the map center
319
  centroid_lat = locations_df.geometry.centroid.y.mean()
320
  centroid_lon = locations_df.geometry.centroid.x.mean()
321
-
322
  m = leafmap.Map(center=[centroid_lat, centroid_lon], zoom=10)
323
-
324
- # Add polygons to the map using a loop
325
  for _, row in locations_df.iterrows():
326
  polygon = row['geometry']
327
- if polygon.is_valid: # Check if polygon is valid
328
- # Create a GeoDataFrame for this polygon
329
  gdf = gpd.GeoDataFrame([row], geometry=[polygon], crs=locations_df.crs)
330
  m.add_gdf(gdf=gdf, layer_name=row.get('name', 'Unnamed Polygon'))
331
-
332
- # Display map
333
  st.write("Map of Uploaded Polygons:")
334
  m.to_streamlit()
335
-
336
- # Store the map in session_state
337
  st.session_state.map_data = m
338
 
339
- # Initialize session state for storing results if not already done
340
  if 'results' not in st.session_state:
341
  st.session_state.results = []
342
  if 'last_params' not in st.session_state:
343
  st.session_state.last_params = {}
344
  if 'map_data' not in st.session_state:
345
- st.session_state.map_data = None # Initialize map_data
 
 
346
 
347
  # Function to check if parameters have changed
348
  def parameters_changed():
349
  return (
350
  st.session_state.last_params.get('main_selection') != main_selection or
351
  st.session_state.last_params.get('dataset_id') != dataset_id or
352
- st.session_state.last_params.get('index_choice') != index_choice or
 
353
  st.session_state.last_params.get('start_date_str') != start_date_str or
354
  st.session_state.last_params.get('end_date_str') != end_date_str or
355
  st.session_state.last_params.get('shape_type') != shape_type or
356
- st.session_state.last_params.get('file_upload') != file_upload
 
 
357
  )
358
 
359
  # If parameters have changed, reset the results
360
  if parameters_changed():
361
- st.session_state.results = [] # Clear the previous results
362
  st.session_state.last_params = {
363
  'main_selection': main_selection,
364
  'dataset_id': dataset_id,
365
- 'index_choice': index_choice,
 
366
  'start_date_str': start_date_str,
367
  'end_date_str': end_date_str,
368
  'shape_type': shape_type,
369
- 'file_upload': file_upload
 
 
370
  }
371
 
372
- # Function to calculate NDVI with the selected reducer
373
- def calculate_ndvi(image, geometry, reducer_choice):
374
- ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
375
- return ndvi
376
-
377
- # Function to calculate NDWI
378
- def calculate_ndwi(image, geometry, reducer_choice):
379
- ndwi = image.normalizedDifference(['B3', 'B8']).rename('NDWI')
380
- return ndwi
381
-
382
- def calculate_custom_formula(image, geometry, custom_formula, reducer_choice, scale=30):
383
  try:
384
- if "," in custom_formula:
385
- band1, band2 = [b.strip() for b in custom_formula.split(",")]
386
- band_names = image.bandNames().getInfo()
387
- if band1 not in band_names or band2 not in band_names:
388
- raise ValueError(f"One or both bands ({band1}, {band2}) do not exist in the image.")
389
- result = image.normalizedDifference([band1, band2]).rename('custom_formula')
390
- else:
391
- band = custom_formula.strip()
392
- band_names = image.bandNames().getInfo()
393
  if band not in band_names:
394
- raise ValueError(f"The band '{band}' does not exist in the image.")
395
- result = image.select(band).rename('custom_formula')
396
- return result
397
- except Exception as e:
398
- return ee.Image(0).rename('custom_formula').set('error', str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
- # Modify aggregation functions to return the correct time period and aggregated results
401
- def aggregate_data_daily(collection):
402
- # Extract day from the image date (using the exact date)
403
- collection = collection.map(lambda image: image.set('day', ee.Date(image.get('system:time_start')).format('YYYY-MM-dd')))
404
 
405
- # Group images by day (distinct days)
406
- grouped_by_day = collection.aggregate_array('day').distinct()
 
 
 
 
 
 
 
 
 
 
407
 
 
 
 
 
 
 
 
 
408
  def calculate_daily_mean(day):
409
- # Filter the collection by the specific day
410
  daily_collection = collection.filter(ee.Filter.eq('day', day))
411
- daily_mean = daily_collection.mean() # Calculate mean for the day
412
  return daily_mean.set('day', day)
413
-
414
- # Calculate the daily mean for each day
415
  daily_images = ee.List(grouped_by_day.map(calculate_daily_mean))
416
-
417
  return ee.ImageCollection(daily_images)
418
 
419
  def aggregate_data_weekly(collection):
420
- # Extract the start date of the week from the image date
421
- collection = collection.map(lambda image: image.set(
422
- 'week_start', ee.Date(image.get('system:time_start'))
423
- .advance(-ee.Date(image.get('system:time_start')).getRelative('day', 'week'), 'day')
424
- ))
425
- # Group images by week start date
 
426
  grouped_by_week = collection.aggregate_array('week_start').distinct()
427
-
428
  def calculate_weekly_mean(week_start):
429
- # Filter the collection by the specific week start date
430
  weekly_collection = collection.filter(ee.Filter.eq('week_start', week_start))
431
- weekly_mean = weekly_collection.mean() # Calculate mean for the week
432
  return weekly_mean.set('week_start', week_start)
433
-
434
- # Calculate the weekly mean for each week
435
  weekly_images = ee.List(grouped_by_week.map(calculate_weekly_mean))
436
  return ee.ImageCollection(weekly_images)
437
-
438
  def aggregate_data_monthly(collection, start_date, end_date):
439
- # Filter the collection for the specific date range
440
  collection = collection.filterDate(start_date, end_date)
441
-
442
- # Extract month and year from the image date
443
  collection = collection.map(lambda image: image.set('month', ee.Date(image.get('system:time_start')).format('YYYY-MM')))
444
-
445
- # Group images by month
446
  grouped_by_month = collection.aggregate_array('month').distinct()
447
-
448
  def calculate_monthly_mean(month):
449
  monthly_collection = collection.filter(ee.Filter.eq('month', month))
450
  monthly_mean = monthly_collection.mean()
451
  return monthly_mean.set('month', month)
452
-
453
- # Calculate the monthly mean for each month
454
  monthly_images = ee.List(grouped_by_month.map(calculate_monthly_mean))
455
-
456
  return ee.ImageCollection(monthly_images)
457
-
458
  def aggregate_data_yearly(collection):
459
- # Extract year from the image date
460
  collection = collection.map(lambda image: image.set('year', ee.Date(image.get('system:time_start')).format('YYYY')))
461
-
462
- # Group images by year
463
  grouped_by_year = collection.aggregate_array('year').distinct()
464
-
465
  def calculate_yearly_mean(year):
466
  yearly_collection = collection.filter(ee.Filter.eq('year', year))
467
  yearly_mean = yearly_collection.mean()
468
  return yearly_mean.set('year', year)
469
-
470
- # Calculate the yearly mean for each year
471
  yearly_images = ee.List(grouped_by_year.map(calculate_yearly_mean))
472
-
473
  return ee.ImageCollection(yearly_images)
474
 
475
- # Function to calculate index based on the selected choice
476
- def calculate_index_for_period(image, roi, index_choice, reducer_choice, custom_formula):
477
- if index_choice.lower() == 'ndvi':
478
- return calculate_ndvi(image, roi, reducer_choice)
479
- elif index_choice.lower() == 'ndwi':
480
- return calculate_ndwi(image, roi, reducer_choice)
481
- elif index_choice.lower() == 'average no₂':
482
- mean_no2 = image.select('NO2').mean().rename('Average NO₂')
483
- return mean_no2
484
- elif index_choice.lower() == 'custom formula':
485
- # Pass the custom formula here, not the index_choice
486
- return calculate_custom_formula(image, roi, custom_formula, reducer_choice)
487
- else:
488
- st.write("Please Select any one option...."+ index_choice.lower())
489
-
490
- def aggregate_data_weekly(collection):
491
- def set_week_start(image):
492
- # Get the image timestamp
493
- date = ee.Date(image.get('system:time_start'))
494
- # Calculate days since the start of the week (0 = Monday, 6 = Sunday)
495
- days_since_week_start = date.getRelative('day', 'week')
496
- # Convert to ee.Number and negate it to get the offset to the week start
497
- offset = ee.Number(days_since_week_start).multiply(-1)
498
- # Advance the date by the negative offset to get the week start
499
- week_start = date.advance(offset, 'day')
500
- return image.set('week_start', week_start.format('YYYY-MM-dd')) # Ensure string format
501
-
502
- # Apply the week start calculation to each image
503
- collection = collection.map(set_week_start)
504
-
505
- # Group images by week start date
506
- grouped_by_week = collection.aggregate_array('week_start').distinct()
507
-
508
- def calculate_weekly_mean(week_start):
509
- # Filter the collection by the specific week start date
510
- weekly_collection = collection.filter(ee.Filter.eq('week_start', week_start))
511
- weekly_mean = weekly_collection.mean() # Calculate mean for the week
512
- return weekly_mean.set('week_start', week_start)
513
-
514
- # Calculate the weekly mean for each week
515
- weekly_images = ee.List(grouped_by_week.map(calculate_weekly_mean))
516
- return ee.ImageCollection(weekly_images)
517
-
518
- def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id, index_choice, reducer_choice, shape_type, aggregation_period, custom_formula=""):
519
  aggregated_results = []
520
 
521
- if index_choice.lower() == 'custom_formula' and not custom_formula:
522
  st.error("Custom formula cannot be empty. Please provide a formula.")
523
  return aggregated_results
524
 
@@ -536,15 +480,22 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
536
  continue
537
 
538
  location_name = row.get('name', f"Location_{idx}")
539
- roi = ee.Geometry.Point([longitude, latitude])
 
 
 
 
 
 
 
 
540
 
541
  collection = ee.ImageCollection(dataset_id) \
542
  .filterDate(ee.Date(start_date_str), ee.Date(end_date_str)) \
543
  .filterBounds(roi)
544
 
545
- # Aggregate data based on the selected period
546
- if aggregation_period.lower() == 'daily':
547
- collection = aggregate_data_daily(collection)
548
  elif aggregation_period.lower() == 'weekly':
549
  collection = aggregate_data_weekly(collection)
550
  elif aggregation_period.lower() == 'monthly':
@@ -552,21 +503,19 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
552
  elif aggregation_period.lower() == 'yearly':
553
  collection = aggregate_data_yearly(collection)
554
 
555
- # Process each image in the collection
556
  image_list = collection.toList(collection.size())
557
- processed_weeks = set() # Track processed weeks to avoid duplicates
558
  for i in range(image_list.size().getInfo()):
559
  image = ee.Image(image_list.get(i))
560
 
561
- if aggregation_period.lower() == 'daily':
562
  timestamp = image.get('day')
563
  period_label = 'Date'
564
  date = ee.Date(timestamp).format('YYYY-MM-dd').getInfo()
565
  elif aggregation_period.lower() == 'weekly':
566
  timestamp = image.get('week_start')
567
  period_label = 'Week'
568
- date = ee.String(timestamp).getInfo() # Already formatted as YYYY-MM-dd
569
- # Skip if week is outside the date range or already processed
570
  if (pd.to_datetime(date) < pd.to_datetime(start_date_str) or
571
  pd.to_datetime(date) > pd.to_datetime(end_date_str) or
572
  date in processed_weeks):
@@ -581,14 +530,14 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
581
  period_label = 'Year'
582
  date = ee.Date(timestamp).format('YYYY').getInfo()
583
 
584
- index_image = calculate_index_for_period(image, roi, index_choice, reducer_choice, custom_formula)
585
 
586
  try:
587
  index_value = index_image.reduceRegion(
588
  reducer=get_reducer(reducer_choice),
589
  geometry=roi,
590
  scale=30
591
- ).get(index_image.bandNames().get(0))
592
 
593
  calculated_value = index_value.getInfo()
594
 
@@ -619,6 +568,8 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
619
 
620
  try:
621
  roi = convert_to_ee_geometry(polygon_geometry)
 
 
622
  except ValueError as e:
623
  st.warning(f"Skipping invalid polygon {polygon_name}: {e}")
624
  continue
@@ -627,9 +578,8 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
627
  .filterDate(ee.Date(start_date_str), ee.Date(end_date_str)) \
628
  .filterBounds(roi)
629
 
630
- # Aggregate data based on the selected period
631
- if aggregation_period.lower() == 'daily':
632
- collection = aggregate_data_daily(collection)
633
  elif aggregation_period.lower() == 'weekly':
634
  collection = aggregate_data_weekly(collection)
635
  elif aggregation_period.lower() == 'monthly':
@@ -637,21 +587,19 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
637
  elif aggregation_period.lower() == 'yearly':
638
  collection = aggregate_data_yearly(collection)
639
 
640
- # Process each image in the collection
641
  image_list = collection.toList(collection.size())
642
- processed_weeks = set() # Track processed weeks to avoid duplicates
643
  for i in range(image_list.size().getInfo()):
644
  image = ee.Image(image_list.get(i))
645
 
646
- if aggregation_period.lower() == 'daily':
647
  timestamp = image.get('day')
648
  period_label = 'Date'
649
  date = ee.Date(timestamp).format('YYYY-MM-dd').getInfo()
650
  elif aggregation_period.lower() == 'weekly':
651
  timestamp = image.get('week_start')
652
  period_label = 'Week'
653
- date = ee.String(timestamp).getInfo() # Already formatted as YYYY-MM-dd
654
- # Skip if week is outside the date range or already processed
655
  if (pd.to_datetime(date) < pd.to_datetime(start_date_str) or
656
  pd.to_datetime(date) > pd.to_datetime(end_date_str) or
657
  date in processed_weeks):
@@ -666,14 +614,14 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
666
  period_label = 'Year'
667
  date = ee.Date(timestamp).format('YYYY').getInfo()
668
 
669
- index_image = calculate_index_for_period(image, roi, index_choice, reducer_choice, custom_formula)
670
 
671
  try:
672
  index_value = index_image.reduceRegion(
673
  reducer=get_reducer(reducer_choice),
674
  geometry=roi,
675
  scale=30
676
- ).get(index_image.bandNames().get(0))
677
 
678
  calculated_value = index_value.getInfo()
679
 
@@ -696,81 +644,76 @@ def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id,
696
 
697
  if aggregated_results:
698
  result_df = pd.DataFrame(aggregated_results)
699
- if aggregation_period.lower() == 'daily':
700
- aggregated_output = result_df.groupby('Location Name').agg({
701
- 'Latitude': 'first' if shape_type.lower() == 'point' else None,
702
- 'Longitude': 'first' if shape_type.lower() == 'point' else None,
703
  'Start Date': 'first',
704
  'End Date': 'first',
705
  'Calculated Value': 'mean'
706
- }).reset_index()
707
- # Remove None columns (for polygons)
708
- aggregated_output = aggregated_output[[col for col in aggregated_output.columns if col is not None]]
 
 
709
  aggregated_output.rename(columns={'Calculated Value': 'Aggregated Value'}, inplace=True)
710
  return aggregated_output.to_dict(orient='records')
711
  else:
712
  return result_df.to_dict(orient='records')
713
-
714
  return []
715
 
716
- # When the user clicks the process button, start the calculation
717
- if st.button(f"Calculate ({index_choice})"):
718
  if file_upload is not None:
719
- if shape_type.lower() == "point":
720
- results = process_aggregation(
721
- locations_df,
722
- start_date_str,
723
- end_date_str,
724
- dataset_id,
725
- index_choice,
726
- reducer_choice,
727
- shape_type,
728
- aggregation_period,
729
- custom_formula
730
- )
731
- if results:
732
- result_df = pd.DataFrame(results)
733
- st.write(f"Processed Results Table ({aggregation_period}):")
734
- st.dataframe(result_df)
735
- filename = f"{main_selection}_{dataset_id}_{start_date.strftime('%Y/%m/%d')}_{end_date.strftime('%Y/%m/%d')}_{aggregation_period.lower()}.csv"
736
- st.download_button(
737
- label="Download results as CSV",
738
- data=result_df.to_csv(index=False).encode('utf-8'),
739
- file_name=filename,
740
- mime='text/csv'
741
- )
742
- st.spinner('')
743
- st.success('Processing complete!')
744
- else:
745
- st.warning("No results were generated.")
746
-
747
- elif shape_type.lower() == "polygon":
748
  results = process_aggregation(
749
  locations_df,
750
  start_date_str,
751
  end_date_str,
752
  dataset_id,
753
- index_choice,
754
  reducer_choice,
755
  shape_type,
756
  aggregation_period,
757
- custom_formula
 
 
758
  )
759
  if results:
760
  result_df = pd.DataFrame(results)
761
- st.write(f"Processed Results Table ({aggregation_period}):")
762
  st.dataframe(result_df)
763
- filename = f"{main_selection}_{dataset_id}_{start_date.strftime('%Y/%m/%d')}_{end_date.strftime('%Y/%m/%d')}_{aggregation_period.lower()}.csv"
764
  st.download_button(
765
  label="Download results as CSV",
766
  data=result_df.to_csv(index=False).encode('utf-8'),
767
  file_name=filename,
768
  mime='text/csv'
769
  )
770
- st.spinner('')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  st.success('Processing complete!')
772
  else:
773
- st.warning("No results were generated.")
774
-
775
  else:
776
- st.warning("Please upload a file.")
 
 
 
30
  st.write(
31
  f"""
32
  <div style="display: flex; justify-content: space-between; align-items: center;">
33
+ <img src="https://huggingface.co/spaces/YashMK89/GEE_Calculator/resolve/main/ISRO_Logo.png" style="width: 20%; margin-right: auto;">
34
+ <img src="https://huggingface.co/spaces/YashMK89/GEE_Calculator/resolve/main/SAC_Logo.png" style="width: 20%; margin-left: auto;">
35
  </div>
36
  """,
37
  unsafe_allow_html=True,
 
44
  """,
45
  unsafe_allow_html=True,
46
  )
47
+ st.write("<h2><div style='text-align: center;'>User Inputs</div></h2>", unsafe_allow_html=True)
48
 
49
  # Authenticate and initialize Earth Engine
50
  earthengine_credentials = os.environ.get("EE_Authentication")
 
55
  f.write(earthengine_credentials)
56
 
57
  ee.Initialize(project='ee-yashsacisro24')
58
+ # Imagery base selection
59
+ imagery_base = st.selectbox("Select Imagery Base", ["Sentinel", "Landsat"], index=0)
60
 
61
+ # Load the appropriate dataset based on imagery base
62
+ if imagery_base == "Sentinel":
63
+ dataset_file = "sentinel_datasets.json"
64
+ else:
65
+ dataset_file = "landsat_datasets.json"
66
+
67
+ with open(dataset_file) as f:
68
  data = json.load(f)
69
 
70
  # Display the title for the Streamlit app
71
+ st.title(f"{imagery_base} Dataset")
72
 
73
  # Select dataset category (main selection)
74
+ main_selection = st.selectbox(f"Select {imagery_base} Dataset Category", list(data.keys()))
75
+
76
+ # Initialize sub_selection and dataset_id as None
77
+ sub_selection = None
78
+ dataset_id = None
79
 
80
  # If a category is selected, display the sub-options (specific datasets)
81
  if main_selection:
82
  sub_options = data[main_selection]["sub_options"]
83
+ sub_selection = st.selectbox(f"Select Specific {imagery_base} Dataset ID", list(sub_options.keys()))
84
 
85
  # Display the selected dataset ID based on user input
86
  if sub_selection:
87
+ st.write(f"You selected: {main_selection} -> {sub_options[sub_selection]}")
88
+ st.write(f"Dataset ID: {sub_selection}")
89
+ dataset_id = sub_selection # Use the key directly as the dataset ID
 
 
90
 
91
  # Earth Engine Index Calculator Section
92
  st.header("Earth Engine Index Calculator")
93
 
94
+ # Load band information based on selected dataset
95
+ if main_selection and sub_selection: # Now safe because sub_selection is initialized
96
+ dataset_bands = data[main_selection]["bands"].get(sub_selection, [])
97
+ st.write(f"Available Bands for {sub_options[sub_selection]}: {', '.join(dataset_bands)}")
98
+
99
+ # Allow user to select 1 or 2 bands
100
+ selected_bands = st.multiselect(
101
+ "Select 1 or 2 Bands for Calculation",
102
+ options=dataset_bands,
103
+ default=[dataset_bands[0]] if dataset_bands else [],
104
+ help=f"Select 1 or 2 bands from: {', '.join(dataset_bands)}"
105
+ )
106
+
107
+ # Ensure minimum 1 and maximum 2 bands are selected
108
+ if len(selected_bands) < 1:
109
+ st.warning("Please select at least one band.")
110
+ st.stop()
111
+ elif len(selected_bands) > 2:
112
+ st.warning("You can select a maximum of 2 bands.")
113
+ st.stop()
114
+
115
+ # Show custom formula input if bands are selected
116
+ if selected_bands:
117
+ # Provide a default formula based on the number of selected bands
118
+ if len(selected_bands) == 1:
119
+ default_formula = f"{selected_bands[0]}"
120
+ example = f"'{selected_bands[0]} * 2' or '{selected_bands[0]} + 1'"
121
+ else: # len(selected_bands) == 2
122
+ default_formula = f"({selected_bands[0]} - {selected_bands[1]}) / ({selected_bands[0]} + {selected_bands[1]})"
123
+ example = f"'{selected_bands[0]} * {selected_bands[1]} / 2' or '({selected_bands[0]} - {selected_bands[1]}) / ({selected_bands[0]} + {selected_bands[1]})'"
124
+
125
+ custom_formula = st.text_input(
126
+ "Enter Custom Formula",
127
+ value=default_formula,
128
+ help=f"Use only these bands: {', '.join(selected_bands)}. Examples: {example}"
129
+ )
130
+
131
+ # Validate the formula
132
+ def validate_formula(formula, selected_bands):
133
+ allowed_chars = set(" +-*/()0123456789.")
134
+ terms = re.findall(r'[a-zA-Z][a-zA-Z0-9_]*', formula)
135
+ invalid_terms = [term for term in terms if term not in selected_bands]
136
+ if invalid_terms:
137
+ return False, f"Invalid terms in formula: {', '.join(invalid_terms)}. Use only {', '.join(selected_bands)}."
138
+ if not all(char in allowed_chars or char in ''.join(selected_bands) for char in formula):
139
+ return False, "Formula contains invalid characters. Use only bands, numbers, and operators (+, -, *, /, ())"
140
+ return True, ""
141
+
142
+ is_valid, error_message = validate_formula(custom_formula, selected_bands)
143
+ if not is_valid:
144
+ st.error(error_message)
145
+ st.stop()
146
+ elif not custom_formula:
147
+ st.warning("Please enter a custom formula to proceed.")
148
+ st.stop()
149
+
150
+ # Display the validated formula
151
+ st.write(f"Custom Formula: {custom_formula}")
152
+
153
+ # The rest of your code (reducer, geometry conversion, date input, aggregation, etc.) remains unchanged...
154
 
155
  # Function to get the corresponding reducer based on user input
156
  def get_reducer(reducer_name):
 
 
 
157
  reducers = {
158
  'mean': ee.Reducer.mean(),
159
  'sum': ee.Reducer.sum(),
 
162
  'max': ee.Reducer.max(),
163
  'count': ee.Reducer.count(),
164
  }
 
 
165
  return reducers.get(reducer_name.lower(), ee.Reducer.mean())
166
 
167
  # Streamlit selectbox for reducer choice
168
  reducer_choice = st.selectbox(
169
+ "Select Reducer (e.g, mean , sum , median , min , max , count)",
170
  ['mean', 'sum', 'median', 'min', 'max', 'count'],
171
  index=0 # Default to 'mean'
172
  )
173
 
174
+ # Function to convert geometry to Earth Engine format
175
  def convert_to_ee_geometry(geometry):
 
176
  if isinstance(geometry, base.BaseGeometry):
177
  if geometry.is_valid:
178
  geojson = geometry.__geo_interface__
 
179
  return ee.Geometry(geojson)
180
  else:
181
  raise ValueError("Invalid geometry: The polygon geometry is not valid.")
 
 
182
  elif isinstance(geometry, dict) or isinstance(geometry, str):
183
  try:
184
  if isinstance(geometry, str):
185
  geometry = json.loads(geometry)
186
  if 'type' in geometry and 'coordinates' in geometry:
 
187
  return ee.Geometry(geometry)
188
  else:
189
  raise ValueError("GeoJSON format is invalid.")
190
  except Exception as e:
191
  raise ValueError(f"Error parsing GeoJSON: {e}")
 
 
192
  elif isinstance(geometry, str) and geometry.lower().endswith(".kml"):
193
  try:
 
194
  tree = ET.parse(geometry)
195
  kml_root = tree.getroot()
 
 
 
196
  kml_namespace = {'kml': 'http://www.opengis.net/kml/2.2'}
197
  coordinates = kml_root.findall(".//kml:coordinates", kml_namespace)
 
198
  if coordinates:
 
199
  coords_text = coordinates[0].text.strip()
200
  coords = coords_text.split()
 
201
  coords = [tuple(map(float, coord.split(','))) for coord in coords]
202
+ geojson = {"type": "Polygon", "coordinates": [coords]}
 
 
 
 
 
 
 
203
  return ee.Geometry(geojson)
204
  else:
205
  raise ValueError("KML does not contain valid coordinates.")
206
  except Exception as e:
207
  raise ValueError(f"Error parsing KML: {e}")
 
208
  else:
209
  raise ValueError("Unsupported geometry input type. Supported types are Shapely, GeoJSON, and KML.")
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  # Date Input for Start and End Dates
212
  start_date = st.date_input("Start Date", value=pd.to_datetime('2024-11-01'))
213
  end_date = st.date_input("End Date", value=pd.to_datetime('2024-12-01'))
 
217
  end_date_str = end_date.strftime('%Y-%m-%d')
218
 
219
  # Aggregation period selection
220
+ aggregation_period = st.selectbox(
221
+ "Select Aggregation Period (e.g, Custom(Start Date to End Date) , Weekly , Monthly , Yearly)",
222
+ ["Custom (Start Date to End Date)", "Weekly", "Monthly", "Yearly"],
223
+ index=0
224
+ )
225
 
226
+ # Ask user whether they want to process 'Point' or 'Polygon' data
227
  shape_type = st.selectbox("Do you want to process 'Point' or 'Polygon' data?", ["Point", "Polygon"])
228
 
229
+ # Additional options based on shape type
230
+ kernel_size = None
231
+ include_boundary = None
232
+ if shape_type.lower() == "point":
233
+ kernel_size = st.selectbox(
234
+ "Select Calculation Area(e.g, Point , 3x3 Kernel , 5x5 Kernel)",
235
+ ["Point", "3x3 Kernel", "5x5 Kernel"],
236
+ index=0,
237
+ help="Choose 'Point' for exact point calculation, or a kernel size for area averaging."
238
+ )
239
+ elif shape_type.lower() == "polygon":
240
+ include_boundary = st.checkbox(
241
+ "Include Boundary Pixels",
242
+ value=True,
243
+ help="Check to include pixels on the polygon boundary; uncheck to exclude them."
244
+ )
245
+
246
+ # Ask user to upload a file based on shape type
247
  file_upload = st.file_uploader(f"Upload your {shape_type} data (CSV, GeoJSON, KML)", type=["csv", "geojson", "kml"])
248
 
249
  if file_upload is not None:
250
  # Read the user-uploaded file
251
  if shape_type.lower() == "point":
 
252
  if file_upload.name.endswith('.csv'):
253
  locations_df = pd.read_csv(file_upload)
254
  elif file_upload.name.endswith('.geojson'):
 
259
  st.error("Unsupported file format. Please upload CSV, GeoJSON, or KML.")
260
  locations_df = pd.DataFrame()
261
 
 
262
  if 'geometry' in locations_df.columns:
 
263
  if locations_df.geometry.geom_type.isin(['Polygon', 'MultiPolygon']).any():
264
  st.warning("The uploaded file contains polygon data. Please select 'Polygon' for processing.")
265
+ st.stop()
266
 
 
267
  with st.spinner('Processing Map...'):
268
  if locations_df is not None and not locations_df.empty:
 
269
  if 'geometry' in locations_df.columns:
 
270
  locations_df['latitude'] = locations_df['geometry'].y
271
  locations_df['longitude'] = locations_df['geometry'].x
272
 
 
273
  if 'latitude' not in locations_df.columns or 'longitude' not in locations_df.columns:
274
  st.error("Uploaded file is missing required 'latitude' or 'longitude' columns.")
275
  else:
 
276
  st.write("Preview of the uploaded points data:")
277
  st.dataframe(locations_df.head())
 
 
278
  m = leafmap.Map(center=[locations_df['latitude'].mean(), locations_df['longitude'].mean()], zoom=10)
 
 
279
  for _, row in locations_df.iterrows():
280
  latitude = row['latitude']
281
  longitude = row['longitude']
 
 
282
  if pd.isna(latitude) or pd.isna(longitude):
283
+ continue
 
284
  m.add_marker(location=[latitude, longitude], popup=row.get('name', 'No Name'))
 
 
285
  st.write("Map of Uploaded Points:")
286
  m.to_streamlit()
 
 
287
  st.session_state.map_data = m
288
 
289
  elif shape_type.lower() == "polygon":
 
290
  if file_upload.name.endswith('.csv'):
291
  locations_df = pd.read_csv(file_upload)
292
  elif file_upload.name.endswith('.geojson'):
 
297
  st.error("Unsupported file format. Please upload CSV, GeoJSON, or KML.")
298
  locations_df = pd.DataFrame()
299
 
 
300
  if 'geometry' in locations_df.columns:
 
301
  if locations_df.geometry.geom_type.isin(['Point', 'MultiPoint']).any():
302
  st.warning("The uploaded file contains point data. Please select 'Point' for processing.")
303
+ st.stop()
304
 
 
305
  with st.spinner('Processing Map...'):
306
  if locations_df is not None and not locations_df.empty:
 
307
  if 'geometry' not in locations_df.columns:
308
  st.error("Uploaded file is missing required 'geometry' column.")
309
  else:
 
310
  st.write("Preview of the uploaded polygons data:")
311
  st.dataframe(locations_df.head())
 
 
 
312
  centroid_lat = locations_df.geometry.centroid.y.mean()
313
  centroid_lon = locations_df.geometry.centroid.x.mean()
 
314
  m = leafmap.Map(center=[centroid_lat, centroid_lon], zoom=10)
 
 
315
  for _, row in locations_df.iterrows():
316
  polygon = row['geometry']
317
+ if polygon.is_valid:
 
318
  gdf = gpd.GeoDataFrame([row], geometry=[polygon], crs=locations_df.crs)
319
  m.add_gdf(gdf=gdf, layer_name=row.get('name', 'Unnamed Polygon'))
 
 
320
  st.write("Map of Uploaded Polygons:")
321
  m.to_streamlit()
 
 
322
  st.session_state.map_data = m
323
 
324
+ # Initialize session state for storing results
325
  if 'results' not in st.session_state:
326
  st.session_state.results = []
327
  if 'last_params' not in st.session_state:
328
  st.session_state.last_params = {}
329
  if 'map_data' not in st.session_state:
330
+ st.session_state.map_data = None
331
+ if 'show_example' not in st.session_state:
332
+ st.session_state.show_example = True
333
 
334
  # Function to check if parameters have changed
335
  def parameters_changed():
336
  return (
337
  st.session_state.last_params.get('main_selection') != main_selection or
338
  st.session_state.last_params.get('dataset_id') != dataset_id or
339
+ st.session_state.last_params.get('selected_bands') != selected_bands or
340
+ st.session_state.last_params.get('custom_formula') != custom_formula or
341
  st.session_state.last_params.get('start_date_str') != start_date_str or
342
  st.session_state.last_params.get('end_date_str') != end_date_str or
343
  st.session_state.last_params.get('shape_type') != shape_type or
344
+ st.session_state.last_params.get('file_upload') != file_upload or
345
+ st.session_state.last_params.get('kernel_size') != kernel_size or
346
+ st.session_state.last_params.get('include_boundary') != include_boundary
347
  )
348
 
349
  # If parameters have changed, reset the results
350
  if parameters_changed():
351
+ st.session_state.results = []
352
  st.session_state.last_params = {
353
  'main_selection': main_selection,
354
  'dataset_id': dataset_id,
355
+ 'selected_bands': selected_bands,
356
+ 'custom_formula': custom_formula,
357
  'start_date_str': start_date_str,
358
  'end_date_str': end_date_str,
359
  'shape_type': shape_type,
360
+ 'file_upload': file_upload,
361
+ 'kernel_size': kernel_size,
362
+ 'include_boundary': include_boundary
363
  }
364
 
365
+ # Function to calculate custom formula
366
+ def calculate_custom_formula(image, geometry, selected_bands, custom_formula, reducer_choice, scale=30):
 
 
 
 
 
 
 
 
 
367
  try:
368
+ band_values = {}
369
+ band_names = image.bandNames().getInfo()
370
+
371
+ for band in selected_bands:
 
 
 
 
 
372
  if band not in band_names:
373
+ raise ValueError(f"Band '{band}' not found in the dataset.")
374
+ band_values[band] = image.select(band)
375
+
376
+ reducer = get_reducer(reducer_choice)
377
+ reduced_values = {}
378
+ for band in selected_bands:
379
+ value = band_values[band].reduceRegion(
380
+ reducer=reducer,
381
+ geometry=geometry,
382
+ scale=scale
383
+ ).get(band).getInfo()
384
+ reduced_values[band] = float(value if value is not None else 0)
385
+
386
+ formula = custom_formula
387
+ for band in selected_bands:
388
+ formula = formula.replace(band, str(reduced_values[band]))
389
+
390
+ result = eval(formula, {"__builtins__": {}}, reduced_values)
391
+ if not isinstance(result, (int, float)):
392
+ raise ValueError("Formula did not result in a numeric value.")
393
 
394
+ return ee.Image.constant(result).rename('custom_result')
 
 
 
395
 
396
+ except ZeroDivisionError:
397
+ st.error("Error: Division by zero in the formula.")
398
+ return ee.Image(0).rename('custom_result').set('error', 'Division by zero')
399
+ except SyntaxError:
400
+ st.error(f"Error: Invalid syntax in formula '{custom_formula}'.")
401
+ return ee.Image(0).rename('custom_result').set('error', 'Invalid syntax')
402
+ except ValueError as e:
403
+ st.error(f"Error: {str(e)}")
404
+ return ee.Image(0).rename('custom_result').set('error', str(e))
405
+ except Exception as e:
406
+ st.error(f"Unexpected error: {e}")
407
+ return ee.Image(0).rename('custom_result').set('error', str(e))
408
 
409
+ # Function to calculate index for a period
410
+ def calculate_index_for_period(image, roi, selected_bands, custom_formula, reducer_choice):
411
+ return calculate_custom_formula(image, roi, selected_bands, custom_formula, reducer_choice)
412
+
413
+ # Aggregation functions
414
+ def aggregate_data_custom(collection):
415
+ collection = collection.map(lambda image: image.set('day', ee.Date(image.get('system:time_start')).format('YYYY-MM-dd')))
416
+ grouped_by_day = collection.aggregate_array('day').distinct()
417
  def calculate_daily_mean(day):
 
418
  daily_collection = collection.filter(ee.Filter.eq('day', day))
419
+ daily_mean = daily_collection.mean()
420
  return daily_mean.set('day', day)
 
 
421
  daily_images = ee.List(grouped_by_day.map(calculate_daily_mean))
 
422
  return ee.ImageCollection(daily_images)
423
 
424
  def aggregate_data_weekly(collection):
425
+ def set_week_start(image):
426
+ date = ee.Date(image.get('system:time_start'))
427
+ days_since_week_start = date.getRelative('day', 'week')
428
+ offset = ee.Number(days_since_week_start).multiply(-1)
429
+ week_start = date.advance(offset, 'day')
430
+ return image.set('week_start', week_start.format('YYYY-MM-dd'))
431
+ collection = collection.map(set_week_start)
432
  grouped_by_week = collection.aggregate_array('week_start').distinct()
 
433
  def calculate_weekly_mean(week_start):
 
434
  weekly_collection = collection.filter(ee.Filter.eq('week_start', week_start))
435
+ weekly_mean = weekly_collection.mean()
436
  return weekly_mean.set('week_start', week_start)
 
 
437
  weekly_images = ee.List(grouped_by_week.map(calculate_weekly_mean))
438
  return ee.ImageCollection(weekly_images)
439
+
440
  def aggregate_data_monthly(collection, start_date, end_date):
 
441
  collection = collection.filterDate(start_date, end_date)
 
 
442
  collection = collection.map(lambda image: image.set('month', ee.Date(image.get('system:time_start')).format('YYYY-MM')))
 
 
443
  grouped_by_month = collection.aggregate_array('month').distinct()
 
444
  def calculate_monthly_mean(month):
445
  monthly_collection = collection.filter(ee.Filter.eq('month', month))
446
  monthly_mean = monthly_collection.mean()
447
  return monthly_mean.set('month', month)
 
 
448
  monthly_images = ee.List(grouped_by_month.map(calculate_monthly_mean))
 
449
  return ee.ImageCollection(monthly_images)
450
+
451
  def aggregate_data_yearly(collection):
 
452
  collection = collection.map(lambda image: image.set('year', ee.Date(image.get('system:time_start')).format('YYYY')))
 
 
453
  grouped_by_year = collection.aggregate_array('year').distinct()
 
454
  def calculate_yearly_mean(year):
455
  yearly_collection = collection.filter(ee.Filter.eq('year', year))
456
  yearly_mean = yearly_collection.mean()
457
  return yearly_mean.set('year', year)
 
 
458
  yearly_images = ee.List(grouped_by_year.map(calculate_yearly_mean))
 
459
  return ee.ImageCollection(yearly_images)
460
 
461
+ # Process aggregation function
462
+ 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):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  aggregated_results = []
464
 
465
+ if not custom_formula:
466
  st.error("Custom formula cannot be empty. Please provide a formula.")
467
  return aggregated_results
468
 
 
480
  continue
481
 
482
  location_name = row.get('name', f"Location_{idx}")
483
+
484
+ if kernel_size == "3x3 Kernel":
485
+ buffer_size = 45 # 90m x 90m
486
+ roi = ee.Geometry.Point([longitude, latitude]).buffer(buffer_size).bounds()
487
+ elif kernel_size == "5x5 Kernel":
488
+ buffer_size = 75 # 150m x 150m
489
+ roi = ee.Geometry.Point([longitude, latitude]).buffer(buffer_size).bounds()
490
+ else: # Point
491
+ roi = ee.Geometry.Point([longitude, latitude])
492
 
493
  collection = ee.ImageCollection(dataset_id) \
494
  .filterDate(ee.Date(start_date_str), ee.Date(end_date_str)) \
495
  .filterBounds(roi)
496
 
497
+ if aggregation_period.lower() == 'custom (start date to end date)':
498
+ collection = aggregate_data_custom(collection)
 
499
  elif aggregation_period.lower() == 'weekly':
500
  collection = aggregate_data_weekly(collection)
501
  elif aggregation_period.lower() == 'monthly':
 
503
  elif aggregation_period.lower() == 'yearly':
504
  collection = aggregate_data_yearly(collection)
505
 
 
506
  image_list = collection.toList(collection.size())
507
+ processed_weeks = set()
508
  for i in range(image_list.size().getInfo()):
509
  image = ee.Image(image_list.get(i))
510
 
511
+ if aggregation_period.lower() == 'custom (start date to end date)':
512
  timestamp = image.get('day')
513
  period_label = 'Date'
514
  date = ee.Date(timestamp).format('YYYY-MM-dd').getInfo()
515
  elif aggregation_period.lower() == 'weekly':
516
  timestamp = image.get('week_start')
517
  period_label = 'Week'
518
+ date = ee.String(timestamp).getInfo()
 
519
  if (pd.to_datetime(date) < pd.to_datetime(start_date_str) or
520
  pd.to_datetime(date) > pd.to_datetime(end_date_str) or
521
  date in processed_weeks):
 
530
  period_label = 'Year'
531
  date = ee.Date(timestamp).format('YYYY').getInfo()
532
 
533
+ index_image = calculate_index_for_period(image, roi, selected_bands, custom_formula, reducer_choice)
534
 
535
  try:
536
  index_value = index_image.reduceRegion(
537
  reducer=get_reducer(reducer_choice),
538
  geometry=roi,
539
  scale=30
540
+ ).get('custom_result')
541
 
542
  calculated_value = index_value.getInfo()
543
 
 
568
 
569
  try:
570
  roi = convert_to_ee_geometry(polygon_geometry)
571
+ if not include_boundary:
572
+ roi = roi.buffer(-30).bounds()
573
  except ValueError as e:
574
  st.warning(f"Skipping invalid polygon {polygon_name}: {e}")
575
  continue
 
578
  .filterDate(ee.Date(start_date_str), ee.Date(end_date_str)) \
579
  .filterBounds(roi)
580
 
581
+ if aggregation_period.lower() == 'custom (start date to end date)':
582
+ collection = aggregate_data_custom(collection)
 
583
  elif aggregation_period.lower() == 'weekly':
584
  collection = aggregate_data_weekly(collection)
585
  elif aggregation_period.lower() == 'monthly':
 
587
  elif aggregation_period.lower() == 'yearly':
588
  collection = aggregate_data_yearly(collection)
589
 
 
590
  image_list = collection.toList(collection.size())
591
+ processed_weeks = set()
592
  for i in range(image_list.size().getInfo()):
593
  image = ee.Image(image_list.get(i))
594
 
595
+ if aggregation_period.lower() == 'custom (start date to end date)':
596
  timestamp = image.get('day')
597
  period_label = 'Date'
598
  date = ee.Date(timestamp).format('YYYY-MM-dd').getInfo()
599
  elif aggregation_period.lower() == 'weekly':
600
  timestamp = image.get('week_start')
601
  period_label = 'Week'
602
+ date = ee.String(timestamp).getInfo()
 
603
  if (pd.to_datetime(date) < pd.to_datetime(start_date_str) or
604
  pd.to_datetime(date) > pd.to_datetime(end_date_str) or
605
  date in processed_weeks):
 
614
  period_label = 'Year'
615
  date = ee.Date(timestamp).format('YYYY').getInfo()
616
 
617
+ index_image = calculate_index_for_period(image, roi, selected_bands, custom_formula, reducer_choice)
618
 
619
  try:
620
  index_value = index_image.reduceRegion(
621
  reducer=get_reducer(reducer_choice),
622
  geometry=roi,
623
  scale=30
624
+ ).get('custom_result')
625
 
626
  calculated_value = index_value.getInfo()
627
 
 
644
 
645
  if aggregated_results:
646
  result_df = pd.DataFrame(aggregated_results)
647
+ if aggregation_period.lower() == 'custom (start date to end date)':
648
+ agg_dict = {
 
 
649
  'Start Date': 'first',
650
  'End Date': 'first',
651
  'Calculated Value': 'mean'
652
+ }
653
+ if shape_type.lower() == 'point':
654
+ agg_dict['Latitude'] = 'first'
655
+ agg_dict['Longitude'] = 'first'
656
+ aggregated_output = result_df.groupby('Location Name').agg(agg_dict).reset_index()
657
  aggregated_output.rename(columns={'Calculated Value': 'Aggregated Value'}, inplace=True)
658
  return aggregated_output.to_dict(orient='records')
659
  else:
660
  return result_df.to_dict(orient='records')
 
661
  return []
662
 
663
+ # Button to trigger calculation
664
+ if st.button(f"Calculate {custom_formula}"):
665
  if file_upload is not None:
666
+ if shape_type.lower() in ["point", "polygon"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  results = process_aggregation(
668
  locations_df,
669
  start_date_str,
670
  end_date_str,
671
  dataset_id,
672
+ selected_bands,
673
  reducer_choice,
674
  shape_type,
675
  aggregation_period,
676
+ custom_formula,
677
+ kernel_size=kernel_size,
678
+ include_boundary=include_boundary
679
  )
680
  if results:
681
  result_df = pd.DataFrame(results)
682
+ st.write(f"Processed Results Table ({aggregation_period}) for Formula: {custom_formula}")
683
  st.dataframe(result_df)
684
+ filename = f"{main_selection}_{dataset_id}_{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}_{aggregation_period.lower()}.csv"
685
  st.download_button(
686
  label="Download results as CSV",
687
  data=result_df.to_csv(index=False).encode('utf-8'),
688
  file_name=filename,
689
  mime='text/csv'
690
  )
691
+ # Show an example calculation
692
+ if st.session_state.show_example and results:
693
+ example_result = results[0]
694
+ example_image = ee.ImageCollection(dataset_id).filterDate(start_date_str, end_date_str).first()
695
+ example_roi = (
696
+ ee.Geometry.Point([example_result['Longitude'], example_result['Latitude']])
697
+ if shape_type.lower() == 'point'
698
+ else convert_to_ee_geometry(locations_df['geometry'].iloc[0])
699
+ )
700
+ example_values = {}
701
+ for band in selected_bands:
702
+ value = example_image.select(band).reduceRegion(
703
+ reducer=get_reducer(reducer_choice),
704
+ geometry=example_roi,
705
+ scale=30
706
+ ).get(band).getInfo()
707
+ example_values[band] = float(value if value is not None else 0)
708
+ example_formula = custom_formula
709
+ for band in selected_bands:
710
+ example_formula = example_formula.replace(band, str(example_values[band]))
711
+ # st.write(f"Example Calculation: {custom_formula} -> {example_formula} = {example_result.get('Calculated Value', example_result.get('Aggregated Value'))}")
712
+ st.session_state.show_example = False
713
  st.success('Processing complete!')
714
  else:
715
+ st.warning("No results were generated. Check your inputs or formula.")
 
716
  else:
717
+ st.warning("Please upload a file to process.")
718
+ else:
719
+ st.warning("Please upload a file to proceed.")