Skip to content

Commit

Permalink
feat: minor updates for R004 (#43)
Browse files Browse the repository at this point in the history
* docs: bump `docutils`
* docs: use `importlib` rather than `pkg_resources`
* chore: bump version and add release notes
* refactor: bump default version to 004
  • Loading branch information
tsutterley authored Jul 31, 2024
1 parent c3a5faa commit dcbfdbe
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 154 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ _*.c
*.shp
*.shx
*.tif
*.vrt
*.zarr
# Logs and databases #
######################
Expand Down
105 changes: 51 additions & 54 deletions IS2view/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python
u"""
api.py
Written by Tyler Sutterley (04/2024)
Written by Tyler Sutterley (06/2024)
Plotting tools for visualizing rioxarray variables on leaflet maps
PYTHON DEPENDENCIES:
Expand All @@ -28,6 +28,7 @@
https://xyzservices.readthedocs.io/en/stable/
UPDATE HISTORY:
Updated 06/2024: use wrapper to importlib for optional dependencies
Updated 04/2024: add connections and functions for changing variables
and other attributes of the leaflet map visualization
simplify and generalize mapping between observables and functionals
Expand Down Expand Up @@ -56,20 +57,21 @@
import collections.abc
from traitlets import HasTraits, Float, Tuple, observe
from traitlets.utils.bunch import Bunch
from IS2view.utilities import import_dependency

# attempt imports
try:
import geopandas as gpd
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.debug("geopandas not available")
try:
import ipywidgets
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.debug("ipywidgets not available")
try:
import ipyleaflet
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.debug("ipyleaflet not available")
gpd = import_dependency('geopandas')
ipywidgets = import_dependency('ipywidgets')
ipyleaflet = import_dependency('ipyleaflet')
owslib = import_dependency('owslib')
owslib.wms = import_dependency('owslib.wms')
rio = import_dependency('rasterio')
rio.transform = import_dependency('rasterio.transform')
rio.warp = import_dependency('rasterio.warp')
xr = import_dependency('xarray')
xyzservices = import_dependency('xyzservices')

# attempt matplotlib imports
try:
import matplotlib
import matplotlib.cm as cm
Expand All @@ -81,23 +83,6 @@
matplotlib.rcParams['mathtext.default'] = 'regular'
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("matplotlib not available")
try:
import owslib.wms
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.debug("owslib not available")
try:
import rasterio.transform
import rasterio.warp
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("rasterio not available")
try:
import xarray as xr
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("xarray not available")
try:
import xyzservices
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.debug("xyzservices not available")

# set environmental variable for anonymous s3 access
os.environ['AWS_NO_SIGN_REQUEST'] = 'YES'
Expand Down Expand Up @@ -230,6 +215,8 @@ def _tile_provider(provider):

# create traitlets of basemap providers
basemaps = _load_dict(providers)
# set default map dimensions
_default_layout = ipywidgets.Layout(width='70%', height='600px')

# draw ipyleaflet map
class Leaflet:
Expand All @@ -241,6 +228,8 @@ class Leaflet:
``ipyleaflet.Map``
basemap : obj or NoneType
Basemap for the ``ipyleaflet.Map``
layout : obj, default ``ipywidgets.Layout(width='70%', height='600px')``
Layout for the ``ipyleaflet.Map``
attribution : bool, default False
Include layer attributes on leaflet map
scale_control : bool, default False
Expand Down Expand Up @@ -280,6 +269,7 @@ class Leaflet:
def __init__(self, projection, **kwargs):
# set default keyword arguments
kwargs.setdefault('map', None)
kwargs.setdefault('layout', _default_layout)
kwargs.setdefault('attribution', False)
kwargs.setdefault('full_screen_control', False)
kwargs.setdefault('scale_control', False)
Expand All @@ -300,7 +290,9 @@ def __init__(self, projection, **kwargs):
zoom=kwargs['zoom'], max_zoom=5,
attribution_control=kwargs['attribution'],
basemap=kwargs['basemap'],
crs=projections['EPSG:3413'])
crs=projections['EPSG:3413'],
layout=kwargs['layout']
)
self.crs = 'EPSG:3413'
elif (projection == 'South'):
kwargs.setdefault('basemap',
Expand All @@ -310,7 +302,9 @@ def __init__(self, projection, **kwargs):
zoom=kwargs['zoom'], max_zoom=5,
attribution_control=kwargs['attribution'],
basemap=kwargs['basemap'],
crs=projections['EPSG:3031'])
crs=projections['EPSG:3031'],
layout=kwargs['layout']
)
self.crs = 'EPSG:3031'
else:
# use a predefined ipyleaflet map
Expand Down Expand Up @@ -508,7 +502,7 @@ def plot_basemap(self, ax=None, **kwargs):
ax: obj, default None
Figure axis
kwargs: dict, default {}
Additional keyword arguments for ``wms.getmap``
Additional keyword arguments for ``owslib.wms.getmap``
"""
# set default keyword arguments
kwargs.setdefault('layers', ['BlueMarble_NextGeneration'])
Expand Down Expand Up @@ -777,12 +771,8 @@ def plot(self, m, **kwargs):
# reduce to variable and lag
self._variable = copy.copy(kwargs['variable'])
self.lag = int(kwargs['lag'])
if (self._ds[self._variable].ndim == 3) and ('time' in self._ds[self._variable].dims):
self._ds_selected = self._ds[self._variable].sel(time=self._ds.time[self.lag])
elif (self._ds[self._variable].ndim == 3) and ('band' in self._ds[self._variable].dims):
self._ds_selected = self._ds[self._variable].sel(band=1)
else:
self._ds_selected = self._ds[self._variable]
# select data variable
self.set_dataset()
# get the normalization bounds
self.get_norm_bounds(**kwargs)
# create matplotlib normalization
Expand Down Expand Up @@ -911,7 +901,7 @@ def get_bounds(self):
"""get the bounds of the leaflet map in geographical coordinates
"""
self.get_bbox()
lon, lat = rasterio.warp.transform(
lon, lat = rio.warp.transform(
self.crs['name'], 'EPSG:4326',
[self.sw['x'], self.ne['x']],
[self.sw['y'], self.ne['y']])
Expand Down Expand Up @@ -1000,7 +990,7 @@ def clip_image(self, ds):
# attempt to get the coordinate reference system of the dataset
self.get_crs()
# convert map bounds to coordinate reference system of image
minx, miny, maxx, maxy = rasterio.warp.transform_bounds(
minx, miny, maxx, maxy = rio.warp.transform_bounds(
self.crs['name'], self._ds.rio.crs,
self.sw['x'], self.sw['y'],
self.ne['x'], self.ne['y'])
Expand All @@ -1023,14 +1013,14 @@ def clip_image(self, ds):
# warp image to map bounds and resolution
# input and output affine transformations
src_transform = ds.rio.transform()
dst_transform = rasterio.transform.from_origin(minx, maxy,
dst_transform = rio.transform.from_origin(minx, maxy,
self.resolution, self.resolution)
# allocate for output warped image
dst_width = int((maxx - minx)//self.resolution)
dst_height = int((maxy - miny)//self.resolution)
dst_data = np.zeros((dst_height, dst_width), dtype=ds.dtype.type)
# warp image to output resolution
rasterio.warp.reproject(source=ds.values, destination=dst_data,
rio.warp.reproject(source=ds.values, destination=dst_data,
src_transform=src_transform,
src_crs=self._ds.rio.crs,
src_nodata=np.nan,
Expand Down Expand Up @@ -1148,21 +1138,28 @@ def set_observables(self, widget, **kwargs):
except (AttributeError, NameError, ValueError) as exc:
pass

def set_variable(self, sender):
"""update the dataframe variable for a new selected variable
def set_dataset(self):
"""Select the dataset for the selected variable
and time lag
"""
# only update variable if a new final
if isinstance(sender['new'], str):
self._variable = sender['new']
else:
return
# reduce to variable and lag
if (self._ds[self._variable].ndim == 3) and ('time' in self._ds[self._variable].dims):
self._ds_selected = self._ds[self._variable].sel(time=self._ds.time[self.lag])
elif (self._ds[self._variable].ndim == 3) and ('band' in self._ds[self._variable].dims):
self._ds_selected = self._ds[self._variable].sel(band=1)
else:
self._ds_selected = self._ds[self._variable]

def set_variable(self, sender):
"""update the plotted variable
"""
# only update variable if a new final
if isinstance(sender['new'], str):
self._variable = sender['new']
else:
return
# reduce to variable and lag
self.set_dataset()
# check if dynamic normalization is enabled
if self._dynamic:
self.get_norm_bounds()
Expand Down Expand Up @@ -1263,7 +1260,7 @@ def handle_click(self, **kwargs):
else:
self._ds.rio.set_crs(crs)
# get the clicked point in dataset coordinate reference system
x, y = rasterio.warp.transform('EPSG:4326', crs, [lon], [lat])
x, y = rio.warp.transform('EPSG:4326', crs, [lon], [lat])
# find nearest point in dataset
self._data = self._ds_selected.sel(x=x, y=y, method='nearest').values[0]
self._units = self._ds[self._variable].attrs['units']
Expand Down Expand Up @@ -1575,7 +1572,7 @@ def point(self, ax, **kwargs):
"""
# convert point to dataset coordinate reference system
lon, lat = self.geometry['coordinates']
x, y = rasterio.warp.transform(self.crs, self._ds.rio.crs, [lon], [lat])
x, y = rio.warp.transform(self.crs, self._ds.rio.crs, [lon], [lat])
# output time series for point
self._data = np.zeros_like(self._ds.time)
# reduce dataset to geometry
Expand Down Expand Up @@ -1629,7 +1626,7 @@ def transect(self, ax, **kwargs):
"""
# convert linestring to dataset coordinate reference system
lon, lat = np.transpose(self.geometry['coordinates'])
x, y = rasterio.warp.transform(self.crs, self._ds.rio.crs, lon, lat)
x, y = rio.warp.transform(self.crs, self._ds.rio.crs, lon, lat)
# get coordinates of each grid cell
gridx, gridy = np.meshgrid(self._ds.x, self._ds.y)
# clip ice area to geometry
Expand Down Expand Up @@ -1997,7 +1994,7 @@ def transect(self, ax, **kwargs):
"""
# convert linestring to dataset coordinate reference system
lon, lat = np.transpose(self.geometry['coordinates'])
x, y = rasterio.warp.transform(self.crs, self._ds.rio.crs, lon, lat)
x, y = rio.warp.transform(self.crs, self._ds.rio.crs, lon, lat)
# get coordinates of each grid cell
gridx, gridy = np.meshgrid(self._ds.x, self._ds.y)
# clip variable to geometry and create mask
Expand Down
14 changes: 5 additions & 9 deletions IS2view/convert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
convert.py
Written by Tyler Sutterley (08/2023)
Written by Tyler Sutterley (06/2024)
Utilities for converting gridded ICESat-2 files from native netCDF4
PYTHON DEPENDENCIES:
Expand All @@ -13,6 +13,7 @@
https://docs.xarray.dev/en/stable/
UPDATE HISTORY:
Updated 06/2024: use wrapper to importlib for optional dependencies
Updated 08/2023: use h5netcdf as the netCDF4 driver for xarray
Updated 07/2023: use logging instead of warnings for import attempts
Updated 06/2023: using pathlib to define and expand paths
Expand All @@ -23,16 +24,11 @@
import logging
import pathlib
import numpy as np
from IS2view.utilities import import_dependency

# attempt imports
try:
import h5netcdf
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("h5netcdf not available")
try:
import xarray as xr
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("xarray not available")
h5netcdf = import_dependency('h5netcdf')
xr = import_dependency('xarray')

# default groups to skip
_default_skip_groups = ('METADATA', 'orbit_info', 'quality_assessment',)
Expand Down
22 changes: 7 additions & 15 deletions IS2view/io.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
io.py
Written by Tyler Sutterley (10/2023)
Written by Tyler Sutterley (06/2024)
Utilities for reading gridded ICESat-2 files using rasterio and xarray
PYTHON DEPENDENCIES:
Expand All @@ -18,6 +18,7 @@
https://docs.xarray.dev/en/stable/
UPDATE HISTORY:
Updated 06/2024: use wrapper to importlib for optional dependencies
Updated 10/2023: use dask.delayed to read multiple files in parallel
Updated 08/2023: use xarray h5netcdf to read files streaming from s3
add open_dataset function for opening multiple granules
Expand All @@ -27,22 +28,13 @@
"""
from __future__ import annotations
import os
import logging
from IS2view.utilities import import_dependency

# attempt imports
try:
import rioxarray
import rioxarray.merge
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("rioxarray not available")
try:
import dask
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("dask not available")
try:
import xarray as xr
except (AttributeError, ImportError, ModuleNotFoundError) as exc:
logging.critical("xarray not available")
rioxarray = import_dependency('rioxarray')
rioxarray.merge = import_dependency('rioxarray.merge')
dask = import_dependency('dask')
xr = import_dependency('xarray')

# set environmental variable for anonymous s3 access
os.environ['AWS_NO_SIGN_REQUEST'] = 'YES'
Expand Down
Loading

0 comments on commit dcbfdbe

Please sign in to comment.