package de.mpicbg.knime.scripting.r.plots;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import org.knime.core.data.image.png.PNGImageContent;
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.defaultnodesettings.SettingsModelBoolean;
import org.knime.core.node.defaultnodesettings.SettingsModelIntegerBounded;
import org.knime.core.node.defaultnodesettings.SettingsModelString;
import org.knime.core.node.port.PortObject;
import org.knime.core.node.port.PortObjectSpec;
import org.knime.core.node.port.PortType;
import org.knime.core.node.port.image.ImagePortObject;
import org.knime.core.node.port.image.ImagePortObjectSpec;
import org.rosuda.REngine.REXP;
import org.rosuda.REngine.REXPMismatchException;
import org.rosuda.REngine.Rserve.RConnection;
import org.rosuda.REngine.Rserve.RserveException;
import de.mpicbg.knime.scripting.core.AbstractScriptingNodeModel;
import de.mpicbg.knime.scripting.core.FlowVarUtils;
import de.mpicbg.knime.scripting.core.ScriptingModelConfig;
import de.mpicbg.knime.scripting.core.TemplateConfigurator;
import de.mpicbg.knime.scripting.core.exceptions.KnimeScriptingException;
import de.mpicbg.knime.scripting.r.AbstractRScriptingNodeModel;
import de.mpicbg.knime.scripting.r.R4KnimeBundleActivator;
import de.mpicbg.knime.scripting.r.RUtils;
import de.mpicbg.knime.scripting.r.prefs.RPreferenceInitializer;
/**
* abstract R plot node model class for implementation plot relevant methods
*
* @author Holger Brandl, Antje Janosch
*/
public abstract class AbstractRPlotNodeModel extends AbstractRScriptingNodeModel {
protected static final ImagePortObjectSpec IM_PORT_SPEC = new ImagePortObjectSpec(PNGImageContent.TYPE);
public BufferedImage m_image; // image created by R
public File m_nodeImageFile; // image file (internals)
protected File m_rWorkspaceFile; // workspace file (internals)
private boolean isImageIcon = false;
/**
* MODEL - SETTINGS
*/
public static final String CFG_WIDTH = "figure.width";
public static final int CFG_WIDTH_DFT = 1000;
public static final String CFG_HEIGHT = "figure.height";
public static final int CFG_HEIGHT_DFT = 700;
public static final String CFG_OUTFILE = "figure.output.file";
public static final String CFG_OVERWRITE = "overwrite.ok";
public static final boolean CFG_OVERWRITE_DFT = false;
public static final String CFG_WRITE = "write.output.file";
public static final boolean CFG_WRITE_DFT = true;
public static final String CFG_IMGTYPE = "figure.ouput.type";
public static final String CFG_IMGTYPE_DFT = "png";
public static final String DEFAULT_R_PLOTCMD = "plot(1:10)";
public static String TODAY = new SimpleDateFormat("yyMMdd").format(new Date(System.currentTimeMillis()));
/**
* constructor with node config settings
* @param nodeModelConfig
*/
public AbstractRPlotNodeModel(ScriptingModelConfig nodeModelConfig) {
super(nodeModelConfig);
this.addModelSetting(CFG_HEIGHT, createHeightSM());
this.addModelSetting(CFG_WIDTH, createWidthSM());
this.addModelSetting(CFG_IMGTYPE, createImgTypeSM());
this.addModelSetting(CFG_OUTFILE, createOutputFileSM());
this.addModelSetting(CFG_OVERWRITE, createOverwriteSM());
this.addModelSetting(CFG_WRITE, createWriteFileSM());
}
/**
* {@inheritDoc}
* <br>
* adds functionality to create + save image and to prvode image output
*/
@Override
protected PortObject[] pullOutputFromR(ExecutionContext exec) throws KnimeScriptingException {
try {
createInternals(m_con);
saveFigureAsFile();
} catch (KnimeScriptingException e) {
closeRConnection();
throw e;
}
// retrieve table + generic outputs if present
PortObject[] outPorts = super.pullOutputFromR(exec);
for(int i = 0; i < getNrOutPorts(); i++) {
PortType pType = this.getOutPortType(i);
// create image for image port
if(pType.equals(ImagePortObject.TYPE)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImageIO.write(m_image, "png", baos);
} catch (IOException e) {
throw new KnimeScriptingException(e.getMessage());
}
PNGImageContent content = new PNGImageContent(baos.toByteArray());
outPorts[i] = new ImagePortObject(content, IM_PORT_SPEC);
}
}
return outPorts;
}
/**
* method writes image to file
* @throws KnimeScriptingException
*/
protected void saveFigureAsFile() throws KnimeScriptingException {
boolean enableFileOutput = ((SettingsModelBoolean) getModelSetting(CFG_WRITE)).getBooleanValue();
// no need to save image to file ?
if(!enableFileOutput) return;
assert m_image != null;
String fileName = ((SettingsModelString) getModelSetting(CFG_OUTFILE)).getStringValue();
boolean overwriteFileOutput = ((SettingsModelBoolean) getModelSetting(CFG_OVERWRITE)).getBooleanValue();
if(fileName == null) return;
if(fileName.length() == 0) return;
fileName = prepareOutputFileName(fileName);
// the plot should be written to file
if (!fileName.isEmpty()) {
File imageFile = new File(fileName);
// check if the file already exists but should not be overwritten
if(imageFile.exists()) {
if(!overwriteFileOutput)
throw new KnimeScriptingException("Overwrite file is disabled but image file '" + fileName + "' already exsists.");
} else {
try {
imageFile.createNewFile();
} catch(IOException e) {
throw new KnimeScriptingException("Output file '" + fileName + "' cannot be created. Please check the output path! (" + e.getMessage() + ")");
}
}
FileOutputStream fsOut = null;
try {
fsOut = new FileOutputStream(new File(fileName));
ImageIO.write(m_image, "png", fsOut);
fsOut.close();
} catch (IOException e) {
throw new KnimeScriptingException("Failed to sava image to file:\n" + e.getMessage());
}
}
}
/**
* create settings model: enable output to file yes/no
* @return
*/
public static SettingsModelBoolean createWriteFileSM() {
return new SettingsModelBoolean(CFG_WRITE, CFG_WRITE_DFT);
}
/**
* create settings model: overwrite output to file yes/no
* @return
*/
public static SettingsModelBoolean createOverwriteSM() {
return new SettingsModelBoolean(CFG_OVERWRITE, CFG_OVERWRITE_DFT);
}
/**
* create settings model: output file
* @return
*/
public static SettingsModelString createOutputFileSM() {
return new SettingsModelString(CFG_OUTFILE, null);
}
/**
* create settings model: image type
* @return
*/
public static SettingsModelString createImgTypeSM() {
return new SettingsModelString(CFG_IMGTYPE, CFG_IMGTYPE_DFT);
}
/**
* create settings model: image width in pixels
* @return
*/
public static SettingsModelIntegerBounded createWidthSM() {
return new SettingsModelIntegerBounded(CFG_WIDTH, CFG_WIDTH_DFT, 0, Integer.MAX_VALUE);
}
/**
* create settings model: image height in pixels
* @return
*/
public static SettingsModelIntegerBounded createHeightSM() {
return new SettingsModelIntegerBounded(CFG_HEIGHT, CFG_HEIGHT_DFT, 0, Integer.MAX_VALUE);
}
/**
* Creates a figure using the R-variable in the current connection's workspace as input.
* @throws IOException
* @throws KnimeScriptingException
*/
protected void createInternals(RConnection connection) throws KnimeScriptingException {
// workspace file saved as internal and used to recreate image
try {
RUtils.saveWorkspaceToFile(getTempWSFile(), connection, RUtils.getHost());
} catch (IOException e) {
throw new KnimeScriptingException(e.getMessage());
}
// create the image the script
String script = prepareScript();
m_image = createImage(connection, script, getDefWidth(), getDefHeight(), getDevice());
}
/**
* run R script to save the plot as a temporary file
* note: connection is not closed when exceptions occur
* @param connection
* @param script
* @param width
* @param height
* @param device
* @return
* @throws KnimeScriptingException
*/
public static BufferedImage createImage(RConnection connection, String script, int width, int height, String device)
throws KnimeScriptingException {
// check preferences
boolean useEvaluate = R4KnimeBundleActivator.getDefault().getPreferenceStore().getBoolean(RPreferenceInitializer.USE_EVALUATE_PACKAGE);
String tempFileName = "rmPlotFile." + device;
// create plot device on R side
String deviceArgs = device.equals("jpeg") ? "quality=97," : "";
REXP xp = null;
String openDevice = "try(" + device + "('" + tempFileName + "'," + deviceArgs + " width = " + width + ", height = " + height + "))";
try {
xp = connection.eval(openDevice);
if (xp.inherits("try-error")) { // if the result is of the class try-error then there was a problem
// this is analogous to 'warnings', but for us it's sufficient to get just the 1st warning
REXP w = connection.eval("if (exists('last.warning') && length(last.warning)>0) names(last.warning)[1] else 0");
if (w.isString()) System.err.println(w.asString()); {
throw new KnimeScriptingException("Can't open " + device + " graphics device:\n" + xp.asString());
}
}
} catch (RserveException | REXPMismatchException e) {
throw new KnimeScriptingException("Failed to open image device from R:\n" + openDevice);
}
// the device should be fine
// parse script
String preparedScript = AbstractScriptingNodeModel.fixEncoding(script);
try {
parseScript(connection, preparedScript);
} catch (RserveException | KnimeScriptingException | REXPMismatchException e) {
throw new KnimeScriptingException("Failed to parse the script:\n" + e.getMessage());
}
// evaluate script
try {
if(useEvaluate) {
// parse and run script
// evaluation list, can be used to create a console view
evaluateScript(preparedScript, connection);
} else {
// parse and run script
evalScript(connection, preparedScript);
}
} catch (RserveException | REXPMismatchException | KnimeScriptingException e) {
throw new KnimeScriptingException("Failed to evaluate the script:\n" + e.getMessage());
}
// close the image
byte[] image = null;
try {
connection.eval("dev.off();");
// check if the plot file has been written
int xpInt = connection.eval("file.access('" + tempFileName + "',0)").asInteger();
if(xpInt == -1) throw new KnimeScriptingException("Plot could not be created. Please check your script");
// we limit the file size to 1MB which should be sufficient and we delete the file as well
xp = connection.eval("try({ binImage <- readBin('" + tempFileName + "','raw',2024*2024); unlink('" + tempFileName + "'); binImage })");
if (xp.inherits("try-error")) { // if the result is of the class try-error then there was a problem
throw new KnimeScriptingException(xp.asString());
}
image = xp.asBytes();
} catch (RserveException | REXPMismatchException e) {
throw new KnimeScriptingException("Failed to close image device and to read in plot as binary:+\n" + e.getMessage());
}
// create image object from bytes
BufferedImage img = null;
try {
img = ImageIO.read(new ByteArrayInputStream(image));
} catch (IOException e) {
throw new KnimeScriptingException(e.getMessage());
}
return img;
}
/**
* replace placeholders in filename with appropriate values
* @param fileName
* @return final filename
*/
private String prepareOutputFileName(String fileName) {
// process flow-variables
fileName = FlowVarUtils.replaceFlowVars(fileName, this);
// replace wildcards
// 1) date
fileName = fileName.replace("$$DATE$$", TODAY);
// 2) user
fileName = fileName.replace("$$USER$$", System.getProperty("user.name"));
// 3) workspace dir
if (fileName.contains("$$WS$$")) {
String wsLocation = getFlowVariable("knime.workspace");
fileName = fileName.replace("$$WS$$", wsLocation);
}
return fileName;
}
/**
* @return file handle for temporary workspace file
* @throws IOException
*/
protected File getTempWSFile() throws IOException {
if (m_rWorkspaceFile == null) {
// note: this 'R' is not a workspace variable name but a file suffix
m_rWorkspaceFile = File.createTempFile("genericR", ".RData");
}
return m_rWorkspaceFile;
}
/**
* {@inheritDoc}
* <br>
* added: remove temporary workspace file if existing
*/
@Override
protected PortObjectSpec[] configure(PortObjectSpec[] inSpecs) throws InvalidSettingsException {
PortObjectSpec[] portSpecs = super.configure(inSpecs);
try {
removeTempWorkspace();
} catch (IOException e) {
setWarningMessage("Failed to remove temporary R workspace file:" + e.getMessage());
}
return portSpecs;
}
/**
* remove temporary workspace file if it exists
* @throws IOException if it fails to delete that file
*/
private void removeTempWorkspace() throws IOException {
// delete temporary R workspace when node is configured
if(m_rWorkspaceFile != null) {
if(m_rWorkspaceFile.exists())
Files.delete(m_rWorkspaceFile.toPath());
m_rWorkspaceFile = null;
}
}
/**
* @return image device setting
*/
public String getDevice() {
return ((SettingsModelString) getModelSetting(CFG_IMGTYPE)).getStringValue();
}
/**
* @return image height (pixels)
*/
public int getDefHeight() {
return ((SettingsModelIntegerBounded) getModelSetting(CFG_HEIGHT)).getIntValue();
}
/**
* @return image width (pixels)
*/
public int getDefWidth() {
return ((SettingsModelIntegerBounded) getModelSetting(CFG_WIDTH)).getIntValue();
}
/**
* {@inheritDoc}
*/
@Override
public String getDefaultScript(String defaultScript) {
return super.getDefaultScript(DEFAULT_R_PLOTCMD);
}
public File getWSFile() {
return m_rWorkspaceFile;
}
public BufferedImage getImage() {
try {
if (m_image == null && m_nodeImageFile != null && m_nodeImageFile.isFile()) {
logger.warn("Restoring image from disk. This might take a few seconds...");
deserializeImage();
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
return m_image;
}
public void setPlotWarning() {
setWarningMessage("You need to re-execute the node before the view will show up");
}
@Override
protected void saveInternals(File nodeDir, ExecutionMonitor executionMonitor) throws IOException, CanceledExecutionException {
if (m_rWorkspaceFile != null) {
File f = new File(nodeDir, "pushtable.R");
Files.copy(m_rWorkspaceFile.toPath(), f.toPath());
}
if (m_image != null) {
File imageFile = new File(nodeDir, "image.png");
ImageIO.write(m_image, "png", imageFile);
}
}
@Override
protected void loadInternals(File nodeDir, ExecutionMonitor executionMonitor) throws IOException, CanceledExecutionException {
super.loadInternals(nodeDir, executionMonitor);
try {
File internalWS = new File(nodeDir,"pushtable.R");
m_rWorkspaceFile = getTempWSFile();
Files.copy(internalWS.toPath(), m_rWorkspaceFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
// support for old internal representation of plot images
File imgBin = new File(nodeDir, "image.bin");
File imgPng = new File(nodeDir, "image.png");
if(imgBin.exists()) {
m_nodeImageFile = imgBin;
isImageIcon = true;
}
if(imgPng.exists()) {
m_nodeImageFile = imgPng;
isImageIcon = false;
}
} catch(IOException e) {
throw new CanceledExecutionException("Failed to load internal representation of R workspace:\n" + e.getMessage());
}
}
@Override
protected void reset() {
super.reset();
m_image = null;
}
@Override
protected void onDispose() {
super.onDispose();
try {
removeTempWorkspace();
} catch (IOException e) {
logger.debug("temporary R workspace file could not be deleted:\t" + e.getMessage());
e.printStackTrace();
}
}
/**
* read image from file
* @throws IOException
*/
protected void deserializeImage() throws IOException {
if (m_nodeImageFile.isFile()) {
// support for old internal representation of plot images
if(isImageIcon) {
FileInputStream f_in = new FileInputStream(m_nodeImageFile);
// Read object using ObjectInputStream
ObjectInputStream obj_in = new ObjectInputStream(new BufferedInputStream(f_in));
// Read an object
try {
m_image = createBufferedImage(((ImageIcon) obj_in.readObject()));
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
obj_in.close();
}
} else {
m_image = ImageIO.read(m_nodeImageFile);
}
}
}
/**
* create BufferedImage from ImageIcon
* @param icon
* @return newly created buffered image
*/
public static BufferedImage createBufferedImage(ImageIcon icon) {
BufferedImage bi = new BufferedImage(
icon.getIconWidth(),
icon.getIconHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics g = bi.createGraphics();
icon.paintIcon(null, g, 0,0);
g.dispose();
return bi;
}
}