first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.nc filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ipynb filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c65f38d91762e7167ef17d7cbdaf71547a323a9d485833e7acf62550674ea840
|
||||||
|
size 863658
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2ba3e38fac960f3049906d20a4d2255789415028b47a25c62fe4505bebccdddf
|
||||||
|
size 3375914
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5ae460f1a110627f8037f7daad1e781dd3128586d0dd6094ddf70f13354ee92c
|
||||||
|
size 1671089
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ff33ab950e3524342a99e70a02598ccb1ca57e60627c887c10b34466eabf3e87
|
||||||
|
size 3536032
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4a62b68a633d79c53a6fd8893e8ea42dcf2b9a8a3e907b1b9861661f04f21517
|
||||||
|
size 72
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d9702f45dddb21e62fa5f65fce514f1e6dd2632556758fe75cf50f227e0e9fcf
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2c842ea560305344c483d4b9dbbf117526655cc684e29d8135610f4e9e4b3969
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:86d72d0e70f63442e5e1979fdd48844890821b8d28e2ffa794bf5b5aafcf63a4
|
||||||
|
size 3066028
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
PROJCS["Transverse_Mercator",GEOGCS["GCS_WGS_1984",DATUM["D_unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563],TOWGS84[-192.873,-39.382,-111.202,-0.00205,-0.0005,0.00335,0.0188]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
PROJCS["Transverse_Mercator",GEOGCS["GCS_WGS_1984",DATUM["D_unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563],TOWGS84[-192.873,-39.382,-111.202,-0.00205,-0.0005,0.00335,0.0188]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2948384b4c43483675d366b75b7bcdceb63fe5a8ad7d9b7a84ee4a1018cfc826
|
||||||
|
size 55333632
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1d5a53bba7a6db30b7f80bc4250d09edacfd244e97d7993382927a9bde85c6f7
|
||||||
|
size 55738194
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
UTF-8
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
PROJCS["VN-2000_TM-3_105-30",GEOGCS["GCS_VN_2000",DATUM["D_Vietnam_2000",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",105.5],PARAMETER["Scale_Factor",0.9999],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||||
|
<qgis version="3.30.0-'s-Hertogenbosch">
|
||||||
|
<identifier></identifier>
|
||||||
|
<parentidentifier></parentidentifier>
|
||||||
|
<language></language>
|
||||||
|
<type></type>
|
||||||
|
<title></title>
|
||||||
|
<abstract></abstract>
|
||||||
|
<links/>
|
||||||
|
<dates/>
|
||||||
|
<fees></fees>
|
||||||
|
<encoding></encoding>
|
||||||
|
<crs>
|
||||||
|
<spatialrefsys nativeFormat="Wkt">
|
||||||
|
<wkt></wkt>
|
||||||
|
<proj4></proj4>
|
||||||
|
<srsid>0</srsid>
|
||||||
|
<srid>0</srid>
|
||||||
|
<authid></authid>
|
||||||
|
<description></description>
|
||||||
|
<projectionacronym></projectionacronym>
|
||||||
|
<ellipsoidacronym></ellipsoidacronym>
|
||||||
|
<geographicflag>false</geographicflag>
|
||||||
|
</spatialrefsys>
|
||||||
|
</crs>
|
||||||
|
<extent/>
|
||||||
|
</qgis>
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
|||||||
|
__locales__ = __path__[0] + '/locales'
|
||||||
|
|
||||||
|
|
||||||
|
def set_lang(lang=None):
|
||||||
|
if lang is None:
|
||||||
|
import os
|
||||||
|
os_lang = os.getenv('LANG')
|
||||||
|
|
||||||
|
# Just take the first 2 letters: 'fr' not 'fr_FR.UTF-8'
|
||||||
|
if os_lang is not None and len(os_lang) >=2:
|
||||||
|
lang = [os_lang[:2]]
|
||||||
|
else:
|
||||||
|
lang = [lang]
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
try:
|
||||||
|
translation = gettext.translation(
|
||||||
|
'deafrica_tools',
|
||||||
|
localedir=__locales__,
|
||||||
|
languages=lang,
|
||||||
|
fallback=True
|
||||||
|
)
|
||||||
|
translation.install()
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f'Could not load lang={lang}')
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,942 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Satellite imagery animation widget, which can be used to interactively
|
||||||
|
produce animations for multiple DE Africa products.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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 itertools
|
||||||
|
import numpy as np
|
||||||
|
import geopandas as gpd
|
||||||
|
from io import BytesIO
|
||||||
|
import ipywidgets as widgets
|
||||||
|
import datetime
|
||||||
|
from skimage import exposure
|
||||||
|
from skimage.filters import unsharp_mask
|
||||||
|
|
||||||
|
from datacube.utils import masking
|
||||||
|
from datacube.utils.geometry import Geometry
|
||||||
|
from datacube.utils.masking import mask_invalid_data
|
||||||
|
import deafrica_tools.app.widgetconstructors as deawidgets
|
||||||
|
from deafrica_tools.dask import create_local_dask_cluster
|
||||||
|
from deafrica_tools.spatial import reverse_geocode
|
||||||
|
from deafrica_tools.datahandling import pan_sharpen_brovey
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
|
||||||
|
# WMS params and satellite style bands
|
||||||
|
sat_params = {
|
||||||
|
"Landsat": {
|
||||||
|
"products": ["ls5_sr", "ls7_sr", "ls8_sr", "ls9_sr"],
|
||||||
|
"styles": {
|
||||||
|
"True colour": ("true_colour", ["red", "green", "blue"]),
|
||||||
|
"False colour": (
|
||||||
|
"false_colour",
|
||||||
|
["swir_1", "nir", "green"],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Sentinel-2": {
|
||||||
|
"products": ["s2_l2a"],
|
||||||
|
"styles": {
|
||||||
|
"True colour": ("simple_rgb", ["red", "green", "blue"]),
|
||||||
|
"False colour": (
|
||||||
|
"infrared_green",
|
||||||
|
["swir_2", "nir_1", "green"],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_map_layers(self):
|
||||||
|
"""
|
||||||
|
Updates map to add new DE Africa layers, styles or basemap when selected
|
||||||
|
using menu options. Triggers data reload by resetting load params
|
||||||
|
and output arrays.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# Clear all layers and add basemap
|
||||||
|
self.map_layers.clear_layers()
|
||||||
|
self.map_layers.add_layer(self.basemap)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_data(self):
|
||||||
|
|
||||||
|
# Connect to datacube database
|
||||||
|
dc = datacube.Datacube(app="Exporting satellite images")
|
||||||
|
|
||||||
|
# Configure local dask cluster
|
||||||
|
client = create_local_dask_cluster(return_client=True, display_client=True)
|
||||||
|
|
||||||
|
# Convert to geopolygon
|
||||||
|
geopolygon = Geometry(geom=self.gdf_drawn.geometry[0], crs=self.gdf_drawn.crs)
|
||||||
|
|
||||||
|
# Create query.
|
||||||
|
start_date = np.datetime64(self.start_date)
|
||||||
|
end_date = np.datetime64(self.end_date)
|
||||||
|
|
||||||
|
self.query_params = {
|
||||||
|
"time": (str(start_date), str(end_date)),
|
||||||
|
"geopolygon": geopolygon,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find matching datasets
|
||||||
|
dss = [
|
||||||
|
dc.find_datasets(product=i, **self.query_params)
|
||||||
|
for i in sat_params[self.dealayer]["products"]
|
||||||
|
]
|
||||||
|
dss = list(itertools.chain.from_iterable(dss))
|
||||||
|
|
||||||
|
# If data is found
|
||||||
|
if len(dss) > 0:
|
||||||
|
|
||||||
|
# Get CRS
|
||||||
|
crs = str(dss[0].crs)
|
||||||
|
|
||||||
|
self.load_params = {
|
||||||
|
"measurements": sat_params[self.dealayer]["styles"][self.style][1],
|
||||||
|
"resolution": (-self.resolution, self.resolution),
|
||||||
|
"output_crs": crs,
|
||||||
|
"group_by": "solar_day",
|
||||||
|
"dask_chunks": {"time": 1, "x": 2048, "y": 2048},
|
||||||
|
"resampling": {"*": "cubic", "oa_fmask": "nearest", "fmask": "nearest"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load data
|
||||||
|
from deafrica_tools.datahandling import load_ard
|
||||||
|
|
||||||
|
timeseries_ds = load_ard(
|
||||||
|
dc=dc,
|
||||||
|
products=sat_params[self.dealayer]["products"],
|
||||||
|
min_gooddata=1.0 - (self.max_cloud_cover / 100),
|
||||||
|
ls7_slc_off=False,
|
||||||
|
mask_pixel_quality=self.cloud_mask,
|
||||||
|
**self.load_params,
|
||||||
|
**self.query_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set invalid nodata pixels to NaN
|
||||||
|
timeseries_ds = mask_invalid_data(timeseries_ds)
|
||||||
|
|
||||||
|
# Else if no data is returned, return None
|
||||||
|
else:
|
||||||
|
timeseries_ds = None
|
||||||
|
|
||||||
|
# Close down the dask client
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
return timeseries_ds.compute()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_data(self, fname):
|
||||||
|
|
||||||
|
# Data to plot
|
||||||
|
to_plot = self.timeseries_ds
|
||||||
|
|
||||||
|
# If rolling median specified
|
||||||
|
if self.rolling_median:
|
||||||
|
with self.status_info:
|
||||||
|
print(
|
||||||
|
f"\nApplying rolling median ({self.rolling_median_window} timesteps window)"
|
||||||
|
)
|
||||||
|
to_plot = to_plot.rolling(
|
||||||
|
time=int(self.rolling_median_window), center=True, min_periods=1
|
||||||
|
).median()
|
||||||
|
|
||||||
|
# If resampling freq specified
|
||||||
|
if self.resample_freq:
|
||||||
|
with self.status_info:
|
||||||
|
print(f"\nResampling data to {self.resample_freq} frequency")
|
||||||
|
to_plot = to_plot.resample(time=self.resample_freq).median()
|
||||||
|
|
||||||
|
# Raise by power to dampen bright features and enhance dark.
|
||||||
|
# Raise vmin and vmax by same amount to ensure proper stretch
|
||||||
|
if self.power < 1.0:
|
||||||
|
with self.status_info:
|
||||||
|
print(f"\nApplying power transformation ({self.power})")
|
||||||
|
to_plot = to_plot ** self.power
|
||||||
|
|
||||||
|
# Apply unsharp masking to enhance overall dynamic range,
|
||||||
|
# and improve fine scale detail
|
||||||
|
if self.unsharp_mask:
|
||||||
|
with self.status_info:
|
||||||
|
print(
|
||||||
|
f"\nApplying unsharp masking with {self.unsharp_mask_radius} "
|
||||||
|
f"radius and {self.unsharp_mask_amount} amount"
|
||||||
|
)
|
||||||
|
from skimage.exposure import rescale_intensity
|
||||||
|
|
||||||
|
funcs_list = [
|
||||||
|
rescale_intensity,
|
||||||
|
lambda x: unsharp_mask(
|
||||||
|
x, radius=self.unsharp_mask_radius, amount=self.unsharp_mask_amount
|
||||||
|
),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
funcs_list = None
|
||||||
|
|
||||||
|
from deafrica_tools.plotting import xr_animation
|
||||||
|
|
||||||
|
xr_animation(
|
||||||
|
output_path=fname,
|
||||||
|
ds=to_plot.dropna(dim="time", how="all"),
|
||||||
|
show_text="",
|
||||||
|
bands=sat_params[self.dealayer]["styles"][self.style][1],
|
||||||
|
interval=self.interval,
|
||||||
|
width_pixels=self.width,
|
||||||
|
show_gdf=deacoastlines_overlay(to_plot) if self.deacoastlines else None,
|
||||||
|
gdf_kwargs={"linewidth": 3},
|
||||||
|
percentile_stretch=(self.vmin, self.vmax),
|
||||||
|
image_proc_funcs=funcs_list,
|
||||||
|
show_date="%Y" if self.resample_freq == "1Y" else "%b %Y",
|
||||||
|
annotation_kwargs={"fontsize": 75},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add plot preview below map and finish
|
||||||
|
plt.show()
|
||||||
|
with self.status_info:
|
||||||
|
print(f"\nImage successfully exported to:\n{fname}.")
|
||||||
|
|
||||||
|
|
||||||
|
def deacoastlines_overlay(ds):
|
||||||
|
|
||||||
|
import geopandas as gpd
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib
|
||||||
|
from shapely.geometry import box, Point
|
||||||
|
from deafrica_tools.coastal import get_coastlines
|
||||||
|
|
||||||
|
# Get bounding box of data
|
||||||
|
xmin, ymin, xmax, ymax = ds.geobox.geographic_extent.boundingbox
|
||||||
|
bounds = [xmin, ymin, xmax, ymax]
|
||||||
|
|
||||||
|
# Load data
|
||||||
|
deacl_gdf = get_coastlines(bbox=bounds)
|
||||||
|
|
||||||
|
# Clip to extent of satellite data
|
||||||
|
bbox = gpd.GeoDataFrame(geometry=[ds.geobox.extent.geom], crs=ds.geobox.crs)
|
||||||
|
deacl_gdf = gpd.overlay(deacl_gdf, bbox.to_crs(deacl_gdf.crs))
|
||||||
|
deacl_gdf = deacl_gdf.dissolve("year") # values("year", ascending=True)
|
||||||
|
|
||||||
|
# Apply colours
|
||||||
|
norm = matplotlib.colors.Normalize(vmin=0, vmax=len(deacl_gdf.index))
|
||||||
|
cmap = matplotlib.cm.get_cmap("inferno")
|
||||||
|
rgba = cmap(norm(deacl_gdf.reset_index().index))
|
||||||
|
deacl_gdf["color"] = list(rgba)
|
||||||
|
deacl_gdf["start_time"] = pd.to_datetime(deacl_gdf.index) + pd.DateOffset(months=0)
|
||||||
|
deacl_gdf = deacl_gdf.sort_index()
|
||||||
|
|
||||||
|
if len(deacl_gdf.index) > 0:
|
||||||
|
return deacl_gdf
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class animation_app(HBox):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
######################
|
||||||
|
# INITIAL ATTRIBUTES #
|
||||||
|
######################
|
||||||
|
|
||||||
|
# Basemap
|
||||||
|
self.basemap_list = [
|
||||||
|
("ESRI World Imagery", basemap_to_tiles(basemaps.Esri.WorldImagery)),
|
||||||
|
("Open Street Map", basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)),
|
||||||
|
]
|
||||||
|
self.basemap = self.basemap_list[0][1]
|
||||||
|
|
||||||
|
# Satellite data
|
||||||
|
end_date = datetime.datetime.today()
|
||||||
|
start_date = datetime.datetime(
|
||||||
|
year=end_date.year - 3, month=end_date.month, day=end_date.day
|
||||||
|
)
|
||||||
|
self.start_date = start_date.strftime("%Y-%m-%d")
|
||||||
|
self.end_date = end_date.strftime("%Y-%m-%d")
|
||||||
|
self.dealayer_list = [
|
||||||
|
("Landsat", "Landsat"),
|
||||||
|
("Sentinel-2", "Sentinel-2"),
|
||||||
|
]
|
||||||
|
self.dealayer = self.dealayer_list[0][1]
|
||||||
|
|
||||||
|
# Styles
|
||||||
|
self.styles_list = ["True colour", "False colour"]
|
||||||
|
self.style = self.styles_list[0]
|
||||||
|
|
||||||
|
# Analysis params
|
||||||
|
self.resolution = 30
|
||||||
|
self.vmin = 0.01
|
||||||
|
self.vmax = 0.99
|
||||||
|
self.power = 1.0
|
||||||
|
self.output_list = [("MP4", "mp4"), ("GIF", "gif")]
|
||||||
|
self.output_format = self.output_list[0][1]
|
||||||
|
self.rolling_median = False
|
||||||
|
self.rolling_median_window = 20
|
||||||
|
self.unsharp_mask = False
|
||||||
|
self.unsharp_mask_radius = 20
|
||||||
|
self.unsharp_mask_amount = 0.3
|
||||||
|
self.max_size = False
|
||||||
|
self.width = 900
|
||||||
|
self.interval = 100
|
||||||
|
self.cloud_mask = False
|
||||||
|
self.max_cloud_cover = 20
|
||||||
|
self.resample_list = [
|
||||||
|
("None", False),
|
||||||
|
("Monthly", "1M"),
|
||||||
|
("Quarterly", "Q-DEC"),
|
||||||
|
("Yearly", "1Y"),
|
||||||
|
]
|
||||||
|
self.resample_freq = self.resample_list[0][1]
|
||||||
|
self.deacoastlines = False
|
||||||
|
|
||||||
|
# Drawing params
|
||||||
|
self.target = None
|
||||||
|
self.action = None
|
||||||
|
self.gdf_drawn = None
|
||||||
|
|
||||||
|
# Data load params
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
##################
|
||||||
|
# HEADER FOR APP #
|
||||||
|
##################
|
||||||
|
|
||||||
|
# Create the Header widget
|
||||||
|
header_title_text = (
|
||||||
|
"<h3>Digital Earth Africa satellite imagery animations</h3>"
|
||||||
|
)
|
||||||
|
instruction_text = (
|
||||||
|
"<p>Select the desired satellite data, imagery date range "
|
||||||
|
"and image style, then zoom in and draw a rectangle to "
|
||||||
|
"select an area export as a satellite imagery time-series "
|
||||||
|
"animation.</p>"
|
||||||
|
)
|
||||||
|
self.header = deawidgets.create_html(f"{header_title_text}{instruction_text}")
|
||||||
|
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):
|
||||||
|
|
||||||
|
# Get data from action
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# 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_ha = 10000
|
||||||
|
area = gdf_drawn_nsidc.area.values[0] / m2_per_ha
|
||||||
|
polyarea_label = "Total area of satellite data to extract"
|
||||||
|
polyarea_text = f"<b>{polyarea_label}</b>: {area:.2f} ha</sup>"
|
||||||
|
|
||||||
|
# Test area size
|
||||||
|
if self.max_size:
|
||||||
|
confirmation_text = (
|
||||||
|
'<span style="color: #33cc33"> '
|
||||||
|
"<b>(Overriding maximum size limit; use with caution as may lead to memory issues)</b></span>"
|
||||||
|
)
|
||||||
|
self.header.value = (
|
||||||
|
header_title_text
|
||||||
|
+ instruction_text
|
||||||
|
+ polyarea_text
|
||||||
|
+ confirmation_text
|
||||||
|
)
|
||||||
|
self.gdf_drawn = gdf
|
||||||
|
elif area <= 50000:
|
||||||
|
confirmation_text = (
|
||||||
|
'<span style="color: #33cc33"> '
|
||||||
|
"<b>(Area to extract falls within "
|
||||||
|
"recommended 50000 ha limit)</b></span>"
|
||||||
|
)
|
||||||
|
self.header.value = (
|
||||||
|
header_title_text
|
||||||
|
+ instruction_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 an area less than 50000 "
|
||||||
|
"ha)</b></span>"
|
||||||
|
)
|
||||||
|
self.header.value = (
|
||||||
|
header_title_text + instruction_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 = ["rectangle"]
|
||||||
|
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
||||||
|
|
||||||
|
# Begin by displaying an empty layer group, and update the group with desired WMS on interaction.
|
||||||
|
self.map_layers = LayerGroup(layers=())
|
||||||
|
self.map_layers.name = "Map Overlays"
|
||||||
|
|
||||||
|
# Create map widget
|
||||||
|
self.m = deawidgets.create_map(map_center=(5.65, 26.17), zoom_level=13)
|
||||||
|
self.m.layout = make_box_layout()
|
||||||
|
|
||||||
|
# Add tools to map widget
|
||||||
|
self.m.add_control(draw_control)
|
||||||
|
self.m.add_layer(self.map_layers)
|
||||||
|
|
||||||
|
# Update all maps to starting defaults
|
||||||
|
update_map_layers(self)
|
||||||
|
|
||||||
|
############################
|
||||||
|
# WIDGETS FOR APP CONTROLS #
|
||||||
|
############################
|
||||||
|
|
||||||
|
# Create parameter widgets
|
||||||
|
dropdown_basemap = deawidgets.create_dropdown(
|
||||||
|
self.basemap_list, self.basemap_list[0][1]
|
||||||
|
)
|
||||||
|
dropdown_dealayer = deawidgets.create_dropdown(
|
||||||
|
self.dealayer_list, self.dealayer_list[0][1]
|
||||||
|
)
|
||||||
|
dropdown_output = deawidgets.create_dropdown(
|
||||||
|
self.output_list, self.output_list[0][1]
|
||||||
|
)
|
||||||
|
date_picker_start = deawidgets.create_datepicker(
|
||||||
|
value=start_date,
|
||||||
|
)
|
||||||
|
date_picker_end = deawidgets.create_datepicker(
|
||||||
|
value=end_date,
|
||||||
|
)
|
||||||
|
dropdown_styles = deawidgets.create_dropdown(
|
||||||
|
self.styles_list, self.styles_list[0]
|
||||||
|
)
|
||||||
|
slider_percentile = widgets.FloatRangeSlider(
|
||||||
|
value=[0.01, 0.99],
|
||||||
|
min=0,
|
||||||
|
max=1,
|
||||||
|
step=0.001,
|
||||||
|
description="",
|
||||||
|
layout={"width": "85%"},
|
||||||
|
)
|
||||||
|
run_button = create_expanded_button("Generate animation", "info")
|
||||||
|
|
||||||
|
floatslider_max_cloud_cover = widgets.IntSlider(
|
||||||
|
value=20,
|
||||||
|
min=0,
|
||||||
|
max=100,
|
||||||
|
step=1,
|
||||||
|
description="",
|
||||||
|
layout={"width": "85%"},
|
||||||
|
)
|
||||||
|
|
||||||
|
checkbox_rolling_median = deawidgets.create_checkbox(
|
||||||
|
self.rolling_median,
|
||||||
|
"Apply rolling median<br>to produce smooth, <br> cloud-free animations",
|
||||||
|
layout={"width": "90%",
|
||||||
|
"height": "4em"},
|
||||||
|
)
|
||||||
|
text_rolling_median_window = widgets.IntText(
|
||||||
|
value=20,
|
||||||
|
step=1,
|
||||||
|
description="</br>Rolling window (timesteps)",
|
||||||
|
layout={
|
||||||
|
"width": "85%",
|
||||||
|
"margin": "0px",
|
||||||
|
"padding": "0px",
|
||||||
|
"display": "none",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Expandable advanced section
|
||||||
|
text_interval = widgets.IntText(
|
||||||
|
value=100, description="", step=50, layout={"width": "95%"}
|
||||||
|
)
|
||||||
|
text_resolution = widgets.FloatText(
|
||||||
|
value=30,
|
||||||
|
description="",
|
||||||
|
layout={"width": "95%", "margin": "0px", "padding": "0px"},
|
||||||
|
)
|
||||||
|
text_width = widgets.IntText(
|
||||||
|
value=900, description="", step=50, layout={"width": "95%"}
|
||||||
|
)
|
||||||
|
dropdown_resampling = deawidgets.create_dropdown(
|
||||||
|
self.resample_list,
|
||||||
|
self.resample_freq,
|
||||||
|
description="",
|
||||||
|
layout={"width": "95%"},
|
||||||
|
)
|
||||||
|
checkbox_cloud_mask = deawidgets.create_checkbox(
|
||||||
|
self.cloud_mask, "Mask out cloudy <br> pixels", layout={"width": "95%", "height": "auto"}
|
||||||
|
)
|
||||||
|
slider_power = widgets.FloatSlider(
|
||||||
|
value=1.0,
|
||||||
|
min=0.01,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
description="",
|
||||||
|
layout={"width": "95%"},
|
||||||
|
)
|
||||||
|
checkbox_unsharp_mask = deawidgets.create_checkbox(
|
||||||
|
self.unsharp_mask, "Enable", layout={"width": "95%"}
|
||||||
|
)
|
||||||
|
text_unsharp_mask_radius = widgets.FloatText(
|
||||||
|
value=20,
|
||||||
|
step=1,
|
||||||
|
description="Radius",
|
||||||
|
layout={
|
||||||
|
"width": "95%",
|
||||||
|
"margin": "0px",
|
||||||
|
"padding": "0px",
|
||||||
|
"display": "none",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
text_unsharp_mask_amount = widgets.FloatText(
|
||||||
|
value=0.3,
|
||||||
|
step=0.1,
|
||||||
|
description="Amount",
|
||||||
|
layout={
|
||||||
|
"width": "95%",
|
||||||
|
"margin": "0px",
|
||||||
|
"padding": "0px",
|
||||||
|
"display": "none",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
checkbox_deacoastlines = deawidgets.create_checkbox(
|
||||||
|
self.deacoastlines, "Add DE Africa Coastlines overlay", layout={"width": "95%"}
|
||||||
|
)
|
||||||
|
checkbox_max_size = deawidgets.create_checkbox(
|
||||||
|
self.max_size, "Enable", layout={"width": "95%"}
|
||||||
|
)
|
||||||
|
expand_box = widgets.VBox(
|
||||||
|
[
|
||||||
|
HTML("Frame interval (milliseconds):"),
|
||||||
|
text_interval,
|
||||||
|
HTML("</br>Resolution (metres):"),
|
||||||
|
text_resolution,
|
||||||
|
HTML("</br>Width of output animation in pixels:"),
|
||||||
|
text_width,
|
||||||
|
HTML("</br>Apply temporal resampling:"),
|
||||||
|
dropdown_resampling,
|
||||||
|
HTML("</br>"),
|
||||||
|
checkbox_cloud_mask,
|
||||||
|
checkbox_deacoastlines,
|
||||||
|
HTML("</br>Apply power transformation to darken bright features:"),
|
||||||
|
slider_power,
|
||||||
|
HTML("</br>Apply unsharp masking to sharpen imagery:"),
|
||||||
|
checkbox_unsharp_mask,
|
||||||
|
text_unsharp_mask_radius,
|
||||||
|
text_unsharp_mask_amount,
|
||||||
|
HTML(
|
||||||
|
"</br>Override maximum size limit: (use with caution; may cause memory issues/crashes)"
|
||||||
|
),
|
||||||
|
checkbox_max_size,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
expand = widgets.Accordion(
|
||||||
|
children=[expand_box],
|
||||||
|
selected_index=None,
|
||||||
|
)
|
||||||
|
expand.set_title(0, "Advanced")
|
||||||
|
|
||||||
|
# Add specific dialogs to class so they can be modified
|
||||||
|
self.text_resolution = text_resolution
|
||||||
|
self.text_unsharp_mask_radius = text_unsharp_mask_radius
|
||||||
|
self.text_unsharp_mask_amount = text_unsharp_mask_amount
|
||||||
|
self.text_rolling_median_window = text_rolling_median_window
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# UPDATE FUNCTIONS FOR EACH WIDGET #
|
||||||
|
####################################
|
||||||
|
|
||||||
|
# Run update functions whenever various widgets are changed.
|
||||||
|
date_picker_start.observe(self.update_start_date, "value")
|
||||||
|
date_picker_end.observe(self.update_end_date, "value")
|
||||||
|
dropdown_basemap.observe(self.update_basemap, "value")
|
||||||
|
dropdown_dealayer.observe(self.update_dealayer, "value")
|
||||||
|
dropdown_styles.observe(self.update_styles, "value")
|
||||||
|
|
||||||
|
slider_percentile.observe(self.update_slider_percentile, "value")
|
||||||
|
floatslider_max_cloud_cover.observe(
|
||||||
|
self.update_floatslider_max_cloud_cover, "value"
|
||||||
|
)
|
||||||
|
checkbox_rolling_median.observe(self.update_checkbox_rolling_median, "value")
|
||||||
|
text_rolling_median_window.observe(
|
||||||
|
self.update_text_rolling_median_window, "value"
|
||||||
|
)
|
||||||
|
dropdown_output.observe(self.update_output, "value")
|
||||||
|
run_button.on_click(self.run_app)
|
||||||
|
draw_control.on_draw(update_geojson)
|
||||||
|
|
||||||
|
# Advanced params
|
||||||
|
text_resolution.observe(self.update_text_resolution, "value")
|
||||||
|
slider_power.observe(self.update_slider_power, "value")
|
||||||
|
text_width.observe(self.update_width, "value")
|
||||||
|
text_interval.observe(self.update_interval, "value")
|
||||||
|
dropdown_resampling.observe(self.update_dropdown_resampling, "value")
|
||||||
|
checkbox_cloud_mask.observe(self.update_checkbox_cloud_mask, "value")
|
||||||
|
checkbox_unsharp_mask.observe(self.update_checkbox_unsharp_mask, "value")
|
||||||
|
text_unsharp_mask_radius.observe(self.update_text_unsharp_mask_radius, "value")
|
||||||
|
text_unsharp_mask_amount.observe(self.update_text_unsharp_mask_amount, "value")
|
||||||
|
checkbox_deacoastlines.observe(self.update_deacoastlines, "value")
|
||||||
|
checkbox_max_size.observe(self.update_checkbox_max_size, "value")
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# COLLECTION OF ALL APP CONTROLS #
|
||||||
|
##################################
|
||||||
|
|
||||||
|
parameter_selection = VBox(
|
||||||
|
[
|
||||||
|
HTML("<b>Satellite imagery:</b>"),
|
||||||
|
dropdown_dealayer,
|
||||||
|
HTML("<b>Start date:</b>"),
|
||||||
|
date_picker_start,
|
||||||
|
HTML("<b>End date:</b>"),
|
||||||
|
date_picker_end,
|
||||||
|
HTML("<b>Style:</b>"),
|
||||||
|
dropdown_styles,
|
||||||
|
HTML("<b>Colour percentile stretch:</b>"),
|
||||||
|
slider_percentile,
|
||||||
|
HTML("<b>Maximum cloud cover (%):</b>"),
|
||||||
|
floatslider_max_cloud_cover,
|
||||||
|
checkbox_rolling_median,
|
||||||
|
text_rolling_median_window,
|
||||||
|
HTML("</br><b>Output file format:</b>"),
|
||||||
|
dropdown_output,
|
||||||
|
HTML("</br>"),
|
||||||
|
expand,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
map_selection = VBox(
|
||||||
|
[
|
||||||
|
HTML("</br><b>Map overlay:</b>"),
|
||||||
|
dropdown_basemap,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
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 | Figure/output |
|
||||||
|
# 8 | | |
|
||||||
|
# 9 | | |
|
||||||
|
# 10 | | |
|
||||||
|
# 11 ---------------------------------------------
|
||||||
|
|
||||||
|
# Create the layout #[rowspan, colspan]
|
||||||
|
grid = GridspecLayout(12, 10, height="1500px", 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:, 0:4] = self.status_info
|
||||||
|
grid[7:, 4:] = self.output_plot
|
||||||
|
|
||||||
|
# Display using HBox children attribute
|
||||||
|
self.children = [grid]
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
||||||
|
######################################
|
||||||
|
|
||||||
|
# Update date
|
||||||
|
def update_start_date(self, change):
|
||||||
|
self.start_date = str(change.new)
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# Update date
|
||||||
|
def update_end_date(self, change):
|
||||||
|
self.end_date = str(change.new)
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# Update colour stretch
|
||||||
|
def update_slider_percentile(self, change):
|
||||||
|
self.vmin, self.vmax = change.new
|
||||||
|
|
||||||
|
# Update power transform
|
||||||
|
def update_slider_power(self, change):
|
||||||
|
self.power = change.new
|
||||||
|
|
||||||
|
# Update good data slider
|
||||||
|
def update_floatslider_max_cloud_cover(self, change):
|
||||||
|
self.max_cloud_cover = change.new
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# Enable unsharp masking and show/hide custom params
|
||||||
|
def update_checkbox_unsharp_mask(self, change):
|
||||||
|
self.unsharp_mask = change.new
|
||||||
|
|
||||||
|
# Show unsharp masking params in menu if activated
|
||||||
|
if change.new:
|
||||||
|
self.text_unsharp_mask_radius.layout.display = "block"
|
||||||
|
self.text_unsharp_mask_amount.layout.display = "block"
|
||||||
|
else:
|
||||||
|
self.text_unsharp_mask_radius.layout.display = "none"
|
||||||
|
self.text_unsharp_mask_amount.layout.display = "none"
|
||||||
|
|
||||||
|
# Change unsharp masking radius
|
||||||
|
def update_text_unsharp_mask_radius(self, change):
|
||||||
|
self.unsharp_mask_radius = change.new
|
||||||
|
|
||||||
|
# Change unsharp masking amount
|
||||||
|
def update_text_unsharp_mask_amount(self, change):
|
||||||
|
self.unsharp_mask_amount = change.new
|
||||||
|
|
||||||
|
# Enable rolling median and show/hide custom params
|
||||||
|
def update_checkbox_rolling_median(self, change):
|
||||||
|
self.rolling_median = change.new
|
||||||
|
|
||||||
|
# Show rolling median params in menu if activated
|
||||||
|
if change.new:
|
||||||
|
self.text_rolling_median_window.layout.display = "block"
|
||||||
|
else:
|
||||||
|
self.text_rolling_median_window.layout.display = "none"
|
||||||
|
|
||||||
|
# Change rolling median window
|
||||||
|
def update_text_rolling_median_window(self, change):
|
||||||
|
self.rolling_median_window = change.new
|
||||||
|
|
||||||
|
# Override max size limit
|
||||||
|
def update_checkbox_max_size(self, change):
|
||||||
|
self.max_size = change.new
|
||||||
|
|
||||||
|
# Add DE Africa Coastlines overlay
|
||||||
|
def update_deacoastlines(self, change):
|
||||||
|
self.deacoastlines = change.new
|
||||||
|
|
||||||
|
# Apply cloud mask in load_ard
|
||||||
|
def update_checkbox_cloud_mask(self, change):
|
||||||
|
self.cloud_mask = change.new
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# Override min width
|
||||||
|
def update_width(self, change):
|
||||||
|
self.width = change.new
|
||||||
|
|
||||||
|
# Override interval
|
||||||
|
def update_interval(self, change):
|
||||||
|
self.interval = change.new
|
||||||
|
|
||||||
|
# Update resolution
|
||||||
|
def update_text_resolution(self, change):
|
||||||
|
self.resolution = change.new
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# Change layers shown on the map
|
||||||
|
def update_dealayer(self, change):
|
||||||
|
self.dealayer = change.new
|
||||||
|
|
||||||
|
if change.new == "Landsat":
|
||||||
|
self.text_resolution.value = 30
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.text_resolution.value = 10
|
||||||
|
|
||||||
|
# Update basemap
|
||||||
|
def update_basemap(self, change):
|
||||||
|
self.basemap = change.new
|
||||||
|
update_map_layers(self)
|
||||||
|
|
||||||
|
# Set imagery style
|
||||||
|
def update_styles(self, change):
|
||||||
|
self.style = change.new
|
||||||
|
|
||||||
|
# Clear data load params to trigger data re-load
|
||||||
|
self.timeseries_ds = None
|
||||||
|
self.load_params = None
|
||||||
|
self.query_params = None
|
||||||
|
|
||||||
|
# Set output file format
|
||||||
|
def update_output(self, change):
|
||||||
|
self.output_format = change.new
|
||||||
|
|
||||||
|
# Set output file format
|
||||||
|
def update_dropdown_resampling(self, change):
|
||||||
|
self.resample_freq = change.new
|
||||||
|
|
||||||
|
def run_app(self, change):
|
||||||
|
|
||||||
|
# Clear progress bar and output areas before running
|
||||||
|
self.status_info.clear_output()
|
||||||
|
self.output_plot.clear_output()
|
||||||
|
|
||||||
|
# Verify that polygon was drawn
|
||||||
|
if self.gdf_drawn is not None:
|
||||||
|
|
||||||
|
with self.status_info:
|
||||||
|
|
||||||
|
# Load data and add to attribute
|
||||||
|
if self.timeseries_ds is None:
|
||||||
|
self.timeseries_ds = extract_data(self)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Using previously loaded data")
|
||||||
|
|
||||||
|
if self.timeseries_ds is not None:
|
||||||
|
|
||||||
|
with self.status_info:
|
||||||
|
|
||||||
|
# Create unique file name
|
||||||
|
centre_coords = self.gdf_drawn.geometry[0].centroid.coords[0][::-1]
|
||||||
|
site = reverse_geocode(coords=centre_coords)
|
||||||
|
fname = (
|
||||||
|
f"{self.dealayer}_{site}_{self.start_date}_"
|
||||||
|
f"{self.end_date}_{self.style}_{self.resolution:.0f}m."
|
||||||
|
f"{self.output_format}".replace(" ", "")
|
||||||
|
.replace(",", "")
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"\nExporting animation for {site}.\nThis may take several minutes..."
|
||||||
|
)
|
||||||
|
|
||||||
|
############
|
||||||
|
# Plotting #
|
||||||
|
############
|
||||||
|
|
||||||
|
with self.output_plot:
|
||||||
|
plot_data(self, fname)
|
||||||
|
|
||||||
|
else:
|
||||||
|
with self.status_info:
|
||||||
|
print(
|
||||||
|
"No satellite data found in the selected area. "
|
||||||
|
"Please select a new rectangle over an area with "
|
||||||
|
"satellite imagery."
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
with self.status_info:
|
||||||
|
print(
|
||||||
|
'Please draw a valid rectangle on the map, then press "Generate animation".'
|
||||||
|
)
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
"""
|
||||||
|
Loading and interacting with data in the change filmstrips notebook,
|
||||||
|
inside the Real_world_examples folder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load modules
|
||||||
|
import os
|
||||||
|
import dask
|
||||||
|
import datacube
|
||||||
|
import warnings
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import xarray as xr
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from odc.algo import geomedian_with_mads
|
||||||
|
from odc.ui import select_on_a_map
|
||||||
|
from dask.utils import parse_bytes
|
||||||
|
from datacube.utils.geometry import CRS, assign_crs
|
||||||
|
from datacube.utils.rio import configure_s3_access
|
||||||
|
from datacube.utils.dask import start_local_dask
|
||||||
|
from ipyleaflet import basemaps, basemap_to_tiles
|
||||||
|
|
||||||
|
# Load utility functions
|
||||||
|
from deafrica_tools.datahandling import load_ard, mostcommon_crs
|
||||||
|
from deafrica_tools.dask import create_local_dask_cluster
|
||||||
|
|
||||||
|
|
||||||
|
def run_filmstrip_app(
|
||||||
|
output_name,
|
||||||
|
time_range,
|
||||||
|
time_step,
|
||||||
|
tide_range=(0.0, 1.0),
|
||||||
|
resolution=(-30, 30),
|
||||||
|
max_cloud=0.5,
|
||||||
|
ls7_slc_off=False,
|
||||||
|
size_limit=10000,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
An interactive app that allows the user to select a region from a
|
||||||
|
map, then load Digital Earth Africa Landsat data and combine it
|
||||||
|
using the geometric median ("geomedian") statistic to reveal the
|
||||||
|
median or 'typical' appearance of the landscape for a series of
|
||||||
|
time periods.
|
||||||
|
|
||||||
|
The results for each time period are combined into a 'filmstrip'
|
||||||
|
plot which visualises how the landscape has changed in appearance
|
||||||
|
across time, with a 'change heatmap' panel highlighting potential
|
||||||
|
areas of greatest change.
|
||||||
|
|
||||||
|
For coastal applications, the analysis can be customised to select
|
||||||
|
only satellite images obtained during a specific tidal range
|
||||||
|
(e.g. low, average or high tide).
|
||||||
|
|
||||||
|
Last modified: April 2020
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
output_name : str
|
||||||
|
A name that will be used to name the output filmstrip plot file.
|
||||||
|
time_range : tuple
|
||||||
|
A tuple giving the date range to analyse
|
||||||
|
(e.g. `time_range = ('1988-01-01', '2017-12-31')`).
|
||||||
|
time_step : dict
|
||||||
|
This parameter sets the length of the time periods to compare
|
||||||
|
(e.g. `time_step = {'years': 5}` will generate one filmstrip
|
||||||
|
plot for every five years of data; `time_step = {'months': 18}`
|
||||||
|
will generate one plot for each 18 month period etc. Time
|
||||||
|
periods are counted from the first value given in `time_range`.
|
||||||
|
tide_range : tuple, optional
|
||||||
|
An optional parameter that can be used to generate filmstrip
|
||||||
|
plots based on specific ocean tide conditions. This can be
|
||||||
|
valuable for analysing change consistently along the coast.
|
||||||
|
For example, `tide_range = (0.0, 0.2)` will select only
|
||||||
|
satellite images acquired at the lowest 20% of tides;
|
||||||
|
`tide_range = (0.8, 1.0)` will select images from the highest
|
||||||
|
20% of tides. The default is `tide_range = (0.0, 1.0)` which
|
||||||
|
will select all images regardless of tide.
|
||||||
|
resolution : tuple, optional
|
||||||
|
The spatial resolution to load data. The default is
|
||||||
|
`resolution = (-30, 30)`, which will load data at 30 m pixel
|
||||||
|
resolution. Increasing this (e.g. to `resolution = (-100, 100)`)
|
||||||
|
can be useful for loading large spatial extents.
|
||||||
|
max_cloud : float, optional
|
||||||
|
This parameter can be used to exclude satellite images with
|
||||||
|
excessive cloud. The default is `0.5`, which will keep all images
|
||||||
|
with less than 50% cloud.
|
||||||
|
ls7_slc_off : bool, optional
|
||||||
|
An optional boolean indicating whether to include data from
|
||||||
|
after the Landsat 7 SLC failure (i.e. SLC-off). Defaults to
|
||||||
|
False, which removes all Landsat 7 observations > May 31 2003.
|
||||||
|
size_limit : int, optional
|
||||||
|
An optional integer (in hectares) specifying the size limit
|
||||||
|
for the data query. Queries larger than this size will receive
|
||||||
|
a warning that he data query is too large (and may
|
||||||
|
therefore result in memory errors).
|
||||||
|
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ds_geomedian : xarray Dataset
|
||||||
|
An xarray dataset containing geomedian composites for each
|
||||||
|
timestep in the analysis.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Select and load data #
|
||||||
|
########################
|
||||||
|
|
||||||
|
# Define centre_coords as a global variable
|
||||||
|
global centre_coords
|
||||||
|
|
||||||
|
# Test if centre_coords is in the global namespace;
|
||||||
|
# use default value if it isn't
|
||||||
|
if "centre_coords" not in globals():
|
||||||
|
centre_coords = (6.587292, 1.532833)
|
||||||
|
|
||||||
|
# Plot interactive map to select area
|
||||||
|
basemap = basemap_to_tiles(basemaps.Esri.WorldImagery)
|
||||||
|
geopolygon = select_on_a_map(height="600px",
|
||||||
|
layers=(basemap,),
|
||||||
|
center=centre_coords,
|
||||||
|
zoom=14)
|
||||||
|
|
||||||
|
# Set centre coords based on most recent selection to re-focus
|
||||||
|
# subsequent data selections
|
||||||
|
centre_coords = geopolygon.centroid.points[0][::-1]
|
||||||
|
|
||||||
|
# Test size of selected area
|
||||||
|
msq_per_hectare = 10000
|
||||||
|
area = geopolygon.to_crs(crs=CRS("epsg:6933")).area / msq_per_hectare
|
||||||
|
radius = np.round(np.sqrt(size_limit), 1)
|
||||||
|
if area > size_limit:
|
||||||
|
print(f"Warning: Your selected area is {area:.00f} hectares. "
|
||||||
|
f"Please select an area of less than {size_limit} hectares."
|
||||||
|
f"\nTo select a smaller area, re-run the cell "
|
||||||
|
f"above and draw a new polygon.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
print("Starting analysis...")
|
||||||
|
|
||||||
|
# Connect to datacube database
|
||||||
|
dc = datacube.Datacube(app="Change_filmstrips")
|
||||||
|
|
||||||
|
# Configure local dask cluster
|
||||||
|
client = create_local_dask_cluster(return_client=True)
|
||||||
|
|
||||||
|
# Obtain native CRS
|
||||||
|
crs = mostcommon_crs(dc=dc,
|
||||||
|
product="ls8_sr",
|
||||||
|
query={
|
||||||
|
"time": "2014",
|
||||||
|
"geopolygon": geopolygon
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create query based on time range, area selected, custom params
|
||||||
|
query = {
|
||||||
|
"time": time_range,
|
||||||
|
"geopolygon": geopolygon,
|
||||||
|
"output_crs": crs,
|
||||||
|
"resolution": resolution,
|
||||||
|
"dask_chunks": {
|
||||||
|
"x": 3000,
|
||||||
|
"y": 3000
|
||||||
|
},
|
||||||
|
"align": (resolution[1] / 2.0, resolution[1] / 2.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load data from all three Landsats
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
ds = load_ard(
|
||||||
|
dc=dc,
|
||||||
|
measurements=["red", "green", "blue"],
|
||||||
|
products=["ls5_sr", "ls7_sr", "ls8_sr"],
|
||||||
|
min_gooddata=max_cloud,
|
||||||
|
ls7_slc_off=ls7_slc_off,
|
||||||
|
**query,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optionally calculate tides for each timestep in the satellite
|
||||||
|
# dataset and drop any observations out side this range
|
||||||
|
if tide_range != (0.0, 1.0):
|
||||||
|
from deafrica_tools.coastal import tidal_tag
|
||||||
|
ds = tidal_tag(ds=ds, tidepost_lat=None, tidepost_lon=None)
|
||||||
|
min_tide, max_tide = ds.tide_height.quantile(tide_range).values
|
||||||
|
ds = ds.sel(time=(ds.tide_height >= min_tide) &
|
||||||
|
(ds.tide_height <= max_tide))
|
||||||
|
ds = ds.drop("tide_height")
|
||||||
|
print(f" Keeping {len(ds.time)} observations with tides "
|
||||||
|
f"between {min_tide:.2f} and {max_tide:.2f} m")
|
||||||
|
|
||||||
|
# Create time step ranges to generate filmstrips from
|
||||||
|
bins_dt = pd.date_range(start=time_range[0],
|
||||||
|
end=time_range[1],
|
||||||
|
freq=pd.DateOffset(**time_step))
|
||||||
|
|
||||||
|
# Bin all satellite observations by timestep. If some observations
|
||||||
|
# fall outside the upper bin, label these with the highest bin
|
||||||
|
labels = bins_dt.astype("str")
|
||||||
|
time_steps = (pd.cut(ds.time.values, bins_dt,
|
||||||
|
labels=labels[:-1]).add_categories(
|
||||||
|
labels[-1]).fillna(labels[-1]))
|
||||||
|
|
||||||
|
time_steps_var = xr.DataArray(time_steps, [("time", ds.time.values)],
|
||||||
|
name="timestep")
|
||||||
|
|
||||||
|
# Resample data temporally into time steps, and compute geomedians
|
||||||
|
ds_geomedian = (ds.groupby(time_steps_var).apply(
|
||||||
|
lambda ds_subset: geomedian_with_mads(
|
||||||
|
ds_subset, compute_mads=False, compute_count=False)))
|
||||||
|
|
||||||
|
print("\nGenerating geomedian composites and plotting "
|
||||||
|
"filmstrips... (click the Dashboard link above for status)")
|
||||||
|
ds_geomedian = ds_geomedian.compute()
|
||||||
|
|
||||||
|
# Reset CRS that is lost during geomedian compositing
|
||||||
|
ds_geomedian = assign_crs(ds_geomedian, crs=ds.geobox.crs)
|
||||||
|
|
||||||
|
############
|
||||||
|
# Plotting #
|
||||||
|
############
|
||||||
|
|
||||||
|
# Convert to array and extract vmin/vmax
|
||||||
|
output_array = ds_geomedian[["red", "green", "blue"]].to_array()
|
||||||
|
percentiles = output_array.quantile(q=(0.02, 0.98)).values
|
||||||
|
|
||||||
|
# Create the plot with one subplot more than timesteps in the
|
||||||
|
# dataset. Figure width is set based on the number of subplots
|
||||||
|
# and aspect ratio
|
||||||
|
n_obs = output_array.sizes["timestep"]
|
||||||
|
ratio = output_array.sizes["x"] / output_array.sizes["y"]
|
||||||
|
fig, axes = plt.subplots(1,
|
||||||
|
n_obs + 1,
|
||||||
|
figsize=(5 * ratio * (n_obs + 1), 5))
|
||||||
|
fig.subplots_adjust(wspace=0.05, hspace=0.05)
|
||||||
|
|
||||||
|
# Add timesteps to the plot, set aspect to equal to preserve shape
|
||||||
|
for i, ax_i in enumerate(axes.flatten()[:n_obs]):
|
||||||
|
output_array.isel(timestep=i).plot.imshow(ax=ax_i,
|
||||||
|
vmin=percentiles[0],
|
||||||
|
vmax=percentiles[1])
|
||||||
|
ax_i.get_xaxis().set_visible(False)
|
||||||
|
ax_i.get_yaxis().set_visible(False)
|
||||||
|
ax_i.set_aspect("equal")
|
||||||
|
|
||||||
|
# Add change heatmap panel to final subplot. Heatmap is computed
|
||||||
|
# by first taking the log of the array (so change in dark areas
|
||||||
|
# can be identified), then computing standard deviation between
|
||||||
|
# all timesteps
|
||||||
|
(np.log(output_array).std(dim=["timestep"]).mean(
|
||||||
|
dim="variable").plot.imshow(ax=axes.flatten()[-1],
|
||||||
|
robust=True,
|
||||||
|
cmap="magma",
|
||||||
|
add_colorbar=False))
|
||||||
|
axes.flatten()[-1].get_xaxis().set_visible(False)
|
||||||
|
axes.flatten()[-1].get_yaxis().set_visible(False)
|
||||||
|
axes.flatten()[-1].set_aspect("equal")
|
||||||
|
axes.flatten()[-1].set_title("Change heatmap")
|
||||||
|
|
||||||
|
# Export to file
|
||||||
|
date_string = "_".join(time_range)
|
||||||
|
ts_v = list(time_step.values())[0]
|
||||||
|
ts_k = list(time_step.keys())[0]
|
||||||
|
fig.savefig(
|
||||||
|
f"filmstrip_{output_name}_{date_string}_{ts_v}{ts_k}.png",
|
||||||
|
dpi=150,
|
||||||
|
bbox_inches="tight",
|
||||||
|
pad_inches=0.1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# close dask client
|
||||||
|
client.shutdown()
|
||||||
|
|
||||||
|
return ds_geomedian
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
# crophealth.py
|
||||||
|
'''
|
||||||
|
Functions for loading and interacting with data in the crop health notebook,
|
||||||
|
inside the Real_world_examples folder.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Load modules
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
from ipyleaflet import (
|
||||||
|
Map,
|
||||||
|
GeoJSON,
|
||||||
|
DrawControl,
|
||||||
|
basemaps
|
||||||
|
)
|
||||||
|
import datetime as dt
|
||||||
|
import datacube
|
||||||
|
from osgeo import ogr
|
||||||
|
import matplotlib as mpl
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import rasterio
|
||||||
|
from rasterio.features import geometry_mask
|
||||||
|
import xarray as xr
|
||||||
|
from IPython.display import display
|
||||||
|
import warnings
|
||||||
|
import ipywidgets as widgets
|
||||||
|
import json
|
||||||
|
import geopandas as gpd
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
# Load utility functions
|
||||||
|
from deafrica_tools.datahandling import load_ard
|
||||||
|
from deafrica_tools.spatial import xr_rasterize
|
||||||
|
from deafrica_tools.bandindices import calculate_indices
|
||||||
|
|
||||||
|
|
||||||
|
def load_crophealth_data(lat, lon, buffer, date):
|
||||||
|
"""
|
||||||
|
Loads Sentinel-2 analysis-ready data (ARD) product for the crop health
|
||||||
|
case-study area over the last two years.
|
||||||
|
Last modified: April 2020
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
lat: float
|
||||||
|
The central latitude to analyse
|
||||||
|
lon: float
|
||||||
|
The central longitude to analyse
|
||||||
|
buffer:
|
||||||
|
The number of square degrees to load around the central latitude and longitude.
|
||||||
|
For reasonable loading times, set this as `0.1` or lower.
|
||||||
|
date:
|
||||||
|
The most recent date to show data for.
|
||||||
|
The app will automatically load all data available for the two years prior to this date.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
ds: xarray.Dataset
|
||||||
|
data set containing combined, masked data
|
||||||
|
Masked values are set to 'nan'
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Suppress warnings
|
||||||
|
warnings.filterwarnings('ignore')
|
||||||
|
|
||||||
|
# Initialise the data cube. 'app' argument is used to identify this app
|
||||||
|
dc = datacube.Datacube(app='Crophealth-app')
|
||||||
|
|
||||||
|
# Define area to load
|
||||||
|
latitude = (lat - buffer, lat + buffer)
|
||||||
|
longitude = (lon - buffer, lon + buffer)
|
||||||
|
|
||||||
|
# Specify the date range
|
||||||
|
# Calculated as today's date, subtract 730 days to collect two years of data
|
||||||
|
# Dates are converted to strings as required by loading function below
|
||||||
|
end_date = dt.datetime.strptime(date, "%Y-%m-%d")
|
||||||
|
start_date = end_date - dt.timedelta(days=730)
|
||||||
|
|
||||||
|
time = (start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"))
|
||||||
|
|
||||||
|
# Construct the data cube query
|
||||||
|
products = ["s2_l2a"]
|
||||||
|
|
||||||
|
query = {
|
||||||
|
'x': longitude,
|
||||||
|
'y': latitude,
|
||||||
|
'time': time,
|
||||||
|
'measurements': [
|
||||||
|
'red',
|
||||||
|
'green',
|
||||||
|
'blue',
|
||||||
|
'nir',
|
||||||
|
'swir_2'
|
||||||
|
],
|
||||||
|
'output_crs': 'EPSG:6933',
|
||||||
|
'resolution': (-20, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load the data and mask out bad quality pixels
|
||||||
|
ds = load_ard(dc, products=products, min_gooddata=0.5, **query)
|
||||||
|
|
||||||
|
# Calculate the normalised difference vegetation index (NDVI) across
|
||||||
|
# all pixels for each image.
|
||||||
|
# This is stored as an attribute of the data
|
||||||
|
ds = calculate_indices(ds, index='NDVI', satellite_mission='s2')
|
||||||
|
|
||||||
|
# Return the data
|
||||||
|
return(ds)
|
||||||
|
|
||||||
|
|
||||||
|
def run_crophealth_app(ds, lat, lon, buffer):
|
||||||
|
"""
|
||||||
|
Plots an interactive map of the crop health case-study area and allows
|
||||||
|
the user to draw polygons. This returns a plot of the average NDVI value
|
||||||
|
in the polygon area.
|
||||||
|
Last modified: January 2020
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds: xarray.Dataset
|
||||||
|
data set containing combined, masked data
|
||||||
|
Masked values are set to 'nan'
|
||||||
|
lat: float
|
||||||
|
The central latitude corresponding to the area of loaded ds
|
||||||
|
lon: float
|
||||||
|
The central longitude corresponding to the area of loaded ds
|
||||||
|
buffer:
|
||||||
|
The number of square degrees to load around the central latitude and longitude.
|
||||||
|
For reasonable loading times, set this as `0.1` or lower.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Suppress warnings
|
||||||
|
warnings.filterwarnings('ignore')
|
||||||
|
|
||||||
|
# Update plotting functionality through rcParams
|
||||||
|
mpl.rcParams.update({'figure.autolayout': True})
|
||||||
|
|
||||||
|
# Define polygon bounds
|
||||||
|
latitude = (lat - buffer, lat + buffer)
|
||||||
|
longitude = (lon - buffer, lon + buffer)
|
||||||
|
|
||||||
|
# Define the bounding box that will be overlayed on the interactive map
|
||||||
|
# The bounds are hard-coded to match those from the loaded data
|
||||||
|
geom_obj = {
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"style": {
|
||||||
|
"stroke": True,
|
||||||
|
"color": 'red',
|
||||||
|
"weight": 4,
|
||||||
|
"opacity": 0.8,
|
||||||
|
"fill": True,
|
||||||
|
"fillColor": False,
|
||||||
|
"fillOpacity": 0,
|
||||||
|
"showArea": True,
|
||||||
|
"clickable": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
longitude[0],
|
||||||
|
latitude[0]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
longitude[1],
|
||||||
|
latitude[0]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
longitude[1],
|
||||||
|
latitude[1]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
longitude[0],
|
||||||
|
latitude[1]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
longitude[0],
|
||||||
|
latitude[0]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a map geometry from the geom_obj dictionary
|
||||||
|
# center specifies where the background map view should focus on
|
||||||
|
# zoom specifies how zoomed in the background map should be
|
||||||
|
loadeddata_geometry = ogr.CreateGeometryFromJson(str(geom_obj['geometry']))
|
||||||
|
loadeddata_center = [
|
||||||
|
loadeddata_geometry.Centroid().GetY(),
|
||||||
|
loadeddata_geometry.Centroid().GetX()
|
||||||
|
]
|
||||||
|
loadeddata_zoom = 16
|
||||||
|
|
||||||
|
# define the study area map
|
||||||
|
studyarea_map = Map(
|
||||||
|
center=loadeddata_center,
|
||||||
|
zoom=loadeddata_zoom,
|
||||||
|
basemap=basemaps.Esri.WorldImagery
|
||||||
|
)
|
||||||
|
|
||||||
|
# define the drawing controls
|
||||||
|
studyarea_drawctrl = DrawControl(
|
||||||
|
polygon={"shapeOptions": {"fillOpacity": 0}},
|
||||||
|
marker={},
|
||||||
|
circle={},
|
||||||
|
circlemarker={},
|
||||||
|
polyline={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# add drawing controls and data bound geometry to the map
|
||||||
|
studyarea_map.add_control(studyarea_drawctrl)
|
||||||
|
studyarea_map.add_layer(GeoJSON(data=geom_obj))
|
||||||
|
|
||||||
|
# Index to count drawn polygons
|
||||||
|
polygon_number = 0
|
||||||
|
|
||||||
|
# Define widgets to interact with
|
||||||
|
instruction = widgets.Output(layout={'border': '1px solid black'})
|
||||||
|
with instruction:
|
||||||
|
print("Draw a polygon within the red box to view a plot of "
|
||||||
|
"average NDVI over time in that area.")
|
||||||
|
|
||||||
|
info = widgets.Output(layout={'border': '1px solid black'})
|
||||||
|
with info:
|
||||||
|
print("Plot status:")
|
||||||
|
|
||||||
|
fig_display = widgets.Output(layout=widgets.Layout(
|
||||||
|
width="50%", # proportion of horizontal space taken by plot
|
||||||
|
))
|
||||||
|
|
||||||
|
with fig_display:
|
||||||
|
plt.ioff()
|
||||||
|
fig, ax = plt.subplots(figsize=(8, 6))
|
||||||
|
ax.set_ylim([0, 1])
|
||||||
|
|
||||||
|
colour_list = plt.rcParams['axes.prop_cycle'].by_key()['color']
|
||||||
|
|
||||||
|
# Function to execute each time something is drawn on the map
|
||||||
|
def handle_draw(self, action, geo_json):
|
||||||
|
nonlocal polygon_number
|
||||||
|
|
||||||
|
# Execute behaviour based on what the user draws
|
||||||
|
if geo_json['geometry']['type'] == 'Polygon':
|
||||||
|
|
||||||
|
info.clear_output(wait=True) # wait=True reduces flicker effect
|
||||||
|
|
||||||
|
# Save geojson polygon to io temporary file to be rasterized later
|
||||||
|
jsonData = json.dumps(geo_json)
|
||||||
|
binaryData = jsonData.encode()
|
||||||
|
io = BytesIO(binaryData)
|
||||||
|
io.seek(0)
|
||||||
|
|
||||||
|
# Read the polygon as a geopandas dataframe
|
||||||
|
gdf = gpd.read_file(io)
|
||||||
|
gdf.crs = "EPSG:4326"
|
||||||
|
|
||||||
|
# Convert the drawn geometry to pixel coordinates
|
||||||
|
xr_poly = xr_rasterize(gdf, ds.NDVI.isel(time=0), crs='EPSG:6933')
|
||||||
|
|
||||||
|
# Construct a mask to only select pixels within the drawn polygon
|
||||||
|
masked_ds = ds.NDVI.where(xr_poly)
|
||||||
|
|
||||||
|
masked_ds_mean = masked_ds.mean(dim=['x', 'y'], skipna=True)
|
||||||
|
colour = colour_list[polygon_number % len(colour_list)]
|
||||||
|
|
||||||
|
# Add a layer to the map to make the most recently drawn polygon
|
||||||
|
# the same colour as the line on the plot
|
||||||
|
studyarea_map.add_layer(
|
||||||
|
GeoJSON(
|
||||||
|
data=geo_json,
|
||||||
|
style={
|
||||||
|
'color': colour,
|
||||||
|
'opacity': 1,
|
||||||
|
'weight': 4.5,
|
||||||
|
'fillOpacity': 0.0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# add new data to the plot
|
||||||
|
xr.plot.plot(
|
||||||
|
masked_ds_mean,
|
||||||
|
marker='*',
|
||||||
|
color=colour,
|
||||||
|
ax=ax
|
||||||
|
)
|
||||||
|
|
||||||
|
# reset titles back to custom
|
||||||
|
ax.set_title("Average NDVI from Sentinel-2")
|
||||||
|
ax.set_xlabel("Date")
|
||||||
|
ax.set_ylabel("NDVI")
|
||||||
|
|
||||||
|
# refresh display
|
||||||
|
fig_display.clear_output(wait=True) # wait=True reduces flicker effect
|
||||||
|
with fig_display:
|
||||||
|
display(fig)
|
||||||
|
|
||||||
|
with info:
|
||||||
|
print("Plot status: polygon sucessfully added to plot.")
|
||||||
|
|
||||||
|
# Iterate the polygon number before drawing another polygon
|
||||||
|
polygon_number = polygon_number + 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
info.clear_output(wait=True)
|
||||||
|
with info:
|
||||||
|
print("Plot status: this drawing tool is not currently "
|
||||||
|
"supported. Please use the polygon tool.")
|
||||||
|
|
||||||
|
# call to say activate handle_draw function on draw
|
||||||
|
studyarea_drawctrl.on_draw(handle_draw)
|
||||||
|
|
||||||
|
with fig_display:
|
||||||
|
# TODO: update with user friendly something
|
||||||
|
display(widgets.HTML(""))
|
||||||
|
|
||||||
|
# Construct UI:
|
||||||
|
# +-----------------------+
|
||||||
|
# | instruction |
|
||||||
|
# +-----------+-----------+
|
||||||
|
# | map | plot |
|
||||||
|
# | | |
|
||||||
|
# +-----------+-----------+
|
||||||
|
# | info |
|
||||||
|
# +-----------------------+
|
||||||
|
ui = widgets.VBox([instruction,
|
||||||
|
widgets.HBox([studyarea_map, fig_display]),
|
||||||
|
info])
|
||||||
|
display(ui)
|
||||||
@@ -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')
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
Geomedian widget: generates an interactive visualisation of
|
||||||
|
the geomedian summary statistic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load modules
|
||||||
|
import ipywidgets as widgets
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from mpl_toolkits.mplot3d import Axes3D
|
||||||
|
import numpy as np
|
||||||
|
import xarray as xr
|
||||||
|
from odc.algo import xr_geomedian
|
||||||
|
|
||||||
|
def run_app():
|
||||||
|
|
||||||
|
"""
|
||||||
|
An interactive app that allows users to visualise the difference between the median and geomedian time-series summary statistics. By modifying the red-green-blue values of three timesteps for a given pixel, the user changes the output summary statistics.
|
||||||
|
|
||||||
|
This allows a visual representation of the difference through the output values, RGB colour, as well as showing values plotted as a vector on a 3-dimensional space.
|
||||||
|
|
||||||
|
Last modified: December 2021
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the red-green-blue sliders for timestep 1
|
||||||
|
p1r = widgets.IntSlider(description='Red', max=255, value=58)
|
||||||
|
p1g = widgets.IntSlider(description='Green', max=255, value=153)
|
||||||
|
p1b = widgets.IntSlider(description='Blue', max=255, value=68)
|
||||||
|
|
||||||
|
# Define the red-green-blue sliders for timestep 2
|
||||||
|
p2r = widgets.IntSlider(description='Red', max=255, value=208)
|
||||||
|
p2g = widgets.IntSlider(description='Green', max=255, value=221)
|
||||||
|
p2b = widgets.IntSlider(description='Blue', max=255, value=203)
|
||||||
|
|
||||||
|
# Define the red-green-blue sliders for timestep 3
|
||||||
|
p3r = widgets.IntSlider(description='Red', max=255, value=202)
|
||||||
|
p3g = widgets.IntSlider(description='Green', max=255, value=82)
|
||||||
|
p3b = widgets.IntSlider(description='Blue', max=255, value=33)
|
||||||
|
|
||||||
|
# Define the median calculation for the timesteps
|
||||||
|
def f(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||||
|
print('Red Median = {}'.format(np.median([p1r, p2r, p3r])))
|
||||||
|
print('Green Median = {}'.format(np.median([p1g, p2g, p3g])))
|
||||||
|
print('Blue Median = {}'.format(np.median([p1b, p2b, p3b])))
|
||||||
|
|
||||||
|
# Define the geomedian calculation for the timesteps
|
||||||
|
def g(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||||
|
print('Red Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0]))
|
||||||
|
print('Green Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0]))
|
||||||
|
print('Blue Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0]))
|
||||||
|
|
||||||
|
# Define the Timestep 1 box colour
|
||||||
|
def h(p1r, p1g, p1b):
|
||||||
|
fig1, axes1 = plt.subplots(figsize=(2,2))
|
||||||
|
fig1 = plt.imshow([[(p1r, p1g, p1b)]])
|
||||||
|
axes1.set_title('Timestep 1')
|
||||||
|
axes1.axis('off')
|
||||||
|
plt.show(fig1)
|
||||||
|
|
||||||
|
# Define the Timestep 2 box colour
|
||||||
|
def hh(p2r, p2g, p2b):
|
||||||
|
fig2, axes2 = plt.subplots(figsize=(2,2))
|
||||||
|
fig2 = plt.imshow([[(p2r, p2g, p2b)]])
|
||||||
|
axes2.set_title('Timestep 2')
|
||||||
|
axes2.axis('off')
|
||||||
|
plt.show(fig2)
|
||||||
|
|
||||||
|
# Define the Timestep 3 box colour
|
||||||
|
def hhh(p3r, p3g, p3b):
|
||||||
|
fig3, axes3 = plt.subplots(figsize=(2,2))
|
||||||
|
fig3 = plt.imshow([[(p3r, p3g, p3b)]])
|
||||||
|
axes3.set_title('Timestep 3')
|
||||||
|
axes3.axis('off')
|
||||||
|
plt.show(fig3)
|
||||||
|
|
||||||
|
# Define the Median RGB colour box
|
||||||
|
def i(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||||
|
fig4, axes4 = plt.subplots(figsize=(3,3))
|
||||||
|
fig4 = plt.imshow([[(int(np.median([p1r, p2r, p3r])), int(np.median([p1g, p2g, p3g])), int(np.median([p1b, p2b, p3b])))]])
|
||||||
|
axes4.set_title('Median RGB - All timesteps')
|
||||||
|
axes4.axis('off')
|
||||||
|
plt.show(fig4)
|
||||||
|
|
||||||
|
# Define the Geomedian RGB colour box
|
||||||
|
def ii(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||||
|
fig5, axes5 = plt.subplots(figsize=(3,3))
|
||||||
|
fig5 = plt.imshow([[(int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0]), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0]), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0]))]])
|
||||||
|
axes5.set_title('Geomedian RGB - All timesteps')
|
||||||
|
axes5.axis('off')
|
||||||
|
plt.show(fig5)
|
||||||
|
|
||||||
|
# Define 3-D axis to display vectors on
|
||||||
|
def j(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||||
|
fig6 = plt.figure()
|
||||||
|
axes6 = fig6.add_subplot(111, projection='3d')
|
||||||
|
x = [p1r, p2r, p3r, int(np.median([p1r, p2r, p3r])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0])]
|
||||||
|
y = [p1g, p2g, p3g, int(np.median([p1g, p2g, p3g])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0])]
|
||||||
|
z = [p1b, p2b, p3b, int(np.median([p1b, p2b, p3b])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0])]
|
||||||
|
labels = [' 1', ' 2', ' 3', ' median', ' geomedian']
|
||||||
|
axes6.scatter(x, y, z, c=['black','black','black','r', 'blue'], marker='o')
|
||||||
|
axes6.set_xlabel('Red')
|
||||||
|
axes6.set_ylabel('Green')
|
||||||
|
axes6.set_zlabel('Blue')
|
||||||
|
axes6.set_xlim3d(0, 255)
|
||||||
|
axes6.set_ylim3d(0, 255)
|
||||||
|
axes6.set_zlim3d(0, 255)
|
||||||
|
for ax, ay, az, label in zip(x, y, z, labels):
|
||||||
|
axes6.text(ax, ay, az, label)
|
||||||
|
plt.title('Each band represents a dimension.')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# Define outputs
|
||||||
|
outf = widgets.interactive_output(f, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||||
|
outg = widgets.interactive_output(g, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||||
|
|
||||||
|
outh = widgets.interactive_output(h, {'p1r': p1r, 'p1g': p1g, 'p1b': p1b})
|
||||||
|
outhh = widgets.interactive_output(hh, {'p2r': p2r, 'p2g': p2g, 'p2b': p2b})
|
||||||
|
outhhh = widgets.interactive_output(hhh, {'p3r': p3r, 'p3g': p3g, 'p3b': p3b})
|
||||||
|
|
||||||
|
outi = widgets.interactive_output(i, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||||
|
outii = widgets.interactive_output(ii, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||||
|
|
||||||
|
outj = widgets.interactive_output(j, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||||
|
|
||||||
|
app_output = widgets.HBox([widgets.VBox([widgets.HBox([outh, widgets.VBox([ p1r, p1g, p1b])]), widgets.HBox([outhh, widgets.VBox([p2r, p2g, p2b])]), widgets.HBox([outhhh, widgets.VBox([ p3r, p3g, p3b])])]), widgets.VBox([widgets.HBox([widgets.VBox([outf, outi]), widgets.VBox([outg, outii])]), outj])])
|
||||||
|
|
||||||
|
return app_output
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
"""
|
||||||
|
Create an interactive map for selecting satellite imagery and exporting image files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load modules
|
||||||
|
import datacube
|
||||||
|
import itertools
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from odc.ui import select_on_a_map
|
||||||
|
from datacube.utils.geometry import CRS
|
||||||
|
from datacube.utils import masking
|
||||||
|
from skimage import exposure
|
||||||
|
from ipyleaflet import (WMSLayer, basemaps, basemap_to_tiles)
|
||||||
|
from traitlets import Unicode
|
||||||
|
|
||||||
|
from deafrica_tools.spatial import reverse_geocode
|
||||||
|
from deafrica_tools.dask import create_local_dask_cluster
|
||||||
|
|
||||||
|
|
||||||
|
def select_region_app(date,
|
||||||
|
satellites,
|
||||||
|
size_limit=10000):
|
||||||
|
"""
|
||||||
|
An interactive app that allows the user to select a region from a
|
||||||
|
map using imagery from Sentinel-2 and Landsat. The output of this
|
||||||
|
function is used as the input to :func:`export_image_app` to export high-
|
||||||
|
resolution satellite images.
|
||||||
|
|
||||||
|
Last modified: September 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
date : str
|
||||||
|
The exact date used to plot imagery on the interactive map
|
||||||
|
(e.g. ``date='1988-01-01'``).
|
||||||
|
satellites : str
|
||||||
|
The satellite data to plot on the interactive map. The
|
||||||
|
following options are supported:
|
||||||
|
|
||||||
|
``'Landsat-9'``: data from the Landsat 9 satellite
|
||||||
|
``'Landsat-8'``: data from the Landsat 8 satellite
|
||||||
|
``'Landsat-7'``: data from the Landsat 7 satellite
|
||||||
|
``'Landsat-5'``: data from the Landsat 5 satellite
|
||||||
|
``'Sentinel-2'``: data from Sentinel-2A and Sentinel-2B
|
||||||
|
``'Sentinel-2 geomedian'``: data from the Sentinel-2 annual geomedian
|
||||||
|
|
||||||
|
size_limit : int, optional
|
||||||
|
An optional size limit for the area selection in sq km.
|
||||||
|
Defaults to 10000 sq km.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
A dictionary containing:
|
||||||
|
|
||||||
|
* 'geopolygon' (defining the area to export imagery from),
|
||||||
|
* 'date' (date used to export imagery), and
|
||||||
|
* 'satellites' (the satellites from which to extract imagery).
|
||||||
|
|
||||||
|
These are passed to the :func:`export_image_app` function to export the image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Select and load data #
|
||||||
|
########################
|
||||||
|
|
||||||
|
# Load DEA WMS
|
||||||
|
class TimeWMSLayer(WMSLayer):
|
||||||
|
time = Unicode('').tag(sync=True, o=True)
|
||||||
|
|
||||||
|
# WMS layers
|
||||||
|
wms_params = {
|
||||||
|
'Landsat-9': 'ls9_sr',
|
||||||
|
'Landsat-8': 'ls8_sr',
|
||||||
|
'Landsat-7': 'ls7_sr',
|
||||||
|
'Landsat-5': 'ls5_sr',
|
||||||
|
'Sentinel-2': 's2_l2a',
|
||||||
|
'Sentinel-2 geomedian': 'gm_s2_annual'
|
||||||
|
}
|
||||||
|
|
||||||
|
time_wms = TimeWMSLayer(url='https://ows.digitalearth.africa/',
|
||||||
|
layers=wms_params[satellites],
|
||||||
|
time=date,
|
||||||
|
format='image/png',
|
||||||
|
transparent=True,
|
||||||
|
attribution='Digital Earth Africa')
|
||||||
|
|
||||||
|
# Plot interactive map to select area
|
||||||
|
basemap = basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)
|
||||||
|
geopolygon = select_on_a_map(height='1000px',
|
||||||
|
layers=(
|
||||||
|
basemap,
|
||||||
|
time_wms,
|
||||||
|
),
|
||||||
|
center=(4, 20),
|
||||||
|
zoom=4)
|
||||||
|
|
||||||
|
# Test size of selected area
|
||||||
|
area = geopolygon.to_crs(crs=CRS('epsg:6933')).area / 1000000
|
||||||
|
if area > size_limit:
|
||||||
|
print(f'Warning: Your selected area is {area:.00f} sq km. '
|
||||||
|
f'Please select an area of less than {size_limit} sq km.'
|
||||||
|
f'\nTo select a smaller area, re-run the cell '
|
||||||
|
f'above and draw a new polygon.')
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {'geopolygon': geopolygon,
|
||||||
|
'date': date,
|
||||||
|
'satellites': satellites}
|
||||||
|
|
||||||
|
|
||||||
|
def export_image_app(geopolygon,
|
||||||
|
date,
|
||||||
|
satellites,
|
||||||
|
style='True colour',
|
||||||
|
resolution=None,
|
||||||
|
vmin=0,
|
||||||
|
vmax=2000,
|
||||||
|
percentile_stretch=None,
|
||||||
|
power=None,
|
||||||
|
image_proc_funcs=None,
|
||||||
|
output_format="jpg",
|
||||||
|
standardise_name=False):
|
||||||
|
"""
|
||||||
|
Exports Digital Earth Africa satellite data as an image file
|
||||||
|
based on the extent and time period selected using
|
||||||
|
:func:`select_region_app`. The function supports Sentinel-2 and Landsat
|
||||||
|
data, creating True and False colour images.
|
||||||
|
|
||||||
|
By default, files are named using:
|
||||||
|
|
||||||
|
``"<product> - <YYYY-MM-DD> - <site, state> - <description>.png"``
|
||||||
|
|
||||||
|
Set ``standardise_name=True`` for a machine-readable name:
|
||||||
|
|
||||||
|
``"<product>_<YYYY-MM-DD>_<site-state>_<description>.png"``
|
||||||
|
|
||||||
|
Last modified: September 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
geopolygon : datacube.utils.geometry object
|
||||||
|
A datacube geopolygon providing the spatial bounds used to load
|
||||||
|
satellite data.
|
||||||
|
date : str
|
||||||
|
The exact date used to extract imagery
|
||||||
|
(e.g. `date='1988-01-01'`).
|
||||||
|
satellites : str
|
||||||
|
The satellite data to be used to extract imagery. The
|
||||||
|
following options are supported:
|
||||||
|
|
||||||
|
``'Landsat-9'``: data from the Landsat 9 satellite
|
||||||
|
``'Landsat-8'``: data from the Landsat 8 satellite
|
||||||
|
``'Landsat-7'``: data from the Landsat 7 satellite
|
||||||
|
``'Landsat-5'``: data from the Landsat 5 satellite
|
||||||
|
``'Sentinel-2'``: data from Sentinel-2A and Sentinel-2B
|
||||||
|
``'Sentinel-2 geomedian'``: data from the Sentinel-2 annual geomedian
|
||||||
|
|
||||||
|
style : str, optional
|
||||||
|
The style used to produce the image. Two options are currently
|
||||||
|
supported:
|
||||||
|
|
||||||
|
* ``'True colour'``: Creates a true colour image using the red,
|
||||||
|
green and blue satellite bands
|
||||||
|
* ``'False colour'``: Creates a false colour image using
|
||||||
|
short-wave infrared, infrared and green satellite bands.
|
||||||
|
The specific bands used vary between Landsat and Sentinel-2.
|
||||||
|
|
||||||
|
resolution : tuple, optional
|
||||||
|
The spatial resolution to load data. By default, the tool will
|
||||||
|
automatically set the best possible resolution depending on the
|
||||||
|
satellites selected (i.e 30 m for Landsat, 10 m for Sentinel-2).
|
||||||
|
Increasing this (e.g. to ``resolution=(-100, 100)``) can be useful
|
||||||
|
for loading large spatial extents.
|
||||||
|
vmin, vmax : int or float
|
||||||
|
The minimum and maximum surface reflectance values used to
|
||||||
|
clip the resulting imagery to enhance contrast.
|
||||||
|
percentile_stretch : tuple of floats, optional
|
||||||
|
An tuple of two floats (between 0.00 and 1.00) that can be used
|
||||||
|
to clip the imagery to based on percentiles to get more control
|
||||||
|
over the brightness and contrast of the image. The default is
|
||||||
|
``None``; ``(0.02, 0.98)`` is equivelent to ``robust=True``. If this
|
||||||
|
parameter is used, ``vmin`` and ``vmax`` will have no effect.
|
||||||
|
power : float, optional
|
||||||
|
Raises imagery by a power to reduce bright features and
|
||||||
|
enhance dark features. This can add extra definition over areas
|
||||||
|
with extremely bright features like snow, beaches or salt pans.
|
||||||
|
image_proc_funcs : list of funcs, optional
|
||||||
|
An optional list containing functions that will be applied to
|
||||||
|
the output image. This can include image processing functions
|
||||||
|
such as increasing contrast, unsharp masking, saturation etc.
|
||||||
|
The function should take AND return a `numpy.ndarray` with
|
||||||
|
shape ``[y, x, bands]``. If your function has parameters, you
|
||||||
|
can pass in custom values using a lambda function, e.g.:
|
||||||
|
``[lambda x: skimage.filters.unsharp_mask(x, radius=5, amount=0.2)]``
|
||||||
|
output_format : str, optional
|
||||||
|
The output file format of the image. Valid options include ``'jpg'``
|
||||||
|
and ``'png'``. Defaults to ``'jpg'``.
|
||||||
|
standardise_name : bool, optional
|
||||||
|
Whether to export the image file with a machine-readable
|
||||||
|
file name (e.g. ``<product>_<YYYY-MM-DD>_<site-state>_<description>.png``)
|
||||||
|
"""
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# Set up satellite params #
|
||||||
|
###########################
|
||||||
|
|
||||||
|
sat_params = {
|
||||||
|
'Landsat-9': {
|
||||||
|
'products': ['ls9_sr'],
|
||||||
|
'resolution': [-30, 30],
|
||||||
|
'styles': {
|
||||||
|
'True colour': ['red', 'green', 'blue'],
|
||||||
|
'False colour': ['swir_1', 'nir', 'green']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Landsat-8': {
|
||||||
|
'products': ['ls8_sr'],
|
||||||
|
'resolution': [-30, 30],
|
||||||
|
'styles': {
|
||||||
|
'True colour': ['red', 'green', 'blue'],
|
||||||
|
'False colour': ['swir_1', 'nir', 'green']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Landsat-7': {
|
||||||
|
'products': ['ls7_sr'],
|
||||||
|
'resolution': [-30, 30],
|
||||||
|
'styles': {
|
||||||
|
'True colour': ['red', 'green', 'blue'],
|
||||||
|
'False colour': ['swir_1', 'nir', 'green']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Landsat-5': {
|
||||||
|
'products': ['ls5_sr'],
|
||||||
|
'resolution': [-30, 30],
|
||||||
|
'styles': {
|
||||||
|
'True colour': ['red', 'green', 'blue'],
|
||||||
|
'False colour': ['swir_1', 'nir', 'green']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Sentinel-2': {
|
||||||
|
'products': ['s2_l2a'],
|
||||||
|
'resolution': [-10, 10],
|
||||||
|
'styles': {
|
||||||
|
'True colour': ['red', 'green', 'blue'],
|
||||||
|
'False colour': ['swir_2', 'nir_1', 'green']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Sentinel-2 geomedian': {
|
||||||
|
'products': ['gm_s2_annual'],
|
||||||
|
'resolution': [-10, 10],
|
||||||
|
'styles': {
|
||||||
|
'True colour': ['red', 'green', 'blue'],
|
||||||
|
'False colour': ['swir_2', 'nir_1', 'green']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Load data #
|
||||||
|
#############
|
||||||
|
|
||||||
|
# Connect to datacube database
|
||||||
|
dc = datacube.Datacube(app='Exporting_satellite_images')
|
||||||
|
|
||||||
|
# Configure local dask cluster
|
||||||
|
client = create_local_dask_cluster(return_client=True)
|
||||||
|
|
||||||
|
# Create query after adjusting interval time to UTC by
|
||||||
|
# adding a UTC offset of -10 hours.
|
||||||
|
start_date = np.datetime64(date)
|
||||||
|
query_params = {
|
||||||
|
'time': (str(start_date)),
|
||||||
|
'geopolygon': geopolygon
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find matching datasets
|
||||||
|
dss = [
|
||||||
|
dc.find_datasets(product=i, **query_params)
|
||||||
|
for i in sat_params[satellites]['products']
|
||||||
|
]
|
||||||
|
dss = list(itertools.chain.from_iterable(dss))
|
||||||
|
|
||||||
|
# Get CRS and sensor
|
||||||
|
crs = str(dss[0].crs)
|
||||||
|
|
||||||
|
if satellites == 'Sentinel-2 geomedian':
|
||||||
|
sensor = satellites
|
||||||
|
else:
|
||||||
|
sensor = dss[0].metadata_doc['properties']['eo:platform'].capitalize()
|
||||||
|
sensor = sensor[0:-1].replace('_', '-') + sensor[-1].capitalize()
|
||||||
|
|
||||||
|
# Use resolution if provided, otherwise use default
|
||||||
|
if resolution:
|
||||||
|
sat_params[satellites]['resolution'] = resolution
|
||||||
|
|
||||||
|
load_params = {
|
||||||
|
'output_crs': crs,
|
||||||
|
'resolution': sat_params[satellites]['resolution'],
|
||||||
|
'resampling': 'bilinear'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load data from datasets
|
||||||
|
ds = dc.load(datasets=dss,
|
||||||
|
measurements=sat_params[satellites]['styles'][style],
|
||||||
|
group_by='solar_day',
|
||||||
|
dask_chunks={
|
||||||
|
'time': 1,
|
||||||
|
'x': 3000,
|
||||||
|
'y': 3000
|
||||||
|
},
|
||||||
|
**load_params,
|
||||||
|
**query_params)
|
||||||
|
ds = masking.mask_invalid_data(ds)
|
||||||
|
|
||||||
|
rgb_array = ds.isel(time=0).to_array().values
|
||||||
|
|
||||||
|
############
|
||||||
|
# Plotting #
|
||||||
|
############
|
||||||
|
|
||||||
|
# Create unique file name
|
||||||
|
centre_coords = geopolygon.centroid.coords[0][::-1]
|
||||||
|
site = reverse_geocode(coords=centre_coords)
|
||||||
|
fname = (f"{sensor} - {date} - {site} - {style}, "
|
||||||
|
f"{load_params['resolution'][1]} m resolution.{output_format}")
|
||||||
|
|
||||||
|
# Remove spaces and commas if requested
|
||||||
|
if standardise_name:
|
||||||
|
fname = fname.replace(' - ', '_').replace(', ',
|
||||||
|
'-').replace(' ',
|
||||||
|
'-').lower()
|
||||||
|
|
||||||
|
print(
|
||||||
|
f'\nExporting image to {fname}.\nThis may take several minutes to complete...'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to numpy array
|
||||||
|
rgb_array = np.transpose(rgb_array, axes=[1, 2, 0])
|
||||||
|
|
||||||
|
# If percentile stretch is supplied, calculate vmin and vmax
|
||||||
|
# from percentiles
|
||||||
|
if percentile_stretch:
|
||||||
|
vmin, vmax = np.nanpercentile(rgb_array, percentile_stretch)
|
||||||
|
|
||||||
|
# Raise by power to dampen bright features and enhance dark.
|
||||||
|
# Raise vmin and vmax by same amount to ensure proper stretch
|
||||||
|
if power:
|
||||||
|
rgb_array = rgb_array**power
|
||||||
|
vmin, vmax = vmin**power, vmax**power
|
||||||
|
|
||||||
|
# Rescale/stretch imagery between vmin and vmax
|
||||||
|
rgb_rescaled = exposure.rescale_intensity(rgb_array.astype(float),
|
||||||
|
in_range=(vmin, vmax),
|
||||||
|
out_range=(0.0, 1.0))
|
||||||
|
|
||||||
|
# Apply image processing funcs
|
||||||
|
if image_proc_funcs:
|
||||||
|
for i, func in enumerate(image_proc_funcs):
|
||||||
|
print(f'Applying custom function {i + 1}')
|
||||||
|
rgb_rescaled = func(rgb_rescaled)
|
||||||
|
|
||||||
|
# Plot RGB
|
||||||
|
plt.imshow(rgb_rescaled)
|
||||||
|
|
||||||
|
# Export to file
|
||||||
|
plt.imsave(fname=fname, arr=rgb_rescaled, format=output_format)
|
||||||
|
|
||||||
|
# Close dask client
|
||||||
|
client.shutdown()
|
||||||
|
|
||||||
|
print('Finished exporting image.')
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
"""
|
||||||
|
Wetlands insight tool widget, which can be used to run an interactive
|
||||||
|
version of the wetlands insight tool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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 datacube
|
||||||
|
import warnings
|
||||||
|
import seaborn as sns
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from datacube.utils.geometry import CRS
|
||||||
|
from ipyleaflet import (
|
||||||
|
WMSLayer,
|
||||||
|
basemaps,
|
||||||
|
basemap_to_tiles,
|
||||||
|
Map,
|
||||||
|
DrawControl,
|
||||||
|
WidgetControl,
|
||||||
|
LayerGroup,
|
||||||
|
LayersControl,
|
||||||
|
)
|
||||||
|
from traitlets import Unicode
|
||||||
|
from ipywidgets import (
|
||||||
|
GridspecLayout,
|
||||||
|
Button,
|
||||||
|
Layout,
|
||||||
|
HBox,
|
||||||
|
VBox,
|
||||||
|
HTML,
|
||||||
|
Output,
|
||||||
|
)
|
||||||
|
import json
|
||||||
|
import geopandas as gpd
|
||||||
|
from io import BytesIO
|
||||||
|
from dask.diagnostics import ProgressBar
|
||||||
|
|
||||||
|
import deafrica_tools
|
||||||
|
from deafrica_tools.dask import create_local_dask_cluster
|
||||||
|
from deafrica_tools.wetlands import WIT_drill
|
||||||
|
import deafrica_tools.app.widgetconstructors as deawidgets
|
||||||
|
|
||||||
|
|
||||||
|
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 wit_app(HBox):
|
||||||
|
def __init__(self, lang=None):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
deafrica_tools.set_lang(lang)
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# INITIAL ATTRIBUTES #
|
||||||
|
|
||||||
|
self.startdate = "2020-01-01"
|
||||||
|
self.enddate = "2020-03-01"
|
||||||
|
self.mingooddata = 0.0
|
||||||
|
self.resamplingfreq = "1M"
|
||||||
|
self.out_csv = "example_WIT.csv"
|
||||||
|
self.out_plot = "example_WIT.png"
|
||||||
|
self.product_list = [
|
||||||
|
(_("None"), "none"),
|
||||||
|
(_("ESRI World Imagery"), "esri_world_imagery"),
|
||||||
|
(_("Sentinel-2 Geomedian"), "gm_s2_annual"),
|
||||||
|
(_("Water Observations from Space"), "wofs_ls_summary_annual"),
|
||||||
|
|
||||||
|
]
|
||||||
|
self.product = self.product_list[0][1]
|
||||||
|
self.product_year = "2020-01-01"
|
||||||
|
self.target = None
|
||||||
|
self.action = None
|
||||||
|
self.gdf_drawn = None
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# HEADER FOR APP #
|
||||||
|
|
||||||
|
# Create the Header widget
|
||||||
|
header_title_text = _("Wetlands Insight Tool")
|
||||||
|
instruction_text = _("Select parameters and AOI")
|
||||||
|
self.header = deawidgets.create_html(f"<h3>{header_title_text}</h3><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):
|
||||||
|
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
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"
|
||||||
|
self.gdf_drawn = gdf
|
||||||
|
|
||||||
|
gdf_drawn_epsg6933 = gdf.copy().to_crs("EPSG:6933")
|
||||||
|
m2_per_km2 = 10 ** 6
|
||||||
|
area = gdf_drawn_epsg6933.area.values[0] / m2_per_km2
|
||||||
|
polyarea_label = _('Total polygon area')
|
||||||
|
polyarea_text = f"<p><b>{polyarea_label}</b>: {area:.2f} km<sup>2</sup></p>"
|
||||||
|
|
||||||
|
if area <= 3000:
|
||||||
|
confirmation_text = '<p style="color:#33cc33;">' + _('Area falls within recommended limit') + '</p>'
|
||||||
|
self.header.value = header_title_text + polyarea_text + confirmation_text
|
||||||
|
else:
|
||||||
|
warning_text = '<p style="color:#ff5050;">' + _('Area is too large, please update your polygon') + '</p>'
|
||||||
|
self.header.value = header_title_text + polyarea_text + warning_text
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# WIDGETS FOR APP OUTPUTS #
|
||||||
|
|
||||||
|
self.dask_client = Output(layout=make_box_layout())
|
||||||
|
self.progress_bar = Output(layout=make_box_layout())
|
||||||
|
self.wit_plot = Output(layout=make_box_layout())
|
||||||
|
self.progress_header = deawidgets.create_html("")
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# MAP WIDGET, DRAWING TOOLS, WMS LAYERS #
|
||||||
|
|
||||||
|
# Create drawing tools
|
||||||
|
desired_drawtools = ['rectangle', 'polygon']
|
||||||
|
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
||||||
|
|
||||||
|
# Begin by displaying an empty layer group, and update the group with desired WMS on interaction.
|
||||||
|
self.deafrica_layers = LayerGroup(layers=())
|
||||||
|
self.deafrica_layers.name = _('Map Overlays')
|
||||||
|
|
||||||
|
# Create map widget
|
||||||
|
self.m = deawidgets.create_map()
|
||||||
|
|
||||||
|
self.m.layout = make_box_layout()
|
||||||
|
|
||||||
|
# Add tools to map widget
|
||||||
|
self.m.add_control(draw_control)
|
||||||
|
self.m.add_layer(self.deafrica_layers)
|
||||||
|
|
||||||
|
# Store current basemap for future use
|
||||||
|
self.basemap = self.m.basemap
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# WIDGETS FOR APP CONTROLS #
|
||||||
|
|
||||||
|
# Create parameter widgets
|
||||||
|
startdate_picker = deawidgets.create_datepicker()
|
||||||
|
enddate_picker = deawidgets.create_datepicker()
|
||||||
|
min_good_data = deawidgets.create_boundedfloattext(self.mingooddata, 0.0, 1.0, 0.05)
|
||||||
|
resampling_freq = deawidgets.create_inputtext(self.resamplingfreq, self.resamplingfreq)
|
||||||
|
output_csv = deawidgets.create_inputtext(self.out_csv, self.out_csv)
|
||||||
|
output_plot = deawidgets.create_inputtext(self.out_plot, self.out_plot)
|
||||||
|
deaoverlay_dropdown = deawidgets.create_dropdown(self.product_list, self.product_list[0][1])
|
||||||
|
run_button = create_expanded_button(_("Run"), "info")
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# COLLECTION OF ALL APP CONTROLS #
|
||||||
|
|
||||||
|
parameter_selection = VBox(
|
||||||
|
[
|
||||||
|
HTML("<b>" + _("Map Overlay:") + "</b>"),
|
||||||
|
deaoverlay_dropdown,
|
||||||
|
HTML("<b>" + _("Start Date:") + "</b>"),
|
||||||
|
startdate_picker,
|
||||||
|
HTML("<b>" + _("End Date:") + "</b>"),
|
||||||
|
enddate_picker,
|
||||||
|
HTML("<b>" + _("Minimum Good Data:") + "</b>"),
|
||||||
|
min_good_data,
|
||||||
|
HTML("<b>" + _("Resampling Frequency:") + "</b>"),
|
||||||
|
resampling_freq,
|
||||||
|
HTML("<b>" + _("Output CSV:") + "</b>"),
|
||||||
|
output_csv,
|
||||||
|
HTML("<b>" + _("Output Plot:") + "</b>"),
|
||||||
|
output_plot,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
parameter_selection.layout = make_box_layout()
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# SPECIFICATION OF APP LAYOUT #
|
||||||
|
|
||||||
|
# Create the layout #[rowspan, colspan]
|
||||||
|
grid = GridspecLayout(11, 10, height="1100px", width="auto")
|
||||||
|
|
||||||
|
# Controls and Status
|
||||||
|
grid[0, :] = self.header
|
||||||
|
grid[1:6, 0:2] = parameter_selection
|
||||||
|
grid[6, 0:2] = run_button
|
||||||
|
|
||||||
|
# Dask and Progress info
|
||||||
|
grid[1, 7:] = self.dask_client
|
||||||
|
grid[2:7, 7:] = self.progress_bar
|
||||||
|
|
||||||
|
# Map
|
||||||
|
grid[1:7, 2:7] = self.m
|
||||||
|
|
||||||
|
# Plot
|
||||||
|
grid[7:, :] = self.wit_plot
|
||||||
|
|
||||||
|
# Display using HBox children attribute
|
||||||
|
self.children = [grid]
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# SPECIFICATION UPDATE FUNCTIONS FOR EACH WIDGET #
|
||||||
|
|
||||||
|
# Run update functions whenever various widgets are changed.
|
||||||
|
startdate_picker.observe(self.update_startdate, "value")
|
||||||
|
enddate_picker.observe(self.update_enddate, "value")
|
||||||
|
min_good_data.observe(self.update_mingooddata, "value")
|
||||||
|
resampling_freq.observe(self.update_resamplingfreq, "value")
|
||||||
|
output_csv.observe(self.update_outputcsv, "value")
|
||||||
|
output_plot.observe(self.update_outputplot, "value")
|
||||||
|
deaoverlay_dropdown.observe(self.update_deaoverlay, "value")
|
||||||
|
run_button.on_click(self.run_app)
|
||||||
|
draw_control.on_draw(update_geojson)
|
||||||
|
|
||||||
|
##############################################################
|
||||||
|
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
||||||
|
|
||||||
|
# set the start date to the new edited date
|
||||||
|
def update_startdate(self, change):
|
||||||
|
self.startdate = change.new
|
||||||
|
|
||||||
|
# set the end date to the new edited date
|
||||||
|
def update_enddate(self, change):
|
||||||
|
self.enddate = change.new
|
||||||
|
|
||||||
|
# set the min good data
|
||||||
|
def update_mingooddata(self, change):
|
||||||
|
self.mingooddata = change.new
|
||||||
|
|
||||||
|
# set the resampling frequency
|
||||||
|
def update_resamplingfreq(self, change):
|
||||||
|
self.resamplingfreq = change.new
|
||||||
|
|
||||||
|
# set the output csv
|
||||||
|
def update_outputcsv(self, change):
|
||||||
|
self.out_csv = change.new
|
||||||
|
|
||||||
|
# set the output plot
|
||||||
|
def update_outputplot(self, change):
|
||||||
|
self.out_plot = change.new
|
||||||
|
|
||||||
|
# Update product
|
||||||
|
def update_deaoverlay(self, change):
|
||||||
|
|
||||||
|
self.product = change.new
|
||||||
|
|
||||||
|
if self.product == "none":
|
||||||
|
self.deafrica_layers.clear_layers()
|
||||||
|
elif self.product == "esri_world_imagery":
|
||||||
|
self.deafrica_layers.clear_layers()
|
||||||
|
layer = basemap_to_tiles(basemaps.Esri.WorldImagery)
|
||||||
|
self.deafrica_layers.add_layer(layer)
|
||||||
|
else:
|
||||||
|
self.deafrica_layers.clear_layers()
|
||||||
|
layer = deawidgets.create_dea_wms_layer(self.product, self.product_year)
|
||||||
|
self.deafrica_layers.add_layer(layer)
|
||||||
|
|
||||||
|
def run_app(self, change):
|
||||||
|
|
||||||
|
# Clear progress bar and output areas before running
|
||||||
|
self.dask_client.clear_output()
|
||||||
|
self.progress_bar.clear_output()
|
||||||
|
self.wit_plot.clear_output()
|
||||||
|
|
||||||
|
# Connect to datacube database
|
||||||
|
dc = datacube.Datacube(app="wetland_app")
|
||||||
|
|
||||||
|
# Configure local dask cluster
|
||||||
|
with self.dask_client:
|
||||||
|
client = create_local_dask_cluster(
|
||||||
|
return_client=True, display_client=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set any defaults
|
||||||
|
TCW_threshold = -0.035
|
||||||
|
dask_chunks = dict(x=1000, y=1000, time=1)
|
||||||
|
|
||||||
|
#check resampling freq
|
||||||
|
if self.resamplingfreq == 'None':
|
||||||
|
rsf = None
|
||||||
|
else:
|
||||||
|
rsf = self.resamplingfreq
|
||||||
|
|
||||||
|
self.progress_header.value = f"<h3>"+_("Progress")+"</h3>"
|
||||||
|
|
||||||
|
# run wetlands polygon drill
|
||||||
|
with self.progress_bar:
|
||||||
|
# with ProgressBar():
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
try:
|
||||||
|
df = WIT_drill(
|
||||||
|
gdf=self.gdf_drawn,
|
||||||
|
time=(self.startdate, self.enddate),
|
||||||
|
min_gooddata=self.mingooddata,
|
||||||
|
resample_frequency=rsf,
|
||||||
|
TCW_threshold=TCW_threshold,
|
||||||
|
export_csv=self.out_csv,
|
||||||
|
dask_chunks=dask_chunks,
|
||||||
|
verbose=False,
|
||||||
|
verbose_progress=True,
|
||||||
|
)
|
||||||
|
print(_("WIT complete"))
|
||||||
|
except AttributeError:
|
||||||
|
print(_("No polygon selected"))
|
||||||
|
|
||||||
|
# close down the dask client
|
||||||
|
client.shutdown()
|
||||||
|
|
||||||
|
# save the csv
|
||||||
|
if self.out_csv:
|
||||||
|
df.to_csv(self.out_csv, index_label="Datetime")
|
||||||
|
|
||||||
|
# ---Plotting------------------------------
|
||||||
|
|
||||||
|
with self.wit_plot:
|
||||||
|
|
||||||
|
fontsize = 17
|
||||||
|
plt.rcParams.update({"font.size": fontsize})
|
||||||
|
# set up color palette
|
||||||
|
pal = [
|
||||||
|
sns.xkcd_rgb["cobalt blue"],
|
||||||
|
sns.xkcd_rgb["neon blue"],
|
||||||
|
sns.xkcd_rgb["grass"],
|
||||||
|
sns.xkcd_rgb["beige"],
|
||||||
|
sns.xkcd_rgb["brown"],
|
||||||
|
]
|
||||||
|
|
||||||
|
# make a stacked area plot
|
||||||
|
plt.close("all")
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(constrained_layout=True, figsize=(20, 6))
|
||||||
|
|
||||||
|
ax.stackplot(
|
||||||
|
df.index,
|
||||||
|
df.wofs_area_percent,
|
||||||
|
df.wet_percent,
|
||||||
|
df.green_veg_percent,
|
||||||
|
df.dry_veg_percent,
|
||||||
|
df.bare_soil_percent,
|
||||||
|
labels=[
|
||||||
|
_("open water"),
|
||||||
|
_("wet"),
|
||||||
|
_("green veg"),
|
||||||
|
_("dry veg"),
|
||||||
|
_("bare soil"),
|
||||||
|
],
|
||||||
|
colors=pal,
|
||||||
|
alpha=0.6,
|
||||||
|
)
|
||||||
|
|
||||||
|
# set axis limits to the min and max
|
||||||
|
ax.set_ylim(0, 100)
|
||||||
|
ax.set_xlim(df.index[0], df.index[-1])
|
||||||
|
ax.tick_params(axis="x", labelsize=fontsize)
|
||||||
|
|
||||||
|
# add a legend and a tight plot box
|
||||||
|
ax.legend(loc="lower left", framealpha=0.6)
|
||||||
|
ax.set_title(_("Percentage Fractional Cover, Wetness, and Water"))
|
||||||
|
# plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
if self.out_plot:
|
||||||
|
# save the figure
|
||||||
|
fig.savefig(f"{self.out_plot}")
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
"""
|
||||||
|
Functions for easily defining widgets in the context of DE Africa notebooks.
|
||||||
|
|
||||||
|
These are largely customised wrappers around existing widgets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ipyleaflet as leaflet
|
||||||
|
from ipyleaflet import LayersControl
|
||||||
|
import ipywidgets as widgets
|
||||||
|
from traitlets import Unicode
|
||||||
|
|
||||||
|
|
||||||
|
def create_datepicker(description='', value=None, layout={'width': '85%'}):
|
||||||
|
'''
|
||||||
|
Create a DatePicker widget
|
||||||
|
|
||||||
|
Last modified: July 2022
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
description : string
|
||||||
|
descirption label to attach
|
||||||
|
layout : dictionary
|
||||||
|
any layout commands for the widget
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
date_picker : ipywidgets.widgets.widget_date.DatePicker
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
date_picker = widgets.DatePicker(
|
||||||
|
description=description,
|
||||||
|
layout=layout,
|
||||||
|
disabled=False,
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
return date_picker
|
||||||
|
|
||||||
|
|
||||||
|
def create_inputtext(value, placeholder, description="", layout={'width': '85%'}):
|
||||||
|
'''
|
||||||
|
Create a Text widget
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : string
|
||||||
|
initial value of the widget
|
||||||
|
placeholder : string
|
||||||
|
placeholder text to display to the user before intput
|
||||||
|
description : string
|
||||||
|
descirption label to attach
|
||||||
|
layout : dictionary
|
||||||
|
any layout commands for the widget
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
input_text : ipywidgets.widgets.widget_string.Text
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
input_text = widgets.Text(
|
||||||
|
value=value,
|
||||||
|
placeholder=placeholder,
|
||||||
|
description=description,
|
||||||
|
layout=layout,
|
||||||
|
disabled=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return input_text
|
||||||
|
|
||||||
|
|
||||||
|
def create_boundedfloattext(value, min_val, max_val, step_val, description="", layout={'width': '85%'}):
|
||||||
|
'''
|
||||||
|
Create a BoundedFloatText widget
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : float
|
||||||
|
initial value of the widget
|
||||||
|
min_val : float
|
||||||
|
minimum allowed value for the float
|
||||||
|
max_val : float
|
||||||
|
maximum allowed value for the float
|
||||||
|
step_val : float
|
||||||
|
allowed increment for the float
|
||||||
|
description : string
|
||||||
|
descirption label to attach
|
||||||
|
layout : dictionary
|
||||||
|
any layout commands for the widget
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float_text : ipywidgets.widgets.widget_float.BoundedFloatText
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
float_text = widgets.BoundedFloatText(
|
||||||
|
value=value,
|
||||||
|
min=min_val,
|
||||||
|
max=max_val,
|
||||||
|
step=step_val,
|
||||||
|
description=description,
|
||||||
|
layout=layout,
|
||||||
|
disabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return float_text
|
||||||
|
|
||||||
|
|
||||||
|
def create_dropdown(options, value, description="", layout={'width': '85%'}):
|
||||||
|
'''
|
||||||
|
Create a Dropdown widget
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
options : list
|
||||||
|
a list of options for the user to select from
|
||||||
|
value : string
|
||||||
|
initial value of the widget
|
||||||
|
description : string
|
||||||
|
descirption label to attach
|
||||||
|
layout : dictionary
|
||||||
|
any layout commands for the widget
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dropdown : ipywidgets.widgets.widget_selection.Dropdown
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
dropdown = widgets.Dropdown(
|
||||||
|
options=options,
|
||||||
|
value=value,
|
||||||
|
description=description,
|
||||||
|
layout=layout,
|
||||||
|
disabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return dropdown
|
||||||
|
|
||||||
|
|
||||||
|
def create_html(value):
|
||||||
|
'''
|
||||||
|
Create a HTML widget
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : string
|
||||||
|
HTML text to display
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
html : ipywidgets.widgets.widget_string.HTML
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
html = widgets.HTML(
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def create_map(map_center=(4, 20), zoom_level=3, basemap=leaflet.basemaps.OpenStreetMap.Mapnik, basemap_name='Open Street Map'):
|
||||||
|
'''
|
||||||
|
Create an interactive ipyleaflet map
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
map_center : tuple
|
||||||
|
A tuple containing the latitude and longitude to focus on.
|
||||||
|
Defaults to center of Africa, (4, 20)
|
||||||
|
zoom_level : integer
|
||||||
|
Zoom level for the map
|
||||||
|
Defaults to 3 to view all of Africa
|
||||||
|
basemap : ipyleaflet basemap (dict)
|
||||||
|
Basemap to use, can be any from https://ipyleaflet.readthedocs.io/en/latest/api_reference/basemaps.html
|
||||||
|
Defaults to Open Street Map (basemaps.OpenStreetMap.Mapnik)
|
||||||
|
basemap_name : string
|
||||||
|
Layer name for the basemap
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
m : ipyleaflet.leaflet.Map
|
||||||
|
interactive ipyleaflet map
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
basemap_tiles = leaflet.basemap_to_tiles(basemap)
|
||||||
|
basemap_tiles.name = basemap_name
|
||||||
|
|
||||||
|
m = leaflet.Map(center=map_center, zoom=zoom_level, basemap=basemap_tiles, scroll_wheel_zoom=True)
|
||||||
|
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def create_dea_wms_layer(product, date):
|
||||||
|
'''
|
||||||
|
Create a Digital Earth Africa WMS layer to add to a map
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
product : string
|
||||||
|
The Digital Earth Africa product to load
|
||||||
|
(e.g. 'gm_s2_annual')
|
||||||
|
date : string (yyyy-mm-dd format)
|
||||||
|
The date to load the product for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
time_wms : ipyleaflet WMS layer
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# Load DEA WMS
|
||||||
|
class TimeWMSLayer(leaflet.WMSLayer):
|
||||||
|
time = Unicode("").tag(sync=True, o=True)
|
||||||
|
|
||||||
|
time_wms = TimeWMSLayer(
|
||||||
|
url="https://ows.digitalearth.africa/",
|
||||||
|
layers=product,
|
||||||
|
time=date,
|
||||||
|
format="image/png",
|
||||||
|
transparent=True,
|
||||||
|
attribution="Digital Earth Africa",
|
||||||
|
)
|
||||||
|
|
||||||
|
return time_wms
|
||||||
|
|
||||||
|
|
||||||
|
def create_drawcontrol(
|
||||||
|
draw_controls = ['rectangle', 'polygon', 'circle', 'polyline', 'marker', 'circlemarker'],
|
||||||
|
rectangle_options={},
|
||||||
|
polygon_options={},
|
||||||
|
circle_options={},
|
||||||
|
polyline_options={},
|
||||||
|
marker_options={},
|
||||||
|
circlemarker_options={},
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Create a draw control widget to add to ipyleaflet maps
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
draw_controls : list
|
||||||
|
List of draw controls to add to the map. Defaults to adding all
|
||||||
|
Viable options are 'rectangle', 'polygon', 'circle', 'polyline', 'marker', 'circlemarker'
|
||||||
|
rectangle_options : dict
|
||||||
|
Options to customise the appearence of the relevant shape
|
||||||
|
User can supply, or leave blank to get default DE Africa appearence
|
||||||
|
polygon_options : dict
|
||||||
|
Options to customise the appearence of the relevant shape
|
||||||
|
User can supply, or leave blank to get default DE Africa appearence
|
||||||
|
circle_options : dict
|
||||||
|
Options to customise the appearence of the relevant shape
|
||||||
|
User can supply, or leave blank to get default DE Africa appearence
|
||||||
|
polyline_options : dict
|
||||||
|
Options to customise the appearence of the relevant shape
|
||||||
|
User can supply, or leave blank to get default DE Africa appearence
|
||||||
|
marker_options : dict
|
||||||
|
Options to customise the appearence of the relevant shape
|
||||||
|
User can supply, or leave blank to get default DE Africa appearence
|
||||||
|
circlemarker_options : dict
|
||||||
|
Options to customise the appearence of the relevant shape
|
||||||
|
User can supply, or leave blank to get default DE Africa appearence
|
||||||
|
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
draw_control : ipyleaflet.leaflet.DrawControl
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Set defualt DE Africa styling options for polygons
|
||||||
|
default_shapeoptions = {
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"opacity": 0.8,
|
||||||
|
"fillColor": "#336699",
|
||||||
|
"fillOpacity": 0.4,
|
||||||
|
}
|
||||||
|
default_drawerror = {
|
||||||
|
"color": "#FF6633",
|
||||||
|
"message": "Drawing error, clear all and try again"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set draw control appearence to DE Africa defaults
|
||||||
|
# Do this if user has requested a control, but has not provided a corresponding options dict
|
||||||
|
|
||||||
|
if ('rectangle' in draw_controls) and (not rectangle_options):
|
||||||
|
rectangle_options = {"shapeOptions": default_shapeoptions}
|
||||||
|
|
||||||
|
if ('polygon' in draw_controls) and (not polygon_options):
|
||||||
|
polygon_options = {
|
||||||
|
"shapeOptions": default_shapeoptions,
|
||||||
|
"drawError": default_drawerror,
|
||||||
|
"allowIntersection": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('circle' in draw_controls) and (not circle_options):
|
||||||
|
circle_options = {"shapeOptions": default_shapeoptions}
|
||||||
|
|
||||||
|
if ('polyline' in draw_controls) and (not polyline_options):
|
||||||
|
polyline_options = {"shapeOptions": default_shapeoptions}
|
||||||
|
|
||||||
|
if ('marker' in draw_controls) and (not marker_options):
|
||||||
|
marker_options = {'shapeOptions': {'opacity': 1.0}}
|
||||||
|
|
||||||
|
if ('circlemarker' in draw_controls) and (not circlemarker_options):
|
||||||
|
circlemarker_options = {"shapeOptions": default_shapeoptions}
|
||||||
|
|
||||||
|
# Instantiate draw control and add options
|
||||||
|
draw_control = leaflet.DrawControl()
|
||||||
|
draw_control.rectangle = rectangle_options
|
||||||
|
draw_control.polygon = polygon_options
|
||||||
|
draw_control.marker = marker_options
|
||||||
|
draw_control.circle = circle_options
|
||||||
|
draw_control.circlemarker = circlemarker_options
|
||||||
|
draw_control.polyline = polyline_options
|
||||||
|
|
||||||
|
return draw_control
|
||||||
|
|
||||||
|
|
||||||
|
def create_checkbox(value, description="", layout={'width': '85%'}):
|
||||||
|
'''
|
||||||
|
Create a Checkbox widget
|
||||||
|
|
||||||
|
Last modified: July 2022
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : string
|
||||||
|
initial value of the widget; True or False
|
||||||
|
description : string
|
||||||
|
description label to attach
|
||||||
|
layout : dictionary
|
||||||
|
any layout commands for the widget
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dropdown : ipywidgets.widgets.widget_selection.Dropdown
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
checklist = widgets.Checkbox(value=value,
|
||||||
|
description=description,
|
||||||
|
layout=layout,
|
||||||
|
disabled=False,
|
||||||
|
indent=False)
|
||||||
|
|
||||||
|
return checklist
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Function for defining an area of interest using either a point and buffer or a vector file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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 geopandas as gpd
|
||||||
|
from shapely.geometry import box
|
||||||
|
from geojson import Feature, Point, FeatureCollection
|
||||||
|
|
||||||
|
def define_area(lat=None, lon=None, buffer=None, vector_path=None):
|
||||||
|
'''
|
||||||
|
Define an area of interest using either a point and buffer or a vector.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
lat : float, optional
|
||||||
|
The latitude of the center point of the area of interest.
|
||||||
|
lon : float, optional
|
||||||
|
The longitude of the center point of the area of interest.
|
||||||
|
buffer : float, optional
|
||||||
|
The buffer around the center point, in degrees.
|
||||||
|
vector_path : str, optional
|
||||||
|
The path to a vector defining the area of interest.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
feature_collection : dict
|
||||||
|
A GeoJSON feature collection representing the area of interest.
|
||||||
|
'''
|
||||||
|
# Define area using point and buffer
|
||||||
|
if lat is not None and lon is not None and buffer is not None:
|
||||||
|
lat_range = (lat - buffer, lat + buffer)
|
||||||
|
lon_range = (lon - buffer, lon + buffer)
|
||||||
|
box_geom = box(min(lon_range), min(lat_range), max(lon_range), max(lat_range))
|
||||||
|
aoi = gpd.GeoDataFrame(geometry=[box_geom], crs='EPSG:4326')
|
||||||
|
|
||||||
|
# Define area using vector
|
||||||
|
elif vector_path is not None:
|
||||||
|
aoi = gpd.read_file(vector_path).to_crs("EPSG:4326")
|
||||||
|
# If neither option is provided, raise an error
|
||||||
|
else:
|
||||||
|
raise ValueError("Either lat/lon/buffer or vector_path must be provided.")
|
||||||
|
|
||||||
|
# Convert the GeoDataFrame to a GeoJSON FeatureCollection
|
||||||
|
features = [Feature(geometry=row["geometry"], properties=row.drop("geometry").to_dict()) for _, row in aoi.iterrows()]
|
||||||
|
feature_collection = FeatureCollection(features)
|
||||||
|
|
||||||
|
return feature_collection
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
"""
|
||||||
|
Functions for computing remote sensing band indices on Digital Earth Africa
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Import required packages
|
||||||
|
import warnings
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Define custom functions
|
||||||
|
def calculate_indices(
|
||||||
|
ds,
|
||||||
|
index=None,
|
||||||
|
collection=None,
|
||||||
|
satellite_mission=None,
|
||||||
|
custom_varname=None,
|
||||||
|
normalise=True,
|
||||||
|
drop=False,
|
||||||
|
deep_copy=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Takes an xarray dataset containing spectral bands, calculates one of
|
||||||
|
a set of remote sensing indices, and adds the resulting array as a
|
||||||
|
new variable in the original dataset.
|
||||||
|
|
||||||
|
Last modified: July 2022
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds : xarray Dataset
|
||||||
|
A two-dimensional or multi-dimensional array with containing the
|
||||||
|
spectral bands required to calculate the index. These bands are
|
||||||
|
used as inputs to calculate the selected water index.
|
||||||
|
|
||||||
|
index : str or list of strs
|
||||||
|
A string giving the name of the index to calculate or a list of
|
||||||
|
strings giving the names of the indices to calculate:
|
||||||
|
|
||||||
|
* ``'ASI'`` (Artificial Surface Index, Yongquan Zhao & Zhe Zhu 2022)
|
||||||
|
* ``'AWEI_ns'`` (Automated Water Extraction Index, no shadows, Feyisa 2014)
|
||||||
|
* ``'AWEI_sh'`` (Automated Water Extraction Index, shadows, Feyisa 2014)
|
||||||
|
* ``'BAEI'`` (Built-Up Area Extraction Index, Bouzekri et al. 2015)
|
||||||
|
* ``'BAI'`` (Burn Area Index, Martin 1998)
|
||||||
|
* ``'BSI'`` (Bare Soil Index, Rikimaru et al. 2002)
|
||||||
|
* ``'BUI'`` (Built-Up Index, He et al. 2010)
|
||||||
|
* ``'CMR'`` (Clay Minerals Ratio, Drury 1987)
|
||||||
|
* ``'ENDISI'`` (Enhanced Normalised Difference for Impervious Surfaces Index, Chen et al. 2019)
|
||||||
|
* ``'EVI'`` (Enhanced Vegetation Index, Huete 2002)
|
||||||
|
* ``'FMR'`` (Ferrous Minerals Ratio, Segal 1982)
|
||||||
|
* ``'IOR'`` (Iron Oxide Ratio, Segal 1982)
|
||||||
|
* ``'LAI'`` (Leaf Area Index, Boegh 2002)
|
||||||
|
* ``'MBI'`` (Modified Bare Soil Index, Nguyen et al. 2021)
|
||||||
|
* ``'MNDWI'`` (Modified Normalised Difference Water Index, Xu 1996)
|
||||||
|
* ``'MSAVI'`` (Modified Soil Adjusted Vegetation Index, Qi et al. 1994)
|
||||||
|
* ``'NBI'`` (New Built-Up Index, Jieli et al. 2010)
|
||||||
|
* ``'NBR'`` (Normalised Burn Ratio, Lopez Garcia 1991)
|
||||||
|
* ``'NDBI'`` (Normalised Difference Built-Up Index, Zha 2003)
|
||||||
|
* ``'NDCI'`` (Normalised Difference Chlorophyll Index, Mishra & Mishra, 2012)
|
||||||
|
* ``'NDMI'`` (Normalised Difference Moisture Index, Gao 1996)
|
||||||
|
* ``'NDSI'`` (Normalised Difference Snow Index, Hall 1995)
|
||||||
|
* ``'NDTI'`` (Normalised Difference Turbidity Index, Lacaux et al. 2007)
|
||||||
|
* ``'NDVI'`` (Normalised Difference Vegetation Index, Rouse 1973)
|
||||||
|
* ``'NDWI'`` (Normalised Difference Water Index, McFeeters 1996)
|
||||||
|
* ``'SAVI'`` (Soil Adjusted Vegetation Index, Huete 1988)
|
||||||
|
* ``'TCB'`` (Tasseled Cap Brightness, Crist 1985)
|
||||||
|
* ``'TCG'`` (Tasseled Cap Greeness, Crist 1985)
|
||||||
|
* ``'TCW'`` (Tasseled Cap Wetness, Crist 1985)
|
||||||
|
* ``'WI'`` (Water Index, Fisher 2016)
|
||||||
|
|
||||||
|
collection : str
|
||||||
|
Deprecated in version 0.1.7. Use `satellite_mission` instead.
|
||||||
|
|
||||||
|
Valid options are:
|
||||||
|
* ``'c2'`` (for USGS Landsat Collection 2)
|
||||||
|
If 'c2', then `satellite_mission='ls'`.
|
||||||
|
* ``'s2'`` (for Sentinel-2)
|
||||||
|
If 's2', then `satellite_mission='s2'`.
|
||||||
|
|
||||||
|
satellite_mission : str
|
||||||
|
An string that tells the function which satellite mission's data is
|
||||||
|
being used to calculate the index. This is necessary because
|
||||||
|
different satellite missions use different names for bands covering
|
||||||
|
a similar spectra.
|
||||||
|
|
||||||
|
Valid options are:
|
||||||
|
|
||||||
|
* ``'ls'`` (for USGS Landsat)
|
||||||
|
* ``'s2'`` (for Copernicus Sentinel-2)
|
||||||
|
|
||||||
|
custom_varname : str, optional
|
||||||
|
By default, the original dataset will be returned with
|
||||||
|
a new index variable named after `index` (e.g. 'NDVI'). To
|
||||||
|
specify a custom name instead, you can supply e.g.
|
||||||
|
`custom_varname='custom_name'`. Defaults to None, which uses
|
||||||
|
`index` to name the variable.
|
||||||
|
|
||||||
|
normalise : bool, optional
|
||||||
|
Some coefficient-based indices (e.g. ``'WI'``, ``'BAEI'``,
|
||||||
|
``'AWEI_ns'``, ``'AWEI_sh'``, ``'TCW'``, ``'TCG'``, ``'TCB'``,
|
||||||
|
``'EVI'``, ``'LAI'``, ``'SAVI'``, ``'MSAVI'``)
|
||||||
|
produce different results if surface reflectance values are not
|
||||||
|
scaled between 0.0 and 1.0 prior to calculating the index.
|
||||||
|
Setting `normalise=True` first scales values to a 0.0-1.0 range
|
||||||
|
by dividing by 10000.0. Defaults to True.
|
||||||
|
|
||||||
|
drop : bool, optional
|
||||||
|
Provides the option to drop the original input data, thus saving
|
||||||
|
space. If `drop=True`, returns only the index and its values.
|
||||||
|
|
||||||
|
deep_copy: bool, optional
|
||||||
|
If `deep_copy=False`, calculate_indices will modify the original
|
||||||
|
array, adding bands to the input dataset and not removing them.
|
||||||
|
If the calculate_indices function is run more than once, variables
|
||||||
|
may be dropped incorrectly producing unexpected behaviour. This is
|
||||||
|
a bug and may be fixed in future releases. This is only a problem
|
||||||
|
when `drop=True`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ds : xarray Dataset
|
||||||
|
The original xarray Dataset inputted into the function, with a
|
||||||
|
new varible containing the remote sensing index as a DataArray.
|
||||||
|
If drop = True, the new variable/s as DataArrays in the
|
||||||
|
original Dataset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set ds equal to a copy of itself in order to prevent the function
|
||||||
|
# from editing the input dataset. This is to prevent unexpected
|
||||||
|
# behaviour though it uses twice as much memory.
|
||||||
|
if deep_copy:
|
||||||
|
ds = ds.copy(deep=True)
|
||||||
|
|
||||||
|
# Capture input band names in order to drop these if drop=True
|
||||||
|
if drop:
|
||||||
|
bands_to_drop = list(ds.data_vars)
|
||||||
|
print(f"Dropping bands {bands_to_drop}")
|
||||||
|
|
||||||
|
# Dictionary containing remote sensing index band recipes
|
||||||
|
index_dict = {
|
||||||
|
# Normalised Difference Vegation Index, Rouse 1973
|
||||||
|
"NDVI": lambda ds: (ds.nir - ds.red) / (ds.nir + ds.red),
|
||||||
|
# Enhanced Vegetation Index, Huete 2002
|
||||||
|
"EVI": lambda ds: (
|
||||||
|
2.5 * ((ds.nir - ds.red) / (ds.nir + 6 * ds.red - 7.5 * ds.blue + 1))
|
||||||
|
),
|
||||||
|
# Leaf Area Index, Boegh 2002
|
||||||
|
"LAI": lambda ds: (
|
||||||
|
3.618
|
||||||
|
* ((2.5 * (ds.nir - ds.red)) / (ds.nir + (6 * ds.red) - (7.5 * ds.blue) + 1))
|
||||||
|
- 0.118
|
||||||
|
),
|
||||||
|
# Soil Adjusted Vegetation Index, Huete 1988
|
||||||
|
"SAVI": lambda ds: ((1.5 * (ds.nir - ds.red)) / (ds.nir + ds.red + 0.5)),
|
||||||
|
# Mod. Soil Adjusted Vegetation Index, Qi et al. 1994
|
||||||
|
"MSAVI": lambda ds: (
|
||||||
|
(2 * ds.nir + 1 - ((2 * ds.nir + 1) ** 2 - 8 * (ds.nir - ds.red)) ** 0.5)
|
||||||
|
/ 2
|
||||||
|
),
|
||||||
|
# Normalised Difference Moisture Index, Gao 1996
|
||||||
|
"NDMI": lambda ds: (ds.nir - ds.swir_1) / (ds.nir + ds.swir_1),
|
||||||
|
# Normalised Burn Ratio, Lopez Garcia 1991
|
||||||
|
"NBR": lambda ds: (ds.nir - ds.swir_2) / (ds.nir + ds.swir_2),
|
||||||
|
# Burn Area Index, Martin 1998
|
||||||
|
"BAI": lambda ds: (1.0 / ((0.10 - ds.red) ** 2 + (0.06 - ds.nir) ** 2)),
|
||||||
|
# Normalised Difference Chlorophyll Index,
|
||||||
|
# (Mishra & Mishra, 2012)
|
||||||
|
"NDCI": lambda ds: (ds.red_edge_1 - ds.red) / (ds.red_edge_1 + ds.red),
|
||||||
|
# Normalised Difference Snow Index, Hall 1995
|
||||||
|
"NDSI": lambda ds: (ds.green - ds.swir_1) / (ds.green + ds.swir_1),
|
||||||
|
# Normalised Difference Water Index, McFeeters 1996
|
||||||
|
"NDWI": lambda ds: (ds.green - ds.nir) / (ds.green + ds.nir),
|
||||||
|
# Modified Normalised Difference Water Index, Xu 2006
|
||||||
|
"MNDWI": lambda ds: (ds.green - ds.swir_1) / (ds.green + ds.swir_1),
|
||||||
|
# Normalised Difference Built-Up Index, Zha 2003
|
||||||
|
"NDBI": lambda ds: (ds.swir_1 - ds.nir) / (ds.swir_1 + ds.nir),
|
||||||
|
# Built-Up Index, He et al. 2010
|
||||||
|
"BUI": lambda ds: ((ds.swir_1 - ds.nir) / (ds.swir_1 + ds.nir))
|
||||||
|
- ((ds.nir - ds.red) / (ds.nir + ds.red)),
|
||||||
|
# Built-up Area Extraction Index, Bouzekri et al. 2015
|
||||||
|
"BAEI": lambda ds: (ds.red + 0.3) / (ds.green + ds.swir_1),
|
||||||
|
# New Built-up Index, Jieli et al. 2010
|
||||||
|
"NBI": lambda ds: (ds.swir_1 + ds.red) / ds.nir,
|
||||||
|
# Bare Soil Index, Rikimaru et al. 2002
|
||||||
|
"BSI": lambda ds: ((ds.swir_1 + ds.red) - (ds.nir + ds.blue))
|
||||||
|
/ ((ds.swir_1 + ds.red) + (ds.nir + ds.blue)),
|
||||||
|
# Automated Water Extraction Index (no shadows), Feyisa 2014
|
||||||
|
"AWEI_ns": lambda ds: (
|
||||||
|
4 * (ds.green - ds.swir_1) - (0.25 * ds.nir * +2.75 * ds.swir_2)
|
||||||
|
),
|
||||||
|
# Automated Water Extraction Index (shadows), Feyisa 2014
|
||||||
|
"AWEI_sh": lambda ds: (
|
||||||
|
ds.blue + 2.5 * ds.green - 1.5 * (ds.nir + ds.swir_1) - 0.25 * ds.swir_2
|
||||||
|
),
|
||||||
|
# Water Index, Fisher 2016
|
||||||
|
"WI": lambda ds: (
|
||||||
|
1.7204
|
||||||
|
+ 171 * ds.green
|
||||||
|
+ 3 * ds.red
|
||||||
|
- 70 * ds.nir
|
||||||
|
- 45 * ds.swir_1
|
||||||
|
- 71 * ds.swir_2
|
||||||
|
),
|
||||||
|
# Tasseled Cap Wetness, Crist 1985
|
||||||
|
"TCW": lambda ds: (
|
||||||
|
0.0315 * ds.blue
|
||||||
|
+ 0.2021 * ds.green
|
||||||
|
+ 0.3102 * ds.red
|
||||||
|
+ 0.1594 * ds.nir
|
||||||
|
+ -0.6806 * ds.swir_1
|
||||||
|
+ -0.6109 * ds.swir_2
|
||||||
|
),
|
||||||
|
# Tasseled Cap Greeness, Crist 1985
|
||||||
|
"TCG": lambda ds: (
|
||||||
|
-0.1603 * ds.blue
|
||||||
|
+ -0.2819 * ds.green
|
||||||
|
+ -0.4934 * ds.red
|
||||||
|
+ 0.7940 * ds.nir
|
||||||
|
+ -0.0002 * ds.swir_1
|
||||||
|
+ -0.1446 * ds.swir_2
|
||||||
|
),
|
||||||
|
# Tasseled Cap Brightness, Crist 1985
|
||||||
|
"TCB": lambda ds: (
|
||||||
|
0.2043 * ds.blue
|
||||||
|
+ 0.4158 * ds.green
|
||||||
|
+ 0.5524 * ds.red
|
||||||
|
+ 0.5741 * ds.nir
|
||||||
|
+ 0.3124 * ds.swir_1
|
||||||
|
+ -0.2303 * ds.swir_2
|
||||||
|
),
|
||||||
|
# Clay Minerals Ratio, Drury 1987
|
||||||
|
"CMR": lambda ds: (ds.swir_1 / ds.swir_2),
|
||||||
|
# Ferrous Minerals Ratio, Segal 1982
|
||||||
|
"FMR": lambda ds: (ds.swir_1 / ds.nir),
|
||||||
|
# Iron Oxide Ratio, Segal 1982
|
||||||
|
"IOR": lambda ds: (ds.red / ds.blue),
|
||||||
|
# Normalized Difference Turbidity Index, Lacaux, J.P. et al. 2007
|
||||||
|
"NDTI": lambda ds: (ds.red - ds.green) / (ds.red + ds.green),
|
||||||
|
# Modified Bare Soil Index, Nguyen et al. 2021
|
||||||
|
"MBI": lambda ds: ((ds.swir_1 - ds.swir_2 - ds.nir) / (ds.swir_1 + ds.swir_2 + ds.nir)) + 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enhanced Normalised Difference Impervious Surfaces Index, Chen et al. 2019
|
||||||
|
def mndwi(ds):
|
||||||
|
return (ds.green - ds.swir_1) / (ds.green + ds.swir_1)
|
||||||
|
def swir_diff(ds):
|
||||||
|
return ds.swir_1/ds.swir_2
|
||||||
|
def alpha(ds):
|
||||||
|
return (2*(np.mean(ds.blue)))/(np.mean(swir_diff(ds)) + np.mean(mndwi(ds)**2))
|
||||||
|
def ENDISI(ds):
|
||||||
|
m = mndwi(ds)
|
||||||
|
s = swir_diff(ds)
|
||||||
|
a = alpha(ds)
|
||||||
|
return (ds.blue - (a)*(s + m**2))/(ds.blue + (a)*(s + m**2))
|
||||||
|
|
||||||
|
index_dict["ENDISI"] = ENDISI
|
||||||
|
|
||||||
|
## Artificial Surface Index, Yongquan Zhao & Zhe Zhu 2022
|
||||||
|
def af(ds):
|
||||||
|
AF = (ds.nir - ds.blue) / (ds.nir + ds.blue)
|
||||||
|
AF_norm = (AF - AF.min(dim=["y","x"]))/(AF.max(dim=["y","x"]) - AF.min(dim=["y","x"]))
|
||||||
|
return AF_norm
|
||||||
|
def ndvi(ds):
|
||||||
|
return (ds.nir - ds.red) / (ds.nir + ds.red)
|
||||||
|
def msavi(ds):
|
||||||
|
return ((2 * ds.nir + 1 - ((2 * ds.nir + 1) ** 2 - 8 * (ds.nir - ds.red)) ** 0.5) / 2 )
|
||||||
|
def vsf(ds):
|
||||||
|
NDVI = ndvi(ds)
|
||||||
|
MSAVI = msavi(ds)
|
||||||
|
VSF = 1 - NDVI * MSAVI
|
||||||
|
VSF_norm = (VSF - VSF.min(dim=["y","x"]))/(VSF.max(dim=["y","x"]) - VSF.min(dim=["y","x"]))
|
||||||
|
return VSF_norm
|
||||||
|
def mbi(ds):
|
||||||
|
return ((ds.swir_1 - ds.swir_2 - ds.nir) / (ds.swir_1 + ds.swir_2 + ds.nir)) + 0.5
|
||||||
|
def embi(ds):
|
||||||
|
MBI = mbi(ds)
|
||||||
|
MNDWI = mndwi(ds)
|
||||||
|
return (MBI - MNDWI - 0.5) / (MBI + MNDWI + 1.5)
|
||||||
|
def ssf(ds):
|
||||||
|
EMBI = embi(ds)
|
||||||
|
SSF = 1 - EMBI
|
||||||
|
SSF_norm = (SSF - SSF.min(dim=["y","x"]))/(SSF.max(dim=["y","x"]) - SSF.min(dim=["y","x"]))
|
||||||
|
return SSF_norm
|
||||||
|
# Overall modulation using the Modulation Factor (MF).
|
||||||
|
def mf(ds):
|
||||||
|
MF = ((ds.blue + ds.green) - (ds.nir + ds.swir_1)) / ((ds.blue + ds.green) + (ds.nir + ds.swir_1))
|
||||||
|
MF_norm = (MF - MF.min(dim=["y","x"]))/(MF.max(dim=["y","x"]) - MF.min(dim=["y","x"]))
|
||||||
|
return MF_norm
|
||||||
|
def ASI(ds):
|
||||||
|
AF = af(ds)
|
||||||
|
VSF = vsf(ds)
|
||||||
|
SSF = ssf(ds)
|
||||||
|
MF = mf(ds)
|
||||||
|
return AF * VSF * SSF * MF
|
||||||
|
|
||||||
|
index_dict["ASI"] = ASI
|
||||||
|
|
||||||
|
# If index supplied is not a list, convert to list. This allows us to
|
||||||
|
# iterate through either multiple or single indices in the loop below
|
||||||
|
indices = index if isinstance(index, list) else [index]
|
||||||
|
|
||||||
|
# calculate for each index in the list of indices supplied (indexes)
|
||||||
|
for index in indices:
|
||||||
|
|
||||||
|
# Select an index function from the dictionary
|
||||||
|
index_func = index_dict.get(str(index))
|
||||||
|
|
||||||
|
# If no index is provided or if no function is returned due to an
|
||||||
|
# invalid option being provided, raise an exception informing user to
|
||||||
|
# choose from the list of valid options
|
||||||
|
if index is None:
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"No remote sensing `index` was provided. Please "
|
||||||
|
"refer to the function \ndocumentation for a full "
|
||||||
|
"list of valid options for `index` (e.g. 'NDVI')"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif (
|
||||||
|
index
|
||||||
|
in [
|
||||||
|
"WI",
|
||||||
|
"BAEI",
|
||||||
|
"AWEI_ns",
|
||||||
|
"AWEI_sh",
|
||||||
|
"EVI",
|
||||||
|
"LAI",
|
||||||
|
"SAVI",
|
||||||
|
"MSAVI",
|
||||||
|
]
|
||||||
|
and not normalise
|
||||||
|
):
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
f"\nA coefficient-based index ('{index}') normally "
|
||||||
|
"applied to surface reflectance values in the \n"
|
||||||
|
"0.0-1.0 range was applied to values in the 0-10000 "
|
||||||
|
"range. This can produce unexpected results; \nif "
|
||||||
|
"required, resolve this by setting `normalise=True`"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif index_func is None:
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"The selected index '{index}' is not one of the "
|
||||||
|
"valid remote sensing index options. \nPlease "
|
||||||
|
"refer to the function documentation for a full "
|
||||||
|
"list of valid options for `index`"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deprecation warning if `collection` is specified instead of `satellite_mission`.
|
||||||
|
if collection is not None:
|
||||||
|
warnings.warn('`collection` was deprecated in version 0.1.7. Use `satelite_mission` instead.',
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2)
|
||||||
|
# Map the collection values to the valid satellite_mission values.
|
||||||
|
if collection == "c2":
|
||||||
|
satellite_mission = "ls"
|
||||||
|
elif collection == "s2":
|
||||||
|
satellite_mission = "s2"
|
||||||
|
# Raise error if no valid collection name is provided:
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"'{collection}' is not a valid option for "
|
||||||
|
"`collection`. Please specify either \n"
|
||||||
|
"'c2' or 's2'.")
|
||||||
|
|
||||||
|
|
||||||
|
# Rename bands to a consistent format if depending on what satellite mission
|
||||||
|
# is specified in `satellite_mission`. This allows the same index calculations
|
||||||
|
# to be applied to all satellite missions. If no satellite mission was provided,
|
||||||
|
# raise an exception.
|
||||||
|
if satellite_mission is None:
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"No `satellite_mission` was provided. Please specify "
|
||||||
|
"either 'ls' or 's2' to ensure the \nfunction "
|
||||||
|
"calculates indices using the correct spectral "
|
||||||
|
"bands."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif satellite_mission == "ls":
|
||||||
|
sr_max = 1.0
|
||||||
|
# Dictionary mapping full data names to simpler alias names
|
||||||
|
# This only applies to properly-scaled "ls" data i.e. from
|
||||||
|
# the Landsat geomedians. calculate_indices will not show
|
||||||
|
# correct output for raw (unscaled) Landsat data (i.e. default
|
||||||
|
# outputs from dc.load)
|
||||||
|
bandnames_dict = {
|
||||||
|
"SR_B1": "blue",
|
||||||
|
"SR_B2": "green",
|
||||||
|
"SR_B3": "red",
|
||||||
|
"SR_B4": "nir",
|
||||||
|
"SR_B5": "swir_1",
|
||||||
|
"SR_B7": "swir_2",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rename bands in dataset to use simple names (e.g. 'red')
|
||||||
|
bands_to_rename = {
|
||||||
|
a: b for a, b in bandnames_dict.items() if a in ds.variables
|
||||||
|
}
|
||||||
|
|
||||||
|
elif satellite_mission == "s2":
|
||||||
|
sr_max = 10000
|
||||||
|
# Dictionary mapping full data names to simpler alias names
|
||||||
|
bandnames_dict = {
|
||||||
|
"nir_1": "nir",
|
||||||
|
"B02": "blue",
|
||||||
|
"B03": "green",
|
||||||
|
"B04": "red",
|
||||||
|
"B05": "red_edge_1",
|
||||||
|
"B06": "red_edge_2",
|
||||||
|
"B07": "red_edge_3",
|
||||||
|
"B08": "nir",
|
||||||
|
"B11": "swir_1",
|
||||||
|
"B12": "swir_2",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rename bands in dataset to use simple names (e.g. 'red')
|
||||||
|
bands_to_rename = {
|
||||||
|
a: b for a, b in bandnames_dict.items() if a in ds.variables
|
||||||
|
}
|
||||||
|
|
||||||
|
# Raise error if no valid satellite_mission name is provided:
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"'{satellite_mission}' is not a valid option for "
|
||||||
|
"`satellite_mission`. Please specify either \n"
|
||||||
|
"'ls' or 's2'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply index function
|
||||||
|
try:
|
||||||
|
# If normalised=True, divide data by 10,000 before applying func
|
||||||
|
mult = sr_max if normalise else 1.0
|
||||||
|
index_array = index_func(ds.rename(bands_to_rename) / mult)
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Please verify that all bands required to "
|
||||||
|
f"compute {index} are present in `ds`."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add as a new variable in dataset
|
||||||
|
output_band_name = custom_varname if custom_varname else index
|
||||||
|
ds[output_band_name] = index_array
|
||||||
|
|
||||||
|
# Once all indexes are calculated, drop input bands if drop=True
|
||||||
|
if drop:
|
||||||
|
ds = ds.drop(bands_to_drop)
|
||||||
|
|
||||||
|
# Return input dataset with added water index variable
|
||||||
|
return ds
|
||||||
|
|
||||||
|
def dualpol_indices(
|
||||||
|
ds,
|
||||||
|
co_pol='vv',
|
||||||
|
cross_pol='vh',
|
||||||
|
index=None,
|
||||||
|
custom_varname=None,
|
||||||
|
drop=False,
|
||||||
|
deep_copy=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Takes an xarray dataset containing dual-polarization radar backscatter,
|
||||||
|
calculates one or a set of indices, and adds the resulting array as a
|
||||||
|
new variable in the original dataset.
|
||||||
|
|
||||||
|
Last modified: July 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds : xarray Dataset
|
||||||
|
A two-dimensional or multi-dimensional array containing the
|
||||||
|
two polarization bands.
|
||||||
|
|
||||||
|
co_pol: str
|
||||||
|
Measurement name for the co-polarization band.
|
||||||
|
Default is 'vv' for Sentinel-1.
|
||||||
|
|
||||||
|
cross_pol: str
|
||||||
|
Measurement name for the cross-polarization band.
|
||||||
|
Default is 'vh' for Sentinel-1.
|
||||||
|
|
||||||
|
index : str or list of strs
|
||||||
|
A string giving the name of the index to calculate or a list of
|
||||||
|
strings giving the names of the indices to calculate:
|
||||||
|
|
||||||
|
* ``'RVI'`` (Radar Vegetation Index for dual-pol, Trudel et al. 2012; Nasirzadehdizaji et al., 2019; Gururaj et al., 2019)
|
||||||
|
* ``'VDDPI'`` (Vertical dual depolarization index, Periasamy 2018)
|
||||||
|
* ``'theta'`` (pseudo scattering-type, Bhogapurapu et al. 2021)
|
||||||
|
* ``'entropy'`` (pseudo scattering entropy, Bhogapurapu et al. 2021)
|
||||||
|
* ``'purity'`` (co-pol purity, Bhogapurapu et al. 2021)
|
||||||
|
* ``'ratio'`` (cross-pol/co-pol ratio)
|
||||||
|
|
||||||
|
custom_varname : str, optional
|
||||||
|
By default, the original dataset will be returned with
|
||||||
|
a new index variable named after `index` (e.g. 'RVI'). To
|
||||||
|
specify a custom name instead, you can supply e.g.
|
||||||
|
`custom_varname='custom_name'`. Defaults to None, which uses
|
||||||
|
`index` to name the variable.
|
||||||
|
|
||||||
|
drop : bool, optional
|
||||||
|
Provides the option to drop the original input data, thus saving
|
||||||
|
space. If `drop=True`, returns only the index and its values.
|
||||||
|
|
||||||
|
deep_copy: bool, optional
|
||||||
|
If `deep_copy=False`, calculate_indices will modify the original
|
||||||
|
array, adding bands to the input dataset and not removing them.
|
||||||
|
If the calculate_indices function is run more than once, variables
|
||||||
|
may be dropped incorrectly producing unexpected behaviour. This is
|
||||||
|
a bug and may be fixed in future releases. This is only a problem
|
||||||
|
when `drop=True`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ds : xarray Dataset
|
||||||
|
The original xarray Dataset inputted into the function, with a
|
||||||
|
new varible containing the remote sensing index as a DataArray.
|
||||||
|
If drop = True, the new variable/s as DataArrays in the
|
||||||
|
original Dataset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not co_pol in list(ds.data_vars):
|
||||||
|
raise ValueError(f"{co_pol} measurement is not in the dataset")
|
||||||
|
if not cross_pol in list(ds.data_vars):
|
||||||
|
raise ValueError(f"{cross_pol} measurement is not in the dataset")
|
||||||
|
|
||||||
|
# Set ds equal to a copy of itself in order to prevent the function
|
||||||
|
# from editing the input dataset. This is to prevent unexpected
|
||||||
|
# behaviour though it uses twice as much memory.
|
||||||
|
if deep_copy:
|
||||||
|
ds = ds.copy(deep=True)
|
||||||
|
|
||||||
|
# Capture input band names in order to drop these if drop=True
|
||||||
|
if drop:
|
||||||
|
bands_to_drop = list(ds.data_vars)
|
||||||
|
print(f"Dropping bands {bands_to_drop}")
|
||||||
|
|
||||||
|
def ratio(ds):
|
||||||
|
return ds[cross_pol] / ds[co_pol]
|
||||||
|
|
||||||
|
def purity(ds):
|
||||||
|
return (1 - ratio(ds)) / (1 + ratio(ds))
|
||||||
|
|
||||||
|
def theta(ds):
|
||||||
|
return np.arctan((1 - ratio(ds))**2 / (1 + ratio(ds)**2 - ratio(ds)))
|
||||||
|
|
||||||
|
def P1(ds):
|
||||||
|
return 1 / (1 + ratio(ds))
|
||||||
|
|
||||||
|
def P2(ds):
|
||||||
|
return 1 - P1(ds)
|
||||||
|
|
||||||
|
def entropy(ds):
|
||||||
|
return P1(ds)*np.log2(P1(ds)) + P2(ds)*np.log2(P2(ds))
|
||||||
|
|
||||||
|
# Dictionary containing remote sensing index band recipes
|
||||||
|
index_dict = {
|
||||||
|
# Radar Vegetation Index for dual-pol, Trudel et al. 2012
|
||||||
|
"RVI": lambda ds: 4*ds[cross_pol] / (ds[co_pol] + ds[cross_pol]),
|
||||||
|
# Vertical dual depolarization index, Periasamy 2018
|
||||||
|
"VDDPI": lambda ds: (ds[co_pol] + ds[cross_pol]) / ds[co_pol],
|
||||||
|
# cross-pol/co-pol ratio
|
||||||
|
"ratio": ratio,
|
||||||
|
# co-pol purity, Bhogapurapu et al. 2021
|
||||||
|
"purity": purity,
|
||||||
|
# pseudo scattering-type, Bhogapurapu et al. 2021
|
||||||
|
"theta": theta,
|
||||||
|
# pseudo scattering entropy, Bhogapurapu et al. 2021
|
||||||
|
"entropy": entropy,
|
||||||
|
}
|
||||||
|
|
||||||
|
# If index supplied is not a list, convert to list. This allows us to
|
||||||
|
# iterate through either multiple or single indices in the loop below
|
||||||
|
indices = index if isinstance(index, list) else [index]
|
||||||
|
|
||||||
|
# calculate for each index in the list of indices supplied (indexes)
|
||||||
|
for index in indices:
|
||||||
|
|
||||||
|
# Select an index function from the dictionary
|
||||||
|
index_func = index_dict.get(str(index))
|
||||||
|
|
||||||
|
# If no index is provided or if no function is returned due to an
|
||||||
|
# invalid option being provided, raise an exception informing user to
|
||||||
|
# choose from the list of valid options
|
||||||
|
if index is None:
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"No radar `index` was provided. Please "
|
||||||
|
"refer to the function \ndocumentation for a full "
|
||||||
|
"list of valid options for `index` (e.g. 'RVI')"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif index_func is None:
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"The selected index '{index}' is not one of the "
|
||||||
|
"valid remote sensing index options. \nPlease "
|
||||||
|
"refer to the function documentation for a full "
|
||||||
|
"list of valid options for `index`"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply index function
|
||||||
|
index_array = index_func(ds)
|
||||||
|
|
||||||
|
# Add as a new variable in dataset
|
||||||
|
output_band_name = custom_varname if custom_varname else index
|
||||||
|
ds[output_band_name] = index_array
|
||||||
|
|
||||||
|
# Once all indexes are calculated, drop input bands if drop=True
|
||||||
|
if drop:
|
||||||
|
ds = ds.drop(bands_to_drop)
|
||||||
|
|
||||||
|
# Return input dataset with added water index variable
|
||||||
|
return ds
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Functions for simplifying the creation of a local dask cluster.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from importlib.util import find_spec
|
||||||
|
import os
|
||||||
|
import dask
|
||||||
|
from aiohttp import ClientConnectionError
|
||||||
|
from datacube.utils.dask import start_local_dask
|
||||||
|
from datacube.utils.rio import configure_s3_access
|
||||||
|
|
||||||
|
_HAVE_PROXY = bool(find_spec('jupyter_server_proxy'))
|
||||||
|
_IS_AWS = ('AWS_ACCESS_KEY_ID' in os.environ or
|
||||||
|
'AWS_DEFAULT_REGION' in os.environ)
|
||||||
|
|
||||||
|
|
||||||
|
def create_local_dask_cluster(spare_mem='3Gb', display_client=True, return_client=False):
|
||||||
|
"""
|
||||||
|
Using the datacube utils function `start_local_dask`, generate
|
||||||
|
a local dask cluster. Automatically detects if on AWS or NCI.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
spare_mem : String, optional
|
||||||
|
The amount of memory, in Gb, to leave for the notebook to run.
|
||||||
|
This memory will not be used by the cluster. e.g '3Gb'
|
||||||
|
display_client : Bool, optional
|
||||||
|
An optional boolean indicating whether to display a summary of
|
||||||
|
the dask client, including a link to monitor progress of the
|
||||||
|
analysis. Set to False to hide this display.
|
||||||
|
return_client : Bool, optional
|
||||||
|
An optional boolean indicating whether to return the dask client
|
||||||
|
object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if _HAVE_PROXY:
|
||||||
|
# Configure dashboard link to go over proxy
|
||||||
|
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
|
||||||
|
dask.config.set({"distributed.dashboard.link":
|
||||||
|
prefix + "proxy/{port}/status"})
|
||||||
|
|
||||||
|
# Start up a local cluster
|
||||||
|
client = start_local_dask(mem_safety_margin=spare_mem)
|
||||||
|
|
||||||
|
if _IS_AWS:
|
||||||
|
# Configure GDAL for s3 access
|
||||||
|
configure_s3_access(aws_unsigned=True,
|
||||||
|
client=client)
|
||||||
|
|
||||||
|
# Show the dask cluster settings
|
||||||
|
if display_client:
|
||||||
|
from IPython.display import display
|
||||||
|
display(client)
|
||||||
|
|
||||||
|
# return the client as an object
|
||||||
|
if return_client:
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dask_gateway import Gateway
|
||||||
|
|
||||||
|
def create_dask_gateway_cluster(profile='r5_L', workers=2):
|
||||||
|
"""
|
||||||
|
Create a cluster in our internal dask cluster.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
profile : str
|
||||||
|
Possible values are:
|
||||||
|
- r5_L (2 cores, 15GB memory)
|
||||||
|
- r5_XL (4 cores, 31GB memory)
|
||||||
|
- r5_2XL (8 cores, 63GB memory)
|
||||||
|
- r5_4XL (16 cores, 127GB memory)
|
||||||
|
|
||||||
|
workers : int
|
||||||
|
Number of workers in the cluster.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
gateway = Gateway()
|
||||||
|
|
||||||
|
# Close any existing clusters
|
||||||
|
cluster_names = gateway.list_clusters()
|
||||||
|
if len(cluster_names) > 0:
|
||||||
|
print("Cluster(s) still running:", cluster_names)
|
||||||
|
for n in cluster_names:
|
||||||
|
cluster = gateway.connect(n.name)
|
||||||
|
cluster.shutdown()
|
||||||
|
|
||||||
|
options = gateway.cluster_options()
|
||||||
|
options['profile'] = profile
|
||||||
|
|
||||||
|
# limit username to alphanumeric characters
|
||||||
|
# kubernetes pods won't launch if labels contain anything other than [a-Z, -, _]
|
||||||
|
options['jupyterhub_user'] = ''.join(c if c.isalnum() else '-' for c in os.getenv('JUPYTERHUB_USER'))
|
||||||
|
|
||||||
|
cluster = gateway.new_cluster(options)
|
||||||
|
cluster.scale(workers)
|
||||||
|
return cluster
|
||||||
|
except ClientConnectionError:
|
||||||
|
raise ConnectionError("access to dask gateway cluster unauthorized")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
def create_dask_gateway_cluster(*args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,411 @@
|
|||||||
|
"""
|
||||||
|
Functions to retrieve ERA5 gridded climate data.
|
||||||
|
|
||||||
|
Updated Apr 2020 to directly access Zarr format data in PDS
|
||||||
|
|
||||||
|
Previous code for downloading and loading netcdf adpated from scripts by Andrew Cherry and Brian Killough.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import numpy as np
|
||||||
|
import xarray as xr
|
||||||
|
import fsspec
|
||||||
|
from datacube.utils.geometry import assign_crs
|
||||||
|
|
||||||
|
# # only used for netcdf access
|
||||||
|
# from dateutil.parser import parse
|
||||||
|
# import boto3
|
||||||
|
# import botocore
|
||||||
|
# import warnings
|
||||||
|
|
||||||
|
|
||||||
|
ERA5_VARS = ['air_pressure_at_mean_sea_level',
|
||||||
|
'air_temperature_at_2_metres',
|
||||||
|
'air_temperature_at_2_metres_1hour_Maximum',
|
||||||
|
'air_temperature_at_2_metres_1hour_Minimum',
|
||||||
|
'dew_point_temperature_at_2_metres',
|
||||||
|
'eastward_wind_at_100_metres',
|
||||||
|
'eastward_wind_at_10_metres',
|
||||||
|
'integral_wrt_time_of_surface_direct_downwelling_shortwave_flux_in_air_1hour_Accumulation',
|
||||||
|
'lwe_thickness_of_surface_snow_amount',
|
||||||
|
'northward_wind_at_100_metres',
|
||||||
|
'northward_wind_at_10_metres',
|
||||||
|
'precipitation_amount_1hour_Accumulation',
|
||||||
|
'sea_surface_temperature',
|
||||||
|
'snow_density',
|
||||||
|
'surface_air_pressure']
|
||||||
|
|
||||||
|
|
||||||
|
def load_era5(
|
||||||
|
var, lat, lon, time,
|
||||||
|
reduce_func=None,
|
||||||
|
resample="1D",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Download and return an ERA5 variable for a defined time window.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
var : string
|
||||||
|
Name of the ERA5 climate variable to download, e.g "air_temperature_at_2_metres"
|
||||||
|
|
||||||
|
lat: tuple or list
|
||||||
|
Latitude range for query.
|
||||||
|
|
||||||
|
lon: tuple or list
|
||||||
|
Longitude range for query.
|
||||||
|
|
||||||
|
time: string or datetime object or a list or tuple of strings or datetime objects
|
||||||
|
Used to define starting and end date dates of the time window.
|
||||||
|
|
||||||
|
reduce_func: numpy function
|
||||||
|
lets you specify a function to apply to each day's worth of data.
|
||||||
|
The default is np.mean, which computes daily average. To get a sum, use np.sum.
|
||||||
|
|
||||||
|
resample: string
|
||||||
|
Temporal resampling frequency to be used for xarray's resample function.
|
||||||
|
The default is '1D', which is daily.
|
||||||
|
Since this is applied on monthly ERA5 data, maximum resampling period is '1M'.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
A lazy-loaded xarray dataset containing an ERA5 variable for the selected region and time window.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# constrain query to available variables
|
||||||
|
assert var in ERA5_VARS, "var must be one of [{}] (got {})".format(
|
||||||
|
",".join(ERA5_VARS), var
|
||||||
|
)
|
||||||
|
|
||||||
|
# set default reduction function
|
||||||
|
if reduce_func is None:
|
||||||
|
reduce_func = np.mean
|
||||||
|
|
||||||
|
# process date range
|
||||||
|
if type(time) in [list, tuple]:
|
||||||
|
date_from = np.datetime64(min(time)).astype('datetime64[D]')
|
||||||
|
date_to = (np.datetime64(max(time))+1).astype('datetime64[D]')-np.timedelta64(1,'D')
|
||||||
|
elif type(time) in [str, np.datetime64]:
|
||||||
|
date_from = np.datetime64(time).astype('datetime64[D]')
|
||||||
|
date_to = (np.datetime64(time)+1).astype('datetime64[D]')-np.timedelta64(1,'D')
|
||||||
|
else:
|
||||||
|
raise(ValueError)
|
||||||
|
|
||||||
|
# actual lat lon ranges will be infered from nearest match to data
|
||||||
|
lat_range = None
|
||||||
|
lon_range = None
|
||||||
|
|
||||||
|
datasets = []
|
||||||
|
# Loop through month and year to access ERA5 zarr
|
||||||
|
month = date_from.astype('datetime64[M]')
|
||||||
|
while month <= date_to.astype('datetime64[M]'):
|
||||||
|
url = f"s3://era5-pds/zarr/{month.astype(object).year:04}/{month.astype(object).month:02}/data/{var}.zarr"
|
||||||
|
ds = xr.open_zarr(fsspec.get_mapper(url, anon=True,
|
||||||
|
client_kwargs={'region_name':'us-east-1'}),
|
||||||
|
consolidated=True)
|
||||||
|
|
||||||
|
# re-order along longitude to go from -180 to 180 if needed
|
||||||
|
if min(lon) < 0:
|
||||||
|
ds = ds.assign_coords({"lon": (((ds.lon + 180) % 360) - 180)})
|
||||||
|
ds = ds.reindex({"lon": np.sort(ds.lon)})
|
||||||
|
|
||||||
|
if lat_range is None:
|
||||||
|
# find the nearest lat lon boundary points
|
||||||
|
test = ds.sel(lat=list(lat), lon=list(lon), method="nearest")
|
||||||
|
# define the lat/lon grid
|
||||||
|
lat_range = slice(test.lat.max().values, test.lat.min().values)
|
||||||
|
lon_range = slice(test.lon.min().values, test.lon.max().values)
|
||||||
|
|
||||||
|
if "time0" in ds.dims:
|
||||||
|
ds = ds.rename({"time0": "time"})
|
||||||
|
if "time1" in ds.dims:
|
||||||
|
ds = ds.rename(
|
||||||
|
{"time1": "time"}
|
||||||
|
) # This should INTENTIONALLY error if both times are defined
|
||||||
|
|
||||||
|
output = ds[[var]].sel(lat=lat_range, lon=lon_range, time=slice(date_from, date_to)).resample(time=resample).reduce(reduce_func)
|
||||||
|
output.attrs = ds.attrs
|
||||||
|
for v in output.data_vars:
|
||||||
|
output[v].attrs = ds[v].attrs
|
||||||
|
|
||||||
|
datasets.append(output)
|
||||||
|
month += np.timedelta64(1,'M')
|
||||||
|
|
||||||
|
return assign_crs(xr.combine_by_coords(datasets), 'EPSG:4326')
|
||||||
|
|
||||||
|
|
||||||
|
# # older version of scripts to download and use netcdf
|
||||||
|
|
||||||
|
# ERA5_VARS_NC = [
|
||||||
|
# "air_pressure_at_mean_sea_level",
|
||||||
|
# "air_temperature_at_2_metres",
|
||||||
|
# "air_temperature_at_2_metres_1hour_Maximum",
|
||||||
|
# "air_temperature_at_2_metres_1hour_Minimum",
|
||||||
|
# "dew_point_temperature_at_2_metres",
|
||||||
|
# "eastward_wind_at_100_metres",
|
||||||
|
# "eastward_wind_at_10_metres",
|
||||||
|
# "integral_wrt_time_of_surface_direct_downwelling_shortwave_flux_in_air_1hour_Accumulation",
|
||||||
|
# "lwe_thickness_of_surface_snow_amount",
|
||||||
|
# "northward_wind_at_100_metres",
|
||||||
|
# "northward_wind_at_10_metres",
|
||||||
|
# "precipitation_amount_1hour_Accumulation",
|
||||||
|
# "sea_surface_temperature",
|
||||||
|
# "sea_surface_wave_from_direction",
|
||||||
|
# "sea_surface_wave_mean_period",
|
||||||
|
# "significant_height_of_wind_and_swell_waves",
|
||||||
|
# "snow_density",
|
||||||
|
# "surface_air_pressure",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
# def get_era5_daily(
|
||||||
|
# var,
|
||||||
|
# date_from_arg,
|
||||||
|
# date_to_arg=None,
|
||||||
|
# reduce_func=None,
|
||||||
|
# cache_dir="era5",
|
||||||
|
# resample="1D",
|
||||||
|
# ):
|
||||||
|
# """
|
||||||
|
# Download and return an ERA5 variable for a defined time window.
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
# ----------
|
||||||
|
# var : string
|
||||||
|
# Name of the ERA5 climate variable to download, e.g "air_temperature_at_2_metres"
|
||||||
|
|
||||||
|
# date_from_arg: string or datetime object
|
||||||
|
# Starting date of the time window.
|
||||||
|
|
||||||
|
# date_to_arg: string or datetime object
|
||||||
|
# End date of the time window. If not supplied, set to be the same as starting date.
|
||||||
|
|
||||||
|
# reduce_func: numpy function
|
||||||
|
# lets you specify a function to apply to each day's worth of data.
|
||||||
|
# The default is np.mean, which computes daily average. To get a sum, use np.sum.
|
||||||
|
|
||||||
|
# cache_dir: sting
|
||||||
|
# Path to save downloaded ERA5 data. The path will be created if not already exists.
|
||||||
|
# The default is 'era5'.
|
||||||
|
|
||||||
|
# resample: string
|
||||||
|
# Temporal resampling frequency to be used for xarray's resample function.
|
||||||
|
# The default is '1D', which is daily.
|
||||||
|
# Since ERA5 data is provided as one file per month, maximum resampling period is '1M'.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
# -------
|
||||||
|
# A lazy-loaded xarray dataset containing an ERA5 variable for the selected time window.
|
||||||
|
|
||||||
|
# """
|
||||||
|
|
||||||
|
# # Massage input data
|
||||||
|
# assert var in ERA5_VARS_NC, "var must be one of [{}] (got {})".format(
|
||||||
|
# ",".join(ERA5_VARS_NC), var
|
||||||
|
# )
|
||||||
|
# if not os.path.exists(cache_dir):
|
||||||
|
# os.mkdir(cache_dir)
|
||||||
|
# if reduce_func is None:
|
||||||
|
# reduce_func = np.mean
|
||||||
|
# if type(date_from_arg) == str:
|
||||||
|
# date_from_arg = parse(date_from_arg)
|
||||||
|
# if type(date_to_arg) == str:
|
||||||
|
# date_to_arg = parse(date_to_arg)
|
||||||
|
# if date_to_arg is None:
|
||||||
|
# date_to_arg = date_from_arg
|
||||||
|
# # Make sure our dates are in the correct order
|
||||||
|
# from_date = min(date_from_arg, date_to_arg)
|
||||||
|
# to_date = max(date_from_arg, date_to_arg)
|
||||||
|
# # Download ERA5 files to local cache if they don't already exist
|
||||||
|
# client = None # Boto client (if needed)
|
||||||
|
# local_files = [] # Will hold list of local filenames
|
||||||
|
# Y, M = from_date.year, from_date.month # Loop vars
|
||||||
|
# loop_end = to_date.year * 12 + to_date.month # Loop sentinel
|
||||||
|
# while Y * 12 + M <= loop_end:
|
||||||
|
# local_file = os.path.join(
|
||||||
|
# cache_dir, "{Y:04}_{M:02}_{var}.nc".format(Y=Y, M=M, var=var)
|
||||||
|
# )
|
||||||
|
# data_key = "{Y:04}/{M:02}/data/{var}.nc".format(Y=Y, M=M, var=var)
|
||||||
|
# if not os.path.isfile(
|
||||||
|
# local_file
|
||||||
|
# ): # check if file already exists (TODO: move to temp?) (TODO: catch failed download)
|
||||||
|
# if client is None:
|
||||||
|
# client = boto3.client(
|
||||||
|
# "s3",
|
||||||
|
# config=botocore.client.Config(signature_version=botocore.UNSIGNED),
|
||||||
|
# )
|
||||||
|
# client.download_file("era5-pds", data_key, local_file)
|
||||||
|
# local_files.append(local_file)
|
||||||
|
# if M == 12:
|
||||||
|
# Y += 1
|
||||||
|
# M = 1
|
||||||
|
# else:
|
||||||
|
# M += 1
|
||||||
|
# # Load and merge the locally-cached ERA5 data from the list of filenames
|
||||||
|
# date_slice = slice(
|
||||||
|
# str(from_date.date()), str(to_date.date())
|
||||||
|
# ) # I do this to INCLUDE the whole end date, not just 00:00
|
||||||
|
|
||||||
|
# def prepro(ds):
|
||||||
|
# if "time0" in ds.dims:
|
||||||
|
# ds = ds.rename({"time0": "time"})
|
||||||
|
# if "time1" in ds.dims:
|
||||||
|
# ds = ds.rename(
|
||||||
|
# {"time1": "time"}
|
||||||
|
# ) # This should INTENTIONALLY error if both times are defined
|
||||||
|
# ds = ds[[var]]
|
||||||
|
# output = ds.sel(time=date_slice).resample(time=resample).reduce(reduce_func)
|
||||||
|
# output.attrs = ds.attrs
|
||||||
|
# for v in output.data_vars:
|
||||||
|
# output[v].attrs = ds[v].attrs
|
||||||
|
# return output
|
||||||
|
|
||||||
|
# return xr.open_mfdataset(
|
||||||
|
# local_files,
|
||||||
|
# combine="by_coords",
|
||||||
|
# compat="equals",
|
||||||
|
# preprocess=prepro,
|
||||||
|
# parallel=True,
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
# def era5_area_crop(ds, lat, lon):
|
||||||
|
# """
|
||||||
|
# Crop a dataset containing EAR5 variables to a location.
|
||||||
|
# The output spatial grid will either include input grid points within lat/lon boundaries or the nearest point if none is within the search location.
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
# ----------
|
||||||
|
# ds : xarray dataset
|
||||||
|
# A dataset containing ERA5 variables of interest.
|
||||||
|
|
||||||
|
# lat: tuple or list
|
||||||
|
# Latitude range for query.
|
||||||
|
|
||||||
|
# lon: tuple or list
|
||||||
|
# Longitude range for query.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
# -------
|
||||||
|
# An xarray dataset containing ERA5 variables for the selected location.
|
||||||
|
|
||||||
|
# """
|
||||||
|
|
||||||
|
# # Handle single value lat/lon args by wrapping them in lists
|
||||||
|
# try:
|
||||||
|
# min(lat)
|
||||||
|
# except TypeError:
|
||||||
|
# lat = [lat]
|
||||||
|
# try:
|
||||||
|
# min(lon)
|
||||||
|
# except TypeError:
|
||||||
|
# lon = [lon]
|
||||||
|
# if min(lon) < 0:
|
||||||
|
# # re-order along longitude to go from -180 to 180
|
||||||
|
# ds = ds.assign_coords({"lon": (((ds.lon + 180) % 360) - 180)})
|
||||||
|
# ds = ds.reindex({"lon": np.sort(ds.lon)})
|
||||||
|
# # Issue warnings if args outside range.
|
||||||
|
# if min(lat) < ds.lat.min() or max(lat) > ds.lat.max():
|
||||||
|
# warnings.warn(
|
||||||
|
# "Lats must be in range {} .. {}. Got: {}".format(
|
||||||
|
# ds.lat.min().values, ds.lat.max().values, lat
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
# if min(lon) < ds.lon.min() or max(lon) > ds.lon.max():
|
||||||
|
# warnings.warn(
|
||||||
|
# "Lons must be in range {} .. {}. Got: {}".format(
|
||||||
|
# ds.lon.min().values, ds.lon.max().values, lon
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
# # Find existing coords between min&max
|
||||||
|
# lats = ds.lat[np.logical_and(ds.lat >= min(lat), ds.lat <= max(lat))].values
|
||||||
|
# # If there was nothing between, just plan to grab closest
|
||||||
|
# if len(lats) == 0:
|
||||||
|
# lats = np.unique(ds.lat.sel(lat=np.array(lat), method="nearest"))
|
||||||
|
# lons = ds.lon[np.logical_and(ds.lon >= min(lon), ds.lon <= max(lon))].values
|
||||||
|
# if len(lons) == 0:
|
||||||
|
# lons = np.unique(ds.lon.sel(lon=np.array(lon), method="nearest"))
|
||||||
|
# # crop and keep attrs
|
||||||
|
# output = ds.sel(lat=lats, lon=lons)
|
||||||
|
# output.attrs = ds.attrs
|
||||||
|
# for var in output.data_vars:
|
||||||
|
# output[var].attrs = ds[var].attrs
|
||||||
|
# return output
|
||||||
|
|
||||||
|
|
||||||
|
# def era5_area_nearest(ds, lat, lon):
|
||||||
|
# """
|
||||||
|
# Crop a dataset containing EAR5 variables to a location.
|
||||||
|
# The output spatial grid is snapped to the nearest input grid points.
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
# ----------
|
||||||
|
# ds : xarray dataset
|
||||||
|
# A dataset containing ERA5 variables of interest.
|
||||||
|
|
||||||
|
# lat: tuple or list
|
||||||
|
# Latitude range for query.
|
||||||
|
|
||||||
|
# lon: tuple or list
|
||||||
|
# Longitude range for query.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
# -------
|
||||||
|
# An xarray dataset containing ERA5 variables for the selected location.
|
||||||
|
|
||||||
|
# """
|
||||||
|
|
||||||
|
# if min(lon) < 0:
|
||||||
|
# # re-order along longitude to go from -180 to 180
|
||||||
|
# ds = ds.assign_coords({"lon": (((ds.lon + 180) % 360) - 180)})
|
||||||
|
# ds = ds.reindex({"lon": np.sort(ds.lon)})
|
||||||
|
|
||||||
|
# # find the nearest lat lon boundary points
|
||||||
|
# test = ds.sel(lat=lat, lon=lon, method="nearest")
|
||||||
|
# # define the lat/lon grid
|
||||||
|
# lat_range = slice(test.lat.max().values, test.lat.min().values)
|
||||||
|
# lon_range = slice(test.lon.min().values, test.lon.max().values)
|
||||||
|
# # crop and keep attrs
|
||||||
|
# output = ds.sel(lat=lat_range, lon=lon_range)
|
||||||
|
# output.attrs = ds.attrs
|
||||||
|
# for var in output.data_vars:
|
||||||
|
# output[var].attrs = ds[var].attrs
|
||||||
|
# return output
|
||||||
|
|
||||||
|
|
||||||
|
# def load_era5_netcdf(var, lat, lon, time, grid="nearest", **kwargs):
|
||||||
|
# """
|
||||||
|
# Returns a ERA5 variable for a selected location and time window.
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
# ----------
|
||||||
|
# var : string
|
||||||
|
# Name of the ERA5 climate variable to download, e.g "air_temperature_at_2_metres"
|
||||||
|
|
||||||
|
# lat: tuple or list
|
||||||
|
# Latitude range for query.
|
||||||
|
|
||||||
|
# lon: tuple or list
|
||||||
|
# Longitude range for query.
|
||||||
|
|
||||||
|
# time: tuple or list
|
||||||
|
# Time range for query.
|
||||||
|
|
||||||
|
# grid: string
|
||||||
|
# Option for output spatial gridding.
|
||||||
|
# The default is 'nearest', for which output spatial grid is snapped to the nearest ERA5 input grid points.
|
||||||
|
# Alternatively, output spatial grid will either include input grid points within lat/lon boundaries or the nearest point if none is within the search location.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
# -------
|
||||||
|
# An xarray dataset containing the variable for the selected location and time window.
|
||||||
|
|
||||||
|
# """
|
||||||
|
|
||||||
|
# ds = get_era5_daily(var, time[0], time[1], **kwargs)
|
||||||
|
# if grid == "nearest":
|
||||||
|
# return era5_area_nearest(ds, lat, lon).compute()
|
||||||
|
# else:
|
||||||
|
# return era5_area_crop(ds, lat, lon).compute()
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
Functions to retrieve iSDAsoil data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import xarray as xr
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.patches as mpatches
|
||||||
|
import rasterio as rio
|
||||||
|
from pyproj import Transformer
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import boto3
|
||||||
|
from pystac import stac_io, Catalog
|
||||||
|
|
||||||
|
#this function allows us to directly query the data on s3, adapted from iSDA tutorial https://github.com/iSDA-Africa/isdasoil-tutorial/blob/main/iSDAsoil-tutorial.ipynb
|
||||||
|
def my_read_method(uri):
|
||||||
|
parsed = urlparse(uri)
|
||||||
|
if parsed.scheme == 's3':
|
||||||
|
bucket = parsed.netloc
|
||||||
|
key = parsed.path[1:]
|
||||||
|
s3 = boto3.resource('s3')
|
||||||
|
obj = s3.Object(bucket, key)
|
||||||
|
return obj.get()['Body'].read().decode('utf-8')
|
||||||
|
else:
|
||||||
|
return stac_io.default_read_text_method(uri)
|
||||||
|
|
||||||
|
stac_io.read_text_method = my_read_method
|
||||||
|
|
||||||
|
catalog = Catalog.from_file("https://isdasoil.s3.amazonaws.com/catalog.json")
|
||||||
|
|
||||||
|
assets = {}
|
||||||
|
|
||||||
|
for root, catalogs, items in catalog.walk():
|
||||||
|
for item in items:
|
||||||
|
str(f"Type: {item.get_parent().title}")
|
||||||
|
# save all items to a dictionary as we go along
|
||||||
|
assets[item.id] = item
|
||||||
|
for asset in item.assets.values():
|
||||||
|
if asset.roles == ['data']:
|
||||||
|
str(f"Title: {asset.title}")
|
||||||
|
str(f"Description: {asset.description}")
|
||||||
|
str(f"URL: {asset.href}")
|
||||||
|
str("------------")
|
||||||
|
|
||||||
|
# define load_isda() function
|
||||||
|
|
||||||
|
def load_isda(var, lat, lon):
|
||||||
|
"""
|
||||||
|
Download and return iSDA variable with number of bands corresponding to number of iSDA layers.
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
var : string
|
||||||
|
Name of the iSDA variable to download, e.g "ph"
|
||||||
|
lat: tuple or list
|
||||||
|
Latitude range for query.
|
||||||
|
lon: tuple or list
|
||||||
|
Longitude range for query.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bands = assets[var].assets["image"].extra_fields.get('eo:bands')
|
||||||
|
bands = [val['description'] for val in bands]
|
||||||
|
|
||||||
|
if len(np.unique(bands)) > 1:
|
||||||
|
|
||||||
|
ds = xr.open_dataset(assets[var].assets["image"].href, engine="rasterio").rio.clip_box(
|
||||||
|
minx=lon[0],
|
||||||
|
miny=lat[0],
|
||||||
|
maxx=lon[1],
|
||||||
|
maxy=lat[1],
|
||||||
|
crs="EPSG:4326",
|
||||||
|
)
|
||||||
|
|
||||||
|
ds_layered = ds.drop_dims('band')
|
||||||
|
for x in np.unique(ds.band):
|
||||||
|
ds_layered[bands[x-1]] = ds.sel(band=x).to_array(dim='band').squeeze()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
ds_layered = xr.open_dataset(assets[var].assets["image"].href, engine="rasterio").rio.clip_box(
|
||||||
|
minx=lon[0],
|
||||||
|
miny=lat[0],
|
||||||
|
maxx=lon[1],
|
||||||
|
maxy=lat[1],
|
||||||
|
crs="EPSG:4326",
|
||||||
|
).squeeze()
|
||||||
|
|
||||||
|
return ds_layered
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import xarray as xr
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# function to load soil moisture data
|
||||||
|
|
||||||
|
def load_soil_moisture(lat, lon, time, product = 'surface', grid = 'nearest'):
|
||||||
|
product_baseurl = 'https://dapds00.nci.org.au/thredds/dodsC/ub8/global/GRAFS/'
|
||||||
|
assert product in ['surface', 'rootzone'], 'product parameter must be surface or root-zone'
|
||||||
|
# lat, lon grid
|
||||||
|
if grid == 'nearest':
|
||||||
|
# select lat/lon range from data; snap to nearest grid
|
||||||
|
lat_range, lon_range = None, None
|
||||||
|
else:
|
||||||
|
# define a grid that covers the entire area of interest
|
||||||
|
lat_range = np.arange(np.max(np.ceil(np.array(lat)*10.+0.5)/10.-0.05), np.min(np.floor(np.array(lat)*10.-0.5)/10.+0.05)-0.05, -0.1)
|
||||||
|
lon_range = np.arange(np.min(np.floor(np.array(lon)*10.-0.5)/10.+0.05), np.max(np.ceil(np.array(lon)*10.+0.5)/10.-0.05)+0.05, 0.1)
|
||||||
|
# split time window into years
|
||||||
|
day_range = np.array(time).astype("M8[D]")
|
||||||
|
year_range = np.array(time).astype("M8[Y]")
|
||||||
|
if product == 'surface':
|
||||||
|
product_name = 'GRAFS_TopSoilRelativeWetness_'
|
||||||
|
else: product_name = 'GRAFS_RootzoneSoilWaterIndex_'
|
||||||
|
datasets = []
|
||||||
|
for year in np.arange(year_range[0], year_range[1]+1, np.timedelta64(1, 'Y')):
|
||||||
|
start = np.max([day_range[0], year.astype("M8[D]")])
|
||||||
|
end = np.min([day_range[1], (year+1).astype("M8[D]")-1])
|
||||||
|
product_url = product_baseurl + product_name +'%s.nc'%str(year)
|
||||||
|
print(product_url)
|
||||||
|
# data is loaded lazily through OPeNDAP
|
||||||
|
ds = xr.open_dataset(product_url)
|
||||||
|
if lat_range is None:
|
||||||
|
# select lat/lon range from data if not specified; snap to nearest grid
|
||||||
|
test = ds.sel(lat=list(lat), lon=list(lon), method='nearest')
|
||||||
|
lat_range = slice(test.lat.values[0], test.lat.values[1])
|
||||||
|
lon_range = slice(test.lon.values[0], test.lon.values[1])
|
||||||
|
# slice before return
|
||||||
|
ds = ds.sel(lat=lat_range, lon=lon_range, time=slice(start, end)).compute()
|
||||||
|
datasets.append(ds)
|
||||||
|
return xr.merge(datasets)
|
||||||
Binary file not shown.
@@ -0,0 +1,117 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: POEditor.com\n"
|
||||||
|
"Project-Id-Version: deafrica_tools\n"
|
||||||
|
"Language: fr\n"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:83
|
||||||
|
msgid "None"
|
||||||
|
msgstr "Aucun"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:84
|
||||||
|
msgid "ESRI World Imagery"
|
||||||
|
msgstr "Imagerie mondiale ESRI"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:85
|
||||||
|
msgid "Sentinel-2 Geomedian"
|
||||||
|
msgstr "Sentinel-2 Geomedian"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:86
|
||||||
|
msgid "Water Observations from Space"
|
||||||
|
msgstr "Observations de l'eau depuis l'espace-WOfS"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:99
|
||||||
|
msgid "Wetlands Insight Tool"
|
||||||
|
msgstr "Outil d'analyse des zones humides"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:100
|
||||||
|
msgid "Select parameters and AOI"
|
||||||
|
msgstr "Sélectionner les paramètres et la zone d'intérêt"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:124
|
||||||
|
msgid "Total polygon area"
|
||||||
|
msgstr "Superficie totale du polygone"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:128
|
||||||
|
msgid "Area falls within recommended limit"
|
||||||
|
msgstr "La zone se situe dans la limite recommandée"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:131
|
||||||
|
msgid "Area is too large, please update your polygon"
|
||||||
|
msgstr "La zone est trop grande, veuillez réduire votre polygone"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:151
|
||||||
|
msgid "Map Overlays"
|
||||||
|
msgstr "Superpositions de cartes"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:176
|
||||||
|
msgid "Run"
|
||||||
|
msgstr "Exécuter"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:183
|
||||||
|
msgid "Map Overlay:"
|
||||||
|
msgstr "Carte superposée :"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:185
|
||||||
|
msgid "Start Date:"
|
||||||
|
msgstr "Date de début :"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:187
|
||||||
|
msgid "End Date:"
|
||||||
|
msgstr "Date de fin :"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:189
|
||||||
|
msgid "Minimum Good Data:"
|
||||||
|
msgstr "Minimum de bonnes données :"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:191
|
||||||
|
msgid "Resampling Frequency:"
|
||||||
|
msgstr "Fréquence de rééchantillonnage :"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:193
|
||||||
|
msgid "Output CSV:"
|
||||||
|
msgstr "Sortie CSV :"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:195
|
||||||
|
msgid "Output Plot:"
|
||||||
|
msgstr "Tracé de sortie :"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:308
|
||||||
|
msgid "Progress"
|
||||||
|
msgstr "Progrès"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:326
|
||||||
|
msgid "WIT complete"
|
||||||
|
msgstr "WIT achevée"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:328
|
||||||
|
msgid "No polygon selected"
|
||||||
|
msgstr "Aucun polygone sélectionné"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:365
|
||||||
|
msgid "open water"
|
||||||
|
msgstr "eau libre"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:366
|
||||||
|
msgid "wet"
|
||||||
|
msgstr "humide"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:367
|
||||||
|
msgid "green veg"
|
||||||
|
msgstr "végétation verts"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:368
|
||||||
|
msgid "dry veg"
|
||||||
|
msgstr "végétation seche"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:369
|
||||||
|
msgid "bare soil"
|
||||||
|
msgstr "sol nu"
|
||||||
|
|
||||||
|
#: Tools/deafrica_tools/app/wetlandsinsighttool.py:382
|
||||||
|
msgid "Percentage Fractional Cover, Wetness, and Water"
|
||||||
|
msgstr "Pourcentage de couverture fractionnée, humidité et eau"
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,949 @@
|
|||||||
|
'''
|
||||||
|
Spatial analyses functions for Digital Earth Africa data.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# 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 multiprocessing as mp
|
||||||
|
|
||||||
|
import dask
|
||||||
|
import fiona
|
||||||
|
import geopandas as gpd
|
||||||
|
import numpy as np
|
||||||
|
import odc.geo.xr # adds `.odc.x` attributes to our xarray objects.
|
||||||
|
import pandas as pd
|
||||||
|
import rasterio.features
|
||||||
|
import scipy.interpolate
|
||||||
|
import xarray as xr
|
||||||
|
from datacube.api.query import query_group_by
|
||||||
|
from datacube.model.utils import xr_apply
|
||||||
|
from datacube.utils.cog import write_cog
|
||||||
|
from datacube.utils.geometry import CRS, Geometry
|
||||||
|
from geopy.geocoders import Nominatim
|
||||||
|
from rasterstats import zonal_stats
|
||||||
|
from shapely.geometry import LineString, MultiLineString, mapping, shape
|
||||||
|
from skimage.measure import find_contours, label
|
||||||
|
|
||||||
|
|
||||||
|
def add_geobox(ds, crs=None):
|
||||||
|
"""
|
||||||
|
Ensure that an xarray DataArray has a GeoBox and .odc.* accessor
|
||||||
|
using `odc.geo`.
|
||||||
|
|
||||||
|
If `ds` is missing a Coordinate Reference System (CRS), this can be
|
||||||
|
supplied using the `crs` param.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds : xarray.Dataset or xarray.DataArray
|
||||||
|
Input xarray object that needs to be checked for spatial
|
||||||
|
information.
|
||||||
|
crs : str, optional
|
||||||
|
Coordinate Reference System (CRS) information for the input `ds`
|
||||||
|
array. If `ds` already has a CRS, then `crs` is not required.
|
||||||
|
Default is None.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
xarray.Dataset or xarray.DataArray
|
||||||
|
The input xarray object with added `.odc.x` attributes to access
|
||||||
|
spatial information.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If a CRS is not found, use custom provided CRS
|
||||||
|
if ds.odc.crs is None and crs is not None:
|
||||||
|
ds = ds.odc.assign_crs(crs)
|
||||||
|
elif ds.odc.crs is None and crs is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Unable to determine `ds`'s coordinate "
|
||||||
|
"reference system (CRS). Please provide a "
|
||||||
|
"CRS using the `crs` parameter "
|
||||||
|
"(e.g. `crs='EPSG:3577'`)."
|
||||||
|
)
|
||||||
|
|
||||||
|
return ds
|
||||||
|
|
||||||
|
|
||||||
|
def xr_vectorize(
|
||||||
|
da,
|
||||||
|
attribute_col=None,
|
||||||
|
crs=None,
|
||||||
|
dtype="float32",
|
||||||
|
output_path=None,
|
||||||
|
verbose=True,
|
||||||
|
**rasterio_kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Vectorises a raster ``xarray.DataArray`` into a vector
|
||||||
|
``geopandas.GeoDataFrame``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
da : xarray.DataArray
|
||||||
|
The input ``xarray.DataArray`` data to vectorise.
|
||||||
|
attribute_col : str, optional
|
||||||
|
Name of the attribute column in the resulting
|
||||||
|
``geopandas.GeoDataFrame``. Values from ``da`` converted
|
||||||
|
to polygons will be assigned to this column. If None,
|
||||||
|
the column name will default to 'attribute'.
|
||||||
|
crs : str or CRS object, optional
|
||||||
|
If ``da``'s coordinate reference system (CRS) cannot be
|
||||||
|
determined, provide a CRS using this parameter.
|
||||||
|
(e.g. 'EPSG:3577').
|
||||||
|
dtype : str, optional
|
||||||
|
Data type of must be one of int16, int32, uint8, uint16,
|
||||||
|
or float32
|
||||||
|
output_path : string, optional
|
||||||
|
Provide an optional string file path to export the vectorised
|
||||||
|
data to file. Supports any vector file formats supported by
|
||||||
|
``geopandas.GeoDataFrame.to_file()``.
|
||||||
|
verbose : bool, optional
|
||||||
|
Print debugging messages. Default True.
|
||||||
|
**rasterio_kwargs :
|
||||||
|
A set of keyword arguments to ``rasterio.features.shapes``.
|
||||||
|
Can include `mask` and `connectivity`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
gdf : geopandas.GeoDataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add GeoBox and odc.* accessor to array using `odc-geo`
|
||||||
|
da = add_geobox(da, crs)
|
||||||
|
|
||||||
|
# Run the vectorizing function
|
||||||
|
vectors = rasterio.features.shapes(
|
||||||
|
source=da.data.astype(dtype), transform=da.odc.transform, **rasterio_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert the generator into a list
|
||||||
|
vectors = list(vectors)
|
||||||
|
|
||||||
|
# Extract the polygon coordinates and values from the list
|
||||||
|
polygons = [polygon for polygon, value in vectors]
|
||||||
|
values = [value for polygon, value in vectors]
|
||||||
|
|
||||||
|
# Convert polygon coordinates into polygon shapes
|
||||||
|
polygons = [shape(polygon) for polygon in polygons]
|
||||||
|
|
||||||
|
# Create a geopandas dataframe populated with the polygon shapes
|
||||||
|
attribute_name = attribute_col if attribute_col is not None else "attribute"
|
||||||
|
gdf = gpd.GeoDataFrame(
|
||||||
|
data={attribute_name: values}, geometry=polygons, crs=da.odc.crs
|
||||||
|
)
|
||||||
|
|
||||||
|
# If a file path is supplied, export to file
|
||||||
|
if output_path is not None:
|
||||||
|
if verbose:
|
||||||
|
print(f"Exporting vector data to {output_path}")
|
||||||
|
gdf.to_file(output_path)
|
||||||
|
|
||||||
|
return gdf
|
||||||
|
|
||||||
|
|
||||||
|
def xr_rasterize(
|
||||||
|
gdf,
|
||||||
|
da,
|
||||||
|
attribute_col=None,
|
||||||
|
crs=None,
|
||||||
|
name=None,
|
||||||
|
output_path=None,
|
||||||
|
verbose=True,
|
||||||
|
**rasterio_kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Rasterizes a vector ``geopandas.GeoDataFrame`` into a
|
||||||
|
raster ``xarray.DataArray``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
gdf : geopandas.GeoDataFrame
|
||||||
|
A ``geopandas.GeoDataFrame`` object containing the vector
|
||||||
|
data you want to rasterise.
|
||||||
|
da : xarray.DataArray or xarray.Dataset
|
||||||
|
The shape, coordinates, dimensions, and transform of this object
|
||||||
|
are used to define the array that ``gdf`` is rasterized into.
|
||||||
|
It effectively provides a spatial template.
|
||||||
|
attribute_col : string, optional
|
||||||
|
Name of the attribute column in ``gdf`` containing values for
|
||||||
|
each vector feature that will be rasterized. If None, the
|
||||||
|
output will be a boolean array of 1's and 0's.
|
||||||
|
crs : str or CRS object, optional
|
||||||
|
If ``da``'s coordinate reference system (CRS) cannot be
|
||||||
|
determined, provide a CRS using this parameter.
|
||||||
|
(e.g. 'EPSG:3577').
|
||||||
|
name : str, optional
|
||||||
|
An optional name used for the output ``xarray.DataArray`.
|
||||||
|
output_path : string, optional
|
||||||
|
Provide an optional string file path to export the rasterized
|
||||||
|
data as a GeoTIFF file.
|
||||||
|
verbose : bool, optional
|
||||||
|
Print debugging messages. Default True.
|
||||||
|
**rasterio_kwargs :
|
||||||
|
A set of keyword arguments to ``rasterio.features.rasterize``.
|
||||||
|
Can include: 'all_touched', 'merge_alg', 'dtype'.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
da_rasterized : xarray.DataArray
|
||||||
|
The rasterized vector data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add GeoBox and odc.* accessor to array using `odc-geo`
|
||||||
|
da = add_geobox(da, crs)
|
||||||
|
|
||||||
|
# Reproject vector data to raster's CRS
|
||||||
|
gdf_reproj = gdf.to_crs(crs=da.odc.crs)
|
||||||
|
|
||||||
|
# If an attribute column is specified, rasterise using vector
|
||||||
|
# attribute values. Otherwise, rasterise into a boolean array
|
||||||
|
if attribute_col is not None:
|
||||||
|
# Use the geometry and attributes from `gdf` to create an iterable
|
||||||
|
shapes = zip(gdf_reproj.geometry, gdf_reproj[attribute_col])
|
||||||
|
else:
|
||||||
|
# Use geometry directly (will produce a boolean numpy array)
|
||||||
|
shapes = gdf_reproj.geometry
|
||||||
|
|
||||||
|
# Rasterise shapes into a numpy array
|
||||||
|
im = rasterio.features.rasterize(
|
||||||
|
shapes=shapes,
|
||||||
|
out_shape=da.odc.geobox.shape,
|
||||||
|
transform=da.odc.geobox.transform,
|
||||||
|
**rasterio_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert numpy array to a full xarray.DataArray
|
||||||
|
# and set array name if supplied
|
||||||
|
da_rasterized = odc.geo.xr.wrap_xr(im=im, gbox=da.odc.geobox)
|
||||||
|
da_rasterized = da_rasterized.rename(name)
|
||||||
|
|
||||||
|
# If a file path is supplied, export to file
|
||||||
|
if output_path is not None:
|
||||||
|
if verbose:
|
||||||
|
print(f"Exporting raster data to {output_path}")
|
||||||
|
write_cog(da_rasterized, output_path, overwrite=True)
|
||||||
|
|
||||||
|
return da_rasterized
|
||||||
|
|
||||||
|
|
||||||
|
def subpixel_contours(
|
||||||
|
da,
|
||||||
|
z_values=[0.0],
|
||||||
|
crs=None,
|
||||||
|
attribute_df=None,
|
||||||
|
output_path=None,
|
||||||
|
min_vertices=2,
|
||||||
|
dim="time",
|
||||||
|
time_format="%Y-%m-%d",
|
||||||
|
errors="ignore",
|
||||||
|
verbose=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Uses `skimage.measure.find_contours` to extract multiple z-value
|
||||||
|
contour lines from a two-dimensional array (e.g. multiple elevations
|
||||||
|
from a single DEM), or one z-value for each array along a specified
|
||||||
|
dimension of a multi-dimensional array (e.g. to map waterlines
|
||||||
|
across time by extracting a 0 NDWI contour from each individual
|
||||||
|
timestep in an xarray timeseries).
|
||||||
|
|
||||||
|
Contours are returned as a geopandas.GeoDataFrame with one row per
|
||||||
|
z-value or one row per array along a specified dimension. The
|
||||||
|
`attribute_df` parameter can be used to pass custom attributes
|
||||||
|
to the output contour features.
|
||||||
|
|
||||||
|
Last modified: May 2023
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
da : xarray DataArray
|
||||||
|
A two-dimensional or multi-dimensional array from which
|
||||||
|
contours are extracted. If a two-dimensional array is provided,
|
||||||
|
the analysis will run in 'single array, multiple z-values' mode
|
||||||
|
which allows you to specify multiple `z_values` to be extracted.
|
||||||
|
If a multi-dimensional array is provided, the analysis will run
|
||||||
|
in 'single z-value, multiple arrays' mode allowing you to
|
||||||
|
extract contours for each array along the dimension specified
|
||||||
|
by the `dim` parameter.
|
||||||
|
z_values : int, float or list of ints, floats
|
||||||
|
An individual z-value or list of multiple z-values to extract
|
||||||
|
from the array. If operating in 'single z-value, multiple
|
||||||
|
arrays' mode specify only a single z-value.
|
||||||
|
crs : string or CRS object, optional
|
||||||
|
If ``da``'s coordinate reference system (CRS) cannot be
|
||||||
|
determined, provide a CRS using this parameter.
|
||||||
|
(e.g. 'EPSG:3577').
|
||||||
|
output_path : string, optional
|
||||||
|
The path and filename for the output shapefile.
|
||||||
|
attribute_df : pandas.Dataframe, optional
|
||||||
|
A pandas.Dataframe containing attributes to pass to the output
|
||||||
|
contour features. The dataframe must contain either the same
|
||||||
|
number of rows as supplied `z_values` (in 'multiple z-value,
|
||||||
|
single array' mode), or the same number of rows as the number
|
||||||
|
of arrays along the `dim` dimension ('single z-value, multiple
|
||||||
|
arrays mode').
|
||||||
|
min_vertices : int, optional
|
||||||
|
The minimum number of vertices required for a contour to be
|
||||||
|
extracted. The default (and minimum) value is 2, which is the
|
||||||
|
smallest number required to produce a contour line (i.e. a start
|
||||||
|
and end point). Higher values remove smaller contours,
|
||||||
|
potentially removing noise from the output dataset.
|
||||||
|
dim : string, optional
|
||||||
|
The name of the dimension along which to extract contours when
|
||||||
|
operating in 'single z-value, multiple arrays' mode. The default
|
||||||
|
is 'time', which extracts contours for each array along the time
|
||||||
|
dimension.
|
||||||
|
time_format : string, optional
|
||||||
|
The format used to convert `numpy.datetime64` values to strings
|
||||||
|
if applied to data with a "time" dimension. Defaults to
|
||||||
|
"%Y-%m-%d".
|
||||||
|
errors : string, optional
|
||||||
|
If 'raise', then any failed contours will raise an exception.
|
||||||
|
If 'ignore' (the default), a list of failed contours will be
|
||||||
|
printed. If no contours are returned, an exception will always
|
||||||
|
be raised.
|
||||||
|
verbose : bool, optional
|
||||||
|
Print debugging messages. Default is True.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
output_gdf : geopandas geodataframe
|
||||||
|
A geopandas geodataframe object with one feature per z-value
|
||||||
|
('single array, multiple z-values' mode), or one row per array
|
||||||
|
along the dimension specified by the `dim` parameter ('single
|
||||||
|
z-value, multiple arrays' mode). If `attribute_df` was
|
||||||
|
provided, these values will be included in the shapefile's
|
||||||
|
attribute table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _contours_to_multiline(da_i, z_value, min_vertices=2):
|
||||||
|
"""
|
||||||
|
Helper function to apply marching squares contour extraction
|
||||||
|
to an array and return a data as a shapely MultiLineString.
|
||||||
|
The `min_vertices` parameter allows you to drop small contours
|
||||||
|
with less than X vertices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Extracts contours from array, and converts each discrete
|
||||||
|
# contour into a Shapely LineString feature. If the function
|
||||||
|
# returns a KeyError, this may be due to an unresolved issue in
|
||||||
|
# scikit-image: https://github.com/scikit-image/scikit-image/issues/4830
|
||||||
|
# A temporary workaround is to peturb the z-value by a tiny
|
||||||
|
# amount (1e-12) before using it to extract the contour.
|
||||||
|
try:
|
||||||
|
line_features = [
|
||||||
|
LineString(i[:, [1, 0]])
|
||||||
|
for i in find_contours(da_i.data, z_value)
|
||||||
|
if i.shape[0] >= min_vertices
|
||||||
|
]
|
||||||
|
except KeyError:
|
||||||
|
line_features = [
|
||||||
|
LineString(i[:, [1, 0]])
|
||||||
|
for i in find_contours(da_i.data, z_value + 1e-12)
|
||||||
|
if i.shape[0] >= min_vertices
|
||||||
|
]
|
||||||
|
|
||||||
|
# Output resulting lines into a single combined MultiLineString
|
||||||
|
return MultiLineString(line_features)
|
||||||
|
|
||||||
|
def _time_format(i, time_format):
|
||||||
|
"""
|
||||||
|
Converts numpy.datetime64 into formatted strings;
|
||||||
|
otherwise returns data as-is.
|
||||||
|
"""
|
||||||
|
if isinstance(i, np.datetime64):
|
||||||
|
ts = pd.to_datetime(str(i))
|
||||||
|
i = ts.strftime(time_format)
|
||||||
|
return i
|
||||||
|
|
||||||
|
# Verify input data is a xr.DataArray
|
||||||
|
if not isinstance(da, xr.DataArray):
|
||||||
|
raise ValueError(
|
||||||
|
"The input `da` is not an xarray.DataArray. "
|
||||||
|
"If you supplied an xarray.Dataset, pass in one "
|
||||||
|
"of its data variables using the syntax "
|
||||||
|
"`da=ds.<variable name>`."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add GeoBox and odc.* accessor to array using `odc-geo`
|
||||||
|
da = add_geobox(da, crs)
|
||||||
|
|
||||||
|
# If z_values is supplied is not a list, convert to list:
|
||||||
|
z_values = (
|
||||||
|
z_values
|
||||||
|
if (isinstance(z_values, list) or isinstance(z_values, np.ndarray))
|
||||||
|
else [z_values]
|
||||||
|
)
|
||||||
|
|
||||||
|
# If dask collection, load into memory
|
||||||
|
if dask.is_dask_collection(da):
|
||||||
|
if verbose:
|
||||||
|
print("Loading data into memory using Dask")
|
||||||
|
da = da.compute()
|
||||||
|
|
||||||
|
# Test number of dimensions in supplied data array
|
||||||
|
if len(da.shape) == 2:
|
||||||
|
if verbose:
|
||||||
|
print("Operating in multiple z-value, single array mode")
|
||||||
|
dim = "z_value"
|
||||||
|
contour_arrays = {
|
||||||
|
_time_format(i, time_format): _contours_to_multiline(da, i, min_vertices)
|
||||||
|
for i in z_values
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Test if only a single z-value is given when operating in
|
||||||
|
# single z-value, multiple arrays mode
|
||||||
|
if verbose:
|
||||||
|
print("Operating in single z-value, multiple arrays mode")
|
||||||
|
if len(z_values) > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"Please provide a single z-value when operating "
|
||||||
|
"in single z-value, multiple arrays mode"
|
||||||
|
)
|
||||||
|
|
||||||
|
contour_arrays = {
|
||||||
|
_time_format(i, time_format): _contours_to_multiline(
|
||||||
|
da_i, z_values[0], min_vertices
|
||||||
|
)
|
||||||
|
for i, da_i in da.groupby(dim)
|
||||||
|
}
|
||||||
|
|
||||||
|
# If attributes are provided, add the contour keys to that dataframe
|
||||||
|
if attribute_df is not None:
|
||||||
|
try:
|
||||||
|
attribute_df.insert(0, dim, contour_arrays.keys())
|
||||||
|
|
||||||
|
# If this fails, it is due to the applied attribute table not
|
||||||
|
# matching the structure of the loaded data
|
||||||
|
except ValueError:
|
||||||
|
if len(da.shape) == 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"The provided `attribute_df` contains a different "
|
||||||
|
f"number of rows ({len(attribute_df.index)}) "
|
||||||
|
f"than the number of supplied `z_values` "
|
||||||
|
f"({len(z_values)})."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"The provided `attribute_df` contains a different "
|
||||||
|
f"number of rows ({len(attribute_df.index)}) "
|
||||||
|
f"than the number of arrays along the '{dim}' "
|
||||||
|
f"dimension ({len(da[dim])})."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Otherwise, use the contour keys as the only main attributes
|
||||||
|
else:
|
||||||
|
attribute_df = list(contour_arrays.keys())
|
||||||
|
|
||||||
|
# Convert output contours to a geopandas.GeoDataFrame
|
||||||
|
contours_gdf = gpd.GeoDataFrame(
|
||||||
|
data=attribute_df, geometry=list(contour_arrays.values()), crs=da.odc.crs
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define affine and use to convert array coords to geographic coords.
|
||||||
|
# We need to add 0.5 x pixel size to the x and y to obtain the centre
|
||||||
|
# point of our pixels, rather than the top-left corner
|
||||||
|
affine = da.odc.geobox.transform
|
||||||
|
shapely_affine = [
|
||||||
|
affine.a,
|
||||||
|
affine.b,
|
||||||
|
affine.d,
|
||||||
|
affine.e,
|
||||||
|
affine.xoff + affine.a / 2.0,
|
||||||
|
affine.yoff + affine.e / 2.0,
|
||||||
|
]
|
||||||
|
contours_gdf["geometry"] = contours_gdf.affine_transform(shapely_affine)
|
||||||
|
|
||||||
|
# Rename the data column to match the dimension
|
||||||
|
contours_gdf = contours_gdf.rename({0: dim}, axis=1)
|
||||||
|
|
||||||
|
# Drop empty timesteps
|
||||||
|
empty_contours = contours_gdf.geometry.is_empty
|
||||||
|
failed = ", ".join(map(str, contours_gdf[empty_contours][dim].to_list()))
|
||||||
|
contours_gdf = contours_gdf[~empty_contours]
|
||||||
|
|
||||||
|
# Raise exception if no data is returned, or if any contours fail
|
||||||
|
# when `errors='raise'. Otherwise, print failed contours
|
||||||
|
if empty_contours.all() and errors == "raise":
|
||||||
|
raise ValueError(
|
||||||
|
"Failed to generate any valid contours; verify that "
|
||||||
|
"values passed to `z_values` are valid and present "
|
||||||
|
"in `da`"
|
||||||
|
)
|
||||||
|
elif empty_contours.all() and errors == "ignore":
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
"Failed to generate any valid contours; verify that "
|
||||||
|
"values passed to `z_values` are valid and present "
|
||||||
|
"in `da`"
|
||||||
|
)
|
||||||
|
elif empty_contours.any() and errors == "raise":
|
||||||
|
raise Exception(f"Failed to generate contours: {failed}")
|
||||||
|
elif empty_contours.any() and errors == "ignore":
|
||||||
|
if verbose:
|
||||||
|
print(f"Failed to generate contours: {failed}")
|
||||||
|
|
||||||
|
# If asked to write out file, test if GeoJSON or ESRI Shapefile. If
|
||||||
|
# GeoJSON, convert to EPSG:4326 before exporting.
|
||||||
|
if output_path and output_path.endswith(".geojson"):
|
||||||
|
if verbose:
|
||||||
|
print(f"Writing contours to {output_path}")
|
||||||
|
contours_gdf.to_crs("EPSG:4326").to_file(filename=output_path)
|
||||||
|
|
||||||
|
if output_path and output_path.endswith(".shp"):
|
||||||
|
if verbose:
|
||||||
|
print(f"Writing contours to {output_path}")
|
||||||
|
contours_gdf.to_file(filename=output_path)
|
||||||
|
|
||||||
|
return contours_gdf
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_2d(ds,
|
||||||
|
x_coords,
|
||||||
|
y_coords,
|
||||||
|
z_coords,
|
||||||
|
method='linear',
|
||||||
|
factor=1,
|
||||||
|
verbose=False,
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
"""
|
||||||
|
This function takes points with X, Y and Z coordinates, and
|
||||||
|
interpolates Z-values across the extent of an existing xarray
|
||||||
|
dataset. This can be useful for producing smooth surfaces from point
|
||||||
|
data that can be compared directly against satellite data derived
|
||||||
|
from an OpenDataCube query.
|
||||||
|
|
||||||
|
Supported interpolation methods include 'linear', 'nearest' and
|
||||||
|
'cubic (using `scipy.interpolate.griddata`), and 'rbf' (using
|
||||||
|
`scipy.interpolate.Rbf`).
|
||||||
|
|
||||||
|
Last modified: February 2020
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds : xarray DataArray or Dataset
|
||||||
|
A two-dimensional or multi-dimensional array from which x and y
|
||||||
|
dimensions will be copied and used for the area in which to
|
||||||
|
interpolate point data.
|
||||||
|
x_coords, y_coords : numpy array
|
||||||
|
Arrays containing X and Y coordinates for all points (e.g.
|
||||||
|
longitudes and latitudes).
|
||||||
|
z_coords : numpy array
|
||||||
|
An array containing Z coordinates for all points (e.g.
|
||||||
|
elevations). These are the values you wish to interpolate
|
||||||
|
between.
|
||||||
|
method : string, optional
|
||||||
|
The method used to interpolate between point values. This string
|
||||||
|
is either passed to `scipy.interpolate.griddata` (for 'linear',
|
||||||
|
'nearest' and 'cubic' methods), or used to specify Radial Basis
|
||||||
|
Function interpolation using `scipy.interpolate.Rbf` ('rbf').
|
||||||
|
Defaults to 'linear'.
|
||||||
|
factor : int, optional
|
||||||
|
An optional integer that can be used to subsample the spatial
|
||||||
|
interpolation extent to obtain faster interpolation times, then
|
||||||
|
up-sample this array back to the original dimensions of the
|
||||||
|
data as a final step. For example, setting `factor=10` will
|
||||||
|
interpolate data into a grid that has one tenth of the
|
||||||
|
resolution of `ds`. This approach will be significantly faster
|
||||||
|
than interpolating at full resolution, but will potentially
|
||||||
|
produce less accurate or reliable results.
|
||||||
|
verbose : bool, optional
|
||||||
|
Print debugging messages. Default False.
|
||||||
|
**kwargs :
|
||||||
|
Optional keyword arguments to pass to either
|
||||||
|
`scipy.interpolate.griddata` (if `method` is 'linear', 'nearest'
|
||||||
|
or 'cubic'), or `scipy.interpolate.Rbf` (is `method` is 'rbf').
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
interp_2d_array : xarray DataArray
|
||||||
|
An xarray DataArray containing with x and y coordinates copied
|
||||||
|
from `ds_array`, and Z-values interpolated from the points data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Extract xy and elev points
|
||||||
|
points_xy = np.vstack([x_coords, y_coords]).T
|
||||||
|
|
||||||
|
# Extract x and y coordinates to interpolate into.
|
||||||
|
# If `factor` is greater than 1, the coordinates will be subsampled
|
||||||
|
# for faster run-times. If the last x or y value in the subsampled
|
||||||
|
# grid aren't the same as the last x or y values in the original
|
||||||
|
# full resolution grid, add the final full resolution grid value to
|
||||||
|
# ensure data is interpolated up to the very edge of the array
|
||||||
|
if ds.x[::factor][-1].item() == ds.x[-1].item():
|
||||||
|
x_grid_coords = ds.x[::factor].values
|
||||||
|
else:
|
||||||
|
x_grid_coords = ds.x[::factor].values.tolist() + [ds.x[-1].item()]
|
||||||
|
|
||||||
|
if ds.y[::factor][-1].item() == ds.y[-1].item():
|
||||||
|
y_grid_coords = ds.y[::factor].values
|
||||||
|
else:
|
||||||
|
y_grid_coords = ds.y[::factor].values.tolist() + [ds.y[-1].item()]
|
||||||
|
|
||||||
|
# Create grid to interpolate into
|
||||||
|
grid_y, grid_x = np.meshgrid(x_grid_coords, y_grid_coords)
|
||||||
|
|
||||||
|
# Apply scipy.interpolate.griddata interpolation methods
|
||||||
|
if method in ('linear', 'nearest', 'cubic'):
|
||||||
|
|
||||||
|
# Interpolate x, y and z values
|
||||||
|
interp_2d = scipy.interpolate.griddata(points=points_xy,
|
||||||
|
values=z_coords,
|
||||||
|
xi=(grid_y, grid_x),
|
||||||
|
method=method,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
# Apply Radial Basis Function interpolation
|
||||||
|
elif method == 'rbf':
|
||||||
|
|
||||||
|
# Interpolate x, y and z values
|
||||||
|
rbf = scipy.interpolate.Rbf(x_coords, y_coords, z_coords, **kwargs)
|
||||||
|
interp_2d = rbf(grid_y, grid_x)
|
||||||
|
|
||||||
|
# Create xarray dataarray from the data and resample to ds coords
|
||||||
|
interp_2d_da = xr.DataArray(interp_2d,
|
||||||
|
coords=[y_grid_coords, x_grid_coords],
|
||||||
|
dims=['y', 'x'])
|
||||||
|
|
||||||
|
# If factor is greater than 1, resample the interpolated array to
|
||||||
|
# match the input `ds` array
|
||||||
|
if factor > 1:
|
||||||
|
interp_2d_da = interp_2d_da.interp_like(ds)
|
||||||
|
|
||||||
|
return interp_2d_da
|
||||||
|
|
||||||
|
|
||||||
|
def contours_to_arrays(gdf, col):
|
||||||
|
"""
|
||||||
|
This function converts a polyline shapefile into an array with three
|
||||||
|
columns giving the X, Y and Z coordinates of each vertex. This data
|
||||||
|
can then be used as an input to interpolation procedures (e.g. using
|
||||||
|
a function like `interpolate_2d`.
|
||||||
|
|
||||||
|
Last modified: October 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
gdf : Geopandas GeoDataFrame
|
||||||
|
A GeoPandas GeoDataFrame of lines to convert into point
|
||||||
|
coordinates.
|
||||||
|
col : str
|
||||||
|
A string giving the name of the GeoDataFrame field to use as
|
||||||
|
Z-values.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
A numpy array with three columns giving the X, Y and Z coordinates
|
||||||
|
of each vertex in the input GeoDataFrame.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Explode multi-part geometries into multiple single geometries.
|
||||||
|
gdf = gdf.explode(ignore_index=True)
|
||||||
|
|
||||||
|
coords_zvals = []
|
||||||
|
|
||||||
|
for i in range(0, len(gdf)):
|
||||||
|
val = gdf.iloc[i][col]
|
||||||
|
|
||||||
|
try:
|
||||||
|
coords = np.concatenate(
|
||||||
|
[np.vstack(x.coords.xy).T for x in gdf.iloc[i].geometry.geoms]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
coords = np.vstack(gdf.iloc[i].geometry.coords.xy).T
|
||||||
|
|
||||||
|
coords_zvals.append(
|
||||||
|
np.column_stack((coords, np.full(np.shape(coords)[0], fill_value=val)))
|
||||||
|
)
|
||||||
|
|
||||||
|
return np.concatenate(coords_zvals)
|
||||||
|
|
||||||
|
|
||||||
|
def largest_region(bool_array, **kwargs):
|
||||||
|
|
||||||
|
'''
|
||||||
|
Takes a boolean array and identifies the largest contiguous region of
|
||||||
|
connected True values. This is returned as a new array with cells in
|
||||||
|
the largest region marked as True, and all other cells marked as False.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bool_array : boolean array
|
||||||
|
A boolean array (numpy or xarray.DataArray) with True values for
|
||||||
|
the areas that will be inspected to find the largest group of
|
||||||
|
connected cells
|
||||||
|
**kwargs :
|
||||||
|
Optional keyword arguments to pass to `measure.label`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
largest_region : boolean array
|
||||||
|
A boolean array with cells in the largest region marked as True,
|
||||||
|
and all other cells marked as False.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
# First, break boolean array into unique, discrete regions/blobs
|
||||||
|
blobs_labels = label(bool_array, background=0, **kwargs)
|
||||||
|
|
||||||
|
# Count the size of each blob, excluding the background class (0)
|
||||||
|
ids, counts = np.unique(blobs_labels[blobs_labels > 0],
|
||||||
|
return_counts=True)
|
||||||
|
|
||||||
|
# Identify the region ID of the largest blob
|
||||||
|
largest_region_id = ids[np.argmax(counts)]
|
||||||
|
|
||||||
|
# Produce a boolean array where 1 == the largest region
|
||||||
|
largest_region = blobs_labels == largest_region_id
|
||||||
|
|
||||||
|
return largest_region
|
||||||
|
|
||||||
|
|
||||||
|
def transform_geojson_wgs_to_epsg(geojson, EPSG):
|
||||||
|
"""
|
||||||
|
Takes a geojson dictionary and converts it from WGS84 (EPSG:4326) to desired EPSG
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
geojson: dict
|
||||||
|
a geojson dictionary containing a 'geometry' key, in WGS84 coordinates
|
||||||
|
EPSG: int
|
||||||
|
numeric code for the EPSG coordinate referecnce system to transform into
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
transformed_geojson: dict
|
||||||
|
a geojson dictionary containing a 'coordinates' key, in the desired CRS
|
||||||
|
|
||||||
|
"""
|
||||||
|
gg = Geometry(geojson['geometry'], CRS('epsg:4326'))
|
||||||
|
gg = gg.to_crs(CRS(f'epsg:{EPSG}'))
|
||||||
|
return gg.__geo_interface__
|
||||||
|
|
||||||
|
|
||||||
|
def zonal_stats_parallel(shp,
|
||||||
|
raster,
|
||||||
|
statistics,
|
||||||
|
out_shp,
|
||||||
|
ncpus,
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Summarizing raster datasets based on vector geometries in parallel.
|
||||||
|
Each cpu recieves an equal chunk of the dataset.
|
||||||
|
Utilizes the perrygeo/rasterstats package.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
shp : str
|
||||||
|
Path to shapefile that contains polygons over
|
||||||
|
which zonal statistics are calculated
|
||||||
|
raster: str
|
||||||
|
Path to the raster from which the statistics are calculated.
|
||||||
|
This can be a virtual raster (.vrt).
|
||||||
|
statistics: list
|
||||||
|
list of statistics to calculate. e.g.
|
||||||
|
['min', 'max', 'median', 'majority', 'sum']
|
||||||
|
out_shp: str
|
||||||
|
Path to export shapefile containing zonal statistics.
|
||||||
|
ncpus: int
|
||||||
|
number of cores to parallelize the operations over.
|
||||||
|
kwargs:
|
||||||
|
Any other keyword arguments to rasterstats.zonal_stats()
|
||||||
|
See https://github.com/perrygeo/python-rasterstats for
|
||||||
|
all options
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Exports a shapefile to disk containing the zonal statistics requested
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# yields n sized chunks from list l (used for splitting task to multiple processes)
|
||||||
|
def chunks(l, n):
|
||||||
|
for i in range(0, len(l), n):
|
||||||
|
yield l[i:i + n]
|
||||||
|
|
||||||
|
# calculates zonal stats and adds results to a dictionary
|
||||||
|
def worker(z, raster, d):
|
||||||
|
z_stats = zonal_stats(z, raster, stats=statistics, **kwargs)
|
||||||
|
for i in range(0, len(z_stats)):
|
||||||
|
d[z[i]['id']] = z_stats[i]
|
||||||
|
|
||||||
|
# write output polygon
|
||||||
|
def write_output(zones, out_shp, d):
|
||||||
|
# copy schema and crs from input and add new fields for each statistic
|
||||||
|
schema = zones.schema.copy()
|
||||||
|
crs = zones.crs
|
||||||
|
for stat in statistics:
|
||||||
|
schema['properties'][stat] = 'float'
|
||||||
|
|
||||||
|
with fiona.open(out_shp, 'w', 'ESRI Shapefile', schema, crs) as output:
|
||||||
|
for elem in zones:
|
||||||
|
for stat in statistics:
|
||||||
|
elem['properties'][stat] = d[elem['id']][stat]
|
||||||
|
output.write({'properties': elem['properties'], 'geometry': mapping(shape(elem['geometry']))})
|
||||||
|
|
||||||
|
with fiona.open(shp) as zones:
|
||||||
|
jobs = []
|
||||||
|
|
||||||
|
# create manager dictionary (polygon ids=keys, stats=entries)
|
||||||
|
# where multiple processes can write without conflicts
|
||||||
|
man = mp.Manager()
|
||||||
|
d = man.dict()
|
||||||
|
|
||||||
|
# split zone polygons into 'ncpus' chunks for parallel processing
|
||||||
|
# and call worker() for each
|
||||||
|
split = chunks(zones, len(zones)//ncpus)
|
||||||
|
for z in split:
|
||||||
|
p = mp.Process(target=worker, args=(z, raster, d))
|
||||||
|
p.start()
|
||||||
|
jobs.append(p)
|
||||||
|
|
||||||
|
# wait that all chunks are finished
|
||||||
|
[j.join() for j in jobs]
|
||||||
|
|
||||||
|
write_output(zones, out_shp, d)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_geocode(coords, site_classes=None, state_classes=None):
|
||||||
|
"""
|
||||||
|
Takes a latitude and longitude coordinate, and performs a reverse
|
||||||
|
geocode to return a plain-text description of the location in the
|
||||||
|
form:
|
||||||
|
|
||||||
|
Site, State
|
||||||
|
|
||||||
|
E.g.: `reverse_geocode(coords=(-35.282163, 149.128835))`
|
||||||
|
|
||||||
|
'Canberra, Australian Capital Territory'
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
coords : tuple of floats
|
||||||
|
A tuple of (latitude, longitude) coordinates used to perform
|
||||||
|
the reverse geocode.
|
||||||
|
site_classes : list of strings, optional
|
||||||
|
A list of strings used to define the site part of the plain
|
||||||
|
text location description. Because the contents of the geocoded
|
||||||
|
address can vary greatly depending on location, these strings
|
||||||
|
are tested against the address one by one until a match is made.
|
||||||
|
|
||||||
|
Defaults to:
|
||||||
|
|
||||||
|
``['city', 'town', 'village', 'suburb', 'hamlet', 'county', 'municipality']``
|
||||||
|
|
||||||
|
state_classes : list of strings, optional
|
||||||
|
A list of strings used to define the state part of the plain
|
||||||
|
text location description. These strings are tested against the
|
||||||
|
address one by one until a match is made. Defaults to:
|
||||||
|
`['state', 'territory']`.
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
If a valid geocoded address is found, a plain text location
|
||||||
|
description will be returned:
|
||||||
|
|
||||||
|
'Site, State'
|
||||||
|
|
||||||
|
If no valid address is found, formatted coordinates will be returned
|
||||||
|
instead:
|
||||||
|
|
||||||
|
'XX.XX S, XX.XX E'
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Run reverse geocode using coordinates
|
||||||
|
geocoder = Nominatim(user_agent='Digital Earth Africa')
|
||||||
|
out = geocoder.reverse(coords)
|
||||||
|
|
||||||
|
# Create plain text-coords as fall-back
|
||||||
|
lat = f'{-coords[0]:.2f} S' if coords[0] < 0 else f'{coords[0]:.2f} N'
|
||||||
|
lon = f'{-coords[1]:.2f} W' if coords[1] < 0 else f'{coords[1]:.2f} E'
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Get address from geocoded data
|
||||||
|
address = out.raw['address']
|
||||||
|
|
||||||
|
# Use site and state classes if supplied; else use defaults
|
||||||
|
default_site_classes = ['city', 'town', 'village', 'suburb', 'hamlet',
|
||||||
|
'county', 'municipality']
|
||||||
|
default_state_classes = ['state', 'territory']
|
||||||
|
site_classes = site_classes if site_classes else default_site_classes
|
||||||
|
state_classes = state_classes if state_classes else default_state_classes
|
||||||
|
|
||||||
|
# Return the first site or state class that exists in address dict
|
||||||
|
site = next((address[k] for k in site_classes if k in address), None)
|
||||||
|
state = next((address[k] for k in state_classes if k in address), None)
|
||||||
|
|
||||||
|
# If site and state exist in the data, return this.
|
||||||
|
# Otherwise, return N/E/S/W coordinates.
|
||||||
|
if site and state:
|
||||||
|
|
||||||
|
# Return as site, state formatted string
|
||||||
|
return f'{site}, {state}'
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# If no geocoding result, return N/E/S/W coordinates
|
||||||
|
print('No valid geocoded location; returning coordinates instead')
|
||||||
|
return f'{lat}, {lon}'
|
||||||
|
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
|
||||||
|
# If no geocoding result, return N/E/S/W coordinates
|
||||||
|
print('No valid geocoded location; returning coordinates instead')
|
||||||
|
return f'{lat}, {lon}'
|
||||||
|
|
||||||
|
|
||||||
|
def sun_angles(dc, query):
|
||||||
|
"""
|
||||||
|
For a given spatiotemporal query, calculate mean sun
|
||||||
|
azimuth and elevation for each satellite observation, and
|
||||||
|
return these as a new `xarray.Dataset` with 'sun_elevation'
|
||||||
|
and 'sun_azimuth' variables.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
dc : datacube.Datacube object
|
||||||
|
Datacube instance used to load data.
|
||||||
|
query : dict
|
||||||
|
A dictionary containing query parameters used to identify
|
||||||
|
satellite observations and load metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
sun_angles_ds : xarray.Dataset
|
||||||
|
An `xarray.set` containing a 'sun_elevation' and
|
||||||
|
'sun_azimuth' variables.
|
||||||
|
"""
|
||||||
|
# Identify satellite datasets and group outputs using the
|
||||||
|
# same approach used to group satellite imagery (i.e. solar day)
|
||||||
|
gb = query_group_by(**query)
|
||||||
|
datasets = dc.find_datasets(**query)
|
||||||
|
dataset_array = dc.group_datasets(datasets, gb)
|
||||||
|
|
||||||
|
# Load and take the mean of metadata from each product
|
||||||
|
sun_azimuth = xr_apply(
|
||||||
|
dataset_array,
|
||||||
|
lambda t, dd: np.mean([d.metadata.eo_sun_azimuth for d in dd]),
|
||||||
|
dtype=float,
|
||||||
|
)
|
||||||
|
sun_elevation = xr_apply(
|
||||||
|
dataset_array,
|
||||||
|
lambda t, dd: np.mean([d.metadata.eo_sun_elevation for d in dd]),
|
||||||
|
dtype=float,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine into new xarray.Dataset
|
||||||
|
sun_angles_ds = xr.merge(
|
||||||
|
[sun_elevation.rename("sun_elevation"), sun_azimuth.rename("sun_azimuth")]
|
||||||
|
)
|
||||||
|
|
||||||
|
return sun_angles_ds
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
"""
|
||||||
|
Functions for calculating per-pixel temporal summary statistics on a
|
||||||
|
timeseries stored in a xarray.DataArray.
|
||||||
|
|
||||||
|
The key functions are:
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:caption: Primary functions
|
||||||
|
:nosignatures:
|
||||||
|
:toctree: gen
|
||||||
|
|
||||||
|
xr_phenology
|
||||||
|
temporal_statistics
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
:toctree: gen
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import dask
|
||||||
|
import numpy as np
|
||||||
|
import xarray as xr
|
||||||
|
import hdstats
|
||||||
|
from packaging import version
|
||||||
|
from datacube.utils.geometry import assign_crs
|
||||||
|
|
||||||
|
|
||||||
|
def allNaN_arg(da, dim, stat):
|
||||||
|
"""
|
||||||
|
Calculate da.argmax() or da.argmin() while handling
|
||||||
|
all-NaN slices. Fills all-NaN locations with an
|
||||||
|
float and then masks the offending cells.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
da : xarray.DataArray
|
||||||
|
dim : str
|
||||||
|
Dimension over which to calculate argmax, argmin e.g. 'time'
|
||||||
|
stat : str
|
||||||
|
The statistic to calculte, either 'min' for argmin()
|
||||||
|
or 'max' for .argmax()
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
xarray.DataArray
|
||||||
|
"""
|
||||||
|
# generate a mask where entire axis along dimension is NaN
|
||||||
|
mask = da.isnull().all(dim)
|
||||||
|
|
||||||
|
if stat == "max":
|
||||||
|
y = da.fillna(float(da.min() - 1))
|
||||||
|
y = y.argmax(dim=dim, skipna=True).where(~mask)
|
||||||
|
return y
|
||||||
|
|
||||||
|
if stat == "min":
|
||||||
|
y = da.fillna(float(da.max() + 1))
|
||||||
|
y = y.argmin(dim=dim, skipna=True).where(~mask)
|
||||||
|
return y
|
||||||
|
|
||||||
|
|
||||||
|
def _vpos(da):
|
||||||
|
"""
|
||||||
|
vPOS = Value at peak of season
|
||||||
|
"""
|
||||||
|
return da.max("time")
|
||||||
|
|
||||||
|
|
||||||
|
def _pos(da):
|
||||||
|
"""
|
||||||
|
POS = DOY of peak of season
|
||||||
|
"""
|
||||||
|
return da.isel(time=da.argmax("time")).time.dt.dayofyear
|
||||||
|
|
||||||
|
|
||||||
|
def _trough(da):
|
||||||
|
"""
|
||||||
|
Trough = Minimum value
|
||||||
|
"""
|
||||||
|
return da.min("time")
|
||||||
|
|
||||||
|
|
||||||
|
def _aos(vpos, trough):
|
||||||
|
"""
|
||||||
|
AOS = Amplitude of season
|
||||||
|
"""
|
||||||
|
return vpos - trough
|
||||||
|
|
||||||
|
|
||||||
|
def _vsos(da, pos, method_sos="first"):
|
||||||
|
"""
|
||||||
|
vSOS = Value at the start of season
|
||||||
|
Params
|
||||||
|
-----
|
||||||
|
da : xarray.DataArray
|
||||||
|
method_sos : str,
|
||||||
|
If 'first' then vSOS is estimated
|
||||||
|
as the first positive slope on the
|
||||||
|
greening side of the curve. If 'median',
|
||||||
|
then vSOS is estimated as the median value
|
||||||
|
of the postive slopes on the greening side
|
||||||
|
of the curve.
|
||||||
|
"""
|
||||||
|
# select timesteps before peak of season (AKA greening)
|
||||||
|
greenup = da.where(da.time < pos.time)
|
||||||
|
# find the first order slopes
|
||||||
|
green_deriv = greenup.differentiate("time")
|
||||||
|
# find where the first order slope is postive
|
||||||
|
pos_green_deriv = green_deriv.where(green_deriv > 0)
|
||||||
|
# positive slopes on greening side
|
||||||
|
pos_greenup = greenup.where(~np.isnan(pos_green_deriv))
|
||||||
|
# find the median
|
||||||
|
median = pos_greenup.median("time")
|
||||||
|
# distance of values from median
|
||||||
|
distance = pos_greenup - median
|
||||||
|
|
||||||
|
if method_sos == "first":
|
||||||
|
# find index (argmin) where distance is most negative
|
||||||
|
idx = allNaN_arg(distance, "time", "min").astype("int16")
|
||||||
|
|
||||||
|
if method_sos == "median":
|
||||||
|
# find index (argmin) where distance is smallest absolute value
|
||||||
|
idx = allNaN_arg(np.fabs(distance), "time", "min").astype("int16")
|
||||||
|
|
||||||
|
return pos_greenup.isel(time=idx)
|
||||||
|
|
||||||
|
|
||||||
|
def _sos(vsos):
|
||||||
|
"""
|
||||||
|
SOS = DOY for start of season
|
||||||
|
"""
|
||||||
|
return vsos.time.dt.dayofyear
|
||||||
|
|
||||||
|
|
||||||
|
def _veos(da, pos, method_eos="last"):
|
||||||
|
"""
|
||||||
|
vEOS = Value at the end of season
|
||||||
|
Params
|
||||||
|
-----
|
||||||
|
method_eos : str
|
||||||
|
If 'last' then vEOS is estimated
|
||||||
|
as the last negative slope on the
|
||||||
|
senescing side of the curve. If 'median',
|
||||||
|
then vEOS is estimated as the 'median' value
|
||||||
|
of the negative slopes on the senescing
|
||||||
|
side of the curve.
|
||||||
|
"""
|
||||||
|
# select timesteps before peak of season (AKA greening)
|
||||||
|
senesce = da.where(da.time > pos.time)
|
||||||
|
# find the first order slopes
|
||||||
|
senesce_deriv = senesce.differentiate("time")
|
||||||
|
# find where the fst order slope is negative
|
||||||
|
neg_senesce_deriv = senesce_deriv.where(~np.isnan(senesce_deriv < 0))
|
||||||
|
# negative slopes on senescing side
|
||||||
|
neg_senesce = senesce.where(neg_senesce_deriv)
|
||||||
|
# find medians
|
||||||
|
median = neg_senesce.median("time")
|
||||||
|
# distance to the median
|
||||||
|
distance = neg_senesce - median
|
||||||
|
|
||||||
|
if method_eos == "last":
|
||||||
|
# index where last negative slope occurs
|
||||||
|
idx = allNaN_arg(distance, "time", "min").astype("int16")
|
||||||
|
|
||||||
|
if method_eos == "median":
|
||||||
|
# index where median occurs
|
||||||
|
idx = allNaN_arg(np.fabs(distance), "time", "min").astype("int16")
|
||||||
|
|
||||||
|
return neg_senesce.isel(time=idx)
|
||||||
|
|
||||||
|
|
||||||
|
def _eos(veos):
|
||||||
|
"""
|
||||||
|
EOS = DOY for end of seasonn
|
||||||
|
"""
|
||||||
|
return veos.time.dt.dayofyear
|
||||||
|
|
||||||
|
|
||||||
|
def _los(da, eos, sos):
|
||||||
|
"""
|
||||||
|
LOS = Length of season (in DOY)
|
||||||
|
"""
|
||||||
|
los = eos - sos
|
||||||
|
#handle negative values
|
||||||
|
los = xr.where(
|
||||||
|
los >= 0,
|
||||||
|
los,
|
||||||
|
da.time.dt.dayofyear.values[-1] + (eos.where(los < 0) - sos.where(los < 0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return los
|
||||||
|
|
||||||
|
|
||||||
|
def _rog(vpos, vsos, pos, sos):
|
||||||
|
"""
|
||||||
|
ROG = Rate of Greening (Days)
|
||||||
|
"""
|
||||||
|
return (vpos - vsos) / (pos - sos)
|
||||||
|
|
||||||
|
|
||||||
|
def _ros(veos, vpos, eos, pos):
|
||||||
|
"""
|
||||||
|
ROG = Rate of Senescing (Days)
|
||||||
|
"""
|
||||||
|
return (veos - vpos) / (eos - pos)
|
||||||
|
|
||||||
|
|
||||||
|
def xr_phenology(
|
||||||
|
da,
|
||||||
|
stats=[
|
||||||
|
"SOS",
|
||||||
|
"POS",
|
||||||
|
"EOS",
|
||||||
|
"Trough",
|
||||||
|
"vSOS",
|
||||||
|
"vPOS",
|
||||||
|
"vEOS",
|
||||||
|
"LOS",
|
||||||
|
"AOS",
|
||||||
|
"ROG",
|
||||||
|
"ROS",
|
||||||
|
],
|
||||||
|
method_sos="first",
|
||||||
|
method_eos="last",
|
||||||
|
verbose=True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Obtain land surface phenology metrics from an
|
||||||
|
xarray.DataArray containing a timeseries of a
|
||||||
|
vegetation index like NDVI.
|
||||||
|
|
||||||
|
last modified June 2020
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
da : xarray.DataArray
|
||||||
|
DataArray should contain a 2D or 3D time series of a
|
||||||
|
vegetation index like NDVI, EVI
|
||||||
|
stats : list
|
||||||
|
list of phenological statistics to return. Regardless of
|
||||||
|
the metrics returned, all statistics are calculated
|
||||||
|
due to inter-dependencies between metrics.
|
||||||
|
Options include:
|
||||||
|
|
||||||
|
* `SOS` = DOY of start of season
|
||||||
|
* `POS` = DOY of peak of season
|
||||||
|
* `EOS` = DOY of end of season
|
||||||
|
* `vSOS` = Value at start of season
|
||||||
|
* `vPOS` = Value at peak of season
|
||||||
|
* `vEOS` = Value at end of season
|
||||||
|
* `Trough` = Minimum value of season
|
||||||
|
* `LOS` = Length of season (DOY)
|
||||||
|
* `AOS` = Amplitude of season (in value units)
|
||||||
|
* `ROG` = Rate of greening
|
||||||
|
* `ROS` = Rate of senescence
|
||||||
|
|
||||||
|
method_sos : str
|
||||||
|
If 'first' then vSOS is estimated as the first positive
|
||||||
|
slope on the greening side of the curve. If 'median',
|
||||||
|
then vSOS is estimated as the median value of the postive
|
||||||
|
slopes on the greening side of the curve.
|
||||||
|
method_eos : str
|
||||||
|
If 'last' then vEOS is estimated as the last negative slope
|
||||||
|
on the senescing side of the curve. If 'median', then vEOS is
|
||||||
|
estimated as the 'median' value of the negative slopes on the
|
||||||
|
senescing side of the curve.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
xarray.Dataset
|
||||||
|
Dataset containing variables for the selected
|
||||||
|
phenology statistics
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Check inputs before running calculations
|
||||||
|
if dask.is_dask_collection(da):
|
||||||
|
if version.parse(xr.__version__) < version.parse("0.16.0"):
|
||||||
|
raise TypeError(
|
||||||
|
"Dask arrays are not currently supported by this function, "
|
||||||
|
+ "run da.compute() before passing dataArray."
|
||||||
|
)
|
||||||
|
stats_dtype = {
|
||||||
|
"SOS": np.int16,
|
||||||
|
"POS": np.int16,
|
||||||
|
"EOS": np.int16,
|
||||||
|
"Trough": np.float32,
|
||||||
|
"vSOS": np.float32,
|
||||||
|
"vPOS": np.float32,
|
||||||
|
"vEOS": np.float32,
|
||||||
|
"LOS": np.int16,
|
||||||
|
"AOS": np.float32,
|
||||||
|
"ROG": np.float32,
|
||||||
|
"ROS": np.float32,
|
||||||
|
}
|
||||||
|
da_template = da.isel(time=0).drop("time")
|
||||||
|
template = xr.Dataset(
|
||||||
|
{
|
||||||
|
var_name: da_template.astype(var_dtype)
|
||||||
|
for var_name, var_dtype in stats_dtype.items()
|
||||||
|
if var_name in stats
|
||||||
|
}
|
||||||
|
)
|
||||||
|
da_all_time = da.chunk({"time": -1})
|
||||||
|
|
||||||
|
lazy_phenology = da_all_time.map_blocks(
|
||||||
|
xr_phenology,
|
||||||
|
kwargs=dict(
|
||||||
|
stats=stats,
|
||||||
|
method_sos=method_sos,
|
||||||
|
method_eos=method_eos,
|
||||||
|
),
|
||||||
|
template=xr.Dataset(template),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
crs = da.geobox.crs
|
||||||
|
lazy_phenology = assign_crs(lazy_phenology, str(crs))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return lazy_phenology
|
||||||
|
|
||||||
|
if method_sos not in ("median", "first"):
|
||||||
|
raise ValueError("method_sos should be either 'median' or 'first'")
|
||||||
|
|
||||||
|
if method_eos not in ("median", "last"):
|
||||||
|
raise ValueError("method_eos should be either 'median' or 'last'")
|
||||||
|
|
||||||
|
# If stats supplied is not a list, convert to list.
|
||||||
|
stats = stats if isinstance(stats, list) else [stats]
|
||||||
|
|
||||||
|
# try to grab the crs info
|
||||||
|
try:
|
||||||
|
crs = da.geobox.crs
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# remove any remaining all-NaN pixels
|
||||||
|
mask = da.isnull().all("time")
|
||||||
|
da = da.where(~mask, other=0)
|
||||||
|
|
||||||
|
# calculate the statistics
|
||||||
|
if verbose:
|
||||||
|
print(" Phenology...")
|
||||||
|
vpos = _vpos(da)
|
||||||
|
pos = _pos(da)
|
||||||
|
trough = _trough(da)
|
||||||
|
aos = _aos(vpos, trough)
|
||||||
|
vsos = _vsos(da, pos, method_sos=method_sos)
|
||||||
|
sos = _sos(vsos)
|
||||||
|
veos = _veos(da, pos, method_eos=method_eos)
|
||||||
|
eos = _eos(veos)
|
||||||
|
los = _los(da, eos, sos)
|
||||||
|
rog = _rog(vpos, vsos, pos, sos)
|
||||||
|
ros = _ros(veos, vpos, eos, pos)
|
||||||
|
|
||||||
|
# Dictionary containing the statistics
|
||||||
|
stats_dict = {
|
||||||
|
"SOS": sos.astype(np.int16),
|
||||||
|
"EOS": eos.astype(np.int16),
|
||||||
|
"vSOS": vsos.astype(np.float32),
|
||||||
|
"vPOS": vpos.astype(np.float32),
|
||||||
|
"Trough": trough.astype(np.float32),
|
||||||
|
"POS": pos.astype(np.int16),
|
||||||
|
"vEOS": veos.astype(np.float32),
|
||||||
|
"LOS": los.astype(np.int16),
|
||||||
|
"AOS": aos.astype(np.float32),
|
||||||
|
"ROG": rog.astype(np.float32),
|
||||||
|
"ROS": ros.astype(np.float32),
|
||||||
|
}
|
||||||
|
|
||||||
|
# intialise dataset with first statistic
|
||||||
|
ds = stats_dict[stats[0]].to_dataset(name=stats[0])
|
||||||
|
|
||||||
|
# add the other stats to the dataset
|
||||||
|
for stat in stats[1:]:
|
||||||
|
if verbose:
|
||||||
|
print(" " + stat)
|
||||||
|
stats_keep = stats_dict.get(stat)
|
||||||
|
ds[stat] = stats_dict[stat]
|
||||||
|
|
||||||
|
try:
|
||||||
|
ds = assign_crs(ds, str(crs))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ds.drop("time")
|
||||||
|
|
||||||
|
|
||||||
|
def temporal_statistics(da, stats):
|
||||||
|
"""
|
||||||
|
Calculate various generic summary statistics on any timeseries.
|
||||||
|
|
||||||
|
This function uses the hdstats temporal library:
|
||||||
|
https://github.com/daleroberts/hdstats/blob/master/hdstats/ts.pyx
|
||||||
|
|
||||||
|
last modified June 2020
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
da : xarray.DataArray
|
||||||
|
DataArray should contain a 3D time series.
|
||||||
|
stats : list
|
||||||
|
list of temporal statistics to calculate.
|
||||||
|
Options include:
|
||||||
|
|
||||||
|
* 'discordance' =
|
||||||
|
* 'f_std' = std of discrete fourier transform coefficients, returns
|
||||||
|
three layers: f_std_n1, f_std_n2, f_std_n3
|
||||||
|
* 'f_mean' = mean of discrete fourier transform coefficients, returns
|
||||||
|
three layers: f_mean_n1, f_mean_n2, f_mean_n3
|
||||||
|
* 'f_median' = median of discrete fourier transform coefficients, returns
|
||||||
|
three layers: f_median_n1, f_median_n2, f_median_n3
|
||||||
|
* 'mean_change' = mean of discrete difference along time dimension
|
||||||
|
* 'median_change' = median of discrete difference along time dimension
|
||||||
|
* 'abs_change' = mean of absolute discrete difference along time dimension
|
||||||
|
* 'complexity' =
|
||||||
|
* 'central_diff' =
|
||||||
|
* 'num_peaks' : The number of peaks in the timeseries, defined with a local
|
||||||
|
window of size 10. NOTE: This statistic is very slow
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
xarray.Dataset
|
||||||
|
Dataset containing variables for the selected
|
||||||
|
temporal statistics
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# if dask arrays then map the blocks
|
||||||
|
if dask.is_dask_collection(da):
|
||||||
|
if version.parse(xr.__version__) < version.parse("0.16.0"):
|
||||||
|
raise TypeError(
|
||||||
|
"Dask arrays are only supported by this function if using, "
|
||||||
|
+ "xarray v0.16, run da.compute() before passing dataArray."
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a template that matches the final datasets dims & vars
|
||||||
|
arr = da.isel(time=0).drop("time")
|
||||||
|
|
||||||
|
# deal with the case where fourier is first in the list
|
||||||
|
if stats[0] in ("f_std", "f_median", "f_mean"):
|
||||||
|
template = xr.zeros_like(arr).to_dataset(name=stats[0] + "_n1")
|
||||||
|
template[stats[0] + "_n2"] = xr.zeros_like(arr)
|
||||||
|
template[stats[0] + "_n3"] = xr.zeros_like(arr)
|
||||||
|
|
||||||
|
for stat in stats[1:]:
|
||||||
|
if stat in ("f_std", "f_median", "f_mean"):
|
||||||
|
template[stat + "_n1"] = xr.zeros_like(arr)
|
||||||
|
template[stat + "_n2"] = xr.zeros_like(arr)
|
||||||
|
template[stat + "_n3"] = xr.zeros_like(arr)
|
||||||
|
else:
|
||||||
|
template[stat] = xr.zeros_like(arr)
|
||||||
|
else:
|
||||||
|
template = xr.zeros_like(arr).to_dataset(name=stats[0])
|
||||||
|
|
||||||
|
for stat in stats:
|
||||||
|
if stat in ("f_std", "f_median", "f_mean"):
|
||||||
|
template[stat + "_n1"] = xr.zeros_like(arr)
|
||||||
|
template[stat + "_n2"] = xr.zeros_like(arr)
|
||||||
|
template[stat + "_n3"] = xr.zeros_like(arr)
|
||||||
|
else:
|
||||||
|
template[stat] = xr.zeros_like(arr)
|
||||||
|
try:
|
||||||
|
template = template.drop("spatial_ref")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ensure the time chunk is set to -1
|
||||||
|
da_all_time = da.chunk({"time": -1})
|
||||||
|
|
||||||
|
# apply function across chunks
|
||||||
|
lazy_ds = da_all_time.map_blocks(
|
||||||
|
temporal_statistics, kwargs={"stats": stats}, template=template
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
crs = da.geobox.crs
|
||||||
|
lazy_ds = assign_crs(lazy_ds, str(crs))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return lazy_ds
|
||||||
|
|
||||||
|
# If stats supplied is not a list, convert to list.
|
||||||
|
stats = stats if isinstance(stats, list) else [stats]
|
||||||
|
|
||||||
|
# grab all the attributes of the xarray
|
||||||
|
x, y, time, attrs = da.x, da.y, da.time, da.attrs
|
||||||
|
|
||||||
|
# deal with any all-NaN pixels by filling with 0's
|
||||||
|
mask = da.isnull().all("time")
|
||||||
|
da = da.where(~mask, other=0)
|
||||||
|
|
||||||
|
# ensure dim order is correct for functions
|
||||||
|
da = da.transpose("y", "x", "time").values
|
||||||
|
|
||||||
|
stats_dict = {
|
||||||
|
"discordance": lambda da: hdstats.discordance(da, n=10),
|
||||||
|
"f_std": lambda da: hdstats.fourier_std(da, n=3, step=5),
|
||||||
|
"f_mean": lambda da: hdstats.fourier_mean(da, n=3, step=5),
|
||||||
|
"f_median": lambda da: hdstats.fourier_median(da, n=3, step=5),
|
||||||
|
"mean_change": lambda da: hdstats.mean_change(da),
|
||||||
|
"median_change": lambda da: hdstats.median_change(da),
|
||||||
|
"abs_change": lambda da: hdstats.mean_abs_change(da),
|
||||||
|
"complexity": lambda da: hdstats.complexity(da),
|
||||||
|
"central_diff": lambda da: hdstats.mean_central_diff(da),
|
||||||
|
"num_peaks": lambda da: hdstats.number_peaks(da, 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
print(" Statistics:")
|
||||||
|
# if one of the fourier functions is first (or only)
|
||||||
|
# stat in the list then we need to deal with this
|
||||||
|
if stats[0] in ("f_std", "f_median", "f_mean"):
|
||||||
|
print(" " + stats[0])
|
||||||
|
stat_func = stats_dict.get(str(stats[0]))
|
||||||
|
zz = stat_func(da)
|
||||||
|
n1 = zz[:, :, 0]
|
||||||
|
n2 = zz[:, :, 1]
|
||||||
|
n3 = zz[:, :, 2]
|
||||||
|
|
||||||
|
# intialise dataset with first statistic
|
||||||
|
ds = xr.DataArray(
|
||||||
|
n1, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"]
|
||||||
|
).to_dataset(name=stats[0] + "_n1")
|
||||||
|
|
||||||
|
# add other datasets
|
||||||
|
for i, j in zip([n2, n3], ["n2", "n3"]):
|
||||||
|
ds[stats[0] + "_" + j] = xr.DataArray(
|
||||||
|
i, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# simpler if first function isn't fourier transform
|
||||||
|
first_func = stats_dict.get(str(stats[0]))
|
||||||
|
print(" " + stats[0])
|
||||||
|
ds = first_func(da)
|
||||||
|
|
||||||
|
# convert back to xarray dataset
|
||||||
|
ds = xr.DataArray(
|
||||||
|
ds, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"]
|
||||||
|
).to_dataset(name=stats[0])
|
||||||
|
|
||||||
|
# loop through the other functions
|
||||||
|
for stat in stats[1:]:
|
||||||
|
print(" " + stat)
|
||||||
|
|
||||||
|
# handle the fourier transform examples
|
||||||
|
if stat in ("f_std", "f_median", "f_mean"):
|
||||||
|
stat_func = stats_dict.get(str(stat))
|
||||||
|
zz = stat_func(da)
|
||||||
|
n1 = zz[:, :, 0]
|
||||||
|
n2 = zz[:, :, 1]
|
||||||
|
n3 = zz[:, :, 2]
|
||||||
|
|
||||||
|
for i, j in zip([n1, n2, n3], ["n1", "n2", "n3"]):
|
||||||
|
ds[stat + "_" + j] = xr.DataArray(
|
||||||
|
i, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"]
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Select a stats function from the dictionary
|
||||||
|
# and add to the dataset
|
||||||
|
stat_func = stats_dict.get(str(stat))
|
||||||
|
ds[stat] = xr.DataArray(
|
||||||
|
stat_func(da), attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# try to add back the geobox
|
||||||
|
try:
|
||||||
|
crs = da.geobox.crs
|
||||||
|
ds = assign_crs(ds, str(crs))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ds
|
||||||
@@ -0,0 +1,732 @@
|
|||||||
|
"""
|
||||||
|
Functions for working with the Wetlands Insight Tool (WIT)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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 warnings
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import geopandas as gpd
|
||||||
|
import seaborn as sns
|
||||||
|
import xarray as xr
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from skimage import exposure
|
||||||
|
import matplotlib.animation as animation
|
||||||
|
import matplotlib.patheffects as PathEffects
|
||||||
|
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
|
||||||
|
from dask.distributed import progress
|
||||||
|
|
||||||
|
import datacube
|
||||||
|
from datacube.utils import masking
|
||||||
|
from datacube.utils import geometry
|
||||||
|
|
||||||
|
from deafrica_tools.bandindices import calculate_indices
|
||||||
|
from deafrica_tools.datahandling import load_ard, wofs_fuser
|
||||||
|
from deafrica_tools.spatial import xr_rasterize
|
||||||
|
from deafrica_tools.classification import HiddenPrints
|
||||||
|
|
||||||
|
|
||||||
|
def WIT_drill(
|
||||||
|
gdf,
|
||||||
|
time,
|
||||||
|
min_gooddata=0.85,
|
||||||
|
TCW_threshold=-0.035,
|
||||||
|
resample_frequency=None,
|
||||||
|
export_csv=None,
|
||||||
|
dask_chunks=None,
|
||||||
|
verbose=False,
|
||||||
|
verbose_progress=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
The Wetlands Insight Tool run onver an extent covered by a polygon.
|
||||||
|
This function loads FC, WOfS, and Landsat data, and calculates tasseled
|
||||||
|
cap wetness, in order to determine the dominant land cover class
|
||||||
|
within a polygon at each satellite observation.
|
||||||
|
|
||||||
|
The output is a pandas dataframe containing a timeseries of the relative
|
||||||
|
fractions of each class at each time-step. This forms the input to produce
|
||||||
|
a stacked line-plot.
|
||||||
|
|
||||||
|
Last modified: Oct 2021
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
gdf : geopandas.GeoDataFrame
|
||||||
|
The dataframe must only contain a single row,
|
||||||
|
containing the polygon you wish to interrograte.
|
||||||
|
time : tuple
|
||||||
|
a tuple containing the time range over which to run the WIT.
|
||||||
|
e.g. ('2015-01' , '2019-12')
|
||||||
|
min_gooddata : Float, optional
|
||||||
|
A number between 0 and 1 (e.g 0.8) indicating the minimum percentage
|
||||||
|
of good quality pixels required for a satellite observation to be loaded
|
||||||
|
and therefore included in the WIT plot. This number should, at a minimum,
|
||||||
|
be set to 0.80 to limit biases in the result if not resampling the time-series.
|
||||||
|
If resampling the data using the parameter `resample_frequency`, then
|
||||||
|
setting this number to 0 (or a low float number) is acceptable.
|
||||||
|
TCW_threshold : Int, optional
|
||||||
|
The tasseled cap wetness threshold, beyond which a pixel will be
|
||||||
|
considered 'wet'. Defaults to -0.035.
|
||||||
|
resample_frequency : str
|
||||||
|
Option for resampling time-series of input datasets. This option is useful
|
||||||
|
for either smoothing the WIT plot, or because the area of analysis is larger
|
||||||
|
than a scene width and therefore requires composites. Options include any
|
||||||
|
str accepted by `xarray.resample(time=)`. The resampling method used is .max()
|
||||||
|
export_csv : str, optional
|
||||||
|
To save the returned pandas dataframe as a .csv file, pass a
|
||||||
|
a location string (e.g. 'output/results.csv')
|
||||||
|
dask_chunks : dict, optional
|
||||||
|
To lazily load the datasets using dask, pass a dictionary containing
|
||||||
|
the dimensions over which to chunk e.g. {'time':-1, 'x':250, 'y':250}.
|
||||||
|
verbose: bool, optional
|
||||||
|
If true, print statements are putput detailing the progress of the tool.
|
||||||
|
verbose_progress: bool, optional
|
||||||
|
For use with Dask progress bar
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
df : Pandas.Dataframe
|
||||||
|
A pandas dataframe containing the timeseries of relative fractions
|
||||||
|
of each land cover class (WOfs, FC, TCW)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# add geom to dc query dict
|
||||||
|
if isinstance(gdf, datacube.utils.geometry._base.Geometry):
|
||||||
|
gdf = gpd.GeoDataFrame({'col1':['name'],'geometry':gdf.geom}, crs=gdf.crs)
|
||||||
|
geom = geometry.Geometry(geom=gdf.iloc[0].geometry, crs=gdf.crs)
|
||||||
|
query = {"geopolygon": geom, "time": time}
|
||||||
|
|
||||||
|
# Create a datacube instance
|
||||||
|
dc = datacube.Datacube(app="wetlands insight tool")
|
||||||
|
|
||||||
|
# load landsat 5,7,8 data
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
if verbose_progress:
|
||||||
|
print("Loading Landsat data")
|
||||||
|
ds_ls = load_ard(
|
||||||
|
dc=dc,
|
||||||
|
products=["ls8_sr", "ls7_sr", "ls5_sr"],
|
||||||
|
output_crs="epsg:6933",
|
||||||
|
min_gooddata=min_gooddata,
|
||||||
|
mask_filters=(['opening', 3], ['dilation', 3]),
|
||||||
|
measurements=["red", "green", "blue", "nir", "swir_1", "swir_2"],
|
||||||
|
dask_chunks=dask_chunks,
|
||||||
|
group_by="solar_day",
|
||||||
|
resolution=(-30, 30),
|
||||||
|
verbose=verbose,
|
||||||
|
**query,
|
||||||
|
)
|
||||||
|
|
||||||
|
# create polygon mask
|
||||||
|
mask = xr_rasterize(gdf.iloc[[0]], ds_ls)
|
||||||
|
ds_ls = ds_ls.where(mask)
|
||||||
|
|
||||||
|
# calculate tasselled cap wetness within masked AOI
|
||||||
|
if verbose:
|
||||||
|
print("calculating tasseled cap wetness index ")
|
||||||
|
|
||||||
|
with HiddenPrints(): #suppres the prints from this func
|
||||||
|
tcw = calculate_indices(
|
||||||
|
ds_ls, index=["TCW"], normalise=False, satellite_mission="ls", drop=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if resample_frequency is not None:
|
||||||
|
if verbose:
|
||||||
|
print('Resampling TCW to '+ resample_frequency)
|
||||||
|
tcw = tcw.resample(time=resample_frequency).max()
|
||||||
|
|
||||||
|
tcw = tcw.TCW >= TCW_threshold
|
||||||
|
tcw = tcw.where(mask, 0)
|
||||||
|
tcw = tcw.persist()
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("Loading WOfS layers ")
|
||||||
|
|
||||||
|
wofls = dc.load(
|
||||||
|
product="wofs_ls",
|
||||||
|
like=ds_ls,
|
||||||
|
fuse_func=wofs_fuser,
|
||||||
|
dask_chunks=dask_chunks,
|
||||||
|
collection_category="T1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# boolean of wet/dry
|
||||||
|
wofls_wet = masking.make_mask(wofls.water, wet=True)
|
||||||
|
|
||||||
|
if resample_frequency is not None:
|
||||||
|
if verbose:
|
||||||
|
print('Resampling WOfS to '+ resample_frequency)
|
||||||
|
wofls_wet = wofls_wet.resample(time=resample_frequency).max()
|
||||||
|
|
||||||
|
# mask sure wofs matches other datasets
|
||||||
|
wofls_wet = wofls_wet.where(wofls_wet.time == tcw.time)
|
||||||
|
|
||||||
|
# apply the polygon mask
|
||||||
|
wofls_wet = wofls_wet.where(mask)
|
||||||
|
|
||||||
|
# load Fractional cover
|
||||||
|
if verbose:
|
||||||
|
print("Loading fractional Cover")
|
||||||
|
|
||||||
|
# load fractional cover
|
||||||
|
fc_ds = dc.load(
|
||||||
|
product="fc_ls",
|
||||||
|
time=time,
|
||||||
|
dask_chunks=dask_chunks,
|
||||||
|
like=ds_ls,
|
||||||
|
measurements=["pv", "npv", "bs"],
|
||||||
|
collection_category="T1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# use wofls mask to cloud mask FC
|
||||||
|
clear_and_dry = masking.make_mask(wofls, dry=True).water
|
||||||
|
fc_ds = fc_ds.where(clear_and_dry)
|
||||||
|
|
||||||
|
if resample_frequency is not None:
|
||||||
|
if verbose:
|
||||||
|
print('Resampling FC to '+ resample_frequency)
|
||||||
|
fc_ds = fc_ds.resample(time=resample_frequency).max()
|
||||||
|
|
||||||
|
# mask sure fc matches other datasets
|
||||||
|
fc_ds = fc_ds.where(fc_ds.time == tcw.time)
|
||||||
|
|
||||||
|
# mask with polygon
|
||||||
|
fc_ds = fc_ds.where(mask)
|
||||||
|
|
||||||
|
# mask with TC wetness
|
||||||
|
fc_ds_noTCW = fc_ds.where(tcw == False)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("Generating classification")
|
||||||
|
|
||||||
|
# Cast the dataset to a dataarray
|
||||||
|
fc_ds_noTCW = fc_ds_noTCW.to_array(dim="variable", name="fc_ds_noTCW")
|
||||||
|
|
||||||
|
# turn FC array into integer only as nanargmax doesn't
|
||||||
|
# seem to handle floats the way we want it to
|
||||||
|
fc_int = fc_ds_noTCW.astype("int8")
|
||||||
|
|
||||||
|
# use nanargmax to get the index of the maximum value
|
||||||
|
BSPVNPV = fc_int.argmax(dim="variable")
|
||||||
|
|
||||||
|
#int dytype remocves NaNs so we need to create mask again
|
||||||
|
FC_mask = np.isfinite(fc_ds_noTCW).all(dim="variable")
|
||||||
|
BSPVNPV = BSPVNPV.where(FC_mask)
|
||||||
|
|
||||||
|
# Restack the Fractional cover dataset all together
|
||||||
|
# CAUTION:ARGMAX DEPENDS ON ORDER OF VARIABALES IN
|
||||||
|
# DATASET. NEED TO ADJUST BELOW DEPENDING ON ORDER OF FC VARIABLES
|
||||||
|
|
||||||
|
FC_dominant = xr.Dataset(
|
||||||
|
{
|
||||||
|
"bs": (BSPVNPV == 2).where(FC_mask),
|
||||||
|
"pv": (BSPVNPV == 0).where(FC_mask),
|
||||||
|
"npv": (BSPVNPV == 1).where(FC_mask),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# pixel counts
|
||||||
|
pixels = mask.sum(dim=["x", "y"])
|
||||||
|
|
||||||
|
|
||||||
|
if verbose_progress:
|
||||||
|
print("Computing wetness")
|
||||||
|
tcw_pixel_count = tcw.sum(dim=["x", "y"]).compute()
|
||||||
|
|
||||||
|
if verbose_progress:
|
||||||
|
print("Computing green veg, dry veg, and bare soil")
|
||||||
|
FC_count = FC_dominant.sum(dim=["x", "y"]).compute()
|
||||||
|
|
||||||
|
if verbose_progress:
|
||||||
|
print("Computing open water")
|
||||||
|
wofs_pixels = wofls_wet.sum(dim=["x", "y"]).compute()
|
||||||
|
|
||||||
|
# count percentages
|
||||||
|
wofs_area_percent = (wofs_pixels / pixels) * 100
|
||||||
|
tcw_area_percent = (tcw_pixel_count / pixels) * 100
|
||||||
|
tcw_less_wofs = tcw_area_percent - wofs_area_percent # wet not wofs
|
||||||
|
|
||||||
|
# Fractional cover pixel count method
|
||||||
|
# Get number of FC pixels, divide by total number of pixels per polygon
|
||||||
|
# Work out the number of nodata pixels in the data
|
||||||
|
BS_percent = (FC_count.bs / pixels) * 100
|
||||||
|
PV_percent = (FC_count.pv / pixels) * 100
|
||||||
|
NPV_percent = (FC_count.npv / pixels) * 100
|
||||||
|
NoData_count = ((
|
||||||
|
100 - wofs_area_percent - tcw_less_wofs - PV_percent - NPV_percent - BS_percent
|
||||||
|
) / 100) * pixels
|
||||||
|
|
||||||
|
# re-do percentages but now handling any no-data pixels within polygon
|
||||||
|
BS_percent = (FC_count.bs / (pixels - NoData_count)) * 100
|
||||||
|
PV_percent = (FC_count.pv / (pixels - NoData_count)) * 100
|
||||||
|
NPV_percent = (FC_count.npv / (pixels - NoData_count)) * 100
|
||||||
|
wofs_area_percent = (wofs_pixels / (pixels - NoData_count)) * 100
|
||||||
|
tcw_area_percent = (tcw_pixel_count / (pixels - NoData_count)) * 100
|
||||||
|
tcw_less_wofs = tcw_area_percent - wofs_area_percent
|
||||||
|
|
||||||
|
# Sometimes when we resample datastes, WOfS extent can be
|
||||||
|
# greater than the wetness extent, thus make negative values == zero
|
||||||
|
tcw_less_wofs = tcw_less_wofs.where(tcw_less_wofs>=0, 0)
|
||||||
|
|
||||||
|
# start setup of dataframe by adding only one dataset
|
||||||
|
df = pd.DataFrame(
|
||||||
|
data=wofs_area_percent.data,
|
||||||
|
index=wofs_area_percent.time.values,
|
||||||
|
columns=["wofs_area_percent"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# add data into pandas dataframe for export
|
||||||
|
df["wet_percent"] = tcw_less_wofs.data
|
||||||
|
df["green_veg_percent"] = PV_percent.data
|
||||||
|
df["dry_veg_percent"] = NPV_percent.data
|
||||||
|
df["bare_soil_percent"] = BS_percent.data
|
||||||
|
|
||||||
|
# round numbers
|
||||||
|
df = df.round(2)
|
||||||
|
|
||||||
|
# save the csv of the output data used to create the stacked plot for the polygon drill
|
||||||
|
if export_csv:
|
||||||
|
if verbose:
|
||||||
|
print("exporting csv: " + export_csv)
|
||||||
|
df.to_csv(export_csv, index_label="Datetime")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def animated_timeseries_WIT(
|
||||||
|
ds,
|
||||||
|
df,
|
||||||
|
output_path,
|
||||||
|
width_pixels=1000,
|
||||||
|
interval=200,
|
||||||
|
bands=["red", "green", "blue"],
|
||||||
|
percentile_stretch=(0.02, 0.98),
|
||||||
|
image_proc_func=None,
|
||||||
|
title=False,
|
||||||
|
show_date=True,
|
||||||
|
annotation_kwargs={},
|
||||||
|
onebandplot_cbar=True,
|
||||||
|
onebandplot_kwargs={},
|
||||||
|
shapefile_path=None,
|
||||||
|
shapefile_kwargs={},
|
||||||
|
pandasplot_kwargs={},
|
||||||
|
time_dim="time",
|
||||||
|
x_dim="x",
|
||||||
|
y_dim="y",
|
||||||
|
):
|
||||||
|
|
||||||
|
###############
|
||||||
|
# Setup steps #
|
||||||
|
###############
|
||||||
|
|
||||||
|
# Test if all dimensions exist in dataset
|
||||||
|
if time_dim in ds and x_dim in ds and y_dim in ds:
|
||||||
|
|
||||||
|
# Test if there is one or three bands, and that all exist in both datasets:
|
||||||
|
if ((len(bands) == 3) | (len(bands) == 1)) & all(
|
||||||
|
[(b in ds.data_vars) for b in bands]
|
||||||
|
):
|
||||||
|
|
||||||
|
# Import xarrays as lists of three band numpy arrays
|
||||||
|
imagelist, vmin, vmax = _ds_to_arrraylist(
|
||||||
|
ds,
|
||||||
|
bands=bands,
|
||||||
|
time_dim=time_dim,
|
||||||
|
x_dim=x_dim,
|
||||||
|
y_dim=y_dim,
|
||||||
|
percentile_stretch=percentile_stretch,
|
||||||
|
image_proc_func=image_proc_func,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get time, x and y dimensions of dataset and calculate width vs height of plot
|
||||||
|
timesteps = len(ds[time_dim])
|
||||||
|
width = len(ds[x_dim])
|
||||||
|
height = len(ds[y_dim])
|
||||||
|
width_ratio = float(width) / float(height)
|
||||||
|
height = 10.0 / width_ratio
|
||||||
|
|
||||||
|
# If title is supplied as a string, multiply out to a list with one string per timestep.
|
||||||
|
# Otherwise, use supplied list for plot titles.
|
||||||
|
if isinstance(title, str) or isinstance(title, bool):
|
||||||
|
title_list = [title] * timesteps
|
||||||
|
else:
|
||||||
|
title_list = title
|
||||||
|
|
||||||
|
# Set up annotation parameters that plt.imshow plotting for single band array images.
|
||||||
|
# The nested dict structure sets default values which can be overwritten/customised by the
|
||||||
|
# manually specified `onebandplot_kwargs`
|
||||||
|
onebandplot_kwargs = dict(
|
||||||
|
{
|
||||||
|
"cmap": "Greys",
|
||||||
|
"interpolation": "bilinear",
|
||||||
|
"vmin": vmin,
|
||||||
|
"vmax": vmax,
|
||||||
|
"tick_colour": "black",
|
||||||
|
"tick_fontsize": 11,
|
||||||
|
},
|
||||||
|
**onebandplot_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use pop to remove the two special tick kwargs from the onebandplot_kwargs dict, and save individually
|
||||||
|
onebandplot_tick_colour = onebandplot_kwargs.pop("tick_colour")
|
||||||
|
onebandplot_tick_fontsize = onebandplot_kwargs.pop("tick_fontsize")
|
||||||
|
|
||||||
|
# Set up annotation parameters that control font etc. The nested dict structure sets default
|
||||||
|
# values which can be overwritten/customised by the manually specified `annotation_kwargs`
|
||||||
|
annotation_kwargs = dict(
|
||||||
|
{
|
||||||
|
"xy": (1, 1),
|
||||||
|
"xycoords": "axes fraction",
|
||||||
|
"xytext": (-5, -5),
|
||||||
|
"textcoords": "offset points",
|
||||||
|
"horizontalalignment": "right",
|
||||||
|
"verticalalignment": "top",
|
||||||
|
"fontsize": 15,
|
||||||
|
"color": "white",
|
||||||
|
"path_effects": [
|
||||||
|
PathEffects.withStroke(linewidth=3, foreground="black")
|
||||||
|
],
|
||||||
|
},
|
||||||
|
**annotation_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define default plotting parameters for the overlaying shapefile(s). The nested dict structure sets
|
||||||
|
# default values which can be overwritten/customised by the manually specified `shapefile_kwargs`
|
||||||
|
shapefile_kwargs = dict(
|
||||||
|
{"linewidth": 2, "edgecolor": "black", "facecolor": "#00000000"},
|
||||||
|
**shapefile_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define default plotting parameters for the right-hand line plot. The nested dict structure sets
|
||||||
|
# default values which can be overwritten/customised by the manually specified `pandasplot_kwargs`
|
||||||
|
pandasplot_kwargs = dict({}, **pandasplot_kwargs)
|
||||||
|
|
||||||
|
###################
|
||||||
|
# Initialise plot #
|
||||||
|
###################
|
||||||
|
|
||||||
|
# Set up figure
|
||||||
|
fig, (ax1, ax2) = plt.subplots(
|
||||||
|
ncols=2, gridspec_kw={"width_ratios": [1, 2]}
|
||||||
|
)
|
||||||
|
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0.2, hspace=0)
|
||||||
|
fig.set_size_inches(10.0, height * 0.5, forward=True)
|
||||||
|
ax1.axis("off")
|
||||||
|
ax2.margins(x=0.01)
|
||||||
|
ax2.xaxis.label.set_visible(False)
|
||||||
|
|
||||||
|
# Initialise axesimage objects to be updated during animation, setting extent from dims
|
||||||
|
extents = [
|
||||||
|
float(ds[x_dim].min()),
|
||||||
|
float(ds[x_dim].max()),
|
||||||
|
float(ds[y_dim].min()),
|
||||||
|
float(ds[y_dim].max()),
|
||||||
|
]
|
||||||
|
im = ax1.imshow(imagelist[0], extent=extents, **onebandplot_kwargs)
|
||||||
|
|
||||||
|
# Initialise right panel and set y axis limits
|
||||||
|
# set up color palette
|
||||||
|
pal = [
|
||||||
|
sns.xkcd_rgb["cobalt blue"],
|
||||||
|
sns.xkcd_rgb["neon blue"],
|
||||||
|
sns.xkcd_rgb["grass"],
|
||||||
|
sns.xkcd_rgb["beige"],
|
||||||
|
sns.xkcd_rgb["brown"],
|
||||||
|
]
|
||||||
|
|
||||||
|
# make a stacked area plot
|
||||||
|
ax2.stackplot(
|
||||||
|
df.index,
|
||||||
|
df.wofs_area_percent,
|
||||||
|
df.wet_percent,
|
||||||
|
df.green_veg_percent,
|
||||||
|
df.dry_veg_percent,
|
||||||
|
df.bare_soil_percent,
|
||||||
|
labels=["open water", "wet", "green veg", "dry veg", "bare soil"],
|
||||||
|
colors=pal,
|
||||||
|
alpha=0.6,
|
||||||
|
**pandasplot_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax2.legend(loc="lower left", framealpha=0.6)
|
||||||
|
|
||||||
|
df1 = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"wofs_area_percent": df.wofs_area_percent,
|
||||||
|
"wet_percent": df.wofs_area_percent + df.wet_percent,
|
||||||
|
"green_veg_percent": df.wofs_area_percent
|
||||||
|
+ df.wet_percent
|
||||||
|
+ df.green_veg_percent,
|
||||||
|
"dry_veg_percent": df.wofs_area_percent
|
||||||
|
+ df.wet_percent
|
||||||
|
+ df.green_veg_percent
|
||||||
|
+ df.dry_veg_percent,
|
||||||
|
"bare_soil_percent": df.dry_veg_percent
|
||||||
|
+ df.green_veg_percent
|
||||||
|
+ df.wofs_area_percent
|
||||||
|
+ df.wet_percent
|
||||||
|
+ df.bare_soil_percent,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
df1 = df1.set_index(df.index)
|
||||||
|
|
||||||
|
line_test = df1.plot(
|
||||||
|
ax=ax2, legend=False, color="black", **pandasplot_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# set axis limits to the min and max
|
||||||
|
ax2.set(xlim=(df.index[0], df.index[-1]), ylim=(0, 100))
|
||||||
|
|
||||||
|
# add a legend and a tight plot box
|
||||||
|
|
||||||
|
ax2.set_title("Fractional Cover, Wetness, and Water")
|
||||||
|
|
||||||
|
# Initialise annotation objects to be updated during animation
|
||||||
|
t = ax1.annotate("", **annotation_kwargs)
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# Add optional overlays #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# Optionally add shapefile overlay(s) from either string path or list of string paths
|
||||||
|
if isinstance(shapefile_path, str):
|
||||||
|
|
||||||
|
shapefile = gpd.read_file(shapefile_path)
|
||||||
|
shapefile.plot(**shapefile_kwargs, ax=ax1)
|
||||||
|
|
||||||
|
elif isinstance(shapefile_path, list):
|
||||||
|
|
||||||
|
# Iterate through list of string paths
|
||||||
|
for shapefile in shapefile_path:
|
||||||
|
|
||||||
|
shapefile = gpd.read_file(shapefile)
|
||||||
|
shapefile.plot(**shapefile_kwargs, ax=ax1)
|
||||||
|
|
||||||
|
# After adding shapefile, fix extents of plot
|
||||||
|
ax1.set_xlim(extents[0], extents[1])
|
||||||
|
ax1.set_ylim(extents[2], extents[3])
|
||||||
|
|
||||||
|
# Optionally add colourbar for one band images
|
||||||
|
if (len(bands) == 1) & onebandplot_cbar:
|
||||||
|
_add_colourbar(
|
||||||
|
ax1,
|
||||||
|
im,
|
||||||
|
tick_fontsize=onebandplot_tick_fontsize,
|
||||||
|
tick_colour=onebandplot_tick_colour,
|
||||||
|
vmin=onebandplot_kwargs["vmin"],
|
||||||
|
vmax=onebandplot_kwargs["vmax"],
|
||||||
|
)
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Create function to update each frame #
|
||||||
|
########################################
|
||||||
|
|
||||||
|
# Function to update figure
|
||||||
|
|
||||||
|
def update_figure(frame_i):
|
||||||
|
|
||||||
|
####################
|
||||||
|
# Plot image panel #
|
||||||
|
####################
|
||||||
|
|
||||||
|
# If possible, extract dates from time dimension
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Get human-readable date info (e.g. "16 May 1990")
|
||||||
|
ts = ds[time_dim][{time_dim: frame_i}].dt
|
||||||
|
year = ts.year.item()
|
||||||
|
month = ts.month.item()
|
||||||
|
day = ts.day.item()
|
||||||
|
date_string = "{} {} {}".format(
|
||||||
|
day, calendar.month_abbr[month], year
|
||||||
|
)
|
||||||
|
|
||||||
|
except:
|
||||||
|
|
||||||
|
date_string = ds[time_dim][{time_dim: frame_i}].values.item()
|
||||||
|
|
||||||
|
# Create annotation string based on title and date specifications:
|
||||||
|
title = title_list[frame_i]
|
||||||
|
if title and show_date:
|
||||||
|
title_date = "{}\n{}".format(date_string, title)
|
||||||
|
elif title and not show_date:
|
||||||
|
title_date = "{}".format(title)
|
||||||
|
elif show_date and not title:
|
||||||
|
title_date = "{}".format(date_string)
|
||||||
|
else:
|
||||||
|
title_date = ""
|
||||||
|
|
||||||
|
# Update left panel with annotation and image
|
||||||
|
im.set_array(imagelist[frame_i])
|
||||||
|
t.set_text(title_date)
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Plot linegraph panel #
|
||||||
|
########################
|
||||||
|
|
||||||
|
# Create list of artists to return
|
||||||
|
artist_list = [im, t]
|
||||||
|
|
||||||
|
# Update right panel with temporal line subset, adding each new line into artist_list
|
||||||
|
for i, line in enumerate(line_test.lines):
|
||||||
|
|
||||||
|
# Clip line data to current time, and get x and y values
|
||||||
|
y = df1[
|
||||||
|
df1.index
|
||||||
|
<= datetime(year=year, month=month, day=day, hour=23, minute=59)
|
||||||
|
].iloc[:, i]
|
||||||
|
x = df1[
|
||||||
|
df1.index
|
||||||
|
<= datetime(year=year, month=month, day=day, hour=23, minute=59)
|
||||||
|
].index
|
||||||
|
|
||||||
|
# Plot lines after stripping NaNs (this produces continuous, unbroken lines)
|
||||||
|
line.set_data(x[y.notnull()], y[y.notnull()])
|
||||||
|
artist_list.extend([line])
|
||||||
|
|
||||||
|
# Return the artists set
|
||||||
|
return artist_list
|
||||||
|
|
||||||
|
# Nicely space subplots
|
||||||
|
fig.tight_layout()
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# Generate and run animation #
|
||||||
|
##############################
|
||||||
|
|
||||||
|
# Generate animation
|
||||||
|
ani = animation.FuncAnimation(
|
||||||
|
fig=fig,
|
||||||
|
func=update_figure,
|
||||||
|
frames=timesteps,
|
||||||
|
interval=interval,
|
||||||
|
blit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Export as either MP4 or GIF
|
||||||
|
if output_path[-3:] == "mp4":
|
||||||
|
print(" Exporting animation to {}".format(output_path))
|
||||||
|
ani.save(output_path, dpi=width_pixels / 10.0)
|
||||||
|
|
||||||
|
elif output_path[-3:] == "wmv":
|
||||||
|
print(" Exporting animation to {}".format(output_path))
|
||||||
|
ani.save(
|
||||||
|
output_path,
|
||||||
|
dpi=width_pixels / 10.0,
|
||||||
|
writer=animation.FFMpegFileWriter(
|
||||||
|
fps=1000 / interval, bitrate=4000, codec="wmv2"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif output_path[-3:] == "gif":
|
||||||
|
print(" Exporting animation to {}".format(output_path))
|
||||||
|
ani.save(output_path, dpi=width_pixels / 10.0, writer="imagemagick")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(" Output file type must be either .mp4, .wmv or .gif")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"Please select either one or three bands that all exist in the input dataset"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"At least one x, y or time dimension does not exist in the input dataset. Please use the `time_dim`,"
|
||||||
|
"`x_dim` or `y_dim` parameters to override the default dimension names used for plotting"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Define function to convert xarray dataset to list of one or three band numpy arrays
|
||||||
|
|
||||||
|
|
||||||
|
def _ds_to_arrraylist(
|
||||||
|
ds, bands, time_dim, x_dim, y_dim, percentile_stretch, image_proc_func=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Converts an xarray dataset to a list of numpy arrays for plt.imshow plotting
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Compute percents
|
||||||
|
p_low, p_high = ds[bands].to_array().quantile(percentile_stretch).values
|
||||||
|
|
||||||
|
array_list = []
|
||||||
|
for i, timestep in enumerate(ds[time_dim]):
|
||||||
|
|
||||||
|
# Select single timestep from the data array
|
||||||
|
ds_i = ds[{time_dim: i}]
|
||||||
|
|
||||||
|
# Get shape of array
|
||||||
|
x = len(ds[x_dim])
|
||||||
|
y = len(ds[y_dim])
|
||||||
|
|
||||||
|
if len(bands) == 1:
|
||||||
|
|
||||||
|
# Create new one band array
|
||||||
|
img_toshow = exposure.rescale_intensity(
|
||||||
|
ds_i[bands[0]].values, in_range=(p_low, p_high), out_range="image"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# Create new three band array
|
||||||
|
rawimg = np.zeros((y, x, 3), dtype=np.float32)
|
||||||
|
|
||||||
|
# Add xarray bands into three dimensional numpy array
|
||||||
|
for band, colour in enumerate(bands):
|
||||||
|
|
||||||
|
rawimg[:, :, band] = ds_i[colour].values
|
||||||
|
|
||||||
|
# Stretch contrast using percentile values
|
||||||
|
img_toshow = exposure.rescale_intensity(
|
||||||
|
rawimg, in_range=(p_low, p_high), out_range=(0, 1.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optionally image processing
|
||||||
|
if image_proc_func:
|
||||||
|
|
||||||
|
img_toshow = image_proc_func(img_toshow).clip(0, 1)
|
||||||
|
|
||||||
|
array_list.append(img_toshow)
|
||||||
|
|
||||||
|
return array_list, p_low, p_high
|
||||||
|
|
||||||
|
|
||||||
|
def _add_colourbar(
|
||||||
|
ax, im, vmin, vmax, cmap="Greys", tick_fontsize=15, tick_colour="black"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Add a nicely formatted colourbar to an animation panel
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add colourbar
|
||||||
|
axins2 = inset_axes(ax, width="97%", height="4%", loc=8, borderpad=1)
|
||||||
|
plt.gcf().colorbar(
|
||||||
|
im, cax=axins2, orientation="horizontal", ticks=np.linspace(vmin, vmax, 3)
|
||||||
|
)
|
||||||
|
axins2.xaxis.set_ticks_position("top")
|
||||||
|
axins2.tick_params(axis="x", colors=tick_colour, labelsize=tick_fontsize)
|
||||||
|
|
||||||
|
# Justify left and right labels to edge of plot
|
||||||
|
axins2.get_xticklabels()[0].set_horizontalalignment("left")
|
||||||
|
axins2.get_xticklabels()[-1].set_horizontalalignment("right")
|
||||||
|
labels = [item.get_text() for item in axins2.get_xticklabels()]
|
||||||
|
labels[0] = " " + labels[0]
|
||||||
|
labels[-1] = labels[-1] + " "
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# print that we are running the testing
|
||||||
|
print("Testing..")
|
||||||
|
# import doctest to test our module for documentation
|
||||||
|
import doctest
|
||||||
|
|
||||||
|
doctest.testmod()
|
||||||
|
print("Testing done")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a446d333bf7f6d0cb7df014f12b0da3f7298f85bdfb4de06893173e90fbd5ccb
|
||||||
|
size 14112695
|
||||||
+320
@@ -0,0 +1,320 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
# Common imports and settings
|
||||||
|
import os, sys
|
||||||
|
os.environ['USE_PYGEOS'] = '0'
|
||||||
|
from IPython.display import Markdown
|
||||||
|
import pandas as pd
|
||||||
|
pd.set_option("display.max_rows", None)
|
||||||
|
import xarray as xr
|
||||||
|
|
||||||
|
# Datacube
|
||||||
|
import datacube
|
||||||
|
from datacube.utils.rio import configure_s3_access
|
||||||
|
from datacube.utils import masking
|
||||||
|
from datacube.utils.cog import write_cog
|
||||||
|
# https://github.com/GeoscienceAustralia/dea-notebooks/tree/develop/Tools
|
||||||
|
from dea_tools.plotting import display_map, rgb
|
||||||
|
from dea_tools.datahandling import mostcommon_crs
|
||||||
|
|
||||||
|
# EASI defaults
|
||||||
|
easinotebooksrepo = '/home/jovyan/easi-notebooks'
|
||||||
|
if easinotebooksrepo not in sys.path: sys.path.append(easinotebooksrepo)
|
||||||
|
from easi_tools import EasiDefaults, xarray_object_size, notebook_utils, unset_cachingproxy
|
||||||
|
from easi_tools.load_s2l2a import load_s2l2a_with_offset
|
||||||
|
from dask.distributed import progress
|
||||||
|
|
||||||
|
# Data tools
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Datacube
|
||||||
|
from datacube.utils import masking # https://github.com/opendatacube/datacube-core/blob/develop/datacube/utils/masking.py
|
||||||
|
from odc.algo import enum_to_bool # https://github.com/opendatacube/odc-algo/blob/main/odc/algo/_masking.py
|
||||||
|
from odc.algo import xr_reproject # https://github.com/opendatacube/odc-algo/blob/main/odc/algo/_warp.py
|
||||||
|
from datacube.utils.geometry import GeoBox, box # https://github.com/opendatacube/datacube-core/blob/develop/datacube/utils/geometry/_base.py
|
||||||
|
|
||||||
|
# Holoviews, Datashader and Bokeh
|
||||||
|
import hvplot.pandas
|
||||||
|
import hvplot.xarray
|
||||||
|
import holoviews as hv
|
||||||
|
import panel as pn
|
||||||
|
import colorcet as cc
|
||||||
|
import cartopy.crs as ccrs
|
||||||
|
from datashader import reductions
|
||||||
|
from holoviews import opts
|
||||||
|
from utils import load_data_geo
|
||||||
|
import rasterio
|
||||||
|
import rioxarray
|
||||||
|
# import geoviews as gv
|
||||||
|
# from holoviews.operation.datashader import rasterize
|
||||||
|
hv.extension('bokeh', logo=False)
|
||||||
|
|
||||||
|
from deafrica_tools.bandindices import calculate_indices
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from sklearn.metrics import accuracy_score, classification_report
|
||||||
|
from sklearn.preprocessing import LabelEncoder
|
||||||
|
|
||||||
|
from sklearn.pipeline import Pipeline
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.impute import SimpleImputer
|
||||||
|
from sklearn.preprocessing import StandardScaler
|
||||||
|
from sklearn.model_selection import GridSearchCV
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from sklearn.metrics import accuracy_score
|
||||||
|
from shapely.geometry import Point, Polygon
|
||||||
|
import geopandas as gpd
|
||||||
|
from pyproj import CRS
|
||||||
|
from matplotlib.colors import ListedColormap
|
||||||
|
from holoviews import opts
|
||||||
|
from datashader import reductions
|
||||||
|
from bokeh.models.tickers import FixedTicker
|
||||||
|
from rioxarray.merge import merge_arrays
|
||||||
|
|
||||||
|
import joblib
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(dc, date_range, longtitude_range, latitude_range):
|
||||||
|
product = 's2_l2a'
|
||||||
|
query = {
|
||||||
|
'product': product, # Product name
|
||||||
|
'x': longtitude_range, # "x" axis bounds
|
||||||
|
'y': latitude_range, # "y" axis bounds
|
||||||
|
'time': date_range, # Any parsable date strings
|
||||||
|
}
|
||||||
|
native_crs = notebook_utils.mostcommon_crs(dc, query)
|
||||||
|
print(f'Most common native CRS: {native_crs}')
|
||||||
|
measurements = ['blue', 'green', 'red', 'nir', 'scl']
|
||||||
|
|
||||||
|
load_params = {
|
||||||
|
'measurements': measurements, # Selected measurement or alias names
|
||||||
|
'output_crs': native_crs, # Target EPSG code
|
||||||
|
'resolution': (-10, 10), # Target resolution
|
||||||
|
'group_by': 'solar_day', # Scene grouping
|
||||||
|
'dask_chunks': {'x': 2048, 'y': 2048}, # Dask chunks
|
||||||
|
}
|
||||||
|
data = load_s2l2a_with_offset(
|
||||||
|
dc,
|
||||||
|
query | load_params # Combine the two dicts that contain our search and load parameters
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def mask_clean(data):
|
||||||
|
flag_name = 'scl'
|
||||||
|
flag_desc = masking.describe_variable_flags(data[flag_name]) # Pandas dataframe
|
||||||
|
display(flag_desc)
|
||||||
|
display(flag_desc.loc['qa'].values[1])
|
||||||
|
# Create a "data quality" Mask layer
|
||||||
|
flags_def = flag_desc.loc['qa'].values[1]
|
||||||
|
good_pixel_flags = [flags_def[str(i)] for i in [2, 4, 5, 6]] # To pass strings to enum_to_bool()
|
||||||
|
|
||||||
|
# enum_to_bool calculates the pixel-wise "or" of each set of pixels given by good_pixel_flags
|
||||||
|
# 1 = good data
|
||||||
|
# 0 = "bad" data
|
||||||
|
good_pixel_mask = enum_to_bool(data[flag_name], good_pixel_flags)
|
||||||
|
data_layer_names = [x for x in data.data_vars if x != 'scl']
|
||||||
|
# Apply good pixel mask to blue, green, red and nir.
|
||||||
|
result = data[data_layer_names].where(good_pixel_mask).persist()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def fill_nan(ndvi, time_split):
|
||||||
|
rs = []
|
||||||
|
for times in time_split:
|
||||||
|
tmp = ndvi.sel(time=times)
|
||||||
|
fill_ds = tmp.sel(time=times).bfill(dim='time')
|
||||||
|
fill_ds = fill_ds.sel(time=times).ffill(dim='time')
|
||||||
|
rs.append(fill_ds)
|
||||||
|
merged_ndvi = xr.concat([i for i in rs], dim="time")
|
||||||
|
fill_m = merged_ndvi.bfill(dim="time")
|
||||||
|
fill_m = fill_m.ffill(dim="time")
|
||||||
|
return fill_m
|
||||||
|
|
||||||
|
|
||||||
|
def load_train_data(train_path):
|
||||||
|
train = load_data_geo(train_path)
|
||||||
|
return train
|
||||||
|
|
||||||
|
|
||||||
|
def load_sen1(name_vh, name_vv):
|
||||||
|
dsvv = rioxarray.open_rasterio(name_vv)
|
||||||
|
dsvh = rioxarray.open_rasterio(name_vh)
|
||||||
|
return dsvh, dsvv
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_sen1_and_sen2(train, average_ndvi, dsvh, dsvv):
|
||||||
|
loaded_datasets = {}
|
||||||
|
for idx, point in train.iterrows():
|
||||||
|
key = f"point_{idx + 1}"
|
||||||
|
try:
|
||||||
|
ndvi_data = average_ndvi.sel(x=point.geometry.x, y=point.geometry.y, method='nearest').values
|
||||||
|
vh_data = dsvh.sel(x=point.geometry.x, y=point.geometry.y, method='nearest').values
|
||||||
|
vv_data = dsvv.sel(x=point.geometry.x, y=point.geometry.y, method='nearest').values
|
||||||
|
loaded_datasets[key] = {
|
||||||
|
"data": np.concatenate((ndvi_data, vh_data, vv_data)),
|
||||||
|
"label": point.HT_code
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
# loaded_datasets[key] = None
|
||||||
|
print(e)
|
||||||
|
return loaded_datasets
|
||||||
|
|
||||||
|
|
||||||
|
def split_train_data(train, label_mapping, datasets):
|
||||||
|
label_encoder = LabelEncoder()
|
||||||
|
|
||||||
|
# Fit and transform the labels
|
||||||
|
labels = train.Hientrang.values
|
||||||
|
numeric_labels = label_encoder.fit_transform([label_mapping[label] for label in labels])
|
||||||
|
X = []
|
||||||
|
x_new = []
|
||||||
|
lb_new = []
|
||||||
|
for k, v in datasets.items():
|
||||||
|
X.append(v)
|
||||||
|
for i in range(len(X)):
|
||||||
|
if X[i] is not None:
|
||||||
|
x_new.append(X[i]["data"])
|
||||||
|
lb_new.append(numeric_labels[i])
|
||||||
|
X_train, X_temp, y_train, y_temp= train_test_split(x_new, lb_new, test_size=0.4, random_state=42)
|
||||||
|
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
|
||||||
|
return X_train, X_val, X_test, y_train, y_val, y_test
|
||||||
|
|
||||||
|
|
||||||
|
def train_with_rf(X_train, X_val, y_train, y_val):
|
||||||
|
# Takes 1-2 minutes to complete
|
||||||
|
|
||||||
|
# Tạo RandomForestClassifier mặc định để sử dụng làm mô hình ban đầu trong pipeline
|
||||||
|
base_model = RandomForestClassifier(random_state=42, n_jobs=-1)
|
||||||
|
|
||||||
|
# Tạo pipeline
|
||||||
|
pipeline = Pipeline([
|
||||||
|
# ('imputer', SimpleImputer(strategy='mean')),
|
||||||
|
('scaler', StandardScaler()),
|
||||||
|
('classifier', base_model),
|
||||||
|
])
|
||||||
|
# Thiết lập các tham số bạn muốn tối ưu hóa
|
||||||
|
param_grid = {
|
||||||
|
'classifier__n_estimators': [100, 300, 500, 700, 1000],
|
||||||
|
'classifier__max_depth': [6, 8, 10, 15, 20],
|
||||||
|
'classifier__criterion': ['gini', 'entropy'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sử dụng GridSearchCV để tìm bộ tham số tốt nhất
|
||||||
|
grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='accuracy', n_jobs=-1)
|
||||||
|
grid_search.fit(X_train, y_train)
|
||||||
|
|
||||||
|
# In ra bộ tham số tốt nhất
|
||||||
|
best_params = grid_search.best_params_
|
||||||
|
print("Best Parameters:", best_params)
|
||||||
|
|
||||||
|
# Dự đoán trên tập kiểm tra
|
||||||
|
y_pred = grid_search.predict(X_val)
|
||||||
|
|
||||||
|
# Đánh giá kết quả
|
||||||
|
accuracy = accuracy_score(y_val, y_pred)
|
||||||
|
print(f"Accuracy: {round(accuracy, 2)*100} %")
|
||||||
|
return grid_search
|
||||||
|
|
||||||
|
|
||||||
|
def save_model(name_file, grid_search):
|
||||||
|
dir_save_model = "model_train"
|
||||||
|
if not os.path.exists(dir_save_model):
|
||||||
|
os.mkdir(dir_save_model)
|
||||||
|
joblib.dump(grid_search, os.path.join(dir_save_model, name_file))
|
||||||
|
print("Done!")
|
||||||
|
|
||||||
|
|
||||||
|
def predict(model, data_crs, ndvi, vh, vv):
|
||||||
|
data_predict = []
|
||||||
|
for i in range(ndvi.shape[1]):
|
||||||
|
ndvi_tmp = ndvi.isel(y=i).values
|
||||||
|
vh_data = vh.sel(y=ndvi.y.values[i], method='nearest').values
|
||||||
|
vv_data = vv.sel(y=ndvi.y.values[i], method='nearest').values
|
||||||
|
all_tmp = np.concatenate((ndvi_tmp, vh_data, vv_data), axis=0)
|
||||||
|
data_predict.extend(all_tmp.T)
|
||||||
|
y_pred = model.predict(data_predict)
|
||||||
|
final_label = y_pred.reshape(ndvi.y.shape[0], ndvi.x.shape[0])
|
||||||
|
|
||||||
|
final_xarray_save = xr.DataArray(final_label, dims=("y", "x"))
|
||||||
|
final_xarray_save = final_xarray_save.rio.write_crs(data_crs)
|
||||||
|
|
||||||
|
x_values = ndvi.x.values
|
||||||
|
y_values = ndvi.y.values
|
||||||
|
|
||||||
|
data_array = xr.DataArray(final_xarray_save,
|
||||||
|
coords={'x': x_values, 'y': y_values},
|
||||||
|
dims=['y', 'x'])
|
||||||
|
data_array = data_array.rio.write_crs(ndvi.rio.crs)
|
||||||
|
return data_array
|
||||||
|
|
||||||
|
|
||||||
|
def cut_according_shp(thuanhoa_path, average_ndvi, data_array):
|
||||||
|
gdf = gpd.read_file(thuanhoa_path)
|
||||||
|
gdf = gdf.to_crs(average_ndvi.rio.crs)
|
||||||
|
polygon_coords = list(gdf.geometry.values[0].exterior.coords)
|
||||||
|
polygon_coordinates = [(x, y) for x, y in polygon_coords]
|
||||||
|
|
||||||
|
geometries = [
|
||||||
|
{
|
||||||
|
'type': 'Polygon',
|
||||||
|
'coordinates': [polygon_coordinates]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
region_result = data_array.rio.clip(geometries, data_array.rio.crs, drop=False)
|
||||||
|
region_result = region_result.where(region_result >= 0, float('nan'))
|
||||||
|
return region_result
|
||||||
|
|
||||||
|
|
||||||
|
def compare(KD_path, KetQuaPhanLoaiDat, CODE_MAP, HT_MAP):
|
||||||
|
gdf = gpd.read_file(KD_path, crs="EPSG:9209")
|
||||||
|
polygon = gdf.geometry.values
|
||||||
|
label = gdf.tenchu.values
|
||||||
|
ouput_image = rioxarray.open_rasterio(KetQuaPhanLoaiDat)
|
||||||
|
code_tq = HT_MAP["TQ"]["data"][0]
|
||||||
|
code_pnn = HT_MAP["PNN"]["data"][0]
|
||||||
|
result = {}
|
||||||
|
for key, values in HT_MAP.items():
|
||||||
|
print(f"process {key}")
|
||||||
|
array_list = []
|
||||||
|
for i in range(len(polygon)):
|
||||||
|
po = polygon[i]
|
||||||
|
lb = label[i]
|
||||||
|
code_lb = CODE_MAP.get(lb, code_tq)
|
||||||
|
try:
|
||||||
|
qr = ouput_image.rio.clip([po], "EPSG:9209")
|
||||||
|
if code_lb in values["data"]:
|
||||||
|
if code_lb == code_pnn:
|
||||||
|
qr = qr.where((qr != float(code_pnn)), np.nan)
|
||||||
|
# qr = qr.where((qr != 3.0), np.nan)
|
||||||
|
elif code_lb == code_tq:
|
||||||
|
qr = qr.where((qr != float(code_pnn)), np.nan)
|
||||||
|
qr = qr.where((qr != 3.0), np.nan)
|
||||||
|
else:
|
||||||
|
qr = qr.where(qr != float(code_lb), np.nan)
|
||||||
|
else:
|
||||||
|
qr.values[:, :, :] = np.nan
|
||||||
|
array_list.append(qr)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
result.update({key: array_list})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def save_result(result, HT_MAP):
|
||||||
|
# cmap = ListedColormap(colors)
|
||||||
|
save_path = "ThuanHoa/KetQua"
|
||||||
|
if not os.path.exists(save_path):
|
||||||
|
os.mkdir(save_path)
|
||||||
|
|
||||||
|
for k, v in result.items():
|
||||||
|
rs = merge_arrays(v, nodata = np.nan)
|
||||||
|
rs.rio.to_raster(f"{save_path}/{k}.tif")
|
||||||
|
print(f"save {save_path}/{k}.tif")
|
||||||
|
# img = rs.plot(cmap=cmap, add_colorbar=False)
|
||||||
|
# cbar = plt.colorbar(img)
|
||||||
|
# cbar.ax.set_yticklabels(labels)
|
||||||
|
# plt.title(f'{HT_MAP[k]["name"]}')
|
||||||
|
# plt.axis('off')
|
||||||
|
# plt.show()
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433],AUTHORITY["EPSG",4326]]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
PROJCS["WGS_1984_UTM_Zone_48N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]
|
||||||
@@ -0,0 +1,835 @@
|
|||||||
|
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||||
|
<qgis simplifyLocal="1" labelsEnabled="0" simplifyMaxScale="1" symbologyReferenceScale="-1" simplifyDrawingHints="0" readOnly="0" version="3.30.0-'s-Hertogenbosch" styleCategories="AllStyleCategories" minScale="100000000" hasScaleBasedVisibilityFlag="0" simplifyAlgorithm="0" simplifyDrawingTol="1" maxScale="0">
|
||||||
|
<flags>
|
||||||
|
<Identifiable>1</Identifiable>
|
||||||
|
<Removable>1</Removable>
|
||||||
|
<Searchable>1</Searchable>
|
||||||
|
<Private>0</Private>
|
||||||
|
</flags>
|
||||||
|
<temporal durationUnit="min" startField="" startExpression="" enabled="0" mode="0" endField="" limitMode="0" endExpression="" durationField="" fixedDuration="0" accumulate="0">
|
||||||
|
<fixedRange>
|
||||||
|
<start></start>
|
||||||
|
<end></end>
|
||||||
|
</fixedRange>
|
||||||
|
</temporal>
|
||||||
|
<elevation zoffset="0" clamping="Terrain" zscale="1" type="IndividualFeatures" extrusionEnabled="0" showMarkerSymbolInSurfacePlots="0" respectLayerSymbol="1" extrusion="0" symbology="Line" binding="Centroid">
|
||||||
|
<data-defined-properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data-defined-properties>
|
||||||
|
<profileLineSymbol>
|
||||||
|
<symbol alpha="1" name="" type="line" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleLine" locked="0" pass="0" enabled="1" id="{4823d26e-1862-4571-b95a-700950c67ef2}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||||
|
<Option name="capstyle" type="QString" value="square"/>
|
||||||
|
<Option name="customdash" type="QString" value="5;2"/>
|
||||||
|
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||||
|
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="line_color" type="QString" value="213,180,60,255"/>
|
||||||
|
<Option name="line_style" type="QString" value="solid"/>
|
||||||
|
<Option name="line_width" type="QString" value="0.6"/>
|
||||||
|
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="offset" type="QString" value="0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="ring_filter" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||||
|
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||||
|
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</profileLineSymbol>
|
||||||
|
<profileFillSymbol>
|
||||||
|
<symbol alpha="1" name="" type="fill" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleFill" locked="0" pass="0" enabled="1" id="{e14e2a41-3f73-4ada-ae93-be42049e0fc8}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="color" type="QString" value="213,180,60,255"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="152,129,43,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0.2"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="style" type="QString" value="solid"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</profileFillSymbol>
|
||||||
|
<profileMarkerSymbol>
|
||||||
|
<symbol alpha="1" name="" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{dac44936-adba-4f10-96ad-7fd6e313a662}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="213,180,60,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="diamond"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="152,129,43,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0.2"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="3"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</profileMarkerSymbol>
|
||||||
|
</elevation>
|
||||||
|
<renderer-v2 symbollevels="0" type="categorizedSymbol" referencescale="-1" forceraster="0" attr="HT_code" enableorderby="0">
|
||||||
|
<categories>
|
||||||
|
<category type="long" label="1 - Lua tom" symbol="0" render="true" value="1"/>
|
||||||
|
<category type="long" label="2 - Lua 2 vu" symbol="1" render="true" value="2"/>
|
||||||
|
<category type="long" label="3 - Lua 3 vu" symbol="2" render="true" value="3"/>
|
||||||
|
<category type="long" label="4 - Cay hang nam" symbol="3" render="true" value="4"/>
|
||||||
|
<category type="long" label="5 - Cay lau nam" symbol="4" render="true" value="5"/>
|
||||||
|
<category type="long" label="6 - TS" symbol="5" render="true" value="6"/>
|
||||||
|
<category type="long" label="7 - Song rach" symbol="6" render="true" value="7"/>
|
||||||
|
<category type="long" label="8 - Dat xay dung" symbol="7" render="true" value="8"/>
|
||||||
|
<category type="long" label="9 - Rung" symbol="8" render="true" value="9"/>
|
||||||
|
<category type="string" label="" symbol="9" render="true" value=""/>
|
||||||
|
</categories>
|
||||||
|
<symbols>
|
||||||
|
<symbol alpha="1" name="0" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="108,101,225,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="1" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="100,233,133,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="2" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="214,61,87,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="3" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="18,125,213,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="4" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="199,140,233,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="5" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="237,104,206,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="6" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="225,231,108,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="7" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="84,213,14,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="8" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="50,208,184,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" name="9" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="213,44,75,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</symbols>
|
||||||
|
<source-symbol>
|
||||||
|
<symbol alpha="1" name="0" type="marker" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleMarker" locked="0" pass="0" enabled="1" id="{0eba47fb-4130-4afe-b08f-be065656540b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="69,237,206,255"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</source-symbol>
|
||||||
|
<rotation/>
|
||||||
|
<sizescale/>
|
||||||
|
</renderer-v2>
|
||||||
|
<customproperties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="dualview/previewExpressions" type="List">
|
||||||
|
<Option type="QString" value=""No""/>
|
||||||
|
</Option>
|
||||||
|
<Option name="embeddedWidgets/count" type="int" value="0"/>
|
||||||
|
<Option name="variableNames"/>
|
||||||
|
<Option name="variableValues"/>
|
||||||
|
</Option>
|
||||||
|
</customproperties>
|
||||||
|
<blendMode>0</blendMode>
|
||||||
|
<featureBlendMode>0</featureBlendMode>
|
||||||
|
<layerOpacity>1</layerOpacity>
|
||||||
|
<SingleCategoryDiagramRenderer attributeLegend="1" diagramType="Histogram">
|
||||||
|
<DiagramCategory lineSizeScale="3x:0,0,0,0,0,0" penAlpha="255" minScaleDenominator="0" scaleBasedVisibility="0" spacing="5" lineSizeType="MM" scaleDependency="Area" opacity="1" backgroundColor="#ffffff" height="15" diagramOrientation="Up" barWidth="5" width="15" maxScaleDenominator="1e+08" sizeScale="3x:0,0,0,0,0,0" labelPlacementMethod="XHeight" backgroundAlpha="255" rotationOffset="270" enabled="0" penColor="#000000" minimumSize="0" sizeType="MM" direction="0" spacingUnitScale="3x:0,0,0,0,0,0" spacingUnit="MM" showAxis="1" penWidth="0">
|
||||||
|
<fontProperties description="MS Shell Dlg 2,8.25,-1,5,50,0,0,0,0,0" strikethrough="0" italic="0" underline="0" style="" bold="0"/>
|
||||||
|
<attribute color="#000000" label="" colorOpacity="1" field=""/>
|
||||||
|
<axisSymbol>
|
||||||
|
<symbol alpha="1" name="" type="line" is_animated="0" force_rhr="0" frame_rate="10" clip_to_extent="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer class="SimpleLine" locked="0" pass="0" enabled="1" id="{2b5bc07c-e6a7-41b4-bf16-35cf42f3de0b}">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||||
|
<Option name="capstyle" type="QString" value="square"/>
|
||||||
|
<Option name="customdash" type="QString" value="5;2"/>
|
||||||
|
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||||
|
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="line_color" type="QString" value="35,35,35,255"/>
|
||||||
|
<Option name="line_style" type="QString" value="solid"/>
|
||||||
|
<Option name="line_width" type="QString" value="0.26"/>
|
||||||
|
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="offset" type="QString" value="0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="ring_filter" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||||
|
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||||
|
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</axisSymbol>
|
||||||
|
</DiagramCategory>
|
||||||
|
</SingleCategoryDiagramRenderer>
|
||||||
|
<DiagramLayerSettings priority="0" placement="0" linePlacementFlags="18" showAll="1" zIndex="0" obstacle="0" dist="0">
|
||||||
|
<properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</properties>
|
||||||
|
</DiagramLayerSettings>
|
||||||
|
<geometryOptions removeDuplicateNodes="0" geometryPrecision="0">
|
||||||
|
<activeChecks/>
|
||||||
|
<checkConfiguration/>
|
||||||
|
</geometryOptions>
|
||||||
|
<legend type="default-vector" showLabelLegend="0"/>
|
||||||
|
<referencedLayers/>
|
||||||
|
<fieldConfiguration>
|
||||||
|
<field name="No" configurationFlags="None">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="X" configurationFlags="None">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="Y" configurationFlags="None">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="LU2022" configurationFlags="None">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="Hientrang" configurationFlags="None">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="HT_code" configurationFlags="None">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
</fieldConfiguration>
|
||||||
|
<aliases>
|
||||||
|
<alias index="0" name="" field="No"/>
|
||||||
|
<alias index="1" name="" field="X"/>
|
||||||
|
<alias index="2" name="" field="Y"/>
|
||||||
|
<alias index="3" name="" field="LU2022"/>
|
||||||
|
<alias index="4" name="" field="Hientrang"/>
|
||||||
|
<alias index="5" name="" field="HT_code"/>
|
||||||
|
</aliases>
|
||||||
|
<splitPolicies>
|
||||||
|
<policy field="No" policy="Duplicate"/>
|
||||||
|
<policy field="X" policy="Duplicate"/>
|
||||||
|
<policy field="Y" policy="Duplicate"/>
|
||||||
|
<policy field="LU2022" policy="Duplicate"/>
|
||||||
|
<policy field="Hientrang" policy="Duplicate"/>
|
||||||
|
<policy field="HT_code" policy="Duplicate"/>
|
||||||
|
</splitPolicies>
|
||||||
|
<defaults>
|
||||||
|
<default applyOnUpdate="0" expression="" field="No"/>
|
||||||
|
<default applyOnUpdate="0" expression="" field="X"/>
|
||||||
|
<default applyOnUpdate="0" expression="" field="Y"/>
|
||||||
|
<default applyOnUpdate="0" expression="" field="LU2022"/>
|
||||||
|
<default applyOnUpdate="0" expression="" field="Hientrang"/>
|
||||||
|
<default applyOnUpdate="0" expression="" field="HT_code"/>
|
||||||
|
</defaults>
|
||||||
|
<constraints>
|
||||||
|
<constraint constraints="0" exp_strength="0" field="No" unique_strength="0" notnull_strength="0"/>
|
||||||
|
<constraint constraints="0" exp_strength="0" field="X" unique_strength="0" notnull_strength="0"/>
|
||||||
|
<constraint constraints="0" exp_strength="0" field="Y" unique_strength="0" notnull_strength="0"/>
|
||||||
|
<constraint constraints="0" exp_strength="0" field="LU2022" unique_strength="0" notnull_strength="0"/>
|
||||||
|
<constraint constraints="0" exp_strength="0" field="Hientrang" unique_strength="0" notnull_strength="0"/>
|
||||||
|
<constraint constraints="0" exp_strength="0" field="HT_code" unique_strength="0" notnull_strength="0"/>
|
||||||
|
</constraints>
|
||||||
|
<constraintExpressions>
|
||||||
|
<constraint exp="" desc="" field="No"/>
|
||||||
|
<constraint exp="" desc="" field="X"/>
|
||||||
|
<constraint exp="" desc="" field="Y"/>
|
||||||
|
<constraint exp="" desc="" field="LU2022"/>
|
||||||
|
<constraint exp="" desc="" field="Hientrang"/>
|
||||||
|
<constraint exp="" desc="" field="HT_code"/>
|
||||||
|
</constraintExpressions>
|
||||||
|
<expressionfields/>
|
||||||
|
<attributeactions>
|
||||||
|
<defaultAction key="Canvas" value="{00000000-0000-0000-0000-000000000000}"/>
|
||||||
|
</attributeactions>
|
||||||
|
<attributetableconfig sortOrder="0" actionWidgetStyle="dropDown" sortExpression=""HT_code"">
|
||||||
|
<columns>
|
||||||
|
<column width="-1" name="No" type="field" hidden="0"/>
|
||||||
|
<column width="-1" name="X" type="field" hidden="0"/>
|
||||||
|
<column width="-1" name="Y" type="field" hidden="0"/>
|
||||||
|
<column width="138" name="LU2022" type="field" hidden="0"/>
|
||||||
|
<column width="-1" name="Hientrang" type="field" hidden="0"/>
|
||||||
|
<column width="-1" name="HT_code" type="field" hidden="0"/>
|
||||||
|
<column width="-1" type="actions" hidden="1"/>
|
||||||
|
</columns>
|
||||||
|
</attributetableconfig>
|
||||||
|
<conditionalstyles>
|
||||||
|
<rowstyles/>
|
||||||
|
<fieldstyles/>
|
||||||
|
</conditionalstyles>
|
||||||
|
<storedexpressions/>
|
||||||
|
<editform tolerant="1"></editform>
|
||||||
|
<editforminit/>
|
||||||
|
<editforminitcodesource>0</editforminitcodesource>
|
||||||
|
<editforminitfilepath></editforminitfilepath>
|
||||||
|
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
QGIS forms can have a Python function that is called when the form is
|
||||||
|
opened.
|
||||||
|
|
||||||
|
Use this function to add extra logic to your forms.
|
||||||
|
|
||||||
|
Enter the name of the function in the "Python Init function"
|
||||||
|
field.
|
||||||
|
An example follows:
|
||||||
|
"""
|
||||||
|
from qgis.PyQt.QtWidgets import QWidget
|
||||||
|
|
||||||
|
def my_form_open(dialog, layer, feature):
|
||||||
|
geom = feature.geometry()
|
||||||
|
control = dialog.findChild(QWidget, "MyLineEdit")
|
||||||
|
]]></editforminitcode>
|
||||||
|
<featformsuppress>0</featformsuppress>
|
||||||
|
<editorlayout>generatedlayout</editorlayout>
|
||||||
|
<editable>
|
||||||
|
<field name="HT_code" editable="1"/>
|
||||||
|
<field name="Hientrang" editable="1"/>
|
||||||
|
<field name="LU2022" editable="1"/>
|
||||||
|
<field name="No" editable="1"/>
|
||||||
|
<field name="X" editable="1"/>
|
||||||
|
<field name="Y" editable="1"/>
|
||||||
|
</editable>
|
||||||
|
<labelOnTop>
|
||||||
|
<field name="HT_code" labelOnTop="0"/>
|
||||||
|
<field name="Hientrang" labelOnTop="0"/>
|
||||||
|
<field name="LU2022" labelOnTop="0"/>
|
||||||
|
<field name="No" labelOnTop="0"/>
|
||||||
|
<field name="X" labelOnTop="0"/>
|
||||||
|
<field name="Y" labelOnTop="0"/>
|
||||||
|
</labelOnTop>
|
||||||
|
<reuseLastValue>
|
||||||
|
<field reuseLastValue="0" name="HT_code"/>
|
||||||
|
<field reuseLastValue="0" name="Hientrang"/>
|
||||||
|
<field reuseLastValue="0" name="LU2022"/>
|
||||||
|
<field reuseLastValue="0" name="No"/>
|
||||||
|
<field reuseLastValue="0" name="X"/>
|
||||||
|
<field reuseLastValue="0" name="Y"/>
|
||||||
|
</reuseLastValue>
|
||||||
|
<dataDefinedFieldProperties/>
|
||||||
|
<widgets/>
|
||||||
|
<previewExpression>"No"</previewExpression>
|
||||||
|
<mapTip></mapTip>
|
||||||
|
<layerGeometryType>0</layerGeometryType>
|
||||||
|
</qgis>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PROJCS["WGS 84 / UTM zone 48N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32648"]]
|
||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user