Skip to content

ui module

This module contains the user interface for the hypercoast package.

SpectralWidget (HBox)

A widget for spectral data visualization on a map.

Attributes:

Name Type Description
_host_map Map

The map to host the widget.

on_close function

Function to be called when the widget is closed.

_output_widget widgets.Output

The output widget to display results.

_output_control ipyleaflet.WidgetControl

The control for the output widget.

_on_map_interaction function

Function to handle map interactions.

_spectral_widget SpectralWidget

The spectral widget itself.

_spectral_control ipyleaflet.WidgetControl

The control for the spectral widget.

Source code in hypercoast/ui.py
class SpectralWidget(widgets.HBox):
    """
    A widget for spectral data visualization on a map.

    Attributes:
        _host_map (Map): The map to host the widget.
        on_close (function): Function to be called when the widget is closed.
        _output_widget (widgets.Output): The output widget to display results.
        _output_control (ipyleaflet.WidgetControl): The control for the output widget.
        _on_map_interaction (function): Function to handle map interactions.
        _spectral_widget (SpectralWidget): The spectral widget itself.
        _spectral_control (ipyleaflet.WidgetControl): The control for the spectral widget.
    """

    def __init__(
        self, host_map, stack=True, position="topright", xlim=None, ylim=None, **kwargs
    ):
        """
        Initializes a new instance of the SpectralWidget class.

        Args:
            host_map (Map): The map to host the widget.
            stack (bool, optional): Whether to stack the plots. Defaults to True.
            position (str, optional): The position of the widget on the map. Defaults to "topright".
            xlim (tuple, optional): The x-axis limits. Defaults to None.
            ylim (tuple, optional): The y-axis limits. Defaults to None.
        """
        self._host_map = host_map
        self.on_close = None
        self._stack = stack
        self._show_plot = False

        fig_margin = {"top": 20, "bottom": 35, "left": 50, "right": 20}
        fig = plt.figure(
            # title=None,
            fig_margin=fig_margin,
            layout={"width": "500px", "height": "300px"},
        )

        self._fig = fig
        self._host_map._fig = fig

        layer_names = list(host_map.cog_layer_dict.keys())
        layers_widget = widgets.Dropdown(options=layer_names)
        layers_widget.layout.width = "18ex"

        close_btn = widgets.Button(
            icon="times",
            tooltip="Close the widget",
            button_style="primary",
            layout=widgets.Layout(width="32px"),
        )

        reset_btn = widgets.Button(
            icon="trash",
            tooltip="Remove all markers",
            button_style="primary",
            layout=widgets.Layout(width="32px"),
        )

        settings_btn = widgets.Button(
            icon="gear",
            tooltip="Change layer settings",
            button_style="primary",
            layout=widgets.Layout(width="32px"),
        )

        stack_btn = widgets.ToggleButton(
            value=stack,
            icon="area-chart",
            tooltip="Stack spectral signatures",
            button_style="primary",
            layout=widgets.Layout(width="32px"),
        )

        def settings_btn_click(_):
            self._host_map._add_layer_editor(
                position="topright",
                layer_dict=self._host_map.cog_layer_dict[layers_widget.value],
            )

        settings_btn.on_click(settings_btn_click)

        def reset_btn_click(_):
            if hasattr(self._host_map, "_plot_marker_cluster"):
                self._host_map._plot_marker_cluster.markers = []
                self._host_map._plot_markers = []

            if hasattr(self._host_map, "_spectral_data"):
                self._host_map._spectral_data = {}

            self._output_widget.clear_output()
            self._show_plot = False
            plt.clear()

        reset_btn.on_click(reset_btn_click)

        save_btn = widgets.Button(
            icon="floppy-o",
            tooltip="Save the data to a CSV",
            button_style="primary",
            layout=widgets.Layout(width="32px"),
        )

        def chooser_callback(chooser):
            if chooser.selected:
                file_path = chooser.selected
                self._host_map.spectral_to_csv(file_path)
                if (
                    hasattr(self._host_map, "_file_chooser_control")
                    and self._host_map._file_chooser_control in self._host_map.controls
                ):
                    self._host_map.remove_control(self._host_map._file_chooser_control)
                    self._host_map._file_chooser.close()

        def save_btn_click(_):
            if not hasattr(self._host_map, "_spectral_data"):
                return

            self._output_widget.clear_output()
            file_chooser = FileChooser(
                os.getcwd(), layout=widgets.Layout(width="454px")
            )
            file_chooser.filter_pattern = "*.csv"
            file_chooser.use_dir_icons = True
            file_chooser.title = "Save spectral data to a CSV file"
            file_chooser.default_filename = "spectral_data.csv"
            file_chooser.show_hidden = False
            file_chooser.register_callback(chooser_callback)
            file_chooser_control = ipyleaflet.WidgetControl(
                widget=file_chooser, position="topright"
            )
            self._host_map.add(file_chooser_control)
            setattr(self._host_map, "_file_chooser", file_chooser)
            setattr(self._host_map, "_file_chooser_control", file_chooser_control)

        save_btn.on_click(save_btn_click)

        def close_widget(_):
            self.cleanup()

        close_btn.on_click(close_widget)

        super().__init__(
            [layers_widget, settings_btn, stack_btn, reset_btn, save_btn, close_btn]
        )

        output = widgets.Output()
        output_control = ipyleaflet.WidgetControl(widget=output, position="bottomright")
        self._output_widget = output
        self._output_control = output_control
        self._host_map.add(output_control)

        if not hasattr(self._host_map, "_spectral_data"):
            self._host_map._spectral_data = {}

        def handle_interaction(**kwargs):

            latlon = kwargs.get("coordinates")
            lat = latlon[0]
            lon = latlon[1]
            if kwargs.get("type") == "click" and self._host_map._layer_editor is None:
                layer_name = layers_widget.value

                if not hasattr(self._host_map, "_plot_markers"):
                    self._host_map._plot_markers = []
                markers = self._host_map._plot_markers
                marker_cluster = self._host_map._plot_marker_cluster
                markers.append(ipyleaflet.Marker(location=latlon, draggable=False))
                marker_cluster.markers = markers
                self._host_map._plot_marker_cluster = marker_cluster

                xlabel = "Wavelength (nm)"
                ylabel = "Reflectance"

                ds = self._host_map.cog_layer_dict[layer_name]["xds"]

                if self._host_map.cog_layer_dict[layer_name]["hyper"] == "XARRAY":
                    da = extract_spectral(ds, lat, lon)
                    xlabel = "Band"
                    ylabel = "Value"

                elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "EMIT":
                    da = ds.sel(latitude=lat, longitude=lon, method="nearest")[
                        "reflectance"
                    ]

                    if "wavelength" not in self._host_map._spectral_data:
                        self._host_map._spectral_data["wavelength"] = ds[
                            "wavelength"
                        ].values
                elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "PACE":
                    try:
                        da = extract_pace(ds, lat, lon)
                    except:
                        da = xr.DataArray(
                            np.full(len(ds["wavelength"]), np.nan),
                            dims=["wavelength"],
                            coords={"wavelength": ds["wavelength"]},
                        )
                    if "wavelengths" not in self._host_map._spectral_data:
                        self._host_map._spectral_data["wavelengths"] = ds[
                            "wavelength"
                        ].values

                elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "DESIS":
                    da = extract_desis(ds, lat, lon)

                elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "NEON":
                    da = extract_neon(ds, lat, lon)

                elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "AVIRIS":
                    da = extract_aviris(ds, lat, lon)

                self._host_map._spectral_data[f"({lat:.4f} {lon:.4f})"] = da.values

                if self._host_map.cog_layer_dict[layer_name]["hyper"] != "XARRAY":
                    da[da < 0] = np.nan
                    x_axis_options = {"label_offset": "30px"}
                else:
                    x_axis_options = {
                        "label_offset": "30px",
                        "tick_format": "0d",
                        "num_ticks": da.sizes["band"],
                    }
                axes_options = {
                    "x": x_axis_options,
                    "y": {"label_offset": "35px"},
                }

                if not stack_btn.value:
                    plt.clear()
                    plt.plot(
                        da.coords[da.dims[0]].values,
                        da.values,
                        axes_options=axes_options,
                    )
                else:
                    color = np.random.rand(
                        3,
                    )
                    plt.plot(
                        da.coords[da.dims[0]].values,
                        da.values,
                        color=color,
                        axes_options=axes_options,
                    )
                    try:
                        if isinstance(self._fig.axes[0], bqplot.ColorAxis):
                            self._fig.axes = self._fig.axes[1:]
                        elif isinstance(self._fig.axes[-1], bqplot.ColorAxis):
                            self._fig.axes = self._fig.axes[:-1]
                    except Exception:
                        pass

                plt.xlabel(xlabel)
                plt.ylabel(ylabel)
                if xlim:
                    plt.xlim(xlim[0], xlim[1])
                if ylim:
                    plt.ylim(ylim[0], ylim[1])

                if not self._show_plot:
                    with self._output_widget:
                        plt.show()
                        self._show_plot = True

                self._host_map.default_style = {"cursor": "crosshair"}

        self._host_map.on_interaction(handle_interaction)
        self._on_map_interaction = handle_interaction

        self._spectral_widget = self
        self._spectral_control = ipyleaflet.WidgetControl(
            widget=self, position=position
        )
        self._host_map.add(self._spectral_control)

    def cleanup(self):
        """Removes the widget from the map and performs cleanup."""
        if self._host_map:
            self._host_map.default_style = {"cursor": "default"}
            self._host_map.on_interaction(self._on_map_interaction, remove=True)

            if self._output_control:
                self._host_map.remove_control(self._output_control)

                if self._output_widget:
                    self._output_widget.close()
                    self._output_widget = None

            if self._spectral_control:
                self._host_map.remove_control(self._spectral_control)
                self._spectral_control = None

                if self._spectral_widget:
                    self._spectral_widget.close()
                    self._spectral_widget = None

            if hasattr(self._host_map, "_plot_marker_cluster"):
                self._host_map._plot_marker_cluster.markers = []
                self._host_map._plot_markers = []

            if hasattr(self._host_map, "_spectral_data"):
                self._host_map._spectral_data = {}

            if hasattr(self, "_output_widget") and self._output_widget is not None:
                self._output_widget.clear_output()

        if self.on_close is not None:
            self.on_close()

__init__(self, host_map, stack=True, position='topright', xlim=None, ylim=None, **kwargs) special

Initializes a new instance of the SpectralWidget class.

Parameters:

Name Type Description Default
host_map Map

The map to host the widget.

required
stack bool

Whether to stack the plots. Defaults to True.

True
position str

The position of the widget on the map. Defaults to "topright".

'topright'
xlim tuple

The x-axis limits. Defaults to None.

None
ylim tuple

The y-axis limits. Defaults to None.

None
Source code in hypercoast/ui.py
def __init__(
    self, host_map, stack=True, position="topright", xlim=None, ylim=None, **kwargs
):
    """
    Initializes a new instance of the SpectralWidget class.

    Args:
        host_map (Map): The map to host the widget.
        stack (bool, optional): Whether to stack the plots. Defaults to True.
        position (str, optional): The position of the widget on the map. Defaults to "topright".
        xlim (tuple, optional): The x-axis limits. Defaults to None.
        ylim (tuple, optional): The y-axis limits. Defaults to None.
    """
    self._host_map = host_map
    self.on_close = None
    self._stack = stack
    self._show_plot = False

    fig_margin = {"top": 20, "bottom": 35, "left": 50, "right": 20}
    fig = plt.figure(
        # title=None,
        fig_margin=fig_margin,
        layout={"width": "500px", "height": "300px"},
    )

    self._fig = fig
    self._host_map._fig = fig

    layer_names = list(host_map.cog_layer_dict.keys())
    layers_widget = widgets.Dropdown(options=layer_names)
    layers_widget.layout.width = "18ex"

    close_btn = widgets.Button(
        icon="times",
        tooltip="Close the widget",
        button_style="primary",
        layout=widgets.Layout(width="32px"),
    )

    reset_btn = widgets.Button(
        icon="trash",
        tooltip="Remove all markers",
        button_style="primary",
        layout=widgets.Layout(width="32px"),
    )

    settings_btn = widgets.Button(
        icon="gear",
        tooltip="Change layer settings",
        button_style="primary",
        layout=widgets.Layout(width="32px"),
    )

    stack_btn = widgets.ToggleButton(
        value=stack,
        icon="area-chart",
        tooltip="Stack spectral signatures",
        button_style="primary",
        layout=widgets.Layout(width="32px"),
    )

    def settings_btn_click(_):
        self._host_map._add_layer_editor(
            position="topright",
            layer_dict=self._host_map.cog_layer_dict[layers_widget.value],
        )

    settings_btn.on_click(settings_btn_click)

    def reset_btn_click(_):
        if hasattr(self._host_map, "_plot_marker_cluster"):
            self._host_map._plot_marker_cluster.markers = []
            self._host_map._plot_markers = []

        if hasattr(self._host_map, "_spectral_data"):
            self._host_map._spectral_data = {}

        self._output_widget.clear_output()
        self._show_plot = False
        plt.clear()

    reset_btn.on_click(reset_btn_click)

    save_btn = widgets.Button(
        icon="floppy-o",
        tooltip="Save the data to a CSV",
        button_style="primary",
        layout=widgets.Layout(width="32px"),
    )

    def chooser_callback(chooser):
        if chooser.selected:
            file_path = chooser.selected
            self._host_map.spectral_to_csv(file_path)
            if (
                hasattr(self._host_map, "_file_chooser_control")
                and self._host_map._file_chooser_control in self._host_map.controls
            ):
                self._host_map.remove_control(self._host_map._file_chooser_control)
                self._host_map._file_chooser.close()

    def save_btn_click(_):
        if not hasattr(self._host_map, "_spectral_data"):
            return

        self._output_widget.clear_output()
        file_chooser = FileChooser(
            os.getcwd(), layout=widgets.Layout(width="454px")
        )
        file_chooser.filter_pattern = "*.csv"
        file_chooser.use_dir_icons = True
        file_chooser.title = "Save spectral data to a CSV file"
        file_chooser.default_filename = "spectral_data.csv"
        file_chooser.show_hidden = False
        file_chooser.register_callback(chooser_callback)
        file_chooser_control = ipyleaflet.WidgetControl(
            widget=file_chooser, position="topright"
        )
        self._host_map.add(file_chooser_control)
        setattr(self._host_map, "_file_chooser", file_chooser)
        setattr(self._host_map, "_file_chooser_control", file_chooser_control)

    save_btn.on_click(save_btn_click)

    def close_widget(_):
        self.cleanup()

    close_btn.on_click(close_widget)

    super().__init__(
        [layers_widget, settings_btn, stack_btn, reset_btn, save_btn, close_btn]
    )

    output = widgets.Output()
    output_control = ipyleaflet.WidgetControl(widget=output, position="bottomright")
    self._output_widget = output
    self._output_control = output_control
    self._host_map.add(output_control)

    if not hasattr(self._host_map, "_spectral_data"):
        self._host_map._spectral_data = {}

    def handle_interaction(**kwargs):

        latlon = kwargs.get("coordinates")
        lat = latlon[0]
        lon = latlon[1]
        if kwargs.get("type") == "click" and self._host_map._layer_editor is None:
            layer_name = layers_widget.value

            if not hasattr(self._host_map, "_plot_markers"):
                self._host_map._plot_markers = []
            markers = self._host_map._plot_markers
            marker_cluster = self._host_map._plot_marker_cluster
            markers.append(ipyleaflet.Marker(location=latlon, draggable=False))
            marker_cluster.markers = markers
            self._host_map._plot_marker_cluster = marker_cluster

            xlabel = "Wavelength (nm)"
            ylabel = "Reflectance"

            ds = self._host_map.cog_layer_dict[layer_name]["xds"]

            if self._host_map.cog_layer_dict[layer_name]["hyper"] == "XARRAY":
                da = extract_spectral(ds, lat, lon)
                xlabel = "Band"
                ylabel = "Value"

            elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "EMIT":
                da = ds.sel(latitude=lat, longitude=lon, method="nearest")[
                    "reflectance"
                ]

                if "wavelength" not in self._host_map._spectral_data:
                    self._host_map._spectral_data["wavelength"] = ds[
                        "wavelength"
                    ].values
            elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "PACE":
                try:
                    da = extract_pace(ds, lat, lon)
                except:
                    da = xr.DataArray(
                        np.full(len(ds["wavelength"]), np.nan),
                        dims=["wavelength"],
                        coords={"wavelength": ds["wavelength"]},
                    )
                if "wavelengths" not in self._host_map._spectral_data:
                    self._host_map._spectral_data["wavelengths"] = ds[
                        "wavelength"
                    ].values

            elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "DESIS":
                da = extract_desis(ds, lat, lon)

            elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "NEON":
                da = extract_neon(ds, lat, lon)

            elif self._host_map.cog_layer_dict[layer_name]["hyper"] == "AVIRIS":
                da = extract_aviris(ds, lat, lon)

            self._host_map._spectral_data[f"({lat:.4f} {lon:.4f})"] = da.values

            if self._host_map.cog_layer_dict[layer_name]["hyper"] != "XARRAY":
                da[da < 0] = np.nan
                x_axis_options = {"label_offset": "30px"}
            else:
                x_axis_options = {
                    "label_offset": "30px",
                    "tick_format": "0d",
                    "num_ticks": da.sizes["band"],
                }
            axes_options = {
                "x": x_axis_options,
                "y": {"label_offset": "35px"},
            }

            if not stack_btn.value:
                plt.clear()
                plt.plot(
                    da.coords[da.dims[0]].values,
                    da.values,
                    axes_options=axes_options,
                )
            else:
                color = np.random.rand(
                    3,
                )
                plt.plot(
                    da.coords[da.dims[0]].values,
                    da.values,
                    color=color,
                    axes_options=axes_options,
                )
                try:
                    if isinstance(self._fig.axes[0], bqplot.ColorAxis):
                        self._fig.axes = self._fig.axes[1:]
                    elif isinstance(self._fig.axes[-1], bqplot.ColorAxis):
                        self._fig.axes = self._fig.axes[:-1]
                except Exception:
                    pass

            plt.xlabel(xlabel)
            plt.ylabel(ylabel)
            if xlim:
                plt.xlim(xlim[0], xlim[1])
            if ylim:
                plt.ylim(ylim[0], ylim[1])

            if not self._show_plot:
                with self._output_widget:
                    plt.show()
                    self._show_plot = True

            self._host_map.default_style = {"cursor": "crosshair"}

    self._host_map.on_interaction(handle_interaction)
    self._on_map_interaction = handle_interaction

    self._spectral_widget = self
    self._spectral_control = ipyleaflet.WidgetControl(
        widget=self, position=position
    )
    self._host_map.add(self._spectral_control)

cleanup(self)

Removes the widget from the map and performs cleanup.

Source code in hypercoast/ui.py
def cleanup(self):
    """Removes the widget from the map and performs cleanup."""
    if self._host_map:
        self._host_map.default_style = {"cursor": "default"}
        self._host_map.on_interaction(self._on_map_interaction, remove=True)

        if self._output_control:
            self._host_map.remove_control(self._output_control)

            if self._output_widget:
                self._output_widget.close()
                self._output_widget = None

        if self._spectral_control:
            self._host_map.remove_control(self._spectral_control)
            self._spectral_control = None

            if self._spectral_widget:
                self._spectral_widget.close()
                self._spectral_widget = None

        if hasattr(self._host_map, "_plot_marker_cluster"):
            self._host_map._plot_marker_cluster.markers = []
            self._host_map._plot_markers = []

        if hasattr(self._host_map, "_spectral_data"):
            self._host_map._spectral_data = {}

        if hasattr(self, "_output_widget") and self._output_widget is not None:
            self._output_widget.clear_output()

    if self.on_close is not None:
        self.on_close()