Source code for dea_tools.coastal

## dea_coastaltools.py
"""
Coastal and intertidal analysis tools.

License: The code in this notebook is licensed under the Apache License,
Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0). Digital Earth
Australia data is licensed under the Creative Commons by Attribution 4.0
license (https://creativecommons.org/licenses/by/4.0/).

Contact: If you need assistance, post a question on the Open Data Cube
Discord chat (https://discord.com/invite/4hhBQVas5U) or the GIS Stack Exchange
(https://gis.stackexchange.com/questions/ask?tags=open-data-cube) using
the `open-data-cube` tag (you can view previously asked questions here:
https://gis.stackexchange.com/questions/tagged/open-data-cube).

If you would like to report an issue with this script, you can file one
on GitHub (https://github.com/GeoscienceAustralia/dea-notebooks/issues/new).

Last modified: July 2024

"""

# Import required packages
import warnings

import geopandas as gpd
import numpy as np
import pandas as pd
from owslib.wfs import WebFeatureService
from pandas.plotting import register_matplotlib_converters
from shapely.geometry import box

register_matplotlib_converters()


WFS_ADDRESS = "https://geoserver.dea.ga.gov.au/geoserver/wfs"


[docs] def transect_distances(transects_gdf, lines_gdf, mode="distance"): """ Take a set of transects (e.g. shore-normal beach survey lines), and determine the distance along the transect to each object in a set of lines (e.g. shorelines). Distances are measured in the CRS of the input datasets. For coastal applications, transects should be drawn from land to water (with the first point being on land so that it can be used as a consistent location from which to measure distances. The distance calculation can be performed using two modes: - 'distance': Distances are measured from the start of the transect to where it intersects with each line. Any transect that intersects a line more than once is ignored. This mode is useful for measuring e.g. the distance to the shoreline over time from a consistent starting location. - 'width' Distances are measured between the first and last intersection between a transect and each line. Any transect that intersects a line only once is ignored. This is useful for e.g. measuring the width of a narrow area of coastline over time, e.g. the neck of a spit or tombolo. Parameters ---------- transects_gdf : geopandas.GeoDataFrame A GeoDataFrame containing one or multiple vector profile lines. The GeoDataFrame's index column will be used to name the rows in the output distance table. lines_gdf : geopandas.GeoDataFrame A GeoDataFrame containing one or multiple vector line features that intersect the profile lines supplied to `transects_gdf`. The GeoDataFrame's index column will be used to name the columns in the output distance table. mode : string, optional Whether to use 'distance' (for measuring distances from the start of a profile) or 'width' mode (for measuring the width between two profile intersections). See docstring above for more info; defaults to 'distance'. Returns ------- distance_df : pandas.DataFrame A DataFrame containing distance measurements for each profile line (rows) and line feature (columns). """ from shapely.errors import ShapelyDeprecationWarning from shapely.geometry import Point def _intersect_dist(transect_gdf, lines_gdf, mode=mode): """ Take an individual transect, and determine the distance along the transect to each object in a set of lines (e.g. shorelines). """ # Identify intersections between transects and lines intersect_points = lines_gdf.apply(lambda x: x.geometry.intersection(transect_gdf.geometry), axis=1) # In distance mode, identify transects with one intersection only, # and use this as the end point and the start of the transect as the # start point when measuring distances if mode == "distance": start_point = Point(transect_gdf.geometry.coords[0]) point_df = intersect_points.apply( lambda x: ( pd.Series({"start": start_point, "end": x}) if x.type == "Point" else pd.Series({"start": None, "end": None}) ) ) # In width mode, identify transects with multiple intersections, and # use the first intersection as the start point and the second # intersection for the end point when measuring distances if mode == "width": point_df = intersect_points.apply( lambda x: ( pd.Series({"start": x.geoms[0], "end": x.geoms[-1]}) if x.type == "MultiPoint" else pd.Series({"start": None, "end": None}) ) ) # Calculate distances between valid start and end points return point_df.apply(lambda x: x.start.distance(x.end) if x.start else None, axis=1) # Run code after ignoring Shapely pre-v2.0 warnings with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=ShapelyDeprecationWarning) # Assert that both datasets use the same CRS assert transects_gdf.crs == lines_gdf.crs, "Please ensure both input datasets use the same CRS." # Run distance calculations distance_df = transects_gdf.apply(lambda x: _intersect_dist(x, lines_gdf), axis=1) return pd.DataFrame(distance_df)
[docs] def get_coastlines(bbox: tuple, crs="EPSG:4326", layer="shorelines_annual", drop_wms=True) -> gpd.GeoDataFrame: """ Load DEA Coastlines annual shorelines or rates of change points data for a provided bounding box using WFS. For a full description of the DEA Coastlines dataset, refer to the official Geoscience Australia product description: /data/product/dea-coastlines Parameters ---------- bbox : (xmin, ymin, xmax, ymax), or geopandas object Bounding box expressed as a tutple. Alternatively, a bounding box can be automatically extracted by suppling a geopandas.GeoDataFrame or geopandas.GeoSeries. crs : str, optional Optional CRS for the bounding box. This is ignored if `bbox` is provided as a geopandas object. layer : str, optional Which DEA Coastlines layer to load. Options include the annual shoreline vectors ("shorelines_annual") and the rates of change points ("rates_of_change"). Defaults to "shorelines_annual". drop_wms : bool, optional Whether to drop WMS-specific attribute columns from the data. These columns are used for visualising the dataset on DEA Maps, and are unlikely to be useful for scientific analysis. Defaults to True. Returns ------- gpd.GeoDataFrame A GeoDataFrame containing shoreline or point features and associated metadata. """ # If bbox is a geopandas object, convert to bbox try: crs = str(bbox.crs) bbox = bbox.total_bounds except: pass # Query WFS wfs = WebFeatureService(url=WFS_ADDRESS, version="1.1.0") layer_name = f"dea:{layer}" response = wfs.getfeature( typename=layer_name, bbox=tuple(bbox) + (crs,), outputFormat="json", ) # Load data as a geopandas.GeoDataFrame coastlines_gdf = gpd.read_file(response) # Clip to extent of bounding box extent = gpd.GeoSeries(box(*bbox), crs=crs).to_crs(coastlines_gdf.crs) coastlines_gdf = coastlines_gdf.clip(extent) # Optionally drop WMS-specific columns if drop_wms: coastlines_gdf = coastlines_gdf.loc[:, ~coastlines_gdf.columns.str.contains("wms_")] return coastlines_gdf
[docs] def glint_angle(solar_azimuth, solar_zenith, view_azimuth, view_zenith): """ Calculates glint angles for each pixel in a satellite image based on the relationship between the solar and sensor zenith and azimuth viewing angles at the moment the image was acquired. Glint angle is considered a predictor of sunglint over water; small glint angles (e.g. < 20 degrees) are associated with a high probability of sunglint due to the viewing angle of the sensor being aligned with specular reflectance of the sun from the water's surface. Based on code from https://towardsdatascience.com/how-to-implement- sunglint-detection-for-sentinel-2-images-in-python-using-metadata- info-155e683d50 Parameters ---------- solar_azimuth : array-like Array of solar azimuth angles in degrees. In DEA Collection 3, this is contained in the "oa_solar_azimuth" band. solar_zenith : array-like Array of solar zenith angles in degrees. In DEA Collection 3, this is contained in the "oa_solar_zenith" band. view_azimuth : array-like Array of sensor/viewing azimuth angles in degrees. In DEA Collection 3, this is contained in the "oa_satellite_azimuth" band. view_zenith : array-like Array of sensor/viewing zenith angles in degrees. In DEA Collection 3, this is contained in the "oa_satellite_view" band. Returns ------- glint_array : numpy.ndarray Array of glint angles in degrees. Small values indicate higher probabilities of sunglint. """ # Convert angle arrays to radians solar_zenith_rad = np.deg2rad(solar_zenith) solar_azimuth_rad = np.deg2rad(solar_azimuth) view_zenith_rad = np.deg2rad(view_zenith) view_azimuth_rad = np.deg2rad(view_azimuth) # Calculate sunglint angle phi = solar_azimuth_rad - view_azimuth_rad glint_angle = np.cos(view_zenith_rad) * np.cos(solar_zenith_rad) - np.sin(view_zenith_rad) * np.sin( solar_zenith_rad ) * np.cos(phi) # Convert to degrees return np.degrees(np.arccos(glint_angle))
def model_tides(*args, **kwargs): raise ImportError( "The `model_tides` function has been removed and is no longer available in this package.\n" "Please install and use the `eo-tides` package instead:\n" "https://geoscienceaustralia.github.io/eo-tides/migration/" ) def pixel_tides(*args, **kwargs): raise ImportError( "The `pixel_tides` function has been removed and is no longer available in this package.\n" "Please install and use the `eo-tides` package instead:\n" "https://geoscienceaustralia.github.io/eo-tides/migration/" ) def tidal_tag(*args, **kwargs): raise ImportError( "The `tidal_tag` function has been removed and is no longer available in this package.\n" "Please install and use the `eo-tides` package instead:\n" "https://geoscienceaustralia.github.io/eo-tides/migration/" ) def tidal_stats(*args, **kwargs): raise ImportError( "The `tidal_stats` function has been removed and is no longer available in this package.\n" "Please install and use the `eo-tides` package instead:\n" "https://geoscienceaustralia.github.io/eo-tides/migration/" ) def tidal_tag_otps(*args, **kwargs): raise ImportError( "The `tidal_tag_otps` function has been removed and is no longer available in this package.\n" "Please install and use the `eo-tides` package instead:\n" "https://geoscienceaustralia.github.io/eo-tides/migration/" ) def tidal_stats_otps(*args, **kwargs): raise ImportError( "The `tidal_stats_otps` function has been removed and is no longer available in this package.\n" "Please install and use the `eo-tides` package instead:\n" "https://geoscienceaustralia.github.io/eo-tides/migration/" )