| import os
|
| import sys
|
| import time
|
| import io
|
| import json
|
| from pathlib import Path
|
| from utils import print_with_line_number
|
|
|
|
|
| parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| sys.path.append(parent_dir)
|
|
|
| import ee
|
|
|
|
|
| credential_path = 'kilunga app/Sentinel2App/assets'
|
|
|
| with open(credential_path) as f:
|
| service_account_info = json.load(f)
|
|
|
|
|
| credentials = ee.ServiceAccountCredentials(os.environ.get("SERVICE_EMAIL"), key_data=os.environ.get("SERVICE_JSON"))
|
|
|
| import math
|
| from typing import Optional
|
| from shiny import App, render, ui, reactive, Inputs, Outputs, Session, req
|
| import ipyleaflet as L
|
| from ipywidgets import Layout
|
| from htmltools import css
|
| import numpy as np
|
| import pandas as pd
|
|
|
| from shinywidgets import output_widget, reactive_read, register_widget
|
| from geopy.geocoders import Nominatim
|
| import json
|
| import requests
|
| import traceback
|
| from datetime import datetime, date
|
| from typing import List
|
| from timezonefinder import TimezoneFinder
|
|
|
|
|
| import tensorflow
|
| import joblib
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| tf = TimezoneFinder()
|
|
|
|
|
|
|
| GEEurl = 'https://earthengine.googleapis.com/v1alpha/projects/earthengine-legacy/maps/{mapid}/tiles/{z}/{x}/{y}?token={token}'
|
| GEEmap_id = 'projects/ee-joaonvula501/maps/c483c79bcb04b7f2e2b15d6be8fa4b29-3e09ed6b5f9bd283fb1caeba520f6d3e'
|
| GEEtoken = ''
|
|
|
|
|
| def custom_loss(lam, cov_real_data):
|
| def loss(y_true, y_pred):
|
| mse_loss = tensorflow.reduce_mean(tensorflow.square(y_true - y_pred))
|
| cov_pred = tensorflow.linalg.matmul(tensorflow.transpose(y_pred - tensorflow.reduce_mean(y_pred, axis=0)),
|
| (y_pred - tensorflow.reduce_mean(y_pred, axis=0))) / tensorflow.cast(tensorflow.shape(y_pred)[0], tensorflow.float32)
|
| cov_penalty = tensorflow.reduce_sum(tensorflow.square(cov_pred - cov_real_data))
|
| return mse_loss + lam * cov_penalty
|
| return loss
|
|
|
| def load_model_and_preprocessors(model_path, cov_real_data_path, scaler_X_path, scaler_y_path):
|
|
|
| cov_real_data = np.load(cov_real_data_path)
|
|
|
| model = tensorflow.keras.models.load_model(model_path, custom_objects={'loss': custom_loss(1e-6, cov_real_data)})
|
|
|
| scaler_X = joblib.load(scaler_X_path)
|
|
|
| scaler_Y = joblib.load(scaler_y_path)
|
| return model, scaler_X, scaler_Y
|
|
|
|
|
| model, loaded_scaler_X, loaded_scaler_Y = load_model_and_preprocessors(
|
| "ANN_assests/model",
|
| "ANN_assests/cov_real_data.npy",
|
| "ANN_assests/scaler_X.pkl",
|
| "ANN_assests/scaler_y.pkl"
|
| )
|
|
|
|
|
| labels = ['Longitude', 'Latitude', 'B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B9', 'B11', 'B12', 'N', 'Cab', 'Ccx', 'Cw', 'Cm']
|
|
|
| X_labels = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B9', 'B11', 'B12']
|
|
|
| output_labels = ["N", "Cab", "Ccx", "Cw", "Cm"]
|
|
|
| layer_names = ["structure parameter", "Chlorophylla+b content (µg/cm2)", "Carotenoids content (µg/cm2)", "Equivalent Water content (cm)", "Leaf Mass per Area (g/cm2)"]
|
|
|
| data_to_map = {
|
| "structure parameter": "N",
|
| "Chlorophylla+b content (µg/cm2)": "Cab",
|
| "Carotenoids content (µg/cm2)": "Ccx",
|
| "Equivalent Water content (cm)": "Cw",
|
| "Leaf Mass per Area (g/cm2)": "Cm"
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| gradient_parula = {
|
| 0.0: 'rgba(128, 0, 128, 1.0)',
|
| 0.2: 'rgba(0, 0, 255, 1.0)',
|
| 0.4: 'rgba(0, 255, 255, 1.0)',
|
| 0.5: 'rgba(0, 250, 154, 1.0)',
|
| 0.6: 'rgba(50, 205, 50, 1.0)',
|
| 0.7: 'rgba(173, 255, 47, 1.0)',
|
| 0.8: 'rgba(255, 255, 0, 1.0)',
|
| 0.9: 'rgba(255, 165, 0, 1.0)',
|
| 1.0: 'rgba(255, 0, 0, 1.0)'
|
| }
|
|
|
| gradient_settings = {
|
| "structure parameter": gradient_parula,
|
| "Chlorophylla+b content (µg/cm2)": gradient_parula,
|
| "Carotenoids content (µg/cm2)": gradient_parula,
|
| "Equivalent Water content (cm)": gradient_parula,
|
| "Leaf Mass per Area (g/cm2)": gradient_parula
|
| }
|
|
|
| print_with_line_number("Finish loading the ANN model!")
|
|
|
| def runModel(input_data, scaler_X, scaler_Y, ANNmodel):
|
|
|
|
|
| input_data_scaled = scaler_X.transform(input_data)
|
|
|
|
|
|
|
| output_data_scaled = ANNmodel.predict(input_data_scaled)
|
|
|
|
|
|
|
| output_data = scaler_Y.inverse_transform(output_data_scaled)
|
|
|
|
|
|
|
|
|
| datasets = {}
|
| for i, label in enumerate(output_labels):
|
| datasets[label] = output_data[:, i]
|
|
|
|
|
| for label, data in datasets.items():
|
| print(label, data)
|
|
|
| return datasets
|
|
|
| gpsurl = 'https://www.googleapis.com/geolocation/v1/geolocate?key=' + os.environ.get("GMAP_TOKEN")
|
|
|
| def getGPS():
|
| GPSurl = gpsurl
|
| data = {'homeMobileCountryCode': 310, 'homeMobileNetworkCode': 410, 'considerIp': 'True'}
|
| response = requests.post(GPSurl, data=json.dumps(data))
|
| result = json.loads(response.content)
|
| return result
|
|
|
| def get_location(lat, lon):
|
| geolocator = Nominatim(timeout=120, user_agent="when-to-fly")
|
| location = geolocator.reverse(f"{lat},{lon}")
|
| return location.address
|
|
|
| app_ui = ui.page_fluid(
|
| ui.div(
|
| ui.input_action_button("analyze", "Analyze", class_="btn-success"),
|
| ui.download_button("download_polygon", "Download spectral data as tif", class_="btn-success"),
|
| ui.download_button("download_output", "Download spectral and output data as csv", class_="btn-success"),
|
| ui.download_button("download_output", "Download spectral and output data report", class_="btn-success"),
|
| style=css(display="flex", justify_content="center", align_items="center", gap="2rem"),
|
| ),
|
| ui.strong("Must analyze (to renew the image information) before downloading any file.", style="color: green;"),
|
| ui.layout_sidebar(
|
| ui.panel_sidebar(
|
| ui.div(
|
| ui.div(
|
| ui.input_date("date", "Date:"),
|
| ui.input_slider("zoom", "Map zoom level", value=12, min=1, max=18),
|
| ),
|
| ui.div(
|
| ui.input_numeric("lat", "Latitude", value=38.53667742),
|
| ui.input_numeric("long", "Longitude", value=-121.75387309),
|
| ),
|
| style=css(display="flex", justify_content="center", align_items="center", gap="1rem"),
|
| ),
|
| ),
|
| ui.panel_main(
|
| ui.div(
|
| ui.output_text("N_range"),
|
| ui.output_text("Cab_range"),
|
| ui.output_text("Ccx_range"),
|
| ui.output_text("Cw_range"),
|
| ui.output_text("Cm_range"),
|
| style=css(display="flex", justify_content="center", align_items="center", gap="2rem"),
|
| ),
|
| ui.img(src="legend.png"),
|
| ),
|
| ),
|
| output_widget("map"),
|
|
|
|
|
|
|
| )
|
|
|
|
|
|
|
| def server(input, output, session):
|
|
|
| ee.Initialize(credentials)
|
|
|
| global address_line, polygoned_image, output_df
|
| address_line = None
|
| polygoned_image = None
|
| polygon_data = reactive.Value([])
|
| output_df = pd.DataFrame()
|
| N = reactive.Value("structure parameter")
|
| Cab = reactive.Value("Chlorophylla+b content (µg/cm2)")
|
| Ccx = reactive.Value("Carotenoids content (µg/cm2)")
|
| Cw = reactive.Value("Equivalent Water content (cm)")
|
| Cm = reactive.Value("Leaf Mass per Area (g/cm2)")
|
| m = ui.modal(
|
| "Please wait for progress...",
|
| easy_close=False,
|
| size="s",
|
| footer=None,
|
| fade=True
|
| )
|
|
|
| @output
|
| @render.text
|
| def N_range():
|
| return N.get()
|
|
|
| @output
|
| @render.text
|
| def Cab_range():
|
| return Cab.get()
|
|
|
| @output
|
| @render.text
|
| def Ccx_range():
|
| return Ccx.get()
|
|
|
| @output
|
| @render.text
|
| def Cw_range():
|
| return Cw.get()
|
|
|
| @output
|
| @render.text
|
| def Cm_range():
|
| return Cm.get()
|
|
|
| def handle_draw(self, action, geo_json):
|
| print("运行handle_draw")
|
| if geo_json['type'] == 'Feature':
|
|
|
| if geo_json['geometry']['type'] == 'Polygon':
|
|
|
| coordinates = geo_json['geometry']['coordinates'][0]
|
|
|
|
|
|
|
|
|
| polygon_data.set([(lon, lat) for lon, lat in coordinates])
|
|
|
|
|
|
|
| print("Polygon Vertex Coordinates:")
|
| for lon, lat in polygon_data.get():
|
| print(f"Latitude: {lat}, Longitude: {lon}")
|
|
|
| ui.modal_show(m)
|
|
|
|
|
| asset_roots = ee.data.getAssetRoots()
|
| if asset_roots:
|
| print("API is connected and working.")
|
| else:
|
| print("API is not connected or not working.")
|
|
|
| try:
|
|
|
| current_gps = getGPS()
|
| print_with_line_number(current_gps)
|
| current_location = get_location(current_gps['location']['lat'], current_gps['location']['lng'])
|
| print_with_line_number(current_location)
|
| ui.update_text(id="address",
|
| label="Data for",
|
| value=current_location)
|
|
|
|
|
| map = L.Map(center=(current_gps['location']['lat'], current_gps['location']['lng']), zoom=12, scroll_wheel_zoom=True)
|
| map.layout = Layout(height='600px')
|
|
|
| @reactive.isolate()
|
| def update_text_inputs(lat: Optional[float], long: Optional[float]) -> None:
|
| req(lat is not None, long is not None)
|
| lat = round(lat, 8)
|
| long = round(long, 8)
|
| if lat != input.lat():
|
| input.lat.freeze()
|
| ui.update_text("lat", value=lat)
|
| if long != input.long():
|
| input.long.freeze()
|
| ui.update_text("long", value=long)
|
|
|
| map.add_layer(L.TileLayer(url='https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', name='Natural Map'))
|
|
|
|
|
| map.add_control(L.leaflet.ScaleControl(position="bottomleft"))
|
| layer_control = L.LayersControl(position='topright')
|
| map.add_control(layer_control)
|
|
|
|
|
| draw_control = L.DrawControl(
|
| polygon = {
|
| "shapeOptions": {
|
| "fillColor": "transparent",
|
| "fillOpacity": 0.0
|
| }
|
| }
|
| )
|
| map.add_control(draw_control)
|
|
|
| draw_control.on_draw(handle_draw)
|
| register_widget("map", map)
|
|
|
| ui.modal_remove()
|
|
|
| except Exception as e:
|
| ui.modal_remove()
|
| error_modal = ui.modal(
|
| str(e),
|
| title="An Error occured. Please refresh",
|
| easy_close=True,
|
| size="xl",
|
| footer=None,
|
| fade=True
|
| )
|
|
|
| ui.modal_show(error_modal)
|
| traceback.print_exc()
|
|
|
|
|
| @reactive.Effect
|
| def _():
|
| map.zoom = input.zoom()
|
|
|
|
|
| @reactive.Effect
|
| def _():
|
| ui.update_slider("zoom", value=reactive_read(map, "zoom"))
|
|
|
| @reactive.Effect
|
| def location():
|
| """Returns tuple of (lat,long) floats--or throws silent error if no lat/long is
|
| selected"""
|
|
|
| req(input.lat() is not None, input.long() is not None)
|
|
|
| try:
|
| long = input.long()
|
|
|
| long = (long + 180) % 360 - 180
|
| if round(map.center[0], 8) == input.lat() and round(map.center[1], 8) == long:
|
| return
|
| map.center = (input.lat(), long)
|
|
|
| except ValueError as e:
|
| error_modal = ui.modal(
|
| str(e),
|
| title="Invalid latitude/longitude specification. Please refresh",
|
| easy_close=True,
|
| size="xl",
|
| footer=None,
|
| fade=True
|
| )
|
|
|
| ui.modal_show(error_modal)
|
| traceback.print_exc()
|
|
|
|
|
|
|
| @reactive.Effect
|
| def map_bounds():
|
| center = reactive_read(map, "center")
|
| if len(center) == 0:
|
| return
|
| lon = (center[1] + 180) % 360 - 180
|
| update_text_inputs(center[0], lon)
|
|
|
| def update_or_create_heatmaps(output_datasets, scale):
|
| """
|
| Check if a heatmap layer exists for each dataset in output_datasets.
|
| If it exists, update the heatmap, otherwise create a new heatmap.
|
|
|
| Parameters:
|
| output_datasets (list of dict): The datasets for creating/updating heatmaps
|
| """
|
|
|
| existing_layers = {layer.name: layer for layer in map.layers}
|
| print_with_line_number(existing_layers)
|
|
|
| for layer_name in layer_names:
|
|
|
| if layer_name in existing_layers:
|
| print("deleting ", layer_name)
|
| map.remove_layer(existing_layers[layer_name])
|
|
|
| heatmap_data = []
|
| data_values = output_datasets[data_to_map[layer_name]]
|
|
|
| q03 = np.percentile(data_values, 3)
|
| q97 = np.percentile(data_values, 97)
|
| min_value = min(data_values)
|
| if min_value < 0:
|
| min_value = 0
|
| max_value = max(data_values)
|
|
|
| if (data_to_map[layer_name] == "N"):
|
| N.set(layer_name + ": " + str(min_value) + " ~ " + str(max_value))
|
| elif (data_to_map[layer_name] == "Cab"):
|
| Cab.set(layer_name + ": " + str(min_value) + " ~ " + str(max_value))
|
| elif (data_to_map[layer_name] == "Ccx"):
|
| Ccx.set(layer_name + ": " + str(min_value) + " ~ " + str(max_value))
|
| elif (data_to_map[layer_name] == "Cw"):
|
| Cw.set(layer_name + ": " + str(min_value) + " ~ " + str(max_value))
|
| else:
|
| Cm.set(layer_name + ": " + str(min_value) + " ~ " + str(max_value))
|
|
|
| for coord, n in zip(output_datasets["Coordinates"], data_values):
|
|
|
| if n <= q03:
|
| normalized_value = 0
|
| elif n >= q97:
|
| normalized_value = 1
|
| else:
|
| normalized_value = (n - q03) / (q97 - q03)
|
| heatmap_data.append([coord[1], coord[0], normalized_value])
|
|
|
|
|
| heatmap = L.Heatmap(
|
| locations=heatmap_data,
|
| radius=scale * 1.4,
|
| gradient=gradient_settings[layer_name],
|
| max=1,
|
| blur=scale / 2,
|
| name=layer_name
|
| )
|
|
|
|
|
| map.add_layer(heatmap)
|
|
|
| @reactive.Effect
|
| @reactive.event(input.analyze, ignore_none=True, ignore_init=True)
|
| def _():
|
| global output_df
|
| if not polygon_data.get():
|
| return
|
| ui.modal_show(m)
|
| global polygoned_image
|
| polygon = ee.Geometry.Polygon(polygon_data.get())
|
| print("Polygon Data: " , polygon_data.get())
|
| print("Polygon: " , polygon)
|
|
|
|
|
|
|
| current_date = input.date()
|
| today = ee.Date(input.date().strftime('%Y-%m-%d'))
|
| start_date = today.advance(-15, 'day')
|
|
|
| print("Start Date: ", start_date.format('YYYY-MM-dd').getInfo(), "| End Date: ", today.format('YYYY-MM-dd').getInfo())
|
|
|
| sentinel2 = ee.ImageCollection("COPERNICUS/S2_SR")\
|
| .filterDate(start_date, today)\
|
| .filterBounds(polygon)\
|
| .sort('CLOUDY_PIXEL_PERCENTAGE', True)
|
|
|
|
|
| polygoned_image = sentinel2.first()
|
|
|
|
|
| if not polygoned_image:
|
| print("No image available for download.")
|
| return
|
|
|
| retry = 5
|
| while(sentinel2.size().getInfo() == 0):
|
| if(retry == 0):
|
| ui.update_date("date", label="Date:", value=current_date)
|
| ui.modal_remove()
|
| error_modal = ui.modal(
|
| "Please do not choose a date that is too far in the future. This application will search for remote sensing data within the two weeks prior to the selected date.",
|
| title="Something wrong happened, try \"Analyze\" again ",
|
| easy_close=True,
|
| size="xl",
|
| footer=None,
|
| fade=True
|
| )
|
| ui.modal_show(error_modal)
|
| print("fail to fecth image.")
|
| return
|
| print("wait for fetching.")
|
| time.sleep(2)
|
| retry -= 1
|
|
|
|
|
| print_with_line_number("Type of sentinel2: " + str(type(sentinel2)))
|
| print("Counts of Fetched image: ", sentinel2.size().getInfo())
|
|
|
|
|
| clipped_image = polygoned_image.clip(polygon)
|
|
|
|
|
|
|
|
|
|
|
|
|
| scale=1
|
| polygon_area = polygon.area().getInfo()
|
| num = math.ceil(polygon_area / scale / scale)
|
| if (num > 4999):
|
| per_area = math.ceil(polygon_area / 4998)
|
| scale = math.ceil(math.pow(per_area, 1.0/2))
|
|
|
| print("polygon_area(m2): ", polygon_area, "scale: ", scale)
|
|
|
|
|
| spectral_values = clipped_image.select('B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B9', 'B11', 'B12').sample(
|
| region=polygon,
|
| scale=scale,
|
| numPixels=4999,
|
| geometries=True
|
| )
|
|
|
| print_with_line_number("Pre-process the bands data.")
|
|
|
| spectral_values = spectral_values.getInfo()
|
|
|
| spectral_values_json = json.dumps(spectral_values)
|
|
|
| spectral_values_dict = json.loads(spectral_values_json)
|
| features = spectral_values_dict['features']
|
|
|
| print_with_line_number("Extract the center coordinates and values of B1-B12 for each pixel block")
|
| coords = []
|
| input_data = []
|
| for feature in features:
|
| coords.append(feature['geometry']['coordinates'])
|
| props = feature['properties']
|
| input_data.append([props[b] for b in X_labels])
|
|
|
|
|
| coords = np.array(coords)
|
|
|
| input_data = np.array(input_data)
|
|
|
|
|
| output_datasets = runModel(input_data, loaded_scaler_X, loaded_scaler_Y, model)
|
|
|
|
|
| data_combined = np.column_stack((coords, input_data, output_datasets['N'], output_datasets['Cab'], output_datasets['Ccx'], output_datasets['Cw'], output_datasets['Cm']))
|
|
|
| output_df = pd.DataFrame(data_combined, columns=labels)
|
|
|
| print_with_line_number("Add a dataset for the coordinates")
|
| output_datasets['Coordinates'] = coords
|
|
|
| update_or_create_heatmaps(output_datasets, scale)
|
| register_widget("map", map)
|
| ui.modal_remove()
|
|
|
| @reactive.Effect
|
| def _():
|
| print("Current navbar page: ", input.navbar_id())
|
|
|
| @session.download(
|
| filename=lambda: f"image-{input.date().isoformat()}-{np.random.randint(100, 999)}.tif"
|
| )
|
| async def download_polygon():
|
|
|
|
|
|
|
|
|
| if not polygoned_image:
|
| print("No image available for download.")
|
| return
|
|
|
|
|
| clipped_image = polygoned_image.clip(ee.Geometry.Polygon(polygon_data.get()))
|
| print("clipped_image: ", clipped_image)
|
|
|
|
|
| download_params = {
|
| 'scale': 10,
|
| 'region': polygon_data.get(),
|
| 'format': 'GeoTIFF',
|
| }
|
|
|
|
|
| download_url = clipped_image.getDownloadURL(download_params)
|
|
|
|
|
| response = requests.get(download_url)
|
|
|
|
|
| with io.BytesIO() as buf:
|
|
|
| buf.write(response.content)
|
| buf.seek(0)
|
|
|
|
|
| yield buf.getvalue()
|
|
|
| print("Image downloaded successfully!")
|
|
|
| @session.download(
|
| filename=lambda: f"data-{input.date().isoformat()}-{np.random.randint(100, 999)}.csv"
|
| )
|
| def download_output():
|
| global output_df
|
|
|
| if output_df.empty:
|
| print("No data available for download.")
|
| return
|
|
|
|
|
| csv_data = output_df.to_csv(index=False).encode()
|
|
|
|
|
| with io.BytesIO() as buf:
|
| buf.write(csv_data)
|
|
|
| buf.seek(0)
|
|
|
| yield buf.getvalue()
|
|
|
| print("Data downloaded successfully!")
|
|
|
| static_dir = Path(__file__).parent / "assets"
|
| app = App(app_ui, server, static_assets=static_dir) |