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.
Files changed (44) hide show
  1. {dea_tools-0.3.6.dev41/dea_tools.egg-info → dea_tools-0.3.6.dev42}/PKG-INFO +1 -1
  2. dea_tools-0.3.6.dev42/dea_tools/app/wetlandsinsighttool.py +635 -0
  3. dea_tools-0.3.6.dev42/dea_tools/wit_app.py +502 -0
  4. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42/dea_tools.egg-info}/PKG-INFO +1 -1
  5. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/SOURCES.txt +2 -0
  6. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/.gitignore +0 -0
  7. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/LICENSE +0 -0
  8. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/MANIFEST.in +0 -0
  9. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/README.rst +0 -0
  10. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/__init__.py +0 -0
  11. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/__main__.py +0 -0
  12. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/__init__.py +0 -0
  13. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/animations.py +0 -0
  14. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/changefilmstrips.py +0 -0
  15. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/crophealth.py +0 -0
  16. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/deacoastlines.py +0 -0
  17. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/geomedian.py +0 -0
  18. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/imageexport.py +0 -0
  19. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/miningrehab.py +0 -0
  20. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/app/widgetconstructors.py +0 -0
  21. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/bandindices.py +0 -0
  22. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/bom.py +0 -0
  23. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/classification.py +0 -0
  24. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/climate.py +0 -0
  25. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/coastal.py +0 -0
  26. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/dask.py +0 -0
  27. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/datahandling.py +0 -0
  28. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/landcover.py +0 -0
  29. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/maps.py +0 -0
  30. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/plotting.py +0 -0
  31. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/pyfes_model.py +0 -0
  32. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/spatial.py +0 -0
  33. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/temporal.py +0 -0
  34. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/validation.py +0 -0
  35. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/waterbodies.py +0 -0
  36. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools/wetlands.py +0 -0
  37. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/dependency_links.txt +0 -0
  38. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/requires.txt +0 -0
  39. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/dea_tools.egg-info/top_level.txt +0 -0
  40. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/index.rst +0 -0
  41. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/mock_imports.txt +0 -0
  42. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/pyproject.toml +0 -0
  43. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/setup.cfg +0 -0
  44. {dea_tools-0.3.6.dev41 → dea_tools-0.3.6.dev42}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dea-tools
3
- Version: 0.3.6.dev41
3
+ Version: 0.3.6.dev42
4
4
  Summary: Functions and algorithms for analysing Digital Earth Australia data.
5
5
  Home-page: https://github.com/GeoscienceAustralia/dea-notebooks
6
6
  Author: Geoscience Australia
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dea-tools
3
- Version: 0.3.6.dev41
3
+ Version: 0.3.6.dev42
4
4
  Summary: Functions and algorithms for analysing Digital Earth Australia data.
5
5
  Home-page: https://github.com/GeoscienceAustralia/dea-notebooks
6
6
  Author: Geoscience Australia
@@ -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