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