+
+
+
+
+
diff --git a/jdaviz/configs/cubeviz/cubeviz.yaml b/jdaviz/configs/cubeviz/cubeviz.yaml
index ade379f001..92a7ac1a4c 100644
--- a/jdaviz/configs/cubeviz/cubeviz.yaml
+++ b/jdaviz/configs/cubeviz/cubeviz.yaml
@@ -31,7 +31,7 @@ tray:
- cubeviz-moment-maps
- cubeviz-spectral-extraction
- imviz-aper-phot-simple
- - g-export-plot
+ - export
viewer_area:
- container: col
children:
diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
index 0266dc52fa..37a359801f 100644
--- a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
+++ b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
@@ -86,8 +86,8 @@ def test_remote_server_disable_save_serverside():
cubeviz_app = Application(config)
cubeviz_helper = Cubeviz(cubeviz_app)
- ep = cubeviz_helper.plugins['Export Plot']
- assert ep._obj.movie_enabled is False
+ exp = cubeviz_helper.plugins['Export']
+ assert 'mp4' not in exp.viewer_format.choices
mm = cubeviz_helper.plugins['Moment Maps']
assert mm._obj.export_enabled is False
diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py b/jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py
index 2c4d3ebf28..9470f1ef1a 100644
--- a/jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py
+++ b/jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py
@@ -2,7 +2,7 @@
import pytest
-from jdaviz.configs.default.plugins.export_plot.export_plot import HAS_OPENCV
+from jdaviz.configs.default.plugins.export.export import HAS_OPENCV
# TODO: Remove skip when https://github.com/bqplot/bqplot/pull/1397/files#r726500097 is resolved.
@@ -13,12 +13,12 @@ def test_export_movie(cubeviz_helper, spectrum1d_cube, tmp_path):
os.chdir(tmp_path)
try:
cubeviz_helper.load_data(spectrum1d_cube, data_label="test")
- plugin = cubeviz_helper.plugins["Export Plot"]
- assert plugin.i_start == 0
- assert plugin.i_end == 1
- assert plugin.movie_filename == "mymovie.mp4"
+ plugin = cubeviz_helper.plugins["Export"]
+ assert plugin._obj.i_start == 0
+ assert plugin._obj.i_end == 1
- plugin._obj.vue_save_movie("mp4")
+ plugin.viewer_format = 'mp4'
+ plugin._obj.export()
assert os.path.isfile("mymovie.mp4"), tmp_path
finally:
os.chdir(orig_path)
@@ -27,22 +27,18 @@ def test_export_movie(cubeviz_helper, spectrum1d_cube, tmp_path):
@pytest.mark.skipif(HAS_OPENCV, reason="opencv-python is installed")
def test_no_opencv(cubeviz_helper, spectrum1d_cube):
cubeviz_helper.load_data(spectrum1d_cube, data_label="test")
- plugin = cubeviz_helper.plugins["Export Plot"]
- assert plugin._obj.movie_msg != ""
+ plugin = cubeviz_helper.plugins["Export"]
+ assert 'mp4' in plugin.viewer_format.choices
+ assert not plugin._obj.movie_enabled
+ plugin.viewer_format = 'mp4'
with pytest.raises(ImportError, match="Please install opencv-python"):
- plugin.save_movie()
+ plugin.export()
@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed")
def test_export_movie_not_cubeviz(imviz_helper):
- plugin = imviz_helper.plugins["Export Plot"]
-
- with pytest.raises(NotImplementedError, match="save_movie is not available for config"):
- plugin._obj.save_movie()
-
- # Also not available via plugin public API.
- with pytest.raises(AttributeError):
- plugin.save_movie()
+ plugin = imviz_helper.plugins["Export"]
+ assert 'mp4' not in plugin.viewer_format.choices
@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed")
@@ -50,54 +46,45 @@ def test_export_movie_cubeviz_exceptions(cubeviz_helper, spectrum1d_cube):
cubeviz_helper.load_data(spectrum1d_cube, data_label="test")
cubeviz_helper.default_viewer._obj.shape = (100, 100)
cubeviz_helper.app.get_viewer("uncert-viewer").shape = (100, 100)
- plugin = cubeviz_helper.plugins["Export Plot"]
- assert plugin._obj.movie_msg == ""
- assert plugin.i_start == 0
- assert plugin.i_end == 1
- assert plugin.movie_filename == "mymovie.mp4"
-
- with pytest.raises(NotImplementedError, match="filetype"):
- plugin.save_movie(filetype="gif")
-
- with pytest.raises(NotImplementedError, match="filetype"):
- plugin.save_movie(filename="mymovie.gif", filetype=None)
+ plugin = cubeviz_helper.plugins["Export"]
+ assert plugin._obj.i_start == 0
+ assert plugin._obj.i_end == 1
+ plugin.viewer_format = 'mp4'
+ plugin._obj.i_end = 0
with pytest.raises(ValueError, match="No frames to write"):
- plugin.save_movie(i_start=0, i_end=0)
+ plugin.export()
+ plugin._obj.i_end = 1
+ plugin._obj.movie_fps = 0
with pytest.raises(ValueError, match="Invalid frame rate"):
- plugin.save_movie(fps=0)
+ plugin.export()
- plugin.movie_filename = "fake_path/mymovie.mp4"
+ plugin._obj.movie_fps = 5
+ plugin.filename = "fake_path/mymovie.mp4"
with pytest.raises(ValueError, match="Invalid path"):
- plugin.save_movie()
-
- plugin.movie_filename = "mymovie.mp4"
- plugin.viewer = 'spectrum-viewer'
- with pytest.raises(TypeError, match=r"Movie for.*is not supported"):
- plugin.save_movie()
+ plugin.export()
- plugin.movie_filename = ""
+ plugin.filename = ""
plugin.viewer = 'uncert-viewer'
with pytest.raises(ValueError, match="Invalid filename"):
- plugin.save_movie()
+ plugin.export()
@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed")
def test_export_movie_cubeviz_empty(cubeviz_helper):
- plugin = cubeviz_helper.plugins["Export Plot"]
- assert plugin.i_start == 0
- assert plugin.i_end == 0
+ plugin = cubeviz_helper.plugins["Export"]
+ assert plugin._obj.i_start == 0
+ assert plugin._obj.i_end == 0
+ plugin.viewer_format = 'mp4'
with pytest.raises(ValueError, match="Selected viewer has no display shape"):
- plugin.save_movie(i_start=0, i_end=1)
+ plugin.export()
def test_export_plot_exceptions(cubeviz_helper):
- plugin = cubeviz_helper.plugins["Export Plot"]
-
- with pytest.raises(NotImplementedError, match="filetype.*not supported"):
- plugin.save_figure(filetype="gif")
+ plugin = cubeviz_helper.plugins["Export"]
+ plugin.filename = "/fake/path/image.png"
with pytest.raises(ValueError, match="Invalid path"):
- plugin.save_figure(filename="/fake/path/image.png")
+ plugin.export()
diff --git a/jdaviz/configs/default/default.yaml b/jdaviz/configs/default/default.yaml
index d5a7eba645..4dda8cbb79 100644
--- a/jdaviz/configs/default/default.yaml
+++ b/jdaviz/configs/default/default.yaml
@@ -15,4 +15,4 @@ toolbar:
tray:
- g-subset-plugin
- g-gaussian-smooth
- - g-export-plot
+ - export
diff --git a/jdaviz/configs/default/plugins/__init__.py b/jdaviz/configs/default/plugins/__init__.py
index f0fc504d4b..e516c2dd82 100644
--- a/jdaviz/configs/default/plugins/__init__.py
+++ b/jdaviz/configs/default/plugins/__init__.py
@@ -8,6 +8,6 @@
from .collapse.collapse import * # noqa
from .line_lists.line_lists import * # noqa
from .metadata_viewer.metadata_viewer import * # noqa
-from .export_plot.export_plot import * # noqa
+from .export.export import * # noqa
from .plot_options.plot_options import * # noqa
from .markers.markers import * # noqa
diff --git a/jdaviz/configs/default/plugins/export/__init__.py b/jdaviz/configs/default/plugins/export/__init__.py
new file mode 100644
index 0000000000..1e34ea082e
--- /dev/null
+++ b/jdaviz/configs/default/plugins/export/__init__.py
@@ -0,0 +1 @@
+from .export import * # noqa
diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py
new file mode 100644
index 0000000000..b08913c1b6
--- /dev/null
+++ b/jdaviz/configs/default/plugins/export/export.py
@@ -0,0 +1,399 @@
+import os
+from pathlib import Path
+from traitlets import Any, Bool, List, Unicode, observe
+from glue_jupyter.bqplot.image import BqplotImageView
+
+from jdaviz.core.custom_traitlets import FloatHandleEmpty, IntHandleEmpty
+from jdaviz.core.registries import tray_registry
+from jdaviz.core.template_mixin import (PluginTemplateMixin, SelectPluginComponent,
+ ViewerSelectMixin, DatasetMultiSelectMixin,
+ SubsetSelectMixin, MultiselectMixin, with_spinner)
+from jdaviz.core.events import AddDataMessage, SnackbarMessage
+from jdaviz.core.user_api import PluginUserApi
+
+try:
+ import cv2
+except ImportError:
+ HAS_OPENCV = False
+else:
+ import threading
+ import time
+ HAS_OPENCV = True
+
+__all__ = ['Export']
+
+
+@tray_registry('export', label="Export")
+class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin,
+ DatasetMultiSelectMixin, MultiselectMixin):
+ """
+ See the :ref:`Export Plugin Documentation ` for more details.
+
+ Only the following attributes and methods are available through the
+ :ref:`public plugin API `:
+
+ * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
+ * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
+ * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
+ * ``viewer`` (:class:`~jdaviz.core.template_mixin.ViewerSelect`)
+ * ``viewer_format`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`)
+ * ``filename``
+ * :meth:`export`
+ """
+ template_file = __file__, "export.vue"
+
+ # feature flag for cone support
+ dev_dataset_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring
+ dev_subset_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring
+ dev_table_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring
+ dev_plot_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring
+ dev_multi_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring
+
+ table_items = List().tag(sync=True)
+ table_selected = Any().tag(sync=True)
+
+ plot_items = List().tag(sync=True)
+ plot_selected = Any().tag(sync=True)
+
+ viewer_format_items = List().tag(sync=True)
+ viewer_format_selected = Unicode().tag(sync=True)
+
+ filename = Unicode().tag(sync=True)
+
+ # For Cubeviz movie.
+ movie_enabled = Bool(False).tag(sync=True)
+ i_start = IntHandleEmpty(0).tag(sync=True)
+ i_end = IntHandleEmpty(0).tag(sync=True)
+ movie_fps = FloatHandleEmpty(5.0).tag(sync=True)
+ movie_recording = Bool(False).tag(sync=True)
+ movie_interrupt = Bool(False).tag(sync=True)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.table = SelectPluginComponent(self,
+ items='table_items',
+ selected='table_selected',
+ multiselect='multiselect',
+ default_mode='empty',
+ manual_options=['table-tst1', 'table-tst2'])
+
+ self.plot = SelectPluginComponent(self,
+ items='plot_items',
+ selected='plot_selected',
+ multiselect='multiselect',
+ default_mode='empty',
+ manual_options=['plot-tst1', 'plot-tst2'])
+
+ viewer_format_options = ['png', 'svg']
+ if self.config == 'cubeviz':
+ if not self.app.state.settings.get('server_is_remote'):
+ viewer_format_options += ['mp4']
+ # still list mp4 as an option, but display a message (and raise an error) if
+ # opencv is not available
+ self.movie_enabled = HAS_OPENCV
+
+ self.viewer_format = SelectPluginComponent(self,
+ items='viewer_format_items',
+ selected='viewer_format_selected',
+ manual_options=viewer_format_options)
+
+ # default selection:
+ self.dataset._default_mode = 'empty'
+ self.viewer.select_default()
+ self.filename = f"{self.app.config}_export"
+
+ if self.config == "cubeviz":
+ self.session.hub.subscribe(self, AddDataMessage, handler=self._on_cubeviz_data_added) # noqa: E501
+
+ @property
+ def user_api(self):
+ # TODO: backwards compat for save_figure, save_movie,
+ # i_start, i_end, movie_fps, movie_filename
+ # TODO: expose export method once API is finalized
+ expose = ['viewer', 'viewer_format', 'filename', 'export']
+
+ if self.dev_dataset_support:
+ expose += ['dataset']
+ if self.dev_subset_support:
+ expose += ['subset']
+ if self.dev_table_support:
+ expose += ['table']
+ if self.dev_plot_support:
+ expose += ['plot']
+ if self.dev_multi_support:
+ expose += ['multiselect']
+
+ return PluginUserApi(self, expose=expose)
+
+ def _on_cubeviz_data_added(self, msg):
+ # NOTE: This needs revising if we allow loading more than one cube.
+ if isinstance(msg.viewer, BqplotImageView):
+ if len(msg.data.shape) == 3:
+ self.i_end = msg.data.shape[-1] - 1
+
+ @observe('multiselect', 'viewer_multiselect')
+ def _sync_multiselect_traitlets(self, event):
+ # ViewerSelectMixin brings viewer_multiselect, but we want a single traitlet to control
+ # all select inputs, so we'll keep them synced here and only expose multiselect through
+ # the user API
+ self.multiselect = event.get('new')
+ self.viewer_multiselect = event.get('new')
+ if not self.multiselect:
+ # default to just a single viewer
+ self._sync_singleselect({'name': 'viewer', 'new': self.viewer_selected})
+
+ @observe('viewer_selected', 'dataset_selected', 'subset_selected',
+ 'table_selected', 'plot_selected')
+ def _sync_singleselect(self, event):
+ if not hasattr(self, 'dataset') or not hasattr(self, 'viewer'):
+ # plugin not fully intialized
+ return
+ # if multiselect is not enabled, only allow a single selection across all select components
+ if self.multiselect:
+ return
+ if event.get('new') in ('', []):
+ return
+ name = event.get('name')
+ for attr in ('viewer_selected', 'dataset_selected', 'subset_selected',
+ 'table_selected', 'plot_selected'):
+ if name != attr:
+ setattr(self, attr, '')
+
+ @with_spinner()
+ def export(self, filename=None, show_dialog=None):
+ """
+ Export selected item(s)
+
+ Parameters
+ ----------
+ filename : str, optional
+ If not provided, plugin value will be used.
+ """
+ if self.dataset.selected is not None and len(self.dataset.selected):
+ raise NotImplementedError("dataset export not yet supported")
+ if self.subset.selected is not None and len(self.subset.selected):
+ raise NotImplementedError("subset export not yet supported")
+ if self.table.selected is not None and len(self.table.selected):
+ raise NotImplementedError("table export not yet supported")
+ if self.plot.selected is not None and len(self.plot.selected):
+ raise NotImplementedError("plot export not yet supported")
+ if self.multiselect:
+ raise NotImplementedError("batch export not yet supported")
+
+ if not len(self.viewer.selected):
+ raise ValueError("no viewers selected to export")
+
+ viewer = self.viewer.selected_obj
+ filename = filename if filename is not None else self.filename
+ filetype = self.viewer_format.selected
+
+ # at this point, we can assume only a single figure is selected
+ if len(filename):
+ if not filename.endswith(filetype):
+ filename += f".{filetype}"
+ filename = Path(filename).expanduser()
+ else:
+ filename = None
+
+ if filetype == "mp4":
+ self.save_movie(viewer, filename, filetype)
+ else:
+ self.save_figure(viewer, filename, filetype, show_dialog=show_dialog)
+
+ def vue_export_from_ui(self, *args, **kwargs):
+ try:
+ self.export(show_dialog=True)
+ except Exception as e:
+ self.hub.broadcast(SnackbarMessage(
+ f"Export failed with: {e}", sender=self, color="error"))
+
+ def save_figure(self, viewer, filename=None, filetype="png", show_dialog=False):
+ if filetype == "png":
+ if filename is None or show_dialog:
+ viewer.figure.save_png(str(filename) if filename is not None else None)
+ else:
+ if not filename.parent.exists():
+ raise ValueError(f"Invalid path={filename.parent}")
+
+ # support writing without save dialog
+ # https://github.com/bqplot/bqplot/pull/1397
+ def on_img_received(data):
+ try:
+ with filename.open(mode='bw') as f:
+ f.write(data)
+ except Exception as e:
+ self.hub.broadcast(SnackbarMessage(
+ f"{self.viewer.selected} failed to export to {str(filename)}: {e}",
+ sender=self, color="error"))
+ finally:
+ self.hub.broadcast(SnackbarMessage(
+ f"{self.viewer.selected} exported to {str(filename)}",
+ sender=self, color="success"))
+
+ if viewer.figure._upload_png_callback is not None:
+ raise ValueError("previous png export is still in progress. Wait to complete "
+ "before making another call to save_figure")
+
+ viewer.figure.get_png_data(on_img_received)
+
+ elif filetype == "svg":
+ viewer.figure.save_svg(str(filename) if filename is not None else None)
+
+ @with_spinner('movie_recording')
+ def _save_movie(self, viewer, i_start, i_end, fps, filename, rm_temp_files):
+ # NOTE: All the stuff here has to be in the same thread but
+ # separate from main app thread to work.
+
+ if not self.movie_enabled:
+ if not HAS_OPENCV:
+ raise ImportError("Please install opencv-python")
+ raise ValueError("movie support disabled")
+
+ slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj
+ orig_slice = viewer.slice
+ temp_png_files = []
+ i = i_start
+ video = None
+
+ # TODO: Expose to users?
+ i_step = 1 # Need n_frames check if we allow tweaking
+
+ try:
+ while i <= i_end:
+ if self.movie_interrupt:
+ break
+
+ 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)
+ temp_png_files.append(cur_pngfile)
+ i += i_step
+
+ # Wait for the roundtrip to the frontend to complete.
+ while viewer.figure._upload_png_callback is not None:
+ time.sleep(0.05)
+
+ if not self.movie_interrupt:
+ # Grab frame size.
+ frame_shape = cv2.imread(temp_png_files[0]).shape
+ frame_size = (frame_shape[1], frame_shape[0])
+
+ video = cv2.VideoWriter(filename, cv2.VideoWriter_fourcc(*'mp4v'), fps, frame_size, True) # noqa: E501
+ for cur_pngfile in temp_png_files:
+ video.write(cv2.imread(cur_pngfile))
+ except Exception as err:
+ self.hub.broadcast(SnackbarMessage(
+ f"Error saving {filename}: {err!r}", sender=self, color="error"))
+ finally:
+ cv2.destroyAllWindows()
+ if video:
+ video.release()
+ slice_plg._on_slider_updated({'new': orig_slice})
+
+ if rm_temp_files or self.movie_interrupt:
+ for cur_pngfile in temp_png_files:
+ if os.path.exists(cur_pngfile):
+ os.remove(cur_pngfile)
+
+ if self.movie_interrupt:
+ if os.path.exists(filename):
+ os.remove(filename)
+ self.movie_interrupt = False
+
+ def save_movie(self, viewer, filename, filetype, i_start=None, i_end=None, fps=None,
+ rm_temp_files=True):
+ """Save selected slices as a movie.
+
+ This method creates a PNG file per frame (``._cubeviz_movie_frame_.png``)
+ in the working directory before stitching all the frames into a movie.
+ Please make sure you have sufficient memory for this operation.
+ PNG files are deleted after the movie is created unless otherwise specified.
+ If another PNG file with the same name already exists, it will be silently replaced.
+
+ Parameters
+ ----------
+ i_start, i_end : int or `None`
+ Slices to record; each slice will be a frame in the movie.
+ If not given, it is obtained from plugin inputs.
+ Unlike Python indexing, ``i_end`` is inclusive.
+ Wrapping and reverse indexing are not supported.
+
+ fps : float or `None`
+ Frame rate in frames per second (FPS).
+ If not given, it is obtained from plugin inputs.
+
+ filename : str or `None`
+ Filename for the movie to be recorded. Include path if necessary.
+ If not given, it is obtained from plugin inputs.
+ If another file with the same name already exists, it will be silently replaced.
+
+ filetype : {'mp4', `None`}
+ Currently only MPEG-4 is supported. This keyword is reserved for future support
+ of other format(s).
+
+ rm_temp_files : bool
+ Remove temporary PNG files after movie creation. Default is `True`.
+
+ Returns
+ -------
+ out_filename : str
+ The absolute path to the actual output file.
+
+ """
+ if self.config != "cubeviz":
+ raise NotImplementedError(f"save_movie is not available for config={self.config}")
+
+ if not HAS_OPENCV:
+ raise ImportError("Please install opencv-python to save cube as movie.")
+
+ if filetype != "mp4":
+ raise NotImplementedError(f"filetype={filetype} not supported")
+
+ if viewer.shape is None:
+ raise ValueError("Selected viewer has no display shape.")
+
+ if fps is None:
+ fps = float(self.movie_fps)
+ if fps <= 0:
+ raise ValueError("Invalid frame rate, must be positive non-zero value.")
+
+ # Make sure file does not end up in weird places in standalone mode.
+ if filename is None:
+ raise ValueError("Invalid filename")
+ path = filename.parent
+ if path and not path.exists():
+ raise ValueError(f"Invalid path={path}")
+ elif (not path or str(path).startswith("..")) and os.environ.get("JDAVIZ_START_DIR", ""): # noqa: E501 # pragma: no cover
+ filename = os.environ["JDAVIZ_START_DIR"] / filename
+
+ if i_start is None:
+ i_start = int(self.i_start)
+
+ if i_end is None:
+ i_end = int(self.i_end)
+
+ # No wrapping. Forward only.
+ slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj
+ if i_start < 0: # pragma: no cover
+ i_start = 0
+ max_slice = len(slice_plg.valid_values_sorted) - 1
+ if i_end > max_slice: # pragma: no cover
+ i_end = max_slice
+ if i_end <= i_start:
+ raise ValueError(f"No frames to write: i_start={i_start}, i_end={i_end}")
+
+ filename = str(filename.resolve())
+ threading.Thread(
+ target=lambda: self._save_movie(viewer, i_start, i_end, fps, filename, rm_temp_files)
+ ).start()
+
+ return filename
+
+ def vue_interrupt_recording(self, *args): # pragma: no cover
+ self.movie_interrupt = True
+ # TODO: this will need updating when batch/multiselect support is added
+ self.hub.broadcast(SnackbarMessage(
+ f"Movie recording interrupted by user, {self.filename} will be deleted.",
+ sender=self, color="warning"))
diff --git a/jdaviz/configs/default/plugins/export/export.vue b/jdaviz/configs/default/plugins/export/export.vue
new file mode 100644
index 0000000000..749e5582f9
--- /dev/null
+++ b/jdaviz/configs/default/plugins/export/export.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+ Viewers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ opencv-python required to export to movie
+
+
+
+
+
+ Data
+
+
+
+
+
+ Subsets
+
+
+
+
+
+ Plugin Tables
+
+
+
+
+
+ Plugin Plots
+
+
+
+
+
+
+
+
+
+
+
+
+ stop
+
+
+
+
+ Export
+
+
+
+
+
+
diff --git a/jdaviz/configs/default/plugins/export_plot/__init__.py b/jdaviz/configs/default/plugins/export_plot/__init__.py
deleted file mode 100644
index 580f1fe95e..0000000000
--- a/jdaviz/configs/default/plugins/export_plot/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .export_plot import * # noqa
diff --git a/jdaviz/configs/default/plugins/export_plot/export_plot.py b/jdaviz/configs/default/plugins/export_plot/export_plot.py
deleted file mode 100644
index 176ad84a3c..0000000000
--- a/jdaviz/configs/default/plugins/export_plot/export_plot.py
+++ /dev/null
@@ -1,335 +0,0 @@
-import os
-from pathlib import Path
-
-from glue_jupyter.bqplot.image import BqplotImageView
-from traitlets import Any, Bool, Unicode
-
-from jdaviz.core.custom_traitlets import FloatHandleEmpty, IntHandleEmpty
-from jdaviz.core.events import AddDataMessage, SnackbarMessage
-from jdaviz.core.registries import tray_registry
-from jdaviz.core.template_mixin import PluginTemplateMixin, ViewerSelectMixin, with_spinner
-from jdaviz.core.user_api import PluginUserApi
-
-try:
- import cv2
-except ImportError:
- HAS_OPENCV = False
-else:
- import threading
- import time
- HAS_OPENCV = True
-
-__all__ = ['ExportViewer']
-
-
-@tray_registry('g-export-plot', label="Export Plot")
-class ExportViewer(PluginTemplateMixin, ViewerSelectMixin):
- """
- See the :ref:`Export Plot Plugin Documentation ` for more details.
-
- Only the following attributes and methods are available through the
- :ref:`public plugin API `:
-
- * ``viewer`` (:class:`~jdaviz.core.template_mixin.ViewerSelect`):
- Viewer to select for exporting the figure image.
- * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
- * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
- * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
- * :meth:`save_figure`
- * :meth:`save_movie` (Cubeviz only)
- * `i_start` (Cubeviz only)
- * `i_end` (Cubeviz only)
- * `movie_fps` (Cubeviz only)
- * `movie_filename` (Cubeviz only)
- """
- template_file = __file__, "export_plot.vue"
-
- # For Cubeviz movie.
- i_start = IntHandleEmpty(0).tag(sync=True)
- i_end = IntHandleEmpty(0).tag(sync=True)
-
- # movie_enabled controls whether movie support is enabled via the UI. This is a temporary
- # measure to allow server-installations to disable support for saving movies but setting this
- # traitlet until movie-support is refactored to be sent through the browser instead of saved
- # server-side in python.
- movie_enabled = Bool(True).tag(sync=True)
- movie_fps = FloatHandleEmpty(5.0).tag(sync=True)
- movie_filename = Any("mymovie.mp4").tag(sync=True)
- movie_msg = Unicode("").tag(sync=True)
- movie_recording = Bool(False).tag(sync=True)
- movie_interrupt = Bool(False).tag(sync=True)
-
- @property
- def user_api(self):
- if self.config == "cubeviz":
- return PluginUserApi(self, expose=('viewer', 'save_figure', 'save_movie', 'i_start',
- 'i_end', 'movie_fps', 'movie_filename'))
- return PluginUserApi(self, expose=('viewer', 'save_figure'))
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- if self.config == "cubeviz":
- if HAS_OPENCV:
- self.session.hub.subscribe(self, AddDataMessage, handler=self._on_cubeviz_data_added) # noqa: E501
- else:
- # NOTE: HTML tags do not work here.
- self.movie_msg = 'Please install opencv-python to use this feature.'
-
- if self.app.state.settings.get('server_is_remote', False):
- # when the server is remote, saving thet movie in python would save on the server, not
- # on the user's machine, so movie support in cubeviz should be disabled
- self.movie_enabled = False
-
- def _on_cubeviz_data_added(self, msg):
- # NOTE: This needs revising if we allow loading more than one cube.
- if isinstance(msg.viewer, BqplotImageView):
- if len(msg.data.shape) == 3:
- self.i_end = msg.data.shape[-1] - 1
-
- def save_figure(self, filename=None, filetype=None):
- """
- Save the figure to an image with a provided filename or through an interactive save dialog.
-
- If ``filetype`` is 'png' (or defaults to 'png' based on ``filename``), the interactive save
- dialog will be bypassed (this is not supported for 'svg').
-
- Parameters
- ----------
- filename : str or `None`
- Filename to autopopulate the save dialog.
- filetype : {'png', 'svg', `None`}
- Filetype (PNG or SVG). If `None`, will default based on filename or to PNG.
-
- """
- if filename is not None:
- filename = Path(filename).expanduser()
-
- if filetype is None:
- if filename is not None and filename.suffix:
- filetype = filename.suffix[1:].lower()
- else:
- filetype = "png" # default to png
-
- viewer = self.viewer.selected_obj
-
- if filetype == "png":
- if filename is None:
- viewer.figure.save_png()
- else:
- if not filename.parent.exists():
- raise ValueError(f"Invalid path={filename.parent}")
-
- # support writing without save dialog
- # https://github.com/bqplot/bqplot/pull/1397
- def on_img_received(data):
- with filename.open(mode='bw') as f:
- f.write(data)
-
- if viewer.figure._upload_png_callback is not None:
- raise ValueError("previous png export is still in progress. Wait to complete "
- "before making another call to save_figure")
-
- viewer.figure.get_png_data(on_img_received)
-
- elif filetype == "svg":
- if filename is not None:
- filename = str(filename)
- viewer.figure.save_svg(filename)
-
- else:
- raise NotImplementedError(f"filetype={filetype} not supported")
-
- def vue_save_figure(self, filetype):
- """
- Callback for save figure events in the front end viewer toolbars. Uses
- the bqplot.Figure save methods.
- """
- self.save_figure(filetype=filetype)
-
- @with_spinner('movie_recording')
- def _save_movie(self, i_start, i_end, fps, filename, rm_temp_files):
- # NOTE: All the stuff here has to be in the same thread but
- # separate from main app thread to work.
-
- viewer = self.viewer.selected_obj
- slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj
- orig_slice = viewer.slice
- temp_png_files = []
- i = i_start
- video = None
-
- # TODO: Expose to users?
- i_step = 1 # Need n_frames check if we allow tweaking
-
- try:
- while i <= i_end:
- if self.movie_interrupt:
- break
-
- slice_plg.vue_play_next()
- cur_pngfile = f"._cubeviz_movie_frame_{i}.png"
- self.save_figure(filename=cur_pngfile, filetype="png")
- temp_png_files.append(cur_pngfile)
- i += i_step
-
- # Wait for the roundtrip to the frontend to complete.
- while viewer.figure._upload_png_callback is not None:
- time.sleep(0.05)
-
- if not self.movie_interrupt:
- # Grab frame size.
- frame_shape = cv2.imread(temp_png_files[0]).shape
- frame_size = (frame_shape[1], frame_shape[0])
-
- video = cv2.VideoWriter(filename, cv2.VideoWriter_fourcc(*'mp4v'), fps, frame_size, True) # noqa: E501
- for cur_pngfile in temp_png_files:
- video.write(cv2.imread(cur_pngfile))
- finally:
- cv2.destroyAllWindows()
- if video:
- video.release()
- slice_plg._on_slider_updated({'new': orig_slice})
-
- if rm_temp_files or self.movie_interrupt:
- for cur_pngfile in temp_png_files:
- if os.path.exists(cur_pngfile):
- os.remove(cur_pngfile)
-
- if self.movie_interrupt:
- if os.path.exists(filename):
- os.remove(filename)
- self.movie_interrupt = False
-
- def save_movie(self, i_start=None, i_end=None, fps=None, filename=None, filetype=None,
- rm_temp_files=True):
- """Save selected slices as a movie.
-
- This method creates a PNG file per frame (``._cubeviz_movie_frame_.png``)
- in the working directory before stitching all the frames into a movie.
- Please make sure you have sufficient memory for this operation.
- PNG files are deleted after the movie is created unless otherwise specified.
- If another PNG file with the same name already exists, it will be silently replaced.
-
- Parameters
- ----------
- i_start, i_end : int or `None`
- Slices to record; each slice will be a frame in the movie.
- If not given, it is obtained from plugin inputs.
- Unlike Python indexing, ``i_end`` is inclusive.
- Wrapping and reverse indexing are not supported.
-
- fps : float or `None`
- Frame rate in frames per second (FPS).
- If not given, it is obtained from plugin inputs.
-
- filename : str or `None`
- Filename for the movie to be recorded. Include path if necessary.
- If not given, it is obtained from plugin inputs.
- If another file with the same name already exists, it will be silently replaced.
-
- filetype : {'mp4', `None`}
- Currently only MPEG-4 is supported. This keyword is reserved for future support
- of other format(s).
-
- rm_temp_files : bool
- Remove temporary PNG files after movie creation. Default is `True`.
-
- Returns
- -------
- out_filename : str
- The absolute path to the actual output file.
-
- """
- if self.config != "cubeviz":
- raise NotImplementedError(f"save_movie is not available for config={self.config}")
-
- if not self.movie_enabled:
- # this should never be triggered since this is intended for UI-disabling and the
- # UI section is hidden, but would prevent any JS-hacking
- raise ValueError("save_movie is currently disabled")
-
- if not HAS_OPENCV:
- raise ImportError("Please install opencv-python to save cube as movie.")
-
- if filename is None:
- if self.movie_filename:
- filename = self.movie_filename
- else:
- raise ValueError("Invalid filename.")
-
- filename = Path(filename).expanduser()
-
- if filetype is None:
- if filename.suffix:
- filetype = filename.suffix[1:].lower()
- else:
- filetype = "mp4" # default to MPEG-4
-
- if filetype != "mp4":
- raise NotImplementedError(f"filetype={filetype} not supported")
-
- viewer = self.viewer.selected_obj
- if not isinstance(viewer, BqplotImageView): # Profile viewer in glue-jupyter cannot do this
- raise TypeError(f"Movie for {viewer.__class__.__name__} is not supported.")
- if viewer.shape is None:
- raise ValueError("Selected viewer has no display shape.")
-
- if fps is None:
- fps = float(self.movie_fps)
- if fps <= 0:
- raise ValueError("Invalid frame rate, must be positive non-zero value.")
-
- # Make sure file does not end up in weird places in standalone mode.
- path = filename.parent
- if path and not path.exists():
- raise ValueError(f"Invalid path={path}")
- elif (not path or str(path).startswith("..")) and os.environ.get("JDAVIZ_START_DIR", ""): # noqa: E501 # pragma: no cover
- filename = os.environ["JDAVIZ_START_DIR"] / filename
-
- if i_start is None:
- i_start = int(self.i_start)
-
- if i_end is None:
- i_end = int(self.i_end)
-
- # No wrapping. Forward only.
- slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj
- if i_start < 0: # pragma: no cover
- i_start = 0
- max_slice = len(slice_plg.valid_values_sorted) - 1
- if i_end > max_slice: # pragma: no cover
- i_end = max_slice
- if i_end <= i_start:
- raise ValueError(f"No frames to write: i_start={i_start}, i_end={i_end}")
-
- filename = str(filename.resolve())
- threading.Thread(
- target=lambda: self._save_movie(i_start, i_end, fps, filename, rm_temp_files)
- ).start()
-
- return filename
-
- def vue_save_movie(self, filetype): # pragma: no cover
- """
- Callback for save movie events in the front end viewer toolbars. Uses
- the bqplot.Figure save methods.
- """
- try:
- filename = self.save_movie(filetype=filetype)
- except Exception as err: # pragma: no cover
- self.hub.broadcast(SnackbarMessage(
- f"Error saving {self.movie_filename}: {err!r}", sender=self, color="error"))
- else:
- # Let the user know where we saved the file.
- # NOTE: Because of threading, this will be emitted even as movie as recording.
- self.hub.broadcast(SnackbarMessage(
- f"Movie being saved to {filename} for slices {self.i_start} to {self.i_end}, "
- f"inclusive, at {self.movie_fps} FPS.",
- sender=self, color="success"))
-
- def vue_interrupt_recording(self, *args): # pragma: no cover
- self.movie_interrupt = True
- self.hub.broadcast(SnackbarMessage(
- f"Movie recording interrupted by user, {self.movie_filename} will be deleted.",
- sender=self, color="warning"))
diff --git a/jdaviz/configs/default/plugins/export_plot/export_plot.vue b/jdaviz/configs/default/plugins/export_plot/export_plot.vue
deleted file mode 100644
index a1a5540206..0000000000
--- a/jdaviz/configs/default/plugins/export_plot/export_plot.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-
-
-
-
-
-