diff --git a/clients/python/sliderule/ipysliderule.py b/clients/python/sliderule/ipysliderule.py
index 2caab93f9..eb4809fa5 100644
--- a/clients/python/sliderule/ipysliderule.py
+++ b/clients/python/sliderule/ipysliderule.py
@@ -78,6 +78,9 @@ def __init__(self, **kwargs):
kwargs.setdefault('style', {})
# set style
self.style = copy.copy(kwargs['style'])
+ # pass through some ipywidgets objects
+ self.HBox = ipywidgets.HBox
+ self.VBox = ipywidgets.VBox
# dropdown menu for setting asset
self.asset = ipywidgets.Dropdown(
@@ -91,7 +94,7 @@ def __init__(self, **kwargs):
# dropdown menu for ICESat-2 product
self.product = ipywidgets.Dropdown(
- options=['ATL03','ATL06','ATL08'],
+ options=['ATL03', 'ATL06', 'ATL08'],
value='ATL03',
description='Product:',
description_tooltip=("Product: ICESat-2 data product "
@@ -127,7 +130,7 @@ def __init__(self, **kwargs):
)
# multiple select for photon classification
- class_options = ['atl03','quality','atl08','yapc']
+ class_options = ['atl03', 'quality', 'atl08', 'yapc']
self.classification = ipywidgets.SelectMultiple(
options=class_options,
value=['atl03','atl08'],
@@ -316,6 +319,69 @@ def __init__(self, **kwargs):
)
self.yapc_weight.layout.display = 'none'
+ # ATL08 PhoREAL parameters
+ # slider for setting PhoREAL histogram bin size
+ self.phoreal_binsize = ipywidgets.FloatSlider(
+ value=1,
+ min=0.25,
+ max=10,
+ step=0.25,
+ description='PhoREAL Bin Size:',
+ description_tooltip="PhoREAL Bin Size: size of the vertical photon bin in meters",
+ disabled=False,
+ continuous_update=False,
+ orientation='horizontal',
+ readout=True,
+ readout_format='0.2f',
+ style=self.style,
+ )
+
+ # dropdown menu for setting PhoREAL geolocation algorithm
+ # mean - takes the average value across all photons in the segment
+ # median - takes the median value across all photons in the segment
+ # center - takes the halfway value calculated by the average of the first and last photon in the segment
+ phoreal_geolocation_list = ['mean','median','center']
+ self.phoreal_geolocation = ipywidgets.Dropdown(
+ options=phoreal_geolocation_list,
+ value='center',
+ description='PhoREAL Geolocation:',
+ description_tooltip=("PhoREAL Geolocation: method for calculating segment geolocation variables\n\t"
+ "mean: average value across all photons in the segment\n\t"
+ "median: median value across all photons in the segment\n\t"
+ "center: center of first and last photons in the segment"),
+ disabled=False,
+ style=self.style,
+ )
+
+ # checkbox for using PhoREAL absolute elevation
+ self.phoreal_abs_h = ipywidgets.Checkbox(
+ value=False,
+ description='PhoREAL use abs h',
+ description_tooltip=("PhoREAL use abs h: use absolute photon heights "
+ "instead of the normalized heights"),
+ disabled=False,
+ style=self.style,
+ )
+
+ # checkbox for using the PhoREAL ABoVE classifier
+ self.phoreal_above = ipywidgets.Checkbox(
+ value=False,
+ description='PhoREAL use ABoVE',
+ description_tooltip="PhoREAL use ABoVE: use the ABoVE photon classifier",
+ disabled=False,
+ style=self.style,
+ )
+
+ # checkbox for sending PhoREAL waveform
+ self.phoreal_waveform = ipywidgets.Checkbox(
+ value=False,
+ description='PhoREAL waveform',
+ description_tooltip=("PhoREAL waveform: send the photon height "
+ "histograms in addition to the vegetation statistics"),
+ disabled=False,
+ style=self.style,
+ )
+
# slider for setting length of ATL06-SR segment in meters
self.length = ipywidgets.IntSlider(
value=40,
@@ -446,7 +512,7 @@ def __init__(self, **kwargs):
description='Projection:',
description_tooltip=("Projection: leaflet map projection\n\t"
"Global: Web Mercator (EPSG:3857)\n\t"
- "Alaska Polar Stereographic (EPSG:5936)\n\t"
+ "North: Alaska Polar Stereographic (EPSG:5936)\n\t"
"South: Polar Stereographic South (EPSG:3031)"),
disabled=False,
style=self.style,
@@ -676,10 +742,11 @@ def set_atl03_defaults(self):
"""sets the default widget parameters for ATL03 requests
"""
# default photon classifications
- class_options = ['atl03','atl08','yapc']
- self.classification.value = class_options
+ class_options = ['atl03', 'quality', 'atl08', 'yapc']
+ self.classification.options = class_options
+ self.classification.value = ['atl03', 'atl08', 'yapc']
# default ATL03 confidence
- self.confidence.value = -2
+ self.confidence.value = -1
# set land class options
land_options = [
'atl08_noise',
@@ -705,8 +772,9 @@ def set_atl06_defaults(self):
"""sets the default widget parameters for ATL06 requests
"""
# default photon classifications
- class_options = ['atl03','atl08']
- self.classification.value = class_options
+ class_options = ['atl03', 'quality', 'atl08', 'yapc']
+ self.classification.options = class_options
+ self.classification.value = ['atl03', 'atl08']
# default ATL06-SR confidence
self.confidence.value = 4
# set land class options
@@ -723,6 +791,124 @@ def set_atl06_defaults(self):
self.file = copy.copy(self.atl06_filename)
self.savelabel.value = self.file
+ def set_atl08_defaults(self):
+ """sets the default widget parameters for ATL08 requests
+ """
+ # default photon classifications
+ class_options = ['atl03', 'quality', 'atl08']
+ self.classification.options = class_options
+ self.classification.value = ['atl08']
+ # default ATL08-SR confidence
+ self.confidence.value = -1
+ # set land class options
+ land_options = [
+ 'atl08_ground',
+ 'atl08_canopy',
+ 'atl08_top_of_canopy',
+ ]
+ self.land_class.value = land_options
+ # set default ATL08-SR length
+ self.length.value = 30
+ # set PhoREAL parameters
+ self.phoreal_binsize.value = 1.0
+ self.phoreal_geolocation.value = 'center'
+ self.phoreal_abs_h.value = False
+ self.phoreal_above.value = False
+ self.phoreal_waveform.value = False
+ # update variable list for ATL08-SR variables
+ variable_list = ['h_canopy', 'h_min_canopy', 'h_mean_canopy',
+ 'h_max_canopy', 'canopy_openness', 'h_te_median',
+ 'landcover', 'snowcover', 'solar_elevation', 'cycle', 'rgt']
+ self.variable.options = variable_list
+ self.variable.value = 'h_canopy'
+ # set default filename
+ self.file = copy.copy(self.atl08_filename)
+ self.savelabel.value = self.file
+
+ def atl03(self, **kwargs):
+ """returns a list of widgets for SlideRule ATL03 requests
+ """
+ kwargs.setdefault('display', 'advanced')
+ assert str(kwargs['display']).lower() in ['advanced','basic']
+ if (str(kwargs['display']).lower() == 'basic'):
+ return [
+ self.start_date,
+ self.end_date,
+ self.surface_type,
+ ]
+ else:
+ return [
+ self.start_date,
+ self.end_date,
+ self.classification,
+ self.surface_type,
+ self.confidence,
+ self.quality,
+ self.land_class,
+ self.yapc_knn,
+ self.yapc_win_h,
+ self.yapc_win_x,
+ self.yapc_min_ph,
+ ]
+
+ def atl06(self, **kwargs):
+ """returns a list of widgets for SlideRule ATL06 requests
+ """
+ kwargs.setdefault('display', 'advanced')
+ assert str(kwargs['display']).lower() in ['advanced','basic']
+ if (str(kwargs['display']).lower() == 'basic'):
+ return [
+ self.surface_type,
+ self.length,
+ ]
+ else:
+ return [
+ self.classification,
+ self.surface_type,
+ self.confidence,
+ self.quality,
+ self.land_class,
+ self.yapc_knn,
+ self.yapc_win_h,
+ self.yapc_win_x,
+ self.yapc_min_ph,
+ self.yapc_weight,
+ self.length,
+ self.step,
+ self.iteration,
+ self.spread,
+ self.count,
+ self.window,
+ self.sigma,
+ ]
+
+ def atl08(self, **kwargs):
+ """returns a list of widgets for SlideRule ATL08 requests
+ """
+ kwargs.setdefault('display', 'advanced')
+ assert str(kwargs['display']).lower() in ['advanced','basic']
+ if (str(kwargs['display']).lower() == 'basic'):
+ return [
+ self.surface_type,
+ self.length,
+ self.phoreal_binsize,
+ ]
+ else:
+ return [
+ self.classification,
+ self.surface_type,
+ self.confidence,
+ self.quality,
+ self.land_class,
+ self.phoreal_binsize,
+ self.phoreal_geolocation,
+ self.phoreal_abs_h,
+ self.phoreal_above,
+ self.phoreal_waveform,
+ self.length,
+ self.step,
+ ]
+
@property
def time_start(self):
"""start time in ISO format
@@ -856,7 +1042,7 @@ def set_loadfile(self, sender):
@property
def atl03_filename(self):
- """default input and output file string
+ """default input and output file string for ATL03 requests
"""
# get sliderule submission time
now = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
@@ -865,13 +1051,22 @@ def atl03_filename(self):
@property
def atl06_filename(self):
- """default input and output file string
+ """default input and output file string for ATL06 requests
"""
# get sliderule submission time
now = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
args = (now, self.release.value)
return "ATL06-SR_{0}_{1}.h5".format(*args)
+ @property
+ def atl08_filename(self):
+ """default input and output file string for ATL08 requests
+ """
+ # get sliderule submission time
+ now = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
+ args = (now, self.release.value)
+ return "ATL08-SR_{0}_{1}.h5".format(*args)
+
@property
def format(self):
"""return the file format from file string
@@ -1014,6 +1209,46 @@ def build_atl06(self, **parms):
# return the parameter dictionary
return parms
+ # build sliderule ATL08 parameters using latest values from widget
+ def build_atl08(self, **parms):
+ """Build a SlideRule parameters dictionary for making ATL08 requests
+
+ Parameters
+ ----------
+ parms : dict, dictionary of SlideRule parameters to update
+ """
+ # default parameters for all cases
+ # still return photon segments that fail checks
+ parms["pass_invalid"] = True
+ # length of ATL06-SR segment in meters
+ parms["len"] = self.length.value
+ # step distance for successive ATL06-SR segments in meters
+ parms["res"] = self.step.value
+ # PhoREAL parameters
+ parms["phoreal"] = {}
+ parms["phoreal"]["binsize"] = self.phoreal_binsize.value
+ parms["phoreal"]["geoloc"] = self.phoreal_geolocation.value
+ parms["phoreal"]["use_abs_h"] = self.phoreal_abs_h.value
+ parms["phoreal"]["send_waveform"] = self.phoreal_waveform.value
+ parms["phoreal"]["above_classifier"] = self.phoreal_above.value
+ # photon classification
+ # atl03 photon confidence level
+ if ('atl03' in self.classification.value):
+ # surface type: 0-land, 1-ocean, 2-sea ice, 3-land ice, 4-inland water
+ parms["srt"] = self.surface_type.index
+ # confidence level for PE selection
+ parms["cnf"] = self.confidence.value
+ # atl03 photon quality flags
+ if ('quality' in self.classification.value):
+ # confidence level for PE selection
+ parms["quality_ph"] = list(self.quality.value)
+ # atl08 land classification flags
+ if ('atl08' in self.classification.value):
+ # ATL08 land surface classifications
+ parms["atl08_class"] = list(self.land_class.value)
+ # return the parameter dictionary
+ return parms
+
# update values from widget using sliderule parameters dictionary
def set_values(self, parms):
"""Set widget values using a SlideRule parameters dictionary
@@ -1071,9 +1306,26 @@ def set_values(self, parms):
self.yapc_win_h.value = parms["yapc"]["win_h"]
if ('yapc' in parms.keys()) and ('win_x' in parms['yapc'].keys()):
self.yapc_win_x.value = parms["yapc"]["win_x"]
+ # PhoREAL parameters
+ if ('phoreal' in parms.keys()) and ('binsize' in parms['phoreal'].keys()):
+ self.phoreal_binsize.value = parms["phoreal"]["binsize"]
+ if ('phoreal' in parms.keys()) and ('geoloc' in parms['phoreal'].keys()):
+ self.phoreal_geolocation.value = parms["phoreal"]["geoloc"]
+ if ('phoreal' in parms.keys()) and ('use_abs_h' in parms['phoreal'].keys()):
+ self.phoreal_abs_h.value = parms["yapc"]["use_abs_h"]
+ if ('phoreal' in parms.keys()) and ('send_waveform' in parms['phoreal'].keys()):
+ self.phoreal_waveform.value = parms["phoreal"]["send_waveform"]
+ if ('phoreal' in parms.keys()) and ('above_classifier' in parms['phoreal'].keys()):
+ self.phoreal_above.value = parms["phoreal"]["above_classifier"]
# update values
return self
+ @property
+ def column_name(self):
+ """Column name from variable
+ """
+ return self.variable.value
+
@property
def RGT(self):
"""extract and verify Reference Ground Tracks (RGTs)
@@ -1135,6 +1387,13 @@ def orbital_cycle(self):
logging.critical(f"Cycle {self.cycle.value} is outside available range")
return "0"
+ @property
+ def plot_kwargs(self):
+ """return the plot keywords
+ """
+ return dict(column_name=self.column_name, cycle=self.orbital_cycle,
+ RGT=self.RGT, GT=self.GT, LR=self.LR, PT=self.PT)
+
def plot(self, gdf=None, **kwargs):
"""Creates plots of SlideRule outputs
@@ -1632,25 +1891,29 @@ class leaflet:
def __init__(self, projection, **kwargs):
# set default keyword arguments
kwargs.setdefault('map',None)
- kwargs.setdefault('prefer_canvas',False)
- kwargs.setdefault('attribution',False)
- kwargs.setdefault('zoom_control',False)
- kwargs.setdefault('scale_control',False)
- kwargs.setdefault('cursor_control',True)
- kwargs.setdefault('layer_control',True)
- kwargs.setdefault('center',(39,-108))
- kwargs.setdefault('color','green')
+ kwargs.setdefault('prefer_canvas', False)
+ kwargs.setdefault('attribution', False)
+ kwargs.setdefault('zoom_control', False)
+ kwargs.setdefault('scale_control', False)
+ kwargs.setdefault('full_screen_control', False)
+ kwargs.setdefault('cursor_control', True)
+ kwargs.setdefault('layer_control', True)
+ kwargs.setdefault('color', 'green')
# create basemap in projection
if (projection == 'Global'):
+ kwargs.setdefault('center', (39,-108))
+ kwargs.setdefault('zoom', 9)
self.map = ipyleaflet.Map(center=kwargs['center'],
- zoom=9, max_zoom=15, world_copy_jump=True,
+ zoom=kwargs['zoom'], max_zoom=15, world_copy_jump=True,
prefer_canvas=kwargs['prefer_canvas'],
attribution_control=kwargs['attribution'],
basemap=ipyleaflet.basemaps.Esri.WorldTopoMap)
self.crs = 'EPSG:3857'
elif (projection == 'North'):
- self.map = ipyleaflet.Map(center=(90,0),
- zoom=5, max_zoom=24,
+ kwargs.setdefault('center', (90,0))
+ kwargs.setdefault('zoom', 5)
+ self.map = ipyleaflet.Map(center=kwargs['center'],
+ zoom=kwargs['zoom'], max_zoom=24,
prefer_canvas=kwargs['prefer_canvas'],
attribution_control=kwargs['attribution'],
basemap=basemaps.Esri.ArcticOceanBase,
@@ -1660,8 +1923,10 @@ def __init__(self, projection, **kwargs):
self.map.add(ipyleaflet.basemap_to_tiles(reference))
self.crs = 'EPSG:5936'
elif (projection == 'South'):
- self.map = ipyleaflet.Map(center=(-90,0),
- zoom=2, max_zoom=9,
+ kwargs.setdefault('center', (-90,0))
+ kwargs.setdefault('zoom', 2)
+ self.map = ipyleaflet.Map(center=kwargs['center'],
+ zoom=kwargs['zoom'], max_zoom=9,
prefer_canvas=kwargs['prefer_canvas'],
attribution_control=kwargs['attribution'],
basemap=basemaps.Esri.AntarcticBasemap,
@@ -1669,8 +1934,13 @@ def __init__(self, projection, **kwargs):
self.crs = 'EPSG:3031'
else:
# use a predefined ipyleaflet map
+ assert kwargs['map'], 'Leaflet map needs to be defined'
self.map = kwargs['map']
self.crs = self.map.crs['name']
+ # add control for full screen
+ if kwargs['full_screen_control']:
+ self.full_screen_control = ipyleaflet.FullScreenControl()
+ self.map.add(self.full_screen_control)
# add control for layers
if kwargs['layer_control']:
self.layer_control = ipyleaflet.LayersControl(position='topleft')
@@ -1715,8 +1985,9 @@ def __init__(self, projection, **kwargs):
self.colorbar = None
# initialize hover control
self.hover_control = None
- # initialize selected feature
+ # initialize feature callbacks
self.selected_callback = None
+ self.region_callback = None
# add sliderule regions to map
def add_region(self, regions, **kwargs):
@@ -1895,6 +2166,8 @@ def handle_draw(self, obj, action, geo_json):
self.regions.append(region)
elif (action == 'deleted'):
self.regions.remove(region)
+ if self.region_callback is not None:
+ self.region_callback(action)
# remove any prior instances of a data layer
if (action == 'deleted') and self.geojson is not None:
self.map.remove(self.geojson)
@@ -1940,6 +2213,8 @@ def GeoData(self, gdf, **kwargs):
kwargs.setdefault('fields', self.default_atl06_fields())
kwargs.setdefault('colorbar', True)
kwargs.setdefault('position', 'topright')
+ # add warning that function is deprecated
+ logging.critical(f"Deprecated. Will be removed in a future release")
# remove any prior instances of a data layer
if self.geojson is not None:
self.map.remove(self.geojson)
@@ -2049,6 +2324,11 @@ def add_selected_callback(self, callback):
"""
self.selected_callback = callback
+ def add_region_callback(self, callback):
+ """set callback for handling region actions
+ """
+ self.region_callback = callback
+
# add colorbar widget to leaflet map
def add_colorbar(self, **kwargs):
"""Creates colorbars on leaflet maps
@@ -2110,3 +2390,485 @@ def default_atl06_fields():
"""
return ['cycle', 'dh_fit_dx', 'gt', 'h_mean',
'h_sigma', 'rgt', 'rms_misfit', 'w_surface_window_final']
+
+ @staticmethod
+ def default_atl08_fields():
+ """List of ATL08-SR tooltip fields
+ """
+ return ['canopy_openness', 'cycle', 'gt', 'h_canopy',
+ 'h_min_canopy', 'h_mean_canopy',
+ 'h_max_canopy', 'h_te_median', 'rgt']
+
+ @staticmethod
+ def default_mosaic_fields(**kwargs):
+ kwargs.setdefault('with_flags', False)
+ kwargs.setdefault('zonal_stats', False)
+ """List of mosaic tooltip fields
+ """
+ columns = ['time','value']
+ if kwargs['with_flags']:
+ columns += ['flags']
+ if kwargs['zonal_stats']:
+ columns += ['count','min','max','mean','median','stdev','mad']
+ return [f'mosaic.{c}' for c in columns]
+
+@gpd.pd.api.extensions.register_dataframe_accessor("leaflet")
+class LeafletMap:
+ """A geopandas GeoDataFrame extension for interactive map plotting,
+ based on ipyleaflet
+ """
+
+ def __init__(self, gdf):
+ # initialize map
+ self.map = None
+ self.crs = None
+ # initialize geodataframe
+ self._gdf = gdf
+ # initialize data and colorbars
+ self.geojson = None
+ self.tooltip = None
+ self.tooltip_width = None
+ self.tooltip_height = None
+ self.fields = []
+ self.colorbar = None
+ # initialize hover control
+ self.hover_control = None
+ # initialize selected feature
+ self.selected_callback = None
+
+ # add geodataframe data to leaflet map
+ def GeoData(self, m, **kwargs):
+ """Creates scatter plots of GeoDataFrames on leaflet maps
+
+ Parameters
+ ----------
+ m : obj, leaflet object
+ column_name : str, GeoDataFrame column to plot
+ cmap : str, matplotlib colormap
+ vmin : float, minimum value for normalization
+ vmax : float, maximum value for normalization
+ norm : obj, matplotlib color normalization object
+ radius : float, radius of scatter plot markers
+ fillOpacity : float, opacity of scatter plot markers
+ weight : float, weight of scatter plot markers
+ stride : int, number between successive array elements
+ max_plot_points : int, total number of plot markers to render
+ tooltip : bool, show hover tooltips
+ fields : list, GeoDataFrame fields to show in hover tooltips
+ colorbar : bool, show colorbar for rendered variable
+ position : str, position of colorbar on leaflet map
+ """
+ kwargs.setdefault('column_name', 'h_mean')
+ kwargs.setdefault('cmap', 'viridis')
+ kwargs.setdefault('vmin', None)
+ kwargs.setdefault('vmax', None)
+ kwargs.setdefault('norm', None)
+ kwargs.setdefault('radius', 1.0)
+ kwargs.setdefault('fillOpacity', 0.5)
+ kwargs.setdefault('weight', 3.0)
+ kwargs.setdefault('stride', None)
+ kwargs.setdefault('max_plot_points', 10000)
+ kwargs.setdefault('tooltip', True)
+ kwargs.setdefault('tooltip_height', "300px")
+ kwargs.setdefault('tooltip_width', "220px")
+ kwargs.setdefault('fields', [])
+ kwargs.setdefault('colorbar', True)
+ kwargs.setdefault('position', 'topright')
+ # set map and map coordinate reference system
+ self.map = m
+ self.crs = m.crs['name']
+ # remove any prior instances of a data layer
+ if self.geojson is not None:
+ self.map.remove(self.geojson)
+ if kwargs['stride'] is not None:
+ stride = np.copy(kwargs['stride'])
+ elif (self._gdf.shape[0] > kwargs['max_plot_points']):
+ stride = int(self._gdf.shape[0]//kwargs['max_plot_points'])
+ else:
+ stride = 1
+ # sliced geodataframe for plotting
+ geodataframe = self._gdf[slice(None,None,stride)]
+ self.column_name = copy.copy(kwargs['column_name'])
+ geodataframe['data'] = geodataframe[self.column_name]
+ # set colorbar limits to 2-98 percentile
+ # if not using a defined plot range
+ clim = geodataframe['data'].quantile((0.02, 0.98)).values
+ if kwargs['vmin'] is None:
+ vmin = clim[0]
+ else:
+ vmin = np.copy(kwargs['vmin'])
+ if kwargs['vmax'] is None:
+ vmax = clim[-1]
+ else:
+ vmax = np.copy(kwargs['vmax'])
+ # create matplotlib normalization
+ if kwargs['norm'] is None:
+ norm = colors.Normalize(vmin=vmin, vmax=vmax, clip=True)
+ else:
+ norm = copy.copy(kwargs['norm'])
+ # normalize data to be within vmin and vmax
+ normalized = norm(geodataframe['data'])
+ # create HEX colors for each point in the dataframe
+ geodataframe["color"] = np.apply_along_axis(colors.to_hex, 1,
+ cm.get_cmap(kwargs['cmap'], 256)(normalized))
+ # leaflet map point style
+ point_style = {key:kwargs[key] for key in ['radius','fillOpacity','weight']}
+ # convert to GeoJSON object
+ self.geojson = ipyleaflet.GeoJSON(data=geodataframe.__geo_interface__,
+ point_style=point_style, style_callback=self.style_callback)
+ # add GeoJSON object to map
+ self.map.add(self.geojson)
+ # fields for tooltip views
+ if kwargs['fields'] is None:
+ self.fields = geodataframe.columns.drop(
+ [geodataframe.geometry.name, "data", "color"])
+ else:
+ self.fields = copy.copy(kwargs['fields'])
+ # add hover tooltips
+ if kwargs['tooltip']:
+ self.tooltip = ipywidgets.HTML()
+ self.tooltip.layout.margin = "0px 20px 20px 20px"
+ self.tooltip.layout.visibility = 'hidden'
+ self.tooltip_height = kwargs['tooltip_height']
+ self.tooltip_width = kwargs['tooltip_width']
+ # create widget for hover tooltips
+ self.hover_control = ipyleaflet.WidgetControl(
+ widget=self.tooltip,
+ position='bottomright')
+ self.geojson.on_hover(self.handle_hover)
+ self.geojson.on_msg(self.handle_mouseout)
+ self.geojson.on_click(self.handle_click)
+ # add colorbar
+ if kwargs['colorbar']:
+ self.add_colorbar(column_name=self.column_name,
+ cmap=kwargs['cmap'], norm=norm,
+ position=kwargs['position'])
+
+ # functional call for setting colors of each point
+ def style_callback(self, feature):
+ """callback for setting marker colors
+ """
+ return {
+ "fillColor": feature["properties"]["color"],
+ "color": feature["properties"]["color"],
+ }
+
+ # functional calls for hover events
+ def handle_hover(self, feature, **kwargs):
+ """callback for creating hover tooltips
+ """
+ # combine html strings for hover tooltip
+ self.tooltip.value = '{0}: {1}
'.format('id',feature['id'])
+ self.tooltip.value += '
'.join(['{0}: {1}'.format(field,
+ feature["properties"][field]) for field in self.fields])
+ self.tooltip.layout.width = self.tooltip_width
+ self.tooltip.layout.height = self.tooltip_height
+ self.tooltip.layout.visibility = 'visible'
+ self.map.add(self.hover_control)
+
+ def handle_mouseout(self, _, content, buffers):
+ """callback for removing hover tooltips upon mouseout
+ """
+ event_type = content.get('type', '')
+ if event_type == 'mouseout':
+ self.tooltip.value = ''
+ self.tooltip.layout.width = "0px"
+ self.tooltip.layout.height = "0px"
+ self.tooltip.layout.visibility = 'hidden'
+ self.map.remove(self.hover_control)
+
+ # functional calls for click events
+ def handle_click(self, feature, **kwargs):
+ """callback for handling mouse clicks
+ """
+ if self.selected_callback != None:
+ self.selected_callback(feature)
+
+ def add_selected_callback(self, callback):
+ """set callback for handling mouse clicks
+ """
+ self.selected_callback = callback
+
+ def handle_region(self, action, **kwargs):
+ """callback for handling region deletions
+ """
+ # remove any prior instances of a data layer
+ if (action == 'deleted') and self.geojson is not None:
+ self.remove(self.geojson)
+ self.geojson = None
+ # remove any prior instances of a colorbar
+ if (action == 'deleted') and self.colorbar is not None:
+ self.remove(self.colorbar)
+ self.colorbar = None
+
+ # remove map layers
+ def remove(self, layer):
+ """wrapper function for removing layers from leaflet maps
+ """
+ # try to remove layer from map
+ try:
+ self.map.remove(layer)
+ except ipyleaflet.LayerException as e:
+ logging.info(f"Layer {layer} already removed from map")
+ pass
+ except ipyleaflet.ControlException as e:
+ logging.info(f"Control {layer} already removed from map")
+ pass
+ except Exception as e:
+ logging.critical(f"Could not remove layer {layer}")
+ logging.error(traceback.format_exc())
+ pass
+
+ # add colorbar widget to leaflet map
+ def add_colorbar(self, **kwargs):
+ """Creates colorbars on leaflet maps
+
+ Parameters
+ ----------
+ column_name : str, GeoDataFrame column to plot
+ cmap : str, matplotlib colormap
+ norm : obj, matplotlib color normalization object
+ alpha : float, opacity of colormap
+ orientation : str, orientation of colorbar
+ position : str, position of colorbar on leaflet map
+ width : float, width of colorbar
+ height : float, height of colorbar
+ """
+ kwargs.setdefault('column_name', 'h_mean')
+ kwargs.setdefault('cmap', 'viridis')
+ kwargs.setdefault('norm', None)
+ kwargs.setdefault('alpha', 1.0)
+ kwargs.setdefault('orientation', 'horizontal')
+ kwargs.setdefault('position', 'topright')
+ kwargs.setdefault('width', 6.0)
+ kwargs.setdefault('height', 0.4)
+ # remove any prior instances of a colorbar
+ if self.colorbar is not None:
+ self.map.remove(self.colorbar)
+ # colormap for colorbar
+ cmap = cm.get_cmap(kwargs['cmap'])
+ # create matplotlib colorbar
+ _, ax = plt.subplots(figsize=(kwargs['width'], kwargs['height']))
+ cbar = matplotlib.colorbar.ColorbarBase(ax, cmap=cmap,
+ norm=kwargs['norm'], alpha=kwargs['alpha'],
+ orientation=kwargs['orientation'],
+ label=kwargs['column_name'])
+ cbar.solids.set_rasterized(True)
+ cbar.ax.tick_params(which='both', width=1, direction='in')
+ # save colorbar to in-memory png object
+ png = io.BytesIO()
+ plt.savefig(png, bbox_inches='tight', format='png')
+ png.seek(0)
+ # create output widget
+ output = ipywidgets.Image(value=png.getvalue(), format='png')
+ self.colorbar = ipyleaflet.WidgetControl(widget=output,
+ transparent_bg=True, position=kwargs['position'])
+ # add colorbar
+ self.map.add(self.colorbar)
+ plt.close()
+
+@gpd.pd.api.extensions.register_dataframe_accessor("icesat2")
+class ICESat2:
+ """A geopandas GeoDataFrame extension for plotting ICESat-2 transects
+ """
+
+ def __init__(self, gdf):
+ # initialize geodataframe
+ self._gdf = gdf
+ # initialize data for time series plot
+ self._data = None
+ self._dist = None
+ self._units = None
+ self._longname = None
+ self._line = None
+
+ def plot(self, **kwargs):
+ """Creates plots of SlideRule outputs
+
+ Parameters
+ ----------
+ ax : obj, matplotlib axes object
+ kind : str, kind of plot to produce
+
+ - 'scatter' : scatter plot of along-track heights
+ - 'cycles' : time series plot for each orbital cycle
+ cmap : str, matplotlib colormap
+ title: str, title to use for the plot
+ legend: bool, title to use for the plot
+ legend_label: str, legend label type for 'cycles' plot
+ legend_frameon: bool, use a background patch for legend
+ column_name: str, GeoDataFrame column for 'cycles' plot
+ atl03: obj, ATL03 GeoDataFrame for 'scatter' plot
+ classification: str, ATL03 photon classification for scatter plot
+
+ - 'atl03' : ATL03 photon confidence
+ - 'atl08' : ATL08 photon-level land classification
+ - 'yapc' : Yet Another Photon Classification photon-density
+ - 'none' : no classification of photons
+ cycle_start: int, beginning cycle for 'cycles' plot
+ """
+ # default keyword arguments
+ kwargs.setdefault('ax', None)
+ kwargs.setdefault('kind', 'cycles')
+ kwargs.setdefault('cmap', 'viridis')
+ kwargs.setdefault('title', None)
+ kwargs.setdefault('legend', False)
+ kwargs.setdefault('legend_label','date')
+ kwargs.setdefault('legend_frameon',True)
+ kwargs.setdefault('column_name', 'h_mean')
+ kwargs.setdefault('atl03', None)
+ kwargs.setdefault('classification', None)
+ kwargs.setdefault('segments', True)
+ kwargs.setdefault('cycle_start', 3)
+ kwargs.setdefault('cycle', 0)
+ kwargs.setdefault('RGT', 0)
+ kwargs.setdefault('GT', 0)
+ # variable to plot
+ column = kwargs['column_name']
+ RGT = int(kwargs['RGT'])
+ GT = int(kwargs['GT'])
+ # skip plot creation if no values are entered
+ if (RGT == 0) or (GT == 0):
+ return
+ # create figure axis
+ if kwargs['ax'] is None:
+ fig,ax = plt.subplots(num=1, figsize=(8,6))
+ fig.set_facecolor('white')
+ fig.canvas.header_visible = False
+ else:
+ ax = kwargs['ax']
+ # list of legend elements
+ legend_elements = []
+ # different plot types
+ # cycles: along-track plot showing all available cycles
+ # scatter: plot showing a single cycle possibly with ATL03
+ if (kwargs['kind'] == 'cycles'):
+ # for each unique cycles
+ for cycle in self._gdf['cycle'].unique():
+ # skip cycles with significant off pointing
+ if (cycle < kwargs['cycle_start']):
+ continue
+ # reduce data frame to RGT, ground track and cycle
+ geodataframe = self._gdf[
+ (self._gdf['rgt'] == RGT) &
+ (self._gdf['gt'] == GT) &
+ (self._gdf['cycle'] == cycle)]
+ if not any(geodataframe[column].values):
+ continue
+ # plot reduced data frame
+ l, = ax.plot(geodataframe['x_atc'].values,
+ geodataframe[column].values,
+ marker='.', lw=0, ms=1.5)
+ # create legend element for cycle
+ if (kwargs['legend_label'] == 'date'):
+ label = geodataframe.index[0].strftime('%Y-%m-%d')
+ elif (kwargs['legend_label'] == 'cycle'):
+ label = f'Cycle {cycle:0.0f}'
+ legend_elements.append(matplotlib.lines.Line2D([0], [0],
+ color=l.get_color(), lw=6, label=label))
+ # add axes labels
+ ax.set_xlabel('Along-Track Distance [m]')
+ ax.set_ylabel(f'SlideRule {column}')
+ elif (kwargs['kind'] == 'scatter'):
+ # extract orbital cycle parameters
+ cycle = int(kwargs['cycle'])
+ if (kwargs['atl03'] == 'dataframe'):
+ # reduce entered data frame to RGT, ground track and cycle
+ atl03 = self._gdf[(self._gdf['rgt'] == RGT) &
+ (self._gdf['track'] == self.PT(GT)) &
+ (self._gdf['pair'] == self.LR(GT)) &
+ (self._gdf['cycle'] == cycle)]
+ elif (kwargs['atl03'] is not None):
+ # reduce ATL03 data frame to RGT, ground track and cycle
+ atl03 = kwargs['atl03'][(kwargs['atl03']['rgt'] == RGT) &
+ (kwargs['atl03']['track'] == self.PT(GT)) &
+ (kwargs['atl03']['pair'] == self.LR(GT)) &
+ (kwargs['atl03']['cycle'] == cycle)]
+ if (kwargs['classification'] == 'atl08'):
+ # noise, ground, canopy, top of canopy, unclassified
+ colormap = np.array(['c','b','g','g','y'])
+ classes = ['noise','ground','canopy','toc','unclassified']
+ sc = ax.scatter(atl03.index.values, atl03["height"].values,
+ c=colormap[atl03["atl08_class"].values.astype('i')],
+ s=1.5, rasterized=True)
+ for i,lab in enumerate(classes):
+ element = matplotlib.lines.Line2D([0], [0],
+ color=colormap[i], lw=6, label=lab)
+ legend_elements.append(element)
+ elif (kwargs['classification'] == 'yapc'):
+ sc = ax.scatter(atl03.index.values,
+ atl03["height"].values,
+ c=atl03["yapc_score"],
+ cmap=kwargs['cmap'],
+ s=1.5, rasterized=True)
+ plt.colorbar(sc)
+ elif (kwargs['classification'] == 'atl03'):
+ # background, buffer, low, medium, high
+ colormap = np.array(['y','c','b','g','m'])
+ confidences = ['background','buffer','low','medium','high']
+ # reduce data frame to photon classified for surface
+ atl03 = atl03[atl03["atl03_cnf"] >= 0]
+ sc = ax.scatter(atl03.index.values, atl03["height"].values,
+ c=colormap[atl03["atl03_cnf"].values.astype('i')],
+ s=1.5, rasterized=True)
+ for i,lab in enumerate(confidences):
+ element = matplotlib.lines.Line2D([0], [0],
+ color=colormap[i], lw=6, label=lab)
+ legend_elements.append(element)
+ elif (kwargs['atl03'] is not None):
+ # plot all available ATL03 points as gray
+ sc = ax.scatter(atl03.index.values, atl03["height"].values,
+ c='0.4', s=0.5, rasterized=True)
+ legend_elements.append(matplotlib.lines.Line2D([0], [0],
+ color='0.4', lw=6, label='ATL03'))
+ if kwargs['segments']:
+ geodataframe = self._gdf[
+ (self._gdf['rgt'] == RGT) &
+ (self._gdf['gt'] == GT) &
+ (self._gdf['cycle'] == cycle)]
+ # plot reduced data frame
+ sc = ax.scatter(geodataframe.index.values,
+ geodataframe["h_mean"].values,
+ c='red', s=2.5, rasterized=True)
+ legend_elements.append(matplotlib.lines.Line2D([0], [0],
+ color='red', lw=6, label='ATL06-SR'))
+ # add axes labels
+ ax.set_xlabel('UTC')
+ ax.set_ylabel('Height (m)')
+ # add title
+ if kwargs['title']:
+ ax.set_title(kwargs['title'])
+ # create legend
+ if kwargs['legend']:
+ lgd = ax.legend(handles=legend_elements, loc=3,
+ frameon=kwargs['legend_frameon'])
+ # set legend frame to solid white
+ if kwargs['legend'] and kwargs['legend_frameon']:
+ lgd.get_frame().set_alpha(1.0)
+ lgd.get_frame().set_edgecolor('white')
+ if kwargs['ax'] is None:
+ # show the figure
+ plt.tight_layout()
+
+ def ground_track(self, GT):
+ """extract the ground track name for a given Ground Track (GT) index
+ """
+ ground_track_list = ['gt1l', 'gt1r', 'gt2l', 'gt2r', 'gt3l', 'gt3r']
+ return ground_track_list[GT//10 - 1]
+
+ def PT(self, GT):
+ """extract Pair Track (PT) index for a given Ground Track (GT) index
+ """
+ # mapping between ground tracks and sliderule tracks
+ ground_track = self.ground_track(GT)
+ pair_track_dict = dict(gt1l=1,gt1r=1,gt2l=2,gt2r=2,gt3l=3,gt3r=3)
+ return pair_track_dict[ground_track]
+
+ def LR(self, GT):
+ """extract Left-Right (LR) index for a given Ground Track (GT) index
+ """
+ # mapping between ground tracks and sliderule tracks
+ ground_track = self.ground_track(GT)
+ lr_track_dict = dict(gt1l=0,gt1r=1,gt2l=0,gt2r=1,gt3l=0,gt3r=1)
+ return lr_track_dict[ground_track]
+
diff --git a/clients/python/sliderule/raster.py b/clients/python/sliderule/raster.py
index 99be37397..d0057f780 100644
--- a/clients/python/sliderule/raster.py
+++ b/clients/python/sliderule/raster.py
@@ -87,7 +87,7 @@ def sample(asset, coordinates, parms={}):
'''
# Massage Arguments
if type(coordinates[0]) != list:
- coorindates = [coorindates]
+ coordinates = [coordinates]
# Perform Request
rqst = {"samples": {"asset": asset, **parms}, "coordinates": coordinates}
@@ -194,7 +194,7 @@ def subset(asset, extents, parms={}):
'''
# Massage Arguments
if type(extents[0]) != list:
- coorindates = [coorindates]
+ extents = [extents]
# Perform Request
rqst = {"samples": {"asset": asset, **parms}, "extents": extents}
diff --git a/clients/python/sliderule/version.py b/clients/python/sliderule/version.py
index c33abca29..fa063eee7 100644
--- a/clients/python/sliderule/version.py
+++ b/clients/python/sliderule/version.py
@@ -1,11 +1,15 @@
#!/usr/bin/env python
u"""
-version.py (04/2021)
-Gets semantic version number and commit hash from setuptools-scm
+version.py (11/2023)
+Gets version number of a package
"""
-from pkg_resources import get_distribution
+import importlib.metadata
-# get semantic version from setuptools-scm
-version = get_distribution("sliderule").version
+# package metadata
+metadata = importlib.metadata.metadata("sliderule")
+# get version
+version = metadata["version"]
# append "v" before the version
-full_version = "v{0}".format(version)
+full_version = f"v{version}"
+# get project name
+project_name = metadata["Name"]