YashMK89 commited on
Commit
74e8b71
·
verified ·
1 Parent(s): ad7d92d

update app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -945
app.py CHANGED
@@ -1,948 +1,3 @@
1
- # import streamlit as st
2
- # import json
3
- # import ee
4
- # import os
5
- # import pandas as pd
6
- # import geopandas as gpd
7
- # from datetime import datetime
8
- # import leafmap.foliumap as leafmap
9
- # import re
10
- # from shapely.geometry import base
11
- # from xml.etree import ElementTree as XET
12
- # from concurrent.futures import ThreadPoolExecutor, as_completed
13
- # import time
14
- # import matplotlib.pyplot as plt
15
- # import plotly.express as px
16
-
17
- # # Set up the page layout
18
- # st.set_page_config(layout="wide")
19
-
20
- # # Custom button styling
21
- # m = st.markdown(
22
- # """
23
- # <style>
24
- # div.stButton > button:first-child {
25
- # background-color: #006400;
26
- # color:#ffffff;
27
- # }
28
- # </style>""",
29
- # unsafe_allow_html=True,
30
- # )
31
-
32
- # # Logo and Title
33
- # st.write(
34
- # f"""
35
- # <div style="display: flex; justify-content: space-between; align-items: center;">
36
- # <img src="https://huggingface.co/spaces/YashMK89/SATRANG/resolve/main/ISRO_Logo.png" style="width: 20%; margin-right: auto;">
37
- # <img src="https://huggingface.co/spaces/YashMK89/SATRANG/resolve/main/SAC_Logo.png" style="width: 20%; margin-left: auto;">
38
- # </div>
39
- # """,
40
- # unsafe_allow_html=True,
41
- # )
42
- # st.markdown(
43
- # f"""
44
- # <div style="display: flex; flex-direction: column; align-items: center;">
45
- # <img src="https://huggingface.co/spaces/YashMK89/SATRANG/resolve/main/SATRANG.png" style="width: 30%;">
46
- # <h3 style="text-align: center; margin: 0;">( Spatial and Temporal Aggregation for Remote-sensing Analysis of GEE Data )</h3>
47
- # </div>
48
- # <hr>
49
- # """,
50
- # unsafe_allow_html=True,
51
- # )
52
-
53
- # # Authenticate and initialize Earth Engine
54
- # earthengine_credentials = os.environ.get("EE_Authentication")
55
- # os.makedirs(os.path.expanduser("~/.config/earthengine/"), exist_ok=True)
56
- # with open(os.path.expanduser("~/.config/earthengine/credentials"), "w") as f:
57
- # f.write(earthengine_credentials)
58
- # ee.Initialize(project='ee-yashsacisro24')
59
-
60
- # # Helper function to get reducer
61
- # def get_reducer(reducer_name):
62
- # reducers = {
63
- # 'mean': ee.Reducer.mean(),
64
- # 'sum': ee.Reducer.sum(),
65
- # 'median': ee.Reducer.median(),
66
- # 'min': ee.Reducer.min(),
67
- # 'max': ee.Reducer.max(),
68
- # 'count': ee.Reducer.count(),
69
- # }
70
- # return reducers.get(reducer_name.lower(), ee.Reducer.mean())
71
-
72
- # # Function to convert geometry to Earth Engine format
73
- # def convert_to_ee_geometry(geometry):
74
- # if isinstance(geometry, base.BaseGeometry):
75
- # if geometry.is_valid:
76
- # geojson = geometry.__geo_interface__
77
- # return ee.Geometry(geojson)
78
- # else:
79
- # raise ValueError("Invalid geometry: The polygon geometry is not valid.")
80
- # elif isinstance(geometry, dict) or isinstance(geometry, str):
81
- # try:
82
- # if isinstance(geometry, str):
83
- # geometry = json.loads(geometry)
84
- # if 'type' in geometry and 'coordinates' in geometry:
85
- # return ee.Geometry(geometry)
86
- # else:
87
- # raise ValueError("GeoJSON format is invalid.")
88
- # except Exception as e:
89
- # raise ValueError(f"Error parsing GeoJSON: {e}")
90
- # elif isinstance(geometry, str) and geometry.lower().endswith(".kml"):
91
- # try:
92
- # tree = XET.parse(geometry)
93
- # kml_root = tree.getroot()
94
- # kml_namespace = {'kml': 'http://www.opengis.net/kml/2.2'}
95
- # coordinates = kml_root.findall(".//kml:coordinates", kml_namespace)
96
- # if coordinates:
97
- # coords_text = coordinates[0].text.strip()
98
- # coords = coords_text.split()
99
- # coords = [tuple(map(float, coord.split(','))) for coord in coords]
100
- # geojson = {"type": "Polygon", "coordinates": [coords]}
101
- # return ee.Geometry(geojson)
102
- # else:
103
- # raise ValueError("KML does not contain valid coordinates.")
104
- # except Exception as e:
105
- # raise ValueError(f"Error parsing KML: {e}")
106
- # else:
107
- # raise ValueError("Unsupported geometry input type. Supported types are Shapely, GeoJSON, and KML.")
108
-
109
- # # Function to calculate custom formula with dynamic scale handling
110
- # def calculate_custom_formula(image, geometry, selected_bands, custom_formula, reducer_choice, dataset_id, user_scale=None):
111
- # try:
112
- # # Fetch the nominal scales of the selected bands
113
- # band_scales = []
114
- # for band in selected_bands:
115
- # band_scale = image.select(band).projection().nominalScale().getInfo()
116
- # band_scales.append(band_scale)
117
-
118
- # # Determine the finest (smallest) scale among the selected bands
119
- # default_scale = min(band_scales) if band_scales else 30 # Default to 30m if no bands are found
120
-
121
- # # # Compute the finest scale among all bands
122
- # # band_scales = [
123
- # # first_image.select(band).projection().nominalScale().getInfo()
124
- # # for band in first_image.bandNames().getInfo()
125
- # # ]
126
- # # default_scale = min(band_scales)
127
-
128
- # # Use user-defined scale if provided, otherwise use the finest scale
129
-
130
- # scale = user_scale if user_scale is not None else default_scale
131
-
132
- # # Rescale all bands to the chosen scale
133
- # rescaled_bands = {}
134
- # for band in selected_bands:
135
- # band_image = image.select(band)
136
- # band_scale = band_image.projection().nominalScale().getInfo()
137
- # if band_scale != scale:
138
- # # Resample the band to match the target scale
139
- # rescaled_band = band_image.resample('bilinear').reproject(
140
- # crs=band_image.projection().crs(),
141
- # scale=scale
142
- # )
143
- # rescaled_bands[band] = rescaled_band
144
- # else:
145
- # rescaled_bands[band] = band_image
146
-
147
- # # Validate and extract band values
148
- # reduced_values = {}
149
- # reducer = get_reducer(reducer_choice)
150
- # for band in selected_bands:
151
- # value = rescaled_bands[band].reduceRegion(
152
- # reducer=reducer,
153
- # geometry=geometry,
154
- # scale=scale # Use the determined scale here
155
- # ).get(band).getInfo()
156
- # reduced_values[band] = float(value if value is not None else 0)
157
-
158
- # # Evaluate the custom formula
159
- # formula = custom_formula
160
- # for band in selected_bands:
161
- # formula = formula.replace(band, str(reduced_values[band]))
162
- # result = eval(formula, {"__builtins__": {}}, reduced_values)
163
-
164
- # # Validate the result
165
- # if not isinstance(result, (int, float)):
166
- # raise ValueError("Formula did not result in a numeric value.")
167
-
168
- # return ee.Image.constant(result).rename('custom_result')
169
-
170
- # except ZeroDivisionError:
171
- # st.error("Error: Division by zero in the formula.")
172
- # return ee.Image(0).rename('custom_result').set('error', 'Division by zero')
173
- # except SyntaxError:
174
- # st.error(f"Error: Invalid syntax in formula '{custom_formula}'.")
175
- # return ee.Image(0).rename('custom_result').set('error', 'Invalid syntax')
176
- # except ValueError as e:
177
- # st.error(f"Error: {str(e)}")
178
- # return ee.Image(0).rename('custom_result').set('error', str(e))
179
- # except Exception as e:
180
- # st.error(f"Unexpected error: {e}")
181
- # return ee.Image(0).rename('custom_result').set('error', str(e))
182
-
183
- # # Aggregation functions
184
- # def aggregate_data_custom(collection):
185
- # collection = collection.map(lambda image: image.set('day', ee.Date(image.get('system:time_start')).format('YYYY-MM-dd')))
186
- # grouped_by_day = collection.aggregate_array('day').distinct()
187
- # def calculate_daily_mean(day):
188
- # daily_collection = collection.filter(ee.Filter.eq('day', day))
189
- # daily_mean = daily_collection.mean()
190
- # return daily_mean.set('day', day)
191
- # daily_images = ee.List(grouped_by_day.map(calculate_daily_mean))
192
- # return ee.ImageCollection(daily_images)
193
-
194
- # def aggregate_data_daily(collection):
195
- # def set_day_start(image):
196
- # date = ee.Date(image.get('system:time_start'))
197
- # day_start = date.format('YYYY-MM-dd')
198
- # return image.set('day_start', day_start)
199
- # collection = collection.map(set_day_start)
200
- # grouped_by_day = collection.aggregate_array('day_start').distinct()
201
- # def calculate_daily_mean(day_start):
202
- # daily_collection = collection.filter(ee.Filter.eq('day_start', day_start))
203
- # daily_mean = daily_collection.mean()
204
- # return daily_mean.set('day_start', day_start)
205
- # daily_images = ee.List(grouped_by_day.map(calculate_daily_mean))
206
- # return ee.ImageCollection(daily_images)
207
-
208
- # def aggregate_data_weekly(collection, start_date_str, end_date_str):
209
- # start_date = ee.Date(start_date_str)
210
- # end_date = ee.Date(end_date_str)
211
- # days_diff = end_date.difference(start_date, 'day')
212
- # num_weeks = days_diff.divide(7).ceil().getInfo()
213
- # weekly_images = []
214
- # for week in range(num_weeks):
215
- # week_start = start_date.advance(week * 7, 'day')
216
- # week_end = week_start.advance(7, 'day')
217
- # weekly_collection = collection.filterDate(week_start, week_end)
218
- # if weekly_collection.size().getInfo() > 0:
219
- # weekly_mean = weekly_collection.mean()
220
- # weekly_mean = weekly_mean.set('week_start', week_start.format('YYYY-MM-dd'))
221
- # weekly_images.append(weekly_mean)
222
- # return ee.ImageCollection.fromImages(weekly_images)
223
-
224
- # def aggregate_data_monthly(collection, start_date, end_date):
225
- # collection = collection.filterDate(start_date, end_date)
226
- # collection = collection.map(lambda image: image.set('month', ee.Date(image.get('system:time_start')).format('YYYY-MM')))
227
- # grouped_by_month = collection.aggregate_array('month').distinct()
228
- # def calculate_monthly_mean(month):
229
- # monthly_collection = collection.filter(ee.Filter.eq('month', month))
230
- # monthly_mean = monthly_collection.mean()
231
- # return monthly_mean.set('month', month)
232
- # monthly_images = ee.List(grouped_by_month.map(calculate_monthly_mean))
233
- # return ee.ImageCollection(monthly_images)
234
-
235
- # def aggregate_data_yearly(collection):
236
- # collection = collection.map(lambda image: image.set('year', ee.Date(image.get('system:time_start')).format('YYYY')))
237
- # grouped_by_year = collection.aggregate_array('year').distinct()
238
- # def calculate_yearly_mean(year):
239
- # yearly_collection = collection.filter(ee.Filter.eq('year', year))
240
- # yearly_mean = yearly_collection.mean()
241
- # return yearly_mean.set('year', year)
242
- # yearly_images = ee.List(grouped_by_year.map(calculate_yearly_mean))
243
- # return ee.ImageCollection(yearly_images)
244
-
245
- # # Define the function before using it
246
- # def calculate_cloud_percentage(image, cloud_band='QA60'):
247
- # """
248
- # Calculate the percentage of cloud-covered pixels in an image using the QA60 bitmask.
249
- # Assumes the presence of the QA60 cloud mask band.
250
- # """
251
- # # Decode the QA60 bitmask
252
- # qa60 = image.select(cloud_band)
253
- # opaque_clouds = qa60.bitwiseAnd(1 << 10) # Bit 10: Opaque clouds
254
- # cirrus_clouds = qa60.bitwiseAnd(1 << 11) # Bit 11: Cirrus clouds
255
- # # Combine both cloud types into a single cloud mask
256
- # cloud_mask = opaque_clouds.Or(cirrus_clouds)
257
- # # Count total pixels and cloudy pixels
258
- # total_pixels = qa60.reduceRegion(
259
- # reducer=ee.Reducer.count(),
260
- # geometry=image.geometry(),
261
- # scale=60, # QA60 resolution is 60 meters
262
- # maxPixels=1e13
263
- # ).get(cloud_band)
264
- # cloudy_pixels = cloud_mask.reduceRegion(
265
- # reducer=ee.Reducer.sum(),
266
- # geometry=image.geometry(),
267
- # scale=60, # QA60 resolution is 60 meters
268
- # maxPixels=1e13
269
- # ).get(cloud_band)
270
- # # Calculate cloud percentage
271
- # if total_pixels == 0:
272
- # return 0 # Avoid division by zero
273
- # return ee.Number(cloudy_pixels).divide(ee.Number(total_pixels)).multiply(100)
274
-
275
- # # Use the function in preprocessing
276
- # def preprocess_collection(collection, tile_cloud_threshold, pixel_cloud_threshold):
277
- # def filter_tile(image):
278
- # cloud_percentage = calculate_cloud_percentage(image, cloud_band='QA60')
279
- # return image.set('cloud_percentage', cloud_percentage).updateMask(cloud_percentage.lt(tile_cloud_threshold))
280
-
281
- # def mask_cloudy_pixels(image):
282
- # qa60 = image.select('QA60')
283
- # opaque_clouds = qa60.bitwiseAnd(1 << 10)
284
- # cirrus_clouds = qa60.bitwiseAnd(1 << 11)
285
- # cloud_mask = opaque_clouds.Or(cirrus_clouds)
286
- # clear_pixels = cloud_mask.Not()
287
- # return image.updateMask(clear_pixels)
288
-
289
- # filtered_collection = collection.map(filter_tile)
290
- # masked_collection = filtered_collection.map(mask_cloudy_pixels)
291
- # return masked_collection
292
-
293
- # def process_single_geometry(row, start_date_str, end_date_str, dataset_id, selected_bands, reducer_choice, shape_type, aggregation_period, custom_formula, original_lat_col, original_lon_col, kernel_size=None, include_boundary=None, user_scale=None):
294
- # if shape_type.lower() == "point":
295
- # latitude = row.get('latitude')
296
- # longitude = row.get('longitude')
297
- # if pd.isna(latitude) or pd.isna(longitude):
298
- # return None
299
- # location_name = row.get('name', f"Location_{row.name}")
300
- # if kernel_size == "3x3 Kernel":
301
- # buffer_size = 45
302
- # roi = ee.Geometry.Point([longitude, latitude]).buffer(buffer_size).bounds()
303
- # elif kernel_size == "5x5 Kernel":
304
- # buffer_size = 75
305
- # roi = ee.Geometry.Point([longitude, latitude]).buffer(buffer_size).bounds()
306
- # else:
307
- # roi = ee.Geometry.Point([longitude, latitude])
308
- # elif shape_type.lower() == "polygon":
309
- # polygon_geometry = row.get('geometry')
310
- # location_name = row.get('name', f"Polygon_{row.name}")
311
- # try:
312
- # roi = convert_to_ee_geometry(polygon_geometry)
313
- # if not include_boundary:
314
- # roi = roi.buffer(-30).bounds()
315
- # except ValueError:
316
- # return None
317
- # collection = ee.ImageCollection(dataset_id) \
318
- # .filterDate(ee.Date(start_date_str), ee.Date(end_date_str)) \
319
- # .filterBounds(roi)
320
- # if aggregation_period.lower() == 'custom (start date to end date)':
321
- # collection = aggregate_data_custom(collection)
322
- # elif aggregation_period.lower() == 'daily':
323
- # collection = aggregate_data_daily(collection)
324
- # elif aggregation_period.lower() == 'weekly':
325
- # collection = aggregate_data_weekly(collection, start_date_str, end_date_str)
326
- # elif aggregation_period.lower() == 'monthly':
327
- # collection = aggregate_data_monthly(collection, start_date_str, end_date_str)
328
- # elif aggregation_period.lower() == 'yearly':
329
- # collection = aggregate_data_yearly(collection)
330
- # image_list = collection.toList(collection.size())
331
- # processed_weeks = set()
332
- # aggregated_results = []
333
- # for i in range(image_list.size().getInfo()):
334
- # image = ee.Image(image_list.get(i))
335
- # if aggregation_period.lower() == 'custom (start date to end date)':
336
- # timestamp = image.get('day')
337
- # period_label = 'Date'
338
- # date = ee.Date(timestamp).format('YYYY-MM-dd').getInfo()
339
- # elif aggregation_period.lower() == 'daily':
340
- # timestamp = image.get('day_start')
341
- # period_label = 'Date'
342
- # date = ee.String(timestamp).getInfo()
343
- # elif aggregation_period.lower() == 'weekly':
344
- # timestamp = image.get('week_start')
345
- # period_label = 'Week'
346
- # date = ee.String(timestamp).getInfo()
347
- # if (pd.to_datetime(date) < pd.to_datetime(start_date_str) or
348
- # pd.to_datetime(date) > pd.to_datetime(end_date_str) or
349
- # date in processed_weeks):
350
- # continue
351
- # processed_weeks.add(date)
352
- # elif aggregation_period.lower() == 'monthly':
353
- # timestamp = image.get('month')
354
- # period_label = 'Month'
355
- # date = ee.Date(timestamp).format('YYYY-MM').getInfo()
356
- # elif aggregation_period.lower() == 'yearly':
357
- # timestamp = image.get('year')
358
- # period_label = 'Year'
359
- # date = ee.Date(timestamp).format('YYYY').getInfo()
360
- # index_image = calculate_custom_formula(image, roi, selected_bands, custom_formula, reducer_choice, dataset_id, user_scale=user_scale)
361
- # try:
362
- # index_value = index_image.reduceRegion(
363
- # reducer=get_reducer(reducer_choice),
364
- # geometry=roi,
365
- # scale=user_scale
366
- # ).get('custom_result')
367
- # calculated_value = index_value.getInfo()
368
- # if isinstance(calculated_value, (int, float)):
369
- # result = {
370
- # 'Location Name': location_name,
371
- # period_label: date,
372
- # 'Start Date': start_date_str,
373
- # 'End Date': end_date_str,
374
- # 'Calculated Value': calculated_value
375
- # }
376
- # if shape_type.lower() == 'point':
377
- # result[original_lat_col] = latitude
378
- # result[original_lon_col] = longitude
379
- # aggregated_results.append(result)
380
- # except Exception as e:
381
- # st.error(f"Error retrieving value for {location_name}: {e}")
382
- # return aggregated_results
383
-
384
- # def process_aggregation(locations_df, start_date_str, end_date_str, dataset_id, selected_bands, reducer_choice, shape_type, aggregation_period, original_lat_col, original_lon_col, custom_formula="", kernel_size=None, include_boundary=None, tile_cloud_threshold=0, pixel_cloud_threshold=0, user_scale=None):
385
- # aggregated_results = []
386
- # total_steps = len(locations_df)
387
- # progress_bar = st.progress(0)
388
- # progress_text = st.empty()
389
- # start_time = time.time()
390
- # raw_collection = ee.ImageCollection(dataset_id) \
391
- # .filterDate(ee.Date(start_date_str), ee.Date(end_date_str))
392
- # st.write(f"Original Collection Size: {raw_collection.size().getInfo()}")
393
- # if tile_cloud_threshold > 0 or pixel_cloud_threshold > 0:
394
- # raw_collection = preprocess_collection(raw_collection, tile_cloud_threshold, pixel_cloud_threshold)
395
- # st.write(f"Preprocessed Collection Size: {raw_collection.size().getInfo()}")
396
- # with ThreadPoolExecutor(max_workers=10) as executor:
397
- # futures = []
398
- # for idx, row in locations_df.iterrows():
399
- # future = executor.submit(
400
- # process_single_geometry,
401
- # row,
402
- # start_date_str,
403
- # end_date_str,
404
- # dataset_id,
405
- # selected_bands,
406
- # reducer_choice,
407
- # shape_type,
408
- # aggregation_period,
409
- # custom_formula,
410
- # original_lat_col,
411
- # original_lon_col,
412
- # kernel_size,
413
- # include_boundary,
414
- # user_scale=user_scale
415
- # )
416
- # futures.append(future)
417
- # completed = 0
418
- # for future in as_completed(futures):
419
- # result = future.result()
420
- # if result:
421
- # aggregated_results.extend(result)
422
- # completed += 1
423
- # progress_percentage = completed / total_steps
424
- # progress_bar.progress(progress_percentage)
425
- # progress_text.markdown(f"Processing: {int(progress_percentage * 100)}%")
426
- # end_time = time.time()
427
- # processing_time = end_time - start_time
428
- # if aggregated_results:
429
- # result_df = pd.DataFrame(aggregated_results)
430
- # if aggregation_period.lower() == 'custom (start date to end date)':
431
- # agg_dict = {
432
- # 'Start Date': 'first',
433
- # 'End Date': 'first',
434
- # 'Calculated Value': 'mean' # Ensure this column is named 'Calculated Value'
435
- # }
436
- # if shape_type.lower() == 'point':
437
- # agg_dict[original_lat_col] = 'first'
438
- # agg_dict[original_lon_col] = 'first'
439
- # aggregated_output = result_df.groupby('Location Name').agg(agg_dict).reset_index()
440
- # aggregated_output['Date Range'] = aggregated_output['Start Date'] + " to " + aggregated_output['End Date']
441
- # return aggregated_output.to_dict(orient='records'), processing_time
442
- # else:
443
- # return result_df.to_dict(orient='records'), processing_time
444
- # return [], processing_time
445
-
446
- # # Streamlit App Logic
447
- # st.markdown("<h5>Image Collection</h5>", unsafe_allow_html=True)
448
- # imagery_base = st.selectbox("Select Imagery Base", ["Sentinel", "Landsat", "MODIS", "VIIRS", "Custom Input"], index=0)
449
- # data = {}
450
- # if imagery_base == "Sentinel":
451
- # dataset_file = "sentinel_datasets.json"
452
- # try:
453
- # with open(dataset_file) as f:
454
- # data = json.load(f)
455
- # except FileNotFoundError:
456
- # st.error(f"Dataset file '{dataset_file}' not found.")
457
- # data = {}
458
- # elif imagery_base == "Landsat":
459
- # dataset_file = "landsat_datasets.json"
460
- # try:
461
- # with open(dataset_file) as f:
462
- # data = json.load(f)
463
- # except FileNotFoundError:
464
- # st.error(f"Dataset file '{dataset_file}' not found.")
465
- # data = {}
466
- # elif imagery_base == "MODIS":
467
- # dataset_file = "modis_datasets.json"
468
- # try:
469
- # with open(dataset_file) as f:
470
- # data = json.load(f)
471
- # except FileNotFoundError:
472
- # st.error(f"Dataset file '{dataset_file}' not found.")
473
- # data = {}
474
- # elif imagery_base == "VIIRS":
475
- # dataset_file = "viirs_datasets.json"
476
- # try:
477
- # with open(dataset_file) as f:
478
- # data = json.load(f)
479
- # except FileNotFoundError:
480
- # st.error(f"Dataset file '{dataset_file}' not found.")
481
- # data = {}
482
- # elif imagery_base == "Custom Input":
483
- # custom_dataset_id = st.text_input("Enter Custom Earth Engine Dataset ID (e.g., AHN/AHN4)", value="")
484
- # if custom_dataset_id:
485
- # try:
486
- # if custom_dataset_id.startswith("ee.ImageCollection("):
487
- # custom_dataset_id = custom_dataset_id.replace("ee.ImageCollection('", "").replace("')", "")
488
- # collection = ee.ImageCollection(custom_dataset_id)
489
- # first_image = collection.first()
490
- # default_scale = first_image.projection().nominalScale().getInfo()
491
- # band_names = first_image.bandNames().getInfo()
492
- # data = {
493
- # f"Custom Dataset: {custom_dataset_id}": {
494
- # "sub_options": {custom_dataset_id: f"Custom Dataset ({custom_dataset_id})"},
495
- # "bands": {custom_dataset_id: band_names}
496
- # }
497
- # }
498
- # st.write(f"Fetched bands for {custom_dataset_id}: {', '.join(band_names)}")
499
- # st.write(f"Default Scale for Dataset: {default_scale} meters")
500
- # except Exception as e:
501
- # st.error(f"Error fetching dataset: {str(e)}. Please check the dataset ID and ensure it's valid in Google Earth Engine.")
502
- # data = {}
503
- # else:
504
- # st.warning("Please enter a custom dataset ID to proceed.")
505
- # data = {}
506
- # if not data:
507
- # st.error("No valid dataset available. Please check your inputs.")
508
- # st.stop()
509
-
510
- # st.markdown("<hr><h5><b>{}</b></h5>".format(imagery_base), unsafe_allow_html=True)
511
- # main_selection = st.selectbox(f"Select {imagery_base} Dataset Category", list(data.keys()))
512
- # sub_selection = None
513
- # dataset_id = None
514
- # if main_selection:
515
- # sub_options = data[main_selection]["sub_options"]
516
- # sub_selection = st.selectbox(f"Select Specific {imagery_base} Dataset ID", list(sub_options.keys()))
517
- # if sub_selection:
518
- # st.write(f"You selected: {main_selection} -> {sub_options[sub_selection]}")
519
- # st.write(f"Dataset ID: {sub_selection}")
520
- # dataset_id = sub_selection
521
-
522
- # # Fetch the default scale for the selected dataset
523
- # try:
524
- # collection = ee.ImageCollection(dataset_id)
525
- # first_image = collection.first()
526
- # # Select the first band to avoid issues with multiple projections
527
- # default_scale = first_image.select(0).projection().nominalScale().getInfo()
528
- # st.write(f"Default Scale for Selected Dataset: {default_scale} meters")
529
- # except Exception as e:
530
- # st.error(f"Error fetching default scale: {str(e)}")
531
-
532
- # st.markdown("<hr><h5><b>Earth Engine Index Calculator</b></h5>", unsafe_allow_html=True)
533
- # if main_selection and sub_selection:
534
- # dataset_bands = data[main_selection]["bands"].get(sub_selection, [])
535
- # st.write(f"Available Bands for {sub_options[sub_selection]}: {', '.join(dataset_bands)}")
536
- # selected_bands = st.multiselect(
537
- # "Select 1 or 2 Bands for Calculation",
538
- # options=dataset_bands,
539
- # default=[dataset_bands[0]] if dataset_bands else [],
540
- # help=f"Select 1 or 2 bands from: {', '.join(dataset_bands)}"
541
- # )
542
- # if len(selected_bands) < 1:
543
- # st.warning("Please select at least one band.")
544
- # st.stop()
545
- # if selected_bands:
546
- # if len(selected_bands) == 1:
547
- # default_formula = f"{selected_bands[0]}"
548
- # example = f"'{selected_bands[0]} * 2' or '{selected_bands[0]} + 1'"
549
- # else:
550
- # default_formula = f"({selected_bands[0]} - {selected_bands[1]}) / ({selected_bands[0]} + {selected_bands[1]})"
551
- # example = f"'{selected_bands[0]} * {selected_bands[1]} / 2' or '({selected_bands[0]} - {selected_bands[1]}) / ({selected_bands[0]} + {selected_bands[1]})'"
552
- # custom_formula = st.text_input(
553
- # "Enter Custom Formula (e.g (B8 - B4) / (B8 + B4) , B4*B3/2)",
554
- # value=default_formula,
555
- # help=f"Use only these bands: {', '.join(selected_bands)}. Examples: {example}"
556
- # )
557
- # def validate_formula(formula, selected_bands):
558
- # allowed_chars = set(" +-*/()0123456789.")
559
- # terms = re.findall(r'[a-zA-Z][a-zA-Z0-9_]*', formula)
560
- # invalid_terms = [term for term in terms if term not in selected_bands]
561
- # if invalid_terms:
562
- # return False, f"Invalid terms in formula: {', '.join(invalid_terms)}. Use only {', '.join(selected_bands)}."
563
- # if not all(char in allowed_chars or char in ''.join(selected_bands) for char in formula):
564
- # return False, "Formula contains invalid characters. Use only bands, numbers, and operators (+, -, *, /, ())"
565
- # return True, ""
566
- # is_valid, error_message = validate_formula(custom_formula, selected_bands)
567
- # if not is_valid:
568
- # st.error(error_message)
569
- # st.stop()
570
- # elif not custom_formula:
571
- # st.warning("Please enter a custom formula to proceed.")
572
- # st.stop()
573
- # st.write(f"Custom Formula: {custom_formula}")
574
-
575
- # reducer_choice = st.selectbox(
576
- # "Select Reducer (e.g, mean , sum , median , min , max , count)",
577
- # ['mean', 'sum', 'median', 'min', 'max', 'count'],
578
- # index=0
579
- # )
580
- # start_date = st.date_input("Start Date", value=pd.to_datetime('2024-11-01'))
581
- # end_date = st.date_input("End Date", value=pd.to_datetime('2024-12-01'))
582
- # start_date_str = start_date.strftime('%Y-%m-%d')
583
- # end_date_str = end_date.strftime('%Y-%m-%d')
584
- # if imagery_base == "Sentinel" and "Sentinel-2" in sub_options[sub_selection]:
585
- # st.markdown("<h5>Cloud Filtering</h5>", unsafe_allow_html=True)
586
- # tile_cloud_threshold = st.slider(
587
- # "Select Maximum Tile-Based Cloud Coverage Threshold (%)",
588
- # min_value=0,
589
- # max_value=100,
590
- # value=20,
591
- # step=5,
592
- # help="Tiles with cloud coverage exceeding this threshold will be excluded."
593
- # )
594
- # pixel_cloud_threshold = st.slider(
595
- # "Select Maximum Pixel-Based Cloud Coverage Threshold (%)",
596
- # min_value=0,
597
- # max_value=100,
598
- # value=10,
599
- # step=5,
600
- # help="Individual pixels with cloud coverage exceeding this threshold will be masked."
601
- # )
602
- # aggregation_period = st.selectbox(
603
- # "Select Aggregation Period (e.g, Custom(Start Date to End Date) , Daily , Weekly , Monthly , Yearly)",
604
- # ["Custom (Start Date to End Date)", "Daily", "Weekly", "Monthly", "Yearly"],
605
- # index=0
606
- # )
607
- # shape_type = st.selectbox("Do you want to process 'Point' or 'Polygon' data?", ["Point", "Polygon"])
608
- # kernel_size = None
609
- # include_boundary = None
610
- # if shape_type.lower() == "point":
611
- # kernel_size = st.selectbox(
612
- # "Select Calculation Area(e.g, Point , 3x3 Kernel , 5x5 Kernel)",
613
- # ["Point", "3x3 Kernel", "5x5 Kernel"],
614
- # index=0,
615
- # help="Choose 'Point' for exact point calculation, or a kernel size for area averaging."
616
- # )
617
- # elif shape_type.lower() == "polygon":
618
- # include_boundary = st.checkbox(
619
- # "Include Boundary Pixels",
620
- # value=True,
621
- # help="Check to include pixels on the polygon boundary; uncheck to exclude them."
622
- # )
623
- # st.markdown("<h5>Calculation Scale</h5>", unsafe_allow_html=True)
624
- # default_scale = ee.ImageCollection(dataset_id).first().select(0).projection().nominalScale().getInfo()
625
- # user_scale = st.number_input(
626
- # "Enter Calculation Scale (meters) [Leave blank to use dataset's default scale]",
627
- # min_value=1.0,
628
- # value=float(default_scale),
629
- # help=f"Default scale for this dataset is {default_scale} meters. Adjust if needed."
630
- # )
631
-
632
- # file_upload = st.file_uploader(f"Upload your {shape_type} data (CSV, GeoJSON, KML)", type=["csv", "geojson", "kml"])
633
- # locations_df = pd.DataFrame()
634
- # original_lat_col = None
635
- # original_lon_col = None
636
- # if file_upload is not None:
637
- # if shape_type.lower() == "point":
638
- # if file_upload.name.endswith('.csv'):
639
- # locations_df = pd.read_csv(file_upload)
640
- # st.write("Preview of your uploaded data (first 5 rows):")
641
- # st.dataframe(locations_df.head())
642
- # all_columns = locations_df.columns.tolist()
643
- # col1, col2 = st.columns(2)
644
- # with col1:
645
- # original_lat_col = st.selectbox(
646
- # "Select Latitude Column",
647
- # options=all_columns,
648
- # index=all_columns.index('latitude') if 'latitude' in all_columns else 0,
649
- # help="Select the column containing latitude values"
650
- # )
651
- # with col2:
652
- # original_lon_col = st.selectbox(
653
- # "Select Longitude Column",
654
- # options=all_columns,
655
- # index=all_columns.index('longitude') if 'longitude' in all_columns else 0,
656
- # help="Select the column containing longitude values"
657
- # )
658
- # if not pd.api.types.is_numeric_dtype(locations_df[original_lat_col]) or not pd.api.types.is_numeric_dtype(locations_df[original_lon_col]):
659
- # st.error("Error: Selected Latitude and Longitude columns must contain numeric values")
660
- # st.stop()
661
- # locations_df = locations_df.rename(columns={
662
- # original_lat_col: 'latitude',
663
- # original_lon_col: 'longitude'
664
- # })
665
- # elif file_upload.name.endswith('.geojson'):
666
- # locations_df = gpd.read_file(file_upload)
667
- # if 'geometry' in locations_df.columns:
668
- # locations_df['latitude'] = locations_df['geometry'].y
669
- # locations_df['longitude'] = locations_df['geometry'].x
670
- # original_lat_col = 'latitude'
671
- # original_lon_col = 'longitude'
672
- # else:
673
- # st.error("GeoJSON file doesn't contain geometry column")
674
- # st.stop()
675
- # elif file_upload.name.endswith('.kml'):
676
- # kml_string = file_upload.read().decode('utf-8')
677
- # try:
678
- # root = XET.fromstring(kml_string)
679
- # ns = {'kml': 'http://www.opengis.net/kml/2.2'}
680
- # points = []
681
- # for placemark in root.findall('.//kml:Placemark', ns):
682
- # name = placemark.findtext('kml:name', default=f"Point_{len(points)}", namespaces=ns)
683
- # coords_elem = placemark.find('.//kml:Point/kml:coordinates', ns)
684
- # if coords_elem is not None:
685
- # coords_text = coords_elem.text.strip()
686
- # coords = [c.strip() for c in coords_text.split(',')]
687
- # if len(coords) >= 2:
688
- # lon, lat = float(coords[0]), float(coords[1])
689
- # points.append({'name': name, 'geometry': f"POINT ({lon} {lat})"})
690
- # if not points:
691
- # st.error("No valid Point data found in the KML file.")
692
- # else:
693
- # locations_df = gpd.GeoDataFrame(points, geometry=gpd.GeoSeries.from_wkt([p['geometry'] for p in points]), crs="EPSG:4326")
694
- # locations_df['latitude'] = locations_df['geometry'].y
695
- # locations_df['longitude'] = locations_df['geometry'].x
696
- # original_lat_col = 'latitude'
697
- # original_lon_col = 'longitude'
698
- # except Exception as e:
699
- # st.error(f"Error parsing KML file: {str(e)}")
700
- # if not locations_df.empty and 'latitude' in locations_df.columns and 'longitude' in locations_df.columns:
701
- # m = leafmap.Map(center=[locations_df['latitude'].mean(), locations_df['longitude'].mean()], zoom=10)
702
- # for _, row in locations_df.iterrows():
703
- # latitude = row['latitude']
704
- # longitude = row['longitude']
705
- # if pd.isna(latitude) or pd.isna(longitude):
706
- # continue
707
- # m.add_marker(location=[latitude, longitude], popup=row.get('name', 'No Name'))
708
- # st.write("Map of Uploaded Points:")
709
- # m.to_streamlit()
710
- # elif shape_type.lower() == "polygon":
711
- # if file_upload.name.endswith('.csv'):
712
- # st.error("CSV upload not supported for polygons. Please upload a GeoJSON or KML file.")
713
- # elif file_upload.name.endswith('.geojson'):
714
- # locations_df = gpd.read_file(file_upload)
715
- # if 'geometry' not in locations_df.columns:
716
- # st.error("GeoJSON file doesn't contain geometry column")
717
- # st.stop()
718
- # elif file_upload.name.endswith('.kml'):
719
- # kml_string = file_upload.read().decode('utf-8')
720
- # try:
721
- # root = XET.fromstring(kml_string)
722
- # ns = {'kml': 'http://www.opengis.net/kml/2.2'}
723
- # polygons = []
724
- # for placemark in root.findall('.//kml:Placemark', ns):
725
- # name = placemark.findtext('kml:name', default=f"Polygon_{len(polygons)}", namespaces=ns)
726
- # coords_elem = placemark.find('.//kml:Polygon//kml:coordinates', ns)
727
- # if coords_elem is not None:
728
- # coords_text = ' '.join(coords_elem.text.split())
729
- # coord_pairs = [pair.split(',')[:2] for pair in coords_text.split() if pair]
730
- # if len(coord_pairs) >= 4:
731
- # coords_str = " ".join([f"{float(lon)} {float(lat)}" for lon, lat in coord_pairs])
732
- # polygons.append({'name': name, 'geometry': f"POLYGON (({coords_str}))"})
733
- # if not polygons:
734
- # st.error("No valid Polygon data found in the KML file.")
735
- # else:
736
- # locations_df = gpd.GeoDataFrame(polygons, geometry=gpd.GeoSeries.from_wkt([p['geometry'] for p in polygons]), crs="EPSG:4326")
737
- # except Exception as e:
738
- # st.error(f"Error parsing KML file: {str(e)}")
739
- # if not locations_df.empty and 'geometry' in locations_df.columns:
740
- # centroid_lat = locations_df.geometry.centroid.y.mean()
741
- # centroid_lon = locations_df.geometry.centroid.x.mean()
742
- # m = leafmap.Map(center=[centroid_lat, centroid_lon], zoom=10)
743
- # for _, row in locations_df.iterrows():
744
- # polygon = row['geometry']
745
- # if polygon.is_valid:
746
- # gdf = gpd.GeoDataFrame([row], geometry=[polygon], crs=locations_df.crs)
747
- # m.add_gdf(gdf=gdf, layer_name=row.get('name', 'Unnamed Polygon'))
748
- # st.write("Map of Uploaded Polygons:")
749
- # m.to_streamlit()
750
-
751
- # if st.button(f"Calculate {custom_formula}"):
752
- # if not locations_df.empty:
753
- # with st.spinner("Processing Data..."):
754
- # try:
755
- # # Call the aggregation function with updated parameters
756
- # results, processing_time = process_aggregation(
757
- # locations_df,
758
- # start_date_str,
759
- # end_date_str,
760
- # dataset_id,
761
- # selected_bands,
762
- # reducer_choice,
763
- # shape_type,
764
- # aggregation_period,
765
- # original_lat_col,
766
- # original_lon_col,
767
- # custom_formula=custom_formula,
768
- # kernel_size=kernel_size,
769
- # include_boundary=include_boundary,
770
- # tile_cloud_threshold=tile_cloud_threshold if "tile_cloud_threshold" in locals() else 0,
771
- # pixel_cloud_threshold=pixel_cloud_threshold if "pixel_cloud_threshold" in locals() else 0,
772
- # user_scale=user_scale
773
- # )
774
-
775
- # # Process and display results
776
- # if results:
777
- # result_df = pd.DataFrame(results)
778
- # st.write(f"Processed Results Table ({aggregation_period}) for Formula: {custom_formula}")
779
- # st.dataframe(result_df)
780
-
781
- # # Download button for CSV
782
- # filename = f"{main_selection}_{dataset_id}_{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}_{aggregation_period.lower()}.csv"
783
- # st.download_button(
784
- # label="Download results as CSV",
785
- # data=result_df.to_csv(index=False).encode('utf-8'),
786
- # file_name=filename,
787
- # mime='text/csv'
788
- # )
789
-
790
- # # Success message
791
- # st.success(f"Processing complete! Total processing time: {processing_time:.2f} seconds.")
792
-
793
- # # Graph Visualization Section
794
- # st.markdown("<h5>Graph Visualization</h5>", unsafe_allow_html=True)
795
-
796
- # # Dynamically identify the time column
797
- # if aggregation_period.lower() == 'custom (start date to end date)':
798
- # x_column = 'Date Range'
799
- # elif 'Date' in result_df.columns:
800
- # x_column = 'Date'
801
- # elif 'Week' in result_df.columns:
802
- # x_column = 'Week'
803
- # elif 'Month' in result_df.columns:
804
- # x_column = 'Month'
805
- # elif 'Year' in result_df.columns:
806
- # x_column = 'Year'
807
- # else:
808
- # st.warning("No valid time column found for plotting.")
809
- # st.stop()
810
-
811
- # # Dynamically identify the value column
812
- # y_column = None
813
- # if 'Calculated Value' in result_df.columns:
814
- # y_column = 'Calculated Value'
815
- # elif 'Aggregated Value' in result_df.columns:
816
- # y_column = 'Aggregated Value'
817
- # else:
818
- # st.warning("No value column found for plotting. Available columns: " + ", ".join(result_df.columns))
819
- # st.stop()
820
-
821
- # # Ensure we have valid data to plot
822
- # if result_df.empty:
823
- # st.warning("No data available for plotting.")
824
- # st.stop()
825
-
826
- # # # Line Chart
827
- # # try:
828
- # # st.subheader("Line Chart")
829
- # # if x_column == 'Location Name':
830
- # # st.line_chart(result_df.set_index(x_column)[y_column])
831
- # # else:
832
- # # # Convert to datetime for better sorting
833
- # # result_df[x_column] = pd.to_datetime(result_df[x_column], errors='ignore')
834
- # # result_df = result_df.sort_values(x_column)
835
- # # st.line_chart(result_df.set_index(x_column)[y_column])
836
- # # except Exception as e:
837
- # # st.error(f"Error creating line chart: {str(e)}")
838
-
839
- # # # Bar Chart
840
- # # try:
841
- # # st.subheader("Bar Chart")
842
- # # if x_column == 'Location Name':
843
- # # st.bar_chart(result_df.set_index(x_column)[y_column])
844
- # # else:
845
- # # result_df[x_column] = pd.to_datetime(result_df[x_column], errors='ignore')
846
- # # result_df = result_df.sort_values(x_column)
847
- # # st.bar_chart(result_df.set_index(x_column)[y_column])
848
- # # except Exception as e:
849
- # # st.error(f"Error creating bar chart: {str(e)}")
850
-
851
- # # Advanced Plot (Plotly)
852
- # try:
853
- # st.subheader("Advanced Interactive Plot (Plotly)")
854
- # if x_column == 'Location Name':
855
- # fig = px.bar(
856
- # result_df,
857
- # x=x_column,
858
- # y=y_column,
859
- # color='Location Name',
860
- # title=f"{custom_formula} by Location"
861
- # )
862
- # else:
863
- # fig = px.line(
864
- # result_df,
865
- # x=x_column,
866
- # y=y_column,
867
- # color='Location Name',
868
- # title=f"{custom_formula} Over Time"
869
- # )
870
- # st.plotly_chart(fig)
871
- # except Exception as e:
872
- # st.error(f"Error creating interactive plot: {str(e)}")
873
-
874
- # else:
875
- # st.warning("No results were generated. Check your inputs or formula.")
876
- # st.info(f"Total processing time: {processing_time:.2f} seconds.")
877
-
878
- # except Exception as e:
879
- # st.error(f"An error occurred during processing: {str(e)}")
880
- # else:
881
- # st.warning("Please upload a valid file to proceed.")
882
- # # if st.button(f"Calculate {custom_formula}"):
883
- # # if not locations_df.empty:
884
- # # with st.spinner("Processing Data..."):
885
- # # try:
886
- # # results, processing_time = process_aggregation(
887
- # # locations_df,
888
- # # start_date_str,
889
- # # end_date_str,
890
- # # dataset_id,
891
- # # selected_bands,
892
- # # reducer_choice,
893
- # # shape_type,
894
- # # aggregation_period,
895
- # # original_lat_col,
896
- # # original_lon_col,
897
- # # custom_formula,
898
- # # kernel_size,
899
- # # include_boundary,
900
- # # tile_cloud_threshold=tile_cloud_threshold if "tile_cloud_threshold" in locals() else 0,
901
- # # pixel_cloud_threshold=pixel_cloud_threshold if "pixel_cloud_threshold" in locals() else 0,
902
- # # user_scale=user_scale
903
- # # )
904
- # # if results:
905
- # # result_df = pd.DataFrame(results)
906
- # # st.write(f"Processed Results Table ({aggregation_period}) for Formula: {custom_formula}")
907
- # # st.dataframe(result_df)
908
- # # filename = f"{main_selection}_{dataset_id}_{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}_{aggregation_period.lower()}.csv"
909
- # # st.download_button(
910
- # # label="Download results as CSV",
911
- # # data=result_df.to_csv(index=False).encode('utf-8'),
912
- # # file_name=filename,
913
- # # mime='text/csv'
914
- # # )
915
- # # st.success(f"Processing complete! Total processing time: {processing_time:.2f} seconds.")
916
- # # st.markdown("<h5>Graph Visualization</h5>", unsafe_allow_html=True)
917
- # # if aggregation_period.lower() == 'custom (start date to end date)':
918
- # # x_column = 'Date Range'
919
- # # elif 'Date' in result_df.columns:
920
- # # x_column = 'Date'
921
- # # elif 'Week' in result_df.columns:
922
- # # x_column = 'Week'
923
- # # elif 'Month' in result_df.columns:
924
- # # x_column = 'Month'
925
- # # elif 'Year' in result_df.columns:
926
- # # x_column = 'Year'
927
- # # else:
928
- # # st.warning("No valid time column found for plotting.")
929
- # # st.stop()
930
- # # y_column = 'Calculated Value'
931
- # # fig = px.line(
932
- # # result_df,
933
- # # x=x_column,
934
- # # y=y_column,
935
- # # color='Location Name',
936
- # # title=f"{custom_formula} Over Time"
937
- # # )
938
- # # st.plotly_chart(fig)
939
- # # else:
940
- # # st.warning("No results were generated. Check your inputs or formula.")
941
- # # st.info(f"Total processing time: {processing_time:.2f} seconds.")
942
- # # except Exception as e:
943
- # # st.error(f"An error occurred during processing: {str(e)}")
944
- # # else:
945
- # # st.warning("Please upload a valid file to proceed.")
946
 
947
  import streamlit as st
948
  import json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
  import streamlit as st
3
  import json