UjjwalKGupta commited on
Commit
077c68e
·
verified ·
1 Parent(s): bb9fd6e

Create app

Browse files
Files changed (1) hide show
  1. app.py +497 -0
app.py ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ import ee
4
+ import json
5
+ import numpy as np
6
+ import geemap.foliumap as gee_folium
7
+ import leafmap.foliumap as leaf_folium
8
+ import gradio as gr
9
+ import pandas as pd
10
+ import geopandas as gpd
11
+ import plotly.express as px
12
+ import branca.colormap as cm
13
+ from shapely.ops import transform
14
+ import pyproj
15
+ from io import BytesIO
16
+ import requests
17
+ 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."""
25
+ try:
26
+ ee.Initialize()
27
+ except Exception as e:
28
+ try:
29
+ # Fallback to service account credentials if default init fails
30
+ credentials_path = os.path.expanduser("~/.config/earthengine/credentials.json")
31
+ ee_credentials = os.environ.get("EE")
32
+ if ee_credentials:
33
+ os.makedirs(os.path.dirname(credentials_path), exist_ok=True)
34
+ with open(credentials_path, "w") as f:
35
+ f.write(ee_credentials)
36
+ credentials = ee.ServiceAccountCredentials('ujjwal@ee-ujjwaliitd.iam.gserviceaccount.com', credentials_path)
37
+ ee.Initialize(credentials, project='ee-ujjwaliitd')
38
+ else:
39
+ raise e
40
+ except Exception as inner_e:
41
+ print(f"Earth Engine initialization failed: {inner_e}")
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."""
77
+ if geometry.geom_type == "Polygon":
78
+ centroid = geometry.centroid
79
+ else:
80
+ raise ValueError("Geometry is not a Polygon.")
81
+
82
+ common_epsg_codes = [
83
+ 7761, # Gujarat
84
+ 7774, # Rajasthan
85
+ 7766, # MadhyaPradesh
86
+ 7767, # Maharastra
87
+ 7755, # India
88
+ # Add other relevant state/country EPSG codes here
89
+ ]
90
+
91
+ for epsg in common_epsg_codes:
92
+ try:
93
+ crs = pyproj.CRS.from_epsg(epsg)
94
+ area_of_use = crs.area_of_use.bounds
95
+ if (area_of_use[0] <= centroid.x <= area_of_use[2]) and \
96
+ (area_of_use[1] <= centroid.y <= area_of_use[3]):
97
+ return epsg
98
+ except pyproj.exceptions.CRSError:
99
+ continue
100
+ return 4326 # Default to WGS84 if no suitable projection is found
101
+
102
+ def shape_3d_to_2d(shape):
103
+ """Converts a 3D geometry to 2D."""
104
+ if shape.has_z:
105
+ return transform(lambda x, y, z: (x, y), shape)
106
+ return shape
107
+
108
+ def preprocess_gdf(gdf):
109
+ """Preprocesses a GeoDataFrame by converting geometries to 2D and fixing invalid ones."""
110
+ gdf["geometry"] = gdf["geometry"].apply(shape_3d_to_2d)
111
+ gdf["geometry"] = gdf.buffer(0)
112
+ return gdf
113
+
114
+ def to_best_crs(gdf):
115
+ """Converts a GeoDataFrame to the most suitable CRS."""
116
+ if not gdf.empty and gdf["geometry"].iloc[0] is not None:
117
+ best_epsg_code = find_best_epsg(gdf.to_crs(epsg=4326)["geometry"].iloc[0])
118
+ return gdf.to_crs(epsg=best_epsg_code)
119
+ return gdf
120
+
121
+ def is_valid_polygon(geometry_gdf):
122
+ """Checks if the geometry in a GeoDataFrame is a valid, non-empty Polygon."""
123
+ if geometry_gdf.empty:
124
+ return False
125
+ geometry = geometry_gdf.geometry.item()
126
+ return (geometry.type == 'Polygon') and (not geometry.is_empty)
127
+
128
+ def add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.0):
129
+ """Adds geometry and its buffer to a folium map."""
130
+ if buffer_geometry_gdf is not None and not buffer_geometry_gdf.empty:
131
+ folium.GeoJson(
132
+ buffer_geometry_gdf.to_crs(epsg=4326),
133
+ name="Geometry Buffer",
134
+ style_function=lambda x: {"color": "red", "fillOpacity": opacity, "fillColor": "red"}
135
+ ).add_to(m)
136
+ if geometry_gdf is not None and not geometry_gdf.empty:
137
+ folium.GeoJson(
138
+ geometry_gdf.to_crs(epsg=4326),
139
+ name="Geometry",
140
+ style_function=lambda x: {"color": "blue", "fillOpacity": opacity, "fillColor": "blue"}
141
+ ).add_to(m)
142
+ return m
143
+
144
+ def get_wayback_data():
145
+ """Fetches and parses Wayback imagery data from ArcGIS."""
146
+ try:
147
+ url = "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml"
148
+ response = requests.get(url)
149
+ response.raise_for_status()
150
+ root = ET.fromstring(response.content)
151
+ ns = {
152
+ "wmts": "http://www.opengis.net/wmts/1.0",
153
+ "ows": "http://www.opengis.net/ows/1.1",
154
+ }
155
+ layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
156
+ layer_data = []
157
+ for layer in layers:
158
+ title = layer.find("ows:Title", ns)
159
+ resource = layer.find("wmts:ResourceURL", ns)
160
+ if title is not None and resource is not None:
161
+ layer_data.append({
162
+ "Title": title.text,
163
+ "ResourceURL_Template": resource.get("template")
164
+ })
165
+ wayback_df = pd.DataFrame(layer_data)
166
+ wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})")[0], errors="coerce")
167
+ wayback_df.set_index("date", inplace=True)
168
+ return wayback_df.sort_index()
169
+ except Exception as e:
170
+ print(f"Could not fetch Wayback data: {e}")
171
+ return pd.DataFrame() # Return empty dataframe on failure
172
+
173
+ def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
174
+ """Calculates and adds multiple vegetation indices to an Earth Engine image."""
175
+ # It's safer to work with the image bands directly
176
+ nir = image.select(nir_band).divide(10000)
177
+ red = image.select(red_band).divide(10000)
178
+ blue = image.select(blue_band).divide(10000)
179
+ green = image.select(green_band).divide(10000)
180
+
181
+ # NDVI
182
+ ndvi = image.normalizedDifference([nir_band, red_band]).rename('NDVI')
183
+
184
+ # EVI
185
+ evi = image.expression(
186
+ 'G * ((NIR - RED) / (NIR + C1 * RED - C2 * BLUE + L))', {
187
+ 'NIR': nir, 'RED': red, 'BLUE': blue,
188
+ 'G': evi_vars['G'], 'C1': evi_vars['C1'], 'C2': evi_vars['C2'], 'L': evi_vars['L']
189
+ }).rename('EVI')
190
+
191
+ # EVI2
192
+ evi2 = image.expression(
193
+ 'G * (NIR - RED) / (NIR + L + C * RED)', {
194
+ 'NIR': nir, 'RED': red,
195
+ 'G': evi_vars['G'], 'L': evi_vars['L'], 'C': evi_vars['C']
196
+ }).rename('EVI2')
197
+
198
+ # RandomForest (This part requires a pre-trained model asset in GEE)
199
+ # For demonstration, we'll return a constant image if asset is not available
200
+ try:
201
+ table = ee.FeatureCollection('projects/in793-aq-nb-24330048/assets/cleanedVDI')
202
+ classifier = ee.Classifier.smileRandomForest(50).train(
203
+ features=table,
204
+ classProperty='cVDI',
205
+ inputProperties=['Blue', 'Red', 'NIR']
206
+ )
207
+ rf = image.select(['Blue', 'Red', 'NIR']).classify(classifier).multiply(0.2).add(0.1).rename('RandomForest')
208
+ except Exception:
209
+ rf = ee.Image.constant(0).rename('RandomForest')
210
+
211
+
212
+ # Cubic Function Index (CI)
213
+ ci = image.expression(
214
+ '(-3.98 * (BLUE/NIR) + 12.54 * (GREEN/NIR) - 5.49 * (RED/NIR) - 0.19) / ' +
215
+ '(-21.87 * (BLUE/NIR) + 12.4 * (GREEN/NIR) + 19.98 * (RED/NIR) + 1) * 2.29', {
216
+ 'NIR': nir, 'RED': red, 'BLUE': blue, 'GREEN': green
217
+ }).rename('CI')
218
+
219
+ # GujVDI
220
+ gujvdi = image.expression(
221
+ '0.5 * (NIR - RED) / (NIR + 6 * RED - 8.25 * BLUE - 0.01)', {
222
+ 'NIR': nir, 'RED': red, 'BLUE': blue
223
+ }).rename('GujVDI')
224
+
225
+ return image.addBands([ndvi, evi, evi2, rf, ci, gujvdi])
226
+
227
+
228
+ # --- Gradio App Logic ---
229
+
230
+ # Initialize GEE and fetch wayback data once
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
245
+ geometry_gdf = None
246
+ for i in range(len(input_gdf)):
247
+ temp_gdf = input_gdf.iloc[[i]]
248
+ if is_valid_polygon(temp_gdf):
249
+ geometry_gdf = temp_gdf
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
+
257
+ # Create buffer
258
+ outer_geometry_gdf = geometry_gdf.copy()
259
+ outer_geometry_gdf["geometry"] = outer_geometry_gdf["geometry"].buffer(buffer_m)
260
+ buffer_geometry_gdf = gpd.GeoDataFrame(
261
+ geometry=[outer_geometry_gdf.unary_union.difference(geometry_gdf.unary_union)],
262
+ crs=geometry_gdf.crs
263
+ )
264
+
265
+ except Exception as e:
266
+ return None, f"Error processing file: {e}", None, None, None
267
+
268
+ progress(0.5, desc="Generating map and stats...")
269
+
270
+ # Create map
271
+ 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)
272
+ if not WAYBACK_DF.empty:
273
+ first_item = WAYBACK_DF.iloc[0]
274
+ wayback_url = (
275
+ first_item["ResourceURL_Template"]
276
+ .replace("{TileMatrixSet}", "GoogleMapsCompatible")
277
+ .replace("{TileMatrix}", "{z}")
278
+ .replace("{TileRow}", "{y}")
279
+ .replace("{TileCol}", "{x}")
280
+ )
281
+ folium.TileLayer(
282
+ tiles=wayback_url,
283
+ attr='Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
284
+ name="Esri Satellite"
285
+ ).add_to(m)
286
+ m.add_child(folium.LayerControl())
287
+ m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
288
+
289
+ # Generate stats
290
+ stats_df = pd.DataFrame({
291
+ "Area (ha)": [geometry_gdf.area.item() / 10000],
292
+ "Perimeter (m)": [geometry_gdf.length.item()],
293
+ "Centroid (Lat, Lon)": [f"({geometry_gdf.to_crs(4326).centroid.y.iloc[0]:.6f}, {geometry_gdf.to_crs(4326).centroid.x.iloc[0]:.6f})"]
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
+
301
+ progress(1, desc="Done!")
302
+ return m._repr_html_(), None, stats_df, geometry_json, buffer_geometry_json
303
+
304
+
305
+ def calculate_indices(
306
+ geometry_json, buffer_geometry_json, veg_indices, evi_vars, date_range,
307
+ min_year, max_year, progress=gr.Progress()
308
+ ):
309
+ """Calculates vegetation indices based on user inputs."""
310
+ if not all([geometry_json, buffer_geometry_json, veg_indices]):
311
+ return "Please process a file and select at least one index first.", None, None, None
312
+
313
+ try:
314
+ # Recreate GDFs from JSON
315
+ geometry_gdf = gpd.read_file(geometry_json)
316
+ buffer_geometry_gdf = gpd.read_file(buffer_geometry_json)
317
+
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
327
+ dates = [
328
+ (f"{year}-{start_month:02d}-{start_day:02d}", f"{year}-{end_month:02d}-{end_day:02d}")
329
+ for year in range(min_year, max_year + 1)
330
+ ]
331
+
332
+ # GEE processing
333
+ collection = (
334
+ ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
335
+ .select(
336
+ ["B2", "B3", "B4", "B8", "MSK_CLDPRB"],
337
+ ["Blue", "Green", "Red", "NIR", "MSK_CLDPRB"]
338
+ )
339
+ .map(lambda img: add_indices(img, 'NIR', 'Red', 'Blue', 'Green', evi_vars))
340
+ )
341
+
342
+ result_rows = []
343
+ total_dates = len(dates)
344
+ for i, (start_date, end_date) in enumerate(dates):
345
+ progress((i + 1) / total_dates, desc=f"Processing {start_date} to {end_date}")
346
+ filtered_collection = collection.filterDate(start_date, end_date).filterBounds(ee_geometry)
347
+ if filtered_collection.size().getInfo() == 0:
348
+ continue
349
+
350
+ row = {'daterange': f"{start_date}_to_{end_date}"}
351
+ for veg_index in veg_indices:
352
+ mosaic = filtered_collection.qualityMosaic(veg_index)
353
+ mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
354
+ buffer_mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=buffer_ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
355
+ row[veg_index] = mean_val
356
+ row[f"{veg_index}_buffer"] = buffer_mean_val
357
+ row[f"{veg_index}_ratio"] = (mean_val / buffer_mean_val) if buffer_mean_val else 0
358
+ result_rows.append(row)
359
+
360
+ if not result_rows:
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 = []
368
+ for veg_index in veg_indices:
369
+ plot_df = result_df[[veg_index, f"{veg_index}_buffer", f"{veg_index}_ratio"]]
370
+ fig = px.line(plot_df, x=plot_df.index, y=plot_df.columns, markers=True, title=f"{veg_index} Time Series")
371
+ fig.update_layout(xaxis_title="Year", yaxis_title="Index Value")
372
+ plots.append(fig)
373
+
374
+ return None, result_df, plots, "Calculation complete."
375
+
376
+ except Exception as e:
377
+ return f"An error occurred during calculation: {e}", None, None, None
378
+
379
+
380
+ # --- Gradio UI Definition ---
381
+
382
+ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
383
+ # Hidden state to store geometry data
384
+ geometry_data = gr.State()
385
+ buffer_geometry_data = gr.State()
386
+
387
+ gr.HTML("""
388
+ <div style="display: flex; justify-content: space-between; align-items: center;">
389
+ <img src="https://huggingface.co/spaces/SustainabilityLabIITGN/NDVI_PERG/resolve/main/Final_IITGN-Logo-symmetric-Color.png" style="width: 10%; margin-right: auto;">
390
+ <h1 style="text-align: center;">Kamlan: KML Analyzer</h1>
391
+ <img src="https://huggingface.co/spaces/SustainabilityLabIITGN/NDVI_PERG/resolve/main/IFS.jpg" style="width: 10%; margin-left: auto;">
392
+ </div>
393
+ """)
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):
404
+ gr.Markdown("### Select Vegetation Indices")
405
+ all_veg_indices = ["GujVDI", "NDVI", "EVI", "EVI2", "RandomForest", "CI"]
406
+ veg_indices_checkboxes = gr.CheckboxGroup(all_veg_indices, label="Indices", value=["NDVI"])
407
+
408
+ gr.Markdown("### EVI/EVI2 Parameters")
409
+ with gr.Row():
410
+ evi_g = gr.Number(label="G", value=2.5)
411
+ evi_c1 = gr.Number(label="C1", value=6.0)
412
+ evi_c2 = gr.Number(label="C2", value=7.5)
413
+ with gr.Row():
414
+ evi_l = gr.Number(label="L", value=1.0)
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")
422
+
423
+ with gr.Row():
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
+
449
+ def calculate_wrapper(geometry_json, buffer_geometry_json, veg_indices,
450
+ g, c1, c2, l, c, start_date_str, end_date_str,
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=[
481
+ geometry_data, buffer_geometry_data, veg_indices_checkboxes,
482
+ evi_g, evi_c1, evi_c2, evi_l, evi_c,
483
+ date_start_input, date_end_input,
484
+ min_year_input, max_year_input
485
+ ],
486
+ outputs=[results_info_box, timeseries_table, plot_output, results_info_box]
487
+ )
488
+
489
+ gr.HTML("""
490
+ <div style="text-align: center; margin-top: 20px;">
491
+ <p>Developed by <a href="https://sustainability-lab.github.io/">Sustainability Lab</a>, <a href="https://www.iitgn.ac.in/">IIT Gandhinagar</a></p>
492
+ <p>Supported by <a href="https://forests.gujarat.gov.in/">Gujarat Forest Department</a></p>
493
+ </div>
494
+ """)
495
+
496
+ if __name__ == "__main__":
497
+ demo.launch(debug=True)