mirror of
https://git.victorphan.net/basketballcantho/CSIROBoeingPhase5-Vietnam.git
synced 2026-08-05 05:43:10 +07:00
first commit
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
"""
|
||||
Digital Earth Africa Coastline widget, which can be used to
|
||||
interactively extract shoreline data using transects.
|
||||
"""
|
||||
|
||||
# Import required packages
|
||||
|
||||
# Force GeoPandas to use Shapely instead of PyGEOS
|
||||
# In a future release, GeoPandas will switch to using Shapely by default.
|
||||
import os
|
||||
os.environ['USE_PYGEOS'] = '0'
|
||||
|
||||
import fiona
|
||||
import sys
|
||||
import datacube
|
||||
import warnings
|
||||
import matplotlib.pyplot as plt
|
||||
from datacube.utils.geometry import CRS
|
||||
from ipyleaflet import (
|
||||
WMSLayer,
|
||||
basemaps,
|
||||
basemap_to_tiles,
|
||||
Map,
|
||||
DrawControl,
|
||||
WidgetControl,
|
||||
LayerGroup,
|
||||
LayersControl,
|
||||
GeoData,
|
||||
)
|
||||
from traitlets import Unicode
|
||||
from ipywidgets import (
|
||||
GridspecLayout,
|
||||
Button,
|
||||
Layout,
|
||||
HBox,
|
||||
VBox,
|
||||
HTML,
|
||||
Output,
|
||||
)
|
||||
import json
|
||||
import geopandas as gpd
|
||||
from io import BytesIO
|
||||
import ipywidgets as widgets
|
||||
|
||||
import deafrica_tools.app.widgetconstructors as deawidgets
|
||||
from deafrica_tools.coastal import get_coastlines, transect_distances
|
||||
from owslib.wms import WebMapService
|
||||
|
||||
def make_box_layout():
|
||||
return Layout(
|
||||
# border='solid 1px black',
|
||||
margin='0px 10px 10px 0px',
|
||||
padding='5px 5px 5px 5px',
|
||||
width='100%',
|
||||
height='100%',
|
||||
)
|
||||
|
||||
|
||||
def create_expanded_button(description, button_style):
|
||||
return Button(
|
||||
description=description,
|
||||
button_style=button_style,
|
||||
layout=Layout(width="auto", height="auto"),
|
||||
)
|
||||
|
||||
|
||||
class transect_app(HBox):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
######################
|
||||
# INITIAL ATTRIBUTES #
|
||||
######################
|
||||
|
||||
self.output_name = "example_output"
|
||||
self.export_csv = False
|
||||
self.export_plot = False
|
||||
self.product_list = [
|
||||
("ESRI World Imagery", "none"),
|
||||
("Open Street Map", "open_street_map"),
|
||||
]
|
||||
self.product = self.product_list[0][1]
|
||||
self.mode_list = [('Distance', 'distance'), ('Width', 'width')]
|
||||
self.mode = self.mode_list[0][1]
|
||||
self.target = None
|
||||
self.action = None
|
||||
self.gdf_drawn = None
|
||||
self.gdf_uploaded = None
|
||||
|
||||
##################
|
||||
# HEADER FOR APP #
|
||||
##################
|
||||
|
||||
# Create the Header widget
|
||||
header_title_text = "<h3>Digital Earth Africa Coastlines shoreline transect extraction</h3>"
|
||||
instruction_text = "Select parameters and draw a transect on the map to extract shoreline data. <b>In distance mode</b>, draw a transect line starting from land that crosses multiple shorelines. <br><b>In width mode</b>, draw a transect line that intersects shorelines at least twice. Alternatively, <b>upload an vector file</b> to extract shoreline data for multiple existing transects."
|
||||
self.header = deawidgets.create_html(
|
||||
f"{header_title_text}<p>{instruction_text}</p>")
|
||||
self.header.layout = make_box_layout()
|
||||
|
||||
#####################################
|
||||
# HANDLER FUNCTION FOR DRAW CONTROL #
|
||||
#####################################
|
||||
|
||||
# Define the action to take once something is drawn on the map
|
||||
def update_geojson(target, action, geo_json):
|
||||
|
||||
# Remove previously uploaded data if present
|
||||
self.gdf_uploaded = None
|
||||
fileupload_transects._counter = 0
|
||||
|
||||
# Get data from action
|
||||
self.action = action
|
||||
|
||||
# Convert data to geopandas
|
||||
json_data = json.dumps(geo_json)
|
||||
binary_data = json_data.encode()
|
||||
io = BytesIO(binary_data)
|
||||
io.seek(0)
|
||||
gdf = gpd.read_file(io)
|
||||
gdf.crs = "EPSG:4326"
|
||||
|
||||
# Convert to WGS 84 / NSIDC EASE-Grid 2.0 Global and compute area
|
||||
gdf_drawn_nsidc = gdf.copy().to_crs("EPSG:6933")
|
||||
m2_per_km2 = 10**6
|
||||
area = gdf_drawn_nsidc.envelope.area.values[0] / m2_per_km2
|
||||
polyarea_label = 'Total area of DE Africa Coastlines data to extract'
|
||||
polyarea_text = f"<b>{polyarea_label}</b>: {area:.2f} km<sup>2</sup>"
|
||||
|
||||
# Test area size
|
||||
if area <= 50000:
|
||||
confirmation_text = '<span style="color: #33cc33"> <b>(Area to extract falls within recommended limit; click "Extract shoreline data" to continue)</b></span>'
|
||||
self.header.value = header_title_text + polyarea_text + confirmation_text
|
||||
self.gdf_drawn = gdf
|
||||
else:
|
||||
warning_text = '<span style="color: #ff5050"> <b>(Area to extract is too large, please select a smaller transect)</b></span>'
|
||||
self.header.value = header_title_text + polyarea_text + warning_text
|
||||
self.gdf_drawn = None
|
||||
|
||||
###########################
|
||||
# WIDGETS FOR APP OUTPUTS #
|
||||
###########################
|
||||
|
||||
self.status_info = Output(layout=make_box_layout())
|
||||
self.output_plot = Output(layout=make_box_layout())
|
||||
|
||||
#########################################
|
||||
# MAP WIDGET, DRAWING TOOLS, WMS LAYERS #
|
||||
#########################################
|
||||
|
||||
# Create drawing tools
|
||||
desired_drawtools = ['polyline']
|
||||
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
||||
|
||||
# Load DEACoastLines WMS
|
||||
deacl_url = "https://geoserver.digitalearth.africa/geoserver/wms"
|
||||
deacl_layer = "coastlines:DEAfrica_Coastlines"
|
||||
deacoastlines = WMSLayer(
|
||||
url=deacl_url,
|
||||
layers=deacl_layer,
|
||||
format='image/png',
|
||||
transparent=True,
|
||||
attribution='DE Africa Coastlines © 2022 Digital Earth Africa')
|
||||
|
||||
# Begin by displaying an empty layer group, and update the group with desired WMS on interaction.
|
||||
self.map_layers = LayerGroup(layers=(deacoastlines,))
|
||||
self.map_layers.name = 'Map Overlays'
|
||||
|
||||
# Create map widget
|
||||
self.m = deawidgets.create_map(map_center=(0.5273, 25.1367),
|
||||
zoom_level=3,
|
||||
basemap=basemaps.Esri.WorldImagery)
|
||||
self.m.layout = make_box_layout()
|
||||
|
||||
# Add tools to map widget
|
||||
self.m.add_control(draw_control)
|
||||
self.m.add_layer(self.map_layers)
|
||||
|
||||
# Store current basemap for future use
|
||||
self.basemap = self.m.basemap
|
||||
|
||||
############################
|
||||
# WIDGETS FOR APP CONTROLS #
|
||||
############################
|
||||
|
||||
# Create parameter widgets
|
||||
text_output_name = deawidgets.create_inputtext(self.output_name,
|
||||
self.output_name)
|
||||
checkbox_csv = deawidgets.create_checkbox(self.export_csv,
|
||||
'Distance table (.csv)')
|
||||
checkbox_plot = deawidgets.create_checkbox(self.export_plot,
|
||||
'Figure (.png)')
|
||||
deaoverlay_dropdown = deawidgets.create_dropdown(
|
||||
self.product_list, self.product_list[0][1])
|
||||
mode_dropdown = deawidgets.create_dropdown(self.mode_list,
|
||||
self.mode_list[0][1])
|
||||
run_button = create_expanded_button("Extract shoreline data", "info")
|
||||
fileupload_transects = widgets.FileUpload(accept='', multiple=True)
|
||||
|
||||
####################################
|
||||
# UPDATE FUNCTIONS FOR EACH WIDGET #
|
||||
####################################
|
||||
|
||||
# Run update functions whenever various widgets are changed.
|
||||
text_output_name.observe(self.update_text_output_name, "value")
|
||||
checkbox_csv.observe(self.update_checkbox_csv, "value")
|
||||
checkbox_plot.observe(self.update_checkbox_plot, "value")
|
||||
deaoverlay_dropdown.observe(self.update_deaoverlay, "value")
|
||||
mode_dropdown.observe(self.update_mode, "value")
|
||||
run_button.on_click(self.run_app)
|
||||
draw_control.on_draw(update_geojson)
|
||||
fileupload_transects.observe(self.update_fileupload_transects, "value")
|
||||
|
||||
##################################
|
||||
# COLLECTION OF ALL APP CONTROLS #
|
||||
##################################
|
||||
|
||||
parameter_selection = VBox([
|
||||
HTML("<b>Output name:</b>"), text_output_name,
|
||||
HTML(
|
||||
'<b>Transect extraction mode:</b><br><img src="https://i.imgur.com/9fdTH9C.png">'
|
||||
),
|
||||
mode_dropdown,
|
||||
HTML("<b></br>Output files:</b>"),
|
||||
checkbox_plot,
|
||||
checkbox_csv,
|
||||
HTML(
|
||||
"</br><i><b>Advanced</b></br>Upload a GeoJSON or ESRI "
|
||||
"Shapefile (<5 mb) containing one or more transect lines.</i>"),
|
||||
fileupload_transects
|
||||
])
|
||||
map_selection = VBox([
|
||||
HTML("</br><b>Map overlay:</b>"),
|
||||
deaoverlay_dropdown,
|
||||
])
|
||||
parameter_selection.layout = make_box_layout()
|
||||
map_selection.layout = make_box_layout()
|
||||
|
||||
###############################
|
||||
# SPECIFICATION OF APP LAYOUT #
|
||||
###############################
|
||||
|
||||
# 0 1 2 3 4 5 6 7 8 9
|
||||
# ---------------------------------------------
|
||||
# 0 | Header | Map sel. |
|
||||
# ---------------------------------------------
|
||||
# 1 | Params | |
|
||||
# 2 | | |
|
||||
# 3 | | |
|
||||
# 4 | | Map |
|
||||
# 5 | | |
|
||||
# ---------- |
|
||||
# 6 | Run | |
|
||||
# ---------------------------------------------
|
||||
# 7 | Status info |
|
||||
# ---------------------------------------------
|
||||
# 8 | |
|
||||
# 9 | Output/figure |
|
||||
# 10 | |
|
||||
# 11 | ------------------------------------------|
|
||||
|
||||
# Create the layout #[rowspan, colspan]
|
||||
grid = GridspecLayout(12, 10, height="1350px", width="auto")
|
||||
|
||||
# Header and controls
|
||||
grid[0, :8] = self.header
|
||||
grid[0, 8:] = map_selection
|
||||
grid[1:6, 0:2] = parameter_selection
|
||||
grid[6, 0:2] = run_button
|
||||
|
||||
# Status info, map and plot
|
||||
grid[1:7, 2:] = self.m # map
|
||||
grid[7:8, :] = self.status_info
|
||||
grid[8:, :] = self.output_plot
|
||||
|
||||
# Display using HBox children attribute
|
||||
self.children = [grid]
|
||||
|
||||
######################################
|
||||
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
||||
######################################
|
||||
|
||||
# Set the output csv
|
||||
def update_fileupload_transects(self, change):
|
||||
|
||||
# Clear any drawn data if present
|
||||
self.gdf_drawn = None
|
||||
|
||||
# Save to file
|
||||
for uploaded_filename in change.new.keys():
|
||||
with open(uploaded_filename, "wb") as output_file:
|
||||
content = change.new[uploaded_filename]['content']
|
||||
output_file.write(content)
|
||||
|
||||
with self.status_info:
|
||||
|
||||
try:
|
||||
|
||||
print('Loading vector data...', end='\r')
|
||||
valid_files = [
|
||||
file for file in change.new.keys()
|
||||
if file.lower().endswith(('.shp', '.geojson'))
|
||||
]
|
||||
valid_file = valid_files[0]
|
||||
transect_gdf = (gpd.read_file(valid_file).to_crs(
|
||||
"EPSG:4326").explode().reset_index(drop=True))
|
||||
|
||||
# Use ID column if it exists
|
||||
if 'id' in transect_gdf:
|
||||
transect_gdf = transect_gdf.set_index('id')
|
||||
print(f"Uploaded '{valid_file}'; automatically labelling "
|
||||
"transects using column 'id'.")
|
||||
else:
|
||||
print(
|
||||
f"Uploaded '{valid_file}'; no 'id' column detected, "
|
||||
f"labelling transects from 0 to {len(transect_gdf.index) - 1}."
|
||||
)
|
||||
|
||||
# Create a geodata
|
||||
geodata = GeoData(geo_dataframe=transect_gdf,
|
||||
style={
|
||||
'color': 'black',
|
||||
'weight': 3
|
||||
})
|
||||
|
||||
# Add to map
|
||||
xmin, ymin, xmax, ymax = transect_gdf.total_bounds
|
||||
self.m.fit_bounds([[ymin, xmin], [ymax, xmax]])
|
||||
self.m.add_layer(geodata)
|
||||
|
||||
# If completed, add to attribute
|
||||
self.gdf_uploaded = transect_gdf
|
||||
|
||||
except IndexError:
|
||||
print(
|
||||
"Cannot read uploaded files. Please ensure that data is "
|
||||
"in either GeoJSON or ESRI Shapefile format.",
|
||||
end='\r')
|
||||
self.gdf_uploaded = None
|
||||
|
||||
except fiona.errors.DriverError:
|
||||
print(
|
||||
"Shapefile is invalid. Please ensure that all shapefile "
|
||||
"components (e.g. .shp, .shx, .dbf, .prj) are uploaded.",
|
||||
end='\r')
|
||||
self.gdf_uploaded = None
|
||||
|
||||
# Set output name
|
||||
def update_text_output_name(self, change):
|
||||
self.output_name = change.new
|
||||
|
||||
# Output CSV
|
||||
def update_checkbox_csv(self, change):
|
||||
self.export_csv = change.new
|
||||
|
||||
# Output plot
|
||||
def update_checkbox_plot(self, change):
|
||||
self.export_plot = change.new
|
||||
|
||||
# Set mode
|
||||
def update_mode(self, change):
|
||||
self.mode = change.new
|
||||
|
||||
# Update product
|
||||
def update_deaoverlay(self, change):
|
||||
|
||||
self.product = change.new
|
||||
|
||||
# Load DE Africa CoastLines WMS
|
||||
deacl_url = "https://geoserver.digitalearth.africa/geoserver/wms"
|
||||
deacl_layer = "coastlines:DEAfrica_Coastlines"
|
||||
deacoastlines = WMSLayer(
|
||||
url=deacl_url,
|
||||
layers=deacl_layer,
|
||||
format="image/png",
|
||||
transparent=True,
|
||||
attribution="DE Africa Coastlines © 2022 Digital Earth Africa")
|
||||
|
||||
if self.product == "none":
|
||||
self.map_layers.clear_layers()
|
||||
self.map_layers.add_layer(deacoastlines)
|
||||
|
||||
elif self.product == "open_street_map":
|
||||
self.map_layers.clear_layers()
|
||||
layer = basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)
|
||||
self.map_layers.add_layer(layer)
|
||||
self.map_layers.add_layer(deacoastlines)
|
||||
|
||||
def run_app(self, change):
|
||||
|
||||
# Clear progress bar and output areas before running
|
||||
self.status_info.clear_output()
|
||||
self.output_plot.clear_output()
|
||||
|
||||
# Run DE Africa Coastlines analysis
|
||||
with self.status_info:
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
# Load transects from either map or uploaded files
|
||||
if self.gdf_uploaded is not None:
|
||||
transect_gdf = self.gdf_uploaded
|
||||
run_text = 'uploaded file'
|
||||
elif self.gdf_drawn is not None:
|
||||
transect_gdf = self.gdf_drawn
|
||||
transect_gdf.index = [self.output_name]
|
||||
run_text = 'selected transect'
|
||||
else:
|
||||
print(f'No transect drawn or uploaded. Please select a transect on the map, or upload a GeoJSON or ESRI Shapefile.',
|
||||
end='\r')
|
||||
transect_gdf = None
|
||||
|
||||
# If valid data was returned, load DEA Coastlines data
|
||||
if transect_gdf is not None:
|
||||
|
||||
# Load Coastlines data from WFS
|
||||
deacl_gdf = get_coastlines(bbox=transect_gdf)
|
||||
|
||||
# Test that data was correctly returned
|
||||
if len(deacl_gdf.index) > 0:
|
||||
|
||||
# Dissolve by year to remove duplicates, then sort by date
|
||||
deacl_gdf = deacl_gdf.dissolve(by='year', as_index=False)
|
||||
deacl_gdf['year'] = deacl_gdf.year.astype(int)
|
||||
deacl_gdf = deacl_gdf.sort_values('year')
|
||||
deacl_gdf = deacl_gdf.set_index('year')
|
||||
|
||||
else:
|
||||
print(
|
||||
"No annual shoreline data was found near the "
|
||||
"supplied transect. Please draw or select a new "
|
||||
"transect.",
|
||||
end='\r')
|
||||
deacl_gdf = None
|
||||
|
||||
# If valid DEA Coastlines data returned, calculate distances
|
||||
if deacl_gdf is not None:
|
||||
print(f'Analysing transect distances using "{self.mode}" mode...',
|
||||
end='\r')
|
||||
dist_df = transect_distances(
|
||||
transect_gdf.to_crs("EPSG:6933"),
|
||||
deacl_gdf.to_crs("EPSG:6933"),
|
||||
mode=self.mode)
|
||||
|
||||
# If valid data was produced:
|
||||
if dist_df.any(axis=None):
|
||||
|
||||
# Successful output
|
||||
print(f'DE Africa Coastlines data successfully extracted for {run_text}.')
|
||||
|
||||
# Export distance data
|
||||
if self.export_csv:
|
||||
|
||||
# Create folder if required and set path
|
||||
out_dir = 'deacoastlines_outputs'
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
csv_filename = f"{out_dir}/{self.output_name}.csv"
|
||||
|
||||
# Export to file
|
||||
dist_df.to_csv(csv_filename, index_label="Transect")
|
||||
print(f'Distance data exported to "{csv_filename}".')
|
||||
|
||||
# Generate plot
|
||||
with self.output_plot:
|
||||
|
||||
fig, ax = plt.subplots(constrained_layout=True,
|
||||
figsize=(15, 5.5))
|
||||
dist_df.T.plot(ax=ax, linewidth=3)
|
||||
|
||||
ax.legend(frameon=False, ncol=3, title='Transect')
|
||||
ax.set_title(f"Digital Earth Africa Coastlines transect extraction - {self.output_name}")
|
||||
ax.set_ylabel(f"Along-transect {self.mode} (m)")
|
||||
ax.set_xlim(dist_df.T.index[0], dist_df.T.index[-1])
|
||||
|
||||
# Hide the right and top spines
|
||||
ax.spines['right'].set_visible(False)
|
||||
ax.spines['top'].set_visible(False)
|
||||
|
||||
# Only show ticks on the left and bottom spines
|
||||
ax.yaxis.set_ticks_position('left')
|
||||
ax.xaxis.set_ticks_position('bottom')
|
||||
plt.show()
|
||||
|
||||
# Export plot
|
||||
with self.status_info:
|
||||
if self.export_plot:
|
||||
|
||||
# Create folder if required and set path
|
||||
out_dir = 'deacoastlines_outputs'
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
figure_filename = f"{out_dir}/{self.output_name}.png"
|
||||
|
||||
# Export to file
|
||||
fig.savefig(figure_filename)
|
||||
print(f'Figure exported to "{figure_filename}".')
|
||||
|
||||
else:
|
||||
print(
|
||||
"No valid shoreline data intersects with the "
|
||||
"supplied transect. This can occur if:\n\n"
|
||||
" - the transect does not intersect with any shorelines\n"
|
||||
" - the transect intersects with shorelines more than once in 'distance' mode\n"
|
||||
" - the transect intersects with shorelines only once in 'width' mode\n\n"
|
||||
"Please draw or upload a new transect.",
|
||||
end='\r')
|
||||
Reference in New Issue
Block a user