dea-tools 0.3.6.dev41__tar.gz → 0.3.6.dev42__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dea_tools-0.3.6.dev41/dea_tools.egg-info → dea_tools-0.3.6.dev42}/PKG-INFO +1 -1
- dea_tools-0.3.6.dev42/dea_tools/app/wetlandsinsighttool.py +635 -0
- dea_tools-0.3.6.dev42/dea_tools/wit_app.py +502 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42/dea_tools.egg-info}/PKG-INFO +1 -1
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/SOURCES.txt +2 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/.gitignore +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/LICENSE +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/MANIFEST.in +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/README.rst +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/__init__.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/__main__.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/__init__.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/animations.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/changefilmstrips.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/crophealth.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/deacoastlines.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/geomedian.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/imageexport.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/miningrehab.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/widgetconstructors.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/bandindices.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/bom.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/classification.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/climate.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/coastal.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/dask.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/datahandling.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/landcover.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/maps.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/plotting.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/pyfes_model.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/spatial.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/temporal.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/validation.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/waterbodies.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/wetlands.py +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/dependency_links.txt +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/requires.txt +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/top_level.txt +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/index.rst +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/mock_imports.txt +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/pyproject.toml +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/setup.cfg +0 -0
- {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/setup.py +0 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Digital Earth Australia Wetlands Insight Tool widget, which can be used to interactively
|
|
3
|
+
extract a stacked line plot using the wetlands insight tool on a wetland polygon.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Import required packages
|
|
7
|
+
import fiona
|
|
8
|
+
import sys
|
|
9
|
+
import datacube
|
|
10
|
+
import warnings
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
from datacube.utils.geometry import CRS
|
|
13
|
+
from ipyleaflet import (
|
|
14
|
+
WMSLayer,
|
|
15
|
+
basemaps,
|
|
16
|
+
basemap_to_tiles,
|
|
17
|
+
Map,
|
|
18
|
+
DrawControl,
|
|
19
|
+
WidgetControl,
|
|
20
|
+
SearchControl,
|
|
21
|
+
Marker,
|
|
22
|
+
LayerGroup,
|
|
23
|
+
LayersControl,
|
|
24
|
+
GeoData,
|
|
25
|
+
)
|
|
26
|
+
from traitlets import Unicode
|
|
27
|
+
from ipywidgets import (
|
|
28
|
+
GridspecLayout,
|
|
29
|
+
Button,
|
|
30
|
+
Layout,
|
|
31
|
+
HBox,
|
|
32
|
+
VBox,
|
|
33
|
+
HTML,
|
|
34
|
+
Output,
|
|
35
|
+
)
|
|
36
|
+
import json
|
|
37
|
+
import geopandas as gpd
|
|
38
|
+
from io import BytesIO
|
|
39
|
+
import ipywidgets as widgets
|
|
40
|
+
import datetime
|
|
41
|
+
import seaborn as sns
|
|
42
|
+
|
|
43
|
+
# from shapely.geometry import box, shape
|
|
44
|
+
import matplotlib.dates as mdates
|
|
45
|
+
|
|
46
|
+
sys.path.insert(1, "../Tools/")
|
|
47
|
+
import dea_tools.app.widgetconstructors as deawidgets
|
|
48
|
+
from dea_tools.dask import create_local_dask_cluster
|
|
49
|
+
from dea_tools.wetlands import generate_low_quality_data_periods
|
|
50
|
+
from dea_tools.wit_app import WIT_drill, spatial_wit
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def make_box_layout():
|
|
54
|
+
return Layout(
|
|
55
|
+
# border='solid 1px black',
|
|
56
|
+
margin="0px 10px 10px 0px",
|
|
57
|
+
padding="5px 5px 5px 5px",
|
|
58
|
+
width="100%",
|
|
59
|
+
height="100%",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def create_expanded_button(description, button_style):
|
|
64
|
+
return Button(
|
|
65
|
+
description=description,
|
|
66
|
+
button_style=button_style,
|
|
67
|
+
layout=Layout(width="auto", height="auto"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class wit_app(HBox):
|
|
72
|
+
|
|
73
|
+
def __init__(self):
|
|
74
|
+
super().__init__()
|
|
75
|
+
|
|
76
|
+
######################
|
|
77
|
+
# INITIAL ATTRIBUTES #
|
|
78
|
+
######################
|
|
79
|
+
|
|
80
|
+
enddate = datetime.datetime.today()
|
|
81
|
+
startdate = datetime.datetime(
|
|
82
|
+
year=enddate.year - 1, month=enddate.month, day=enddate.day
|
|
83
|
+
)
|
|
84
|
+
self.startdate = startdate.strftime("%Y-%m-%d")
|
|
85
|
+
self.enddate = enddate.strftime("%Y-%m-%d")
|
|
86
|
+
self.wetland_name = "example WIT"
|
|
87
|
+
# self.out_csv = "example_WIT.csv"
|
|
88
|
+
self.out_plot = False
|
|
89
|
+
self.product_list = [
|
|
90
|
+
("ESRI World Imagery", "none"),
|
|
91
|
+
("Open Street Map", "open_street_map"),
|
|
92
|
+
]
|
|
93
|
+
self.product = self.product_list[0][1]
|
|
94
|
+
self.target = None
|
|
95
|
+
self.action = None
|
|
96
|
+
self.gdf_drawn = None
|
|
97
|
+
self.gdf_uploaded = None
|
|
98
|
+
self.max_size = False
|
|
99
|
+
self.spatial_wit = False
|
|
100
|
+
|
|
101
|
+
##################
|
|
102
|
+
# HEADER FOR APP #
|
|
103
|
+
##################
|
|
104
|
+
|
|
105
|
+
# Create the Header widget
|
|
106
|
+
header_title_text = "<h3>Digital Earth Australia Wetlands Insight Tool </h3>"
|
|
107
|
+
instruction_text = "Select parameters and draw a polygon on the map to extract a stacked line plot for a given area. Alternatively, <b>upload a shapefile or a GeoJSON</b> to extract a stacked line plot for your wetland of interest polygon."
|
|
108
|
+
self.header = deawidgets.create_html(
|
|
109
|
+
f"{header_title_text}<p>{instruction_text}</p>"
|
|
110
|
+
)
|
|
111
|
+
self.header.layout = make_box_layout()
|
|
112
|
+
|
|
113
|
+
#####################################
|
|
114
|
+
# HANDLER FUNCTION FOR DRAW CONTROL #
|
|
115
|
+
#####################################
|
|
116
|
+
|
|
117
|
+
# Define the action to take once something is drawn on the map
|
|
118
|
+
def update_geojson(target, action, geo_json):
|
|
119
|
+
|
|
120
|
+
# Remove previously uploaded data if present
|
|
121
|
+
self.gdf_uploaded = None
|
|
122
|
+
fileupload_wetlands._counter = 0
|
|
123
|
+
|
|
124
|
+
# Get data from action
|
|
125
|
+
self.action = action
|
|
126
|
+
|
|
127
|
+
# Convert data to geopandas
|
|
128
|
+
json_data = json.dumps(geo_json)
|
|
129
|
+
binary_data = json_data.encode()
|
|
130
|
+
io = BytesIO(binary_data)
|
|
131
|
+
io.seek(0)
|
|
132
|
+
gdf = gpd.read_file(io)
|
|
133
|
+
gdf.crs = "EPSG:4326"
|
|
134
|
+
|
|
135
|
+
# Convert to Albers and compute area
|
|
136
|
+
gdf_drawn_albers = gdf.copy().to_crs("EPSG:3577")
|
|
137
|
+
m2_per_km2 = 10**6
|
|
138
|
+
area = gdf_drawn_albers.area.values[0] / m2_per_km2
|
|
139
|
+
polyarea_label = "Total polygon area"
|
|
140
|
+
polyarea_text = f"<b>{polyarea_label}</b>: {area:.2f} km<sup>2</sup>"
|
|
141
|
+
|
|
142
|
+
# Test area size
|
|
143
|
+
if self.max_size:
|
|
144
|
+
confirmation_text = '<span style="color: #33cc33"> <b>(Overriding maximum size limit; use with caution as may lead to memory issues)</b></span>'
|
|
145
|
+
self.header.value = (
|
|
146
|
+
header_title_text + polyarea_text + confirmation_text
|
|
147
|
+
)
|
|
148
|
+
self.gdf_drawn = gdf
|
|
149
|
+
elif area <= 2000:
|
|
150
|
+
confirmation_text = '<span style="color: #33cc33"> <b>(Area to extract falls within recommended limit)</b></span>'
|
|
151
|
+
self.header.value = (
|
|
152
|
+
header_title_text + polyarea_text + confirmation_text
|
|
153
|
+
)
|
|
154
|
+
self.gdf_drawn = gdf
|
|
155
|
+
else:
|
|
156
|
+
warning_text = '<span style="color: #ff5050"> <b>(Area to extract is too large, please update your polygon)</b></span>'
|
|
157
|
+
self.header.value = header_title_text + polyarea_text + warning_text
|
|
158
|
+
self.gdf_drawn = None
|
|
159
|
+
|
|
160
|
+
###########################
|
|
161
|
+
# WIDGETS FOR APP OUTPUTS #
|
|
162
|
+
###########################
|
|
163
|
+
|
|
164
|
+
self.dask_client = Output(layout=make_box_layout())
|
|
165
|
+
self.progress_bar = Output(layout=make_box_layout())
|
|
166
|
+
self.wit_plot = Output(layout=make_box_layout())
|
|
167
|
+
self.progress_header = deawidgets.create_html("")
|
|
168
|
+
|
|
169
|
+
#########################################
|
|
170
|
+
# MAP WIDGET, DRAWING TOOLS, WMS LAYERS #
|
|
171
|
+
#########################################
|
|
172
|
+
|
|
173
|
+
# Create drawing tools
|
|
174
|
+
desired_drawtools = ["rectangle", "polygon"]
|
|
175
|
+
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
|
176
|
+
|
|
177
|
+
# Begin by displaying an empty layer group, and update the group with desired WMS on interaction.
|
|
178
|
+
self.map_layers = LayerGroup(layers=())
|
|
179
|
+
self.map_layers.name = "Map Overlays"
|
|
180
|
+
|
|
181
|
+
# Create map widget
|
|
182
|
+
self.m = deawidgets.create_map(
|
|
183
|
+
map_center=(-28, 135), zoom_level=4, basemap=basemaps.Esri.WorldImagery
|
|
184
|
+
)
|
|
185
|
+
self.m.layout = make_box_layout()
|
|
186
|
+
|
|
187
|
+
# Add tools to map widget
|
|
188
|
+
self.m.add_control(draw_control)
|
|
189
|
+
self.m.add_control(
|
|
190
|
+
SearchControl(
|
|
191
|
+
position="topleft",
|
|
192
|
+
url="https://nominatim.openstreetmap.org/search?format=json&q={s}",
|
|
193
|
+
zoom=13, # 'Village / Suburb' level zoom
|
|
194
|
+
marker=Marker(draggable=False),
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
self.m.add_layer(self.map_layers)
|
|
198
|
+
|
|
199
|
+
# Store current basemap for future use
|
|
200
|
+
self.basemap = self.m.basemap
|
|
201
|
+
|
|
202
|
+
############################
|
|
203
|
+
# WIDGETS FOR APP CONTROLS #
|
|
204
|
+
############################
|
|
205
|
+
|
|
206
|
+
# Create parameter widgets
|
|
207
|
+
startdate_picker = deawidgets.create_datepicker(
|
|
208
|
+
value=startdate,
|
|
209
|
+
)
|
|
210
|
+
enddate_picker = deawidgets.create_datepicker(
|
|
211
|
+
value=enddate,
|
|
212
|
+
)
|
|
213
|
+
wetland_name = deawidgets.create_inputtext(self.wetland_name, self.wetland_name)
|
|
214
|
+
# output_csv = deawidgets.create_inputtext(self.out_csv, self.out_csv)
|
|
215
|
+
output_plot = deawidgets.create_checkbox(self.out_plot, "Figure (.png)")
|
|
216
|
+
deaoverlay_dropdown = deawidgets.create_dropdown(
|
|
217
|
+
self.product_list, self.product_list[0][1]
|
|
218
|
+
)
|
|
219
|
+
run_button = create_expanded_button("Run", "info")
|
|
220
|
+
fileupload_wetlands = widgets.FileUpload(accept="", multiple=True)
|
|
221
|
+
|
|
222
|
+
# Expandable advanced section
|
|
223
|
+
max_size = deawidgets.create_checkbox(
|
|
224
|
+
self.max_size, "Enable", layout={"width": "95%"}
|
|
225
|
+
)
|
|
226
|
+
output_spatial_wit = deawidgets.create_checkbox(
|
|
227
|
+
self.spatial_wit, "Animation (.gif)"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
####################################
|
|
231
|
+
# UPDATE FUNCTIONS FOR EACH WIDGET #
|
|
232
|
+
####################################
|
|
233
|
+
|
|
234
|
+
# Run update functions whenever various widgets are changed.
|
|
235
|
+
startdate_picker.observe(self.update_startdate, "value")
|
|
236
|
+
enddate_picker.observe(self.update_enddate, "value")
|
|
237
|
+
wetland_name.observe(self.update_wetlandname, "value")
|
|
238
|
+
# output_csv.observe(self.update_outputcsv, "value")
|
|
239
|
+
output_plot.observe(self.update_outputplot, "value")
|
|
240
|
+
deaoverlay_dropdown.observe(self.update_deaoverlay, "value")
|
|
241
|
+
run_button.on_click(self.run_app)
|
|
242
|
+
draw_control.on_draw(update_geojson)
|
|
243
|
+
fileupload_wetlands.observe(self.update_fileupload_wetlands, "value")
|
|
244
|
+
max_size.observe(self.update_maxsize, "value")
|
|
245
|
+
output_spatial_wit.observe(self.update_outputspatialwit, "value")
|
|
246
|
+
|
|
247
|
+
##################################
|
|
248
|
+
# COLLECTION OF ALL APP CONTROLS #
|
|
249
|
+
##################################
|
|
250
|
+
expand_box = VBox(
|
|
251
|
+
[
|
|
252
|
+
HTML(
|
|
253
|
+
"<b>Override maximum size limit:</b></br> (use with caution; may cause memory issues/crashes)"
|
|
254
|
+
),
|
|
255
|
+
max_size,
|
|
256
|
+
HTML("<b>Spatial WIT animation:<b/>"),
|
|
257
|
+
output_spatial_wit,
|
|
258
|
+
]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
expand = widgets.Accordion(
|
|
262
|
+
children=[expand_box],
|
|
263
|
+
selected_index=None,
|
|
264
|
+
)
|
|
265
|
+
expand.set_title(0, "Advanced")
|
|
266
|
+
|
|
267
|
+
parameter_selection = VBox(
|
|
268
|
+
[
|
|
269
|
+
HTML("<b>Start Date:</b>"),
|
|
270
|
+
startdate_picker,
|
|
271
|
+
HTML("<b>End Date:</b>"),
|
|
272
|
+
enddate_picker,
|
|
273
|
+
HTML("<b>Wetland Name:</b>"),
|
|
274
|
+
wetland_name,
|
|
275
|
+
# HTML("<b>Output CSV:</b>"),
|
|
276
|
+
# output_csv,
|
|
277
|
+
HTML("<b>Output Plot:</b>"),
|
|
278
|
+
output_plot,
|
|
279
|
+
HTML(
|
|
280
|
+
"</br><i><b>Upload Polygon:</b></br>Upload a GeoJSON or"
|
|
281
|
+
" Shapefile (<5 mb) containing a wetland polygon.</i>"
|
|
282
|
+
),
|
|
283
|
+
fileupload_wetlands,
|
|
284
|
+
HTML("</br>"),
|
|
285
|
+
expand,
|
|
286
|
+
]
|
|
287
|
+
)
|
|
288
|
+
map_selection = VBox(
|
|
289
|
+
[
|
|
290
|
+
HTML("</br><b>Map overlay:</b>"),
|
|
291
|
+
deaoverlay_dropdown,
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
parameter_selection.layout = make_box_layout()
|
|
295
|
+
map_selection.layout = make_box_layout()
|
|
296
|
+
|
|
297
|
+
###############################
|
|
298
|
+
# SPECIFICATION OF APP LAYOUT #
|
|
299
|
+
###############################
|
|
300
|
+
|
|
301
|
+
# 0 1 2 3 4 5 6 7 8 9
|
|
302
|
+
# ---------------------------------------------
|
|
303
|
+
# 0 | Header | Map sel. |
|
|
304
|
+
# ---------------------------------------------
|
|
305
|
+
# 1 | Params | |
|
|
306
|
+
# 2 | | |
|
|
307
|
+
# 3 | | |
|
|
308
|
+
# 4 | | Map |
|
|
309
|
+
# 5 | | |
|
|
310
|
+
# 6 | | |
|
|
311
|
+
# ---------- |
|
|
312
|
+
# 7 | Run | |
|
|
313
|
+
# ---------------------------------------------
|
|
314
|
+
# 8 | Status info |
|
|
315
|
+
# ---------------------------------------------
|
|
316
|
+
# 9 | |
|
|
317
|
+
# 10 | Output/figure |
|
|
318
|
+
# 11 | |
|
|
319
|
+
# 12 | ------------------------------------------|
|
|
320
|
+
|
|
321
|
+
# Create the layout #[rowspan, colspan]
|
|
322
|
+
grid = GridspecLayout(13, 10, height="1350px", width="auto")
|
|
323
|
+
|
|
324
|
+
# Header and controls
|
|
325
|
+
grid[0, :8] = self.header
|
|
326
|
+
grid[0, 8:] = map_selection
|
|
327
|
+
grid[1:7, 0:2] = parameter_selection
|
|
328
|
+
grid[7, 0:2] = run_button
|
|
329
|
+
|
|
330
|
+
# Status info, map and plot
|
|
331
|
+
grid[1:8, 2:] = self.m # map
|
|
332
|
+
grid[8:9, :] = self.progress_bar
|
|
333
|
+
# grid[7:8, :] = self.dask_client
|
|
334
|
+
|
|
335
|
+
# Plot
|
|
336
|
+
grid[9:, :] = self.wit_plot
|
|
337
|
+
|
|
338
|
+
# Display using HBox children attribute
|
|
339
|
+
self.children = [grid]
|
|
340
|
+
|
|
341
|
+
######################################
|
|
342
|
+
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
|
343
|
+
######################################
|
|
344
|
+
|
|
345
|
+
# Set the output csv
|
|
346
|
+
def update_fileupload_wetlands(self, change):
|
|
347
|
+
|
|
348
|
+
# Clear any drawn data if present
|
|
349
|
+
self.gdf_drawn = None
|
|
350
|
+
|
|
351
|
+
# Temporary compatibility fix for ipywidget > 8.0
|
|
352
|
+
# TODO: Update code to use new fileupload API documented here:
|
|
353
|
+
# https://ipywidgets.readthedocs.io/en/latest/user_migration_guides.html#fileupload
|
|
354
|
+
uploaded_data = {
|
|
355
|
+
f["name"]: {"content": f.content.tobytes()} for f in change.new
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Save to file
|
|
359
|
+
for uploaded_filename in uploaded_data.keys():
|
|
360
|
+
with open(uploaded_filename, "wb") as output_file:
|
|
361
|
+
content = uploaded_data[uploaded_filename]["content"]
|
|
362
|
+
output_file.write(content)
|
|
363
|
+
|
|
364
|
+
with self.progress_bar:
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
|
|
368
|
+
print("Loading vector data...", end="\r")
|
|
369
|
+
valid_files = [
|
|
370
|
+
file
|
|
371
|
+
for file in uploaded_data.keys()
|
|
372
|
+
if file.lower().endswith((".shp", ".geojson"))
|
|
373
|
+
]
|
|
374
|
+
valid_file = valid_files[0]
|
|
375
|
+
wetlands_gdf = (
|
|
376
|
+
gpd.read_file(valid_file)
|
|
377
|
+
.to_crs("EPSG:4326")
|
|
378
|
+
.explode(index_parts=True)
|
|
379
|
+
.reset_index(drop=True)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Use ID column if it exists
|
|
383
|
+
if "id" in wetlands_gdf:
|
|
384
|
+
wetlands_gdf = wetlands_gdf.set_index("id")
|
|
385
|
+
print(f"Uploaded '{valid_file}'; automatically labelling ")
|
|
386
|
+
else:
|
|
387
|
+
print(f"Uploaded '{valid_file}'; no 'id' column detected.")
|
|
388
|
+
|
|
389
|
+
# Create a geodata
|
|
390
|
+
geodata = GeoData(
|
|
391
|
+
geo_dataframe=wetlands_gdf, style={"color": "black", "weight": 3}
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Add to map
|
|
395
|
+
xmin, ymin, xmax, ymax = wetlands_gdf.total_bounds
|
|
396
|
+
self.m.fit_bounds([[ymin, xmin], [ymax, xmax]])
|
|
397
|
+
self.m.add_layer(geodata)
|
|
398
|
+
|
|
399
|
+
# If completed, add to attribute
|
|
400
|
+
self.gdf_uploaded = wetlands_gdf
|
|
401
|
+
|
|
402
|
+
except IndexError:
|
|
403
|
+
print(
|
|
404
|
+
"Cannot read uploaded files. Please ensure that data is "
|
|
405
|
+
"in either GeoJSON or Shapefile format.",
|
|
406
|
+
end="\r",
|
|
407
|
+
)
|
|
408
|
+
self.gdf_uploaded = None
|
|
409
|
+
|
|
410
|
+
except fiona.errors.DriverError:
|
|
411
|
+
print(
|
|
412
|
+
"Shapefile is invalid. Please ensure that all shapefile "
|
|
413
|
+
"components (e.g. .shp, .shx, .dbf, .prj) are uploaded.",
|
|
414
|
+
end="\r",
|
|
415
|
+
)
|
|
416
|
+
self.gdf_uploaded = None
|
|
417
|
+
|
|
418
|
+
# Set the start date to the new edited date
|
|
419
|
+
def update_startdate(self, change):
|
|
420
|
+
self.startdate = change.new
|
|
421
|
+
|
|
422
|
+
# Set the end date to the new edited date
|
|
423
|
+
def update_enddate(self, change):
|
|
424
|
+
self.enddate = change.new
|
|
425
|
+
|
|
426
|
+
# Set the wetland name
|
|
427
|
+
def update_wetlandname(self, change):
|
|
428
|
+
self.wetland_name = change.new
|
|
429
|
+
|
|
430
|
+
# Set the output csv
|
|
431
|
+
# def update_outputcsv(self, change):
|
|
432
|
+
# self.out_csv = change.new
|
|
433
|
+
|
|
434
|
+
# Set the output plot
|
|
435
|
+
def update_outputplot(self, change):
|
|
436
|
+
self.out_plot = change.new
|
|
437
|
+
|
|
438
|
+
# Override max size limit
|
|
439
|
+
def update_maxsize(self, change):
|
|
440
|
+
self.max_size = change.new
|
|
441
|
+
|
|
442
|
+
# Select to output spatial WIT
|
|
443
|
+
def update_outputspatialwit(self, change):
|
|
444
|
+
self.spatial_wit = change.new
|
|
445
|
+
|
|
446
|
+
# Update product
|
|
447
|
+
def update_deaoverlay(self, change):
|
|
448
|
+
|
|
449
|
+
self.product = change.new
|
|
450
|
+
|
|
451
|
+
if self.product == "none":
|
|
452
|
+
self.map_layers.clear_layers()
|
|
453
|
+
|
|
454
|
+
elif self.product == "open_street_map":
|
|
455
|
+
self.map_layers.clear_layers()
|
|
456
|
+
layer = basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)
|
|
457
|
+
self.map_layers.add_layer(layer)
|
|
458
|
+
|
|
459
|
+
def run_app(self, change):
|
|
460
|
+
|
|
461
|
+
# Clear progress bar and output areas before running
|
|
462
|
+
self.progress_bar.clear_output()
|
|
463
|
+
self.wit_plot.clear_output()
|
|
464
|
+
self.dask_client.clear_output()
|
|
465
|
+
|
|
466
|
+
# Configure local dask cluster
|
|
467
|
+
with self.dask_client:
|
|
468
|
+
client = create_local_dask_cluster(return_client=True, display_client=True)
|
|
469
|
+
|
|
470
|
+
# Set any defaults
|
|
471
|
+
dask_chunks = dict(x=1000, y=1000, time=1)
|
|
472
|
+
|
|
473
|
+
self.progress_header.value = "<h3>" + ("Progress") + "</h3>"
|
|
474
|
+
|
|
475
|
+
# Run DEA WIT analysis
|
|
476
|
+
with self.progress_bar:
|
|
477
|
+
warnings.filterwarnings("ignore")
|
|
478
|
+
|
|
479
|
+
# Load polygons from either map or uploaded files
|
|
480
|
+
if self.gdf_uploaded is not None:
|
|
481
|
+
wetlands_gdf = self.gdf_uploaded
|
|
482
|
+
run_text = "uploaded file"
|
|
483
|
+
elif self.gdf_drawn is not None:
|
|
484
|
+
wetlands_gdf = self.gdf_drawn
|
|
485
|
+
|
|
486
|
+
# save the drawn polygon as a geojson in the current directory
|
|
487
|
+
try:
|
|
488
|
+
output_geojson_path = f"{self.wetland_name}_drawn_polygon.geojson"
|
|
489
|
+
wetlands_gdf.to_file(output_geojson_path, driver="GeoJSON")
|
|
490
|
+
print(f"Drawn polygon saved to: {output_geojson_path}")
|
|
491
|
+
except Exception as e:
|
|
492
|
+
print(f"Error saving drawn polygon: {e}")
|
|
493
|
+
run_text = "selected polygon"
|
|
494
|
+
else:
|
|
495
|
+
print(
|
|
496
|
+
f"No polygon drawn or uploaded. Please select a polygon on the map, or upload a GeoJSON or Shapefile.",
|
|
497
|
+
end="\r",
|
|
498
|
+
)
|
|
499
|
+
wetlands_gdf = None
|
|
500
|
+
|
|
501
|
+
# Run wetlands polygon drill
|
|
502
|
+
df = None
|
|
503
|
+
|
|
504
|
+
if not self.wetland_name.endswith(".csv"):
|
|
505
|
+
output_csv = self.wetland_name + ".csv"
|
|
506
|
+
else:
|
|
507
|
+
output_csv = self.wetland_name
|
|
508
|
+
|
|
509
|
+
if wetlands_gdf is not None:
|
|
510
|
+
try:
|
|
511
|
+
ds_wit, df = WIT_drill(
|
|
512
|
+
gdf=wetlands_gdf,
|
|
513
|
+
time=(self.startdate, self.enddate),
|
|
514
|
+
export_csv=output_csv,
|
|
515
|
+
dask_chunks=dask_chunks,
|
|
516
|
+
verbose=False,
|
|
517
|
+
verbose_progress=True,
|
|
518
|
+
)
|
|
519
|
+
print("WIT complete")
|
|
520
|
+
except AttributeError:
|
|
521
|
+
print("No polygon selected")
|
|
522
|
+
|
|
523
|
+
else:
|
|
524
|
+
print(
|
|
525
|
+
"No valid polygon to process. Please select or draw a new polygon."
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# close down the dask client
|
|
529
|
+
client.shutdown()
|
|
530
|
+
|
|
531
|
+
# save the csv
|
|
532
|
+
if df is not None and self.wetland_name:
|
|
533
|
+
df.to_csv(output_csv, index=False)
|
|
534
|
+
|
|
535
|
+
else:
|
|
536
|
+
print("No valid polygon to process. Please select or draw a new polygon.")
|
|
537
|
+
df = None
|
|
538
|
+
|
|
539
|
+
# ---Plotting------------------------------
|
|
540
|
+
if df is not None:
|
|
541
|
+
with self.wit_plot:
|
|
542
|
+
|
|
543
|
+
fontsize = 17
|
|
544
|
+
plt.rcParams.update({"font.size": fontsize})
|
|
545
|
+
# set up color palette
|
|
546
|
+
pal = [
|
|
547
|
+
sns.xkcd_rgb["cobalt blue"],
|
|
548
|
+
sns.xkcd_rgb["neon blue"],
|
|
549
|
+
sns.xkcd_rgb["grass"],
|
|
550
|
+
sns.xkcd_rgb["beige"],
|
|
551
|
+
sns.xkcd_rgb["brown"],
|
|
552
|
+
]
|
|
553
|
+
|
|
554
|
+
# make a stacked area plot
|
|
555
|
+
plt.close("all")
|
|
556
|
+
|
|
557
|
+
fig, ax = plt.subplots(constrained_layout=True, figsize=(20, 6))
|
|
558
|
+
|
|
559
|
+
ax.stackplot(
|
|
560
|
+
df["date"],
|
|
561
|
+
df["water"] * 100,
|
|
562
|
+
df["wet"] * 100,
|
|
563
|
+
df["norm_pv"] * 100,
|
|
564
|
+
df["norm_npv"] * 100,
|
|
565
|
+
df["norm_bs"] * 100,
|
|
566
|
+
colors=pal,
|
|
567
|
+
alpha=0.7,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# manually change the legend display order
|
|
571
|
+
legend = ax.legend(
|
|
572
|
+
["open water", "wet", "green veg", "dry veg", "bare soil"][::-1],
|
|
573
|
+
loc="lower left",
|
|
574
|
+
)
|
|
575
|
+
handles = legend.legend_handles
|
|
576
|
+
|
|
577
|
+
for i, handle in enumerate(handles):
|
|
578
|
+
handle.set_facecolor(pal[::-1][i])
|
|
579
|
+
handle.set_alpha(0.7)
|
|
580
|
+
|
|
581
|
+
# setup the display ranges
|
|
582
|
+
ax.set_ylim(0, 100)
|
|
583
|
+
ax.set_xlim(df["date"].min(), df["date"].max())
|
|
584
|
+
|
|
585
|
+
# add a new column: 'off_value' based on low quality data setting
|
|
586
|
+
df = generate_low_quality_data_periods(df)
|
|
587
|
+
|
|
588
|
+
ax.fill_between(
|
|
589
|
+
df["date"],
|
|
590
|
+
0,
|
|
591
|
+
100,
|
|
592
|
+
where=df["off_value"] == 100,
|
|
593
|
+
color="white",
|
|
594
|
+
alpha=0.5,
|
|
595
|
+
hatch="//",
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
|
599
|
+
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b-%Y"))
|
|
600
|
+
|
|
601
|
+
# Rotates and right-aligns the x labels so they don't crowd each other.
|
|
602
|
+
for label in ax.get_xticklabels(which="major"):
|
|
603
|
+
label.set(rotation=30, horizontalalignment="right")
|
|
604
|
+
|
|
605
|
+
x_label_text = "The Fractional Cover algorithm developed by the Joint Remote Sensing Research Program and\n the Water Observations from Space algorithm developed by Geoscience Australia are used in the production of this data"
|
|
606
|
+
|
|
607
|
+
ax.set_xlabel(x_label_text, style="italic")
|
|
608
|
+
|
|
609
|
+
ax.set_ylabel("Percentage of wetland (%)")
|
|
610
|
+
|
|
611
|
+
# add a title
|
|
612
|
+
plt.title(
|
|
613
|
+
f"Percentage of area dominated by WOs, Wetness, Fractional Cover for\n {self.wetland_name}",
|
|
614
|
+
fontsize=16,
|
|
615
|
+
)
|
|
616
|
+
plt.show()
|
|
617
|
+
|
|
618
|
+
if self.out_plot:
|
|
619
|
+
# save the figure
|
|
620
|
+
fig.savefig(f"{self.wetland_name}")
|
|
621
|
+
|
|
622
|
+
else:
|
|
623
|
+
print("No valid polygon to process. Please select or draw a new polygon.")
|
|
624
|
+
|
|
625
|
+
# Export spatial WIT animation if checkbox is selected
|
|
626
|
+
if self.spatial_wit and ds_wit is not None:
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
ds = spatial_wit(ds=ds_wit, wetland_name=self.wetland_name)
|
|
630
|
+
print("Animation complete")
|
|
631
|
+
except AttributeError:
|
|
632
|
+
print("No polygon selected")
|
|
633
|
+
|
|
634
|
+
else:
|
|
635
|
+
print("No valid polygon to process. Please select or draw a new polygon.")
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# wit_app.py
|
|
2
|
+
"""
|
|
3
|
+
This module is for processing DEA wetlands data, including Spatial WIT.
|
|
4
|
+
|
|
5
|
+
License: The code in this notebook is licensed under the Apache
|
|
6
|
+
License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0).
|
|
7
|
+
Digital Earth Australia data is licensed under the Creative Commons
|
|
8
|
+
by Attribution 4.0 license
|
|
9
|
+
(https://creativecommons.org/licenses/by/4.0/).
|
|
10
|
+
|
|
11
|
+
Contact: If you need assistance, please post a question on the Open
|
|
12
|
+
Data Cube Discord chat (https://discord.com/invite/4hhBQVas5U) or on the
|
|
13
|
+
GIS Stack Exchange
|
|
14
|
+
(https://gis.stackexchange.com/questions/ask?tags=open-data-cube)using
|
|
15
|
+
the `open-data-cube` tag (you can view previously asked questions
|
|
16
|
+
here: https://gis.stackexchange.com/questions/tagged/open-data-cube).
|
|
17
|
+
|
|
18
|
+
If you would like to report an issue with this script, file one on
|
|
19
|
+
GitHub: https://github.com/GeoscienceAustralia/dea-notebooks/issues/new
|
|
20
|
+
|
|
21
|
+
Last modified: March 2025
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Import required packages
|
|
26
|
+
|
|
27
|
+
import datetime
|
|
28
|
+
import geopandas as gpd
|
|
29
|
+
import imageio
|
|
30
|
+
import itertools
|
|
31
|
+
import matplotlib.pyplot as plt
|
|
32
|
+
import matplotlib.colors as mcolors
|
|
33
|
+
import numpy as np
|
|
34
|
+
import os
|
|
35
|
+
import pandas as pd
|
|
36
|
+
import shutil
|
|
37
|
+
import warnings
|
|
38
|
+
import xarray as xr
|
|
39
|
+
|
|
40
|
+
import datacube
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
sys.path.insert(1, "../Tools/")
|
|
44
|
+
import dea_tools.bandindices
|
|
45
|
+
import dea_tools.datahandling
|
|
46
|
+
from dea_tools.spatial import xr_rasterize
|
|
47
|
+
from dea_tools.dask import create_local_dask_cluster
|
|
48
|
+
import dea_tools.wetlands
|
|
49
|
+
|
|
50
|
+
# Create local dask cluster to improve data load time
|
|
51
|
+
client = create_local_dask_cluster(return_client=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def WIT_drill(
|
|
55
|
+
gdf,
|
|
56
|
+
time,
|
|
57
|
+
export_csv=None,
|
|
58
|
+
dask_chunks=None,
|
|
59
|
+
verbose=False,
|
|
60
|
+
verbose_progress=False,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Runs the Wetlands Insight Tool over a polygon. The code is based on the DEA Wetlands
|
|
64
|
+
Insight Tool notebook.
|
|
65
|
+
This function loads FC, WOs, and Landsat Data, and calculates Tasseled Cap Wetness, in
|
|
66
|
+
order to summarise how the different classes have changed over time.
|
|
67
|
+
|
|
68
|
+
The output is an Xarray dataset and a pandas dataframe containing a timeseries of the
|
|
69
|
+
normalised relative fractions of each class at each time-step. This forms the input to
|
|
70
|
+
produce a stacked line plot.
|
|
71
|
+
|
|
72
|
+
Last modified March 2025
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
gdf : geopandas.GeoDataFrame
|
|
77
|
+
The dataframe must only contain a single row containing the polygon you wish to
|
|
78
|
+
interrogate
|
|
79
|
+
time : tuple
|
|
80
|
+
A tuple containing the time range over which to run the WIT.
|
|
81
|
+
e.g. ("2014-01-01", "2015-01-01")
|
|
82
|
+
export_csv : string, optional
|
|
83
|
+
To save the returned pandas dataframe as a .csv file, pass a location string (e.g.
|
|
84
|
+
'output/results.csv')
|
|
85
|
+
dask_chunks : dict, optional
|
|
86
|
+
To lazily load the datasets using dask, pass a dictionary containing the dimensions
|
|
87
|
+
over which to chunk e.g. {'time':1, 'x':2048, 'y':2048}.
|
|
88
|
+
verbose : bool, optional
|
|
89
|
+
If true, print statements are outputted detailing the progress of the tool.
|
|
90
|
+
verbose_progress : bool, optional
|
|
91
|
+
For use with Dask progress bar.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
ds_wit : xarray.Dataset
|
|
96
|
+
An xarray dataset containing values for each cover class (open water, wet, pv, npv, bs).
|
|
97
|
+
polygon_base_df : pandas.DataFrame
|
|
98
|
+
A DataFrame containing the normalised timeseries of relative fractions of each cover
|
|
99
|
+
class (open water, wet, pv, npv, bs).
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Connect to the datacube
|
|
104
|
+
dc = datacube.Datacube(app="WIT_drill")
|
|
105
|
+
|
|
106
|
+
# load landsat 5,7,8 data
|
|
107
|
+
warnings.filterwarnings("ignore")
|
|
108
|
+
|
|
109
|
+
# load wetland polygon and specify the coordinate reference system of the polygon
|
|
110
|
+
if isinstance(gdf, datacube.utils.geometry._base.Geometry):
|
|
111
|
+
gdf = gpd.GeoDataFrame({"col1": ["name"], "geometry": gdf.geom}, crs=gdf.crs)
|
|
112
|
+
gpgon = datacube.utils.geometry.Geometry(gdf.geometry[0], crs=gdf.crs)
|
|
113
|
+
|
|
114
|
+
# Define which spectral bands are being used in the analysis
|
|
115
|
+
bands = [
|
|
116
|
+
f"nbart_{band}" for band in ("blue", "green", "red", "nir", "swir_1", "swir_2")
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
if verbose_progress:
|
|
120
|
+
print("Loading Landsat data")
|
|
121
|
+
|
|
122
|
+
# Load Landsat 5, 7 and 8 data. Not including Landsat 7 SLC off period (31-05-2003 to 06-04-2022)
|
|
123
|
+
ds_ls = dea_tools.datahandling.load_ard(
|
|
124
|
+
dc,
|
|
125
|
+
products=["ga_ls8c_ard_3", "ga_ls7e_ard_3", "ga_ls5t_ard_3"],
|
|
126
|
+
ls7_slc_off=False,
|
|
127
|
+
measurements=bands,
|
|
128
|
+
geopolygon=gpgon,
|
|
129
|
+
output_crs="EPSG:3577",
|
|
130
|
+
resolution=(-30, 30),
|
|
131
|
+
resampling={"fmask": "nearest", "*": "bilinear"},
|
|
132
|
+
time=time,
|
|
133
|
+
group_by="solar_day",
|
|
134
|
+
dask_chunks={"time": 1, "x": 2048, "y": 2048},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Load into memory using Dask
|
|
138
|
+
ds_ls.load()
|
|
139
|
+
|
|
140
|
+
# Load Water Observations dataset into the same spatial grid and resolution of the loaded Landsat dataset
|
|
141
|
+
ds_wo = dc.load(
|
|
142
|
+
"ga_ls_wo_3",
|
|
143
|
+
resampling="nearest",
|
|
144
|
+
group_by="solar_day",
|
|
145
|
+
like=ds_ls,
|
|
146
|
+
dask_chunks={"time": 1, "x": 2048, "y": 2048},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Load Fractional Cover dataset into the same spatial grid and resolution of the loaded Landsat dataset
|
|
150
|
+
ds_fc = dc.load(
|
|
151
|
+
"ga_ls_fc_3",
|
|
152
|
+
resampling="nearest",
|
|
153
|
+
group_by="solar_day",
|
|
154
|
+
like=ds_ls,
|
|
155
|
+
dask_chunks={"time": 1, "x": 2048, "y": 2048},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Load data into memory
|
|
159
|
+
ds_wo.load()
|
|
160
|
+
ds_fc.load()
|
|
161
|
+
|
|
162
|
+
# Locate and remove any observations which aren't in all three datasets
|
|
163
|
+
missing = set()
|
|
164
|
+
for t1, t2 in itertools.product(
|
|
165
|
+
[ds_fc.time.values, ds_wo.time.values, ds_ls.time.values], repeat=2
|
|
166
|
+
):
|
|
167
|
+
missing_ = set(t1) - set(t2)
|
|
168
|
+
missing |= missing_
|
|
169
|
+
|
|
170
|
+
ds_fc = ds_fc.sel(time=[t for t in ds_fc.time.values if t not in missing])
|
|
171
|
+
ds_ls = ds_ls.sel(time=[t for t in ds_ls.time.values if t not in missing])
|
|
172
|
+
ds_wo = ds_wo.sel(time=[t for t in ds_wo.time.values if t not in missing])
|
|
173
|
+
|
|
174
|
+
# Calculate Tasseled Cap Wetness from the Landsat data
|
|
175
|
+
tcw = dea_tools.bandindices.calculate_indices(
|
|
176
|
+
ds_ls,
|
|
177
|
+
index="TCW",
|
|
178
|
+
collection="ga_ls_3",
|
|
179
|
+
normalise=False,
|
|
180
|
+
drop=True,
|
|
181
|
+
inplace=False,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Divide Fractional Cover by 100 to keep them in [0,1]. Keeps data types the same in
|
|
185
|
+
# the output raster
|
|
186
|
+
bs = ds_fc.bs / 100
|
|
187
|
+
pv = ds_fc.pv / 100
|
|
188
|
+
npv = ds_fc.npv / 100
|
|
189
|
+
|
|
190
|
+
# Generate the WIT raster bands by creating an empty dataset called `output_rast` and
|
|
191
|
+
# populate with values from input datasets
|
|
192
|
+
rast_names = ["pv", "npv", "bs", "wet", "water"]
|
|
193
|
+
output_rast = {n: xr.zeros_like(bs) for n in rast_names}
|
|
194
|
+
|
|
195
|
+
output_rast["bs"].values[:] = bs
|
|
196
|
+
output_rast["pv"].values[:] = pv
|
|
197
|
+
output_rast["npv"].values[:] = npv
|
|
198
|
+
|
|
199
|
+
# Masking
|
|
200
|
+
|
|
201
|
+
# Rasterise the shapefile where gdf is the vector data and pv is the xarray template
|
|
202
|
+
poly_raster = xr_rasterize(gdf, pv) > 0
|
|
203
|
+
|
|
204
|
+
# Mask includes No data, Non contiguous data, Cloud shadow, Cloud, and water.
|
|
205
|
+
# See https://knowledge.dea.ga.gov.au/notebooks/DEA_products/DEA_Water_Observations/#Understanding-WOs-bit-flags
|
|
206
|
+
# for more detail.
|
|
207
|
+
mask = (ds_wo.water & 0b01100011) == 0
|
|
208
|
+
mask &= poly_raster
|
|
209
|
+
|
|
210
|
+
# Set open water to water present and classified as water as per Water Observations and bit flags
|
|
211
|
+
open_water = ds_wo.water & (1 << 7) > 0
|
|
212
|
+
|
|
213
|
+
# Thresholding
|
|
214
|
+
|
|
215
|
+
# Set wet pixels where not masked and above threshold of -350
|
|
216
|
+
wet = tcw.where(mask).TCW > -350
|
|
217
|
+
|
|
218
|
+
# Adding wet and water values to output raster
|
|
219
|
+
|
|
220
|
+
# TCW
|
|
221
|
+
output_rast["wet"].values[:] = wet.values.astype(float)
|
|
222
|
+
for name in rast_names[:3]:
|
|
223
|
+
output_rast[name].values[wet.values] = 0
|
|
224
|
+
|
|
225
|
+
# WO
|
|
226
|
+
output_rast["water"].values[:] = open_water.values.astype(float)
|
|
227
|
+
for name in rast_names[:4]:
|
|
228
|
+
output_rast[name].values[open_water.values] = 0
|
|
229
|
+
|
|
230
|
+
# Masking again
|
|
231
|
+
ds_wit = xr.Dataset(output_rast).where(mask)
|
|
232
|
+
|
|
233
|
+
# Calculate percentage missing
|
|
234
|
+
pc_missing = (~mask).where(poly_raster).mean(dim=["x", "y"])
|
|
235
|
+
|
|
236
|
+
# Mask entire observations where the polygon is more than 10% masked
|
|
237
|
+
ds_wit = ds_wit.where(pc_missing < 0.1)
|
|
238
|
+
|
|
239
|
+
# Normalise Fractional Cover Values in WIT result
|
|
240
|
+
|
|
241
|
+
# Convert ds_wit: xarray.Dataset to polygon_base_df: pandas.DataFrame
|
|
242
|
+
polygon_base_df = pd.DataFrame()
|
|
243
|
+
polygon_base_df["date"] = ds_wit.time.values
|
|
244
|
+
|
|
245
|
+
for band in rast_names:
|
|
246
|
+
polygon_base_df[band] = ds_wit[band].mean(dim=["x", "y"])
|
|
247
|
+
|
|
248
|
+
polygon_base_df = dea_tools.wetlands.normalise_wit(polygon_base_df)
|
|
249
|
+
|
|
250
|
+
# Create WIT comma-separated values (CSV) output file
|
|
251
|
+
if export_csv:
|
|
252
|
+
polygon_base_df = polygon_base_df.drop("index", axis=1)
|
|
253
|
+
polygon_base_df.to_csv(export_csv, index_label="date")
|
|
254
|
+
|
|
255
|
+
return ds_wit, polygon_base_df
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def classify_pixel(pv, npv, bs):
|
|
259
|
+
"""
|
|
260
|
+
This function sorts the fractional cover values into their classes to be used in Spatial WIT.
|
|
261
|
+
|
|
262
|
+
Last modified March 2025
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
pv : array
|
|
267
|
+
Array containing values for the photosynthetic (green) vegetation class.
|
|
268
|
+
npv : array
|
|
269
|
+
Array containing values for the non-photosynthetic (dry) vegetation class.
|
|
270
|
+
bs : array
|
|
271
|
+
Array containing values for the base soil class.
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
integer
|
|
276
|
+
Integer values to assign each of the fractional cover classes (https://knowledge.dea.ga.gov.au/data/product/dea-fractional-cover-landsat/).
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
if pv > 2 / 3:
|
|
280
|
+
return 8 # pv
|
|
281
|
+
elif npv > 2 / 3:
|
|
282
|
+
return 0 # ng
|
|
283
|
+
elif bs > 2 / 3:
|
|
284
|
+
return 4 # bs
|
|
285
|
+
elif npv > 1 / 3 and bs > 1 / 3 and pv < 1 / 3:
|
|
286
|
+
return 1 # ng_bs
|
|
287
|
+
elif npv > 1 / 3 and pv > 1 / 3 and bs < 1 / 3:
|
|
288
|
+
return 3 # ng_pv
|
|
289
|
+
elif npv > 1 / 3 and pv < 1 / 3 and bs < 1 / 3:
|
|
290
|
+
return 2 # ng_mix
|
|
291
|
+
elif bs > 1 / 3 and pv > 1 / 3 and npv < 1 / 3:
|
|
292
|
+
return 6 # bs_pv
|
|
293
|
+
elif bs > 1 / 3 and pv < 1 / 3 and npv < 1 / 3:
|
|
294
|
+
return 5 # bs_mix
|
|
295
|
+
elif pv > 1 / 3 and npv < 1 / 3 and bs < 1 / 3:
|
|
296
|
+
return 7 # pv_mix
|
|
297
|
+
return -1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def spatial_wit(ds, wetland_name):
|
|
301
|
+
"""
|
|
302
|
+
Takes Wetlands Insight Tool classifications and represents them in a spatial way. The
|
|
303
|
+
same caveats for those classifications apply (see Dunn et al., 2023 and DEA
|
|
304
|
+
Wetlands Insight Tool notebook). The water and wet classes are binary and the
|
|
305
|
+
vegetation fractional cover classes are percentages per pixel, with the spatial
|
|
306
|
+
representation scaled accordingly.
|
|
307
|
+
|
|
308
|
+
Last modified: March 2025
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
ds : xarray.Dataset
|
|
313
|
+
An xarray dataset containing values for each cover class (open water, wet, pv, npv, bs).
|
|
314
|
+
|
|
315
|
+
wetland_name : string
|
|
316
|
+
Value to be used when naming the output files.
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
output_path :
|
|
321
|
+
File path where the GIF animation will be saved.
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
"""
|
|
325
|
+
# Remove time steps where all values are missing (i.e. NaNs)
|
|
326
|
+
ds = ds.dropna(dim="time", how="all")
|
|
327
|
+
|
|
328
|
+
# Calculate the sum of the fractional cover values for pv, npv and bs then normalise
|
|
329
|
+
# the values for each class
|
|
330
|
+
fraction_sum = ds["pv"] + ds["npv"] + ds["bs"]
|
|
331
|
+
ds["pv"] = ds["pv"] / fraction_sum
|
|
332
|
+
ds["npv"] = ds["npv"] / fraction_sum
|
|
333
|
+
ds["bs"] = ds["bs"] / fraction_sum
|
|
334
|
+
|
|
335
|
+
# Apply the classification function to the dataset across all time, y, x
|
|
336
|
+
fc_class = xr.apply_ufunc(
|
|
337
|
+
classify_pixel,
|
|
338
|
+
ds["pv"],
|
|
339
|
+
ds["npv"],
|
|
340
|
+
ds["bs"],
|
|
341
|
+
vectorize=True,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
fc_class = fc_class.where(fc_class != -1, np.nan)
|
|
345
|
+
|
|
346
|
+
# Add the new classification band to the dataset
|
|
347
|
+
ds["fc_class"] = fc_class
|
|
348
|
+
|
|
349
|
+
# Make a new band called wetland that combines all the fractional cover classes
|
|
350
|
+
# with the water and wet classes
|
|
351
|
+
# i.e. water == 10, wet == 9, all other areas retain original FC class
|
|
352
|
+
wetland = ds["fc_class"].where((ds["water"] == 0) | ds["water"].isnull(), 10)
|
|
353
|
+
ds["wetland"] = wetland
|
|
354
|
+
wetland = ds["wetland"].where((ds["wet"] == 0) | ds["wet"].isnull(), 9)
|
|
355
|
+
ds["wetland"] = wetland
|
|
356
|
+
|
|
357
|
+
# Define labels for each class
|
|
358
|
+
class_labels = [
|
|
359
|
+
"dry veg", # ng
|
|
360
|
+
"dry veg and bare mix", # ng_bs
|
|
361
|
+
"dry mix", # ng_mix
|
|
362
|
+
"dry veg and green veg", # ng_pv
|
|
363
|
+
"bare soil", # bs
|
|
364
|
+
"bare soil mix", # bs_mix
|
|
365
|
+
"bare soil and green veg", # bs_pv
|
|
366
|
+
"green veg mix", # pv_mix
|
|
367
|
+
"green veg", # pv
|
|
368
|
+
"wet",
|
|
369
|
+
"water",
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
# Define the colormap with your custom colors
|
|
373
|
+
cmap = mcolors.ListedColormap(
|
|
374
|
+
[
|
|
375
|
+
"#F1E8C9", # ng
|
|
376
|
+
"#C0AB86", # ng_bs
|
|
377
|
+
"#D6D2A7", # ng_mix
|
|
378
|
+
"#BCD495", # ng_pv
|
|
379
|
+
"#93724C", # bs
|
|
380
|
+
"#9C895D", # bs_mix
|
|
381
|
+
"#8F9C5C", # bs_pv
|
|
382
|
+
"#9DBD74", # pv_mix
|
|
383
|
+
"#8CC46B", # pv
|
|
384
|
+
"#6ce6f8", # wet
|
|
385
|
+
"#676dca", # water
|
|
386
|
+
]
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Create a BoundedNorm to ensure correct mapping of data to the colormap
|
|
390
|
+
norm = mcolors.Normalize(vmin=0, vmax=10)
|
|
391
|
+
|
|
392
|
+
# Create a directory to save the frames
|
|
393
|
+
os.makedirs("deawetlands_outputs", exist_ok=True)
|
|
394
|
+
|
|
395
|
+
# Clean the input name to remove spaces
|
|
396
|
+
wetland_name = wetland_name.replace(" ", "_")
|
|
397
|
+
|
|
398
|
+
# Save all the time steps
|
|
399
|
+
|
|
400
|
+
for t in ds.time:
|
|
401
|
+
wetland_time_step = ds["wetland"].sel(time=t)
|
|
402
|
+
date_str = str(t.values)[:10] # Extract date as string
|
|
403
|
+
wetland_time_step.rio.to_raster(
|
|
404
|
+
f"deawetlands_outputs/{wetland_name}_{date_str}.tif"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Make one big plot
|
|
408
|
+
|
|
409
|
+
num_time_steps = ds.sizes["time"]
|
|
410
|
+
|
|
411
|
+
# Set the number of columns as the number of time steps if less than 10
|
|
412
|
+
num_columns = min(num_time_steps, 10)
|
|
413
|
+
|
|
414
|
+
# Calculate the number of rows
|
|
415
|
+
num_rows = (num_time_steps + num_columns - 1) // num_columns
|
|
416
|
+
|
|
417
|
+
time_step = ds["wetland"].isel(time=0)
|
|
418
|
+
height, width = time_step.shape
|
|
419
|
+
fig, axes = plt.subplots(
|
|
420
|
+
num_rows, num_columns, figsize=(8 * num_columns * width / height, 8 * num_rows)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if num_rows == 1:
|
|
424
|
+
axes = axes.reshape(1, num_columns)
|
|
425
|
+
elif num_columns == 1:
|
|
426
|
+
axes = axes.reshape(num_rows, 1)
|
|
427
|
+
|
|
428
|
+
# Hide any unused axes
|
|
429
|
+
for i in range(num_rows * num_columns):
|
|
430
|
+
if i >= num_time_steps:
|
|
431
|
+
fig.delaxes(axes.flatten()[i])
|
|
432
|
+
|
|
433
|
+
# Plot time steps
|
|
434
|
+
for t in range(num_time_steps):
|
|
435
|
+
time_step = ds["wetland"].isel(time=t)
|
|
436
|
+
time_ns = ds["time"].isel(time=t).values.item()
|
|
437
|
+
time_date = pd.to_datetime(time_ns, unit="ns")
|
|
438
|
+
time_date_str = time_date.strftime("%d-%m-%Y")
|
|
439
|
+
row_idx = t // num_columns
|
|
440
|
+
col_idx = t % num_columns
|
|
441
|
+
time_step.plot.imshow(
|
|
442
|
+
cmap=cmap,
|
|
443
|
+
norm=norm,
|
|
444
|
+
ax=axes[row_idx, col_idx], # Assign subplot
|
|
445
|
+
add_colorbar=False, # Avoid multiple colorbars
|
|
446
|
+
)
|
|
447
|
+
axes[row_idx, col_idx].set_aspect("auto") # keep aspect
|
|
448
|
+
axes[row_idx, col_idx].set_title(f"{time_date_str}")
|
|
449
|
+
|
|
450
|
+
plt.tight_layout()
|
|
451
|
+
plt.savefig(f"{wetland_name}_time_steps.png", dpi=72)
|
|
452
|
+
|
|
453
|
+
# Make a gif
|
|
454
|
+
|
|
455
|
+
# loop through each time step, creating and saving a frame
|
|
456
|
+
num_time_steps = ds.sizes["time"]
|
|
457
|
+
frames = []
|
|
458
|
+
|
|
459
|
+
for t in range(num_time_steps):
|
|
460
|
+
time_step = ds["wetland"].isel(time=t)
|
|
461
|
+
height, width = time_step.shape
|
|
462
|
+
fig, ax = plt.subplots(figsize=(8, 8 * height / width)) # dynamic aspect ratio
|
|
463
|
+
|
|
464
|
+
time_ns = ds["time"].isel(time=t).values.item()
|
|
465
|
+
time_date = pd.to_datetime(time_ns, unit="ns")
|
|
466
|
+
time_date_str = time_date.strftime("%d-%m-%Y")
|
|
467
|
+
|
|
468
|
+
time_step.plot.imshow(
|
|
469
|
+
cmap=cmap,
|
|
470
|
+
norm=norm,
|
|
471
|
+
ax=ax,
|
|
472
|
+
add_colorbar=False, # Avoid multiple colorbars
|
|
473
|
+
# interpolation='none'
|
|
474
|
+
)
|
|
475
|
+
ax.set_aspect("auto") # keep aspect
|
|
476
|
+
plt.title(f"Time:{time_date_str}")
|
|
477
|
+
|
|
478
|
+
# Rotates and right-aligns the x labels so they don't crowd each other.
|
|
479
|
+
for label in ax.get_xticklabels(which="major"):
|
|
480
|
+
label.set(rotation=30, horizontalalignment="right")
|
|
481
|
+
|
|
482
|
+
# Save the frame
|
|
483
|
+
frame_path = f"deawetlands_outputs/{wetland_name}_{time_date_str}.png"
|
|
484
|
+
plt.savefig(frame_path, bbox_inches="tight")
|
|
485
|
+
frames.append(frame_path)
|
|
486
|
+
plt.close(fig)
|
|
487
|
+
|
|
488
|
+
# make the gif
|
|
489
|
+
output_path = f"{wetland_name}_animation.gif"
|
|
490
|
+
with imageio.get_writer(output_path, mode="I", duration=0.7, loop=0) as writer:
|
|
491
|
+
for frame_path in frames:
|
|
492
|
+
image = imageio.imread(frame_path)
|
|
493
|
+
writer.append_data(image)
|
|
494
|
+
|
|
495
|
+
# print("GIF saved as 'wetland_animation.gif'")
|
|
496
|
+
|
|
497
|
+
# clean up
|
|
498
|
+
# for frame_path in frames:
|
|
499
|
+
# os.remove(frame_path)
|
|
500
|
+
# shutil.rmtree("frames")
|
|
501
|
+
|
|
502
|
+
return output_path
|
|
@@ -24,6 +24,7 @@ dea_tools/temporal.py
|
|
|
24
24
|
dea_tools/validation.py
|
|
25
25
|
dea_tools/waterbodies.py
|
|
26
26
|
dea_tools/wetlands.py
|
|
27
|
+
dea_tools/wit_app.py
|
|
27
28
|
dea_tools.egg-info/PKG-INFO
|
|
28
29
|
dea_tools.egg-info/SOURCES.txt
|
|
29
30
|
dea_tools.egg-info/dependency_links.txt
|
|
@@ -37,4 +38,5 @@ dea_tools/app/deacoastlines.py
|
|
|
37
38
|
dea_tools/app/geomedian.py
|
|
38
39
|
dea_tools/app/imageexport.py
|
|
39
40
|
dea_tools/app/miningrehab.py
|
|
41
|
+
dea_tools/app/wetlandsinsighttool.py
|
|
40
42
|
dea_tools/app/widgetconstructors.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|