Skip to content

Commit

Permalink
Add support for displaying spectral signature interactively (#12)
Browse files Browse the repository at this point in the history
* Add SpectralWidget

* Add a working prototype

* Add marker cluster

* Add spectral_to_df

* Clean up

* Change from matplotlib to bqplot

* Add button to save spectral data

* Add reset button

* Fix output widget bug

* Add download file function
  • Loading branch information
giswqs authored Apr 22, 2024
1 parent 96c9879 commit 6195a20
Show file tree
Hide file tree
Showing 3 changed files with 421 additions and 8 deletions.
117 changes: 114 additions & 3 deletions hypercoast/common.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,118 @@
"""The common module contains common functions and classes used by the other modules.
"""

import os

def hello_world():
"""Prints "Hello World!" to the console."""
print("Hello World!")

def github_raw_url(url):
"""Get the raw URL for a GitHub file.
Args:
url (str): The GitHub URL.
Returns:
str: The raw URL.
"""
if isinstance(url, str) and url.startswith("https://github.com/") and "blob" in url:
url = url.replace("github.com", "raw.githubusercontent.com").replace(
"blob/", ""
)
return url


def download_file(
url=None,
output=None,
quiet=False,
proxy=None,
speed=None,
use_cookies=True,
verify=True,
id=None,
fuzzy=False,
resume=False,
unzip=True,
overwrite=False,
subfolder=False,
):
"""Download a file from URL, including Google Drive shared URL.
Args:
url (str, optional): Google Drive URL is also supported. Defaults to None.
output (str, optional): Output filename. Default is basename of URL.
quiet (bool, optional): Suppress terminal output. Default is False.
proxy (str, optional): Proxy. Defaults to None.
speed (float, optional): Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None.
use_cookies (bool, optional): Flag to use cookies. Defaults to True.
verify (bool | str, optional): Either a bool, in which case it controls whether the server's TLS certificate is verified, or a string,
in which case it must be a path to a CA bundle to use. Default is True.. Defaults to True.
id (str, optional): Google Drive's file ID. Defaults to None.
fuzzy (bool, optional): Fuzzy extraction of Google Drive's file Id. Defaults to False.
resume (bool, optional): Resume the download from existing tmp file if possible. Defaults to False.
unzip (bool, optional): Unzip the file. Defaults to True.
overwrite (bool, optional): Overwrite the file if it already exists. Defaults to False.
subfolder (bool, optional): Create a subfolder with the same name as the file. Defaults to False.
Returns:
str: The output file path.
"""
import zipfile
import tarfile
import gdown

if output is None:
if isinstance(url, str) and url.startswith("http"):
output = os.path.basename(url)

out_dir = os.path.abspath(os.path.dirname(output))
if not os.path.exists(out_dir):
os.makedirs(out_dir)

if isinstance(url, str):
if os.path.exists(os.path.abspath(output)) and (not overwrite):
print(
f"{output} already exists. Skip downloading. Set overwrite=True to overwrite."
)
return os.path.abspath(output)
else:
url = github_raw_url(url)

if "https://drive.google.com/file/d/" in url:
fuzzy = True

output = gdown.download(
url, output, quiet, proxy, speed, use_cookies, verify, id, fuzzy, resume
)

if unzip:
if output.endswith(".zip"):
with zipfile.ZipFile(output, "r") as zip_ref:
if not quiet:
print("Extracting files...")
if subfolder:
basename = os.path.splitext(os.path.basename(output))[0]

output = os.path.join(out_dir, basename)
if not os.path.exists(output):
os.makedirs(output)
zip_ref.extractall(output)
else:
zip_ref.extractall(os.path.dirname(output))
elif output.endswith(".tar.gz") or output.endswith(".tar"):
if output.endswith(".tar.gz"):
mode = "r:gz"
else:
mode = "r"

with tarfile.open(output, mode) as tar_ref:
if not quiet:
print("Extracting files...")
if subfolder:
basename = os.path.splitext(os.path.basename(output))[0]
output = os.path.join(out_dir, basename)
if not os.path.exists(output):
os.makedirs(output)
tar_ref.extractall(output)
else:
tar_ref.extractall(os.path.dirname(output))

return os.path.abspath(output)
104 changes: 99 additions & 5 deletions hypercoast/hypercoast.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Main module."""

import ipyleaflet
import leafmap

import xarray as xr
from .common import download_file
from .emit import read_emit, plot_emit, viz_emit, emit_to_netcdf, emit_to_image


Expand All @@ -25,6 +27,23 @@ def __init__(self, **kwargs):
"""
super().__init__(**kwargs)

def add(self, obj, position="topright", **kwargs):
"""Add a layer to the map.
Args:
**kwargs: Arbitrary keyword arguments that are passed to the parent class's add_layer method.
"""

if isinstance(obj, str):
if obj == "spectral":
from .ui import SpectralWidget

SpectralWidget(self, position=position)
self.set_plot_options(add_marker_cluster=True)

else:
super().add(obj, **kwargs)

def search_emit(self, default_datset="EMITL2ARFL"):
"""
Adds a NASA Earth Data search tool to the map with a default dataset for EMIT.
Expand Down Expand Up @@ -109,7 +128,7 @@ def add_emit(
vmax=None,
nodata=None,
attribution=None,
layer_name="Raster",
layer_name="EMIT",
zoom_to_layer=True,
visible=True,
array_args={},
Expand All @@ -131,16 +150,20 @@ def add_emit(
vmax (float, optional): The maximum value to use when colormapping the palette when plotting a single band. Defaults to None.
nodata (float, optional): The value from the band to use to interpret as not valid data. Defaults to None.
attribution (str, optional): Attribution for the source raster. This defaults to a message about it being a local file.. Defaults to None.
layer_name (str, optional): The layer name to use. Defaults to 'Raster'.
layer_name (str, optional): The layer name to use. Defaults to 'EMIT'.
zoom_to_layer (bool, optional): Whether to zoom to the extent of the layer. Defaults to True.
visible (bool, optional): Whether the layer is visible. Defaults to True.
array_args (dict, optional): Additional arguments to pass to `array_to_memory_file` when reading the raster. Defaults to {}.
"""

xds = None
if isinstance(source, str):

ds = read_emit(source, wavelengths=wavelengths)
source = emit_to_image(ds, wavelengths=wavelengths)
xds = read_emit(source)
source = emit_to_image(xds, wavelengths=wavelengths)
elif isinstance(source, xr.Dataset):
xds = source
source = emit_to_image(xds, wavelengths=wavelengths)

self.add_raster(
source,
Expand All @@ -156,3 +179,74 @@ def add_emit(
array_args=array_args,
**kwargs,
)

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

def set_plot_options(
self,
add_marker_cluster=False,
plot_type=None,
overlay=False,
position="bottomright",
min_width=None,
max_width=None,
min_height=None,
max_height=None,
**kwargs,
):
"""Sets plotting options.
Args:
add_marker_cluster (bool, optional): Whether to add a marker cluster. Defaults to False.
sample_scale (float, optional): A nominal scale in meters of the projection to sample in . Defaults to None.
plot_type (str, optional): The plot type can be one of "None", "bar", "scatter" or "hist". Defaults to None.
overlay (bool, optional): Whether to overlay plotted lines on the figure. Defaults to False.
position (str, optional): Position of the control, can be ‘bottomleft’, ‘bottomright’, ‘topleft’, or ‘topright’. Defaults to 'bottomright'.
min_width (int, optional): Min width of the widget (in pixels), if None it will respect the content size. Defaults to None.
max_width (int, optional): Max width of the widget (in pixels), if None it will respect the content size. Defaults to None.
min_height (int, optional): Min height of the widget (in pixels), if None it will respect the content size. Defaults to None.
max_height (int, optional): Max height of the widget (in pixels), if None it will respect the content size. Defaults to None.
"""
plot_options_dict = {}
plot_options_dict["add_marker_cluster"] = add_marker_cluster
plot_options_dict["plot_type"] = plot_type
plot_options_dict["overlay"] = overlay
plot_options_dict["position"] = position
plot_options_dict["min_width"] = min_width
plot_options_dict["max_width"] = max_width
plot_options_dict["min_height"] = min_height
plot_options_dict["max_height"] = max_height

for key in kwargs:
plot_options_dict[key] = kwargs[key]

self._plot_options = plot_options_dict

if not hasattr(self, "_plot_marker_cluster"):
self._plot_marker_cluster = ipyleaflet.MarkerCluster(name="Marker Cluster")

if add_marker_cluster and (self._plot_marker_cluster not in self.layers):
self.add(self._plot_marker_cluster)

def spectral_to_df(self, **kwargs):
"""Converts the spectral data to a pandas DataFrame.
Returns:
pd.DataFrame: The spectral data as a pandas DataFrame.
"""
import pandas as pd

df = pd.DataFrame(self._spectral_data, **kwargs)
return df

def spectral_to_csv(self, filename, index=True, **kwargs):
"""Saves the spectral data to a CSV file.
Args:
filename (str): The output CSV file.
index (bool, optional): Whether to write the index. Defaults to True.
"""
df = self.spectral_to_df()
df = df.rename_axis("band")
df.to_csv(filename, index=index, **kwargs)
Loading

0 comments on commit 6195a20

Please sign in to comment.