/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.dicom.viewer2d.mpr; import java.awt.Component; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JMenu; import javax.swing.JProgressBar; import javax.swing.JSeparator; import javax.swing.SwingUtilities; import org.dcm4che3.data.Tag; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.service.prefs.Preferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.explorer.DataExplorerView; import org.weasis.core.api.explorer.ObservableEvent; import org.weasis.core.api.gui.Insertable.Type; import org.weasis.core.api.gui.InsertableUtil; import org.weasis.core.api.gui.util.ActionState; import org.weasis.core.api.gui.util.ActionW; import org.weasis.core.api.gui.util.ComboItemListener; import org.weasis.core.api.gui.util.GuiExecutor; import org.weasis.core.api.gui.util.JMVUtils; import org.weasis.core.api.gui.util.SliderChangeListener; import org.weasis.core.api.gui.util.SliderCineListener; import org.weasis.core.api.image.GridBagLayoutModel; import org.weasis.core.api.image.LayoutConstraints; import org.weasis.core.api.media.data.MediaSeries; import org.weasis.core.api.media.data.MediaSeriesGroup; import org.weasis.core.api.media.data.Series; import org.weasis.core.api.service.BundlePreferences; import org.weasis.core.api.util.StringUtil; import org.weasis.core.api.util.StringUtil.Suffix; import org.weasis.core.ui.docking.DockableTool; import org.weasis.core.ui.docking.UIManager; import org.weasis.core.ui.editor.SeriesViewerListener; import org.weasis.core.ui.editor.image.CrosshairListener; import org.weasis.core.ui.editor.image.DefaultView2d; import org.weasis.core.ui.editor.image.ImageViewerPlugin; import org.weasis.core.ui.editor.image.MeasureToolBar; import org.weasis.core.ui.editor.image.MouseActions; import org.weasis.core.ui.editor.image.RotationToolBar; import org.weasis.core.ui.editor.image.SynchData; import org.weasis.core.ui.editor.image.SynchView; import org.weasis.core.ui.editor.image.ViewCanvas; import org.weasis.core.ui.editor.image.ViewerToolBar; import org.weasis.core.ui.editor.image.ZoomToolBar; import org.weasis.core.ui.util.ColorLayerUI; import org.weasis.core.ui.util.DefaultAction; import org.weasis.core.ui.util.PrintDialog; import org.weasis.core.ui.util.Toolbar; import org.weasis.dicom.codec.DicomImageElement; import org.weasis.dicom.codec.TagD; import org.weasis.dicom.codec.TagD.Level; import org.weasis.dicom.codec.geometry.ImageOrientation; import org.weasis.dicom.codec.geometry.ImageOrientation.Label; import org.weasis.dicom.explorer.DicomExplorer; import org.weasis.dicom.explorer.DicomModel; import org.weasis.dicom.explorer.print.DicomPrintDialog; import org.weasis.dicom.viewer2d.DcmHeaderToolBar; import org.weasis.dicom.viewer2d.EventManager; import org.weasis.dicom.viewer2d.LutToolBar; import org.weasis.dicom.viewer2d.Messages; import org.weasis.dicom.viewer2d.View2dContainer; import org.weasis.dicom.viewer2d.View2dFactory; import org.weasis.dicom.viewer2d.mpr.MprView.SliceOrientation; public class MPRContainer extends ImageViewerPlugin<DicomImageElement> implements PropertyChangeListener { private static final Logger LOGGER = LoggerFactory.getLogger(MPRContainer.class); public static final List<SynchView> SYNCH_LIST = Collections.synchronizedList(new ArrayList<SynchView>()); static SynchView DEFAULT_MPR; static { SYNCH_LIST.add(SynchView.NONE); HashMap<String, Boolean> actions = new HashMap<>(); actions.put(ActionW.SCROLL_SERIES.cmd(), true); actions.put(ActionW.RESET.cmd(), true); actions.put(ActionW.ZOOM.cmd(), true); actions.put(ActionW.WINDOW.cmd(), true); actions.put(ActionW.LEVEL.cmd(), true); actions.put(ActionW.PRESET.cmd(), true); actions.put(ActionW.LUT_SHAPE.cmd(), true); actions.put(ActionW.LUT.cmd(), true); actions.put(ActionW.INVERT_LUT.cmd(), true); actions.put(ActionW.FILTER.cmd(), true); DEFAULT_MPR = new SynchView("MPR synch", "mpr", SynchData.Mode.STACK, //$NON-NLS-1$ //$NON-NLS-2$ new ImageIcon(SynchView.class.getResource("/icon/22x22/tile.png")), actions); //$NON-NLS-1$ SYNCH_LIST.add(DEFAULT_MPR); } public static final GridBagLayoutModel VIEWS_2x1_mpr = new GridBagLayoutModel( new LinkedHashMap<LayoutConstraints, Component>(3), "mpr", Messages.getString("MPRContainer.title"), null); //$NON-NLS-1$ //$NON-NLS-2$ static { Map<LayoutConstraints, Component> constraints = VIEWS_2x1_mpr.getConstraints(); constraints.put(new LayoutConstraints(MprView.class.getName(), 0, 0, 0, 1, 2, 0.5, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH), null); constraints.put(new LayoutConstraints(MprView.class.getName(), 1, 1, 0, 1, 1, 0.5, 0.5, GridBagConstraints.CENTER, GridBagConstraints.BOTH), null); constraints.put(new LayoutConstraints(MprView.class.getName(), 2, 1, 1, 1, 1, 0.5, 0.5, GridBagConstraints.CENTER, GridBagConstraints.BOTH), null); } public static final List<GridBagLayoutModel> LAYOUT_LIST = Collections.synchronizedList(new ArrayList<GridBagLayoutModel>()); static { LAYOUT_LIST.add(VIEWS_2x1_mpr); LAYOUT_LIST.add(VIEWS_2x2_f2); LAYOUT_LIST.add(VIEWS_2_f1x2); } // Static tools shared by all the View2dContainer instances, tools are registered when a container is selected // Do not initialize tools in a static block (order initialization issue with eventManager), use instead a lazy // initialization with a method. public static final List<Toolbar> TOOLBARS = Collections.synchronizedList(new ArrayList<Toolbar>()); public static final List<DockableTool> TOOLS = View2dContainer.TOOLS; private static volatile boolean initComponents = false; private volatile Thread process; private volatile String lastCommand; public MPRContainer() { this(VIEWS_1x1, null); } public MPRContainer(GridBagLayoutModel layoutModel, String uid) { super(EventManager.getInstance(), layoutModel, uid, MPRFactory.NAME, MPRFactory.ICON, null); setSynchView(SynchView.NONE); if (!initComponents) { initComponents = true; // Add standard toolbars // WProperties props = (WProperties) BundleTools.SYSTEM_PREFERENCES.clone(); // props.putBooleanProperty("weasis.toolbar.synchbouton", false); //$NON-NLS-1$ EventManager evtMg = EventManager.getInstance(); TOOLBARS.add(View2dContainer.TOOLBARS.get(0)); TOOLBARS.add(new MeasureToolBar(evtMg, 11)); TOOLBARS.add(new ZoomToolBar(evtMg, 20, true)); TOOLBARS.add(new RotationToolBar(evtMg, 30)); TOOLBARS.add(new DcmHeaderToolBar(evtMg, 35)); TOOLBARS.add(new LutToolBar(evtMg, 40)); final BundleContext context = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); Preferences prefs = BundlePreferences.getDefaultPreferences(context); if (prefs != null) { String className = this.getClass().getSimpleName().toLowerCase(); InsertableUtil.applyPreferences(TOOLBARS, prefs, context.getBundle().getSymbolicName(), className, Type.TOOLBAR); } } } @Override public void setSelectedImagePaneFromFocus(ViewCanvas<DicomImageElement> defaultView2d) { setSelectedImagePane(defaultView2d); } @Override public JMenu fillSelectedPluginMenu(JMenu menuRoot) { if (menuRoot != null) { menuRoot.removeAll(); if (eventManager instanceof EventManager) { EventManager manager = (EventManager) eventManager; int count = menuRoot.getItemCount(); JMVUtils.addItemToMenu(menuRoot, manager.getPresetMenu("weasis.pluginMenu.presets")); //$NON-NLS-1$ JMVUtils.addItemToMenu(menuRoot, manager.getLutShapeMenu("weasis.pluginMenu.lutShape")); //$NON-NLS-1$ JMVUtils.addItemToMenu(menuRoot, manager.getLutMenu("weasis.pluginMenu.lut")); //$NON-NLS-1$ JMVUtils.addItemToMenu(menuRoot, manager.getLutInverseMenu("weasis.pluginMenu.invertLut")); //$NON-NLS-1$ JMVUtils.addItemToMenu(menuRoot, manager.getFilterMenu("weasis.pluginMenu.filter")); //$NON-NLS-1$ if (count < menuRoot.getItemCount()) { menuRoot.add(new JSeparator()); count = menuRoot.getItemCount(); } JMVUtils.addItemToMenu(menuRoot, manager.getZoomMenu("weasis.pluginMenu.zoom")); //$NON-NLS-1$ JMVUtils.addItemToMenu(menuRoot, manager.getOrientationMenu("weasis.pluginMenu.orientation")); //$NON-NLS-1$ if (count < menuRoot.getItemCount()) { menuRoot.add(new JSeparator()); count = menuRoot.getItemCount(); } menuRoot.add(manager.getResetMenu("weasis.pluginMenu.reset")); //$NON-NLS-1$ } } return menuRoot; } @Override public List<DockableTool> getToolPanel() { return TOOLS; } @Override public void setSelected(boolean selected) { final ViewerToolBar toolBar = getViewerToolBar(); if (selected) { if (toolBar != null) { String command = ActionW.CROSSHAIR.cmd(); MouseActions mouseActions = eventManager.getMouseActions(); String lastAction = mouseActions.getAction(MouseActions.LEFT); if (!command.equals(lastAction)) { lastCommand = lastAction; mouseActions.setAction(MouseActions.LEFT, command); setMouseActions(mouseActions); toolBar.changeButtonState(MouseActions.LEFT, command); } } eventManager.setSelectedView2dContainer(this); // Send event to select the related patient in Dicom Explorer. DataExplorerView dicomView = UIManager.getExplorerplugin(DicomExplorer.NAME); if (dicomView != null && dicomView.getDataExplorerModel() instanceof DicomModel) { dicomView.getDataExplorerModel().firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.SELECT, this, null, getGroupID())); } } else { if (lastCommand != null && toolBar != null) { MouseActions mouseActions = eventManager.getMouseActions(); if (ActionW.CROSSHAIR.cmd().equals(mouseActions.getAction(MouseActions.LEFT))) { mouseActions.setAction(MouseActions.LEFT, lastCommand); setMouseActions(mouseActions); toolBar.changeButtonState(MouseActions.LEFT, lastCommand); lastCommand = null; } } eventManager.setSelectedView2dContainer(null); } } private boolean closeIfNoContent() { if (getOpenSeries().isEmpty()) { close(); handleFocusAfterClosing(); return true; } return false; } @Override public void close() { if (process != null) { final Thread t = process; process = null; t.interrupt(); } MPRFactory.closeSeriesViewer(this); super.close(); } @Override public void propertyChange(PropertyChangeEvent evt) { if (evt instanceof ObservableEvent) { ObservableEvent event = (ObservableEvent) evt; ObservableEvent.BasicAction action = event.getActionCommand(); Object newVal = event.getNewValue(); if (ObservableEvent.BasicAction.REMOVE.equals(action)) { if (newVal instanceof MediaSeriesGroup) { MediaSeriesGroup group = (MediaSeriesGroup) newVal; // Patient Group if (TagD.getUID(Level.PATIENT).equals(group.getTagID())) { if (group.equals(getGroupID())) { // Close the content of the plug-in close(); handleFocusAfterClosing(); } } // Study Group else if (TagD.getUID(Level.STUDY).equals(group.getTagID())) { if (event.getSource() instanceof DicomModel) { DicomModel model = (DicomModel) event.getSource(); for (ViewCanvas<DicomImageElement> v : view2ds) { if (group.equals(model.getParent(v.getSeries(), DicomModel.study))) { v.setSeries(null); if (closeIfNoContent()) { return; } } } } } // Series Group else if (TagD.getUID(Level.SERIES).equals(group.getTagID())) { for (ViewCanvas<DicomImageElement> v : view2ds) { if (newVal.equals(v.getSeries())) { v.setSeries(null); if (closeIfNoContent()) { return; } } } } } } else if (ObservableEvent.BasicAction.REPLACE.equals(action)) { if (newVal instanceof Series) { Series series = (Series) newVal; for (ViewCanvas<DicomImageElement> v : view2ds) { MediaSeries<DicomImageElement> s = v.getSeries(); if (series.equals(s)) { // It will reset MIP view v.setSeries(series, null); } } } } } } @Override public int getViewTypeNumber(GridBagLayoutModel layout, Class<?> defaultClass) { return View2dFactory.getViewTypeNumber(layout, defaultClass); } @Override public boolean isViewType(Class<?> defaultClass, String type) { if (defaultClass != null) { try { Class<?> clazz = Class.forName(type); return defaultClass.isAssignableFrom(clazz); } catch (Exception e) { LOGGER.error("Checking view type", e); //$NON-NLS-1$ } } return false; } @Override public DefaultView2d<DicomImageElement> createDefaultView(String classType) { return new MprView(eventManager); } @Override public JComponent createUIcomponent(String clazz) { if (isViewType(DefaultView2d.class, clazz)) { return createDefaultView(clazz); } try { // FIXME use classloader.loadClass or injection Class<?> cl = Class.forName(clazz); JComponent component = (JComponent) cl.newInstance(); if (component instanceof SeriesViewerListener) { eventManager.addSeriesViewerListener((SeriesViewerListener) component); } return component; } catch (Exception e) { LOGGER.error("Cannot create {}", clazz, e); //$NON-NLS-1$ } return null; } @Override public synchronized List<Toolbar> getToolBar() { return TOOLBARS; } @Override public List<Action> getExportActions() { return selectedImagePane == null ? super.getExportActions() : selectedImagePane.getExportToClipboardAction(); } @Override public List<Action> getPrintActions() { ArrayList<Action> actions = new ArrayList<>(1); final String title = Messages.getString("View2dContainer.print_layout"); //$NON-NLS-1$ DefaultAction printStd = new DefaultAction(title, new ImageIcon(ImageViewerPlugin.class.getResource("/icon/16x16/printer.png")), event -> { //$NON-NLS-1$ ColorLayerUI layer = ColorLayerUI.createTransparentLayerUI(MPRContainer.this); PrintDialog<DicomImageElement> dialog = new PrintDialog<>(SwingUtilities.getWindowAncestor(MPRContainer.this), title, eventManager); ColorLayerUI.showCenterScreen(dialog, layer); }); actions.add(printStd); final String title2 = Messages.getString("View2dContainer.dcm_print"); //$NON-NLS-1$ DefaultAction printStd2 = new DefaultAction(title2, null, event -> { ColorLayerUI layer = ColorLayerUI.createTransparentLayerUI(MPRContainer.this); DicomPrintDialog<?> dialog = new DicomPrintDialog<>(SwingUtilities.getWindowAncestor(MPRContainer.this), title2, eventManager); ColorLayerUI.showCenterScreen(dialog, layer); }); actions.add(printStd2); return actions; } public MprView getMprView(SliceOrientation sliceOrientation) { for (ViewCanvas v : view2ds) { if (v instanceof MprView) { if (sliceOrientation != null && sliceOrientation.equals(((MprView) v).getSliceOrientation())) { return (MprView) v; } } } return null; } @Override public void addSeries(MediaSeries<DicomImageElement> sequence) { if (process != null) { final Thread t = process; process = null; t.interrupt(); } // TODO Should be init elsewhere for (int i = 0; i < view2ds.size(); i++) { ViewCanvas<DicomImageElement> val = view2ds.get(i); if (val instanceof MprView) { SliceOrientation sliceOrientation; switch (i) { case 1: sliceOrientation = SliceOrientation.CORONAL; break; case 2: sliceOrientation = SliceOrientation.SAGITTAL; break; default: sliceOrientation = SliceOrientation.AXIAL; break; } ((MprView) val).setType(sliceOrientation); } } final MprView view = selectLayoutPositionForAddingSeries(sequence); if (view != null) { view.setSeries(sequence); String title = TagD.getTagValue(sequence, Tag.PatientName, String.class); if (title != null) { this.getDockable().setTitleToolTip(title); this.setPluginName(StringUtil.getTruncatedString(title, 25, Suffix.THREE_PTS)); } view.repaint(); process = new Thread(Messages.getString("MPRContainer.build")) { //$NON-NLS-1$ @Override public void run() { try { SeriesBuilder.createMissingSeries(this, MPRContainer.this, view); // Following actions need to be executed in EDT thread GuiExecutor.instance().execute(new Runnable() { @Override public void run() { ActionState synch = eventManager.getAction(ActionW.SYNCH); if (synch instanceof ComboItemListener) { ((ComboItemListener) synch).setSelectedItem(MPRContainer.DEFAULT_MPR); } // Set the middle image (best choice to propagate the default preset of non CT // modalities) ActionState seqAction = eventManager.getAction(ActionW.SCROLL_SERIES); if (seqAction instanceof SliderChangeListener) { SliderCineListener sliceAction = (SliderCineListener) seqAction; sliceAction.setSliderValue(sliceAction.getSliderMax() / 2); } ActionState cross = eventManager.getAction(ActionW.CROSSHAIR); if (cross instanceof CrosshairListener) { ((CrosshairListener) cross).setPoint( view.getImageCoordinatesFromMouse(view.getWidth() / 2, view.getHeight() / 2)); } // Force to propagate the default preset ActionState presetAction = eventManager.getAction(ActionW.PRESET); if (presetAction instanceof ComboItemListener) { ComboItemListener p = (ComboItemListener) presetAction; p.setSelectedItemWithoutTriggerAction(null); p.setSelectedItem(p.getFirstItem()); } } }); } catch (final Exception e) { e.printStackTrace(); // Following actions need to be executed in EDT thread GuiExecutor.instance().execute(() -> showErrorMessage(view2ds, view, e.getMessage())); } } }; process.start(); } else { showErrorMessage(view2ds, null, Messages.getString("MPRContainer.mesg_missing_3d")); //$NON-NLS-1$ } } public static void showErrorMessage(List<ViewCanvas<DicomImageElement>> view2ds, DefaultView2d<DicomImageElement> view, String message) { for (ViewCanvas<DicomImageElement> v : view2ds) { if (v != view && v instanceof MprView) { JProgressBar bar = ((MprView) v).getProgressBar(); if (bar == null) { bar = new JProgressBar(); Dimension dim = new Dimension(v.getJComponent().getWidth() / 2, 30); bar.setSize(dim); bar.setPreferredSize(dim); bar.setMaximumSize(dim); bar.setValue(0); bar.setStringPainted(true); ((MprView) v).setProgressBar(bar); } bar.setString(message); v.getJComponent().repaint(); } } } @Override public void addSeriesList(List<MediaSeries<DicomImageElement>> seriesList, boolean removeOldSeries) { if (seriesList != null && !seriesList.isEmpty()) { addSeries(seriesList.get(0)); } } @Override public void selectLayoutPositionForAddingSeries(List<MediaSeries<DicomImageElement>> seriesList) { // Do it in addSeries() } public MprView selectLayoutPositionForAddingSeries(MediaSeries<DicomImageElement> s) { if (s != null) { Object img = s.getMedia(MediaSeries.MEDIA_POSITION.MIDDLE, null, null); if (img instanceof DicomImageElement) { double[] v = TagD.getTagValue((DicomImageElement) img, Tag.ImageOrientationPatient, double[].class); if (v != null && v.length == 6) { Label orientation = ImageOrientation.makeImageOrientationLabelFromImageOrientationPatient(v[0], v[1], v[2], v[3], v[4], v[5]); SliceOrientation sliceOrientation = SliceOrientation.AXIAL; if (ImageOrientation.Label.CORONAL.equals(orientation)) { sliceOrientation = SliceOrientation.CORONAL; } else if (ImageOrientation.Label.SAGITTAL.equals(orientation)) { sliceOrientation = SliceOrientation.SAGITTAL; } MprView view = getMprView(sliceOrientation); if (view != null) { setSelectedImagePane(view); return view; } } } } return null; } @Override public List<SynchView> getSynchList() { return SYNCH_LIST; } @Override public List<GridBagLayoutModel> getLayoutList() { return LAYOUT_LIST; } }