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

Support URL Input

Browse files
Files changed (1) hide show
  1. app.py +79 -53
app.py CHANGED
@@ -18,7 +18,7 @@ import kml2geojson
18
  import folium
19
  import xml.etree.ElementTree as ET
20
 
21
- # --- Helper Functions from functions.py ---
22
 
23
  def one_time_setup():
24
  """Initializes the Earth Engine API."""
@@ -42,35 +42,63 @@ def one_time_setup():
42
  # In a real app, you might want to show an error to the user here
43
  # For this Gradio app, we'll let it proceed and fail gracefully later if EE is needed.
44
 
 
 
 
 
 
45
 
46
- def get_gdf_from_file(file_obj):
47
- """Reads a KML or GeoJSON file and returns a GeoDataFrame."""
48
- bytes_data = BytesIO(file_obj)
49
- # Read the start of the file to check if it's XML (KML)
50
- # We need to be careful not to consume the stream
51
- start_of_file = bytes_data.read(100)
52
- bytes_data.seek(0) # Reset pointer
53
-
54
- if start_of_file.strip().startswith(b'<?xml'):
55
  try:
56
- # Use a temporary file to work with kml2geojson which might expect a file path
57
- with open("temp.kml", "wb") as f:
58
- f.write(bytes_data.read())
59
- geojson = kml2geojson.convert("temp.kml")
60
- features = geojson[0]["features"]
61
- epsg = 4326
62
- input_gdf = gpd.GeoDataFrame.from_features(features, crs=f"EPSG:{epsg}")
63
- os.remove("temp.kml")
64
  except Exception as e:
65
- raise ValueError(f"Failed to process KML file: {e}")
 
66
  else:
67
  try:
68
- input_gdf = gpd.read_file(bytes_data)
69
  except Exception as e:
70
- raise ValueError(f"Failed to read GeoJSON/Shapefile: {e}")
71
-
72
  return input_gdf
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  def find_best_epsg(geometry):
76
  """Finds the most suitable EPSG code for a given geometry based on its centroid."""
@@ -231,14 +259,20 @@ def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
231
  one_time_setup()
232
  WAYBACK_DF = get_wayback_data()
233
 
234
- def process_and_display(file_obj, buffer_m, progress=gr.Progress()):
235
- """Main function to process the uploaded file and generate initial outputs."""
236
- if file_obj is None:
237
- return None, "Please upload a KML or GeoJSON file.", None, None, None
238
 
239
  progress(0, desc="Reading and processing geometry...")
240
  try:
241
- input_gdf = get_gdf_from_file(file_obj)
 
 
 
 
 
 
242
  input_gdf = preprocess_gdf(input_gdf)
243
 
244
  # Find the first valid polygon
@@ -250,7 +284,7 @@ def process_and_display(file_obj, buffer_m, progress=gr.Progress()):
250
  break
251
 
252
  if geometry_gdf is None:
253
- return None, "No valid polygon found in the uploaded file.", None, None, None
254
 
255
  geometry_gdf = to_best_crs(geometry_gdf)
256
 
@@ -294,7 +328,6 @@ def process_and_display(file_obj, buffer_m, progress=gr.Progress()):
294
  })
295
 
296
  # Save geometry data for later use
297
- # In Gradio, we pass data between functions instead of using session state
298
  geometry_json = geometry_gdf.to_json()
299
  buffer_geometry_json = buffer_geometry_gdf.to_json()
300
 
@@ -318,9 +351,7 @@ def calculate_indices(
318
  # Convert to EE geometry
319
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
320
  buffer_ee_geometry = ee.Geometry(json.loads(buffer_geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
321
- ee_fc = ee.FeatureCollection(ee_geometry)
322
- buffer_ee_fc = ee.FeatureCollection(buffer_ee_geometry)
323
-
324
  # Date ranges
325
  start_day, start_month = date_range[0].day, date_range[0].month
326
  end_day, end_month = date_range[1].day, date_range[1].month
@@ -361,7 +392,7 @@ def calculate_indices(
361
  return "No satellite imagery found for the selected dates.", None, None, None
362
 
363
  result_df = pd.DataFrame(result_rows).set_index('daterange')
364
- result_df.index = result_df.index.str.split('_').str[0] # Use start year as index for plotting
365
 
366
  # Create plots
367
  plots = []
@@ -394,10 +425,12 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
394
 
395
  with gr.Row():
396
  with gr.Column(scale=1):
397
- gr.Markdown("## 1. Upload Geometry")
 
398
  file_input = gr.File(label="Upload KML/GeoJSON File", file_types=[".kml", ".geojson"])
 
399
  buffer_input = gr.Number(label="Buffer (meters)", value=50)
400
- process_button = gr.Button("Process File", variant="primary")
401
  info_box = gr.Textbox(label="Status", interactive=False)
402
 
403
  with gr.Accordion("Advanced Settings", open=False):
@@ -415,7 +448,6 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
415
  evi_c = gr.Number(label="C", value=2.4)
416
 
417
  gr.Markdown("### Date Range")
418
- # Gradio doesn't have a direct date range picker, so we use two date inputs
419
  today = datetime.now()
420
  date_start_input = gr.Textbox(label="Start Date (MM-DD)", value="11-15")
421
  date_end_input = gr.Textbox(label="End Date (MM-DD)", value="12-15")
@@ -424,25 +456,29 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
424
  min_year_input = gr.Number(label="Start Year", value=2019, precision=0)
425
  max_year_input = gr.Number(label="End Year", value=today.year, precision=0)
426
 
427
-
428
  calculate_button = gr.Button("Calculate Vegetation Indices", variant="primary")
429
 
430
-
431
  with gr.Column(scale=2):
432
  gr.Markdown("## 2. Results")
433
  stats_output = gr.DataFrame(label="Geometry Metrics")
434
  map_output = gr.HTML(label="Map View")
435
  results_info_box = gr.Textbox(label="Calculation Status", interactive=False)
436
  timeseries_table = gr.DataFrame(label="Time Series Data")
437
- with gr.Blocks() as plot_output_blocks:
438
- gr.Markdown("### Time Series Plots")
439
- # This will be populated dynamically
440
- # We will handle plot display logic in the backend
441
 
442
  # --- Event Handlers ---
 
 
 
 
 
 
 
 
443
  process_button.click(
444
  fn=process_and_display,
445
- inputs=[file_input, buffer_input],
446
  outputs=[map_output, info_box, stats_output, geometry_data, buffer_geometry_data]
447
  )
448
 
@@ -451,30 +487,20 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
451
  min_year, max_year, progress=gr.Progress()):
452
  """Wrapper to parse inputs before calling the main calculation function."""
453
  try:
454
- # Parse EVI vars
455
  evi_vars = {'G': g, 'C1': c1, 'C2': c2, 'L': l, 'C': c}
456
- # Parse dates - we ignore the year part from the string
457
  start_month, start_day = map(int, start_date_str.split('-'))
458
  end_month, end_day = map(int, end_date_str.split('-'))
459
- # Use a dummy year, the actual year is handled in the loop
460
  date_range = (datetime(2000, start_month, start_day), datetime(2000, end_month, end_day))
461
 
462
  error, df, plots, msg = calculate_indices(
463
  geometry_json, buffer_geometry_json, veg_indices,
464
  evi_vars, date_range, int(min_year), int(max_year), progress
465
  )
466
- # Gradio can't directly update a variable number of plots.
467
- # A workaround is to return a list and handle it, or create a fixed number of plot outputs.
468
- # For simplicity, we'll return the first plot if it exists.
469
  first_plot = plots[0] if plots else None
470
  return error, df, first_plot, msg
471
  except Exception as e:
472
  return f"Input error: {e}", None, None, "Failed"
473
 
474
-
475
- # We create a single plot output for simplicity. For multiple plots, a dynamic UI update is more complex.
476
- plot_output = gr.Plot(label="Time Series Plot")
477
-
478
  calculate_button.click(
479
  fn=calculate_wrapper,
480
  inputs=[
 
18
  import folium
19
  import xml.etree.ElementTree as ET
20
 
21
+ # --- Helper Functions ---
22
 
23
  def one_time_setup():
24
  """Initializes the Earth Engine API."""
 
42
  # In a real app, you might want to show an error to the user here
43
  # For this Gradio app, we'll let it proceed and fail gracefully later if EE is needed.
44
 
45
+ def _process_spatial_data(data_bytes):
46
+ """Core function to process bytes of a KML or GeoJSON file."""
47
+ # Read the first few bytes to determine file type without consuming the stream
48
+ start_of_file = data_bytes.read(100)
49
+ data_bytes.seek(0) # Reset stream position
50
 
51
+ # Check if the file is KML (XML-based)
52
+ if start_of_file.strip().lower().startswith(b'<?xml'):
 
 
 
 
 
 
 
53
  try:
54
+ geojson_data = kml2geojson.convert(data_bytes)
55
+ if not geojson_data or not geojson_data[0].get("features"):
56
+ raise ValueError("KML file is empty or has no features.")
57
+ features = geojson_data[0]["features"]
58
+ input_gdf = gpd.GeoDataFrame.from_features(features, crs="EPSG:4326")
 
 
 
59
  except Exception as e:
60
+ raise ValueError(f"Failed to process KML data: {e}")
61
+ # Otherwise, assume it's a format geopandas can read
62
  else:
63
  try:
64
+ input_gdf = gpd.read_file(data_bytes)
65
  except Exception as e:
66
+ raise ValueError(f"Failed to read GeoJSON or other vector data: {e}")
 
67
  return input_gdf
68
 
69
+ def get_gdf_from_file(file_obj):
70
+ """Reads a KML or GeoJSON file from a Gradio file object and returns a GeoDataFrame."""
71
+ if file_obj is None:
72
+ return None
73
+ with open(file_obj.name, 'rb') as f:
74
+ data_bytes = BytesIO(f.read())
75
+ return _process_spatial_data(data_bytes)
76
+
77
+ 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:
85
+ file_id = url.split('/d/')[1].split('/')[0]
86
+ elif "open?id=" in url:
87
+ file_id = url.split('open?id=')[1].split('&')[0]
88
+ else:
89
+ raise ValueError("Unsupported Google Drive URL format. Please provide a direct link or a shareable link with 'open?id=' or '/file/d/'.")
90
+ download_url = f"https://drive.google.com/uc?export=download&id={file_id}"
91
+ else:
92
+ download_url = url
93
+
94
+ try:
95
+ response = requests.get(download_url, timeout=30)
96
+ response.raise_for_status()
97
+ data_bytes = BytesIO(response.content)
98
+ return _process_spatial_data(data_bytes)
99
+ except requests.exceptions.RequestException as e:
100
+ raise ValueError(f"Failed to download file from URL: {e}")
101
+
102
 
103
  def find_best_epsg(geometry):
104
  """Finds the most suitable EPSG code for a given geometry based on its centroid."""
 
259
  one_time_setup()
260
  WAYBACK_DF = get_wayback_data()
261
 
262
+ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
263
+ """Main function to process the uploaded file or URL and generate initial outputs."""
264
+ if file_obj is None and not (url_str and url_str.strip()):
265
+ return None, "Please upload a file or provide a URL.", None, None, None
266
 
267
  progress(0, desc="Reading and processing geometry...")
268
  try:
269
+ if file_obj is not None:
270
+ # Prioritize file upload
271
+ input_gdf = get_gdf_from_file(file_obj)
272
+ else:
273
+ # Use URL if file is not provided
274
+ input_gdf = get_gdf_from_url(url_str)
275
+
276
  input_gdf = preprocess_gdf(input_gdf)
277
 
278
  # Find the first valid polygon
 
284
  break
285
 
286
  if geometry_gdf is None:
287
+ return None, "No valid polygon found in the provided file or URL.", None, None, None
288
 
289
  geometry_gdf = to_best_crs(geometry_gdf)
290
 
 
328
  })
329
 
330
  # Save geometry data for later use
 
331
  geometry_json = geometry_gdf.to_json()
332
  buffer_geometry_json = buffer_geometry_gdf.to_json()
333
 
 
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
 
392
  return "No satellite imagery found for the selected dates.", None, None, None
393
 
394
  result_df = pd.DataFrame(result_rows).set_index('daterange')
395
+ result_df.index = result_df.index.str.split('-').str[0] # Use start year as index for plotting
396
 
397
  # Create plots
398
  plots = []
 
425
 
426
  with gr.Row():
427
  with gr.Column(scale=1):
428
+ gr.Markdown("## 1. Provide Input Geometry")
429
+ gr.Markdown("Use either file upload OR a URL.")
430
  file_input = gr.File(label="Upload KML/GeoJSON File", file_types=[".kml", ".geojson"])
431
+ url_input = gr.Textbox(label="Or Provide File URL", placeholder="e.g., https://.../my_file.kml")
432
  buffer_input = gr.Number(label="Buffer (meters)", value=50)
433
+ process_button = gr.Button("Process Input", variant="primary")
434
  info_box = gr.Textbox(label="Status", interactive=False)
435
 
436
  with gr.Accordion("Advanced Settings", open=False):
 
448
  evi_c = gr.Number(label="C", value=2.4)
449
 
450
  gr.Markdown("### Date Range")
 
451
  today = datetime.now()
452
  date_start_input = gr.Textbox(label="Start Date (MM-DD)", value="11-15")
453
  date_end_input = gr.Textbox(label="End Date (MM-DD)", value="12-15")
 
456
  min_year_input = gr.Number(label="Start Year", value=2019, precision=0)
457
  max_year_input = gr.Number(label="End Year", value=today.year, precision=0)
458
 
 
459
  calculate_button = gr.Button("Calculate Vegetation Indices", variant="primary")
460
 
 
461
  with gr.Column(scale=2):
462
  gr.Markdown("## 2. Results")
463
  stats_output = gr.DataFrame(label="Geometry Metrics")
464
  map_output = gr.HTML(label="Map View")
465
  results_info_box = gr.Textbox(label="Calculation Status", interactive=False)
466
  timeseries_table = gr.DataFrame(label="Time Series Data")
467
+ plot_output = gr.Plot(label="Time Series Plot")
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")
475
+ return file_url if file_url else ""
476
+
477
+ demo.load(process_on_load, None, url_input)
478
+
479
  process_button.click(
480
  fn=process_and_display,
481
+ inputs=[file_input, url_input, buffer_input],
482
  outputs=[map_output, info_box, stats_output, geometry_data, buffer_geometry_data]
483
  )
484
 
 
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=[