UjjwalKGupta commited on
Commit
b7a64d5
·
verified ·
1 Parent(s): 6e0a1c7

Add Comparison Map

Browse files
Files changed (1) hide show
  1. app.py +119 -83
app.py CHANGED
@@ -179,23 +179,19 @@ def get_wayback_data():
179
  root = ET.fromstring(response.content)
180
 
181
  ns = {
182
- "wmts": "https://www.opengis.net/wmts/1.0",
183
- "ows": "https://www.opengis.net/ows/1.1",
184
- "xlink": "https://www.w3.org/1999/xlink",
185
  }
186
-
187
- # Use a robust XPath to find all 'Layer' elements anywhere in the document.
188
- # This is less brittle than specifying the full path.
189
  layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
190
 
191
  layer_data = []
192
  for layer in layers:
193
  title = layer.find("ows:Title", ns)
194
- identifier = layer.find("ows:Identifier", ns)
195
- resource = layer.find("wmts:ResourceURL", ns) # Tile URL template
196
 
197
  title_text = title.text if title is not None else "N/A"
198
- identifier_text = identifier.text if identifier is not None else "N/A"
199
  url_template = resource.get("template") if resource is not None else "N/A"
200
 
201
  layer_data.append({"Title": title_text, "ResourceURL_Template": url_template})
@@ -203,17 +199,11 @@ def get_wayback_data():
203
  wayback_df = pd.DataFrame(layer_data)
204
  wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})").squeeze(), errors="coerce")
205
  wayback_df.set_index("date", inplace=True)
206
- return wayback_df
207
 
208
- except requests.exceptions.RequestException as e:
209
- print(f"Could not fetch Wayback data from URL: {e}")
210
- return pd.DataFrame()
211
- except ET.ParseError as e:
212
- print(f"Could not parse Wayback XML data: {e}")
213
- return pd.DataFrame()
214
  except Exception as e:
215
- print(f"An unexpected error occurred in get_wayback_data: {e}")
216
- return pd.DataFrame()
217
 
218
 
219
  def get_dem_slope_maps(ee_geometry, map_bounds, zoom=12, wayback_url=None, wayback_title=None):
@@ -274,63 +264,39 @@ def get_dem_slope_maps(ee_geometry, map_bounds, zoom=12, wayback_url=None, wayba
274
 
275
  def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
276
  """Calculates and adds multiple vegetation indices to an Earth Engine image."""
277
- # It's safer to work with the image bands directly
278
  nir = image.select(nir_band).divide(10000)
279
  red = image.select(red_band).divide(10000)
280
  blue = image.select(blue_band).divide(10000)
281
  green = image.select(green_band).divide(10000)
282
-
283
- # NDVI
284
  ndvi = image.normalizedDifference([nir_band, red_band]).rename('NDVI')
285
-
286
- # EVI
287
  evi = image.expression(
288
  'G * ((NIR - RED) / (NIR + C1 * RED - C2 * BLUE + L))', {
289
  'NIR': nir, 'RED': red, 'BLUE': blue,
290
  'G': evi_vars['G'], 'C1': evi_vars['C1'], 'C2': evi_vars['C2'], 'L': evi_vars['L']
291
  }).rename('EVI')
292
-
293
- # EVI2
294
  evi2 = image.expression(
295
  'G * (NIR - RED) / (NIR + L + C * RED)', {
296
  'NIR': nir, 'RED': red,
297
  'G': evi_vars['G'], 'L': evi_vars['L'], 'C': evi_vars['C']
298
  }).rename('EVI2')
299
-
300
- # RandomForest (This part requires a pre-trained model asset in GEE)
301
  try:
302
  table = ee.FeatureCollection('projects/in793-aq-nb-24330048/assets/cleanedVDI').select(
303
- ["B2", "B4", "B8", "cVDI"],
304
- ["Blue", "Red", "NIR", 'cVDI']
305
- )
306
-
307
- bands = ['Blue', 'Red', 'NIR']
308
- label = 'cVDI'
309
-
310
  classifier = ee.Classifier.smileRandomForest(50).train(
311
- features=table,
312
- classProperty=label,
313
- inputProperties=bands,
314
- )
315
  rf = image.classify(classifier).multiply(ee.Number(0.2)).add(ee.Number(0.1)).rename('RandomForest')
316
  except Exception as e:
317
  print(f"Random Forest calculation failed: {e}")
318
- rf = ee.Image.constant(0).rename('RandomForest') # Return a constant image on failure
319
-
320
-
321
- # Cubic Function Index (CI)
322
  ci = image.expression(
323
  '(-3.98 * (BLUE/NIR) + 12.54 * (GREEN/NIR) - 5.49 * (RED/NIR) - 0.19) / ' +
324
  '(-21.87 * (BLUE/NIR) + 12.4 * (GREEN/NIR) + 19.98 * (RED/NIR) + 1) * 2.29', {
325
  'NIR': nir, 'RED': red, 'BLUE': blue, 'GREEN': green
326
  }).rename('CI')
327
-
328
- # GujVDI
329
  gujvdi = image.expression(
330
  '0.5 * (NIR - RED) / (NIR + 6 * RED - 8.25 * BLUE - 0.01)', {
331
  'NIR': nir, 'RED': red, 'BLUE': blue
332
  }).rename('GujVDI')
333
-
334
  return image.addBands([ndvi, evi, evi2, rf, ci, gujvdi])
335
 
336
 
@@ -347,7 +313,6 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
347
 
348
  progress(0, desc="Reading and processing geometry...")
349
  try:
350
- # ... (no changes to the geometry processing part) ...
351
  input_gdf = get_gdf_from_file(file_obj) if file_obj is not None else get_gdf_from_url(url_str)
352
  input_gdf = preprocess_gdf(input_gdf)
353
  geometry_gdf = next((input_gdf.iloc[[i]] for i in range(len(input_gdf)) if is_valid_polygon(input_gdf.iloc[[i]])), None)
@@ -364,16 +329,12 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
364
  return None, f"Error processing file: {e}", None, None, None, None, None
365
 
366
  progress(0.5, desc="Generating maps and stats...")
367
-
368
- # Create main map with folium using the calculated center
369
  m = folium.Map()
370
-
371
- # --- Generate Maps ---
372
  if not WAYBACK_DF.empty:
373
- # Select the first row, which is the most recent date after sorting
374
  latest_item = WAYBACK_DF.iloc[0]
375
  wayback_title = f"Esri Wayback ({latest_item.name.strftime('%Y-%m-%d')})"
376
- print(wayback_title)
377
  wayback_url = (
378
  latest_item["ResourceURL_Template"]
379
  .replace("{TileMatrixSet}", "GoogleMapsCompatible")
@@ -382,22 +343,16 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
382
  .replace("{TileCol}", "{x}")
383
  )
384
  folium.TileLayer(tiles=wayback_url, attr="Esri", name=wayback_title).add_to(m)
385
-
386
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
387
  m.add_child(folium.LayerControl())
388
-
389
- # Fit the map view to the bounds of the geometry
390
  bounds = geometry_gdf.to_crs(epsg=4326).total_bounds
391
- map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]] # Format: [[south, west], [north, east]]
392
  m.fit_bounds(map_bounds, padding=(10, 10))
393
 
394
- # Pass map_center to the function
395
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
396
  dem_html, slope_html = get_dem_slope_maps(ee_geometry, map_bounds, wayback_url=wayback_url, wayback_title=wayback_title)
397
 
398
-
399
-
400
- # ... (rest of the function remains the same) ...
401
  stats_df = pd.DataFrame({
402
  "Area (ha)": [f"{geometry_gdf.area.item() / 10000:.2f}"],
403
  "Perimeter (m)": [f"{geometry_gdf.length.item():.2f}"],
@@ -419,15 +374,11 @@ def calculate_indices(
419
  return "Please process a file and select at least one index first.", None, None, None
420
 
421
  try:
422
- # Recreate GDFs from JSON
423
  geometry_gdf = gpd.read_file(geometry_json)
424
- buffer_geometry_gdf = gpd.read_file(buffer_geometry_json)
425
-
426
- # Convert to EE geometry
427
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
428
  buffer_ee_geometry = ee.Geometry(json.loads(buffer_geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
429
 
430
- # Date ranges
431
  start_day, start_month = date_range[0].day, date_range[0].month
432
  end_day, end_month = date_range[1].day, date_range[1].month
433
  dates = [
@@ -435,7 +386,6 @@ def calculate_indices(
435
  for year in range(min_year, max_year + 1)
436
  ]
437
 
438
- # GEE processing
439
  collection = (
440
  ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
441
  .select(
@@ -452,13 +402,11 @@ def calculate_indices(
452
  if filtered_collection.size().getInfo() == 0:
453
  continue
454
 
455
- # **MODIFIED**: Add 'Year' column data to the row
456
  year_val = int(start_date.split('-')[0])
457
  row = {'Year': year_val, 'Date Range': f"{start_date}_to_{end_date}"}
458
 
459
  for veg_index in veg_indices:
460
  mosaic = filtered_collection.qualityMosaic(veg_index)
461
-
462
  mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
463
  buffer_mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=buffer_ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
464
 
@@ -473,10 +421,8 @@ def calculate_indices(
473
  result_df = pd.DataFrame(result_rows)
474
  result_df = result_df.round(3)
475
 
476
- # Create plots
477
  plots = []
478
  for veg_index in veg_indices:
479
- # **MODIFIED**: Plot using the new 'Year' column for the x-axis
480
  plot_cols = [veg_index, f"{veg_index}_buffer", f"{veg_index}_ratio"]
481
  existing_plot_cols = [col for col in plot_cols if col in result_df.columns]
482
 
@@ -485,7 +431,6 @@ def calculate_indices(
485
  if not plot_df.empty:
486
  fig = px.line(plot_df, x='Year', y=existing_plot_cols, markers=True, title=f"{veg_index} Time Series")
487
  fig.update_layout(xaxis_title="Year", yaxis_title="Index Value")
488
- # Ensure x-axis ticks are whole numbers for years
489
  fig.update_xaxes(dtick=1)
490
  plots.append(fig)
491
 
@@ -496,17 +441,69 @@ def calculate_indices(
496
  traceback.print_exc()
497
  return f"An error occurred during calculation: {e}", None, None, None
498
 
499
- # First, create an instance of the Soft theme
500
- theme = gr.themes.Soft()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
- # Then, directly modify the attribute for the primary background color
503
- theme.background_fill_primary = "white"
 
 
 
 
504
 
505
- # Finally, pass the fully customized theme object to your Gradio Blocks
506
  with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
507
- # Hidden state to store geometry data
508
  geometry_data = gr.State()
509
  buffer_geometry_data = gr.State()
 
510
 
511
  gr.HTML("""
512
  <div style="display: flex; justify-content: space-between; align-items: center;">
@@ -549,10 +546,11 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
549
  max_year_input = gr.Number(label="End Year", value=today.year, precision=0)
550
 
551
  calculate_button = gr.Button("Calculate Vegetation Indices", variant="primary")
552
- info_box = gr.Textbox(label="Status", interactive=False)
553
 
554
  with gr.Column(scale=2):
555
  gr.Markdown("## 2. Results")
 
556
  map_output = gr.HTML(label="Map View")
557
  stats_output = gr.DataFrame(label="Geometry Metrics")
558
 
@@ -567,9 +565,22 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
567
  with gr.TabItem("Time Series Data"):
568
  timeseries_table = gr.DataFrame(label="Time Series Data")
569
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  # --- Event Handlers ---
571
  def process_on_load(request: gr.Request):
572
- """Checks for a 'file_url' query parameter when the app loads and populates the URL input field."""
573
  return request.query_params.get("file_url", "")
574
 
575
  demo.load(process_on_load, None, url_input)
@@ -588,7 +599,6 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
588
  evi_vars = {'G': g, 'C1': c1, 'C2': c2, 'L': l, 'C': c}
589
  start_month, start_day = map(int, start_date_str.split('-'))
590
  end_month, end_day = map(int, end_date_str.split('-'))
591
- # Use a placeholder year; the actual year is iterated inside the main function
592
  date_range = (datetime(2000, start_month, start_day), datetime(2000, end_month, end_day))
593
 
594
  error_msg, df, plots, success_msg = calculate_indices(
@@ -600,10 +610,14 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
600
  first_plot = plots[0] if plots else None
601
  df_display = df.round(3) if df is not None else None
602
 
603
- return status_message, df_display, first_plot
 
 
 
 
604
 
605
  except Exception as e:
606
- return f"An error occurred in the wrapper: {e}", None, None
607
 
608
  calculate_button.click(
609
  fn=calculate_wrapper,
@@ -613,7 +627,29 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
613
  date_start_input, date_end_input,
614
  min_year_input, max_year_input
615
  ],
616
- outputs=[info_box, timeseries_table, plot_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  )
618
 
619
  gr.HTML("""
 
179
  root = ET.fromstring(response.content)
180
 
181
  ns = {
182
+ "wmts": "http://www.opengis.net/wmts/1.0",
183
+ "ows": "http://www.opengis.net/ows/1.1",
184
+ "xlink": "http://www.w3.org/1999/xlink",
185
  }
186
+
 
 
187
  layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
188
 
189
  layer_data = []
190
  for layer in layers:
191
  title = layer.find("ows:Title", ns)
192
+ resource = layer.find("wmts:ResourceURL", ns)
 
193
 
194
  title_text = title.text if title is not None else "N/A"
 
195
  url_template = resource.get("template") if resource is not None else "N/A"
196
 
197
  layer_data.append({"Title": title_text, "ResourceURL_Template": url_template})
 
199
  wayback_df = pd.DataFrame(layer_data)
200
  wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})").squeeze(), errors="coerce")
201
  wayback_df.set_index("date", inplace=True)
202
+ return wayback_df.sort_index(ascending=False)
203
 
 
 
 
 
 
 
204
  except Exception as e:
205
+ print(f"Could not fetch or parse Wayback data: {e}")
206
+ return pd.DataFrame()
207
 
208
 
209
  def get_dem_slope_maps(ee_geometry, map_bounds, zoom=12, wayback_url=None, wayback_title=None):
 
264
 
265
  def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
266
  """Calculates and adds multiple vegetation indices to an Earth Engine image."""
 
267
  nir = image.select(nir_band).divide(10000)
268
  red = image.select(red_band).divide(10000)
269
  blue = image.select(blue_band).divide(10000)
270
  green = image.select(green_band).divide(10000)
 
 
271
  ndvi = image.normalizedDifference([nir_band, red_band]).rename('NDVI')
 
 
272
  evi = image.expression(
273
  'G * ((NIR - RED) / (NIR + C1 * RED - C2 * BLUE + L))', {
274
  'NIR': nir, 'RED': red, 'BLUE': blue,
275
  'G': evi_vars['G'], 'C1': evi_vars['C1'], 'C2': evi_vars['C2'], 'L': evi_vars['L']
276
  }).rename('EVI')
 
 
277
  evi2 = image.expression(
278
  'G * (NIR - RED) / (NIR + L + C * RED)', {
279
  'NIR': nir, 'RED': red,
280
  'G': evi_vars['G'], 'L': evi_vars['L'], 'C': evi_vars['C']
281
  }).rename('EVI2')
 
 
282
  try:
283
  table = ee.FeatureCollection('projects/in793-aq-nb-24330048/assets/cleanedVDI').select(
284
+ ["B2", "B4", "B8", "cVDI"], ["Blue", "Red", "NIR", 'cVDI'])
 
 
 
 
 
 
285
  classifier = ee.Classifier.smileRandomForest(50).train(
286
+ features=table, classProperty='cVDI', inputProperties=['Blue', 'Red', 'NIR'])
 
 
 
287
  rf = image.classify(classifier).multiply(ee.Number(0.2)).add(ee.Number(0.1)).rename('RandomForest')
288
  except Exception as e:
289
  print(f"Random Forest calculation failed: {e}")
290
+ rf = ee.Image.constant(0).rename('RandomForest')
 
 
 
291
  ci = image.expression(
292
  '(-3.98 * (BLUE/NIR) + 12.54 * (GREEN/NIR) - 5.49 * (RED/NIR) - 0.19) / ' +
293
  '(-21.87 * (BLUE/NIR) + 12.4 * (GREEN/NIR) + 19.98 * (RED/NIR) + 1) * 2.29', {
294
  'NIR': nir, 'RED': red, 'BLUE': blue, 'GREEN': green
295
  }).rename('CI')
 
 
296
  gujvdi = image.expression(
297
  '0.5 * (NIR - RED) / (NIR + 6 * RED - 8.25 * BLUE - 0.01)', {
298
  'NIR': nir, 'RED': red, 'BLUE': blue
299
  }).rename('GujVDI')
 
300
  return image.addBands([ndvi, evi, evi2, rf, ci, gujvdi])
301
 
302
 
 
313
 
314
  progress(0, desc="Reading and processing geometry...")
315
  try:
 
316
  input_gdf = get_gdf_from_file(file_obj) if file_obj is not None else get_gdf_from_url(url_str)
317
  input_gdf = preprocess_gdf(input_gdf)
318
  geometry_gdf = next((input_gdf.iloc[[i]] for i in range(len(input_gdf)) if is_valid_polygon(input_gdf.iloc[[i]])), None)
 
329
  return None, f"Error processing file: {e}", None, None, None, None, None
330
 
331
  progress(0.5, desc="Generating maps and stats...")
 
 
332
  m = folium.Map()
333
+ wayback_url = None
334
+ wayback_title = "Esri Satellite"
335
  if not WAYBACK_DF.empty:
 
336
  latest_item = WAYBACK_DF.iloc[0]
337
  wayback_title = f"Esri Wayback ({latest_item.name.strftime('%Y-%m-%d')})"
 
338
  wayback_url = (
339
  latest_item["ResourceURL_Template"]
340
  .replace("{TileMatrixSet}", "GoogleMapsCompatible")
 
343
  .replace("{TileCol}", "{x}")
344
  )
345
  folium.TileLayer(tiles=wayback_url, attr="Esri", name=wayback_title).add_to(m)
346
+
347
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
348
  m.add_child(folium.LayerControl())
 
 
349
  bounds = geometry_gdf.to_crs(epsg=4326).total_bounds
350
+ map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]]
351
  m.fit_bounds(map_bounds, padding=(10, 10))
352
 
 
353
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
354
  dem_html, slope_html = get_dem_slope_maps(ee_geometry, map_bounds, wayback_url=wayback_url, wayback_title=wayback_title)
355
 
 
 
 
356
  stats_df = pd.DataFrame({
357
  "Area (ha)": [f"{geometry_gdf.area.item() / 10000:.2f}"],
358
  "Perimeter (m)": [f"{geometry_gdf.length.item():.2f}"],
 
374
  return "Please process a file and select at least one index first.", None, None, None
375
 
376
  try:
 
377
  geometry_gdf = gpd.read_file(geometry_json)
378
+ buffer_geometry_gdf = gpd.read_file(buffer_json)
 
 
379
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
380
  buffer_ee_geometry = ee.Geometry(json.loads(buffer_geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
381
 
 
382
  start_day, start_month = date_range[0].day, date_range[0].month
383
  end_day, end_month = date_range[1].day, date_range[1].month
384
  dates = [
 
386
  for year in range(min_year, max_year + 1)
387
  ]
388
 
 
389
  collection = (
390
  ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
391
  .select(
 
402
  if filtered_collection.size().getInfo() == 0:
403
  continue
404
 
 
405
  year_val = int(start_date.split('-')[0])
406
  row = {'Year': year_val, 'Date Range': f"{start_date}_to_{end_date}"}
407
 
408
  for veg_index in veg_indices:
409
  mosaic = filtered_collection.qualityMosaic(veg_index)
 
410
  mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
411
  buffer_mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=buffer_ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
412
 
 
421
  result_df = pd.DataFrame(result_rows)
422
  result_df = result_df.round(3)
423
 
 
424
  plots = []
425
  for veg_index in veg_indices:
 
426
  plot_cols = [veg_index, f"{veg_index}_buffer", f"{veg_index}_ratio"]
427
  existing_plot_cols = [col for col in plot_cols if col in result_df.columns]
428
 
 
431
  if not plot_df.empty:
432
  fig = px.line(plot_df, x='Year', y=existing_plot_cols, markers=True, title=f"{veg_index} Time Series")
433
  fig.update_layout(xaxis_title="Year", yaxis_title="Index Value")
 
434
  fig.update_xaxes(dtick=1)
435
  plots.append(fig)
436
 
 
441
  traceback.print_exc()
442
  return f"An error occurred during calculation: {e}", None, None, None
443
 
444
+ def generate_comparison_maps(geometry_json, selected_index, selected_years, evi_vars, date_start_str, date_end_str, progress=gr.Progress()):
445
+ """Generates side-by-side maps for a selected index and two selected years."""
446
+ if not geometry_json or not selected_index or not selected_years:
447
+ return "Please process a file and select an index and years first.", "", ""
448
+ if len(selected_years) != 2:
449
+ return "Please select exactly two years to compare.", "", ""
450
+
451
+ one_time_setup()
452
+ geometry_gdf = gpd.read_file(geometry_json).to_crs(4326)
453
+ ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_json())['features'][0]['geometry'])
454
+ bounds = geometry_gdf.total_bounds
455
+ map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]]
456
+
457
+ start_month, start_day = map(int, date_start_str.split('-'))
458
+ end_month, end_day = map(int, date_end_str.split('-'))
459
+
460
+ vis_params = {
461
+ "min": 0.0, "max": 1.0,
462
+ "palette": ['FFFFFF', 'CE7E45', 'DF923D', 'F1B555', 'FCD163', '99B718', '74A901', '66A000', '529400', '3E8601', '207401', '056201', '004C00', '023B01', '012E01', '011D01', '011301']
463
+ }
464
+
465
+ maps_html = []
466
+ for i, year in enumerate(selected_years):
467
+ progress((i + 1) / 2, desc=f"Generating map for {year}")
468
+ start_date = f"{year}-{start_month:02d}-{start_day:02d}"
469
+ end_date = f"{year}-{end_month:02d}-{end_day:02d}"
470
+
471
+ collection = (ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
472
+ .filterDate(start_date, end_date)
473
+ .filterBounds(ee_geometry)
474
+ .map(lambda img: add_indices(img, 'NIR', 'Red', 'Blue', 'Green', evi_vars)))
475
+
476
+ if collection.size().getInfo() == 0:
477
+ maps_html.append(f"<div style='text-align:center; padding-top: 50px;'>No data found for {year}.</div>")
478
+ continue
479
+
480
+ mosaic = collection.qualityMosaic(selected_index)
481
+ clipped_image = mosaic.select(selected_index).clip(ee_geometry)
482
+
483
+ m = gee_folium.Map(zoom_start=14)
484
+ m.add_basemap("SATELLITE")
485
+ m.addLayer(clipped_image, vis_params, f"{selected_index} {year}")
486
+ m.add_colorbar(vis_params=vis_params, label=f"{selected_index} Value")
487
+ folium.GeoJson(geometry_gdf, name="Geometry", style_function=lambda x: {"color": "yellow", "fillOpacity": 0, "weight": 2.5}).add_to(m)
488
+ m.fit_bounds(map_bounds, padding=(10, 10))
489
+ m.addLayerControl()
490
+ maps_html.append(m._repr_html_())
491
+
492
+ while len(maps_html) < 2:
493
+ maps_html.append("")
494
 
495
+ return f"Comparison generated for {selected_years[0]} and {selected_years[1]}.", maps_html[0], maps_html[1]
496
+
497
+ # --- Gradio Interface ---
498
+ theme = gr.themes.Soft(primary_hue="teal", secondary_hue="orange").set(
499
+ background_fill_primary="white"
500
+ )
501
 
 
502
  with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
503
+ # Hidden state to store data between steps
504
  geometry_data = gr.State()
505
  buffer_geometry_data = gr.State()
506
+ timeseries_df_state = gr.State()
507
 
508
  gr.HTML("""
509
  <div style="display: flex; justify-content: space-between; align-items: center;">
 
546
  max_year_input = gr.Number(label="End Year", value=today.year, precision=0)
547
 
548
  calculate_button = gr.Button("Calculate Vegetation Indices", variant="primary")
549
+
550
 
551
  with gr.Column(scale=2):
552
  gr.Markdown("## 2. Results")
553
+ info_box = gr.Textbox(label="Status", interactive=False, placeholder="Status messages will appear here...")
554
  map_output = gr.HTML(label="Map View")
555
  stats_output = gr.DataFrame(label="Geometry Metrics")
556
 
 
565
  with gr.TabItem("Time Series Data"):
566
  timeseries_table = gr.DataFrame(label="Time Series Data")
567
 
568
+ gr.Markdown("---") # Visual separator
569
+ gr.Markdown("## 3. Year-over-Year Index Comparison")
570
+ with gr.Row():
571
+ comparison_index_select = gr.Radio(all_veg_indices, label="Select Index for Comparison", value="NDVI")
572
+ comparison_years_select = gr.CheckboxGroup(label="Select Two Years to Compare", choices=[])
573
+
574
+ compare_button = gr.Button("Generate Comparison Maps", variant="secondary")
575
+
576
+ with gr.Row():
577
+ map_year_1_output = gr.HTML(label="Comparison Map 1")
578
+ map_year_2_output = gr.HTML(label="Comparison Map 2")
579
+
580
+
581
  # --- Event Handlers ---
582
  def process_on_load(request: gr.Request):
583
+ """Checks for a 'file_url' query parameter when the app loads."""
584
  return request.query_params.get("file_url", "")
585
 
586
  demo.load(process_on_load, None, url_input)
 
599
  evi_vars = {'G': g, 'C1': c1, 'C2': c2, 'L': l, 'C': c}
600
  start_month, start_day = map(int, start_date_str.split('-'))
601
  end_month, end_day = map(int, end_date_str.split('-'))
 
602
  date_range = (datetime(2000, start_month, start_day), datetime(2000, end_month, end_day))
603
 
604
  error_msg, df, plots, success_msg = calculate_indices(
 
610
  first_plot = plots[0] if plots else None
611
  df_display = df.round(3) if df is not None else None
612
 
613
+ available_years = []
614
+ if df is not None and 'Year' in df.columns:
615
+ available_years = sorted(df['Year'].unique().tolist())
616
+
617
+ return status_message, df_display, df, first_plot, gr.update(choices=available_years, value=[])
618
 
619
  except Exception as e:
620
+ return f"An error occurred in the wrapper: {e}", None, None, None, gr.update(choices=[], value=[])
621
 
622
  calculate_button.click(
623
  fn=calculate_wrapper,
 
627
  date_start_input, date_end_input,
628
  min_year_input, max_year_input
629
  ],
630
+ outputs=[info_box, timeseries_table, timeseries_df_state, plot_output, comparison_years_select]
631
+ )
632
+
633
+ def comparison_wrapper(geometry_json, selected_index, selected_years, g, c1, c2, l, c, start_date_str, end_date_str, progress=gr.Progress()):
634
+ """Wrapper for the comparison map generation."""
635
+ try:
636
+ evi_vars = {'G': g, 'C1': c1, 'C2': c2, 'L': l, 'C': c}
637
+ status, map1, map2 = generate_comparison_maps(
638
+ geometry_json, selected_index, selected_years, evi_vars,
639
+ start_date_str, end_date_str, progress
640
+ )
641
+ return status, map1, map2
642
+ except Exception as e:
643
+ return f"Error during comparison: {e}", "", ""
644
+
645
+ compare_button.click(
646
+ fn=comparison_wrapper,
647
+ inputs=[
648
+ geometry_data, comparison_index_select, comparison_years_select,
649
+ evi_g, evi_c1, evi_c2, evi_l, evi_c,
650
+ date_start_input, date_end_input
651
+ ],
652
+ outputs=[info_box, map_year_1_output, map_year_2_output]
653
  )
654
 
655
  gr.HTML("""