/*
* ------------------------------------------------------------------------
*
* Copyright (C) 2003 - 2013
* 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.
* --------------------------------------------------------------------- *
*
*/
package org.knime.knip.base.nodes.proc.imgjep;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import org.knime.core.data.DataCell;
import org.knime.core.data.DataColumnSpec;
import org.knime.core.data.DataColumnSpecCreator;
import org.knime.core.data.DataRow;
import org.knime.core.data.DataTableSpec;
import org.knime.core.data.DataType;
import org.knime.core.data.DoubleValue;
import org.knime.core.data.IntValue;
import org.knime.core.data.RowIterator;
import org.knime.core.data.RowKey;
import org.knime.core.data.container.CellFactory;
import org.knime.core.data.container.ColumnRearranger;
import org.knime.core.data.container.SingleCellFactory;
import org.knime.core.data.def.DefaultRow;
import org.knime.core.node.BufferedDataContainer;
import org.knime.core.node.BufferedDataTable;
import org.knime.core.node.BufferedDataTableHolder;
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.NodeLogger;
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.SettingsModelString;
import org.knime.core.node.port.PortObjectSpec;
import org.knime.core.node.streamable.InputPortRole;
import org.knime.core.node.streamable.OutputPortRole;
import org.knime.core.node.streamable.PartitionInfo;
import org.knime.core.node.streamable.StreamableFunction;
import org.knime.core.node.streamable.StreamableOperator;
import org.knime.core.node.streamable.StreamableOperatorInternals;
import org.knime.knip.base.data.img.ImgPlusCell;
import org.knime.knip.base.data.img.ImgPlusCellFactory;
import org.knime.knip.base.data.img.ImgPlusValue;
import org.knime.knip.core.data.img.DefaultImgMetadata;
import org.knime.knip.core.types.NativeTypes;
import org.knime.knip.core.util.MinimaUtils;
import org.knime.knip.core.util.MiscViews;
import org.nfunk.jep.Variable;
import net.imagej.ImgPlus;
import net.imagej.ImgPlusMetadata;
import net.imglib2.FinalInterval;
import net.imglib2.Interval;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.exception.IncompatibleTypeException;
import net.imglib2.img.Img;
import net.imglib2.img.ImgFactory;
import net.imglib2.img.ImgView;
import net.imglib2.type.NativeType;
import net.imglib2.type.numeric.RealType;
/**
* This is the model implementation of JEP. Math expression parser using JEP (http://www.singularsys.com/jep/)
*
* @author <a href="mailto:dietzc85@googlemail.com">Christian Dietz</a>
* @author <a href="mailto:horn_martin@gmx.de">Martin Horn</a>
* @author <a href="mailto:michael.zinsmaier@googlemail.com">Michael Zinsmaier</a>
*
* @author Bernd Wiswedel, University of Konstanz
*/
public class ImgJEPNodeModel extends NodeModel implements BufferedDataTableHolder {
protected static final int APPEND_COLUMN = 0;
/** NodeSettings key for the option to extend the image */
protected static final String CFG_ADJUST_IMG_DIM = "adjust_img_dim";
/** NodeSettings key which column is to be replaced or appended. */
protected static final String CFG_COLUMN_NAME = "replaced_column";
/** NodeSettings key for the expression. */
protected static final String CFG_EXPRESSION = "expression";
/** NodeSettings key for the reference column selection */
protected static final String CFG_REF_COLUMN = "ref_column";
/** NodeSettings key for the result type */
protected static final String CFG_RESULT_TYPE = "res_type";
/** NodeSettings key is replace or append column or create new table? */
protected static final String CFG_TABLE_CREATION_MODE = "table_creation_mode";
private static final NodeLogger LOGGER = NodeLogger.getLogger(ImgJEPNodeModel.class);
protected static final int NEW_TABLE = 2;
protected static final int REPLACE_COLUMN = 1;
private final SettingsModelBoolean m_adjustImgDim = new SettingsModelBoolean(CFG_ADJUST_IMG_DIM, false);
private String m_colName;
/* internal table for table cell viewer */
private BufferedDataTable m_data;
private String m_expression;
/*
* the parser is created in the configure method and re-used every time
* configure is called (because it is expensive). Do not depend on it
* existence.
*/
private ImgExpressionParser m_exprParser = null;
private ImgPlusCellFactory m_imgCellFactory;
private final SettingsModelString m_referenceColumn = new SettingsModelString(CFG_REF_COLUMN, "");
private final SettingsModelString m_resultType =
new SettingsModelString(CFG_RESULT_TYPE, NativeTypes.UNSIGNEDBYTETYPE.toString());
private int m_tableCreationMode;
/**
* Constructor for the node model.
*/
protected ImgJEPNodeModel() {
super(1, 1);
}
/** {@inheritDoc} */
@Override
protected DataTableSpec[] configure(final DataTableSpec[] inSpecs) throws InvalidSettingsException {
if (m_colName == null) {
throw new InvalidSettingsException("No configuration available");
}
if ((m_exprParser == null) || !inSpecs[0].equalStructure(m_exprParser.getDataTableSpec())) {
// if the input spec changed we need to re-create the
// parser
m_exprParser = new ImgExpressionParser(m_expression, inSpecs[0], -1);
}
final ColumnRearranger a = new ColumnRearranger(inSpecs[0]);
final CellFactory cellFac = createCellFactory(m_exprParser, inSpecs[0]);
DataTableSpec outSpec;
if (m_tableCreationMode == REPLACE_COLUMN) {
a.replace(cellFac, inSpecs[0].findColumnIndex(m_colName));
outSpec = a.createSpec();
} else if (m_tableCreationMode == APPEND_COLUMN) {
a.append(cellFac);
outSpec = a.createSpec();
} else {
outSpec = new DataTableSpec(createNewColumnSpec(inSpecs[0]));
}
return new DataTableSpec[]{outSpec};
}
//TODO as soon as ImgLib2 supports cursors created from RandomAccessibleIntervals with arbitrary iteration order we can get rid of the unnesscary wrapping
//TODO at the moment the only really "slow-down" takes place, if the input only consists of unwrapped CellImgs.
//TODO for any other Img combination it would be wrapped anyway OR the wrapping doesn't influence the performance
private CellFactory createCellFactory(final ImgExpressionParser expParser, final DataTableSpec spec)
throws InvalidSettingsException {
final ImgJEP jep = expParser.getJep();
final SingleCellFactory cellFac = new SingleCellFactory(createNewColumnSpec(expParser.getDataTableSpec())) {
// private int m_rowIndex;
@Override
public DataCell getCell(final DataRow row) {
long[] referenceDims = null;
Object referenceImgIterationOrder = null;
ImgPlusMetadata referenceMetadata = null;
Interval referenceInterval = null;
boolean requireFactoryWrap = false;
WrappedFactory referenceFactory = null;
LinkedList<ImgPlus<RealType<?>>> imgList = new LinkedList<ImgPlus<RealType<?>>>();
boolean useAutoGuess = true;
if (m_referenceColumn.getStringValue() != null) {
final int index = spec.findColumnIndex(m_referenceColumn.getStringValue());
if ((index != -1) && !row.getCell(index).isMissing()) {
// set reference values from
// reference image (once per
// row)
useAutoGuess = false;
final ImgPlus tmpImg =
MinimaUtils.getZeroMinImgPlus(((ImgPlusValue)row.getCell(index)).getImgPlus());
referenceDims = new long[tmpImg.numDimensions()];
tmpImg.dimensions(referenceDims);
referenceImgIterationOrder =
new ImgView(tmpImg.getImg(), createWrappedFactory(tmpImg.factory())).iterationOrder();
referenceMetadata = new DefaultImgMetadata(tmpImg);
referenceInterval = new FinalInterval(tmpImg);
referenceFactory = createWrappedFactory(tmpImg.factory());
}
}
for (int i = 0; i < row.getNumCells(); i++) {
final String col = ImgExpressionParser.createColField(i);
final Variable var = jep.getVar(col);
if (var != null) {
ImgPlus img = null;
Number num = null;
final DataCell c = row.getCell(i);
if (c.isMissing()) {
return DataType.getMissingCell();
} else if (c instanceof ImgPlusValue) {
img = MinimaUtils.getZeroMinImgPlus(((ImgPlusValue)c).getImgPlus());
} else if (c instanceof DoubleValue) {
num = ((DoubleValue)c).getDoubleValue();
} else if (c instanceof IntValue) {
num = ((IntValue)c).getIntValue();
}
// if the current column is an image column
if (c instanceof ImgPlusValue) {
if (useAutoGuess && (referenceDims == null)) {
// autoGuess is active and the reference dim has not yet been set => set it
// i.e. the first occurring img column is used as reference values
referenceDims = new long[img.numDimensions()];
img.dimensions(referenceDims);
referenceImgIterationOrder = new ImgView(img.getImg(), img.factory()).iterationOrder();
referenceMetadata = new DefaultImgMetadata(img);
referenceInterval = new FinalInterval(img);
referenceFactory = createWrappedFactory(img.factory());
}
// at this point we have a reference column either auto-guessed or set by the user
if (m_adjustImgDim.getBooleanValue()) {
final RandomAccessibleInterval res =
MiscViews.synchronizeDimensionality(img.getImg(), img, referenceInterval,
referenceMetadata);
img = new ImgPlus(new ImgView(res, img.factory()),
new DefaultImgMetadata(referenceMetadata));
}
// image exists no error occurred
if (!(img.getImg() instanceof ImgView)) {
img = new ImgPlus(new ImgView(img.getImg(), referenceFactory), img);
}
if (!referenceImgIterationOrder.equals(img.iterationOrder())) {
LOGGER.warn("The images you want to combine don't match in their dimensions. You may want to use the Advanced Settings to resolve the problem.");
setWarningMessage("Some error occured while executing! See console log for details!");
return DataType.getMissingCell();
}
jep.getVar(col).setValue(img);
imgList.add(img);
} else if (num != null) {
jep.getVar(col).setValue(num);
}
}
}
// collects the references of the images from
// the different columns to indicate childs in
// the parse tree
final HashSet<Img<RealType<?>>> tableImages = new HashSet<Img<RealType<?>>>();
// the type
final RealType<?> currentType =
(RealType<?>)NativeTypes.getTypeInstance(NativeTypes.valueOf(m_resultType.getStringValue()));
// we need a wrapped factory to create buffered images in case of cellimg
final ImgOperationEval eval = new ImgOperationEval(currentType, referenceFactory, new HashSet(imgList));
jep.setImgOperationEvaluator(eval);
final Img imgRes = new ImgView((Img)jep.getValueAsObject(), referenceFactory.fac);
if ((imgRes == null) || eval.errorOccured()) {
LOGGER.error("Result for row " + row.getKey() + " can't be calculated.");
setWarningMessage("Some error occured while executing! See console log for details!");
return DataType.getMissingCell();
}
try {
return m_imgCellFactory.createCell(new ImgPlus(imgRes, referenceMetadata));
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private <T extends NativeType<T>> WrappedFactory<T> createWrappedFactory(final ImgFactory<T> factory) {
return new WrappedFactory(factory);
}
@Override
public void setProgress(final int curRowNr, final int rowCount, final RowKey lastKey,
final ExecutionMonitor exec) {
// m_rowIndex = curRowNr; // curRowNr refers to
// last processed
super.setProgress(curRowNr, rowCount, lastKey, exec);
}
};
return cellFac;
}
private DataColumnSpec createNewColumnSpec(final DataTableSpec inSpec) throws InvalidSettingsException {
DataColumnSpec newColSpec;
if (m_tableCreationMode == REPLACE_COLUMN) {
final int targetCol = inSpec.findColumnIndex(m_colName);
if (targetCol < 0) {
throw new InvalidSettingsException("No such column: " + m_colName);
}
final DataColumnSpecCreator clone = new DataColumnSpecCreator(inSpec.getColumnSpec(targetCol));
clone.setType(ImgPlusCell.TYPE);
clone.removeAllHandlers();
clone.setDomain(null);
newColSpec = clone.createSpec();
} else if (m_tableCreationMode == APPEND_COLUMN) {
newColSpec = new DataColumnSpecCreator(DataTableSpec.getUniqueColumnName(inSpec, m_colName), ImgPlusCell.TYPE).createSpec();
} else {
newColSpec = new DataColumnSpecCreator(m_colName, ImgPlusCell.TYPE).createSpec();
}
return newColSpec;
}
/** {@inheritDoc} */
@Override
protected BufferedDataTable[] execute(final BufferedDataTable[] inData, final ExecutionContext exec)
throws Exception {
m_imgCellFactory = new ImgPlusCellFactory(exec);
final BufferedDataTable in = inData[0];
final DataTableSpec spec = in.getDataTableSpec();
final ImgExpressionParser p = new ImgExpressionParser(m_expression, spec, in.getRowCount());
final String[] columnsWithConstants = p.getColumnsWithConstants();
ExecutionMonitor mon = exec;
if (columnsWithConstants.length > 0) {
mon = exec.createSubProgress(0.5);
p.calculateConstants(mon, in);
mon = exec.createSubProgress(0.5);
}
final ColumnRearranger a = new ColumnRearranger(spec);
final CellFactory cellFac = createCellFactory(p, spec);
BufferedDataTable out;
if (m_tableCreationMode == REPLACE_COLUMN) {
a.replace(cellFac, spec.findColumnIndex(m_colName));
out = exec.createColumnRearrangeTable(in, a, mon);
} else if (m_tableCreationMode == APPEND_COLUMN) {
a.append(cellFac);
out = exec.createColumnRearrangeTable(in, a, mon);
} else {
final DataTableSpec newSpec = new DataTableSpec(createNewColumnSpec(spec));
final BufferedDataContainer con = exec.createDataContainer(newSpec);
final RowIterator rowIt = in.iterator();
int rowNr = 0;
final int rowCount = in.getRowCount();
while (rowIt.hasNext()) {
final DataRow row = rowIt.next();
con.addRowToTable(new DefaultRow(row.getKey(), cellFac.getCells(row)));
cellFac.setProgress(rowNr++, rowCount, row.getKey(), exec);
exec.checkCanceled();
}
con.close();
out = con.getTable();
}
m_data = out;
return new BufferedDataTable[]{out};
}
/**
* {@inheritDoc}
*/
@Override
public BufferedDataTable[] getInternalTables() {
return new BufferedDataTable[]{m_data};
}
/** {@inheritDoc} */
@Override
protected void loadInternals(final File internDir, final ExecutionMonitor exec)
throws IOException, CanceledExecutionException {
}
/** {@inheritDoc} */
@Override
protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws InvalidSettingsException {
m_expression = settings.getString(CFG_EXPRESSION);
m_colName = settings.getString(CFG_COLUMN_NAME);
m_tableCreationMode = settings.getInt(CFG_TABLE_CREATION_MODE);
// in case we created a parser for an old expression: delete it
m_exprParser = null;
m_resultType.loadSettingsFrom(settings);
m_adjustImgDim.loadSettingsFrom(settings);
try {
m_referenceColumn.loadSettingsFrom(settings);
} catch (final Exception e) {
m_referenceColumn.setStringValue("");
}
}
/** {@inheritDoc} */
@Override
protected void reset() {
m_data = null;
}
/** {@inheritDoc} */
@Override
protected void saveInternals(final File internDir, final ExecutionMonitor exec)
throws IOException, CanceledExecutionException {
}
/** {@inheritDoc} */
@Override
protected void saveSettingsTo(final NodeSettingsWO settings) {
if (m_expression != null) {
settings.addString(CFG_EXPRESSION, m_expression);
settings.addString(CFG_COLUMN_NAME, m_colName);
settings.addInt(CFG_TABLE_CREATION_MODE, m_tableCreationMode);
}
m_resultType.saveSettingsTo(settings);
m_adjustImgDim.saveSettingsTo(settings);
m_referenceColumn.saveSettingsTo(settings);
}
/**
* {@inheritDoc}
*/
@Override
public void setInternalTables(final BufferedDataTable[] tables) {
m_data = tables[0];
}
/** {@inheritDoc} */
@Override
protected void validateSettings(final NodeSettingsRO settings) throws InvalidSettingsException {
settings.getString(CFG_EXPRESSION);
final String newCol = settings.getString(CFG_COLUMN_NAME);
final int tableCreationMode = settings.getInt(CFG_TABLE_CREATION_MODE);
if (newCol == null) {
throw new InvalidSettingsException("Column name can't be null.");
}
if ((tableCreationMode != REPLACE_COLUMN) && (newCol.trim().length() == 0)) {
throw new InvalidSettingsException("An empty string is not a valid column name");
}
m_resultType.validateSettings(settings);
m_adjustImgDim.validateSettings(settings);
try {
m_referenceColumn.validateSettings(settings);
} catch (final Exception e) {
// Do nothing
}
}
/*
* STREAMING
*/
@Override
public InputPortRole[] getInputPortRoles() {
return new InputPortRole[]{InputPortRole.DISTRIBUTED_STREAMABLE};
}
@Override
public OutputPortRole[] getOutputPortRoles() {
return new OutputPortRole[]{OutputPortRole.DISTRIBUTED};
}
private void prepareExecute(final ExecutionContext ctx) {
if (m_imgCellFactory == null) {
m_imgCellFactory = new ImgPlusCellFactory(ctx);
}
}
@Override
public StreamableOperator createStreamableOperator(final PartitionInfo partitionInfo,
final PortObjectSpec[] inSpecs) throws InvalidSettingsException {
final DataTableSpec inSpec = (DataTableSpec)inSpecs[0];
final ImgExpressionParser p = new ImgExpressionParser(m_expression, inSpec, -1);
final CellFactory cellFac = createCellFactory(p, inSpec);
if (m_tableCreationMode == NEW_TABLE) {
return new StreamableFunction() {
/**
* {@inheritDoc}
*/
@Override
public void init(final ExecutionContext ctx) throws Exception {
prepareExecute(ctx);
}
@Override
public DataRow compute(final DataRow input) throws Exception {
return new DefaultRow(input.getKey(), cellFac.getCells(input));
}
};
} else {
// create column rearranger
final ColumnRearranger colRearranger = new ColumnRearranger(inSpec);
if (m_tableCreationMode == APPEND_COLUMN) {
colRearranger.append(cellFac);
} else {
colRearranger.replace(cellFac, inSpec.findColumnIndex(m_colName));
}
// get column rearranger function
final StreamableFunction columnRearrangerFunction = colRearranger.createStreamableFunction();
// create new streamablefunction, do everything columnrearranger function does but call prepareexceute in init().
return new StreamableFunction() {
/** {@inheritDoc} */
@Override
public void init(final ExecutionContext ctx) throws Exception {
prepareExecute(ctx);
columnRearrangerFunction.init(ctx);
}
/** {@inheritDoc} */
@Override
public DataRow compute(final DataRow inputRow) {
try {
return columnRearrangerFunction.compute(inputRow);
} catch (Exception e) {
throw new IllegalArgumentException("Exception caught while reading row " + inputRow.getKey()
+ "! Caught exception " + e.getMessage());
}
}
/** {@inheritDoc} */
@Override
public void finish() {
columnRearrangerFunction.finish();
}
/** {@inheritDoc} */
@Override
public StreamableOperatorInternals saveInternals() {
return columnRearrangerFunction.saveInternals();
}
};
}
}
}
class WrappedFactory<V extends NativeType<V>> extends ImgFactory<V> {
ImgFactory<V> fac;
public WrappedFactory(final ImgFactory<V> fac) {
this.fac = fac;
}
@Override
public Img<V> create(final long[] dim, final V type) {
return new ImgView<V>(fac.create(dim, type), this);
}
@Override
public <S> ImgFactory<S> imgFactory(final S type) throws IncompatibleTypeException {
return (ImgFactory<S>)this;
}
}