package org.knime.knip.tracking.nodes.hiliter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.knime.core.data.DataRow; import org.knime.core.data.DataTableSpec; import org.knime.core.data.RowKey; import org.knime.core.node.BufferedDataTable; import org.knime.core.node.CanceledExecutionException; import org.knime.core.node.ExecutionContext; import org.knime.core.node.ExecutionMonitor; import org.knime.core.node.InvalidSettingsException; import org.knime.core.node.NodeModel; import org.knime.core.node.NodeSettingsRO; import org.knime.core.node.NodeSettingsWO; import org.knime.core.node.defaultnodesettings.SettingsModelBoolean; import org.knime.core.node.defaultnodesettings.SettingsModelColumnName; import org.knime.core.node.defaultnodesettings.SettingsModelString; import org.knime.core.node.property.hilite.HiLiteHandler; import org.knime.core.node.property.hilite.HiLiteListener; import org.knime.core.node.property.hilite.KeyEvent; import org.knime.knip.base.data.img.ImgPlusValue; import org.knime.knip.base.data.labeling.LabelingCell; import org.knime.knip.base.node.NodeUtils; import org.knime.knip.core.util.EnumUtils; import org.knime.knip.tracking.nodes.hiliter.TrackHilitePropagatorSettingsModels.TrackHilitingMode; import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.roi.labeling.LabelingMapping; /** * Node that connects the Track resulting from the LAP Tracker with the * corresponding rows, so that they can be hilited correctly. * * @author <a href="mailto:gabriel.einsdorf@uni.kn">Gabriel Einsdorf</a> */ public class TrackHilitePropagatorNodeModel extends NodeModel implements HiLiteListener { private static final String SERIALISATION_KEY = "TrackHilitePropagatorState"; /* * SETTING MODELS */ private final SettingsModelColumnName m_trackColumnModel = TrackHilitePropagatorSettingsModels .createTrackColumnSelectionSettingsModel(); private final SettingsModelString m_customTrackPrefixModel = TrackHilitePropagatorSettingsModels.createCustomTrackPrefixModel(); private final SettingsModelBoolean m_useCustomTrackPrefixModel = TrackHilitePropagatorSettingsModels .createUseCustomTrackPrefixModel(); private final SettingsModelString m_trackHilitingModeModel = TrackHilitePropagatorSettingsModels.createTrackHilitingModeModel(); /* * MEMBER */ // the HiliteHandler private HiLiteHandler m_hiliteHandler; // stores the trackdata for each row Map<String, TrackData> m_rowIdToTrackData; /* * Node Begins */ /** * Constructor. */ protected TrackHilitePropagatorNodeModel() { super(1, 1); // for state consistency m_customTrackPrefixModel.setEnabled(false); } @Override protected DataTableSpec[] configure(final DataTableSpec[] inSpecs) throws InvalidSettingsException { final int colIndex = inSpecs[0].findColumnIndex(m_trackColumnModel.getStringValue()); if (colIndex == -1) { if ((NodeUtils.autoOptionalColumnSelection(inSpecs[0], m_trackColumnModel, ImgPlusValue.class)) >= 0) { setWarningMessage("Auto-configure Image Column: " + m_trackColumnModel.getStringValue()); } else { throw new InvalidSettingsException("No column selected!"); } } return null; } @Override protected BufferedDataTable[] execute(final BufferedDataTable[] inData, final ExecutionContext exec) throws CanceledExecutionException { getInHiLiteHandler(0).addHiLiteListener(this); m_rowIdToTrackData = new HashMap<>((int) inData[0].size()); // Get the column from the input table. final int trackingColIndex = inData[0].getDataTableSpec() .findColumnIndex(m_trackColumnModel.getStringValue()); for (final DataRow row : inData[0]) { exec.checkCanceled(); final LabelingCell<?> labelingCell = (LabelingCell<?>) row.getCell(trackingColIndex); // Get the Labeling, and the Labels in it @SuppressWarnings("rawtypes") final ImgLabeling labeling = (ImgLabeling) labelingCell.getLabeling(); @SuppressWarnings("rawtypes") final LabelingMapping mapping = labeling.getMapping(); final String trackPrefix; if (m_useCustomTrackPrefixModel.getBooleanValue()) { // ignoring leading and trailing whitespace for matching trackPrefix = m_customTrackPrefixModel.getStringValue().trim(); } else { trackPrefix = TrackHilitePropagatorSettingsModels.DEFAULT_TRACK_PREFIX; } final Map<String, String> labelToTrack = new HashMap<>(); final Map<String, List<String>> trackToLabels = new HashMap<>(); // Fill the maps with the associated labelings. for (int i = 0; i < mapping.numSets(); i++) { @SuppressWarnings("unchecked") final Set<String> localLabels = mapping.labelsAtIndex(i); // skip mappings that contain less than two labels if (localLabels.size() <= 1) { continue; } // Identify the labels and the track String track = ""; final List<String> otherLabels = new ArrayList<>(); for (final String label : localLabels) { if (label.startsWith(trackPrefix)) { track = label; } else { otherLabels.add(label); } } // Store the identified labels in the maps final List<String> labelsInTrack = trackToLabels.get(track); if (labelsInTrack == null) { trackToLabels.put(track, otherLabels); } else { labelsInTrack.addAll(otherLabels); trackToLabels.put(track, labelsInTrack); } for (final String label : otherLabels) { labelToTrack.put(label, track); } exec.checkCanceled(); } m_rowIdToTrackData.put(row.getKey().getString(), new TrackData(trackToLabels, labelToTrack)); } return inData; } /* * Standard Node methods */ @SuppressWarnings("unchecked") @Override protected void loadInternals(final File nodeInternDir, final ExecutionMonitor exec) throws IOException, CanceledExecutionException { m_hiliteHandler.addHiLiteListener(this); try (final ObjectInputStream in = new ObjectInputStream(new FileInputStream( new File(nodeInternDir, SERIALISATION_KEY)))) { m_rowIdToTrackData = (HashMap<String, TrackData>) in.readObject(); } catch (final ClassNotFoundException e) { throw new IOException("Could not restore the state!", e); } } @Override protected void saveInternals(final File nodeInternDir, final ExecutionMonitor exec) throws IOException, CanceledExecutionException { try (final ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( new File(nodeInternDir, SERIALISATION_KEY)))) { out.writeObject(m_rowIdToTrackData); out.close(); } } @Override protected void saveSettingsTo(final NodeSettingsWO settings) { m_trackColumnModel.saveSettingsTo(settings); m_customTrackPrefixModel.saveSettingsTo(settings); m_useCustomTrackPrefixModel.saveSettingsTo(settings); m_trackHilitingModeModel.saveSettingsTo(settings); } @Override protected void validateSettings(final NodeSettingsRO settings) throws InvalidSettingsException { m_trackColumnModel.validateSettings(settings); m_customTrackPrefixModel.validateSettings(settings); m_useCustomTrackPrefixModel.validateSettings(settings); m_trackHilitingModeModel.validateSettings(settings); } @Override protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws InvalidSettingsException { m_trackColumnModel.loadSettingsFrom(settings); m_customTrackPrefixModel.loadSettingsFrom(settings); m_useCustomTrackPrefixModel.loadSettingsFrom(settings); m_trackHilitingModeModel.loadSettingsFrom(settings); } @Override protected void reset() { m_rowIdToTrackData = null; if (m_hiliteHandler != null) { m_hiliteHandler.removeHiLiteListener(this); } } /* * HILITING */ /** * {@inheritDoc} */ @Override protected void setInHiLiteHandler(final int inIndex, final HiLiteHandler handler) { m_hiliteHandler = handler; } /** * {@inheritDoc} */ @Override protected HiLiteHandler getOutHiLiteHandler(final int outIndex) { return m_hiliteHandler; } /** * {@inheritDoc} */ @Override public void hiLite(final KeyEvent event) { getOutHiLiteHandler(0).fireHiLiteEvent(getKeysForEvent(event)); } /** * {@inheritDoc} */ @Override public void unHiLite(final KeyEvent event) { getOutHiLiteHandler(0).fireUnHiLiteEvent(getKeysForEvent(event)); } /** * {@inheritDoc} */ @Override public void unHiLiteAll(final KeyEvent event) { // nothing to do here } /** * Returns the rowkeys that are to be hilited. * * @param event * @return */ private RowKey[] getKeysForEvent(final KeyEvent event) { final TrackHilitingMode mode = EnumUtils.valueForName( m_trackHilitingModeModel.getStringValue(), TrackHilitingMode.values()); final RowKey[] keys; switch (mode) { case OFF: keys = new RowKey[0]; break; case POINTS_TO_POINTS: keys = pointsToPoints(event); break; case TRACK_TO_POINTS: keys = trackToPoints(event); break; default: throw new AssertionError("Unimplemented hiliting mode!"); } return keys; } private RowKey[] trackToPoints(final KeyEvent event) { final Set<RowKey> tracksToHilite = new HashSet<>(); final String trackPrefix = m_useCustomTrackPrefixModel.getBooleanValue() ? m_customTrackPrefixModel.getStringValue() : TrackHilitePropagatorSettingsModels.DEFAULT_TRACK_PREFIX; for (final RowKey k : event.keys()) { // split the row key into prefix and unique part final String rowKey = k.getString(); final String lookUpkey; try { lookUpkey = rowKey.substring(rowKey.lastIndexOf('#') + 1); } catch (final StringIndexOutOfBoundsException e) { // the key can't be matched continue; } // only select rows that are tracks if (lookUpkey.startsWith(trackPrefix)) { tracksToHilite.add(k); } else { continue; } } return getRowKeysFromTracks(tracksToHilite); } /** * Creates a List of row keys that are on the same Track as the row keys in * the Event. If the event contains * * @return a set of rowkeys that are on the same tracks as the keys in the * hiliting event. */ private RowKey[] pointsToPoints(final KeyEvent event) { // Get all the Labels that are on the same track final Set<RowKey> tracksToHilite = new HashSet<>(); final String trackPrefix = m_useCustomTrackPrefixModel.getBooleanValue() ? m_customTrackPrefixModel.getStringValue() : TrackHilitePropagatorSettingsModels.DEFAULT_TRACK_PREFIX; for (final RowKey k : event.keys()) { // split the row key into prefix and unique part final String rowKeyString = k.getString(); final TrackData data; try { data = m_rowIdToTrackData.get(rowKeyString.substring(0, rowKeyString.lastIndexOf('#'))); } catch (final StringIndexOutOfBoundsException e) { // the key can't be matched continue; } final String lookupKey = rowKeyString.substring(rowKeyString.lastIndexOf('#') + 1); if (lookupKey.startsWith(trackPrefix)) { tracksToHilite.add(k); } else { final String trackName = data.m_labelToTrack.get(lookupKey); if (trackName != null) { final String keyprefix = rowKeyString.substring(0, rowKeyString.lastIndexOf('#')); tracksToHilite.add(new RowKey(keyprefix + '#' + trackName)); } } } return getRowKeysFromTracks(tracksToHilite); } /** * Takes a set of track {@link RowKey}s and returns their {@link RowKey}s * and of all the items that are on these tracks. * * @param tracksToHilite * the tracks which members * @param event * the hilite event, used to extract the key information * * @return The RowKeys of the tracks and the items that are on them. */ private RowKey[] getRowKeysFromTracks(final Set<RowKey> tracksToHilite) { // early escape if (tracksToHilite == null || tracksToHilite.isEmpty()) { return new RowKey[0]; } // mark all rows on all given tracks and the tracks them self for // hiliting. final Set<RowKey> toHilite = new HashSet<>(); for (final RowKey trackRowKey : tracksToHilite) { final String keyPrefix = trackRowKey.getString().substring(0, trackRowKey.getString().lastIndexOf('#')); final String trackKey = trackRowKey.getString() .substring(trackRowKey.getString().lastIndexOf('#') + 1); final TrackData trackData = m_rowIdToTrackData.get(keyPrefix); final List<String> list = trackData.m_trackToLabels.get(trackKey); if (list == null) { continue; } for (final String label : list) { toHilite.add(new RowKey(keyPrefix + '#' + label)); } toHilite.add(trackRowKey); } return toHilite.toArray(new RowKey[toHilite.size()]); } }