class TextViewDialog(QDialog): def __init__(self, parent, text): super().__init__(parent) self.text = text self.setupUi() def setupUi(self): self.resize(640, 480) self.verticalLayout = QVBoxLayout(self) self.textEdit = QPlainTextEdit(self) self.closeButton = QPushButton(self) self.copyButton = QPushButton(self) self.verticalLayout.addWidget(self.textEdit) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setSizeConstraint(QLayout.SetDefaultConstraint) spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.horizontalLayout.addWidget(self.copyButton) self.horizontalLayout.addWidget(self.closeButton) self.verticalLayout.addLayout(self.horizontalLayout) self.closeButton.clicked.connect(self.reject) font = QFont(CONFIG['text_view_dialog_font'], CONFIG['text_view_dialog_font_size']) font.setStyleHint(QFont.Monospace) self.textEdit.setFont(font) self.closeButton.setText('Close') self.copyButton.setText('Copy to clipboard') self.textEdit.setPlainText(self.text) self.copyButton.clicked.connect(self.copy_text) def copy_text(self): clipboard = QApplication.clipboard() clipboard.setText(self.textEdit.toPlainText())
class EditVal_Dialog(QDialog): def __init__(self, parent, init_val): super(EditVal_Dialog, self).__init__(parent) # shortcut save_shortcut = QShortcut(QKeySequence.Save, self) save_shortcut.activated.connect(self.save_triggered) main_layout = QVBoxLayout() self.val_text_edit = QPlainTextEdit() val_str = '' try: val_str = str(init_val) except Exception as e: msg_box = QMessageBox(QMessageBox.Warning, 'Value parsing failed', 'Couldn\'t stringify value', QMessageBox.Ok, self) msg_box.setDefaultButton(QMessageBox.Ok) msg_box.exec_() self.reject() self.val_text_edit.setPlainText(val_str) main_layout.addWidget(self.val_text_edit) button_box = QDialogButtonBox() button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) main_layout.addWidget(button_box) self.setLayout(main_layout) self.resize(450, 300) self.setWindowTitle('edit val') def save_triggered(self): self.accept() def get_val(self): val = self.val_text_edit.toPlainText() try: val = eval(val) except Exception as e: pass return val
class PropertyOperatorDialog(EasyDialog): NAME = _("Property operator") HELP_BODY = _("Example script<br><br>" "propA = - propA <br><br>" "import numpy as np <br>" "propB = np.log10(abs(propA))") sig_start = Signal(str, str) def __init__(self, parent=None): EasyDialog.__init__(self, parent) self.setup_page() def setup_page(self): text = _('Object') geom = ['Point', 'Line', 'Tsurface', 'Gsurface', 'Cube'] self.grabob = self.create_grabob(text, geom=geom) self.layout.addWidget(self.grabob) text = _('Region') default = _("Not available yet") self.region = self.create_lineedit(text, default=default) self.layout.addWidget(self.region) lblScript = QLabel(_('Script')) self.layout.addWidget(lblScript) self.pteScript = QPlainTextEdit() self.layout.addWidget(self.pteScript) action = self.create_action() self.layout.addWidget(action) def apply(self): object_name = self.grabob.lineedit.edit.text() script = self.pteScript.toPlainText() self.sig_start.emit(object_name, script)
class SliceBrowser(QMainWindow): """Navigate between slices of an MRI, CT, etc. image.""" _xy_idx = ( (1, 2), (0, 2), (0, 1), ) def __init__(self, base_image=None, subject=None, subjects_dir=None, verbose=None): """GUI for browsing slices of anatomical images.""" # initialize QMainWindow class super(SliceBrowser, self).__init__() self._verbose = verbose # if bad/None subject, will raise an informative error when loading MRI subject = os.environ.get('SUBJECT') if subject is None else subject subjects_dir = get_subjects_dir(subjects_dir, raise_error=True) self._subject_dir = op.join(subjects_dir, subject) self._load_image_data(base_image=base_image) # GUI design # Main plots: make one plot for each view; sagittal, coronal, axial self._plt_grid = QGridLayout() self._figs = list() for i in range(3): canvas, fig = _make_slice_plot() self._plt_grid.addWidget(canvas, i // 2, i % 2) self._figs.append(fig) self._renderer = _get_renderer(name='Slice Browser', size=(400, 400), bgcolor='w') self._plt_grid.addWidget(self._renderer.plotter, 1, 1) self._set_ras([0., 0., 0.], update_plots=False) self._plot_images() self._configure_ui() def _configure_ui(self): bottom_hbox = self._configure_status_bar() # Put everything together plot_ch_hbox = QHBoxLayout() plot_ch_hbox.addLayout(self._plt_grid) main_vbox = QVBoxLayout() main_vbox.addLayout(plot_ch_hbox) main_vbox.addLayout(bottom_hbox) central_widget = QWidget() central_widget.setLayout(main_vbox) self.setCentralWidget(central_widget) def _load_image_data(self, base_image=None): """Get image data to display and transforms to/from vox/RAS.""" # allows recon-all not to be finished (T1 made in a few minutes) mri_img = 'brain' if op.isfile( op.join(self._subject_dir, 'mri', 'brain.mgz')) else 'T1' self._mri_data, self._vox_ras_t = _load_image( op.join(self._subject_dir, 'mri', f'{mri_img}.mgz')) self._ras_vox_t = np.linalg.inv(self._vox_ras_t) self._voxel_sizes = np.array(self._mri_data.shape) # We need our extents to land the centers of each pixel on the voxel # number. This code assumes 1mm isotropic... img_delta = 0.5 self._img_extents = list([ -img_delta, self._voxel_sizes[idx[0]] - img_delta, -img_delta, self._voxel_sizes[idx[1]] - img_delta ] for idx in self._xy_idx) # ready alternate base image if provided, otherwise use brain/T1 if base_image is None: self._base_data = self._mri_data else: self._base_data, vox_ras_t = _load_image(base_image) if self._mri_data.shape != self._base_data.shape or \ not np.allclose(self._vox_ras_t, vox_ras_t, rtol=1e-6): raise ValueError('Base image is not aligned to MRI, got ' f'Base shape={self._base_data.shape}, ' f'MRI shape={self._mri_data.shape}, ' f'Base affine={vox_ras_t} and ' f'MRI affine={self._vox_ras_t}') if op.exists(op.join(self._subject_dir, 'surf', 'lh.seghead')): self._head = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.seghead')) assert _frame_to_str[self._head['coord_frame']] == 'mri' else: warn('`seghead` not found, using marching cubes on CT for ' 'head plot, use :ref:`mne.bem.make_scalp_surfaces` ' 'to add the scalp surface instead of skull from the CT') self._head = None if op.exists(op.join(self._subject_dir, 'surf', 'lh.pial')): self._lh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.pial')) assert _frame_to_str[self._lh['coord_frame']] == 'mri' self._rh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'rh.pial')) assert _frame_to_str[self._rh['coord_frame']] == 'mri' else: warn('`pial` surface not found, skipping adding to 3D ' 'plot. This indicates the Freesurfer recon-all ' 'has not finished or has been modified and ' 'these files have been deleted.') self._lh = self._rh = None def _plot_images(self): """Use the MRI or CT to make plots.""" # Plot sagittal (0), coronal (1) or axial (2) view self._images = dict(base=list(), cursor_v=list(), cursor_h=list(), bounds=list()) img_min = np.nanmin(self._base_data) img_max = np.nanmax(self._base_data) text_kwargs = dict(fontsize='medium', weight='bold', color='#66CCEE', family='monospace', ha='center', va='center', path_effects=[ patheffects.withStroke(linewidth=4, foreground="k", alpha=0.75) ]) xyz = apply_trans(self._ras_vox_t, self._ras) for axis in range(3): plot_x_idx, plot_y_idx = self._xy_idx[axis] fig = self._figs[axis] ax = fig.axes[0] img_data = np.take(self._base_data, self._current_slice[axis], axis=axis).T self._images['base'].append( ax.imshow(img_data, cmap='gray', aspect='auto', zorder=1, vmin=img_min, vmax=img_max)) img_extent = self._img_extents[axis] # x0, x1, y0, y1 w, h = np.diff(np.array(img_extent).reshape(2, 2), axis=1)[:, 0] self._images['bounds'].append( Rectangle(img_extent[::2], w, h, edgecolor='w', facecolor='none', alpha=0.25, lw=0.5, zorder=1.5)) ax.add_patch(self._images['bounds'][-1]) v_x = (xyz[plot_x_idx], ) * 2 v_y = img_extent[2:4] self._images['cursor_v'].append( ax.plot(v_x, v_y, color='lime', linewidth=0.5, alpha=0.5, zorder=8)[0]) h_y = (xyz[plot_y_idx], ) * 2 h_x = img_extent[0:2] self._images['cursor_h'].append( ax.plot(h_x, h_y, color='lime', linewidth=0.5, alpha=0.5, zorder=8)[0]) # label axes self._figs[axis].text(0.5, 0.05, _IMG_LABELS[axis][0], **text_kwargs) self._figs[axis].text(0.05, 0.5, _IMG_LABELS[axis][1], **text_kwargs) self._figs[axis].axes[0].axis(img_extent) self._figs[axis].canvas.mpl_connect('scroll_event', self._on_scroll) self._figs[axis].canvas.mpl_connect( 'button_release_event', partial(self._on_click, axis=axis)) # add head and brain in mm (convert from m) if self._head is None: logger.info('Using marching cubes on CT for the ' '3D visualization panel') rr, tris = _marching_cubes( np.where(self._base_data < np.quantile(self._base_data, 0.95), 0, 1), [1])[0] rr = apply_trans(self._vox_ras_t, rr) self._renderer.mesh(*rr.T, triangles=tris, color='gray', opacity=0.2, reset_camera=False, render=False) else: self._renderer.mesh(*self._head['rr'].T * 1000, triangles=self._head['tris'], color='gray', opacity=0.2, reset_camera=False, render=False) if self._lh is not None and self._rh is not None: self._renderer.mesh(*self._lh['rr'].T * 1000, triangles=self._lh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._renderer.mesh(*self._rh['rr'].T * 1000, triangles=self._rh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._renderer.set_camera(azimuth=90, elevation=90, distance=300, focalpoint=tuple(self._ras)) # update plots self._draw() self._renderer._update() def _configure_status_bar(self, hbox=None): """Make a bar at the bottom with information in it.""" hbox = QHBoxLayout() if hbox is None else hbox self._intensity_label = QLabel('') # update later hbox.addWidget(self._intensity_label) VOX_label = QLabel('VOX =') self._VOX_textbox = QPlainTextEdit('') # update later self._VOX_textbox.setMaximumHeight(25) self._VOX_textbox.setMaximumWidth(125) self._VOX_textbox.focusOutEvent = self._update_VOX self._VOX_textbox.textChanged.connect(self._check_update_VOX) hbox.addWidget(VOX_label) hbox.addWidget(self._VOX_textbox) RAS_label = QLabel('RAS =') self._RAS_textbox = QPlainTextEdit('') # update later self._RAS_textbox.setMaximumHeight(25) self._RAS_textbox.setMaximumWidth(200) self._RAS_textbox.focusOutEvent = self._update_RAS self._RAS_textbox.textChanged.connect(self._check_update_RAS) hbox.addWidget(RAS_label) hbox.addWidget(self._RAS_textbox) self._update_moved() # update text now return hbox def _update_camera(self, render=False): """Update the camera position.""" self._renderer.set_camera( # needs fix, distance moves when focal point updates distance=self._renderer.plotter.camera.distance * 0.9, focalpoint=tuple(self._ras), reset_camera=False) def _on_scroll(self, event): """Process mouse scroll wheel event to zoom.""" self._zoom(event.step, draw=True) def _zoom(self, sign=1, draw=False): """Zoom in on the image.""" delta = _ZOOM_STEP_SIZE * sign for axis, fig in enumerate(self._figs): xmid = self._images['cursor_v'][axis].get_xdata()[0] ymid = self._images['cursor_h'][axis].get_ydata()[0] xmin, xmax = fig.axes[0].get_xlim() ymin, ymax = fig.axes[0].get_ylim() xwidth = (xmax - xmin) / 2 - delta ywidth = (ymax - ymin) / 2 - delta if xwidth <= 0 or ywidth <= 0: return fig.axes[0].set_xlim(xmid - xwidth, xmid + xwidth) fig.axes[0].set_ylim(ymid - ywidth, ymid + ywidth) if draw: self._figs[axis].canvas.draw() @Slot() def _update_RAS(self, event): """Interpret user input to the RAS textbox.""" text = self._RAS_textbox.toPlainText() ras = self._convert_text(text, 'ras') if ras is not None: self._set_ras(ras) @Slot() def _update_VOX(self, event): """Interpret user input to the RAS textbox.""" text = self._VOX_textbox.toPlainText() ras = self._convert_text(text, 'vox') if ras is not None: self._set_ras(ras) def _convert_text(self, text, text_kind): text = text.replace('\n', '') vals = text.split(',') if len(vals) != 3: vals = text.split(' ') # spaces also okay as in freesurfer vals = [var.lstrip().rstrip() for var in vals] try: vals = np.array([float(var) for var in vals]).reshape(3) except Exception: self._update_moved() # resets RAS label return if text_kind == 'vox': vox = vals ras = apply_trans(self._vox_ras_t, vox) else: assert text_kind == 'ras' ras = vals vox = apply_trans(self._ras_vox_t, ras) wrong_size = any(var < 0 or var > n - 1 for var, n in zip(vox, self._voxel_sizes)) if wrong_size: self._update_moved() # resets RAS label return return ras @property def _ras(self): return self._ras_safe def _set_ras(self, ras, update_plots=True): ras = np.asarray(ras, dtype=float) assert ras.shape == (3, ) msg = ', '.join(f'{x:0.2f}' for x in ras) logger.debug(f'Trying RAS: ({msg}) mm') # clip to valid vox = apply_trans(self._ras_vox_t, ras) vox = np.array([ np.clip(d, 0, self._voxel_sizes[ii] - 1) for ii, d in enumerate(vox) ]) # transform back, make write-only self._ras_safe = apply_trans(self._vox_ras_t, vox) self._ras_safe.flags['WRITEABLE'] = False msg = ', '.join(f'{x:0.2f}' for x in self._ras_safe) logger.debug(f'Setting RAS: ({msg}) mm') if update_plots: self._move_cursors_to_pos() @property def _vox(self): return apply_trans(self._ras_vox_t, self._ras) @property def _current_slice(self): return self._vox.round().astype(int) @Slot() def _check_update_RAS(self): """Check whether the RAS textbox is done being edited.""" if '\n' in self._RAS_textbox.toPlainText(): self._update_RAS(event=None) @Slot() def _check_update_VOX(self): """Check whether the VOX textbox is done being edited.""" if '\n' in self._VOX_textbox.toPlainText(): self._update_VOX(event=None) def _draw(self, axis=None): """Update the figures with a draw call.""" for axis in (range(3) if axis is None else [axis]): self._figs[axis].canvas.draw() def _update_base_images(self, axis=None, draw=False): """Update the base images.""" for axis in range(3) if axis is None else [axis]: img_data = np.take(self._base_data, self._current_slice[axis], axis=axis).T self._images['base'][axis].set_data(img_data) if draw: self._draw(axis) def _update_images(self, axis=None, draw=True): """Update CT and channel images when general changes happen.""" self._update_base_images(axis=axis) if draw: self._draw(axis) def _move_cursors_to_pos(self): """Move the cursors to a position.""" for axis in range(3): x, y = self._vox[list(self._xy_idx[axis])] self._images['cursor_v'][axis].set_xdata([x, x]) self._images['cursor_h'][axis].set_ydata([y, y]) self._zoom(0) # doesn't actually zoom just resets view to center self._update_images(draw=True) self._update_moved() def _show_help(self): """Show the help menu.""" QMessageBox.information( self, 'Help', "Help:\n" "'+'/'-': zoom\nleft/right arrow: left/right\n" "up/down arrow: superior/inferior\n" "left angle bracket/right angle bracket: anterior/posterior") def _key_press_event(self, event): """Execute functions when the user presses a key.""" if event.key() == 'escape': self.close() if event.text() == 'h': self._show_help() if event.text() in ('=', '+', '-'): self._zoom(sign=-2 * (event.text() == '-') + 1, draw=True) # Changing slices if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Comma, QtCore.Qt.Key_Period, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown): ras = np.array(self._ras) if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): ras[2] += 2 * (event.key() == QtCore.Qt.Key_Up) - 1 elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right): ras[0] += 2 * (event.key() == QtCore.Qt.Key_Right) - 1 else: ras[1] += 2 * (event.key() == QtCore.Qt.Key_PageUp or event.key() == QtCore.Qt.Key_Period) - 1 self._set_ras(ras) def _on_click(self, event, axis): """Move to view on MRI and CT on click.""" if event.inaxes is self._figs[axis].axes[0]: # Data coordinates are voxel coordinates pos = (event.xdata, event.ydata) logger.info(f'Clicked {"XYZ"[axis]} ({axis}) axis at pos {pos}') xyz = self._vox xyz[list(self._xy_idx[axis])] = pos logger.debug(f'Using voxel {list(xyz)}') ras = apply_trans(self._vox_ras_t, xyz) self._set_ras(ras) def _update_moved(self): """Update when cursor position changes.""" self._RAS_textbox.setPlainText( '{:.2f}, {:.2f}, {:.2f}'.format(*self._ras)) self._VOX_textbox.setPlainText( '{:3d}, {:3d}, {:3d}'.format(*self._current_slice)) self._intensity_label.setText('intensity = {:.2f}'.format( self._base_data[tuple(self._current_slice)])) @safe_event def closeEvent(self, event): """Clean up upon closing the window.""" self._renderer.plotter.close() self.close()
class MOSVizViewer(DataViewer): LABEL = "MOSViz Viewer" window_closed = Signal() _toolbar_cls = MOSViewerToolbar def __init__(self, session, parent=None): super(MOSVizViewer, self).__init__(session, parent=parent) self.load_ui() # Define some data containers self.filepath = None self.savepath = None self.data_idx = None self.comments = False self.textChangedAt = None self.mask = None self.catalog = None self.current_row = None self._specviz_instance = None self._loaded_data = {} self._primary_data = None self._layer_view = SimpleLayerWidget(parent=self) self._layer_view.layer_combo.currentIndexChanged.connect( self._selection_changed) def load_ui(self): """ Setup the MOSView viewer interface. """ self.central_widget = QWidget(self) path = os.path.join(UI_DIR, 'mos_widget.ui') loadUi(path, self.central_widget) self.image_widget = DrawableImageWidget() self.spectrum2d_widget = MOSImageWidget() self.spectrum1d_widget = Line1DWidget() # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing # and we control the sharing later by setting .sharex and .sharey on the # helper self.spectrum2d_spectrum1d_share = SharedAxisHelper( self.spectrum2d_widget._axes, self.spectrum1d_widget._axes) self.spectrum2d_image_share = SharedAxisHelper( self.spectrum2d_widget._axes, self.image_widget._axes) # We only need to set the image widget to keep the same aspect ratio # since the two other viewers don't require square pixels, so the axes # should not change shape. self.image_widget._axes.set_adjustable('datalim') self.meta_form_layout = self.central_widget.meta_form_layout self.meta_form_layout.setFieldGrowthPolicy( self.meta_form_layout.ExpandingFieldsGrow) self.central_widget.left_vertical_splitter.insertWidget( 0, self.image_widget) self.central_widget.right_vertical_splitter.addWidget( self.spectrum2d_widget) self.central_widget.right_vertical_splitter.addWidget( self.spectrum1d_widget) # Set the splitter stretch factors self.central_widget.left_vertical_splitter.setStretchFactor(0, 1) self.central_widget.left_vertical_splitter.setStretchFactor(1, 8) self.central_widget.right_vertical_splitter.setStretchFactor(0, 1) self.central_widget.right_vertical_splitter.setStretchFactor(1, 2) self.central_widget.horizontal_splitter.setStretchFactor(0, 1) self.central_widget.horizontal_splitter.setStretchFactor(1, 2) # Keep the left and right splitters in sync otherwise the axes don't line up self.central_widget.left_vertical_splitter.splitterMoved.connect( self._left_splitter_moved) self.central_widget.right_vertical_splitter.splitterMoved.connect( self._right_splitter_moved) # Set the central widget self.setCentralWidget(self.central_widget) # Define the options widget self._options_widget = OptionsWidget() @avoid_circular def _right_splitter_moved(self, *args, **kwargs): sizes = self.central_widget.right_vertical_splitter.sizes() self.central_widget.left_vertical_splitter.setSizes(sizes) @avoid_circular def _left_splitter_moved(self, *args, **kwargs): sizes = self.central_widget.left_vertical_splitter.sizes() self.central_widget.right_vertical_splitter.setSizes(sizes) def setup_connections(self): """ Connects gui elements to event calls. """ # Connect the selection event for the combo box to what's displayed self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self.load_selection(self.catalog[ind])) self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self._set_navigation(ind)) # Connect the specviz button if SpecVizViewer is not None: self.toolbar.open_specviz.triggered.connect( lambda: self._open_in_specviz()) else: self.toolbar.open_specviz.setDisabled(True) # Connect previous and forward buttons self.toolbar.cycle_next_action.triggered.connect( lambda: self._set_navigation(self.toolbar.source_select. currentIndex() + 1)) # Connect previous and previous buttons self.toolbar.cycle_previous_action.triggered.connect( lambda: self._set_navigation(self.toolbar.source_select. currentIndex() - 1)) # Connect the toolbar axes setting actions self.toolbar.lock_x_action.triggered.connect( lambda state: self.set_locked_axes(x=state)) self.toolbar.lock_y_action.triggered.connect( lambda state: self.set_locked_axes(y=state)) def options_widget(self): return self._options_widget def initialize_toolbar(self): """ Initialize the custom toolbar for the MOSViz viewer. """ from glue.config import viewer_tool self.toolbar = self._toolbar_cls(self) for tool_id in self.tools: mode_cls = viewer_tool.members[tool_id] mode = mode_cls(self) self.toolbar.add_tool(mode) self.addToolBar(self.toolbar) self.setup_connections() def register_to_hub(self, hub): super(MOSVizViewer, self).register_to_hub(hub) def has_data_or_subset(x): if x.sender is self._primary_data: return True elif isinstance(x.sender, Subset) and x.sender.data is self._primary_data: return True else: return False hub.subscribe(self, msg.SubsetCreateMessage, handler=self._add_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetUpdateMessage, handler=self._update_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetDeleteMessage, handler=self._remove_subset, filter=has_data_or_subset) hub.subscribe(self, msg.DataUpdateMessage, handler=self._update_data, filter=has_data_or_subset) def add_data(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [ data.get_component(cid) for cid in data.visible_components ] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Make sure the loaders and column names are correct result = confirm_loaders_and_column_names(data) if not result: return False self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_subset(self, subset): """ Processes subset messages from the central communication hub. Parameters ---------- subset : Subset object. """ self._layer_view.refresh() index = self._layer_view.layer_combo.findData(subset) self._layer_view.layer_combo.setCurrentIndex(index) return True def _update_data(self, message): """ Update data message. Parameters ---------- message : :class:`glue.core.message.Message` Data message object. """ self._layer_view.refresh() def _add_subset(self, message): """ Add subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() def _update_subset(self, message): """ Update subset message. Parameters ---------- message : :class:`glue.core.message.Message` Update message object. """ self._layer_view.refresh() self._unpack_selection(message.subset) def _remove_subset(self, message): """ Remove subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() self._unpack_selection(message.subset.data) def _selection_changed(self): self._unpack_selection(self._layer_view.layer_combo.currentData()) def _unpack_selection(self, data): """ Interprets the :class:`glue.core.data.Data` object by decomposing the data elements, extracting relevant data, and recomposing a package-agnostic dictionary object containing the relevant data. Parameters ---------- data : :class:`glue.core.data.Data` Glue data object to decompose. """ mask = None if isinstance(data, Subset): try: mask = data.to_mask() except IncompatibleAttribute: return if not np.any(mask): return data = data.data self.mask = mask # Clear the table self.catalog = Table() self.catalog.meta = data.meta self.comments = False col_names = data.components for att in col_names: cid = data.id[att] component = data.get_component(cid) if component.categorical: comp_labels = component.labels[mask] if comp_labels.ndim > 1: comp_labels = comp_labels[0] if str(att) in ["comments", "flag"]: self.comments = True elif str(att) in ['spectrum1d', 'spectrum2d', 'cutout']: self.filepath = component._load_log.path path = '/'.join(component._load_log.path.split('/')[:-1]) self.catalog[str(att)] = [ os.path.join(path, x) for x in comp_labels ] else: self.catalog[str(att)] = comp_labels else: comp_data = component.data[mask] if comp_data.ndim > 1: comp_data = comp_data[0] self.catalog[str(att)] = comp_data if len(self.catalog) > 0: if not self.comments: self.comments = self._load_comments(data.label) #Returns bool else: self._data_collection_index(data.label) self._get_save_path() # Update gui elements self._update_navigation(select=0) def _update_navigation(self, select=0): """ Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the appropriate source `id`s from the MOS catalog. """ if self.toolbar is None: return self.toolbar.source_select.blockSignals(True) self.toolbar.source_select.clear() if len(self.catalog) > 0 and 'id' in self.catalog.colnames: self.toolbar.source_select.addItems(self.catalog['id'][:]) self.toolbar.source_select.setCurrentIndex(select) self.toolbar.source_select.blockSignals(False) self.toolbar.source_select.currentIndexChanged.emit(select) def _set_navigation(self, index): if len(self.catalog) < index: return if 0 <= index < self.toolbar.source_select.count(): self.toolbar.source_select.setCurrentIndex(index) if index <= 0: self.toolbar.cycle_previous_action.setDisabled(True) else: self.toolbar.cycle_previous_action.setDisabled(False) if index >= self.toolbar.source_select.count() - 1: self.toolbar.cycle_next_action.setDisabled(True) else: self.toolbar.cycle_next_action.setDisabled(False) def _open_in_specviz(self): _specviz_instance = self.session.application.new_data_viewer( SpecVizViewer) spec1d_data = self._loaded_data['spectrum1d'] spec_data = Spectrum1DRef( data=spec1d_data.get_component(spec1d_data.id['Flux']).data, dispersion=spec1d_data.get_component( spec1d_data.id['Wavelength']).data, uncertainty=StdDevUncertainty( spec1d_data.get_component(spec1d_data.id['Uncertainty']).data), unit="", name=self.current_row['id'], wcs=WCS(spec1d_data.header)) _specviz_instance.open_data(spec_data) def load_selection(self, row): """ Processes a row in the MOS catalog by first loading the data set, updating the stored data components, and then rendering the data on the visible MOSViz viewer plots. Parameters ---------- row : :class:`astropy.table.Row` A row object representing a row in the MOS catalog. Each key should be a column name. """ self.current_row = row # Get loaders loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"] ["spectrum1d"]] loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"] ["spectrum2d"]] loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]] # Get column names colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"] colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"] colname_cutout = self.catalog.meta["special_columns"]["cutout"] spec1d_data = loader_spectrum1d(row[colname_spectrum1d]) spec2d_data = loader_spectrum2d(row[colname_spectrum2d]) self._update_data_components(spec1d_data, key='spectrum1d') self._update_data_components(spec2d_data, key='spectrum2d') basename = os.path.basename(row[colname_cutout]) if basename == "None": self.render_data(row, spec1d_data, spec2d_data, None) else: image_data = loader_cutout(row[colname_cutout]) self._update_data_components(image_data, key='cutout') self.render_data(row, spec1d_data, spec2d_data, image_data) def _update_data_components(self, data, key): """ Update the data components that act as containers for the displayed data in the MOSViz viewer. This obviates the need to keep creating new data components. Parameters ---------- data : :class:`glue.core.data.Data` Data object to replace within the component. key : str References the particular data set type. """ cur_data = self._loaded_data.get(key, None) if cur_data is None: self._loaded_data[key] = data self.session.data_collection.append(data) else: cur_data.update_values_from_data(data) def render_data(self, row, spec1d_data=None, spec2d_data=None, image_data=None): """ Render the updated data sets in the individual plot widgets within the MOSViz viewer. """ self._check_unsaved_comments() if spec1d_data is not None: spectrum1d_x = spec1d_data[spec1d_data.id['Wavelength']] spectrum1d_y = spec1d_data[spec1d_data.id['Flux']] spectrum1d_yerr = spec1d_data[spec1d_data.id['Uncertainty']] self.spectrum1d_widget.set_data(x=spectrum1d_x, y=spectrum1d_y, yerr=spectrum1d_yerr) # Try to retrieve the wcs information try: flux_unit = spec1d_data.header.get('BUNIT', 'Jy').lower() flux_unit = flux_unit.replace('counts', 'count') flux_unit = u.Unit(flux_unit) except ValueError: flux_unit = u.Unit("Jy") try: disp_unit = spec1d_data.header.get('CUNIT1', 'Angstrom').lower() disp_unit = u.Unit(disp_unit) except ValueError: disp_unit = u.Unit("Angstrom") self.spectrum1d_widget.axes.set_xlabel( "Wavelength [{}]".format(disp_unit)) self.spectrum1d_widget.axes.set_ylabel( "Flux [{}]".format(flux_unit)) if image_data is not None: wcs = image_data.coords.wcs self.image_widget.set_image(image_data.get_component( image_data.id['Flux']).data, wcs=wcs, interpolation='none', origin='lower') self.image_widget.axes.set_xlabel("Spatial X") self.image_widget.axes.set_ylabel("Spatial Y") # Add the slit patch to the plot ra = row[self.catalog.meta["special_columns"] ["slit_ra"]] * u.degree dec = row[self.catalog.meta["special_columns"] ["slit_dec"]] * u.degree slit_width = row[self.catalog.meta["special_columns"] ["slit_width"]] slit_length = row[self.catalog.meta["special_columns"] ["slit_length"]] skycoord = SkyCoord(ra, dec, frame='fk5') xp, yp = skycoord.to_pixel(wcs) scale = np.sqrt(proj_plane_pixel_area(wcs)) * 3600. dx = slit_width / scale dy = slit_length / scale self.image_widget.draw_rectangle(x=xp, y=yp, width=dx, height=dy) self.image_widget._redraw() else: self.image_widget.setVisible(False) # Plot the 2D spectrum data last because by then we can make sure that # we set up the extent of the image appropriately if the cutout and the # 1D spectrum are present so that the axes can be locked. if spec2d_data is not None: wcs = spec2d_data.coords.wcs xp2d = np.arange(spec2d_data.shape[1]) yp2d = np.repeat(0, spec2d_data.shape[1]) spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world( xp2d, yp2d) x_min = spectrum2d_disp.min() x_max = spectrum2d_disp.max() if image_data is None: y_min = -0.5 y_max = spec2d_data.shape[0] - 0.5 else: y_min = yp - dy / 2. y_max = yp + dy / 2. extent = [x_min, x_max, y_min, y_max] self.spectrum2d_widget.set_image(image=spec2d_data.get_component( spec2d_data.id['Flux']).data, interpolation='none', aspect='auto', extent=extent, origin='lower') self.spectrum2d_widget.axes.set_xlabel("Wavelength") self.spectrum2d_widget.axes.set_ylabel("Spatial Y") self.spectrum2d_widget._redraw() # Clear the meta information widget # NOTE: this process is inefficient for i in range(self.meta_form_layout.count()): wid = self.meta_form_layout.itemAt(i).widget() label = self.meta_form_layout.labelForField(wid) if label is not None: label.deleteLater() wid.deleteLater() # Repopulate the form layout # NOTE: this process is inefficient for col in row.colnames: if col.lower() not in ["comments", "flag"]: line_edit = QLineEdit(str(row[col]), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow(col, line_edit) # Set up comment and flag input/display boxes if self.comments: if self.savepath is not None: if self.savepath == -1: line_edit = QLineEdit( os.path.basename("Not Saving to File."), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) else: line_edit = QLineEdit(os.path.basename(self.savepath), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) self.input_flag = QLineEdit(self.get_flag(), self.central_widget.meta_form_widget) self.input_flag.textChanged.connect(self._text_changed) self.input_flag.setStyleSheet( "background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Flag", self.input_flag) self.input_comments = QPlainTextEdit( self.get_comment(), self.central_widget.meta_form_widget) self.input_comments.textChanged.connect(self._text_changed) self.input_comments.setStyleSheet( "background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Comments", self.input_comments) self.input_save = QPushButton('Save', self.central_widget.meta_form_widget) self.input_save.clicked.connect(self.update_comments) self.input_save.setDefault(True) self.input_refresh = QPushButton( 'Reload', self.central_widget.meta_form_widget) self.input_refresh.clicked.connect(self.refresh_comments) self.meta_form_layout.addRow(self.input_save, self.input_refresh) @defer_draw def set_locked_axes(self, x=None, y=None): # Here we only change the setting if x or y are not None # since if set_locked_axes is called with eg. x=True, then # we shouldn't change the y setting. if x is not None: self.spectrum2d_spectrum1d_share.sharex = x if y is not None: self.spectrum2d_image_share.sharey = y self.spectrum1d_widget._redraw() self.spectrum2d_widget._redraw() self.image_widget._redraw() def layer_view(self): return self._layer_view def _text_changed(self): if self.textChangedAt is None: i = self.toolbar.source_select.currentIndex() self.textChangedAt = self._index_hash(i) def _check_unsaved_comments(self): if self.textChangedAt is None: return #Nothing to be changed i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) if self.textChangedAt == i: self.textChangedAt = None return #This is a refresh info = "Comments or flags changed but were not saved. Would you like to save them?" reply = QMessageBox.question(self, '', info, QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.update_comments(True) self.textChangedAt = None def _data_collection_index(self, label): idx = -1 for i, l in enumerate(self.session.data_collection): if l.label == label: idx = i break if idx == -1: return -1 self.data_idx = idx return idx def _index_hash(self, i): """Local selection index -> Table index""" if self.mask is not None: size = self.mask.size temp = np.arange(size) return temp[self.mask][i] else: return i def _id_to_index_hash(self, ID, l): """Object Name -> Table index""" for i, name in enumerate(l): if name == ID: return i return None def get_comment(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("comments") return comp._categorical_data[i] def get_flag(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("flag") return comp._categorical_data[i] def send_NumericalDataChangedMessage(self): idx = self.data_idx data = self.session.data_collection[idx] data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments")) def refresh_comments(self): self.input_flag.setText(self.get_flag()) self.input_comments.setPlainText(self.get_comment()) self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") self.textChangedAt = None def _get_save_path(self): """ Try to get save path from other MOSVizViewer instances """ for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.savepath is not None: if v.data_idx == self.data_idx: self.savepath = v.savepath break def _setup_save_path(self): """ Prompt the user for a file to save comments and flags into. """ fail = True success = False info = "Where would you like to save comments and flags?" option = pick_item( [0, 1], [os.path.basename(self.filepath), "New MOSViz Table file"], label=info, title="Comment Setup") if option == 0: self.savepath = self.filepath elif option == 1: dirname = os.path.dirname(self.filepath) path = compat.getsavefilename(caption="New MOSViz Table File", basedir=dirname, filters="*.txt")[0] if path == "": return fail self.savepath = path else: return fail for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.data_idx == self.data_idx: v.savepath = self.savepath self._layer_view.refresh() return success def update_comments(self, pastSelection=False): """ Process comment and flag changes and save to file. Parameters ---------- pastSelection : bool True when updating past selections. Used when user forgets to save. """ if self.input_flag.text() == "": self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") return i = None try: i = int(self.input_flag.text()) except ValueError: self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") info = QMessageBox.information(self, "Status:", "Flag must be an int!") return self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") idx = self.data_idx if pastSelection: i = self.textChangedAt self.textChangedAt = None else: i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) data = self.session.data_collection[idx] comp = data.get_component("comments") comp._categorical_data.flags.writeable = True comp._categorical_data[i] = self.input_comments.toPlainText() comp = data.get_component("flag") comp._categorical_data.flags.writeable = True comp._categorical_data[i] = self.input_flag.text() self.send_NumericalDataChangedMessage() self.write_comments() self.textChangedAt = None def _load_comments(self, label): """ Populate the comments and flag columns. Attempt to load comments from file. Parameters ---------- label : str The label of the data in session.data_collection. """ #Make sure its the right data #(beacuse subset data is masked) idx = self._data_collection_index(label) if idx == -1: return False data = self.session.data_collection[idx] #Fill in default comments: length = data.shape[0] new_comments = np.array(["" for i in range(length)], dtype=object) new_flags = np.array(["0" for i in range(length)], dtype=object) #Fill in any saved comments: meta = data.meta obj_names = data.get_component("id")._categorical_data if "MOSViz_comments" in meta.keys(): try: comments = meta["MOSViz_comments"] for key in comments.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = comments[key] new_comments[index] = line except Exception as e: print("MOSViz Comment Load Failed: ", e) if "MOSViz_flags" in meta.keys(): try: flags = meta["MOSViz_flags"] for key in flags.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = flags[key] new_flags[index] = line except Exception as e: print("MOSViz Flag Load Failed: ", e) #Send to DC data.add_component(CategoricalComponent(new_flags, "flag"), "flag") data.add_component(CategoricalComponent(new_comments, "comments"), "comments") return True def write_comments(self): """ Setup save file. Write comments and flags to file """ if self.savepath is None: fail = self._setup_save_path() if fail: return if self.savepath == -1: return #Do not save to file option idx = self.data_idx data = self.session.data_collection[idx] save_comments = data.get_component("comments")._categorical_data save_flag = data.get_component("flag")._categorical_data obj_names = data.get_component("id")._categorical_data fn = self.savepath folder = os.path.dirname(fn) t = astropy_table.data_to_astropy_table(data) #Check if load and save dir paths match temp = os.path.dirname(self.filepath) if not os.path.samefile(folder, temp): t['spectrum1d'].flags.writeable = True t['spectrum2d'].flags.writeable = True t['cutout'].flags.writeable = True for i in range(len(t)): t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i]) t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i]) t['cutout'][i] = os.path.abspath(t['cutout'][i]) try: t.remove_column("comments") t.remove_column("flag") keys = t.meta.keys() if "MOSViz_comments" in keys: t.meta.pop("MOSViz_comments") if "MOSViz_flags" in keys: t.meta.pop("MOSViz_flags") comments = OrderedDict() flags = OrderedDict() for i, line in enumerate(save_comments): if line != "": line = line.replace("\n", " ") key = str(obj_names[i]) comments[key] = line for i, line in enumerate(save_flag): if line != "0" and line != "": line = com.replace("\n", " ") key = str(obj_names[i]) flags[key] = line if len(comments) > 0: t.meta["MOSViz_comments"] = comments if len(flags) > 0: t.meta["MOSViz_flags"] = flags t.write(fn, format="ascii.ecsv", overwrite=True) except Exception as e: print("Comment write failed:", e) def closeEvent(self, event): """ Clean up the extraneous data components created when opening the MOSViz viewer by overriding the parent class's close event. """ super(MOSVizViewer, self).closeEvent(event) for data in self._loaded_data.values(): self.session.data_collection.remove(data)
class Terminal(QWidget): sigRunScript = Signal(str) def __init__(self, parent=None, options_button=None): QWidget.__init__(self, parent) self.setup_mainwidget() btn_layout = QHBoxLayout() for btn in self.setup_buttons(): btn.setIconSize(QSize(16, 16)) btn_layout.addWidget(btn) if options_button: btn_layout.addStretch() btn_layout.addWidget(options_button, Qt.AlignRight) layout = create_plugin_layout(btn_layout, self.mainwidget) self.setLayout(layout) def setup_mainwidget(self): lblRunFile = QLabel('Run File') self.lineRunFile = QLineEdit() lblOpenFile = QLabel('Open File') self.lineOpenFile = QLineEdit() fbox = QFormLayout() fbox.addRow(lblRunFile, self.lineRunFile) fbox.addRow(lblOpenFile, self.lineOpenFile) text = "# myprint(self.database) \n" +\ "# myprint(self.treebase)" self.code_view = QPlainTextEdit(text, self) # font = QFont() # font.setFamily(_fromUtf8("FreeMono")) # self.code_view.setFont(font) vbox = QVBoxLayout() vbox.addLayout(fbox) vbox.addWidget(self.code_view) self.mainwidget = QWidget(self) self.mainwidget.setLayout(vbox) # connect syntax highlighter self.pyhigh = PythonHighlighter(self.code_view.document()) self.code_view.textChanged.connect(self.highlightWhileTyping) def setup_buttons(self): # TODO how to fix this bug? # shortcut in plugin conflicts with shortcut in mainwindow # QAction::eventFilter: Ambiguous shortcut overload: Ctrl+O openfile_btn = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Open file'), shortcut="Ctrl+O", triggered=self.open_file) savefile_btn = create_toolbutton(self, icon=ima.icon('filesave'), tip=_('Save to new file'), shortcut="Ctrl+S", triggered=self.save_file) runfile_btn = create_toolbutton(self, icon=ima.icon('run_file'), tip=_('Run code in file'), triggered=self.run_file) run_btn = create_toolbutton(self, icon=ima.icon('run'), tip=_('Run code in view'), shortcut="Ctrl+R", triggered=self.emit_script) help_btn = create_toolbutton_help(self, triggered=self.show_help) buttons = (openfile_btn, savefile_btn, runfile_btn, run_btn, help_btn) return buttons def show_help(self): QMessageBox.information( self, _('How to use'), _("The project console provides API to the project.<br>" "For example, try run myprint(self.database).<br>" "Due to Cython compiler, use myprint() for print().<br>" "<b>TODO</b> add more.<br>")) def highlightWhileTyping(self): # Instantiate class PythonHighlighter everytime text is changed # RecursionError: maximum recursion depth exceeded... # text = self.code_view.toPlainText() # highlight = PythonHighlighter(self.code_view.document()) # self.code_view.setPlainText(text) text = self.code_view.toPlainText() self.pyhigh.highlightBlock(text) def run_file(self): """Run a script file without open it. Assume edited in a user-favorite text editor. """ fn = self.lineRunFile.text() text = open(fn).read() self.sigRunScript.emit(text) def open_file(self): if hasattr(self, 'workdir'): workdir = self.workdir else: workdir = os.getcwd() fn = QFileDialog.getOpenFileName(self, 'Open File', workdir) # If hit Cancel at dialog, fn is string of length zero. # If still go ahead open(fn), receive FileNotFoundError. if len(fn) > 0: # Windows returns tuple fn = fn[0] if isinstance(fn, tuple) else fn self.lineOpenFile.setText(fn) text = open(fn).read() self.code_view.setPlainText(text) def save_file(self): if hasattr(self, 'workdir'): workdir = self.workdir else: workdir = os.getcwd() fn = QFileDialog.getSaveFileName(self, 'Save File', workdir) if len(fn) > 0: text = self.code_view.toPlainText() with open(fn, 'w') as f: f.write(text) def emit_script(self): """ Send script text to parent to run. """ text = self.code_view.toPlainText() print2logger = 'from ezcad.utils.functions import myprint \n' text = print2logger + text self.sigRunScript.emit(text) def run_script(self): """ Deprecated run at local with limited data access""" text = self.code_view.toPlainText() try: eval(text) except SyntaxError: exec(text) def set_work_dir(self, workdir): self.workdir = workdir
class MOSVizViewer(DataViewer): LABEL = "MOSViz Viewer" window_closed = Signal() _toolbar_cls = MOSViewerToolbar tools = [] subtools = [] def __init__(self, session, parent=None): super(MOSVizViewer, self).__init__(session, parent=parent) self.slit_controller = SlitController(self) self.load_ui() # Define some data containers self.filepath = None self.savepath = None self.data_idx = None self.comments = False self.textChangedAt = None self.mask = None self.cutout_wcs = None self.level2_data = None self.spec2d_data = None self.catalog = None self.current_row = None self._specviz_instance = None self._loaded_data = {} self._primary_data = None self._layer_view = SimpleLayerWidget(parent=self) self._layer_view.layer_combo.currentIndexChanged.connect(self._selection_changed) self.resize(800, 600) self.image_viewer_hidden = False def load_ui(self): """ Setup the MOSView viewer interface. """ self.central_widget = QWidget(self) path = os.path.join(UI_DIR, 'mos_widget.ui') loadUi(path, self.central_widget) self.image_widget = DrawableImageWidget(slit_controller=self.slit_controller) self.spectrum2d_widget = Spectrum2DWidget() self._specviz_viewer = Workspace() self._specviz_viewer.add_plot_window() self.spectrum1d_widget = self._specviz_viewer.current_plot_window self.spectrum1d_widget.plot_widget.getPlotItem().layout.setContentsMargins(45, 0, 25, 0) # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing # and we control the sharing later by setting .sharex and .sharey on the # helper self.spectrum2d_image_share = SharedAxisHelper(self.spectrum2d_widget._axes, self.image_widget._axes) # We only need to set the image widget to keep the same aspect ratio # since the two other viewers don't require square pixels, so the axes # should not change shape. self.image_widget._axes.set_adjustable('datalim') self.meta_form_layout = self.central_widget.meta_form_layout self.meta_form_layout.setFieldGrowthPolicy(self.meta_form_layout.ExpandingFieldsGrow) self.central_widget.left_vertical_splitter.insertWidget(0, self.image_widget) self.central_widget.right_vertical_splitter.addWidget(self.spectrum2d_widget) self.central_widget.right_vertical_splitter.addWidget(self.spectrum1d_widget.widget()) # Set the splitter stretch factors self.central_widget.left_vertical_splitter.setStretchFactor(0, 1) self.central_widget.left_vertical_splitter.setStretchFactor(1, 8) self.central_widget.right_vertical_splitter.setStretchFactor(0, 1) self.central_widget.right_vertical_splitter.setStretchFactor(1, 2) self.central_widget.horizontal_splitter.setStretchFactor(0, 1) self.central_widget.horizontal_splitter.setStretchFactor(1, 2) # Keep the left and right splitters in sync otherwise the axes don't line up self.central_widget.left_vertical_splitter.splitterMoved.connect(self._left_splitter_moved) self.central_widget.right_vertical_splitter.splitterMoved.connect(self._right_splitter_moved) # Set the central widget self.setCentralWidget(self.central_widget) self.central_widget.show() # Define the options widget self._options_widget = OptionsWidget() def show(self, *args, **kwargs): super(MOSVizViewer, self).show(*args, **kwargs) # Trigger a sync between the splitters self._left_splitter_moved() if self.image_viewer_hidden: self.image_widget.hide() else: self.image_widget.show() @avoid_circular def _right_splitter_moved(self, *args, **kwargs): if self.image_widget.isHidden(): return sizes = self.central_widget.right_vertical_splitter.sizes() if sizes == [0, 0]: sizes = [230, 230] self.central_widget.left_vertical_splitter.setSizes(sizes) @avoid_circular def _left_splitter_moved(self, *args, **kwargs): if self.image_widget.isHidden(): return sizes = self.central_widget.left_vertical_splitter.sizes() if sizes == [0, 0]: sizes = [230, 230] self.central_widget.right_vertical_splitter.setSizes(sizes) def setup_connections(self): """ Connects gui elements to event calls. """ # Connect the selection event for the combo box to what's displayed self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self.load_selection(self.catalog[ind])) self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self._set_navigation(ind)) # Connect the exposure selection event self.toolbar.exposure_select.currentIndexChanged[int].connect( lambda ind: self.load_exposure(ind)) self.toolbar.exposure_select.currentIndexChanged[int].connect( lambda ind: self._set_exposure_navigation(ind)) # Connect the specviz button if SpecvizDataViewer is not None: self.toolbar.open_specviz.triggered.connect( lambda: self._open_in_specviz()) else: self.toolbar.open_specviz.setDisabled(True) # Connect slit previous and next buttons self.toolbar.cycle_next_action.triggered.connect( lambda: self._set_navigation( self.toolbar.source_select.currentIndex() + 1)) self.toolbar.cycle_previous_action.triggered.connect( lambda: self._set_navigation( self.toolbar.source_select.currentIndex() - 1)) # Connect exposure previous and next buttons self.toolbar.exposure_next_action.triggered.connect( lambda: self._set_exposure_navigation( self.toolbar.exposure_select.currentIndex() + 1)) self.toolbar.exposure_previous_action.triggered.connect( lambda: self._set_exposure_navigation( self.toolbar.exposure_select.currentIndex() - 1)) # Connect the toolbar axes setting actions self.toolbar.lock_x_action.triggered.connect( lambda state: self.set_locked_axes(x=state)) self.toolbar.lock_y_action.triggered.connect( lambda state: self.set_locked_axes(y=state)) def options_widget(self): return self._options_widget def initialize_toolbar(self): """ Initialize the custom toolbar for the MOSViz viewer. """ from glue.config import viewer_tool self.toolbar = self._toolbar_cls(self) for tool_id in self.tools: mode_cls = viewer_tool.members[tool_id] mode = mode_cls(self) self.toolbar.add_tool(mode) self.addToolBar(self.toolbar) self.setup_connections() def register_to_hub(self, hub): super(MOSVizViewer, self).register_to_hub(hub) def has_data_or_subset(x): if x.sender is self._primary_data: return True elif isinstance(x.sender, Subset) and x.sender.data is self._primary_data: return True else: return False hub.subscribe(self, msg.SubsetCreateMessage, handler=self._add_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetUpdateMessage, handler=self._update_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetDeleteMessage, handler=self._remove_subset, filter=has_data_or_subset) hub.subscribe(self, msg.DataUpdateMessage, handler=self._update_data, filter=has_data_or_subset) def add_data(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [data.get_component(cid) for cid in data.main_components] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Make sure the loaders and column names are correct result = confirm_loaders_and_column_names(data) if not result: return False self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_data_for_testing(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [data.get_component(cid) for cid in data.main_components] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical(self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Block of code to bypass the loader_selection gui ######################################################### if 'loaders' not in data.meta: data.meta['loaders'] = {} # Deimos data data.meta['loaders']['spectrum1d'] = "DEIMOS 1D Spectrum" data.meta['loaders']['spectrum2d'] = "DEIMOS 2D Spectrum" data.meta['loaders']['cutout'] = "ACS Cutout Image" if 'special_columns' not in data.meta: data.meta['special_columns'] = {} data.meta['special_columns']['spectrum1d'] = 'spectrum1d' data.meta['special_columns']['spectrum2d'] = 'spectrum2d' data.meta['special_columns']['source_id'] = 'id' data.meta['special_columns']['cutout'] = 'cutout' data.meta['special_columns']['slit_ra'] = 'ra' data.meta['special_columns']['slit_dec'] = 'dec' data.meta['special_columns']['slit_width'] = 'slit_width' data.meta['special_columns']['slit_length'] = 'slit_length' data.meta['loaders_confirmed'] = True ######################################################### self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_subset(self, subset): """ Processes subset messages from the central communication hub. Parameters ---------- subset : Subset object. """ self._layer_view.refresh() index = self._layer_view.layer_combo.findData(subset) self._layer_view.layer_combo.setCurrentIndex(index) return True def _update_data(self, message): """ Update data message. Parameters ---------- message : :class:`glue.core.message.Message` Data message object. """ self._layer_view.refresh() def _add_subset(self, message): """ Add subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() def _update_subset(self, message): """ Update subset message. Parameters ---------- message : :class:`glue.core.message.Message` Update message object. """ self._layer_view.refresh() self._unpack_selection(message.subset) def _remove_subset(self, message): """ Remove subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() self._unpack_selection(message.subset.data) def _selection_changed(self): self._unpack_selection(self._layer_view.layer_combo.currentData()) def _unpack_selection(self, data): """ Interprets the :class:`glue.core.data.Data` object by decomposing the data elements, extracting relevant data, and recomposing a package-agnostic dictionary object containing the relevant data. Parameters ---------- data : :class:`glue.core.data.Data` Glue data object to decompose. """ mask = None if isinstance(data, Subset): try: mask = data.to_mask() except IncompatibleAttribute: return if not np.any(mask): return data = data.data self.mask = mask # Clear the table self.catalog = Table() self.catalog.meta = data.meta self.comments = False col_names = data.components for att in col_names: cid = data.id[att] component = data.get_component(cid) if component.categorical: comp_labels = component.labels[mask] if comp_labels.ndim > 1: comp_labels = comp_labels[0] if str(att) in ["comments", "flag"]: self.comments = True elif str(att) in ['level2', 'spectrum1d', 'spectrum2d', 'cutout']: self.filepath = component._load_log.path p = Path(self.filepath) path = os.path.sep.join(p.parts[:-1]) self.catalog[str(att)] = [os.path.join(path, x) for x in comp_labels] else: self.catalog[str(att)] = comp_labels else: comp_data = component.data[mask] if comp_data.ndim > 1: comp_data = comp_data[0] self.catalog[str(att)] = comp_data if len(self.catalog) > 0: if not self.comments: self.comments = self._load_comments(data.label) #Returns bool else: self._data_collection_index(data.label) self._get_save_path() # Update gui elements self._update_navigation(select=0) def _update_navigation(self, select=0): """ Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the appropriate source `id`s from the MOS catalog. """ if self.toolbar is None: return self.toolbar.source_select.blockSignals(True) self.toolbar.source_select.clear() if len(self.catalog) > 0 and self.catalog.meta["special_columns"]["source_id"] in self.catalog.colnames: self.toolbar.source_select.addItems(self.catalog[self.catalog.meta["special_columns"]["source_id"]][:]) self.toolbar.source_select.setCurrentIndex(select) self.toolbar.source_select.blockSignals(False) self.toolbar.source_select.currentIndexChanged.emit(select) def _set_navigation(self, index): if len(self.catalog) < index: return if 0 <= index < self.toolbar.source_select.count(): self.toolbar.source_select.setCurrentIndex(index) if index <= 0: self.toolbar.cycle_previous_action.setDisabled(True) else: self.toolbar.cycle_previous_action.setDisabled(False) if index >= self.toolbar.source_select.count() - 1: self.toolbar.cycle_next_action.setDisabled(True) else: self.toolbar.cycle_next_action.setDisabled(False) def _set_exposure_navigation(self, index): # For level 3-only data. if index == None: # for some unknown reason (related to layout # managers perhaps?), the combo box does not # disappear from screen even when forced to # hide. Next best solution is to disable it. self.toolbar.exposure_select.setEnabled(False) self.toolbar.exposure_next_action.setEnabled(False) self.toolbar.exposure_previous_action.setEnabled(False) return if index > self.toolbar.exposure_select.count(): return if 0 <= index < self.toolbar.exposure_select.count(): self.toolbar.exposure_select.setCurrentIndex(index) if index < 1: self.toolbar.exposure_previous_action.setEnabled(False) else: self.toolbar.exposure_previous_action.setEnabled(True) if index >= self.toolbar.exposure_select.count() - 1: self.toolbar.exposure_next_action.setEnabled(False) else: self.toolbar.exposure_next_action.setEnabled(True) def _open_in_specviz(self): if self._specviz_instance is None: # Store a reference to the currently opened data viewer. This means # new "open in specviz" events will be added to the current viewer # as opposed to opening a new viewer. self._specviz_instance = self.session.application.new_data_viewer( SpecvizDataViewer) # Clear the reference to ensure no qt dangling pointers def _clear_instance_reference(): self._specviz_instance = None self._specviz_instance.window_closed.connect( _clear_instance_reference) # Create a new Spectrum1D object from the flux data attribute of # the incoming data spec = glue_data_to_spectrum1d(self._loaded_data['spectrum1d'], 'Flux') # Create a DataItem from the Spectrum1D object, which adds the data # to the internel specviz model data_item = self._specviz_instance.current_workspace.model.add_data( spec, 'Spectrum1D') self._specviz_instance.current_workspace.force_plot(data_item) def load_selection(self, row): """ Processes a row in the MOS catalog by first loading the data set, updating the stored data components, and then rendering the data on the visible MOSViz viewer plots. Parameters ---------- row : `astropy.table.Row` A row object representing a row in the MOS catalog. Each key should be a column name. """ self.current_row = row # Get loaders loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"]["spectrum1d"]] loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"]["spectrum2d"]] loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]] # Get column names colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"] colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"] colname_cutout = self.catalog.meta["special_columns"]["cutout"] level2_data = None if "level2" in self.catalog.meta["loaders"]: loader_level2 = LEVEL2_LOADERS[self.catalog.meta["loaders"]["level2"]] colname_level2 = self.catalog.meta["special_columns"]["level2"] level2_basename = os.path.basename(row[colname_level2]) if level2_basename != "None": level2_data = loader_level2(row[colname_level2]) spec1d_basename = os.path.basename(row[colname_spectrum1d]) if spec1d_basename == "None": spec1d_data = None else: spec1d_data = loader_spectrum1d(row[colname_spectrum1d]) spec2d_basename = os.path.basename(row[colname_spectrum2d]) if spec2d_basename == "None": spec2d_data = None else: spec2d_data = loader_spectrum2d(row[colname_spectrum2d]) image_basename = os.path.basename(row[colname_cutout]) if image_basename == "None": image_data = None else: image_data = loader_cutout(row[colname_cutout]) self._update_data_components(spec1d_data, key='spectrum1d') self._update_data_components(spec2d_data, key='spectrum2d') self._update_data_components(image_data, key='cutout') self.level2_data = level2_data self.spec2d_data = spec2d_data self.render_data(row, spec1d_data, spec2d_data, image_data, level2_data) def load_exposure(self, index): ''' Loads the level 2 exposure into the 2D spectrum plot widget. It can also load back the level 3 spectrum. ''' name = self.toolbar.exposure_select.currentText() if 'Level 3' in name: self.spectrum2d_widget.set_image( image = self.spec2d_data.get_component(self.spec2d_data.id['Flux']).data, interpolation = 'none', aspect = 'auto', extent = self.extent, origin='lower') else: if name in [component.label for component in self.level2_data.components]: self.spectrum2d_widget.set_image( image = self.level2_data.get_component(self.level2_data.id[name]).data, interpolation = 'none', aspect = 'auto', extent = self.extent, origin='lower') def _update_data_components(self, data, key): """ Update the data components that act as containers for the displayed data in the MOSViz viewer. This obviates the need to keep creating new data components. Parameters ---------- data : :class:`glue.core.data.Data` Data object to replace within the component. key : str References the particular data set type. """ cur_data = self._loaded_data.get(key, None) if cur_data is not None and data is None: self._loaded_data[key] = None self.session.data_collection.remove(cur_data) elif cur_data is None and data is not None: self._loaded_data[key] = data self.session.data_collection.append(data) elif data is not None: cur_data.update_values_from_data(data) else: return def add_slit(self, row=None, width=None, length=None): if row is None: row = self.current_row wcs = self.cutout_wcs if wcs is None: raise Exception("Image viewer has no WCS information") ra = row[self.catalog.meta["special_columns"]["slit_ra"]] dec = row[self.catalog.meta["special_columns"]["slit_dec"]] if width is None: width = row[self.catalog.meta["special_columns"]["slit_width"]] if length is None: length = row[self.catalog.meta["special_columns"]["slit_length"]] self.slit_controller.add_rectangle_sky_slit(wcs, ra, dec, width, length) def render_data(self, row, spec1d_data=None, spec2d_data=None, image_data=None, level2_data=None): """ Render the updated data sets in the individual plot widgets within the MOSViz viewer. """ self._check_unsaved_comments() self.image_viewer_hidden = image_data is None if spec1d_data is not None: # TODO: This should not be needed. Must explore why the core model # is out of sync with the proxy model. self.spectrum1d_widget.plot_widget.clear_plots() # Clear the specviz model of any rendered plot items self._specviz_viewer.model.clear() # Create a new Spectrum1D object from the flux data attribute of # the incoming data spec = glue_data_to_spectrum1d(spec1d_data, 'Flux') # Create a DataItem from the Spectrum1D object, which adds the data # to the internel specviz model data_item = self._specviz_viewer.model.add_data(spec, 'Spectrum1D') # Get the PlotDataItem rendered via the plot's proxy model and # ensure that it is visible in the plot plot_data_item = self.spectrum1d_widget.proxy_model.item_from_id(data_item.identifier) plot_data_item.visible = True plot_data_item.color = "#000000" # Explicitly let the plot widget know that data items have changed self.spectrum1d_widget.plot_widget.on_item_changed(data_item) if not self.image_viewer_hidden: if not self.image_widget.isVisible(): self.image_widget.setVisible(True) wcs = image_data.coords.wcs self.cutout_wcs = wcs array = image_data.get_component(image_data.id['Flux']).data # Add the slit patch to the plot self.slit_controller.clear_slits() if "slit_width" in self.catalog.meta["special_columns"] and \ "slit_length" in self.catalog.meta["special_columns"] and \ wcs is not None: self.add_slit(row) self.image_widget.draw_slit() else: self.image_widget.reset_limits() self.image_widget.set_image(array, wcs=wcs, interpolation='none', origin='lower') self.image_widget.axes.set_xlabel("Spatial X") self.image_widget.axes.set_ylabel("Spatial Y") if self.slit_controller.has_slits: self.image_widget.set_slit_limits() self.image_widget._redraw() else: self.cutout_wcs = None # Plot the 2D spectrum data last because by then we can make sure that # we set up the extent of the image appropriately if the cutout and the # 1D spectrum are present so that the axes can be locked. # We are repurposing the spectrum 2d widget to handle the display of both # the level 3 and level 2 spectra. if spec2d_data is not None or level2_data is not None: self._load_spectrum2d_widget(spec2d_data, level2_data) else: self.spectrum2d_widget.no_data() # Clear the meta information widget # NOTE: this process is inefficient for i in range(self.meta_form_layout.count()): wid = self.meta_form_layout.itemAt(i).widget() label = self.meta_form_layout.labelForField(wid) if label is not None: label.deleteLater() wid.deleteLater() # Repopulate the form layout # NOTE: this process is inefficient for col in row.colnames: if col.lower() not in ["comments", "flag"]: line_edit = QLineEdit(str(row[col]), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow(col, line_edit) # Set up comment and flag input/display boxes if self.comments: if self.savepath is not None: if self.savepath == -1: line_edit = QLineEdit(os.path.basename("Not Saving to File."), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) else: line_edit = QLineEdit(os.path.basename(self.savepath), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) self.input_flag = QLineEdit(self.get_flag(), self.central_widget.meta_form_widget) self.input_flag.textChanged.connect(self._text_changed) self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Flag", self.input_flag) self.input_comments = QPlainTextEdit(self.get_comment(), self.central_widget.meta_form_widget) self.input_comments.textChanged.connect(self._text_changed) self.input_comments.setStyleSheet("background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Comments", self.input_comments) self.input_save = QPushButton('Save', self.central_widget.meta_form_widget) self.input_save.clicked.connect(self.update_comments) self.input_save.setDefault(True) self.input_refresh = QPushButton('Reload', self.central_widget.meta_form_widget) self.input_refresh.clicked.connect(self.refresh_comments) self.meta_form_layout.addRow(self.input_save, self.input_refresh) if not self.isHidden() and self.image_viewer_hidden: self.image_widget.setVisible(False) def _load_spectrum2d_widget(self, spec2d_data, level2_data): if not spec2d_data: return xp2d = np.arange(spec2d_data.shape[1]) yp2d = np.repeat(0, spec2d_data.shape[1]) spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world(xp2d, yp2d) x_min = spectrum2d_disp.min() x_max = spectrum2d_disp.max() if self.slit_controller.has_slits and \ None not in self.slit_controller.y_bounds: y_min, y_max = self.slit_controller.y_bounds else: y_min = -0.5 y_max = spec2d_data.shape[0] - 0.5 self.extent = [x_min, x_max, y_min, y_max] # By default, displays the level 3 spectrum. The level 2 # data is plotted elsewhere, driven by the exposure_select # combo box signals. self.spectrum2d_widget.set_image( image=spec2d_data.get_component(spec2d_data.id['Flux']).data, interpolation='none', aspect='auto', extent=self.extent, origin='lower') self.spectrum2d_widget.axes.set_xlabel("Wavelength") self.spectrum2d_widget.axes.set_ylabel("Spatial Y") self.spectrum2d_widget._redraw() # If the axis are linked between the 1d and 2d views, setting the data # often ignores the initial bounds and instead uses the bounds of the # 2d data until the 1d view is moved. Force the 2d viewer to honor # the 1d view bounds. self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.emit( None, self.spectrum1d_widget.plot_widget.viewRange()[0]) # Populates the level 2 exposures combo box if level2_data: self.toolbar.exposure_select.clear() self.toolbar.exposure_select.addItems(['Level 3']) components = level2_data.main_components + level2_data.derived_components self.toolbar.exposure_select.addItems([component.label for component in components]) self._set_exposure_navigation(0) else: self._set_exposure_navigation(None) @defer_draw def set_locked_axes(self, x=None, y=None): # Here we only change the setting if x or y are not None # since if set_locked_axes is called with eg. x=True, then # we shouldn't change the y setting. if x is not None: if x: # Lock the x axis if x is True def on_x_range_changed(xlim): self.spectrum2d_widget.axes.set_xlim(*xlim) self.spectrum2d_widget._redraw() self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.connect( lambda a, b: on_x_range_changed(b)) # Call the slot to update the axis linking initially # FIXME: Currently, this does not work for some reason. on_x_range_changed(self.spectrum1d_widget.plot_widget.viewRange()[0]) else: # Unlock the x axis if x is False self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.disconnect() if y is not None: self.spectrum2d_image_share.sharey = y self.spectrum2d_widget._redraw() self.image_widget._redraw() def layer_view(self): return self._layer_view def _text_changed(self): if self.textChangedAt is None: i = self.toolbar.source_select.currentIndex() self.textChangedAt = self._index_hash(i) def _check_unsaved_comments(self): if self.textChangedAt is None: return #Nothing to be changed i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) if self.textChangedAt == i: self.textChangedAt = None return #This is a refresh info = "Comments or flags changed but were not saved. Would you like to save them?" reply = QMessageBox.question(self, '', info, QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.update_comments(True) self.textChangedAt = None def _data_collection_index(self, label): idx = -1 for i, l in enumerate(self.session.data_collection): if l.label == label: idx = i break if idx == -1: return -1 self.data_idx = idx return idx def _index_hash(self, i): """Local selection index -> Table index""" if self.mask is not None: size = self.mask.size temp = np.arange(size) return temp[self.mask][i] else: return i def _id_to_index_hash(self, ID, l): """Object Name -> Table index""" for i, name in enumerate(l): if name == ID: return i return None def get_slit_dimensions_from_file(self): if self.catalog is None: return None if "slit_width" in self.catalog.meta["special_columns"] and \ "slit_length" in self.catalog.meta["special_columns"]: width = self.current_row[self.catalog.meta["special_columns"]["slit_width"]] length = self.current_row[self.catalog.meta["special_columns"]["slit_length"]] return [length, width] return None def get_slit_units_from_file(self): # TODO: Update once units infrastructure is in place return ["arcsec", "arcsec"] def get_comment(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("comments") return comp.labels[i] def get_flag(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("flag") return comp.labels[i] def send_NumericalDataChangedMessage(self): idx = self.data_idx data = self.session.data_collection[idx] data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments")) def refresh_comments(self): self.input_flag.setText(self.get_flag()) self.input_comments.setPlainText(self.get_comment()) self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") self.textChangedAt = None def _get_save_path(self): """ Try to get save path from other MOSVizViewer instances """ for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.savepath is not None: if v.data_idx == self.data_idx: self.savepath = v.savepath break def _setup_save_path(self): """ Prompt the user for a file to save comments and flags into. """ fail = True success = False info = "Where would you like to save comments and flags?" option = pick_item([0, 1], [os.path.basename(self.filepath), "New MOSViz Table file"], label=info, title="Comment Setup") if option == 0: self.savepath = self.filepath elif option == 1: dirname = os.path.dirname(self.filepath) path = compat.getsavefilename(caption="New MOSViz Table File", basedir=dirname, filters="*.txt")[0] if path == "": return fail self.savepath = path else: return fail for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.data_idx == self.data_idx: v.savepath = self.savepath self._layer_view.refresh() return success def update_comments(self, pastSelection = False): """ Process comment and flag changes and save to file. Parameters ---------- pastSelection : bool True when updating past selections. Used when user forgets to save. """ if self.input_flag.text() == "": self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") return i = None try: i = int(self.input_flag.text()) except ValueError: self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") info = QMessageBox.information(self, "Status:", "Flag must be an int!") return self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") idx = self.data_idx if pastSelection: i = self.textChangedAt self.textChangedAt = None else: i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) data = self.session.data_collection[idx] comp = data.get_component("comments") comp.labels.flags.writeable = True comp.labels[i] = self.input_comments.toPlainText() comp = data.get_component("flag") comp.labels.flags.writeable = True comp.labels[i] = self.input_flag.text() self.send_NumericalDataChangedMessage() self.write_comments() self.textChangedAt = None def _load_comments(self, label): """ Populate the comments and flag columns. Attempt to load comments from file. Parameters ---------- label : str The label of the data in session.data_collection. """ #Make sure its the right data #(beacuse subset data is masked) idx = self._data_collection_index(label) if idx == -1: return False data = self.session.data_collection[idx] #Fill in default comments: length = data.shape[0] new_comments = np.array(["" for i in range(length)], dtype=object) new_flags = np.array(["0" for i in range(length)], dtype=object) #Fill in any saved comments: meta = data.meta obj_names = data.get_component(self.catalog.meta["special_columns"]["source_id"]).labels if "MOSViz_comments" in meta.keys(): try: comments = meta["MOSViz_comments"] for key in comments.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = comments[key] new_comments[index] = line except Exception as e: print("MOSViz Comment Load Failed: ", e) if "MOSViz_flags" in meta.keys(): try: flags = meta["MOSViz_flags"] for key in flags.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = flags[key] new_flags[index] = line except Exception as e: print("MOSViz Flag Load Failed: ", e) #Send to DC data.add_component(CategoricalComponent(new_flags, "flag"), "flag") data.add_component(CategoricalComponent(new_comments, "comments"), "comments") return True def write_comments(self): """ Setup save file. Write comments and flags to file """ if self.savepath is None: fail = self._setup_save_path() if fail: return if self.savepath == -1: return #Do not save to file option idx = self.data_idx data = self.session.data_collection[idx] save_comments = data.get_component("comments").labels save_flag = data.get_component("flag").labels obj_names = data.get_component(self.catalog.meta["special_columns"]["source_id"]).labels fn = self.savepath folder = os.path.dirname(fn) t = astropy_table.data_to_astropy_table(data) #Check if load and save dir paths match temp = os.path.dirname(self.filepath) if not os.path.samefile(folder, temp): t['spectrum1d'].flags.writeable = True t['spectrum2d'].flags.writeable = True t['cutout'].flags.writeable = True for i in range(len(t)): t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i]) t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i]) t['cutout'][i] = os.path.abspath(t['cutout'][i]) try: t.remove_column("comments") t.remove_column("flag") keys = t.meta.keys() if "MOSViz_comments" in keys: t.meta.pop("MOSViz_comments") if "MOSViz_flags" in keys: t.meta.pop("MOSViz_flags") comments = OrderedDict() flags = OrderedDict() for i, line in enumerate(save_comments): if line != "": line = line.replace("\n", " ") key = str(obj_names[i]) comments[key] = line for i, line in enumerate(save_flag): if line != "0" and line != "": line = comments.replace("\n", " ") key = str(obj_names[i]) flags[key] = line if len(comments) > 0: t.meta["MOSViz_comments"] = comments if len(flags) > 0: t.meta["MOSViz_flags"] = flags t.write(fn, format="ascii.ecsv", overwrite=True) except Exception as e: print("Comment write failed:", e) def closeEvent(self, event): """ Clean up the extraneous data components created when opening the MOSViz viewer by overriding the parent class's close event. """ super(MOSVizViewer, self).closeEvent(event) for data in self._loaded_data.values(): self.session.data_collection.remove(data)
class VentanaPrincipal(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setup_ui() self.show_home() def setup_ui(self): self.resize(580, 200) self.setFixedHeight(140) self.setFixedWidth(580) self.move((1920 / 2) - (500 / 2), (1080 / 2) - (200 / 2)) self.setWindowTitle('Buscador') self.statusBar().showMessage('Listo') self.setup_menu() def setup_menu(self): menubar = self.menuBar() file_menu = menubar.addMenu('&Archivo') refresh_action = QAction(qta.icon('fa5s.sync'), '&Actualizar', self) refresh_action.setShortcut('Ctrl+A') refresh_action.setStatusTip( 'Actualizando Base de Datos de Artículos....') refresh_action.triggered.connect( scraper.scrapeAll) # Llamar al método de scrapper file_menu.addAction(refresh_action) exit_action = QAction(qta.icon('fa5.times-circle'), '&Salir', self) exit_action.setShortcut('Ctrl+Q') exit_action.setStatusTip('Saliendo de la aplicación....') exit_action.triggered.connect(QApplication.instance().closeAllWindows) file_menu.addAction(exit_action) help_menu = menubar.addMenu('&Ayuda') about_action = QAction(qta.icon('fa5s.info-circle'), '&Acerca de', self) about_action.setShortcut('Ctrl+I') about_action.setStatusTip('Acerca de...') about_action.triggered.connect(self.show_about_dialog) help_menu.addAction(about_action) @Slot() def show_about_dialog(self): ## NUEVA LÍNEA msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setText( "Aplicación de Scrapper y búsqueda en 20 Minutos, El Mundo y El Pais \n\n por Ignacio Triguero y Alexey Zhelezov" ) msg_box.setWindowTitle("Acerca de") msg_box.setStandardButtons(QMessageBox.Close) msg_box.exec_() def show_home(self): labelBusqueda = QLabel('Tu busqueda', self) labelBusqueda.move(10, 25) self.text_edit = QPlainTextEdit(self) self.text_edit.setFixedHeight(30) self.text_edit.setFixedWidth(400) self.text_edit.move(10, 60) labelnum = QLabel('Numero de articulos', self) labelnum.move(420, 25) labelnum.setFixedWidth(150) self.text_edit_num = QPlainTextEdit("5", self) self.text_edit_num.setFixedHeight(30) self.text_edit_num.setFixedWidth(150) self.text_edit_num.move(420, 60) search_button = QPushButton(self) search_button.setFixedWidth(70) search_button.setText("Buscar") search_button.move(500, 100) search_button.clicked.connect(self.buscar) openDirButton = QPushButton(self) openDirButton.setFixedWidth(110) openDirButton.setText("Abrir Directorio") openDirButton.move(380, 100) openDirButton.clicked.connect(self.getFile) def getfile(self): fname = QFileDialog.getOpenFileName(self, "Open Image", "/home") Dialog = QDialog() # self.reject() ui = Noticia_Dialog(Dialog) ui.setupUi(fname) def getFile(self): dialog = FileDialog() def buscar(self): # abrir dialogo carga aqui lista = procesador.search(self.text_edit.toPlainText(), self.text_edit_num.toPlainText()) # cerrar self.abrirVentanaLista(lista, self.text_edit.toPlainText()) #Aqui hay que meter los titulos de cada archivo en el {self.scroll_area} en una lista scrollable def abrirVentanaLista(self, lista, query): Dialog = QDialog() ui = Ui_Dialog(Dialog) ui.setupUi(lista, query)
class FileDialog(QDialog): def __init__(self, file_name, job_name, job_number, realization, iteration, parent=None): super(FileDialog, self).__init__(parent) self.setWindowTitle("{} # {} Realization: {} Iteration: {}".format( job_name, job_number, realization, iteration)) try: self._file = open(file_name, "r") except OSError as error: self._mb = QMessageBox( QMessageBox.Critical, "Error opening file", error.strerror, QMessageBox.Ok, self, ) self._mb.finished.connect(self.accept) self._mb.show() return self._view = QPlainTextEdit() self._view.setReadOnly(True) self._view.setWordWrapMode(QTextOption.NoWrap) # for moving the actual slider self._view.verticalScrollBar().sliderMoved.connect(self._update_cursor) # for mouse wheel and keyboard arrows self._view.verticalScrollBar().valueChanged.connect( self._update_cursor) self._view.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont)) self._follow_mode = False self._init_layout() self._init_thread() self.show() @Slot() def _stop_thread(self): self._thread.quit() self._thread.wait() def _init_layout(self): self.setMinimumWidth(600) self.setMinimumHeight(400) dialog_buttons = QDialogButtonBox(QDialogButtonBox.Ok) dialog_buttons.accepted.connect(self.accept) self._copy_all_button = dialog_buttons.addButton( "Copy All", QDialogButtonBox.ActionRole) self._copy_all_button.clicked.connect(self._copy_all) self._follow_button = dialog_buttons.addButton( "Follow", QDialogButtonBox.ActionRole) self._follow_button.setCheckable(True) self._follow_button.toggled.connect(self._enable_follow_mode) self._enable_follow_mode(self._follow_mode) layout = QVBoxLayout(self) layout.addWidget(self._view) layout.addWidget(dialog_buttons) def _init_thread(self): self._thread = QThread() self._worker = FileUpdateWorker(self._file) self._worker.moveToThread(self._thread) self._worker.read.connect(self._append_text) self._thread.started.connect(self._worker.setup) self._thread.finished.connect(self._worker.stop) self._thread.finished.connect(self._worker.deleteLater) self.finished.connect(self._stop_thread) self._thread.start() def _copy_all(self) -> None: text = self._view.toPlainText() QApplication.clipboard().setText(text, QClipboard.Clipboard) pass def _update_cursor(self, value: int) -> None: if not self._view.textCursor().hasSelection(): block = self._view.document().findBlockByLineNumber(value) cursor = QTextCursor(block) self._view.setTextCursor(cursor) def _enable_follow_mode(self, enable: bool) -> None: if enable: self._view.moveCursor(QTextCursor.End) self._view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._view.verticalScrollBar().setDisabled(True) self._view.setTextInteractionFlags(Qt.NoTextInteraction) self._follow_mode = True else: self._view.verticalScrollBar().setDisabled(False) self._view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self._view.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) self._follow_mode = False def _append_text(self, text: str) -> None: # Remove trailing newline as appendPlainText adds this if text[-1:] == "\n": text = text[:-1] if self._follow_mode: self._view.moveCursor(QTextCursor.End) self._view.appendPlainText(text)
class MOSVizViewer(DataViewer): LABEL = "MOSViz Viewer" window_closed = Signal() _toolbar_cls = MOSViewerToolbar tools = [] subtools = [] def __init__(self, session, parent=None): super(MOSVizViewer, self).__init__(session, parent=parent) self.slit_controller = SlitController(self) self.load_ui() # Define some data containers self.filepath = None self.savepath = None self.data_idx = None self.comments = False self.textChangedAt = None self.mask = None self.cutout_wcs = None self.level2_data = None self.spec2d_data = None self.catalog = None self.current_row = None self._specviz_instance = None self._loaded_data = {} self._primary_data = None self._layer_view = SimpleLayerWidget(parent=self) self._layer_view.layer_combo.currentIndexChanged.connect( self._selection_changed) self.resize(800, 600) self.image_viewer_hidden = False def load_ui(self): """ Setup the MOSView viewer interface. """ self.central_widget = QWidget(self) path = os.path.join(UI_DIR, 'mos_widget.ui') loadUi(path, self.central_widget) self.image_widget = DrawableImageWidget( slit_controller=self.slit_controller) self.spectrum2d_widget = Spectrum2DWidget() self._specviz_viewer = Workspace() self._specviz_viewer.add_plot_window() self.spectrum1d_widget = self._specviz_viewer.current_plot_window self.spectrum1d_widget.plot_widget.getPlotItem( ).layout.setContentsMargins(45, 0, 25, 0) # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing # and we control the sharing later by setting .sharex and .sharey on the # helper self.spectrum2d_image_share = SharedAxisHelper( self.spectrum2d_widget._axes, self.image_widget._axes) # We only need to set the image widget to keep the same aspect ratio # since the two other viewers don't require square pixels, so the axes # should not change shape. self.image_widget._axes.set_adjustable('datalim') self.meta_form_layout = self.central_widget.meta_form_layout self.meta_form_layout.setFieldGrowthPolicy( self.meta_form_layout.ExpandingFieldsGrow) self.central_widget.left_vertical_splitter.insertWidget( 0, self.image_widget) self.central_widget.right_vertical_splitter.addWidget( self.spectrum2d_widget) self.central_widget.right_vertical_splitter.addWidget( self.spectrum1d_widget.widget()) # Set the splitter stretch factors self.central_widget.left_vertical_splitter.setStretchFactor(0, 1) self.central_widget.left_vertical_splitter.setStretchFactor(1, 8) self.central_widget.right_vertical_splitter.setStretchFactor(0, 1) self.central_widget.right_vertical_splitter.setStretchFactor(1, 2) self.central_widget.horizontal_splitter.setStretchFactor(0, 1) self.central_widget.horizontal_splitter.setStretchFactor(1, 2) # Keep the left and right splitters in sync otherwise the axes don't line up self.central_widget.left_vertical_splitter.splitterMoved.connect( self._left_splitter_moved) self.central_widget.right_vertical_splitter.splitterMoved.connect( self._right_splitter_moved) # Set the central widget self.setCentralWidget(self.central_widget) self.central_widget.show() # Define the options widget self._options_widget = OptionsWidget() def show(self, *args, **kwargs): super(MOSVizViewer, self).show(*args, **kwargs) # Trigger a sync between the splitters self._left_splitter_moved() if self.image_viewer_hidden: self.image_widget.hide() else: self.image_widget.show() @avoid_circular def _right_splitter_moved(self, *args, **kwargs): if self.image_widget.isHidden(): return sizes = self.central_widget.right_vertical_splitter.sizes() if sizes == [0, 0]: sizes = [230, 230] self.central_widget.left_vertical_splitter.setSizes(sizes) @avoid_circular def _left_splitter_moved(self, *args, **kwargs): if self.image_widget.isHidden(): return sizes = self.central_widget.left_vertical_splitter.sizes() if sizes == [0, 0]: sizes = [230, 230] self.central_widget.right_vertical_splitter.setSizes(sizes) def setup_connections(self): """ Connects gui elements to event calls. """ # Connect the selection event for the combo box to what's displayed self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self.load_selection(self.catalog[ind])) self.toolbar.source_select.currentIndexChanged[int].connect( lambda ind: self._set_navigation(ind)) # Connect the exposure selection event self.toolbar.exposure_select.currentIndexChanged[int].connect( lambda ind: self.load_exposure(ind)) self.toolbar.exposure_select.currentIndexChanged[int].connect( lambda ind: self._set_exposure_navigation(ind)) # Connect the specviz button if SpecvizDataViewer is not None: self.toolbar.open_specviz.triggered.connect( lambda: self._open_in_specviz()) else: self.toolbar.open_specviz.setDisabled(True) # Connect slit previous and next buttons self.toolbar.cycle_next_action.triggered.connect( lambda: self._set_navigation(self.toolbar.source_select. currentIndex() + 1)) self.toolbar.cycle_previous_action.triggered.connect( lambda: self._set_navigation(self.toolbar.source_select. currentIndex() - 1)) # Connect exposure previous and next buttons self.toolbar.exposure_next_action.triggered.connect( lambda: self._set_exposure_navigation(self.toolbar.exposure_select. currentIndex() + 1)) self.toolbar.exposure_previous_action.triggered.connect( lambda: self._set_exposure_navigation(self.toolbar.exposure_select. currentIndex() - 1)) # Connect the toolbar axes setting actions self.toolbar.lock_x_action.triggered.connect( lambda state: self.set_locked_axes(x=state)) self.toolbar.lock_y_action.triggered.connect( lambda state: self.set_locked_axes(y=state)) def options_widget(self): return self._options_widget def initialize_toolbar(self): """ Initialize the custom toolbar for the MOSViz viewer. """ from glue.config import viewer_tool self.toolbar = self._toolbar_cls(self) for tool_id in self.tools: mode_cls = viewer_tool.members[tool_id] mode = mode_cls(self) self.toolbar.add_tool(mode) self.addToolBar(self.toolbar) self.setup_connections() def register_to_hub(self, hub): super(MOSVizViewer, self).register_to_hub(hub) def has_data_or_subset(x): if x.sender is self._primary_data: return True elif isinstance(x.sender, Subset) and x.sender.data is self._primary_data: return True else: return False hub.subscribe(self, msg.SubsetCreateMessage, handler=self._add_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetUpdateMessage, handler=self._update_subset, filter=has_data_or_subset) hub.subscribe(self, msg.SubsetDeleteMessage, handler=self._remove_subset, filter=has_data_or_subset) hub.subscribe(self, msg.DataUpdateMessage, handler=self._update_data, filter=has_data_or_subset) def add_data(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [data.get_component(cid) for cid in data.main_components] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Make sure the loaders and column names are correct result = confirm_loaders_and_column_names(data) if not result: return False self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_data_for_testing(self, data): """ Processes data message from the central communication hub. Parameters ---------- data : :class:`glue.core.data.Data` Data object. """ # Check whether the data is suitable for the MOSViz viewer - basically # we expect a table of 1D columns with at least three string and four # floating-point columns. if data.ndim != 1: QMessageBox.critical(self, "Error", "MOSViz viewer can only be used " "for data with 1-dimensional components", buttons=QMessageBox.Ok) return False components = [data.get_component(cid) for cid in data.main_components] categorical = [c for c in components if c.categorical] if len(categorical) < 3: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "three string components/columns, representing " "the filenames of the 1D and 2D spectra and " "cutouts", buttons=QMessageBox.Ok) return False # We can relax the following requirement if we make the slit parameters # optional numerical = [c for c in components if c.numeric] if len(numerical) < 4: QMessageBox.critical( self, "Error", "MOSViz viewer expected at least " "four numerical components/columns, representing " "the slit position, length, and position angle", buttons=QMessageBox.Ok) return False # Block of code to bypass the loader_selection gui ######################################################### if 'loaders' not in data.meta: data.meta['loaders'] = {} # Deimos data data.meta['loaders']['spectrum1d'] = "DEIMOS 1D Spectrum" data.meta['loaders']['spectrum2d'] = "DEIMOS 2D Spectrum" data.meta['loaders']['cutout'] = "ACS Cutout Image" if 'special_columns' not in data.meta: data.meta['special_columns'] = {} data.meta['special_columns']['spectrum1d'] = 'spectrum1d' data.meta['special_columns']['spectrum2d'] = 'spectrum2d' data.meta['special_columns']['source_id'] = 'id' data.meta['special_columns']['cutout'] = 'cutout' data.meta['special_columns']['slit_ra'] = 'ra' data.meta['special_columns']['slit_dec'] = 'dec' data.meta['special_columns']['slit_width'] = 'slit_width' data.meta['special_columns']['slit_length'] = 'slit_length' data.meta['loaders_confirmed'] = True ######################################################### self._primary_data = data self._layer_view.data = data self._unpack_selection(data) return True def add_subset(self, subset): """ Processes subset messages from the central communication hub. Parameters ---------- subset : Subset object. """ self._layer_view.refresh() index = self._layer_view.layer_combo.findData(subset) self._layer_view.layer_combo.setCurrentIndex(index) return True def _update_data(self, message): """ Update data message. Parameters ---------- message : :class:`glue.core.message.Message` Data message object. """ self._layer_view.refresh() def _add_subset(self, message): """ Add subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() def _update_subset(self, message): """ Update subset message. Parameters ---------- message : :class:`glue.core.message.Message` Update message object. """ self._layer_view.refresh() self._unpack_selection(message.subset) def _remove_subset(self, message): """ Remove subset message. Parameters ---------- message : :class:`glue.core.message.Message` Subset message object. """ self._layer_view.refresh() self._unpack_selection(message.subset.data) def _selection_changed(self): self._unpack_selection(self._layer_view.layer_combo.currentData()) def _unpack_selection(self, data): """ Interprets the :class:`glue.core.data.Data` object by decomposing the data elements, extracting relevant data, and recomposing a package-agnostic dictionary object containing the relevant data. Parameters ---------- data : :class:`glue.core.data.Data` Glue data object to decompose. """ mask = None if isinstance(data, Subset): try: mask = data.to_mask() except IncompatibleAttribute: return if not np.any(mask): return data = data.data self.mask = mask # Clear the table self.catalog = Table() self.catalog.meta = data.meta self.comments = False col_names = data.components for att in col_names: cid = data.id[att] component = data.get_component(cid) if component.categorical: comp_labels = component.labels[mask] if comp_labels.ndim > 1: comp_labels = comp_labels[0] if str(att) in ["comments", "flag"]: self.comments = True elif str(att) in [ 'level2', 'spectrum1d', 'spectrum2d', 'cutout' ]: self.filepath = component._load_log.path p = Path(self.filepath) path = os.path.sep.join(p.parts[:-1]) self.catalog[str(att)] = [ os.path.join(path, x) for x in comp_labels ] else: self.catalog[str(att)] = comp_labels else: comp_data = component.data[mask] if comp_data.ndim > 1: comp_data = comp_data[0] self.catalog[str(att)] = comp_data if len(self.catalog) > 0: if not self.comments: self.comments = self._load_comments(data.label) #Returns bool else: self._data_collection_index(data.label) self._get_save_path() # Update gui elements self._update_navigation(select=0) def _update_navigation(self, select=0): """ Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the appropriate source `id`s from the MOS catalog. """ if self.toolbar is None: return self.toolbar.source_select.blockSignals(True) self.toolbar.source_select.clear() if len(self.catalog) > 0 and self.catalog.meta["special_columns"][ "source_id"] in self.catalog.colnames: self.toolbar.source_select.addItems(self.catalog[ self.catalog.meta["special_columns"]["source_id"]][:]) self.toolbar.source_select.setCurrentIndex(select) self.toolbar.source_select.blockSignals(False) self.toolbar.source_select.currentIndexChanged.emit(select) def _set_navigation(self, index): if len(self.catalog) < index: return if 0 <= index < self.toolbar.source_select.count(): self.toolbar.source_select.setCurrentIndex(index) if index <= 0: self.toolbar.cycle_previous_action.setDisabled(True) else: self.toolbar.cycle_previous_action.setDisabled(False) if index >= self.toolbar.source_select.count() - 1: self.toolbar.cycle_next_action.setDisabled(True) else: self.toolbar.cycle_next_action.setDisabled(False) def _set_exposure_navigation(self, index): # For level 3-only data. if index == None: # for some unknown reason (related to layout # managers perhaps?), the combo box does not # disappear from screen even when forced to # hide. Next best solution is to disable it. self.toolbar.exposure_select.setEnabled(False) self.toolbar.exposure_next_action.setEnabled(False) self.toolbar.exposure_previous_action.setEnabled(False) return if index > self.toolbar.exposure_select.count(): return if 0 <= index < self.toolbar.exposure_select.count(): self.toolbar.exposure_select.setCurrentIndex(index) if index < 1: self.toolbar.exposure_previous_action.setEnabled(False) else: self.toolbar.exposure_previous_action.setEnabled(True) if index >= self.toolbar.exposure_select.count() - 1: self.toolbar.exposure_next_action.setEnabled(False) else: self.toolbar.exposure_next_action.setEnabled(True) def _open_in_specviz(self): if self._specviz_instance is None: # Store a reference to the currently opened data viewer. This means # new "open in specviz" events will be added to the current viewer # as opposed to opening a new viewer. self._specviz_instance = self.session.application.new_data_viewer( SpecvizDataViewer) # Clear the reference to ensure no qt dangling pointers def _clear_instance_reference(): self._specviz_instance = None self._specviz_instance.window_closed.connect( _clear_instance_reference) # Create a new Spectrum1D object from the flux data attribute of # the incoming data spec = glue_data_to_spectrum1d(self._loaded_data['spectrum1d'], 'Flux') # Create a DataItem from the Spectrum1D object, which adds the data # to the internel specviz model data_item = self._specviz_instance.current_workspace.model.add_data( spec, 'Spectrum1D') self._specviz_instance.current_workspace.force_plot(data_item) def load_selection(self, row): """ Processes a row in the MOS catalog by first loading the data set, updating the stored data components, and then rendering the data on the visible MOSViz viewer plots. Parameters ---------- row : `astropy.table.Row` A row object representing a row in the MOS catalog. Each key should be a column name. """ self.current_row = row # Get loaders loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"] ["spectrum1d"]] loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"] ["spectrum2d"]] loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]] # Get column names colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"] colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"] colname_cutout = self.catalog.meta["special_columns"]["cutout"] level2_data = None if "level2" in self.catalog.meta["loaders"]: loader_level2 = LEVEL2_LOADERS[self.catalog.meta["loaders"] ["level2"]] colname_level2 = self.catalog.meta["special_columns"]["level2"] level2_basename = os.path.basename(row[colname_level2]) if level2_basename != "None": level2_data = loader_level2(row[colname_level2]) spec1d_basename = os.path.basename(row[colname_spectrum1d]) if spec1d_basename == "None": spec1d_data = None else: spec1d_data = loader_spectrum1d(row[colname_spectrum1d]) spec2d_basename = os.path.basename(row[colname_spectrum2d]) if spec2d_basename == "None": spec2d_data = None else: spec2d_data = loader_spectrum2d(row[colname_spectrum2d]) image_basename = os.path.basename(row[colname_cutout]) if image_basename == "None": image_data = None else: image_data = loader_cutout(row[colname_cutout]) self._update_data_components(spec1d_data, key='spectrum1d') self._update_data_components(spec2d_data, key='spectrum2d') self._update_data_components(image_data, key='cutout') self.level2_data = level2_data self.spec2d_data = spec2d_data self.render_data(row, spec1d_data, spec2d_data, image_data, level2_data) def load_exposure(self, index): ''' Loads the level 2 exposure into the 2D spectrum plot widget. It can also load back the level 3 spectrum. ''' name = self.toolbar.exposure_select.currentText() if 'Level 3' in name: self.spectrum2d_widget.set_image( image=self.spec2d_data.get_component( self.spec2d_data.id['Flux']).data, interpolation='none', aspect='auto', extent=self.extent, origin='lower') else: if name in [ component.label for component in self.level2_data.components ]: self.spectrum2d_widget.set_image( image=self.level2_data.get_component( self.level2_data.id[name]).data, interpolation='none', aspect='auto', extent=self.extent, origin='lower') def _update_data_components(self, data, key): """ Update the data components that act as containers for the displayed data in the MOSViz viewer. This obviates the need to keep creating new data components. Parameters ---------- data : :class:`glue.core.data.Data` Data object to replace within the component. key : str References the particular data set type. """ cur_data = self._loaded_data.get(key, None) if cur_data is not None and data is None: self._loaded_data[key] = None self.session.data_collection.remove(cur_data) elif cur_data is None and data is not None: self._loaded_data[key] = data self.session.data_collection.append(data) elif data is not None: cur_data.update_values_from_data(data) else: return def add_slit(self, row=None, width=None, length=None): if row is None: row = self.current_row wcs = self.cutout_wcs if wcs is None: raise Exception("Image viewer has no WCS information") ra = row[self.catalog.meta["special_columns"]["slit_ra"]] dec = row[self.catalog.meta["special_columns"]["slit_dec"]] if width is None: width = row[self.catalog.meta["special_columns"]["slit_width"]] if length is None: length = row[self.catalog.meta["special_columns"]["slit_length"]] self.slit_controller.add_rectangle_sky_slit(wcs, ra, dec, width, length) def render_data(self, row, spec1d_data=None, spec2d_data=None, image_data=None, level2_data=None): """ Render the updated data sets in the individual plot widgets within the MOSViz viewer. """ self._check_unsaved_comments() self.image_viewer_hidden = image_data is None if spec1d_data is not None: # TODO: This should not be needed. Must explore why the core model # is out of sync with the proxy model. self.spectrum1d_widget.plot_widget.clear_plots() # Clear the specviz model of any rendered plot items self._specviz_viewer.model.clear() # Create a new Spectrum1D object from the flux data attribute of # the incoming data spec = glue_data_to_spectrum1d(spec1d_data, 'Flux') # Create a DataItem from the Spectrum1D object, which adds the data # to the internel specviz model data_item = self._specviz_viewer.model.add_data(spec, 'Spectrum1D') # Get the PlotDataItem rendered via the plot's proxy model and # ensure that it is visible in the plot plot_data_item = self.spectrum1d_widget.proxy_model.item_from_id( data_item.identifier) plot_data_item.visible = True plot_data_item.color = "#000000" # Explicitly let the plot widget know that data items have changed self.spectrum1d_widget.plot_widget.on_item_changed(data_item) if not self.image_viewer_hidden: if not self.image_widget.isVisible(): self.image_widget.setVisible(True) wcs = image_data.coords.wcs self.cutout_wcs = wcs array = image_data.get_component(image_data.id['Flux']).data # Add the slit patch to the plot self.slit_controller.clear_slits() if "slit_width" in self.catalog.meta["special_columns"] and \ "slit_length" in self.catalog.meta["special_columns"] and \ wcs is not None: self.add_slit(row) self.image_widget.draw_slit() else: self.image_widget.reset_limits() self.image_widget.set_image(array, wcs=wcs, interpolation='none', origin='lower') self.image_widget.axes.set_xlabel("Spatial X") self.image_widget.axes.set_ylabel("Spatial Y") if self.slit_controller.has_slits: self.image_widget.set_slit_limits() self.image_widget._redraw() else: self.cutout_wcs = None # Plot the 2D spectrum data last because by then we can make sure that # we set up the extent of the image appropriately if the cutout and the # 1D spectrum are present so that the axes can be locked. # We are repurposing the spectrum 2d widget to handle the display of both # the level 3 and level 2 spectra. if spec2d_data is not None or level2_data is not None: self._load_spectrum2d_widget(spec2d_data, level2_data) else: self.spectrum2d_widget.no_data() # Clear the meta information widget # NOTE: this process is inefficient for i in range(self.meta_form_layout.count()): wid = self.meta_form_layout.itemAt(i).widget() label = self.meta_form_layout.labelForField(wid) if label is not None: label.deleteLater() wid.deleteLater() # Repopulate the form layout # NOTE: this process is inefficient for col in row.colnames: if col.lower() not in ["comments", "flag"]: line_edit = QLineEdit(str(row[col]), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow(col, line_edit) # Set up comment and flag input/display boxes if self.comments: if self.savepath is not None: if self.savepath == -1: line_edit = QLineEdit( os.path.basename("Not Saving to File."), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) else: line_edit = QLineEdit(os.path.basename(self.savepath), self.central_widget.meta_form_widget) line_edit.setReadOnly(True) self.meta_form_layout.addRow("Save File", line_edit) self.input_flag = QLineEdit(self.get_flag(), self.central_widget.meta_form_widget) self.input_flag.textChanged.connect(self._text_changed) self.input_flag.setStyleSheet( "background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Flag", self.input_flag) self.input_comments = QPlainTextEdit( self.get_comment(), self.central_widget.meta_form_widget) self.input_comments.textChanged.connect(self._text_changed) self.input_comments.setStyleSheet( "background-color: rgba(255, 255, 255);") self.meta_form_layout.addRow("Comments", self.input_comments) self.input_save = QPushButton('Save', self.central_widget.meta_form_widget) self.input_save.clicked.connect(self.update_comments) self.input_save.setDefault(True) self.input_refresh = QPushButton( 'Reload', self.central_widget.meta_form_widget) self.input_refresh.clicked.connect(self.refresh_comments) self.meta_form_layout.addRow(self.input_save, self.input_refresh) if not self.isHidden() and self.image_viewer_hidden: self.image_widget.setVisible(False) def _load_spectrum2d_widget(self, spec2d_data, level2_data): if not spec2d_data: return xp2d = np.arange(spec2d_data.shape[1]) yp2d = np.repeat(0, spec2d_data.shape[1]) spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world( xp2d, yp2d) x_min = spectrum2d_disp.min() x_max = spectrum2d_disp.max() if self.slit_controller.has_slits and \ None not in self.slit_controller.y_bounds: y_min, y_max = self.slit_controller.y_bounds else: y_min = -0.5 y_max = spec2d_data.shape[0] - 0.5 self.extent = [x_min, x_max, y_min, y_max] # By default, displays the level 3 spectrum. The level 2 # data is plotted elsewhere, driven by the exposure_select # combo box signals. self.spectrum2d_widget.set_image(image=spec2d_data.get_component( spec2d_data.id['Flux']).data, interpolation='none', aspect='auto', extent=self.extent, origin='lower') self.spectrum2d_widget.axes.set_xlabel("Wavelength") self.spectrum2d_widget.axes.set_ylabel("Spatial Y") self.spectrum2d_widget._redraw() # If the axis are linked between the 1d and 2d views, setting the data # often ignores the initial bounds and instead uses the bounds of the # 2d data until the 1d view is moved. Force the 2d viewer to honor # the 1d view bounds. self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.emit( None, self.spectrum1d_widget.plot_widget.viewRange()[0]) # Populates the level 2 exposures combo box if level2_data: self.toolbar.exposure_select.clear() self.toolbar.exposure_select.addItems(['Level 3']) components = level2_data.main_components + level2_data.derived_components self.toolbar.exposure_select.addItems( [component.label for component in components]) self._set_exposure_navigation(0) else: self._set_exposure_navigation(None) @defer_draw def set_locked_axes(self, x=None, y=None): # Here we only change the setting if x or y are not None # since if set_locked_axes is called with eg. x=True, then # we shouldn't change the y setting. if x is not None: if x: # Lock the x axis if x is True def on_x_range_changed(xlim): self.spectrum2d_widget.axes.set_xlim(*xlim) self.spectrum2d_widget._redraw() self.spectrum1d_widget.plot_widget.getPlotItem( ).sigXRangeChanged.connect(lambda a, b: on_x_range_changed(b)) # Call the slot to update the axis linking initially # FIXME: Currently, this does not work for some reason. on_x_range_changed( self.spectrum1d_widget.plot_widget.viewRange()[0]) else: # Unlock the x axis if x is False self.spectrum1d_widget.plot_widget.getPlotItem( ).sigXRangeChanged.disconnect() if y is not None: self.spectrum2d_image_share.sharey = y self.spectrum2d_widget._redraw() self.image_widget._redraw() def layer_view(self): return self._layer_view def _text_changed(self): if self.textChangedAt is None: i = self.toolbar.source_select.currentIndex() self.textChangedAt = self._index_hash(i) def _check_unsaved_comments(self): if self.textChangedAt is None: return #Nothing to be changed i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) if self.textChangedAt == i: self.textChangedAt = None return #This is a refresh info = "Comments or flags changed but were not saved. Would you like to save them?" reply = QMessageBox.question(self, '', info, QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.update_comments(True) self.textChangedAt = None def _data_collection_index(self, label): idx = -1 for i, l in enumerate(self.session.data_collection): if l.label == label: idx = i break if idx == -1: return -1 self.data_idx = idx return idx def _index_hash(self, i): """Local selection index -> Table index""" if self.mask is not None: size = self.mask.size temp = np.arange(size) return temp[self.mask][i] else: return i def _id_to_index_hash(self, ID, l): """Object Name -> Table index""" for i, name in enumerate(l): if name == ID: return i return None def get_slit_dimensions_from_file(self): if self.catalog is None: return None if "slit_width" in self.catalog.meta["special_columns"] and \ "slit_length" in self.catalog.meta["special_columns"]: width = self.current_row[self.catalog.meta["special_columns"] ["slit_width"]] length = self.current_row[self.catalog.meta["special_columns"] ["slit_length"]] return [length, width] return None def get_slit_units_from_file(self): # TODO: Update once units infrastructure is in place return ["arcsec", "arcsec"] def get_comment(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("comments") return comp.labels[i] def get_flag(self): idx = self.data_idx i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) comp = self.session.data_collection[idx].get_component("flag") return comp.labels[i] def send_NumericalDataChangedMessage(self): idx = self.data_idx data = self.session.data_collection[idx] data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments")) def refresh_comments(self): self.input_flag.setText(self.get_flag()) self.input_comments.setPlainText(self.get_comment()) self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") self.textChangedAt = None def _get_save_path(self): """ Try to get save path from other MOSVizViewer instances """ for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.savepath is not None: if v.data_idx == self.data_idx: self.savepath = v.savepath break def _setup_save_path(self): """ Prompt the user for a file to save comments and flags into. """ fail = True success = False info = "Where would you like to save comments and flags?" option = pick_item( [0, 1], [os.path.basename(self.filepath), "New MOSViz Table file"], label=info, title="Comment Setup") if option == 0: self.savepath = self.filepath elif option == 1: dirname = os.path.dirname(self.filepath) path = compat.getsavefilename(caption="New MOSViz Table File", basedir=dirname, filters="*.txt")[0] if path == "": return fail self.savepath = path else: return fail for v in self.session.application.viewers[0]: if isinstance(v, MOSVizViewer): if v.data_idx == self.data_idx: v.savepath = self.savepath self._layer_view.refresh() return success def update_comments(self, pastSelection=False): """ Process comment and flag changes and save to file. Parameters ---------- pastSelection : bool True when updating past selections. Used when user forgets to save. """ if self.input_flag.text() == "": self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") return i = None try: i = int(self.input_flag.text()) except ValueError: self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);") info = QMessageBox.information(self, "Status:", "Flag must be an int!") return self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);") idx = self.data_idx if pastSelection: i = self.textChangedAt self.textChangedAt = None else: i = self.toolbar.source_select.currentIndex() i = self._index_hash(i) data = self.session.data_collection[idx] comp = data.get_component("comments") comp.labels.flags.writeable = True comp.labels[i] = self.input_comments.toPlainText() comp = data.get_component("flag") comp.labels.flags.writeable = True comp.labels[i] = self.input_flag.text() self.send_NumericalDataChangedMessage() self.write_comments() self.textChangedAt = None def _load_comments(self, label): """ Populate the comments and flag columns. Attempt to load comments from file. Parameters ---------- label : str The label of the data in session.data_collection. """ #Make sure its the right data #(beacuse subset data is masked) idx = self._data_collection_index(label) if idx == -1: return False data = self.session.data_collection[idx] #Fill in default comments: length = data.shape[0] new_comments = np.array(["" for i in range(length)], dtype=object) new_flags = np.array(["0" for i in range(length)], dtype=object) #Fill in any saved comments: meta = data.meta obj_names = data.get_component( self.catalog.meta["special_columns"]["source_id"]).labels if "MOSViz_comments" in meta.keys(): try: comments = meta["MOSViz_comments"] for key in comments.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = comments[key] new_comments[index] = line except Exception as e: print("MOSViz Comment Load Failed: ", e) if "MOSViz_flags" in meta.keys(): try: flags = meta["MOSViz_flags"] for key in flags.keys(): index = self._id_to_index_hash(key, obj_names) if index is not None: line = flags[key] new_flags[index] = line except Exception as e: print("MOSViz Flag Load Failed: ", e) #Send to DC data.add_component(CategoricalComponent(new_flags, "flag"), "flag") data.add_component(CategoricalComponent(new_comments, "comments"), "comments") return True def write_comments(self): """ Setup save file. Write comments and flags to file """ if self.savepath is None: fail = self._setup_save_path() if fail: return if self.savepath == -1: return #Do not save to file option idx = self.data_idx data = self.session.data_collection[idx] save_comments = data.get_component("comments").labels save_flag = data.get_component("flag").labels obj_names = data.get_component( self.catalog.meta["special_columns"]["source_id"]).labels fn = self.savepath folder = os.path.dirname(fn) t = astropy_table.data_to_astropy_table(data) #Check if load and save dir paths match temp = os.path.dirname(self.filepath) if not os.path.samefile(folder, temp): t['spectrum1d'].flags.writeable = True t['spectrum2d'].flags.writeable = True t['cutout'].flags.writeable = True for i in range(len(t)): t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i]) t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i]) t['cutout'][i] = os.path.abspath(t['cutout'][i]) try: t.remove_column("comments") t.remove_column("flag") keys = t.meta.keys() if "MOSViz_comments" in keys: t.meta.pop("MOSViz_comments") if "MOSViz_flags" in keys: t.meta.pop("MOSViz_flags") comments = OrderedDict() flags = OrderedDict() for i, line in enumerate(save_comments): if line != "": line = line.replace("\n", " ") key = str(obj_names[i]) comments[key] = line for i, line in enumerate(save_flag): if line != "0" and line != "": line = comments.replace("\n", " ") key = str(obj_names[i]) flags[key] = line if len(comments) > 0: t.meta["MOSViz_comments"] = comments if len(flags) > 0: t.meta["MOSViz_flags"] = flags t.write(fn, format="ascii.ecsv", overwrite=True) except Exception as e: print("Comment write failed:", e) def closeEvent(self, event): """ Clean up the extraneous data components created when opening the MOSViz viewer by overriding the parent class's close event. """ super(MOSVizViewer, self).closeEvent(event) for data in self._loaded_data.values(): self.session.data_collection.remove(data)
class ConfigurePresentationWindow(QWidget): def __init__(self, parent): super().__init__() self.parent = parent # set title self.setWindowTitle("Configure Presentation") # set variables self.setupVariables() # setup Hymn Lyrics from glob import glob from pathlib import Path self.books = sorted([ Path(filename).stem for filename in glob(r"./marvelData/books/Hymn Lyrics*.book") ]) if len(self.books) > 0: self.setMinimumHeight(550) # setup interface self.setupUI() def setupVariables(self): pass def setupUI(self): from functools import partial from qtpy.QtCore import Qt from qtpy.QtWidgets import QHBoxLayout, QFormLayout, QSlider, QPushButton, QPlainTextEdit, QCheckBox, QComboBox from qtpy.QtWidgets import QRadioButton, QWidget, QVBoxLayout, QListView, QSpacerItem, QSizePolicy layout = QHBoxLayout() layout1 = QFormLayout() self.fontsizeslider = QSlider(Qt.Horizontal) self.fontsizeslider.setMinimum(1) self.fontsizeslider.setMaximum(12) self.fontsizeslider.setTickInterval(2) self.fontsizeslider.setSingleStep(2) self.fontsizeslider.setValue(config.presentationFontSize / 0.5) self.fontsizeslider.setToolTip(str(config.presentationFontSize)) self.fontsizeslider.valueChanged.connect( self.presentationFontSizeChanged) layout1.addRow("Font Size", self.fontsizeslider) self.changecolorbutton = QPushButton() buttonStyle = "QPushButton {0}background-color: {2}; color: {3};{1}".format( "{", "}", config.presentationColorOnDarkTheme if config.theme == "dark" else config.presentationColorOnLightTheme, "white" if config.theme == "dark" else "black") self.changecolorbutton.setStyleSheet(buttonStyle) self.changecolorbutton.setToolTip("Change Color") self.changecolorbutton.clicked.connect(self.changeColor) layout1.addRow("Font Color", self.changecolorbutton) self.marginslider = QSlider(Qt.Horizontal) self.marginslider.setMinimum(0) self.marginslider.setMaximum(200) self.marginslider.setTickInterval(50) self.marginslider.setSingleStep(50) self.marginslider.setValue(config.presentationMargin) self.marginslider.setToolTip(str(config.presentationMargin)) self.marginslider.valueChanged.connect(self.presentationMarginChanged) layout1.addRow("Margin", self.marginslider) self.verticalpositionslider = QSlider(Qt.Horizontal) self.verticalpositionslider.setMinimum(10) self.verticalpositionslider.setMaximum(90) self.verticalpositionslider.setTickInterval(10) self.verticalpositionslider.setSingleStep(10) self.verticalpositionslider.setValue( config.presentationVerticalPosition) self.verticalpositionslider.setToolTip( str(config.presentationVerticalPosition)) self.verticalpositionslider.valueChanged.connect( self.presentationVerticalPositionChanged) layout1.addRow("Vertical Position", self.verticalpositionslider) self.horizontalpositionslider = QSlider(Qt.Horizontal) self.horizontalpositionslider.setMinimum(10) self.horizontalpositionslider.setMaximum(90) self.horizontalpositionslider.setTickInterval(10) self.horizontalpositionslider.setSingleStep(10) self.horizontalpositionslider.setValue( config.presentationHorizontalPosition) self.horizontalpositionslider.setToolTip( str(config.presentationHorizontalPosition)) self.horizontalpositionslider.valueChanged.connect( self.presentationHorizontalPositionChanged) layout1.addRow("Horizontal Position", self.horizontalpositionslider) self.showBibleSelection = QRadioButton() self.showBibleSelection.setChecked(True) self.showBibleSelection.clicked.connect( lambda: self.selectRadio("bible")) layout1.addRow("Bible", self.showBibleSelection) if len(self.books) > 0: self.showHymnsSelection = QRadioButton() self.showHymnsSelection.setChecked(False) self.showHymnsSelection.clicked.connect( lambda: self.selectRadio("hymns")) layout1.addRow("Hymns", self.showHymnsSelection) # Second column layout2 = QVBoxLayout() self.bibleWidget = QWidget() self.bibleLayout = QFormLayout() checkbox = QCheckBox() checkbox.setText("") checkbox.setChecked(config.presentationParser) checkbox.stateChanged.connect(self.presentationParserChanged) checkbox.setToolTip("Parse bible verse reference in the entered text") self.bibleLayout.addRow("Bible Reference", checkbox) versionCombo = QComboBox() self.bibleVersions = self.parent.textList versionCombo.addItems(self.bibleVersions) initialIndex = 0 if config.mainText in self.bibleVersions: initialIndex = self.bibleVersions.index(config.mainText) versionCombo.setCurrentIndex(initialIndex) versionCombo.currentIndexChanged.connect(self.changeBibleVersion) self.bibleLayout.addRow("Bible Version", versionCombo) self.textEntry = QPlainTextEdit("John 3:16; Rm 5:8") self.bibleLayout.addRow(self.textEntry) button = QPushButton("Presentation") button.setToolTip("Go to Presentation") button.clicked.connect(self.goToPresentation) self.bibleLayout.addWidget(button) self.bibleLayout.addItem( QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)) self.bibleWidget.setLayout(self.bibleLayout) self.hymnWidget = QWidget() self.hymnLayout = QFormLayout() selected = 0 book = "Hymn Lyrics - English" if book in self.books: selected = self.books.index(book) self.bookList = QComboBox() self.bookList.addItems(self.books) self.bookList.setCurrentIndex(selected) self.bookList.currentIndexChanged.connect(self.selectHymnBook) self.hymnLayout.addWidget(self.bookList) self.chapterlist = QListView() self.chapterlist.clicked.connect(self.selectHymn) # self.chapterlist.selectionModel().selectionChanged.connect(self.selectHymn) self.hymnLayout.addWidget(self.chapterlist) self.buttons = [] for count in range(0, 10): hymnButton = QPushButton() hymnButton.setText(" ") hymnButton.setEnabled(False) hymnButton.clicked.connect(partial(self.selectParagraph, count)) self.hymnLayout.addWidget(hymnButton) self.buttons.append(hymnButton) self.selectHymnBook(selected) self.hymnWidget.setLayout(self.hymnLayout) self.hymnWidget.hide() layout2.addWidget(self.bibleWidget) if len(self.books) > 0: layout2.addWidget(self.hymnWidget) layout.addLayout(layout1) layout.addLayout(layout2) self.setLayout(layout) def selectRadio(self, option): if option == "bible": self.bibleWidget.show() if len(self.books) > 0: self.hymnWidget.hide() elif option == "hymns": self.bibleWidget.hide() if len(self.books) > 0: self.hymnWidget.show() def selectHymnBook(self, option): from ToolsSqlite import Book from qtpy.QtCore import QStringListModel if len(self.books) > 0: self.hymnBook = self.books[option] self.hymns = sorted(Book(self.hymnBook).getTopicList()) self.chapterModel = QStringListModel(self.hymns) self.chapterlist.setModel(self.chapterModel) def selectHymn(self, option): from ToolsSqlite import Book row = option.row() self.hymn = self.hymns[row] book = Book(self.hymnBook) sections = book.getParagraphSectionsByChapter(self.hymn) count = 0 for button in self.buttons: if count < len(sections): section = sections[count] text = section.replace("<br>", "")[:30] button.setText(text) button.setEnabled(True) else: button.setText(" ") button.setEnabled(False) count += 1 def selectParagraph(self, paragraph): command = "SCREENBOOK:::{0}:::{1}:::{2}".format( self.hymnBook, self.hymn, paragraph) self.parent.runTextCommand(command) def goToPresentation(self): command = "SCREEN:::{0}".format(self.textEntry.toPlainText()) self.parent.runTextCommand(command) def changeColor(self): from qtpy.QtGui import QColor from qtpy.QtWidgets import QColorDialog color = QColorDialog.getColor( QColor(config.presentationColorOnDarkTheme if config.theme == "dark" else config.presentationColorOnLightTheme), self) if color.isValid(): colorName = color.name() if config.theme == "dark": config.presentationColorOnDarkTheme = colorName else: config.presentationColorOnLightTheme = colorName buttonStyle = "QPushButton {0}background-color: {2}; color: {3};{1}".format( "{", "}", colorName, "white" if config.theme == "dark" else "black") self.changecolorbutton.setStyleSheet(buttonStyle) def presentationFontSizeChanged(self, value): config.presentationFontSize = value * 0.5 self.fontsizeslider.setToolTip(str(config.presentationFontSize)) def presentationMarginChanged(self, value): config.presentationMargin = value self.marginslider.setToolTip(str(config.presentationMargin)) def presentationVerticalPositionChanged(self, value): config.presentationVerticalPosition = value self.verticalpositionslider.setToolTip( str(config.presentationVerticalPosition)) def presentationHorizontalPositionChanged(self, value): config.presentationHorizontalPosition = value self.horizontalpositionslider.setValue( config.presentationHorizontalPosition) def presentationParserChanged(self): config.presentationParser = not config.presentationParser def changeBibleVersion(self, index): if __name__ == '__main__': config.mainText = self.bibleVersions[index] else: command = "TEXT:::{0}".format(self.bibleVersions[index]) self.parent.runTextCommand(command)
class ZusatzFensterKerndaten(QWidget): def __init__(self, nummer, text): super().__init__() self.initMe(nummer, text) def initMe(self, nummer, text): self.l1 = QLabel(self) self.l1.setText('Inhalt der eingelesenen Zelle') self.l1.move(20, 5) self.nummer = nummer self.setGeometry(400, 300, 500, 700) self.zelle = QPlainTextEdit(self) self.zelle.setGeometry(0, 40, 500, 250) self.zelle.setPlainText(text) self.zelle.setReadOnly(True) self.l2 = QLabel(self) self.l2.setText( """Bitte geben Sie hier den Wert ein nach dem in der Zelle gesucht werden soll. Bsp. Wollen Sie einen Lastpunkt auslesen, welcher mit 5000 rpm angegeben ist, geben Sie 5000 ein. Achtung: keine Einheiten mit angeben. Nur Zahlen!""") self.l2.move(10, 330) self.eing = QLineEdit(self) self.eing.move(10, 410) p = QPushButton('Prüfen', self) p.clicked.connect(self.pruefen) p.move(180, 409) self.l3 = QLabel(self) self.l3.setText('vorangehende Zeichenkette') self.l3.move(10, 460) self.suchstring = QLineEdit(self) self.suchstring.move(180, 459) self.suchstring.setDisabled(True) self.l5 = QLabel(self) self.l5.setStyleSheet("background-color: yellow") self.l5.setText( "Prüfen Sie die vorrangehende Zeichenkette.\nSollte diese nicht stimmen, können Sie selbst eine angeben und erneut prüfen.\nAchtung: Leerzeichen nicht vergessen " ) self.l5.move(10, 490) self.l5.setVisible(False) self.l4 = QLabel(self) self.l4.setText('gefundener Eintrag') self.l4.move(10, 540) self.gefundener_string = QLineEdit(self) self.gefundener_string.move(180, 539) self.gefundener_string.setReadOnly(True) frage = QPushButton(self) frage.setIcon(QIcon("bilder_vorlagenersteller\\FrageIcon.png")) frage.move(450, 10) frage.clicked.connect(self.Hilfe) self.weiter = QPushButton('Weiter', self) self.weiter.move(420, 650) self.weiter.setDisabled(True) self.weiter.clicked.connect(self.weiter_gehts) def suchstring_finden(self): startindex = self.zelle.toPlainText().find(self.eing.text()) if startindex == 0: suchstring = '##Anfang###' elif startindex == -1: suchstring = 'ungültige Eingabe' else: suchstring = '' for i in range(0, 11): suchstring = self.zelle.toPlainText()[startindex - i] + suchstring if (startindex - i) == 0: break return suchstring[:-1] def pruefen(self): suchstring = self.suchstring.text() if suchstring == '': suchstring = self.suchstring_finden() print(suchstring) self.suchstring.setDisabled(False) self.l5.setVisible(True) self.weiter.setDisabled(False) self.suchstring.setText(suchstring) startindex = self.zelle.toPlainText().find(suchstring) + len( suchstring) ende = startindex + len(self.eing.text()) self.gefundener_string.setText( self.zelle.toPlainText()[startindex:ende]) def weiter_gehts(self): w.findChild(QLabel, self.nummer).setVisible(True) w.findChild(QLineEdit, 'suchstr' + self.nummer).setVisible(True) w.findChild(QLineEdit, 'suchstr' + self.nummer).setText(self.suchstring.text()) self.close() def Hilfe(self): self.h = HilfeFenster( "bilder_vorlagenersteller\\erweitertes_einlesen.png") self.h.show()