diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/DoSetViewPort.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/DoSetViewPort.java index 9d35941ac89e588f958a5256d17166792f538357..f61ea60f85913f553a4a03c668746d8849898c64 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/DoSetViewPort.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/DoSetViewPort.java @@ -355,7 +355,7 @@ final class DoSetViewPort extends RBaseNode { } } - // Note: unlike the GnuR conterpart of this method, we expect the LayoutPos to have the NULL + // Note: unlike the GnuR counterpart of this method, we expect the LayoutPos to have the NULL // positions replaced with nrow/ncol already. private ViewPortLocation calcViewportLocationFromLayout(LayoutPos pos, RList parentVP, Size parentSize) { // unlike in GnuR, we maintain parent viewport widths/heights in inches like anything else diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java index f77ddeb6c5b4a9d2a8882984195d530ff99e0654..354cc207472ef0334c9b04627f23b0179e87e366 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/FastRGridExternalLookup.java @@ -22,8 +22,11 @@ */ package com.oracle.truffle.r.library.fastrGrid; +import com.oracle.truffle.r.library.fastrGrid.grDevices.DevCurr; import com.oracle.truffle.r.library.fastrGrid.grDevices.DevHoldFlush; +import com.oracle.truffle.r.library.fastrGrid.grDevices.DevOff; import com.oracle.truffle.r.library.fastrGrid.grDevices.InitWindowedDevice; +import com.oracle.truffle.r.library.fastrGrid.grDevices.SavePlot; import com.oracle.truffle.r.library.fastrGrid.graphics.CPar; import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; import com.oracle.truffle.r.nodes.builtin.RInternalCodeBuiltinNode; @@ -48,6 +51,10 @@ public final class FastRGridExternalLookup { switch (name) { case "devholdflush": return DevHoldFlush.create(); + case "devcur": + return new DevCurr(); + case "devoff": + return DevOff.create(); case "PDF": return new IgnoredGridExternal(RNull.instance); default: @@ -59,6 +66,8 @@ public final class FastRGridExternalLookup { switch (name) { case "C_par": return new CPar(); + case "savePlot": + return SavePlot.create(); case "X11": return new InitWindowedDevice(); default: diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java index ec68fcff20f2af94b77f30ea667e8bb8b0164465..356fc3173b6ffea43e0c830bcc22d75ad33c71ef 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java @@ -22,30 +22,91 @@ */ package com.oracle.truffle.r.library.fastrGrid; -import com.oracle.truffle.r.library.fastrGrid.device.BufferedJFrameDevice; +import java.util.ArrayList; + +import com.oracle.truffle.r.library.fastrGrid.GridState.GridDeviceState; import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; -import com.oracle.truffle.r.library.fastrGrid.device.JFrameDevice; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.DeviceCloseException; +import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedJFrameDevice; +import com.oracle.truffle.r.library.fastrGrid.device.awt.JFrameDevice; +import com.oracle.truffle.r.library.fastrGrid.graphics.RGridGraphicsAdapter; +import com.oracle.truffle.r.runtime.RError; +import com.oracle.truffle.r.runtime.RError.Message; /** - * Encapsulated the acces to the global grid state. + * Encapsulated the access to the global grid state. */ public final class GridContext { private static final GridContext INSTANCE = new GridContext(); private final GridState gridState = new GridState(); - private GridDevice currentDevice; + /** + * This list should correspond to the names inside {@code .Devices} variable in R. + */ + private final ArrayList<DeviceAndState> devices = new ArrayList<>(2); + private int currentDeviceIdx = 0; + + private GridContext() { + devices.add(new DeviceAndState(null)); + } public static GridContext getContext() { return INSTANCE; } public GridState getGridState() { + gridState.setDeviceState(devices.get(currentDeviceIdx).state); return gridState; } + public int getCurrentDeviceIndex() { + return currentDeviceIdx; + } + + public int getDevicesSize() { + return devices.size(); + } + public GridDevice getCurrentDevice() { - if (currentDevice == null) { - currentDevice = new BufferedJFrameDevice(new JFrameDevice()); + assert currentDeviceIdx >= 0 : "accessing devices before they were initialized"; + return devices.get(currentDeviceIdx).device; + } + + public void setCurrentDevice(String name, GridDevice currentDevice) { + RGridGraphicsAdapter.addDevice(name); + RGridGraphicsAdapter.setCurrentDevice(name); + currentDeviceIdx = this.devices.size(); + this.devices.add(new DeviceAndState(currentDevice)); + assert devices.size() == RGridGraphicsAdapter.getDevicesCount(); + } + + public void openDefaultDevice() { + String defaultDev = RGridGraphicsAdapter.getDefaultDevice(); + if (defaultDev.equals("awt") || defaultDev.startsWith("X11")) { + BufferedJFrameDevice result = new BufferedJFrameDevice(JFrameDevice.create()); + setCurrentDevice(defaultDev, result); + } else { + throw RError.error(RError.NO_CALLER, Message.GENERIC, "FastR does not support device '" + defaultDev + "'."); + } + assert devices.size() == RGridGraphicsAdapter.getDevicesCount(); + } + + public void closeDevice(int which) throws DeviceCloseException { + assert which >= 0 && which < devices.size(); + devices.get(which).device.close(); + RGridGraphicsAdapter.removeDevice(which); + devices.remove(which); + if (currentDeviceIdx >= which) { + currentDeviceIdx--; + } + } + + private static final class DeviceAndState { + final GridDevice device; + final GridDeviceState state; + + DeviceAndState(GridDevice device) { + this.device = device; + this.state = new GridDeviceState(); } - return currentDevice; } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridLinesNode.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridLinesNode.java index ba1b404e0ddb2ff9dee47cf69dffb55c3a5dc17d..8db8ff46c3412d83f496c751a747fc24888e9b62 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridLinesNode.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridLinesNode.java @@ -76,7 +76,7 @@ public abstract class GridLinesNode extends Node { // following loop finds series of valid points (finite x and y values) and draws each // such series as a polyline for (int i = 0; i < unitIndexesLen; i++) { - int unitIndex = unitIndexes.getDataAt(i) - 1; // coverting R's 1-based index + int unitIndex = unitIndexes.getDataAt(i) - 1; // converting R's 1-based index Point origLoc = Point.fromUnits(unitToInches, x, y, unitIndex, conversionCtx); Point loc = TransformMatrix.transLocation(origLoc, vpTransform.transform); xx[i] = loc.x; diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridState.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridState.java index 6774a7b7220676b3ded896ea99b0ed6c1efa01a6..deab204f3b8ce9f20b3608108a34ce0c26fe3780 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridState.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridState.java @@ -17,12 +17,8 @@ import com.oracle.truffle.r.runtime.data.RNull; import com.oracle.truffle.r.runtime.env.REnvironment; public final class GridState { - private RList gpar; - private RList viewPort; private REnvironment gridEnv; - private double scale = 1; - private boolean deviceInitialized; - private int devHoldCount; + private GridDeviceState devState; /** * Current grob being drawn (for determining the list of grobs to search when evaluating a @@ -33,23 +29,26 @@ public final class GridState { GridState() { } + void setDeviceState(GridDeviceState state) { + devState = state; + } + public int getDevHoldCount() { - return devHoldCount; + return devState.devHoldCount; } public int setDevHoldCount(int devHoldCount) { - this.devHoldCount = devHoldCount; + devState.devHoldCount = devHoldCount; return devHoldCount; } - public void init(REnvironment gridEnv, GridDevice currentDevice) { + public void init(REnvironment gridEnv) { this.gridEnv = gridEnv; this.currentGrob = RNull.instance; - initGPar(currentDevice); } void initGPar(GridDevice currentDevice) { - gpar = GPar.createNew(currentDevice); + devState.gpar = GPar.createNew(currentDevice); } /** @@ -62,28 +61,33 @@ public final class GridState { public RList getGpar() { assert gridEnv != null : "GridState not initialized"; - return gpar; + return devState.gpar; } public void setGpar(RList gpar) { assert gridEnv != null : "GridState not initialized"; - this.gpar = gpar; + devState.gpar = gpar; } + /** + * Has the current device been initialized for use by grid? Note: the null device should never + * get initialized. The code initializing device should check if any device is open and if not, + * it should open the default device and initialize it. + */ public boolean isDeviceInitialized() { - return deviceInitialized; + return devState.isDeviceInitialized; } public void setDeviceInitialized() { - this.deviceInitialized = true; + devState.isDeviceInitialized = true; } public RList getViewPort() { - return viewPort; + return devState.viewPort; } public void setViewPort(RList viewPort) { - this.viewPort = viewPort; + devState.viewPort = viewPort; } public REnvironment getGridEnv() { @@ -99,7 +103,14 @@ public final class GridState { } public double getScale() { - return scale; + return devState.scale; } + static final class GridDeviceState { + private boolean isDeviceInitialized = false; + private RList gpar; + private RList viewPort; + private double scale = 1; + private int devHoldCount; + } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/IgnoredGridExternal.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/IgnoredGridExternal.java index 37090901e18992f8bc4f215d28e2987a9445ee57..17e5e85daea190ec92d812ff8d980bd0716c4e2a 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/IgnoredGridExternal.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/IgnoredGridExternal.java @@ -26,8 +26,8 @@ import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; import com.oracle.truffle.r.runtime.data.RArgsValuesAndNames; /** - * A node for externals that we ignore, becuase we do not need to implement them or because they - * support functionallity we do not implement yet, especially record/replay. + * A node for externals that we ignore, because we do not need to implement them or because they + * support features we do not implement yet, especially record/replay. */ final class IgnoredGridExternal extends RExternalBuiltinNode { private final Object result; diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LGridDirty.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LGridDirty.java index 2fe0e336c3df91d19d5b25083ebdb1bc19d10f20..a6314dff2c06b37db7b29a26f97f4fb674ea078c 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LGridDirty.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LGridDirty.java @@ -13,15 +13,14 @@ package com.oracle.truffle.r.library.fastrGrid; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.profiles.ConditionProfile; import com.oracle.truffle.r.library.fastrGrid.ViewPort.InitViewPortNode; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; import com.oracle.truffle.r.runtime.data.RArgsValuesAndNames; import com.oracle.truffle.r.runtime.data.RNull; final class LGridDirty extends RExternalBuiltinNode { @Child private InitViewPortNode initViewPort = new InitViewPortNode(); - private final ConditionProfile initViewPortProfile = ConditionProfile.createCountingProfile(); static { Casts.noCasts(LGridDirty.class); @@ -30,16 +29,32 @@ final class LGridDirty extends RExternalBuiltinNode { @Override public Object call(VirtualFrame frame, RArgsValuesAndNames args) { GridState gridState = GridContext.getContext().getGridState(); - if (!gridState.isDeviceInitialized()) { - CompilerDirectives.transferToInterpreter(); - GridContext.getContext().getCurrentDevice().openNewPage(); - gridState.setDeviceInitialized(); + if (gridState.isDeviceInitialized()) { + return RNull.instance; } - if (initViewPortProfile.profile(gridState.getViewPort() == null)) { - // this rarely happens, but we do not have a slow-path implementation (yet) - CompilerDirectives.transferToInterpreter(); + + // the rest only takes place if the device has been changed since the last time + CompilerDirectives.transferToInterpreter(); + + // if no device has been opened yet, open the default one and make it current + if (GridContext.getContext().getCurrentDevice() == null) { + GridContext.getContext().openDefaultDevice(); + gridState = GridContext.getContext().getGridState(); // grid state is device + // dependent + } + + // the current device has not been initialized yet... + GridDevice device = GridContext.getContext().getCurrentDevice(); + device.openNewPage(); + gridState.setViewPort(initViewPort.execute(frame)); + gridState.setDeviceInitialized(); + if (gridState.getGpar() == null) { + gridState.initGPar(device); + } + if (gridState.getViewPort() == null) { gridState.setViewPort(initViewPort.execute(frame)); } + return RNull.instance; } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LInitGrid.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LInitGrid.java index 885ace9762156ae4fba296ebb0e32a710ef8591d..74aa2a0b0d02c99dda2cb92fba9605167e7462f8 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LInitGrid.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/LInitGrid.java @@ -31,7 +31,7 @@ public abstract class LInitGrid extends RExternalBuiltinNode.Arg1 { @TruffleBoundary public Object doEnv(REnvironment gridEnv) { GridContext context = GridContext.getContext(); - context.getGridState().init(gridEnv, context.getCurrentDevice()); + context.getGridState().init(gridEnv); return RNull.instance; } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/Unit.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/Unit.java index 04165ca1749f927b4853bbaa103330760df49886..f881cc708180ce2e8da82a15ad5704eb000c8788 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/Unit.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/Unit.java @@ -56,7 +56,7 @@ import com.oracle.truffle.r.runtime.nodes.RBaseNode; /** * Note: internally in FastR Grid everything is in inches. However, some lists that are exposed to - * the R code should contain values in centimeters, we convert such values immediatelly once they + * the R code should contain values in centimeters, we convert such values immediately once they * enter our system. */ public final class Unit { @@ -185,6 +185,9 @@ public final class Unit { private static double convertToInches(double value, int index, int unitId, RList data, UnitConversionContext ctx, AxisOrDimension axisOrDim) { double vpSize = ctx.getViewPortSize(axisOrDim); + String str; + String[] lines; + double result = 0; switch (unitId) { case INCHES: return value; @@ -207,10 +210,20 @@ public final class Unit { return (value * ctx.gpar.getDrawingContext(index).getFontSize() * ctx.gpar.getDrawingContext(index).getLineHeight()) / INCH_TO_POINTS_FACTOR; case STRINGWIDTH: case MYSTRINGWIDTH: - return ctx.device.getStringWidth(ctx.gpar.getDrawingContext(index), RRuntime.asString(data.getDataAt(0))); + str = RRuntime.asString(data.getDataAt(0)); + lines = str.split("\n"); + for (int i = 0; i < lines.length; i++) { + result = Math.max(result, ctx.device.getStringWidth(ctx.gpar.getDrawingContext(index), lines[i])); + } + return result; case STRINGHEIGHT: case MYSTRINGHEIGHT: - return ctx.device.getStringHeight(ctx.gpar.getDrawingContext(index), RRuntime.asString(data.getDataAt(0))); + str = RRuntime.asString(data.getDataAt(0)); + lines = str.split("\n"); + for (int i = 0; i < lines.length; i++) { + result += ctx.device.getStringHeight(ctx.gpar.getDrawingContext(index), lines[i]); + } + return result; case NULL: return evaluateNullUnit(value, vpSize, ctx.nullLayoutMode, ctx.nullArithmeticMode); default: diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/ViewPort.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/ViewPort.java index 2a694a69f9e0a7c7dd0c25bfbfc691a0ed1dd409..80e93f6270d712ec482957010c2dde21dce72014 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/ViewPort.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/ViewPort.java @@ -153,6 +153,7 @@ final class ViewPort { public RList execute(VirtualFrame frame) { RFunction gridTopLevel = (RFunction) readGridTopLevel.execute(frame); RList topVP = (RList) callNode.execute(frame, gridTopLevel, RArgsValuesAndNames.EMPTY); + topVP.makeSharedPermanent(); GridDevice device = GridContext.getContext().getCurrentDevice(); // TODO: properly set the scale according to the current device diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/BufferedJFrameDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/BufferedJFrameDevice.java deleted file mode 100644 index 131165f954c651e39fd2558735541c9a627b1a3f..0000000000000000000000000000000000000000 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/BufferedJFrameDevice.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code 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 - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package com.oracle.truffle.r.library.fastrGrid.device; - -import java.awt.image.BufferStrategy; -import java.util.ArrayList; - -/** - * Decorator for {@link JFrameDevice} that implements {@link #hold()} and {@link #flush()}. Those - * methods open/draw a 2D graphics buffer, while the buffer is open, any drawing is done in the - * buffer not on the screen and we also record any drawing code to be able to replay it if the - * buffer happens to loose contents, which is a possibility mentioned in the documentation. Note: we - * rely on the fact that {@link DrawingContext} is immutable. - */ -public final class BufferedJFrameDevice implements GridDevice { - private final JFrameDevice inner; - private BufferStrategy buffer; - private ArrayList<Runnable> drawActions; - - public BufferedJFrameDevice(JFrameDevice inner) { - this.inner = inner; - } - - @Override - public void openNewPage() { - inner.openNewPage(); - } - - @Override - public void hold() { - if (buffer != null) { - return; // already buffering - } - buffer = inner.getCurrentFrame().getBufferStrategy(); - if (buffer == null) { - inner.getCurrentFrame().createBufferStrategy(2); - buffer = inner.getCurrentFrame().getBufferStrategy(); - } - if (drawActions == null) { - drawActions = new ArrayList<>(); - } else { - drawActions.clear(); - } - inner.initGraphics(buffer.getDrawGraphics()); - } - - @Override - public void flush() { - if (buffer == null) { - return; - } - - buffer.show(); - // re-draw the buffer if the contents were lost - while (buffer.contentsLost()) { - inner.initGraphics(buffer.getDrawGraphics()); - for (Runnable drawAction : drawActions) { - drawAction.run(); - } - buffer.show(); - } - - inner.initGraphics(inner.getCurrentFrame().getGraphics()); - buffer.dispose(); - buffer = null; - } - - @Override - public void drawRect(DrawingContext ctx, double leftX, double topY, double width, double height, double rotationAnticlockWise) { - inner.drawRect(ctx, leftX, topY, width, height, rotationAnticlockWise); - if (buffer != null) { - drawActions.add(() -> inner.drawRect(ctx, leftX, topY, width, height, rotationAnticlockWise)); - } - } - - @Override - public void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { - inner.drawPolyLines(ctx, x, y, startIndex, length); - if (buffer != null) { - drawActions.add(() -> inner.drawPolyLines(ctx, x, y, startIndex, length)); - } - } - - @Override - public void drawPolygon(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { - inner.drawPolygon(ctx, x, y, startIndex, length); - if (buffer != null) { - drawActions.add(() -> inner.drawPolygon(ctx, x, y, startIndex, length)); - } - } - - @Override - public void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) { - inner.drawCircle(ctx, centerX, centerY, radius); - if (buffer != null) { - drawActions.add(() -> inner.drawCircle(ctx, centerX, centerY, radius)); - } - } - - @Override - public void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) { - inner.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text); - if (buffer != null) { - drawActions.add(() -> inner.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text)); - } - } - - @Override - public double getWidth() { - return inner.getWidth(); - } - - @Override - public double getHeight() { - return inner.getHeight(); - } - - @Override - public double getStringWidth(DrawingContext ctx, String text) { - return inner.getStringWidth(ctx, text); - } - - @Override - public double getStringHeight(DrawingContext ctx, String text) { - return inner.getStringHeight(ctx, text); - } -} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java index afe759f3f09cdd99b23f3fda22107266ab63def3..90a2dcd6e941b70aa7f45f1dd1cefecc4c6eb6a3 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/GridDevice.java @@ -46,15 +46,25 @@ public interface GridDevice { default void flush() { } + /** + * Gets called when the device is closed from R. This is the point where non-interactive devices + * should save their output into a file. + * + * @throws DeviceCloseException if the closing was not successful e.g. because the file could + * not be written. + */ + default void close() throws DeviceCloseException { + } + /** * Draws a rectangle at given position, the center of the rotation should be the center of the * rectangle. The rotation is given in radians. */ - void drawRect(DrawingContext ctx, double leftX, double topY, double width, double height, double rotationAnticlockWise); + void drawRect(DrawingContext ctx, double leftX, double bottomY, double width, double height, double rotationAnticlockWise); /** * Connects given points with a line, there has to be at least two points in order to actually - * draw somethig. + * draw something. */ void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length); @@ -96,11 +106,27 @@ public interface GridDevice { double getStringWidth(DrawingContext ctx, String text); /** - * Gets the height of a line of text in inches, the default implementation uses only the - * parameters from the drawing context, but we allow the device to override this calculation - * with something more precise. + * Gets the height of a line of text in inches. This should include ascent and descent, i.e. + * from the very bottom to the very top of the tallest letter(s). The text is guaranteed to not + * contain any new lines. */ - default double getStringHeight(DrawingContext ctx, @SuppressWarnings("unused") String text) { - return (ctx.getLineHeight() * ctx.getFontSize()) / INCH_TO_POINTS_FACTOR; + double getStringHeight(DrawingContext ctx, String text); + + final class DeviceCloseException extends Exception { + private static final long serialVersionUID = 1182697755931636214L; + + public DeviceCloseException(Throwable cause) { + super(cause); + } + + @Override + public String getMessage() { + return getCause().getMessage(); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/ImageSaver.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/ImageSaver.java new file mode 100644 index 0000000000000000000000000000000000000000..7377c90be20238f0d8edb961100e82e531b2d64e --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/ImageSaver.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device; + +import java.io.IOException; + +/** + * Devices that support saving their current state into a file should implement this interface. + * Note: this only makes sense for interactive devices. Devices like SVG are already saving into a + * file. + */ +public interface ImageSaver { + void save(String path, String fileType) throws IOException; +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java new file mode 100644 index 0000000000000000000000000000000000000000..485ad251614d1fc06524ab20352361391b1060f9 --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device.awt; + +import static java.awt.image.BufferedImage.TYPE_INT_RGB; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +public final class BufferedImageDevice extends Graphics2DDevice { + private final BufferedImage image; + private final String filename; + private final String fileType; + + private BufferedImageDevice(String filename, String fileType, BufferedImage image, Graphics2D graphics, int width, int height) { + super(graphics, width, height, true); + this.filename = filename; + this.fileType = fileType; + this.image = image; + graphics.setBackground(new Color(255, 255, 255)); + graphics.clearRect(0, 0, width, height); + } + + public static BufferedImageDevice open(String filename, String fileType, int width, int height) throws NotSupportedImageFormatException { + if (!isSupportedFormat(fileType)) { + throw new NotSupportedImageFormatException(); + } + BufferedImage image = new BufferedImage(width, height, TYPE_INT_RGB); + return new BufferedImageDevice(filename, fileType, image, (Graphics2D) image.getGraphics(), width, height); + } + + @Override + public void close() throws DeviceCloseException { + try { + ImageIO.write(image, fileType, new File(filename)); + } catch (IOException e) { + throw new DeviceCloseException(e); + } + } + + private static boolean isSupportedFormat(String formatName) { + String[] formatNames = ImageIO.getWriterFormatNames(); + for (String n : formatNames) { + if (n.equals(formatName)) { + return true; + } + } + return false; + } + + public static class NotSupportedImageFormatException extends Exception { + private static final long serialVersionUID = 1182697755931636217L; + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedJFrameDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedJFrameDevice.java new file mode 100644 index 0000000000000000000000000000000000000000..b94f4e39aa0864e6819430128b69eb432dd2439d --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedJFrameDevice.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device.awt; + +import static java.awt.image.BufferedImage.TYPE_INT_RGB; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.image.BufferStrategy; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import javax.imageio.ImageIO; + +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; +import com.oracle.truffle.r.library.fastrGrid.device.ImageSaver; + +/** + * Decorator for {@link JFrameDevice} that implements {@link #hold()} and {@link #flush()} + * functionality and implements the {@link ImageSaver} device. + * + * Methods {@link #hold()} and {@link #flush()} open/draw a 2D graphics buffer, while the buffer is + * open, any drawing is done in the buffer not on the screen and the buffer is dumped to the screen + * once {@link #flush()} is called. + * + * We also record any drawing code to be able to replay it if the buffer happens to loose its + * contents, which is a possibility mentioned in the documentation. Moreover, this record of drawing + * can be used in {@link #save(String, String)} to replay the drawing in a {@code BufferedImage}. + * Note: here we rely on the fact that {@link DrawingContext} is immutable. + */ +public final class BufferedJFrameDevice implements GridDevice, ImageSaver { + private final JFrameDevice inner; + private final ArrayList<Runnable> drawActions = new ArrayList<>(200); + private BufferStrategy buffer; + + public BufferedJFrameDevice(JFrameDevice inner) { + this.inner = inner; + } + + @Override + public void openNewPage() { + inner.openNewPage(); + drawActions.clear(); + if (buffer != null) { + // if new page is opened while we are on hold, we should throw away current buffer. In + // other words that is like starting new hold without previous flush. + buffer.dispose(); + buffer = null; + hold(); + } + } + + @Override + public void hold() { + if (buffer != null) { + return; // already buffering + } + buffer = inner.getCurrentFrame().getBufferStrategy(); + if (buffer == null) { + inner.getCurrentFrame().createBufferStrategy(2); + buffer = inner.getCurrentFrame().getBufferStrategy(); + } + drawActions.clear(); + setGraphics(buffer.getDrawGraphics()); + } + + @Override + public void flush() { + if (buffer == null) { + return; + } + + buffer.show(); + // re-draw the buffer if the contents were lost + while (buffer.contentsLost()) { + setGraphics(buffer.getDrawGraphics()); + for (Runnable drawAction : drawActions) { + drawAction.run(); + } + buffer.show(); + } + + setGraphics(inner.getCurrentFrame().getGraphics()); + buffer.dispose(); + buffer = null; + } + + @Override + public void close() throws DeviceCloseException { + inner.close(); + } + + @Override + public void drawRect(DrawingContext ctx, double leftX, double bottomY, double width, double height, double rotationAnticlockWise) { + inner.drawRect(ctx, leftX, bottomY, width, height, rotationAnticlockWise); + drawActions.add(() -> inner.drawRect(ctx, leftX, bottomY, width, height, rotationAnticlockWise)); + } + + @Override + public void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + inner.drawPolyLines(ctx, x, y, startIndex, length); + double[] xCopy = new double[x.length]; + double[] yCopy = new double[y.length]; + System.arraycopy(x, 0, xCopy, 0, x.length); + System.arraycopy(y, 0, yCopy, 0, y.length); + drawActions.add(() -> inner.drawPolyLines(ctx, xCopy, yCopy, startIndex, length)); + } + + @Override + public void drawPolygon(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + inner.drawPolygon(ctx, x, y, startIndex, length); + double[] xCopy = new double[x.length]; + double[] yCopy = new double[y.length]; + System.arraycopy(x, 0, xCopy, 0, x.length); + System.arraycopy(y, 0, yCopy, 0, y.length); + drawActions.add(() -> inner.drawPolygon(ctx, xCopy, yCopy, startIndex, length)); + } + + @Override + public void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) { + inner.drawCircle(ctx, centerX, centerY, radius); + drawActions.add(() -> inner.drawCircle(ctx, centerX, centerY, radius)); + } + + @Override + public void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) { + inner.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text); + drawActions.add(() -> inner.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text)); + } + + @Override + public double getWidth() { + return inner.getWidth(); + } + + @Override + public double getHeight() { + return inner.getHeight(); + } + + @Override + public double getStringWidth(DrawingContext ctx, String text) { + return inner.getStringWidth(ctx, text); + } + + @Override + public double getStringHeight(DrawingContext ctx, String text) { + return inner.getStringHeight(ctx, text); + } + + @Override + public void save(String path, String fileType) throws IOException { + int realWidth = (int) (getWidth() * JFrameDevice.AWT_POINTS_IN_INCH); + int readHeight = (int) (getHeight() * JFrameDevice.AWT_POINTS_IN_INCH); + BufferedImage image = new BufferedImage(realWidth, readHeight, TYPE_INT_RGB); + Graphics2D imageGraphics = (Graphics2D) image.getGraphics(); + imageGraphics.setBackground(new Color(255, 255, 255)); + imageGraphics.clearRect(0, 0, realWidth, readHeight); + setGraphics(imageGraphics); + for (Runnable drawAction : drawActions) { + drawAction.run(); + } + ImageIO.write(image, fileType, new File(path)); + setGraphics(inner.getCurrentFrame().getGraphics()); + } + + private void setGraphics(Graphics graphics) { + JFrameDevice.defaultInitGraphics((Graphics2D) graphics); + inner.getGraphics2D().dispose(); + inner.setGraphics2D((Graphics2D) graphics); + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/JFrameDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/Graphics2DDevice.java similarity index 54% rename from com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/JFrameDevice.java rename to com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/Graphics2DDevice.java index 6ee2268766a6fb73822b1734bfff37def493a466..055084a451c858ad163dd07e432c2121f14c67b4 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/JFrameDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/Graphics2DDevice.java @@ -20,82 +20,86 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -package com.oracle.truffle.r.library.fastrGrid.device; +package com.oracle.truffle.r.library.fastrGrid.device.awt; import static com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.INCH_TO_POINTS_FACTOR; import static java.awt.geom.Path2D.WIND_EVEN_ODD; import java.awt.BasicStroke; -import java.awt.BorderLayout; import java.awt.Color; -import java.awt.Dimension; import java.awt.Font; -import java.awt.Graphics; +import java.awt.FontMetrics; import java.awt.Graphics2D; -import java.awt.HeadlessException; import java.awt.Paint; -import java.awt.RenderingHints; import java.awt.Shape; -import java.awt.Toolkit; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; -import java.util.function.Supplier; - -import javax.swing.JFrame; -import javax.swing.JPanel; +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext; import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.GridFontStyle; import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.GridLineEnd; import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext.GridLineJoin; +import com.oracle.truffle.r.library.fastrGrid.device.GridColor; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; import com.oracle.truffle.r.runtime.RInternalError; -public final class JFrameDevice implements GridDevice { +/** + * A device that draws to given {@code Graphics2D} object regardless of whether it was created for + * e.g. an image, or window. This device only used by other devices and not exposed at the R level. + * Note: it is responsibility of the use to handle resources management, i.e. calling + * {@code dispose} on the graphics object once it is not needed anymore. + */ +public class Graphics2DDevice implements GridDevice { // Grid's coordinate system has origin in left bottom corner and y axis grows from bottom to - // top. Moreover, the grid system uses inches as units. We use transformation to adjust the java - // coordinate system to the grid one. However, in the case of text rendering, we cannot simply - // turn upside down the y-axis, because the text would be upside down too, so for text rendering - // only, we reset the transformation completely and transform the coordinates ourselves - private static final double POINTS_IN_INCH = 72.; + // top. Moreover, the grid system uses inches as units. We do not use builtin transformations, + // because (a) text rendering gets affected badly (upside down text), (b) the user of this call + // may wish to apply his/her own transformations to the graphics object and we should not + // interfere with these. In cases we do use transformation, we make sure to set back the + // original one after we're done. + static final double AWT_POINTS_IN_INCH = 72.; - private static BasicStroke solidStroke; private static BasicStroke blankStroke; - private FastRFrame currentFrame; + private final int width; + private final int height; private Graphics2D graphics; + private final boolean graphicsIsExclusive; + private DrawingContext cachedContext; + + /** + * @param graphics Object that should be used for the drawing. + * @param width Width of the drawing area in AWT units. + * @param height Height of the drawing area in AWT units. + * @param graphicsIsExclusive If the graphics object is exclusively used for drawing only by + * this class, then it can optimize some things. + */ + Graphics2DDevice(Graphics2D graphics, int width, int height, boolean graphicsIsExclusive) { + initStrokes(); + setGraphics2D(graphics); + this.width = width; + this.height = height; + this.graphicsIsExclusive = graphicsIsExclusive; + } @Override public void openNewPage() { - initStrokes(); - if (currentFrame == null) { - currentFrame = new FastRFrame(); - currentFrame.setVisible(true); - initGraphics(currentFrame.getGraphics()); - } else { - noTransform(() -> { - graphics.clearRect(0, 0, currentFrame.getWidth(), currentFrame.getHeight()); - return null; - }); - } + graphics.clearRect(0, 0, width, height); } @Override - public void drawRect(DrawingContext ctx, double leftX, double topY, double width, double height, double rotationAnticlockWise) { + public void drawRect(DrawingContext ctx, double leftXIn, double bottomYIn, double widthIn, double heightIn, double rotationAnticlockWise) { + int leftX = transX(leftXIn); + int topY = transY(bottomYIn + heightIn); + int width = transDim(widthIn); + int height = transDim(heightIn); setContext(ctx); if (rotationAnticlockWise == 0.) { drawShape(ctx, new Rectangle2D.Double(leftX, topY, width, height)); return; } - AffineTransform oldTr = graphics.getTransform(); - AffineTransform newTr = new AffineTransform(oldTr); - newTr.translate(leftX + width / 2, topY + height / 2); - newTr.rotate(rotationAnticlockWise); - graphics.setTransform(newTr); - drawShape(ctx, new Rectangle2D.Double(-(width / 2), -(height / 2), width, height)); - graphics.setTransform(oldTr); + transformed(leftX + width / 2, topY + height / 2, rotationAnticlockWise, () -> drawShape(ctx, new Rectangle2D.Double(-(width / 2), -(height / 2), width, height))); } @Override @@ -113,78 +117,93 @@ public final class JFrameDevice implements GridDevice { } @Override - public void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) { + public void drawCircle(DrawingContext ctx, double centerXIn, double centerYIn, double radiusIn) { setContext(ctx); + int centerX = transX(centerXIn); + int centerY = transY(centerYIn); + int radius = transDim(radiusIn); drawShape(ctx, new Ellipse2D.Double(centerX - radius, centerY - radius, radius * 2d, radius * 2d)); } @Override - public void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) { - setContext(ctx); - noTransform(() -> { - AffineTransform tr = graphics.getTransform(); - tr.translate((float) (leftX * POINTS_IN_INCH), (float) (currentFrame.getContentPane().getHeight() - bottomY * POINTS_IN_INCH)); - tr.rotate(-rotationAnticlockWise); - graphics.setTransform(tr); - setFont(ctx); - graphics.drawString(text, 0, 0); - return null; - }); + public void drawString(DrawingContext ctx, double leftXIn, double bottomYIn, double rotationAnticlockWise, String text) { + setContextAndFont(ctx); + int leftX = transX(leftXIn); + FontMetrics fontMetrics = graphics.getFontMetrics(graphics.getFont()); + int bottomY = transY(bottomYIn) - fontMetrics.getDescent(); + transformed(leftX, bottomY, rotationAnticlockWise, () -> graphics.drawString(text, 0, 0)); } @Override public double getWidth() { - return currentFrame.getContentPane().getWidth() / POINTS_IN_INCH; + return width / AWT_POINTS_IN_INCH; } @Override public double getHeight() { - return currentFrame.getContentPane().getHeight() / POINTS_IN_INCH; + return height / AWT_POINTS_IN_INCH; } @Override public double getStringWidth(DrawingContext ctx, String text) { - setContext(ctx); - return noTransform(() -> { - setFont(ctx); - int swingUnits = graphics.getFontMetrics(graphics.getFont()).stringWidth(text); - return swingUnits / POINTS_IN_INCH; - }); + setContextAndFont(ctx); + int swingUnits = graphics.getFontMetrics(graphics.getFont()).stringWidth(text); + return swingUnits / AWT_POINTS_IN_INCH; } @Override public double getStringHeight(DrawingContext ctx, String text) { - setContext(ctx); - return noTransform(() -> { - setFont(ctx); - int swingUnits = graphics.getFont().getSize(); - return swingUnits / POINTS_IN_INCH; - }); + setContextAndFont(ctx); + FontMetrics fontMetrics = graphics.getFontMetrics(graphics.getFont()); + double swingUnits = fontMetrics.getAscent() + fontMetrics.getDescent(); + return swingUnits / AWT_POINTS_IN_INCH; } - FastRFrame getCurrentFrame() { - return currentFrame; + void setGraphics2D(Graphics2D newGraphics) { + assert newGraphics != null; + graphics = newGraphics; } - void initGraphics(Graphics newGraphics) { - if (graphics != null) { - graphics.dispose(); + public Graphics2D getGraphics2D() { + return graphics; + } + + private int transY(double y) { + return height - (int) (y * AWT_POINTS_IN_INCH); + } + + private static int transX(double x) { + return (int) (x * AWT_POINTS_IN_INCH); + } + + private static int transDim(double widthOrHeight) { + return (int) (widthOrHeight * AWT_POINTS_IN_INCH); + } + + private static void initStrokes() { + if (blankStroke != null) { + return; } - graphics = (Graphics2D) newGraphics; - graphics.translate(0, currentFrame.getHeight()); - graphics.scale(POINTS_IN_INCH, -POINTS_IN_INCH); - graphics.setStroke(new BasicStroke((float) (1d / POINTS_IN_INCH))); - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + blankStroke = new BasicStroke(0f); + } + + private void transformed(int centreX, int centreY, double radiansAnticlockwise, Runnable action) { + AffineTransform oldTransform = graphics.getTransform(); + AffineTransform newTr = new AffineTransform(oldTransform); + newTr.translate(centreX, centreY); + newTr.rotate(-radiansAnticlockwise); + graphics.setTransform(newTr); + action.run(); + graphics.setTransform(oldTransform); } private Path2D.Double getPath2D(double[] x, double[] y, int startIndex, int length) { assert startIndex >= 0 && startIndex < x.length && startIndex < y.length : "startIndex out of bounds"; assert length > 0 && (startIndex + length) <= Math.min(x.length, y.length) : "length out of bounds"; Path2D.Double path = new Path2D.Double(WIND_EVEN_ODD, x.length); - path.moveTo(x[startIndex], y[startIndex]); + path.moveTo(transX(x[startIndex]), transY(y[startIndex])); for (int i = startIndex + 1; i < length; i++) { - path.lineTo(x[i], y[i]); + path.lineTo(transX(x[i]), transY(y[i])); } return path; } @@ -198,16 +217,26 @@ public final class JFrameDevice implements GridDevice { } private void setContext(DrawingContext ctx) { + if (graphicsIsExclusive && cachedContext == ctx) { + return; + } graphics.setColor(fromGridColor(ctx.getColor())); graphics.setStroke(getStrokeFromCtx(ctx)); + cachedContext = ctx; } - private void setFont(DrawingContext ctx) { - float fontSize = (float) ((ctx.getFontSize() / INCH_TO_POINTS_FACTOR) * POINTS_IN_INCH); + private void setContextAndFont(DrawingContext ctx) { + if (graphicsIsExclusive && cachedContext == ctx) { + return; + } + setContext(ctx); + float fontSize = (float) ((ctx.getFontSize() / INCH_TO_POINTS_FACTOR) * AWT_POINTS_IN_INCH); Font font = new Font(getFontName(ctx.getFontFamily()), getAwtFontStyle(ctx.getFontStyle()), 1).deriveFont(fontSize); graphics.setFont(font); } + // Transformation of DrawingContext data types to AWT constants + private String getFontName(String gridFontFamily) { if (gridFontFamily == null) { return null; @@ -240,19 +269,11 @@ public final class JFrameDevice implements GridDevice { } } - private <T> T noTransform(Supplier<T> action) { - AffineTransform transform = graphics.getTransform(); - graphics.setTransform(new AffineTransform()); - T result = action.get(); - graphics.setTransform(transform); - return result; - } - private static Color fromGridColor(GridColor color) { return new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()); } - private static BasicStroke getStrokeFromCtx(DrawingContext ctx) { + private BasicStroke getStrokeFromCtx(DrawingContext ctx) { byte[] type = ctx.getLineType(); double width = ctx.getLineWidth(); int lineJoin = fromGridLineJoin(ctx.getLineJoin()); @@ -261,16 +282,13 @@ public final class JFrameDevice implements GridDevice { if (type == DrawingContext.GRID_LINE_BLANK) { return blankStroke; } else if (type == DrawingContext.GRID_LINE_SOLID) { - if (width == 1. && solidStroke.getLineJoin() == lineJoin && solidStroke.getMiterLimit() == lineMitre && solidStroke.getEndCap() == endCap) { - return solidStroke; - } - return new BasicStroke((float) (width / POINTS_IN_INCH), endCap, lineJoin, lineMitre); + return new BasicStroke((float) (width), endCap, lineJoin, lineMitre); } float[] pattern = new float[type.length]; for (int i = 0; i < pattern.length; i++) { - pattern[i] = (float) (type[i] / POINTS_IN_INCH); + pattern[i] = (float) (type[i]); } - return new BasicStroke((float) (width / POINTS_IN_INCH), endCap, lineJoin, lineMitre, pattern, 0f); + return new BasicStroke((float) (width), endCap, lineJoin, lineMitre, pattern, 0f); } private static int fromGridLineEnd(GridLineEnd lineEnd) { @@ -298,49 +316,4 @@ public final class JFrameDevice implements GridDevice { throw RInternalError.shouldNotReachHere("unexpected value of GridLineJoin enum"); } } - - private static void initStrokes() { - if (solidStroke != null) { - return; - } - solidStroke = new BasicStroke((float) (1f / POINTS_IN_INCH)); - blankStroke = new BasicStroke(0f); - } - - static class FastRFrame extends JFrame { - private static final long serialVersionUID = 1L; - private final Dimension framePreferredSize = new Dimension(720, 720); - private final JPanel fastRComponent = new JPanel(); - - FastRFrame() throws HeadlessException { - super("FastR"); - addCloseListener(); - createUI(); - center(); - } - - private void createUI() { - setLayout(new BorderLayout()); - setSize(framePreferredSize); - add(fastRComponent, BorderLayout.CENTER); - fastRComponent.setPreferredSize(getSize()); - } - - private void addCloseListener() { - addWindowFocusListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - dispose(); - } - }); - } - - private void center() { - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - Dimension frameSize = getSize(); - int x = (screenSize.width - frameSize.width) / 2; - int y = (screenSize.height - frameSize.height) / 2; - setLocation(x, y); - } - } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/JFrameDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/JFrameDevice.java new file mode 100644 index 0000000000000000000000000000000000000000..6e31f1f417c26d9f791b73dd9e540838c93bc0e5 --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/JFrameDevice.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device.awt; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.HeadlessException; +import java.awt.RenderingHints; +import java.awt.Toolkit; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +public final class JFrameDevice extends Graphics2DDevice { + private final JFrame currentFrame; + private final boolean disposeResources; + + /** + * @param frame The frame that should be used for drawing. + * @param graphics The graphics object that should be used for drawing. This constructor + * overload allows to initialize the graphics object. + * @param disposeResources Should the frame and graphics objects be disposed at {@link #close()} + * . + */ + private JFrameDevice(JFrame frame, Graphics2D graphics, boolean disposeResources) { + super(graphics, frame.getContentPane().getWidth(), frame.getContentPane().getHeight(), true); + currentFrame = frame; + this.disposeResources = disposeResources; + } + + /** + * Creates a standalone device that manages the window itself and closes it once + * {@link #close()} gets called. + */ + public static JFrameDevice create() { + FastRFrame frame = new FastRFrame(); + frame.setVisible(true); + Graphics2D graphics = (Graphics2D) frame.getGraphics(); + defaultInitGraphics(graphics); + return new JFrameDevice(frame, graphics, true); + } + + @Override + public void close() throws DeviceCloseException { + if (disposeResources) { + getGraphics2D().dispose(); + currentFrame.dispose(); + } + } + + JFrame getCurrentFrame() { + return currentFrame; + } + + static void defaultInitGraphics(Graphics2D graphics) { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + } + + static class FastRFrame extends JFrame { + private static final long serialVersionUID = 1L; + private final Dimension framePreferredSize = new Dimension(720, 720); + private final JPanel fastRComponent = new JPanel(); + + FastRFrame() throws HeadlessException { + super("FastR"); + addCloseListener(); + createUI(); + center(); + } + + private void createUI() { + setLayout(new BorderLayout()); + setSize(framePreferredSize); + add(fastRComponent, BorderLayout.CENTER); + fastRComponent.setPreferredSize(getSize()); + } + + private void addCloseListener() { + addWindowFocusListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + dispose(); + } + }); + } + + private void center() { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + Dimension frameSize = getSize(); + int x = (screenSize.width - frameSize.width) / 2; + int y = (screenSize.height - frameSize.height) / 2; + setLocation(x, y); + } + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevCurr.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevCurr.java new file mode 100644 index 0000000000000000000000000000000000000000..5c9046e35dc561a4f13b74eb43bc67676f16c48c --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevCurr.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.grDevices; + +import com.oracle.truffle.r.library.fastrGrid.GridContext; +import com.oracle.truffle.r.library.fastrGrid.graphics.RGridGraphicsAdapter; +import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; +import com.oracle.truffle.r.runtime.data.RDataFactory; +import com.oracle.truffle.r.runtime.data.RStringVector; +import com.oracle.truffle.r.runtime.data.model.RAbstractIntVector; + +public final class DevCurr extends RExternalBuiltinNode.Arg0 { + static { + Casts.noCasts(DevCurr.class); + } + + @Override + public RAbstractIntVector execute() { + int index = GridContext.getContext().getCurrentDeviceIndex(); + RStringVector names = RDataFactory.createStringVectorFromScalar(RGridGraphicsAdapter.getDeviceName(index)); + return RDataFactory.createIntVector(new int[]{index + 1}, RDataFactory.COMPLETE_VECTOR, names); + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevOff.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevOff.java new file mode 100644 index 0000000000000000000000000000000000000000..e0d57b606a07af5bb4a1a457f0c0a90d5d50dffe --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/DevOff.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.grDevices; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.r.library.fastrGrid.GridContext; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.DeviceCloseException; +import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; +import com.oracle.truffle.r.runtime.RError; +import com.oracle.truffle.r.runtime.RError.Message; +import com.oracle.truffle.r.runtime.data.RNull; + +public abstract class DevOff extends RExternalBuiltinNode.Arg1 { + static { + Casts casts = new Casts(DevOff.class); + casts.arg(0).asIntegerVector().findFirst(); + } + + public static DevOff create() { + return DevOffNodeGen.create(); + } + + @Specialization + @TruffleBoundary + public Object devOff(int whichR) { + GridContext ctx = GridContext.getContext(); + int which = whichR - 1; // convert to Java index + if (which < 0 || which >= ctx.getDevicesSize()) { + throw RError.error(RError.NO_CALLER, Message.GENERIC, "Wrong device number."); + } + try { + ctx.closeDevice(which); + } catch (DeviceCloseException e) { + throw error(Message.GENERIC, "Cannot close the device. Details: " + e.getMessage()); + } + return RNull.instance; + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java index d6314346b22dd1fc81e28524a31f56f0b1a27bc6..f1fab85632025eb617b80a6347c691d6a4f8a679 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java @@ -22,18 +22,24 @@ */ package com.oracle.truffle.r.library.fastrGrid.grDevices; +import java.io.IOException; + import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.r.library.fastrGrid.GridContext; -import com.oracle.truffle.r.library.fastrGrid.GridState; -import com.oracle.truffle.r.library.fastrGrid.graphics.RGridGraphicsAdapter; +import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice; +import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice.NotSupportedImageFormatException; +import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedJFrameDevice; +import com.oracle.truffle.r.library.fastrGrid.device.awt.JFrameDevice; import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; +import com.oracle.truffle.r.runtime.RError.Message; +import com.oracle.truffle.r.runtime.RRuntime; import com.oracle.truffle.r.runtime.data.RArgsValuesAndNames; import com.oracle.truffle.r.runtime.data.RNull; /** * Node that handles the {@code C_X11} external calls. Those calls may be initiated from either the - * {@code X11} function or FastR specific {@code awt} function. In either case the result is that - * the AWT window is opened and ready for drawing. + * {@code X11}, {@code jpeg}, {@code bmp}, {@code png} functions and from FastR specific {@code awt} + * . The arguments determine which device should be opened. */ public final class InitWindowedDevice extends RExternalBuiltinNode { static { @@ -43,12 +49,28 @@ public final class InitWindowedDevice extends RExternalBuiltinNode { @Override @TruffleBoundary protected Object call(RArgsValuesAndNames args) { - GridState gridState = GridContext.getContext().getGridState(); - if (!gridState.isDeviceInitialized()) { - GridContext.getContext().getCurrentDevice().openNewPage(); - gridState.setDeviceInitialized(); + // if the first argument is a String, then it may describes the image format and filename to + // use, the format is e.g. "jpeg::quality:filename" + if (args.getLength() >= 1) { + String name = RRuntime.asString(args.getArgument(0)); + if (!RRuntime.isNA(name) && name.contains("::")) { + return openImageDevice(name); + } + } + // otherwise the + GridContext.getContext().setCurrentDevice(args.getLength() == 0 ? "awt" : "X11cairo", new BufferedJFrameDevice(JFrameDevice.create())); + return RNull.instance; + } + + private Object openImageDevice(String name) { + String formatName = name.substring(0, name.indexOf("::")); + String filename = name.substring(name.lastIndexOf(':') + 1); + try { + BufferedImageDevice device = BufferedImageDevice.open(filename, formatName, 700, 700); + GridContext.getContext().setCurrentDevice(formatName, device); + } catch (NotSupportedImageFormatException e) { + throw error(Message.GENERIC, String.format("Format '%s' is not supported.", formatName)); } - RGridGraphicsAdapter.setCurrentDevice(args.getLength() == 0 ? "awt" : "X11cairo"); return RNull.instance; } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R index 16a15ce9b2ba2313874a6d1e22eca5c237f15dfc..390bdacd81a88ad29531012f05a1cdf3e662103f 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/R/fastrGridDevices.R @@ -27,4 +27,20 @@ eval(expression({ awt <- function(...) { .External2(grDevices:::C_X11) } + # GnuR version only works with "X11cairo" device. Our version of savePlot + # works with "awt" device and "X11cairo", which is for us only alias for + # "awt". Moreover, we only support formats that awt supports. + savePlot <- function (filename = paste("Rplot", type, sep = "."), type = c("png", "jpeg", "bmp"), device = dev.cur()) { + type <- match.arg(type) + devlist <- dev.list() + devcur <- match(device, devlist, NA) + if (is.na(devcur)) { + stop("no such device") + } + devname <- names(devlist)[devcur] + if (devname != "X11cairo" && devname != "awt") { + stop("can only copy from 'X11(type=\"*cairo\")' or 'awt' devices") + } + invisible(.External2(C_savePlot, filename, type, device)) + } }), asNamespace("grDevices")) \ No newline at end of file diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/SavePlot.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/SavePlot.java new file mode 100644 index 0000000000000000000000000000000000000000..c42a0e33b9b96c8116cc6439102d52553c929a47 --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/SavePlot.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.grDevices; + +import java.io.IOException; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.r.library.fastrGrid.GridContext; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; +import com.oracle.truffle.r.library.fastrGrid.device.ImageSaver; +import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; +import com.oracle.truffle.r.runtime.RError.Message; +import com.oracle.truffle.r.runtime.data.RNull; + +public abstract class SavePlot extends RExternalBuiltinNode.Arg3 { + static { + Casts casts = new Casts(SavePlot.class); + casts.arg(0).asStringVector().findFirst(); + casts.arg(1).asStringVector().findFirst(); + } + + public static SavePlot create() { + return SavePlotNodeGen.create(); + } + + @Specialization + @TruffleBoundary + Object savePlot(String filename, String type) { + GridDevice device = GridContext.getContext().getCurrentDevice(); + if (!(device instanceof ImageSaver)) { + throw error(Message.GENERIC, "Current device does not support plot saving. " + + "Note that FastR savePlot function ignores the device argument and always uses the current device."); + } + try { + ((ImageSaver) device).save(filename, type); + } catch (IOException e) { + throw error(Message.GENERIC, "I/O error occured when saving the image."); + } + return RNull.instance; + } +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/graphics/RGridGraphicsAdapter.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/graphics/RGridGraphicsAdapter.java index 2bb5adf63c7a98eab79bddc9739ca6327002107f..f99012e978ce97b5acaeadaa66f323021627937c 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/graphics/RGridGraphicsAdapter.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/graphics/RGridGraphicsAdapter.java @@ -19,6 +19,7 @@ import com.oracle.truffle.r.runtime.RError; import com.oracle.truffle.r.runtime.RError.Message; import com.oracle.truffle.r.runtime.ROptions; import com.oracle.truffle.r.runtime.ROptions.OptionsException; +import com.oracle.truffle.r.runtime.RRuntime; import com.oracle.truffle.r.runtime.context.RContext; import com.oracle.truffle.r.runtime.data.RDataFactory; import com.oracle.truffle.r.runtime.data.RPairList; @@ -38,8 +39,14 @@ import com.oracle.truffle.r.runtime.env.REnvironment; * tries to open the default device it uses 'awt'. If the future this should be either 'awt' for * interactive sessions, or some image format device for batch sessions. We should also honor the * R_INTERACTIVE_DEVICE and R_DEFAULT_DEVICE environment variables. + * + * The responsibility of this class if to provide convenient access to those R-level variables. The + * actual devices instances are maintained in + * {@link com.oracle.truffle.r.library.fastrGrid.GridContext} since we only have grid devices and no + * generic graphics devices. */ public final class RGridGraphicsAdapter { + private static final String DEFAULT_DEVICE_OPTION = "device"; private static final String NULL_DEVICE = "null device"; /** * The graphics devices system maintains two variables .Device and .Devices in the base @@ -55,23 +62,72 @@ public final class RGridGraphicsAdapter { } public static void initialize() { + addDevice(NULL_DEVICE); setCurrentDevice(NULL_DEVICE); ROptions.ContextStateImpl options = RContext.getInstance().stateROptions; try { - options.setValue("device", "awt"); + options.setValue(DEFAULT_DEVICE_OPTION, "awt"); } catch (OptionsException e) { RError.warning(RError.NO_CALLER, Message.GENERIC, "FastR could not set the 'device' options to awt."); } } + public static void removeDevice(int index) { + assert index > 0 : "cannot remove null device"; + REnvironment baseEnv = REnvironment.baseEnv(); + RPairList devices = (RPairList) baseEnv.get(DOT_DEVICES); + assert index < devices.getLength() : "wrong index in removeDevice"; + RPairList prev = devices; + for (int i = 0; i < index - 1; ++i) { + prev = (RPairList) prev.cdr(); + } + RPairList toRemove = (RPairList) prev.cdr(); + prev.setCdr(toRemove.cdr()); + setCurrentDevice((String) prev.car()); + } + public static void setCurrentDevice(String name) { REnvironment baseEnv = REnvironment.baseEnv(); + assert contains((RPairList) baseEnv.get(DOT_DEVICES), name) : "setCurrentDevice can be invoked only after the device is added with addDevice"; baseEnv.safePut(DOT_DEVICE, name); - Object devices = baseEnv.get(DOT_DEVICES); - if (devices instanceof RPairList) { - ((RPairList) devices).appendToEnd(RDataFactory.createPairList(name)); + } + + public static void addDevice(String name) { + REnvironment baseEnv = REnvironment.baseEnv(); + baseEnv.safePut(DOT_DEVICE, name); + Object dotDevices = baseEnv.get(DOT_DEVICES); + if (dotDevices instanceof RPairList) { + ((RPairList) dotDevices).appendToEnd(RDataFactory.createPairList(name)); } else { baseEnv.safePut(DOT_DEVICES, RDataFactory.createPairList(name)); } } + + public static String getDefaultDevice() { + ROptions.ContextStateImpl options = RContext.getInstance().stateROptions; + String defaultDev = RRuntime.asString(options.getValue(DEFAULT_DEVICE_OPTION)); + if (RRuntime.isNA(defaultDev)) { + throw RError.error(RError.NO_CALLER, Message.GENERIC, "FastR does only supports character value as the default 'device' option"); + } + return defaultDev; + } + + public static int getDevicesCount() { + Object dotDevices = REnvironment.baseEnv().get(DOT_DEVICES); + return dotDevices instanceof RPairList ? ((RPairList) dotDevices).getLength() : 0; + } + + public static String getDeviceName(int index) { + RPairList dotDevices = (RPairList) REnvironment.baseEnv().get(DOT_DEVICES); + return RRuntime.asString(dotDevices.getDataAtAsObject(index)); + } + + private static boolean contains(RPairList devices, String name) { + for (RPairList dev : devices) { + if (dev.car().equals(name)) { + return true; + } + } + return false; + } }