Fix EE and Wayback Imagery
Browse files
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.
|
| 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": "
|
| 181 |
-
"ows": "
|
|
|
|
| 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 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 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})")
|
| 195 |
wayback_df.set_index("date", inplace=True)
|
| 196 |
-
return wayback_df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
except Exception as e:
|
| 198 |
-
print(f"
|
| 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(
|
|
|
|
| 306 |
if not WAYBACK_DF.empty:
|
| 307 |
-
|
|
|
|
|
|
|
| 308 |
wayback_url = (
|
| 309 |
-
|
| 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=
|
| 318 |
-
name="Esri Satellite"
|
| 319 |
).add_to(m)
|
| 320 |
-
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 501 |
except Exception as e:
|
| 502 |
-
|
|
|
|
| 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
|
| 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)
|