Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions jdaviz/configs/default/plugins/export/export.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import os
import time
import io
from pathlib import Path
from ipywidgets import widget_serialization
import threading
import solara

from astropy import units as u
from astropy.nddata import CCDData
from glue.core.message import SubsetCreateMessage, SubsetDeleteMessage, SubsetUpdateMessage
from glue_jupyter.bqplot.image import BqplotImageView
from regions import CircleSkyRegion, EllipseSkyRegion
from specutils import Spectrum
from traitlets import Bool, List, Unicode, observe
from traitlets import Any, Bool, List, Unicode, observe

from jdaviz.core.custom_traitlets import FloatHandleEmpty, IntHandleEmpty
from jdaviz.core.marks import ShadowMixin
Expand Down Expand Up @@ -119,13 +122,18 @@ class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin,
# saving client-side is supported for all exports.
serverside_enabled = Bool(True).tag(sync=True)

file_download = Any().tag(sync=True, **widget_serialization)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.filename = AutoTextField(self, 'filename_value',
'filename_default',
'filename_auto',
'filename_invalid_msg')
self.file_download = solara.FileDownload.widget(data=self.export_to_buffer,
filename=self.filename.value,
label="Open Browser Dialog")

# description displayed under plugin title in tray
self._plugin_description = 'Export data/plots and other outputs to a file.'
Expand Down Expand Up @@ -368,6 +376,10 @@ def _is_filename_changed(self, event):
# Clear overwrite warning when user changes filename
self.overwrite_warn = False

# Change default filename of solara's file download widget
if hasattr(self.file_download, 'filename'):
self.file_download.filename = self.filename_value

def _set_subset_not_supported_msg(self, msg=None):
"""
Check if selected subset is spectral or composite, and warn and
Expand Down Expand Up @@ -409,6 +421,9 @@ def _set_dataset_not_supported_msg(self, msg=None):
self.data_invalid_msg = ''

def _normalize_filename(self, filename=None, filetype=None, overwrite=False, default_path=False): # noqa: E501
if isinstance(filename, io.BytesIO):
return filename

# Make sure filename is valid and file does not end up in weird places in standalone mode.
if not filename:
raise ValueError("Invalid filename")
Expand Down Expand Up @@ -441,6 +456,11 @@ def _normalize_filename(self, filename=None, filetype=None, overwrite=False, def

return filename

def export_to_buffer(self):
f = io.BytesIO() # TODO: can we reuse this or should we close/clear it?
self.export(filename=f)
return f.getvalue()

@with_spinner()
def export(self, filename=None, show_dialog=None, overwrite=False,
raise_error_for_overwrite=True):
Expand All @@ -452,9 +472,6 @@ def export(self, filename=None, show_dialog=None, overwrite=False,
filename : str, optional
If not provided, plugin value will be used.

show_dialog : bool or `None`
If `True`, prompts dialog to save PNG/SVG from browser.

overwrite : bool
If `True`, silently overwrite an existing file.

Expand All @@ -463,6 +480,8 @@ def export(self, filename=None, show_dialog=None, overwrite=False,
output file already exists. Otherwise, a message will be sent
to application snackbar instead.
"""
if show_dialog:
raise ValueError("show_dialog is no longer supported, use UI to trigger browser save dialog") # noqa
if self.multiselect:
raise NotImplementedError("batch export not yet supported")

Expand Down Expand Up @@ -506,7 +525,7 @@ def export(self, filename=None, show_dialog=None, overwrite=False,
width=f"{self.image_width}px" if self.image_custom_size else None,
height=f"{self.image_height}px" if self.image_custom_size else None)
else:
self.save_figure(viewer, filename, filetype, show_dialog=show_dialog,
self.save_figure(viewer, filename, filetype,
width=f"{self.image_width}px" if self.image_custom_size else None,
height=f"{self.image_height}px" if self.image_custom_size else None) # noqa

Expand Down Expand Up @@ -534,7 +553,7 @@ def export(self, filename=None, show_dialog=None, overwrite=False,
raise FileExistsError(f"{filename} exists but overwrite={overwrite}")
return

self.save_figure(plot, filename, filetype, show_dialog=show_dialog)
self.save_figure(plot, filename, filetype)

elif len(self.plugin_table.selected):
filetype = self.plugin_table_format.selected
Expand All @@ -560,7 +579,10 @@ def export(self, filename=None, show_dialog=None, overwrite=False,
return

if self.subset_format.selected in ('fits', 'reg'):
self.save_subset_as_region(selected_subset_label, filename)
# TODO: what format should be passed for .reg?
# I'm getting errors for a spatial subset
self.save_subset_as_region(selected_subset_label, filename,
format=self.subset_format.selected)
elif self.subset_format.selected == 'ecsv':
self.save_subset_as_table(filename)
elif self.subset_format.selected == 'stcs':
Expand All @@ -575,15 +597,17 @@ def export(self, filename=None, show_dialog=None, overwrite=False,
if raise_error_for_overwrite:
raise FileExistsError(f"{filename} exists but overwrite=False")
return
self.dataset.selected_obj.write(Path(filename), overwrite=True)
self.dataset.selected_obj.write(Path(filename)
if isinstance(filename, str) else filename,
overwrite=True, format=filetype)
else:
raise ValueError("nothing selected for export")

return filename

def vue_export_from_ui(self, *args, **kwargs):
try:
filename = self.export(show_dialog=True, raise_error_for_overwrite=False)
filename = self.export(raise_error_for_overwrite=False)
except Exception as e:
self.hub.broadcast(SnackbarMessage(
f"Export failed with: {e}", sender=self,
Expand All @@ -596,7 +620,7 @@ def vue_export_from_ui(self, *args, **kwargs):
def vue_overwrite_from_ui(self, *args, **kwargs):
"""Attempt to force writing the output if the user confirms the desire to overwrite."""
try:
filename = self.export(show_dialog=True, overwrite=True,
filename = self.export(overwrite=True,
raise_error_for_overwrite=False)
except Exception as e:
self.hub.broadcast(SnackbarMessage(
Expand All @@ -608,7 +632,7 @@ def vue_overwrite_from_ui(self, *args, **kwargs):
f"Exported to {filename} (overwrite)", sender=self, color="success"))
self.overwrite_warn = False

def save_figure(self, viewer, filename=None, filetype="png", show_dialog=False,
def save_figure(self, viewer, filename=None, filetype="png",
width=None, height=None):
if filename is None:
filename = self.filename_default
Expand Down Expand Up @@ -688,12 +712,18 @@ def wait_in_other_thread():
_widget_after_first_display(cloned_viewer.figure, on_figure_displayed)
_show_hidden(cloned_viewer.figure, width, height)
elif filetype == 'png':
# NOTE: get_png already check if _upload_png_callback is not None
get_png(viewer.figure)
if filename is None or isinstance(filename, io.BytesIO):
viewer.figure.save_png(str(filename) if isinstance(filename, Path) else filename)
else:
# NOTE: get_png already check if _upload_png_callback is not None
get_png(viewer.figure)
elif filetype == 'svg':
if viewer.figure._upload_svg_callback is not None:
raise ValueError("previous svg export is still in progress. Wait to complete before making another call to save_figure") # noqa
viewer.figure.get_svg_data(on_img_received)
if filename is None or isinstance(filename, io.BytesIO):
viewer.figure.save_svg(str(filename) if isinstance(filename, Path) else filename)
else:
if viewer.figure._upload_svg_callback is not None:
raise ValueError("previous svg export is still in progress. Wait to complete before making another call to save_figure") # noqa
viewer.figure.get_svg_data(on_img_received)
else:
raise ValueError(f"Unsupported filetype={filetype} for save_figure")

Expand Down Expand Up @@ -725,7 +755,7 @@ def _save_movie(self, viewer, i_start, i_end, fps, filename, rm_temp_files, widt
slice_plg.vue_play_next()
cur_pngfile = Path(f"._cubeviz_movie_frame_{i}.png")
# TODO: skip success snackbars when exporting temp movie frames?
self.save_figure(viewer, filename=cur_pngfile, filetype="png", show_dialog=False,
self.save_figure(viewer, filename=cur_pngfile, filetype="png",
width=width, height=height)
temp_png_files.append(cur_pngfile)
i += i_step
Expand Down Expand Up @@ -875,7 +905,7 @@ def save_subset_as_stcs(self, filename):
with open(filename, 'w') as f:
f.write(stcs_str)

def save_subset_as_region(self, selected_subset_label, filename):
def save_subset_as_region(self, selected_subset_label, filename, format):
"""
Save a subset to file as a Region object in the working directory.
Currently only enabled for non-composite spatial subsets. Can be saved
Expand All @@ -893,7 +923,7 @@ def save_subset_as_region(self, selected_subset_label, filename):

region = region[0][f'{"sky_" if align_by == "wcs" else ""}region']

region.write(str(filename), overwrite=True)
region.write(str(filename), format=format, overwrite=True)

def save_subset_as_table(self, filename):
region = self.app.get_subsets(subset_name=self.subset.selected)
Expand Down
161 changes: 93 additions & 68 deletions jdaviz/configs/default/plugins/export/export.vue
Original file line number Diff line number Diff line change
Expand Up @@ -273,21 +273,7 @@
</v-row>
</div>

<v-row v-if="serverside_enabled" class="row-no-outside-padding row-min-bottom-padding">
<v-col>
<v-text-field
:value="default_filepath"
label="Filepath"
hint="Filepath export location."
persistent-hint
disabled
></v-text-field>
</v-col>
</v-row>

<div style="display: grid; position: relative"> <!-- overlay container -->
<div style="grid-area: 1/1">

<j-plugin-section-header>Save via Browser</j-plugin-section-header>
<plugin-auto-label
:value.sync="filename_value"
:default="filename_default"
Expand All @@ -296,69 +282,108 @@
label="Filename"
:api_hint="'plg.filename = \''+filename_value+'\''"
:api_hints_enabled="api_hints_enabled"
hint="Export to a file on disk."
hint="Filename for download."
></plugin-auto-label>

<v-row justify="end">
<j-tooltip v-if="movie_recording" tooltipcontent="Interrupt recording and delete movie file">
<plugin-action-button
:results_isolated_to_plugin="true"
@click="interrupt_recording"
:disabled="!movie_recording"
>
<v-icon>stop</v-icon>
</plugin-action-button>
</j-tooltip>
<span
v-if="filename_value.length === 0 ||
movie_recording ||
subset_invalid_msg.length > 0 ||
data_invalid_msg.length > 0 ||
subset_format_invalid_msg.length > 0 ||
viewer_invalid_msg.length > 0 ||
(viewer_selected.length > 0 && viewer_format_selected == 'mp4' && !movie_enabled)"
>export disabled</span>
<jupyter-widget v-else :widget="file_download"></jupyter-widget>
</v-row>

<plugin-action-button
:results_isolated_to_plugin="true"
@click="export_from_ui"
:spinner="spinner"
:api_hints_enabled="api_hints_enabled"
:disabled="filename_value.length === 0 ||
movie_recording ||
subset_invalid_msg.length > 0 ||
data_invalid_msg.length > 0 ||
subset_format_invalid_msg.length > 0 ||
viewer_invalid_msg.length > 0 ||
(viewer_selected.length > 0 && viewer_format_selected == 'mp4' && !movie_enabled)"
>
{{ api_hints_enabled ?
'plg.export()'
:
'Export'
}}
</plugin-action-button>
</div>
<div v-if="serverside_enabled">
<j-plugin-section-header v-if="serverside_enabled">Save via Kernel</j-plugin-section-header>
<div style="display: grid; position: relative"> <!-- overlay container -->
<div style="grid-area: 1/1">

<v-overlay
absolute
opacity=0.5
:value="overwrite_warn"
:zIndex=3
style="grid-area: 1/1;
margin-left: -24px;
margin-right: -24px">
<v-row class="row-no-outside-padding row-min-bottom-padding">
<v-col>
<v-text-field
:value="default_filepath"
label="Filepath"
hint="Filepath export location."
persistent-hint
disabled
></v-text-field>
</v-col>
</v-row>

<v-card color="transparent" elevation=0 >
<v-card-text width="100%">
<div class="white--text">
A file with this name is already on disk. Overwrite?
</div>
</v-card-text>
<plugin-auto-label
:value.sync="filename_value"
:default="filename_default"
:auto.sync="filename_auto"
:invalid_msg="filename_invalid_msg"
label="Filename"
:api_hint="'plg.filename = \''+filename_value+'\''"
:api_hints_enabled="api_hints_enabled"
hint="Export to a file on disk."
></plugin-auto-label>

<v-card-actions>
<v-row justify="end">
<v-btn tile small color="primary" class="mr-2" @click="overwrite_warn=false">Cancel</v-btn>
<v-btn tile small color="accent" class="mr-4" @click="overwrite_from_ui">Overwrite</v-btn>
<j-tooltip v-if="movie_recording" tooltipcontent="Interrupt recording and delete movie file">
<plugin-action-button
:results_isolated_to_plugin="true"
@click="interrupt_recording"
:disabled="!movie_recording"
>
<v-icon>stop</v-icon>
</plugin-action-button>
</j-tooltip>

<plugin-action-button
:results_isolated_to_plugin="true"
@click="export_from_ui"
:spinner="spinner"
:api_hints_enabled="api_hints_enabled"
:disabled="filename_value.length === 0 ||
movie_recording ||
subset_invalid_msg.length > 0 ||
data_invalid_msg.length > 0 ||
subset_format_invalid_msg.length > 0 ||
viewer_invalid_msg.length > 0 ||
(viewer_selected.length > 0 && viewer_format_selected == 'mp4' && !movie_enabled)"
>
{{ api_hints_enabled ?
'plg.export()'
:
'Export'
}}
</plugin-action-button>
</v-row>
</v-card-actions>
</v-card>
</div>

</v-overlay>
</div>
<v-overlay
absolute
opacity=0.5
:value="overwrite_warn"
:zIndex=3
style="grid-area: 1/1;
margin-left: -24px;
margin-right: -24px"
>
<v-card color="transparent" elevation=0 >
<v-card-text width="100%">
<div class="white--text">
A file with this name is already on disk. Overwrite?
</div>
</v-card-text>

</v-row>
<v-card-actions>
<v-row justify="end">
<v-btn tile small color="primary" class="mr-2" @click="overwrite_warn=false">Cancel</v-btn>
<v-btn tile small color="accent" class="mr-4" @click="overwrite_from_ui">Overwrite</v-btn>
</v-row>
</v-card-actions>
</v-card>
</v-overlay>
</div>
</div>

</j-tray-plugin>
</template>
Expand Down
Loading