UjjwalKGupta commited on
Commit
6a7d4fa
·
verified ·
1 Parent(s): 0fe3dd6

Fix EE and Wayback Imagery

Browse files
Files changed (1) hide show
  1. app.py +73 -30
app.py CHANGED
@@ -78,7 +78,7 @@ def get_gdf_from_url(url):
78
  """Downloads and reads a KML/GeoJSON from a URL."""
79
  if not url or not url.strip():
80
  return None
81
-
82
  # Handle Google Drive URLs
83
  if "drive.google.com" in url:
84
  if "/file/d/" in url:
@@ -153,7 +153,7 @@ def is_valid_polygon(geometry_gdf):
153
  geometry = geometry_gdf.geometry.item()
154
  return (geometry.type == 'Polygon') and (not geometry.is_empty)
155
 
156
- def add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.0):
157
  """Adds geometry and its buffer to a folium map."""
158
  if buffer_geometry_gdf is not None and not buffer_geometry_gdf.empty:
159
  folium.GeoJson(
@@ -174,28 +174,46 @@ def get_wayback_data():
174
  try:
175
  url = "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml"
176
  response = requests.get(url)
177
- response.raise_for_status()
 
 
178
  root = ET.fromstring(response.content)
 
179
  ns = {
180
- "wmts": "http://www.opengis.net/wmts/1.0",
181
- "ows": "http://www.opengis.net/ows/1.1",
 
182
  }
 
 
 
183
  layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
 
184
  layer_data = []
185
  for layer in layers:
186
  title = layer.find("ows:Title", ns)
187
- resource = layer.find("wmts:ResourceURL", ns)
188
- if title is not None and resource is not None:
189
- layer_data.append({
190
- "Title": title.text,
191
- "ResourceURL_Template": resource.get("template")
192
- })
 
 
 
193
  wayback_df = pd.DataFrame(layer_data)
194
- wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})")[0], errors="coerce")
195
  wayback_df.set_index("date", inplace=True)
196
- return wayback_df.sort_index()
 
 
 
 
 
 
 
197
  except Exception as e:
198
- print(f"Could not fetch Wayback data: {e}")
199
  return pd.DataFrame() # Return empty dataframe on failure
200
 
201
  def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
@@ -301,12 +319,15 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
301
 
302
  progress(0.5, desc="Generating map and stats...")
303
 
304
- # Create map
305
- m = folium.Map(location=[geometry_gdf.to_crs(4326).centroid.y.iloc[0], geometry_gdf.to_crs(4326).centroid.x.iloc[0]], zoom_start=15)
 
306
  if not WAYBACK_DF.empty:
307
- first_item = WAYBACK_DF.iloc[0]
 
 
308
  wayback_url = (
309
- first_item["ResourceURL_Template"]
310
  .replace("{TileMatrixSet}", "GoogleMapsCompatible")
311
  .replace("{TileMatrix}", "{z}")
312
  .replace("{TileRow}", "{y}")
@@ -314,12 +335,22 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
314
  )
315
  folium.TileLayer(
316
  tiles=wayback_url,
317
- attr='Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
318
- name="Esri Satellite"
319
  ).add_to(m)
320
- m.add_child(folium.LayerControl())
 
321
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
322
 
 
 
 
 
 
 
 
 
 
323
  # Generate stats
324
  stats_df = pd.DataFrame({
325
  "Area (ha)": [geometry_gdf.area.item() / 10000],
@@ -334,7 +365,6 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
334
  progress(1, desc="Done!")
335
  return m._repr_html_(), None, stats_df, geometry_json, buffer_geometry_json
336
 
337
-
338
  def calculate_indices(
339
  geometry_json, buffer_geometry_json, veg_indices, evi_vars, date_range,
340
  min_year, max_year, progress=gr.Progress()
@@ -351,7 +381,7 @@ def calculate_indices(
351
  # Convert to EE geometry
352
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
353
  buffer_ee_geometry = ee.Geometry(json.loads(buffer_geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
354
-
355
  # Date ranges
356
  start_day, start_month = date_range[0].day, date_range[0].month
357
  end_day, end_month = date_range[1].day, date_range[1].month
@@ -468,7 +498,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
468
 
469
 
470
  # --- Event Handlers ---
471
-
472
  def process_on_load(request: gr.Request):
473
  """Checks for a 'file_url' query parameter when the app loads and populates the URL input field."""
474
  file_url = request.query_params.get("file_url")
@@ -485,22 +515,34 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
485
  def calculate_wrapper(geometry_json, buffer_geometry_json, veg_indices,
486
  g, c1, c2, l, c, start_date_str, end_date_str,
487
  min_year, max_year, progress=gr.Progress()):
488
- """Wrapper to parse inputs before calling the main calculation function."""
489
  try:
 
490
  evi_vars = {'G': g, 'C1': c1, 'C2': c2, 'L': l, 'C': c}
491
  start_month, start_day = map(int, start_date_str.split('-'))
492
  end_month, end_day = map(int, end_date_str.split('-'))
493
  date_range = (datetime(2000, start_month, start_day), datetime(2000, end_month, end_day))
494
 
495
- error, df, plots, msg = calculate_indices(
 
496
  geometry_json, buffer_geometry_json, veg_indices,
497
  evi_vars, date_range, int(min_year), int(max_year), progress
498
  )
 
 
 
 
 
499
  first_plot = plots[0] if plots else None
500
- return error, df, first_plot, msg
 
 
 
501
  except Exception as e:
502
- return f"Input error: {e}", None, None, "Failed"
 
503
 
 
504
  calculate_button.click(
505
  fn=calculate_wrapper,
506
  inputs=[
@@ -509,9 +551,10 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
509
  date_start_input, date_end_input,
510
  min_year_input, max_year_input
511
  ],
512
- outputs=[results_info_box, timeseries_table, plot_output, results_info_box]
513
  )
514
 
 
515
  gr.HTML("""
516
  <div style="text-align: center; margin-top: 20px;">
517
  <p>Developed by <a href="https://sustainability-lab.github.io/">Sustainability Lab</a>, <a href="https://www.iitgn.ac.in/">IIT Gandhinagar</a></p>
@@ -520,4 +563,4 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
520
  """)
521
 
522
  if __name__ == "__main__":
523
- demo.launch(debug=True)
 
78
  """Downloads and reads a KML/GeoJSON from a URL."""
79
  if not url or not url.strip():
80
  return None
81
+
82
  # Handle Google Drive URLs
83
  if "drive.google.com" in url:
84
  if "/file/d/" in url:
 
153
  geometry = geometry_gdf.geometry.item()
154
  return (geometry.type == 'Polygon') and (not geometry.is_empty)
155
 
156
+ def add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3):
157
  """Adds geometry and its buffer to a folium map."""
158
  if buffer_geometry_gdf is not None and not buffer_geometry_gdf.empty:
159
  folium.GeoJson(
 
174
  try:
175
  url = "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml"
176
  response = requests.get(url)
177
+ response.raise_for_status() # Ensure request was successful
178
+
179
+ # Parse XML
180
  root = ET.fromstring(response.content)
181
+
182
  ns = {
183
+ "wmts": "https://www.opengis.net/wmts/1.0",
184
+ "ows": "https://www.opengis.net/ows/1.1",
185
+ "xlink": "https://www.w3.org/1999/xlink",
186
  }
187
+
188
+ # Use a robust XPath to find all 'Layer' elements anywhere in the document.
189
+ # This is less brittle than specifying the full path.
190
  layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
191
+
192
  layer_data = []
193
  for layer in layers:
194
  title = layer.find("ows:Title", ns)
195
+ identifier = layer.find("ows:Identifier", ns)
196
+ resource = layer.find("wmts:ResourceURL", ns) # Tile URL template
197
+
198
+ title_text = title.text if title is not None else "N/A"
199
+ identifier_text = identifier.text if identifier is not None else "N/A"
200
+ url_template = resource.get("template") if resource is not None else "N/A"
201
+
202
+ layer_data.append({"Title": title_text, "ResourceURL_Template": url_template})
203
+
204
  wayback_df = pd.DataFrame(layer_data)
205
+ wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})").squeeze(), errors="coerce")
206
  wayback_df.set_index("date", inplace=True)
207
+ return wayback_df
208
+
209
+ except requests.exceptions.RequestException as e:
210
+ print(f"Could not fetch Wayback data from URL: {e}")
211
+ return pd.DataFrame()
212
+ except ET.ParseError as e:
213
+ print(f"Could not parse Wayback XML data: {e}")
214
+ return pd.DataFrame()
215
  except Exception as e:
216
+ print(f"An unexpected error occurred in get_wayback_data: {e}")
217
  return pd.DataFrame() # Return empty dataframe on failure
218
 
219
  def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
 
319
 
320
  progress(0.5, desc="Generating map and stats...")
321
 
322
+ # Create map - initialize without a specific location
323
+ m = folium.Map()
324
+
325
  if not WAYBACK_DF.empty:
326
+ # Select the last row, which is the most recent date after sorting
327
+ latest_item = WAYBACK_DF.iloc[0]
328
+ print(latest_item)
329
  wayback_url = (
330
+ latest_item["ResourceURL_Template"]
331
  .replace("{TileMatrixSet}", "GoogleMapsCompatible")
332
  .replace("{TileMatrix}", "{z}")
333
  .replace("{TileRow}", "{y}")
 
335
  )
336
  folium.TileLayer(
337
  tiles=wayback_url,
338
+ attr=f"Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community | Imagery Date: {latest_item.name.strftime('%Y-%m-%d')}",
339
+ name="Latest Esri Satellite"
340
  ).add_to(m)
341
+
342
+ # Add geometries to the map
343
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
344
 
345
+ # Add a layer control panel
346
+ m.add_child(folium.LayerControl())
347
+
348
+ # Fit the map view to the bounds of the geometry
349
+ bounds = geometry_gdf.to_crs(epsg=4326).total_bounds
350
+ map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]] # Format: [[south, west], [north, east]]
351
+ m.fit_bounds(map_bounds, padding=(10, 10))
352
+
353
+
354
  # Generate stats
355
  stats_df = pd.DataFrame({
356
  "Area (ha)": [geometry_gdf.area.item() / 10000],
 
365
  progress(1, desc="Done!")
366
  return m._repr_html_(), None, stats_df, geometry_json, buffer_geometry_json
367
 
 
368
  def calculate_indices(
369
  geometry_json, buffer_geometry_json, veg_indices, evi_vars, date_range,
370
  min_year, max_year, progress=gr.Progress()
 
381
  # Convert to EE geometry
382
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
383
  buffer_ee_geometry = ee.Geometry(json.loads(buffer_geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
384
+
385
  # Date ranges
386
  start_day, start_month = date_range[0].day, date_range[0].month
387
  end_day, end_month = date_range[1].day, date_range[1].month
 
498
 
499
 
500
  # --- Event Handlers ---
501
+
502
  def process_on_load(request: gr.Request):
503
  """Checks for a 'file_url' query parameter when the app loads and populates the URL input field."""
504
  file_url = request.query_params.get("file_url")
 
515
  def calculate_wrapper(geometry_json, buffer_geometry_json, veg_indices,
516
  g, c1, c2, l, c, start_date_str, end_date_str,
517
  min_year, max_year, progress=gr.Progress()):
518
+ """Wrapper to parse inputs and handle outputs for the main calculation function."""
519
  try:
520
+ # Prepare inputs for the main function
521
  evi_vars = {'G': g, 'C1': c1, 'C2': c2, 'L': l, 'C': c}
522
  start_month, start_day = map(int, start_date_str.split('-'))
523
  end_month, end_day = map(int, end_date_str.split('-'))
524
  date_range = (datetime(2000, start_month, start_day), datetime(2000, end_month, end_day))
525
 
526
+ # Call the main calculation function
527
+ error_msg, df, plots, success_msg = calculate_indices(
528
  geometry_json, buffer_geometry_json, veg_indices,
529
  evi_vars, date_range, int(min_year), int(max_year), progress
530
  )
531
+
532
+ # Determine the final status message to display
533
+ status_message = error_msg if error_msg else success_msg
534
+
535
+ # Select the first plot to display, if any exist
536
  first_plot = plots[0] if plots else None
537
+
538
+ # Return a clean set of outputs for the UI
539
+ return status_message, df, first_plot
540
+
541
  except Exception as e:
542
+ # Catch any other unexpected errors during the process
543
+ return f"An error occurred in the calculation wrapper: {e}", None, None
544
 
545
+ # **FIX**: Cleaned up the outputs to have a direct one-to-one mapping.
546
  calculate_button.click(
547
  fn=calculate_wrapper,
548
  inputs=[
 
551
  date_start_input, date_end_input,
552
  min_year_input, max_year_input
553
  ],
554
+ outputs=[results_info_box, timeseries_table, plot_output]
555
  )
556
 
557
+
558
  gr.HTML("""
559
  <div style="text-align: center; margin-top: 20px;">
560
  <p>Developed by <a href="https://sustainability-lab.github.io/">Sustainability Lab</a>, <a href="https://www.iitgn.ac.in/">IIT Gandhinagar</a></p>
 
563
  """)
564
 
565
  if __name__ == "__main__":
566
+ demo.launch(debug=True)