package gui.views.plots;
import gui.bsvComponents.BSVSpinner;
import gui.main.EventController;
import gui.settings.Settings;
import gui.views.ViewPanel;
import gui.views.ViewUtils;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.geom.Rectangle2D;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.IntBuffer;
import java.util.Observable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.imageio.ImageIO;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLDrawableFactory;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.GLPbuffer;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLJPanel;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileFilter;
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import com.jogamp.opengl.util.awt.Screenshot;
import com.jogamp.opengl.util.awt.TextRenderer;
import controller.DataHub;
import controller.Feature;
import controller.SelectionController;
import controller.SubspaceController;
import db.DatabaseAccessException;
/**
* This class implements an abstract OpenGL based view. It prepares the render context and can be used for hardware
* accelerated views and plots. Because this class uses an OpenGL interface, some management work must be done manually
* e.g. some memory management.
*/
public abstract class GLPlot extends ViewPanel implements GLEventListener {
private static final long serialVersionUID = 647149035023106625L;
/**
* How many milliseconds should we count frames to calculate the fps.
*/
private static final int FPS_PART_LENGTH = 1000;
/**
* Should we display the FPS.
*/
private static final boolean SHOW_FPS = false;
/**
* Controls number of pixels between ticks of axis.
*/
private static final int PIXELS_PER_TICK = 40;
/**
* Controls minimum size of saved image.
*/
private static final int SCREENSHOT_SIZE_MIN = 100;
/**
* Controls max size of saved image.
*/
private static final int SCREENSHOT_SIZE_MAX = 5000;
/**
* Action that shows export panel.
*/
private final AbstractAction showExportAction;
/**
* Action that exports.
*/
private final AbstractAction doExportAction;
/**
* Action that cancels the export.
*/
private final AbstractAction closeExportAction;
/**
* Renderer that can be used for rendering text.
*/
protected TextRenderer trenderer;
/**
* Render target used as overlay.
*/
private final GLJPanel glJPanel;
/**
* Sidebar for tools and settings.
*/
private final JPanel sidebar;
/**
* Panel for export.
*/
private final JPanel exportPanel;
/**
* Panel for export buttons.
*/
private final JPanel exportButtonPanel;
/**
* Panel for export size.
*/
private final JPanel exportSizePanel;
/**
* Spinner for export image width.
*/
private final JSpinner exportWidthSpinner;
/**
* Spinner for export image height.
*/
private final JSpinner exportHeightSpinner;
/**
* Font used for rendering text.
*/
private final Font font;
/**
* Stores time of the beginning of the part.
*/
private long fpsPartBegin = 0;
/**
* Counter for rendered frames in this part.
*/
private long fpsPartFrames = 0;
/**
* Stores current fps.
*/
private int fps = 0;
/**
* X position of render shape.
*/
private int shapeX;
/**
* Y position of render shape.
*/
private int shapeY;
/**
* Width of render shape.
*/
private int shapeWidth;
/**
* Height of render shape.
*/
private int shapeHeight;
/**
* User interaction handler.
*/
private InteractionHandler ihandler;
/**
* Flag, indicating the need to update shown data.
*/
private boolean newData = true;
/**
* Flag, indicating if this view is valid. Used for exception handling.
*/
private boolean valid = true;
/**
* Processing task.
*/
private Future<Boolean> processingTask;
/**
* Multithreaded executer for processor.
*/
private final ExecutorService processorExecutor;
/**
* Stores process task status.
*/
private boolean taskReady;
/**
* Multithreaded executor for renderer.
*/
private final ExecutorService renderExecutor = Executors.newSingleThreadScheduledExecutor();
/**
* Flag, indicating if a screenshot should be taken when display method will be called.
*/
private boolean takeScreenshot = false;
/**
* Width of the screenshot.
*/
private int screenshotWidth;
/**
* Height of the screenshot.
*/
private int screenshotHeight;
/**
* File where screenshot should be stored.
*/
private File screenshotFile;
/**
* Stores vertex buffer object handlers.
*/
protected int[] vbo;
/**
* Create Swing UI.
*/
protected abstract void createUI();
/**
* Register all shortcuts for this view.
*/
protected abstract void registerShortcuts();
/**
* Unregister all shortcuts for this view.
*/
protected abstract void unregisterShortcuts();
/**
* Sets features in active subspace.
*
* @param features
* active features.
*/
protected abstract void setFeatures(Feature[] features);
/**
* Will be called when the rendering context is initialized.
*
* Should load data, allocate memory and set context options.
*
* @param gl
* the rendering context.
*/
protected abstract void init(GL2 gl);
/**
* Will be called whenever the context will be reshaped.
*
* @param gl
* the rendering context.
*/
protected abstract void rescale(GL2 gl);
/**
* Will be called whenever a frame should be render.
*
* Should draw all content to the frame. No initialization, cleanup, flushing or buffer swapping is necessary.
*
* @param gl
* the rendering context.
*/
protected abstract void draw(GL2 gl);
/**
* Will be called while the render context is destroyed.
*
* Should do cleanups e.g. freeing memory.
*
* @param gl
* the rendering context.
*/
protected abstract void dispose(GL2 gl);
/**
* Process all data for rendering.
*
* @throws DatabaseAccessException
* if there is an error when getting data.
* @throws InterruptedException
* if process is interrupted.
*/
protected abstract void processData() throws DatabaseAccessException, InterruptedException;
/**
* Transfer data to graphics card memory.
*
* @param gl
* render context.
*/
protected abstract void uploadData(GL2 gl);
/**
* Draw plot using java Graphics2D (used for export).
*
* @param g2d
* render context.
*/
protected abstract void draw(Graphics2D g2d);
/**
* Constructs an GLPlot using a {@link DataHub} and a {@link SelectionController}.
*
* @param dataHub
* the preinitialized DataHub.
* @param selectionController
* the preinitialized SelectionController.
* @param subspaceController
* the preinitialized SubspaceController.
*/
public GLPlot(DataHub dataHub, SelectionController selectionController, SubspaceController subspaceController) {
super(dataHub, selectionController, subspaceController);
// init actions
this.showExportAction = new ShowExportAction();
this.doExportAction = new DoExportAction();
this.closeExportAction = new CloseExportAction();
// layout
super.setLayout(new BorderLayout());
// initialize multithreaded environment
this.processorExecutor = Executors.newSingleThreadExecutor();
// initialize GL context
this.glJPanel = new GLJPanel(getGLCaps());
this.glJPanel.addGLEventListener(this);
// load some data
this.font = new Font("Arial", Font.BOLD, 10);
this.trenderer = new TextRenderer(this.font);
// add canvas to self
this.add(this.glJPanel, BorderLayout.CENTER);
// create sidebar
this.sidebar = new JPanel();
this.sidebar.setBackground(Color.WHITE);
this.sidebar.setLayout(new BoxLayout(this.sidebar, BoxLayout.Y_AXIS));
this.add(this.sidebar, BorderLayout.LINE_END);
// create screenshot button
this.exportPanel = new JPanel();
this.exportPanel.setLayout(new BoxLayout(this.exportPanel, BoxLayout.Y_AXIS));
this.exportButtonPanel = new JPanel(new GridLayout(0, 1));
this.exportButtonPanel.setBackground(Color.WHITE);
this.exportPanel.setBorder(BorderFactory.createTitledBorder(Settings.getInstance().getResourceBundle()
.getString("glPlotPanelExport")));
JButton screenshotButton = new JButton(this.showExportAction);
screenshotButton.setText(Settings.getInstance().getResourceBundle().getString("glPlotSave"));
this.exportButtonPanel.add(screenshotButton);
this.exportPanel.add(this.exportButtonPanel);
this.exportSizePanel = new JPanel();
this.exportSizePanel.setLayout(new BoxLayout(this.exportSizePanel, BoxLayout.Y_AXIS));
this.exportSizePanel.setBackground(Color.WHITE);
this.exportWidthSpinner = new BSVSpinner(new SpinnerNumberModel(SCREENSHOT_SIZE_MIN, SCREENSHOT_SIZE_MIN,
SCREENSHOT_SIZE_MAX, 1));
this.exportHeightSpinner = new BSVSpinner(new SpinnerNumberModel(SCREENSHOT_SIZE_MIN, SCREENSHOT_SIZE_MIN,
SCREENSHOT_SIZE_MAX, 1));
this.exportWidthSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
JSpinner source = (JSpinner) e.getSource();
SpinnerNumberModel model = (SpinnerNumberModel) source.getModel();
screenshotWidth = (Integer) model.getNumber();
}
});
this.exportHeightSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
JSpinner source = (JSpinner) e.getSource();
SpinnerNumberModel model = (SpinnerNumberModel) source.getModel();
screenshotHeight = (Integer) model.getNumber();
}
});
JLabel widthLabel = new JLabel(Settings.getInstance().getResourceBundle().getString("glPlotSizeDialogWidth"));
JLabel heightLabel = new JLabel(Settings.getInstance().getResourceBundle().getString("glPlotSizeDialogHeight"));
JPanel widthPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
widthPanel.setBackground(Color.WHITE);
widthPanel.add(widthLabel);
widthPanel.add(this.exportWidthSpinner);
JPanel heightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
heightPanel.setBackground(Color.WHITE);
heightPanel.add(heightLabel);
heightPanel.add(this.exportHeightSpinner);
this.exportSizePanel.add(widthPanel);
this.exportSizePanel.add(heightPanel);
JButton okButton = new JButton(this.doExportAction);
okButton.setText(Settings.getInstance().getResourceBundle().getString("glPlotSizeDialogOk"));
JButton cancelButton = new JButton(this.closeExportAction);
cancelButton.setText(Settings.getInstance().getResourceBundle().getString("glPlotSizeDialogCancel"));
JPanel okButtonPanel = new JPanel(new GridLayout(0, 1));
JPanel cancelButtonPanel = new JPanel(new GridLayout(0, 1));
okButtonPanel.setBackground(Color.WHITE);
cancelButtonPanel.setBackground(Color.WHITE);
okButtonPanel.add(okButton);
cancelButtonPanel.add(cancelButton);
this.exportSizePanel.add(okButtonPanel);
this.exportSizePanel.add(cancelButtonPanel);
this.exportSizePanel.setVisible(false);
this.exportPanel.add(this.exportSizePanel);
this.addToSidebar(exportPanel);
// initialize user interface
this.createUI();
// first initialization
this.update(this.subspaceController, null);
this.rerender();
}
/**
* Dump image using {@code screenshotFile}, {@code screenshotWidth} and {@code screenshotHeight}.
*/
private void dump() {
this.takeScreenshot = true;
GLDrawableFactory factory = GLDrawableFactory.getFactory(getGLProfile());
GLCapabilities caps = getGLCaps();
caps.setDoubleBuffered(false);
GLPbuffer pBuffer = factory.createGLPbuffer(null, caps, null, screenshotWidth, screenshotHeight, null);
pBuffer.addGLEventListener(this);
pBuffer.display();
takeScreenshot = false;
}
/**
* Generate svg using {@code screenshotFile}, {@code screenshotWidth} and {@code screenshotHeight}.
*/
private void genSVG() throws FileNotFoundException, IOException {
// get DOM implementation
DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();
// create svg document
String svgNG = "http://www.w3.org/2000/svg";
Document document = domImpl.createDocument(svgNG, "svg", null);
// create 2d graphics
SVGGraphics2D svgGenerator = new SVGGraphics2D(document);
// set size
int oldWidth = this.shapeWidth;
int oldHeight = this.shapeHeight;
this.shapeWidth = this.screenshotWidth;
this.shapeHeight = this.screenshotHeight;
svgGenerator.setSVGCanvasSize(new Dimension(this.shapeWidth, this.shapeHeight));
// flip y axis
svgGenerator.translate(0, this.shapeHeight);
svgGenerator.scale(1, -1);
// set some defaults
svgGenerator.setFont(this.font);
// draw
this.draw(svgGenerator);
// restore size
this.shapeWidth = oldWidth;
this.shapeHeight = oldHeight;
// store result
boolean useCSS = true;
Writer out = new OutputStreamWriter(new FileOutputStream(this.screenshotFile), "UTF8");
svgGenerator.stream(out, useCSS);
}
/**
* Rerender OpenGL surface.
*/
protected final synchronized void rerender() {
this.renderExecutor.submit(new Runnable() {
@Override
public void run() {
glJPanel.display();
}
});
}
/**
* Sets interaction controller for this plot.
*
* @param ihandler
* the new interaction controller, can be {@code null}.
*/
protected final void setInteractionHandler(InteractionHandler ihandler) {
if (this.ihandler != null) {
this.glJPanel.removeMouseListener(this.ihandler);
this.glJPanel.removeMouseMotionListener(this.ihandler);
this.glJPanel.removeMouseWheelListener(this.ihandler);
}
this.ihandler = ihandler;
if (this.ihandler != null) {
this.glJPanel.addMouseListener(this.ihandler);
this.glJPanel.addMouseMotionListener(this.ihandler);
this.glJPanel.addMouseWheelListener(this.ihandler);
}
}
/**
* Invalidates this plot. Can be used to signal exceptions to user.
*/
protected final void setInvalid() {
this.valid = false;
if (this.ihandler != null) {
this.ihandler.pause();
}
}
/**
* Adds component to sidebar, used for tools and so forth.
*
* @param component
* component that should be added.
*/
protected final void addToSidebar(JComponent component) {
component.setMaximumSize(new Dimension((int) component.getMaximumSize().getWidth(), (int) component
.getPreferredSize().getHeight()));
component.setAlignmentX(Component.RIGHT_ALIGNMENT);
component.setBackground(Color.WHITE);
this.sidebar.add(component);
}
/**
* Get x position of render shape.
*
* @return x position of render shape.
*/
protected final int getShapeX() {
return this.shapeX;
}
/**
* Get y position of render shape.
*
* @return y position of render shape.
*/
protected final int getShapeY() {
return this.shapeY;
}
/**
* Get width of render shape.
*
* @return width of render shape.
*/
protected final int getShapeWidth() {
return this.shapeWidth;
}
/**
* Get height of render shape.
*
* @return height of render shape.
*/
protected final int getShapeHeight() {
return this.shapeHeight;
}
/**
* Draws message to user on the plot.
*
* @param gl
* render context.
* @param msg
* message text.
*/
protected final void drawMessage(GL2 gl, String msg) {
this.trenderer.beginRendering(this.getShapeWidth(), this.getShapeHeight());
this.trenderer.setColor(0.f, 0.f, 0.f, 1.f);
Rectangle2D msgRec = this.trenderer.getBounds(msg);
this.trenderer.draw(msg, (int) Math.round((this.getShapeWidth() - msgRec.getWidth()) / 2), (int) Math
.round((this.getShapeHeight() - msgRec.getHeight()) / 2));
this.trenderer.endRendering();
}
/**
* Draw an axis on the current location.
*
* @param gl
* The preinitialized and prepared rendering context. The color of the context may be changed by this
* method.
* @param x
* X position of axis root.
* @param y
* Y position of axis root.
* @param length
* The length of the axis in pixel.
* @param name
* The name of the axis.
* @param minVar
* The minimum value that is drawn on the axis.
* @param maxVar
* The maximum value that is drawn on the axis. It must be greater than the minimum value.
* @param horizontal
* If {@code false} the axis is vertical, otherwise, the axis is horizontal.
*/
protected final void drawAxis(GL2 gl, float x, float y, float length, String name, float minVar, float maxVar,
boolean horizontal) {
if (minVar > maxVar) {
throw new IllegalArgumentException("maxVar must be greater than minVar!");
}
// calc markers/labels
float[] markersPos = ViewUtils.calcAxisMarkers(minVar, maxVar, length, PIXELS_PER_TICK);
gl.glPushMatrix();
gl.glTranslatef(x, y, 0.f);
if (horizontal) {
gl.glRotatef(-90.f, 0.f, 0.f, 1.f);
}
// config
gl.glColor3f(0.f, 0.f, 0.f);
gl.glLineWidth(1.f);
this.trenderer.setColor(0.f, 0.f, 0.f, 1.f);
// draw line
gl.glBegin(GL2.GL_LINES);
gl.glVertex2f(0.f, 0.f);
gl.glVertex2f(0.f, length + 5.f);
gl.glEnd();
// draw arrow
gl.glBegin(GL2.GL_TRIANGLES);
gl.glVertex2f(0.f, length + 15.f);
gl.glVertex2f(-10.f, length + 5.f);
gl.glVertex2f(10.f, length + 5.f);
gl.glEnd();
// draw markers
for (int i = 1; i < markersPos.length; i++) {
float dY = (markersPos[i] - minVar) / (maxVar - minVar) * length;
gl.glBegin(GL2.GL_LINES);
gl.glVertex2f(0.f, dY);
if (horizontal) {
gl.glVertex2f(10.f, dY);
} else {
gl.glVertex2f(-10.f, dY);
}
gl.glEnd();
}
// draw labels
gl.glPushMatrix();
for (int i = 1; i < markersPos.length; i++) {
String label = String.format("%." + Math.round(markersPos[0]) + "f", markersPos[i]);
float dY = (markersPos[i] - minVar) / (maxVar - minVar) * length;
Rectangle2D labelRect = this.trenderer.getBounds(label);
int labelLength = (int) Math.ceil(labelRect.getWidth());
int labelHeight = (int) Math.ceil(labelRect.getHeight());
dY -= labelHeight / 2.f;
int dX = 0;
if (horizontal) {
dX = 15;
} else {
dX = -15 - labelLength;
}
gl.glPushMatrix();
gl.glTranslatef(dX, dY, 0.f);
gl.glBegin(GL2.GL_POLYGON);
gl.glColor4f(1.f, 1.f, 1.f, 0.7f);
gl.glVertex2f(-2.f, -2.f);
gl.glVertex2f(-2.f, labelHeight + 2.f);
gl.glVertex2f(labelLength + 2.f, labelHeight + 2.f);
gl.glVertex2f(labelLength + 2.f, -2.f);
gl.glEnd();
gl.glPopMatrix();
gl.glPushMatrix();
this.trenderer.beginRendering(this.getShapeWidth(), this.getShapeHeight());
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glTranslatef(x, y, 0.f);
if (horizontal) {
gl.glRotatef(-90.f, 0.f, 0.f, 1.f);
}
this.trenderer.draw(label, dX, (int) dY);
this.trenderer.endRendering();
gl.glPopMatrix();
}
gl.glPopMatrix();
// draw name background
float nameLength = (float) this.trenderer.getBounds(name).getWidth();
float nameHeight = (float) this.trenderer.getBounds(name).getHeight();
gl.glPushMatrix();
gl.glRotatef(90.f, 0.f, 0.f, 1.f);
gl.glTranslatef((int) Math.floor(length * 0.5f - nameLength * 0.5f), 0.f, 0.f);
if (horizontal) {
gl.glTranslatef(0.f, -12 - 10, 0.f);
} else {
gl.glTranslatef(0.f, 10, 0.f);
}
gl.glBegin(GL2.GL_POLYGON);
gl.glColor4f(1.f, 1.f, 1.f, 0.7f);
gl.glVertex2f(-2.f, -2.f);
gl.glVertex2f(-2.f, nameHeight + 2.f);
gl.glVertex2f(nameLength + 2.f, nameHeight + 2.f);
gl.glVertex2f(nameLength + 2.f, -2.f);
gl.glEnd();
gl.glPopMatrix();
// draw name
gl.glPushMatrix();
this.trenderer.beginRendering(this.getShapeWidth(), this.getShapeHeight());
gl.glMatrixMode(GL2.GL_MODELVIEW);
if (horizontal) {
gl.glTranslatef(x, y - 12 - 10, 0.f);
} else {
gl.glTranslatef(x - 10, y, 0.f);
gl.glRotatef(90.f, 0.f, 0.f, 1.f);
}
this.trenderer.draw(name, (int) Math.floor(length * 0.5f - nameLength * 0.5f), 0);
this.trenderer.endRendering();
gl.glPopMatrix();
gl.glPopMatrix();
}
/**
* Draw an axis on the current location.
*
* @param g2d
* The preinitialized and prepared rendering context. The color of the context may be changed by this
* method.
* @param x
* X postion of axis root.
* @param y
* Y postion of axis root.
* @param length
* The length of the axis in pixel.
* @param name
* The name of the axis.
* @param minVar
* The minimum value that is drawn on the axis.
* @param maxVar
* The maximum value that is drawn on the axis. It must be greater than the minimum value.
* @param horizontal
* If {@code false} the axis is vertical, otherwise, the axis is horizontal.
*/
protected final void drawAxis(Graphics2D g2d, float x, float y, float length, String name, float minVar,
float maxVar, boolean horizontal) {
if (minVar > maxVar) {
throw new IllegalArgumentException("maxVar must be greater than minVar!");
}
// calc markers/labels
float[] markersPos = ViewUtils.calcAxisMarkers(minVar, maxVar, length, PIXELS_PER_TICK);
FontMetrics fm = g2d.getFontMetrics();
// init position
g2d.translate(x, y);
if (horizontal) {
g2d.rotate(-Math.PI / 2.f);
}
// config
g2d.setColor(Color.BLACK);
g2d.setStroke(new BasicStroke(1.f));
// draw line
g2d.drawLine(0, 0, 0, Math.round(length + 5.f));
// draw arrow
g2d.fillPolygon(new int[] { 0, -10, 10 }, new int[] { Math.round(length + 15.f), Math.round(length + 5),
Math.round(length + 5) }, 3);
// draw markers and labels
for (int i = 1; i < markersPos.length; i++) {
String label = String.format("%." + Math.round(markersPos[0]) + "f", markersPos[i]);
Rectangle2D labelRect = fm.getStringBounds(label, g2d);
int labelWidth = (int) Math.round(labelRect.getWidth());
int labelHeight = (int) Math.round(labelRect.getHeight());
int dY = Math.round((markersPos[i] - minVar) / (maxVar - minVar) * length);
if (horizontal) {
g2d.drawLine(0, dY, 10, dY);
} else {
g2d.drawLine(0, dY, -10, dY);
}
int dX;
if (horizontal) {
dX = 15;
} else {
dX = -15 - labelWidth;
}
g2d.scale(1, -1);
g2d.setColor(new Color(1.f, 1.f, 1.f, 0.7f));
g2d.fillRect((int) Math.round(labelRect.getX() - 2) + dX, (int) Math.round(labelRect.getY() + 4) - dY,
(int) Math.round(labelRect.getWidth() + 4), (int) Math.round(labelRect.getHeight() + 4));
g2d.setColor(Color.BLACK);
g2d.drawString(label, dX, -dY + Math.round(labelHeight / 2.f));
g2d.scale(1, -1);
}
// draw name
if (horizontal) {
g2d.translate(10, 0);
} else {
g2d.translate(-10, 0);
}
g2d.rotate(Math.PI / 2.f);
g2d.scale(1, -1);
Rectangle2D nameRect = fm.getStringBounds(name, g2d);
int dX = (int) Math.round(length * 0.5f - nameRect.getWidth() * 0.5f);
int dY = 0;
if (horizontal) {
dY = (int) Math.round(nameRect.getHeight());
}
g2d.setColor(new Color(1.f, 1.f, 1.f, 0.7f));
g2d.fillRect((int) Math.round(nameRect.getX() - 2) + dX, (int) Math.round(nameRect.getY() - 2) + dY, (int) Math
.round(nameRect.getWidth() + 4), (int) Math.round(nameRect.getHeight() + 4));
g2d.setColor(Color.BLACK);
g2d.drawString(name, dX, dY);
g2d.scale(1, -1);
g2d.rotate(-Math.PI / 2.f);
if (horizontal) {
g2d.translate(-10, 0);
} else {
g2d.translate(10, 0);
}
// reset position
g2d.translate(-x, -y);
if (horizontal) {
g2d.rotate(Math.PI / 2.f);
}
}
/**
* Loads and compiles a shader.
*
* @param gl
* render context.
* @param type
* shader type, e.g. {@code GL2.GL_FRAGMENT_SHADER}.
* @param resouce
* resource path to shader file.
* @return shader id, {@code 0} on error.
*/
protected final int setupShader(GL2 gl, int type, String resouce) {
int id = gl.glCreateShader(type);
try {
String[] source = new String[1];
InputStream is = this.getClass().getResourceAsStream(resouce);
// could not load the shader, abort immediately
if (is == null) {
return 0;
}
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
bufferedReader.close();
source[0] = stringBuilder.toString();
gl.glShaderSource(id, 1, source, (int[]) null, 0);
gl.glCompileShader(id);
IntBuffer intBuffer = IntBuffer.allocate(1);
gl.glGetShaderiv(id, GL2.GL_COMPILE_STATUS, intBuffer);
if (intBuffer.get(0) != 1) {
// debug code, activate it, if you wan't to test shader code
/*
* gl.glGetShaderiv(id, GL2.GL_INFO_LOG_LENGTH, intBuffer); int size = intBuffer.get(0);
* System.err.println("Shader compile error: "); if (size > 0) { ByteBuffer byteBuffer =
* ByteBuffer.allocate(size); gl.glGetShaderInfoLog(id, size, intBuffer, byteBuffer); for (byte b :
* byteBuffer.array()) { System.err.print((char) b); } } else { System.out.println("Unknown"); }
*/
id = 0;
}
} catch (IOException e) {
id = 0;
}
return id;
}
@Override
public final void setVisible(boolean visible) {
if (visible != this.isVisible()) {
if (visible) {
super.setVisible(visible);
EventController.getInstance().registerKeyTarget(this);
EventController.getInstance().setAction(this.showExportAction, "eventExportGraphics");
this.registerShortcuts();
this.rerender();
} else {
this.unregisterShortcuts();
EventController.getInstance().removeAction("eventExportGraphics");
super.setVisible(visible);
}
}
}
@Override
public final void init(GLAutoDrawable drawable) {
GL2 gl = drawable.getGL().getGL2();
// optimize quality
// gl.glShadeModel(GL2.GL_SMOOTH); // more performance
// optimize speed
gl.glDisable(GL2.GL_DEPTH_TEST);
gl.glDisable(GL2.GL_DITHER);
gl.setSwapInterval(1);
// enable alpha
gl.glEnable(GL2.GL_BLEND);
gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL2.GL_ONE_MINUS_SRC_ALPHA);
// set background color
gl.glClearColor(1, 1, 1, 1);
// call implementation
this.init(gl);
}
@Override
public final void dispose(GLAutoDrawable drawable) {
GL2 gl = drawable.getGL().getGL2();
// call implementation
this.dispose(gl);
}
@Override
public final synchronized void display(GLAutoDrawable drawable) {
// some inits
GL2 gl = drawable.getGL().getGL2();
gl.glClear(GL2.GL_COLOR_BUFFER_BIT);
// reinit text renderer because of context binding
this.trenderer = new TextRenderer(this.font);
// save context and data
int oldHeight = this.shapeHeight;
int oldWidth = this.shapeWidth;
int[] oldVBO = this.vbo;
if (this.takeScreenshot) {
this.reshape(drawable, this.shapeX, this.shapeY, this.screenshotWidth, this.screenshotHeight);
}
// update framerate
if (SHOW_FPS) {
this.updateFPS();
}
if (this.valid && taskReady) {
this.processingTask = null;
this.uploadData(gl);
}
if (this.valid && this.newData && (this.processingTask == null)) {
taskReady = false;
this.processingTask = this.processorExecutor.submit(new Processor());
}
if (this.valid && (this.processingTask == null)) {
// store context
gl.glPushMatrix();
// fix coord system
gl.glTranslatef(0.f, this.shapeHeight, 0.f);
gl.glScalef(1.f, -1.f, 1.f);
// call implementation
this.draw(gl);
// restore context
gl.glPopMatrix();
gl.glColor4f(0.f, 0.f, 0.f, 0.5f);
}
if (this.valid && (this.processingTask != null)) {
this.drawMessage(gl, Settings.getInstance().getResourceBundle().getString("glPlotProcessing"));
}
if (!this.valid) {
this.drawMessage(gl, Settings.getInstance().getResourceBundle().getString("glPlotInvalid"));
}
// display framerate
if (SHOW_FPS) {
this.trenderer.beginRendering(this.shapeWidth, this.shapeHeight);
this.trenderer.setColor(0.f, 0.f, 0.f, 1.f);
this.trenderer.draw(this.fps + " FPS", 0, 0);
this.trenderer.endRendering();
}
// flush
gl.glFlush();
// take screenshot
if (this.takeScreenshot) {
try {
Screenshot.writeToFile(this.screenshotFile, this.screenshotWidth, this.screenshotHeight);
} catch (IOException ex) {
ex.printStackTrace();
} catch (GLException ex) {
ex.printStackTrace();
}
// restore context and data
this.reshape(drawable, this.shapeX, this.shapeY, oldWidth, oldHeight);
this.vbo = oldVBO;
}
}
@Override
public final void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
this.shapeX = x;
this.shapeY = y;
this.shapeWidth = width;
this.shapeHeight = height;
GL2 gl = drawable.getGL().getGL2();
// set 2d mode
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrtho(0, width, height, 0, 0, 1);
gl.glMatrixMode(GL2.GL_MODELVIEW);
// call implementation
this.rescale(gl);
}
@Override
public final void update(Observable o, Object arg) {
if (o == this.subspaceController) {
try {
this.setFeatures(this.subspaceController.getActiveSubspace().getFeatures());
} catch (DatabaseAccessException ex) {
this.setInvalid();
}
}
this.newData = true;
this.processingTask = null;
if (this.isVisible()) {
this.rerender();
}
}
/**
* Get a prepared and selected GLProfile.
*
* @return ideal GLProfile.
*/
private static GLProfile getGLProfile() {
return GLProfile.getDefault();
}
/**
* Prepares the GLCapabilities for a optimized accelerated rendering context.
*
* @return the detected and optimized capabilities.
*/
private static GLCapabilities getGLCaps() {
GLProfile glprofile = getGLProfile();
GLCapabilities glcaps = new GLCapabilities(glprofile);
glcaps.setDoubleBuffered(true);
glcaps.setHardwareAccelerated(true);
return glcaps;
}
/**
* Updates the FPS counter.
*
* Should be called at be beginning of the rendering process of every frame.
*/
private void updateFPS() {
long now = System.currentTimeMillis();
if (now >= this.fpsPartBegin + FPS_PART_LENGTH) {
this.fps = (int) ((this.fpsPartFrames * 1000) / (now - this.fpsPartBegin));
this.fpsPartFrames = 1;
this.fpsPartBegin = now;
} else {
this.fpsPartFrames++;
}
}
/**
* Show export dialog.
*/
private class ShowExportAction extends AbstractAction {
private static final long serialVersionUID = -5789657423404702178L;
@Override
public void actionPerformed(ActionEvent e) {
screenshotWidth = shapeWidth;
screenshotHeight = shapeHeight;
exportWidthSpinner.setValue(Math.max(SCREENSHOT_SIZE_MIN, Math.min(screenshotWidth, SCREENSHOT_SIZE_MAX)));
exportHeightSpinner
.setValue(Math.max(SCREENSHOT_SIZE_MIN, Math.min(screenshotHeight, SCREENSHOT_SIZE_MAX)));
exportButtonPanel.setVisible(false);
exportSizePanel.setVisible(true);
exportPanel.setMaximumSize(new Dimension((int) exportPanel.getMaximumSize().getWidth(), (int) exportPanel
.getPreferredSize().getHeight()));
}
}
/**
* Do export.
*/
private class DoExportAction extends AbstractAction {
private static final long serialVersionUID = -4352592799039959131L;
@Override
public void actionPerformed(ActionEvent e) {
JFileChooser fc = new JFileChooser(System.getProperty("user.dir"));
fc.setDialogTitle(Settings.getInstance().getResourceBundle().getString("glPlotImgDialogTitle"));
FileFilter ffi = new ImageFileFilter();
FileFilter ffs = new SVGFileFilter();
fc.addChoosableFileFilter(ffi);
fc.addChoosableFileFilter(ffs);
int ret = fc.showSaveDialog(EventController.getInstance().getRootFrame());
if (ret == JFileChooser.APPROVE_OPTION) {
File f = fc.getSelectedFile();
boolean exportSVG = false;
boolean exportRaster = false;
if (ffs.accept(f)) {
exportSVG = true;
} else if (ffi.accept(f)) {
exportRaster = true;
} else {
// fallback to default file type
if (fc.getFileFilter().equals(ffs)) {
f = new File(f.getPath() + ".svg");
exportSVG = true;
} else {
f = new File(f.getPath() + ".png");
exportRaster = true;
}
}
screenshotFile = f;
if (exportSVG) {
try {
genSVG();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
if (exportRaster) {
dump();
}
}
closeExportAction.actionPerformed(e);
}
}
/**
* Cancel export.
*/
private class CloseExportAction extends AbstractAction {
private static final long serialVersionUID = -1077216507135650491L;
@Override
public void actionPerformed(ActionEvent e) {
exportSizePanel.setVisible(false);
exportButtonPanel.setVisible(true);
exportPanel.setMaximumSize(new Dimension((int) exportPanel.getMaximumSize().getWidth(), (int) exportPanel
.getPreferredSize().getHeight()));
}
}
/**
* Filters ImageIO files.
*/
private class ImageFileFilter extends FileFilter {
@Override
public boolean accept(File f) {
if (f.isDirectory()) {
return true;
}
String ext = f.getName().substring(f.getName().lastIndexOf(".") + 1);
return ImageIO.getImageWritersBySuffix(ext).hasNext();
}
@Override
public String getDescription() {
String[] tmp = ImageIO.getWriterFileSuffixes();
StringBuilder result = new StringBuilder();
boolean first = true;
for (int i = 0; i < tmp.length; i++) {
if (first) {
first = false;
} else {
result.append(", ");
}
result.append(tmp[i]);
}
return result.toString();
}
}
/**
* Filters SVG files.
*/
private class SVGFileFilter extends FileFilter {
@Override
public boolean accept(File f) {
return f.isDirectory() || f.getName().endsWith(".svg");
}
@Override
public String getDescription() {
return "svg";
}
}
/**
* Processes data for rendering.
*/
private class Processor implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
Boolean result = Boolean.TRUE;
try {
if (ihandler != null) {
ihandler.pause();
}
try {
processData();
newData = false;
if (ihandler != null) {
ihandler.resume();
}
} catch (DatabaseAccessException ex) {
ex.printStackTrace();
result = Boolean.FALSE;
} catch (InterruptedException ex) {
result = Boolean.TRUE;
}
} catch (Exception e) {
e.printStackTrace();
result = Boolean.FALSE;
}
valid = result;
taskReady = true;
rerender();
return result;
}
}
}