Add Comparison Map
Browse files
app.py
CHANGED
|
@@ -179,23 +179,19 @@ def get_wayback_data():
|
|
| 179 |
root = ET.fromstring(response.content)
|
| 180 |
|
| 181 |
ns = {
|
| 182 |
-
"wmts": "
|
| 183 |
-
"ows": "
|
| 184 |
-
"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 |
-
|
| 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"
|
| 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')
|
| 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 |
-
|
| 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]]]
|
| 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(
|
| 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 |
-
|
| 500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
|
| 502 |
-
|
| 503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("""
|