/* * ------------------------------------------------------------------------ * * Copyright (C) 2003 - 2016 * University of Konstanz, Germany and * KNIME GmbH, Konstanz, Germany * Website: http://www.knime.org; Email: contact@knime.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, Version 3, as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see <http://www.gnu.org/licenses>. * * Additional permission under GNU GPL version 3 section 7: * * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. * Hence, KNIME and ECLIPSE are both independent programs and are not * derived from each other. Should, however, the interpretation of the * GNU GPL Version 3 ("License") under any applicable laws result in * KNIME and ECLIPSE being a combined program, KNIME GMBH herewith grants * you the additional permission to use and propagate KNIME together with * ECLIPSE with only the license terms in place for ECLIPSE applying to * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the * license terms of ECLIPSE themselves allow for the respective use and * propagation of ECLIPSE together with KNIME. * * Additional permission relating to nodes for KNIME that extend the Node * Extension (and in particular that are based on subclasses of NodeModel, * NodeDialog, and NodeView) and that only interoperate with KNIME through * standard APIs ("Nodes"): * Nodes are deemed to be separate and independent programs and to not be * covered works. Notwithstanding anything to the contrary in the * License, the License does not apply to Nodes, you are not required to * license Nodes under the License, and you are granted a license to * prepare and propagate Nodes, in each case even if such Nodes are * propagated with or for interoperation with KNIME. The owner of a Node * may freely choose the license terms applicable to such Node, including * when such Node is propagated with or for interoperation with KNIME. * --------------------------------------------------------------------- * * Created on Feb 28, 2016 by hornm */ package org.knime.knip.base.nodes.proc.binner; import static java.lang.Double.NEGATIVE_INFINITY; import static java.lang.Double.POSITIVE_INFINITY; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.text.ParseException; import javax.swing.BorderFactory; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.ListSelectionModel; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.knime.base.node.preproc.pmml.binner.BinnerNodeDialogPane; import org.knime.core.data.DataType; import org.knime.core.data.def.DoubleCell; import org.knime.core.data.def.IntCell; import org.knime.core.node.InvalidSettingsException; import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeSettingsRO; import org.knime.core.node.NodeSettingsWO; import org.knime.core.node.NotConfigurableException; import org.knime.core.node.port.PortObjectSpec; import org.knime.knip.base.data.img.ImgPlusValue; import org.knime.knip.base.node.ValueToCellNodeDialog; import net.imglib2.type.numeric.RealType; /** * * NB: A lot of code has been copied from {@link BinnerNodeDialogPane}. * * @author <a href="mailto:horn_martin@gmx.de">Martin Horn</a> */ public class IntensityBinnerNodeDialog<T extends RealType<T>> extends ValueToCellNodeDialog<ImgPlusValue<T>> { /** The node logger for this class. */ private static final NodeLogger LOGGER = NodeLogger.getLogger(BinnerNodeDialogPane.class); private final IntervalPanel m_intervalPanel; private final JLabel m_info; /** * Creates a new binner dialog. */ public IntensityBinnerNodeDialog() { super(true); // panel in tab final JPanel intervalTabPanel = new JPanel(new BorderLayout()); m_intervalPanel = new IntervalPanel(intervalTabPanel, DoubleCell.TYPE); // numeric column intervals m_intervalPanel.setBorder(BorderFactory.createTitledBorder("Intervals")); m_intervalPanel.setMinimumSize(new Dimension(350, 300)); m_intervalPanel.setPreferredSize(new Dimension(350, 300)); intervalTabPanel.add(m_intervalPanel, BorderLayout.CENTER); final JPanel infoPanel = new JPanel(); infoPanel.setBorder(BorderFactory.createTitledBorder("Result Pixel Type")); m_info = new JLabel(""); infoPanel.add(m_info); intervalTabPanel.add(infoPanel, BorderLayout.SOUTH); super.addTab(" Intervals ", intervalTabPanel); super.buildDialog(); } private SpinnerNumberModel createNumberModel(final DataType type) { if (IntCell.TYPE.equals(type)) { return new SpinnerNumberModel(0, null, null, 1); } return new SpinnerNumberModel(0.0, NEGATIVE_INFINITY, POSITIVE_INFINITY, 0.1); } /** * Creates new panel holding the binning gui. */ final class IntervalPanel extends JPanel { /** List of intervals. */ private final JList m_intervalList; /** The intervals' model. */ private final DefaultListModel m_intervalMdl; /** * Create new interval panel. * * @param column the current column name * @param parent used to refresh column list is number of bins has changed * @param type the type for the spinner model * */ IntervalPanel(final Component parent, final DataType type) { super(new BorderLayout()); setBorder(BorderFactory.createTitledBorder(" " + "Title" + " ")); m_intervalMdl = new DefaultListModel(); m_intervalList = new JList(m_intervalMdl); Font font = new Font("Monospaced", Font.PLAIN, 12); m_intervalList.setFont(font); final JButton addButton = new JButton("Add"); addButton.addActionListener(new ActionListener() { /** * */ @Override public void actionPerformed(final ActionEvent e) { final int size = m_intervalMdl.getSize(); // if the first interval is added if (size == 0) { m_intervalMdl.addElement(new IntervalItemPanel(IntervalPanel.this, null, null, 0.0, type)); } else { // if the first interval needs to be split if (size == 1) { IntervalItemPanel p = new IntervalItemPanel(IntervalPanel.this, 0.0, null, 1.0, type); m_intervalMdl.addElement(p); p.updateInterval(); } else { Object o = m_intervalList.getSelectedValue(); // if non is selected or the last one is selected if (o == null || m_intervalMdl.indexOf(o) == size - 1) { IntervalItemPanel p1 = (IntervalItemPanel)m_intervalMdl.getElementAt(size - 1); double d = p1.getLeftValue(false); IntervalItemPanel p = new IntervalItemPanel(IntervalPanel.this, d, POSITIVE_INFINITY, (size + 1.0), type); m_intervalMdl.insertElementAt(p, size); p.updateInterval(); } else { IntervalItemPanel p1 = (IntervalItemPanel)o; IntervalItemPanel p2 = (IntervalItemPanel)m_intervalMdl.getElementAt(m_intervalMdl.indexOf(p1) + 1); double d1 = p1.getRightValue(false); double d2 = p2.getLeftValue(false); IntervalItemPanel p = new IntervalItemPanel(IntervalPanel.this, d1, d2, +(size + 1.0), type); m_intervalMdl.insertElementAt(p, m_intervalMdl.indexOf(p1) + 1); p.updateInterval(); } } } updateInfo(); parent.validate(); parent.repaint(); } }); final JButton removeButton = new JButton("Remove"); removeButton.addActionListener(new ActionListener() { /** * */ @Override public void actionPerformed(final ActionEvent e) { IntervalItemPanel p = (IntervalItemPanel)m_intervalList.getSelectedValue(); if (p != null) { int i = m_intervalMdl.indexOf(p); m_intervalMdl.removeElement(p); int size = m_intervalMdl.getSize(); if (size > 0) { if (size == 1 || size == i) { m_intervalList.setSelectedIndex(size - 1); } else { m_intervalList.setSelectedIndex(i); } ((IntervalItemPanel)m_intervalList.getSelectedValue()).updateInterval(); } updateInfo(); parent.validate(); parent.repaint(); } } }); final JPanel buttonPanel = new JPanel(new GridLayout(1, 2)); buttonPanel.add(addButton); buttonPanel.add(removeButton); super.add(buttonPanel, BorderLayout.NORTH); // // interval list // final JPanel selInterval = new JPanel(new GridLayout(1, 1)); selInterval.add(new IntervalItemPanel(this, null, null, null, type)); selInterval.validate(); selInterval.repaint(); m_intervalList.addListSelectionListener(new ListSelectionListener() { /** * */ @Override public void valueChanged(final ListSelectionEvent e) { selInterval.removeAll(); Object o = m_intervalList.getSelectedValue(); if (o == null) { selInterval.add(new IntervalItemPanel(IntervalPanel.this, null, null, null, type)); } else { selInterval.add((IntervalItemPanel)o); } m_intervalPanel.validate(); m_intervalPanel.repaint(); } }); m_intervalList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); final JScrollPane intervalScroll = new JScrollPane(m_intervalList); intervalScroll.setMinimumSize(new Dimension(200, 155)); intervalScroll.setPreferredSize(new Dimension(200, 155)); super.add(intervalScroll, BorderLayout.CENTER); JPanel southPanel = new JPanel(new BorderLayout()); southPanel.add(selInterval, BorderLayout.CENTER); super.add(southPanel, BorderLayout.SOUTH); } private void addIntervalItem(final IntervalItemPanel item) { m_intervalMdl.addElement(item); // m_intervalMdl.insertElementAt(item, m_intervalMdl.getSize()); // item.updateInterval(); } private void removeAllIntervalItems() { m_intervalMdl.clear(); } /** * @param item the current interval item * @return the previous one in the list or <code>null</code> */ private IntervalItemPanel getPrevious(final IntervalItemPanel item) { int i = m_intervalMdl.indexOf(item); if (i > 0) { return (IntervalItemPanel)m_intervalMdl.getElementAt(i - 1); } return null; } /** * @param item the current interval item * @return the next one in the list or <code>null</code> */ private IntervalItemPanel getNext(final IntervalItemPanel item) { int i = m_intervalMdl.indexOf(item); if (i >= 0 && i + 1 < m_intervalMdl.getSize()) { return (IntervalItemPanel)m_intervalMdl.getElementAt(i + 1); } return null; } /** * @return number of interval specified for binning */ public int getNumIntervals() { return m_intervalMdl.getSize(); } /** * @param i index for interval * @return the interval item */ public IntervalItemPanel getInterval(final int i) { return (IntervalItemPanel)m_intervalMdl.get(i); } } /** * Creates a new panel holding one interval. */ final class IntervalItemPanel extends JPanel { private final IntervalPanel m_parent; private final JComboBox m_borderLeft = new JComboBox(); private final JSpinner m_left; private final JSpinner m_right; private final JComboBox m_borderRight = new JComboBox(); private final JSpinner m_bin; /** Left/open or right/closed interval bracket. */ static final String LEFT = "]"; /** Right/open or left/closed interval bracket. */ static final String RIGHT = "["; /** * @param parent the interval item's parent component * @param leftOpen initial left open * @param left initial left value * @param rightOpen initial right open * @param right initial right value * @param binValue the bin value * @param type the column type of this interval */ IntervalItemPanel(final IntervalPanel parent, final boolean leftOpen, final Double left, final boolean rightOpen, final Double right, final Double binValue, final DataType type) { this(parent, left, right, binValue, type); setLeftOpen(leftOpen); setRightOpen(rightOpen); } /** * @param parent the interval item's parent component * @param left initial left value * @param right initial right value * @param binValue the bin value * @param type the column type of this interval */ IntervalItemPanel(final IntervalPanel parent, final Double left, final Double right, final Double binValue, final DataType type) { this(parent, type); if (binValue == null) { m_bin.setValue(0); m_bin.setEnabled(false); } else { m_bin.setValue(binValue); } JPanel p1 = new JPanel(new BorderLayout()); p1.add(m_bin, BorderLayout.CENTER); p1.add(new JLabel(" : "), BorderLayout.EAST); super.add(p1); JPanel p2 = new JPanel(new BorderLayout()); p2.add(m_borderLeft, BorderLayout.WEST); p2.add(m_left, BorderLayout.CENTER); p2.add(new JLabel(" ."), BorderLayout.EAST); setLeftValue(left); super.add(p2); JPanel p3 = new JPanel(new BorderLayout()); p3.add(new JLabel(". "), BorderLayout.WEST); p3.add(m_right, BorderLayout.CENTER); p3.add(m_borderRight, BorderLayout.EAST); setRightValue(right); super.add(p3); initListener(); } /* * @param parent the interval item's parent component */ private IntervalItemPanel(final IntervalPanel parent, final DataType type) { super(new GridLayout(1, 0)); m_parent = parent; m_bin = new JSpinner(createNumberModel(type)); m_bin.setPreferredSize(new Dimension(50, 25)); JSpinner.DefaultEditor editorBin = new JSpinner.NumberEditor(m_bin, "0.0##############"); editorBin.getTextField().setColumns(15); m_bin.setEditor(editorBin); m_bin.setPreferredSize(new Dimension(125, 25)); m_left = new JSpinner(createNumberModel(type)); JSpinner.DefaultEditor editorLeft = new JSpinner.NumberEditor(m_left, "0.0##############"); editorLeft.getTextField().setColumns(15); m_left.setEditor(editorLeft); m_left.setPreferredSize(new Dimension(125, 25)); m_right = new JSpinner(createNumberModel(type)); JSpinner.DefaultEditor editorRight = new JSpinner.NumberEditor(m_right, "0.0##############"); editorRight.getTextField().setColumns(15); m_right.setEditor(editorRight); m_right.setPreferredSize(new Dimension(125, 25)); m_borderLeft.setPreferredSize(new Dimension(50, 25)); m_borderLeft.setLightWeightPopupEnabled(false); m_borderLeft.addItem(RIGHT); m_borderLeft.addItem(LEFT); m_borderRight.setPreferredSize(new Dimension(50, 25)); m_borderRight.setLightWeightPopupEnabled(false); m_borderRight.addItem(LEFT); m_borderRight.addItem(RIGHT); } private void initListener() { m_left.addChangeListener(new ChangeListener() { @Override public void stateChanged(final ChangeEvent e) { repairLeft(); } }); final JSpinner.DefaultEditor editorLeft = (JSpinner.DefaultEditor)m_left.getEditor(); editorLeft.getTextField().addFocusListener(new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { getLeftValue(true); repairLeft(); } @Override public void focusGained(final FocusEvent e) { } }); m_right.addChangeListener(new ChangeListener() { @Override public void stateChanged(final ChangeEvent e) { repairRight(); } }); final JSpinner.DefaultEditor editorRight = (JSpinner.DefaultEditor)m_right.getEditor(); editorRight.getTextField().addFocusListener(new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { getRightValue(true); repairRight(); } @Override public void focusGained(final FocusEvent e) { } }); m_borderLeft.addItemListener(new ItemListener() { @Override public void itemStateChanged(final ItemEvent e) { IntervalItemPanel prev = m_parent.getPrevious(IntervalItemPanel.this); if (prev != null && prev.isRightOpen() == isLeftOpen()) { prev.setRightOpen(!isLeftOpen()); } myRepaint(); } }); m_borderRight.addItemListener(new ItemListener() { @Override public void itemStateChanged(final ItemEvent e) { IntervalItemPanel next = m_parent.getNext(IntervalItemPanel.this); if (next != null && next.isLeftOpen() == isRightOpen()) { next.setLeftOpen(!isRightOpen()); } myRepaint(); } }); m_bin.addChangeListener(new ChangeListener() { @Override public void stateChanged(final ChangeEvent e) { updateInfo(); myRepaint(); } }); final JSpinner.DefaultEditor editorBin = (JSpinner.DefaultEditor)m_bin.getEditor(); editorRight.getTextField().addFocusListener(new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { getBinValue(); myRepaint(); } @Override public void focusGained(final FocusEvent e) { } }); } private void repairLeft() { double l = getLeftValue(false); double r = getRightValue(true); if (l > r) { setRightValue(l); repairNext(l); } repairPrev(l); myRepaint(); } private void repairRight() { double r = getRightValue(false); double l = getLeftValue(true); if (l > r) { repairPrev(r); setLeftValue(r); } repairNext(r); myRepaint(); } /** * @return the name for this interval bin */ public Double getBinValue() { return (Double)m_bin.getValue(); } /** * Checks the current, previous, and next interval for consistency; and updates the intervals if necessary. */ public void updateInterval() { IntervalItemPanel prev = m_parent.getPrevious(this); IntervalItemPanel next = m_parent.getNext(this); if (prev == null && next == null) { this.setLeftValue(null); this.setRightValue(null); this.setLeftOpen(true); this.setRightOpen(true); } else { repairPrev(getLeftValue(true)); repairNext(getRightValue(true)); } myRepaint(); } private void myRepaint() { m_intervalPanel.validate(); m_intervalPanel.repaint(); } private void repairPrev(final double value) { IntervalItemPanel prev = m_parent.getPrevious(this); if (prev != null) { if (prev.getRightValue(false) != value) { prev.setRightValue(value); if (prev.getLeftValue(false) > value) { prev.setLeftValue(value); } } if (prev.isRightOpen() == isLeftOpen()) { prev.setRightOpen(!isLeftOpen()); } } else { setLeftValue(null); setLeftOpen(true); } } private void repairNext(final double value) { IntervalItemPanel next = m_parent.getNext(this); if (next != null) { if (next.getLeftValue(false) != value) { next.setLeftValue(value); if (next.getRightValue(false) < value) { next.setRightValue(value); } } if (next.isLeftOpen() == isRightOpen()) { next.setLeftOpen(!isRightOpen()); } } else { setRightValue(null); setRightOpen(true); } } /** * @param left new left value */ public void setLeftValue(final Double left) { if (left == null || left.doubleValue() == NEGATIVE_INFINITY) { m_borderLeft.setSelectedItem(LEFT); m_borderLeft.setEnabled(false); m_left.setValue(NEGATIVE_INFINITY); m_left.setEnabled(false); } else { m_left.setValue(left); m_left.setEnabled(true); m_borderLeft.setEnabled(true); } } /** * @return left value * @param commit if the value has to be committed first */ public double getLeftValue(final boolean commit) { if (commit) { double old = ((Number)m_left.getValue()).doubleValue(); try { m_left.commitEdit(); } catch (ParseException pe) { return old; } } return ((Number)m_left.getValue()).doubleValue(); } /** * @param left <code>true</code> if the left interval bound is open otherwise <code>false</code> */ public void setLeftOpen(final boolean left) { if (left) { m_borderLeft.setSelectedItem(LEFT); } else { m_borderLeft.setSelectedItem(RIGHT); } } /** * @return <code>true</code> if left side open */ public boolean isLeftOpen() { return LEFT.equals(m_borderLeft.getSelectedItem()); } /** * @param right new right value */ public void setRightValue(final Double right) { if (right == null || right.doubleValue() == POSITIVE_INFINITY) { m_borderRight.setSelectedItem(RIGHT); m_borderRight.setEnabled(false); m_right.setValue(POSITIVE_INFINITY); m_right.setEnabled(false); } else { m_right.setValue(right); m_right.setEnabled(true); m_borderRight.setEnabled(true); } } /** * @param right <code>true</code> if the right interval bound is open otherwise <code>false</code> */ public void setRightOpen(final boolean right) { if (right) { m_borderRight.setSelectedItem(RIGHT); } else { m_borderRight.setSelectedItem(LEFT); } } /** * @return right value * @param commit if the value has to be committed first */ public double getRightValue(final boolean commit) { if (commit) { double old = ((Number)m_right.getValue()).doubleValue(); try { m_right.commitEdit(); } catch (ParseException pe) { return old; } } return ((Number)m_right.getValue()).doubleValue(); } /** * @return <code>true</code> if right open */ public boolean isRightOpen() { return RIGHT.equals(m_borderRight.getSelectedItem()); } /** * @return string containing left and right border, and open/not open */ @Override public String toString() { double left = getLeftValue(false); double right = getRightValue(false); String leftString, rightString; JComponent editor = m_left.getEditor(); if (editor instanceof JSpinner.NumberEditor) { JSpinner.NumberEditor numEdit = (JSpinner.NumberEditor)editor; leftString = numEdit.getFormat().format(left); rightString = numEdit.getFormat().format(right); } else { leftString = Double.toString(left); rightString = Double.toString(right); } String rightBorder = m_borderRight.getSelectedItem().toString(); String leftBorder = m_borderLeft.getSelectedItem().toString(); return getBinValue() + " : " + leftBorder + " " + leftString + " ... " + rightString + " " + rightBorder; } } /** * {@inheritDoc} */ @Override protected String getDefaultSuffixForAppend() { return "_binned"; } /** * {@inheritDoc} */ @Override public void addDialogComponents() { //don't compose the dialog via dialog components } /** * {@inheritDoc} */ @Override public void loadAdditionalSettingsFrom(final NodeSettingsRO settings, final PortObjectSpec[] specs) throws NotConfigurableException { IntensityBins bins = null; try { bins = new IntensityBins(settings.getNodeSettings(IntensityBinnerNodeModel.KEY_BIN_SETTINGS)); } catch (InvalidSettingsException e) { // TODO throw new NotConfigurableException(e.getMessage(), e); } m_intervalPanel.removeAllIntervalItems(); for (int i = 0; i < bins.getNumBins(); i++) { double binValue = bins.getBinValue(i); boolean leftOpen = bins.isLeftOpen(i); double left = bins.getLeftValue(i); boolean rightOpen = bins.isRightOpen(i); double right = bins.getRightValue(i); IntervalItemPanel item = new IntervalItemPanel(m_intervalPanel, leftOpen, left, rightOpen, right, binValue, DoubleCell.TYPE); m_intervalPanel.addIntervalItem(item); } updateInfo(); getPanel().validate(); getPanel().repaint(); } /** * {@inheritDoc} */ @Override public void saveAdditionalSettingsTo(final NodeSettingsWO settings) throws InvalidSettingsException { if (m_intervalPanel.getNumIntervals() == 0) { throw new InvalidSettingsException("No pixel bins have been specified."); } createPixelBins().saveToSettings(settings.addNodeSettings(IntensityBinnerNodeModel.KEY_BIN_SETTINGS)); } /** * Helper method to create the {@link IntensityBins}-object from the IntervalPanel. * * @return */ private IntensityBins createPixelBins() { IntensityBins bins = new IntensityBins(m_intervalPanel.getNumIntervals()); for (int j = 0; j < m_intervalPanel.getNumIntervals(); j++) { IntervalItemPanel p = m_intervalPanel.getInterval(j); bins.setBinAtIndex(j, p.getBinValue(), p.isLeftOpen(), p.getLeftValue(false), p.isRightOpen(), p.getRightValue(false)); } return bins; } /** * Helper method that determines the result pixel type and updates the info label. */ private void updateInfo() { if (m_intervalPanel.getNumIntervals() > 0) { //determine the result pixel type //and update the info label m_info.setText(createPixelBins().getPixelType().getClass().getSimpleName()); } else { m_info.setText("No bins specified."); } } }