Plotting in Matlab is very easy and straightforward. For example:
figure('Position_',[100,80,1000,600])
plot(x,y1,'-.or','MarkerSize',0.2,'MarkerFaceColor','r','LineWidth',2)
xlabel('Matrix1')
ylabel('Matrix2')
grid on
hold on
axis([-1,1,0,var1*1.2])
plot(x,y2,'-k','MarkerSize',0.5,'MarkerFaceColor','k','LineWidth',4)
title('My plot')
figuresdir = 'dir';
saveas(gcf,strcat(figuresdir, 'plotimage'), 'bmp');
I found, however, that plotting in Java is more difficult and I have to use packages like JMathPlot or JFreeChart. However, I find it difficult to merge plots and print them to a file using these packages.
Is there an easy way to make plots in Java that uses (about) the same syntax as Matlab does?
Well, Matlab is designed specifically to make things such as plotting as easy as possible. Other languages simply don't have the same kind of support for quick-and-easy plots.
Therefore I decided to write a little Matlab-style charting class based on JFreeChart, just for you:
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Stroke;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYTitleAnnotation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RectangleEdge;
public class MatlabChart {
Font font;
JFreeChart chart;
LegendTitle legend;
ArrayList<Color> colors;
ArrayList<Stroke> strokes;
XYSeriesCollection dataset;
public MatlabChart() {
font = JFreeChart.DEFAULT_TITLE_FONT;
colors = new ArrayList<Color>();
strokes = new ArrayList<Stroke>();
dataset = new XYSeriesCollection();
}
public void plot(double[] x, double[] y, String spec, float lineWidth, String title) {
final XYSeries series = new XYSeries(title);
for (int i = 0; i < x.length; i++)
series.add(x[i],y[i]);
dataset.addSeries(series);
FindColor(spec,lineWidth);
}
public void RenderPlot() {
// Create chart
JFreeChart chart = null;
if (dataset != null && dataset.getSeriesCount() > 0)
chart = ChartFactory.createXYLineChart(null,null,null,dataset,PlotOrientation.VERTICAL,true, false, false);
else
System.out.println(" [!] First create a chart and add data to it. The plot is empty now!");
// Add customization options to chart
XYPlot plot = chart.getXYPlot();
for (int i = 0; i < colors.size(); i++) {
plot.getRenderer().setSeriesPaint(i, colors.get(i));
plot.getRenderer().setSeriesStroke(i, strokes.get(i));
}
((NumberAxis)plot.getDomainAxis()).setAutoRangeIncludesZero(false);
((NumberAxis)plot.getRangeAxis()).setAutoRangeIncludesZero(false);
plot.setBackgroundPaint(Color.WHITE);
legend = chart.getLegend();
chart.removeLegend();
this.chart = chart;
}
public void CheckExists() {
if (chart == null) {
throw new IllegalArgumentException("First plot something in the chart before you modify it.");
}
}
public void font(String name, int fontSize) {
CheckExists();
font = new Font(name, Font.PLAIN, fontSize);
chart.getTitle().setFont(font);
chart.getXYPlot().getDomainAxis().setLabelFont(font);
chart.getXYPlot().getDomainAxis().setTickLabelFont(font);
chart.getXYPlot().getRangeAxis().setLabelFont(font);
chart.getXYPlot().getRangeAxis().setTickLabelFont(font);
legend.setItemFont(font);
}
public void title(String title) {
CheckExists();
chart.setTitle(title);
}
public void xlim(double l, double u) {
CheckExists();
chart.getXYPlot().getDomainAxis().setRange(l, u);
}
public void ylim(double l, double u) {
CheckExists();
chart.getXYPlot().getRangeAxis().setRange(l, u);
}
public void xlabel(String label) {
CheckExists();
chart.getXYPlot().getDomainAxis().setLabel(label);
}
public void ylabel(String label) {
CheckExists();
chart.getXYPlot().getRangeAxis().setLabel(label);
}
public void legend(String position) {
CheckExists();
legend.setItemFont(font);
legend.setBackgroundPaint(Color.WHITE);
legend.setFrame(new BlockBorder(Color.BLACK));
if (position.toLowerCase().equals("northoutside")) {
legend.setPosition(RectangleEdge.TOP);
chart.addLegend(legend);
} else if (position.toLowerCase().equals("eastoutside")) {
legend.setPosition(RectangleEdge.RIGHT);
chart.addLegend(legend);
} else if (position.toLowerCase().equals("southoutside")) {
legend.setPosition(RectangleEdge.BOTTOM);
chart.addLegend(legend);
} else if (position.toLowerCase().equals("westoutside")) {
legend.setPosition(RectangleEdge.LEFT);
chart.addLegend(legend);
} else if (position.toLowerCase().equals("north")) {
legend.setPosition(RectangleEdge.TOP);
XYTitleAnnotation ta = new XYTitleAnnotation(0.50,0.98,legend,RectangleAnchor.TOP);
chart.getXYPlot().addAnnotation(ta);
} else if (position.toLowerCase().equals("northeast")) {
legend.setPosition(RectangleEdge.TOP);
XYTitleAnnotation ta = new XYTitleAnnotation(0.98,0.98,legend,RectangleAnchor.TOP_RIGHT);
chart.getXYPlot().addAnnotation(ta);
} else if (position.toLowerCase().equals("east")) {
legend.setPosition(RectangleEdge.RIGHT);
XYTitleAnnotation ta = new XYTitleAnnotation(0.98,0.50,legend,RectangleAnchor.RIGHT);
chart.getXYPlot().addAnnotation(ta);
} else if (position.toLowerCase().equals("southeast")) {
legend.setPosition(RectangleEdge.BOTTOM);
XYTitleAnnotation ta = new XYTitleAnnotation(0.98,0.02,legend,RectangleAnchor.BOTTOM_RIGHT);
chart.getXYPlot().addAnnotation(ta);
} else if (position.toLowerCase().equals("south")) {
legend.setPosition(RectangleEdge.BOTTOM);
XYTitleAnnotation ta = new XYTitleAnnotation(0.50,0.02,legend,RectangleAnchor.BOTTOM);
chart.getXYPlot().addAnnotation(ta);
} else if (position.toLowerCase().equals("southwest")) {
legend.setPosition(RectangleEdge.BOTTOM);
XYTitleAnnotation ta = new XYTitleAnnotation(0.02,0.02,legend,RectangleAnchor.BOTTOM_LEFT);
chart.getXYPlot().addAnnotation(ta);
} else if (position.toLowerCase().equals("west")) {
legend.setPosition(RectangleEdge.LEFT);
XYTitleAnnotation ta = new XYTitleAnnotation(0.02,0.50,legend,RectangleAnchor.LEFT);
chart.getXYPlot().addAnnotation(ta);
} else if (position.toLowerCase().equals("northwest")) {
legend.setPosition(RectangleEdge.TOP);
XYTitleAnnotation ta = new XYTitleAnnotation(0.02,0.98,legend,RectangleAnchor.TOP_LEFT);
chart.getXYPlot().addAnnotation(ta);
}
}
public void grid(String xAxis, String yAxis) {
CheckExists();
if (xAxis.equalsIgnoreCase("on")){
chart.getXYPlot().setDomainGridlinesVisible(true);
chart.getXYPlot().setDomainMinorGridlinesVisible(true);
chart.getXYPlot().setDomainGridlinePaint(Color.GRAY);
} else {
chart.getXYPlot().setDomainGridlinesVisible(false);
chart.getXYPlot().setDomainMinorGridlinesVisible(false);
}
if (yAxis.equalsIgnoreCase("on")){
chart.getXYPlot().setRangeGridlinesVisible(true);
chart.getXYPlot().setRangeMinorGridlinesVisible(true);
chart.getXYPlot().setRangeGridlinePaint(Color.GRAY);
} else {
chart.getXYPlot().setRangeGridlinesVisible(false);
chart.getXYPlot().setRangeMinorGridlinesVisible(false);
}
}
public void saveas(String fileName, int width, int height) {
CheckExists();
File file = new File(fileName);
try {
ChartUtilities.saveChartAsJPEG(file,this.chart,width,height);
} catch (IOException e) {
e.printStackTrace();
}
}
public void FindColor(String spec, float lineWidth) {
float dash[] = {5.0f};
float dot[] = {lineWidth};
Color color = Color.RED; // Default color is red
Stroke stroke = new BasicStroke(lineWidth); // Default stroke is line
if (spec.contains("-"))
stroke = new BasicStroke(lineWidth);
else if (spec.contains(":"))
stroke = new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
else if (spec.contains("."))
stroke = new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 2.0f, dot, 0.0f);
if (spec.contains("y"))
color = Color.YELLOW;
else if (spec.contains("m"))
color = Color.MAGENTA;
else if (spec.contains("c"))
color = Color.CYAN;
else if (spec.contains("r"))
color = Color.RED;
else if (spec.contains("g"))
color = Color.GREEN;
else if (spec.contains("b"))
color = Color.BLUE;
else if (spec.contains("k"))
color = Color.BLACK;
colors.add(color);
strokes.add(stroke);
}
}
With this, you can plot in Java with syntax very close to Matlab:
public class Demo {
public static void main(String[] args) {
// Create some sample data
double[] x = new double[100]; x[0] = 1;
double[] y1 = new double[100]; y1[0] = 200;
double[] y2 = new double[100]; y2[0] = 300;
for(int i = 1; i < x.length; i++){
x[i] = i+1;
y1[i] = y1[i-1] + Math.random()*10 - 4;
y2[i] = y2[i-1] + Math.random()*10 - 6;
}
// JAVA: // MATLAB:
MatlabChart fig = new MatlabChart(); // figure('Position',[100 100 640 480]);
fig.plot(x, y1, "-r", 2.0f, "AAPL"); // plot(x,y1,'-r','LineWidth',2);
fig.plot(x, y2, ":k", 3.0f, "BAC"); // plot(x,y2,':k','LineWidth',3);
fig.RenderPlot(); // First render plot before modifying
fig.title("Stock 1 vs. Stock 2"); // title('Stock 1 vs. Stock 2');
fig.xlim(10, 100); // xlim([10 100]);
fig.ylim(200, 300); // ylim([200 300]);
fig.xlabel("Days"); // xlabel('Days');
fig.ylabel("Price"); // ylabel('Price');
fig.grid("on","on"); // grid on;
fig.legend("northeast"); // legend('AAPL','BAC','Location','northeast')
fig.font("Helvetica",15); // .. 'FontName','Helvetica','FontSize',15
fig.saveas("MyPlot.jpeg",640,480); // saveas(gcf,'MyPlot','jpeg');
}
}
Now we can compare the final JFreeChart figure to same Matlab figure that we get from this code:
figure('Position',[100 100 640 480]); hold all;
plot(x,y1,'-r','LineWidth',2);
plot(x,y2,':k','LineWidth',3);
title('Stock 1 vs. Stock 2');
xlim([10 100]);
ylim([200 300]);
xlabel('Days');
ylabel('Price');
grid on;
legend('AAPL','BAC','Location','northeast');
saveas(gcf,'MyPlot','jpeg');
Result Java (with the MatlabChart() class):
Result Matlab:
The MatlabChart() class I wrote has support for some of the basic plotting syntax in Matlab. You can indicate line styles (:,-,.), change line colors (y,m,c,r,g,b,w,k), change the LineWidth and change the position of the legend (northoutside,eastoutside,soutoutside, westoutside,north,east,south,west,northeast,southeast,southwest,northwest). You can also turn the grid on for the x and y-axis independently. For example: grid("off","on"); turns the x-axis grid off and turns the y-axis grid on.
That should make plotting in Java a lot easier for those used to plotting in Matlab :)
Related
So im working on a Java/Jogl application that basically just translates the vertices of a triangle. So far I am able to get the triangle to move left to right, but I cant figure out how to edit the vertex shader so that when I click a button, the triangle begins to move up and down instead of left and right.
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.nio.*;
import javax.swing.*;
import static com.jogamp.opengl.GL4.*;
import com.jogamp.common.nio.Buffers;
import com.jogamp.opengl.*;
import com.jogamp.opengl.awt.GLCanvas;
import com.jogamp.opengl.util.FPSAnimator;
import graphicslib3D.GLSLUtils;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
import java.util.Vector;
public class a1 extends JFrame implements GLEventListener
{
private GLCanvas myCanvas;
private int rendering_program;
private int vao[] = new int[1];
private GLSLUtils util = new GLSLUtils();
private Button button1, button2;
private float x = 0.0f;
private float y = 0.0f;
private float inc = 0.01f;
private float incy = 0.01f;
private int click = 1;
public a1()
{ setTitle("Chapter2 - program2");
setSize(600, 400);
myCanvas = new GLCanvas();
myCanvas.addGLEventListener(this);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(myCanvas, BorderLayout.CENTER);
JPanel sidePanel = new JPanel();
sidePanel.setLayout(new BoxLayout(sidePanel, BoxLayout.Y_AXIS));
button1 = new Button("button");
button2 = new Button("button2");
sidePanel.add(button1);
sidePanel.add(button2);
getContentPane().add(sidePanel, BorderLayout.WEST);
button2.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
click = 1;
}
});
setVisible(true);
FPSAnimator animator = new FPSAnimator(myCanvas, 30);
animator.start();
}
public void display(GLAutoDrawable drawable)
{ GL4 gl = (GL4) GLContext.getCurrentGL();
gl.glUseProgram(rendering_program);
gl.glPointSize(50.0f);
float bkg[] = {0.0f, 0.0f, 0.0f, 1.0f};
FloatBuffer bkgBuffer = Buffers.newDirectFloatBuffer(bkg);
gl.glClearBufferfv(GL_COLOR, 0, bkgBuffer);
x+=inc;
y+=incy;
if(x > 1.0f) inc = -0.01f;
if(x < -1.0f) inc = 0.01f;
if(y > 1.0f) incy = -0.01f;
if(y< -1.0f) incy = 0.01f;
int offset_loc = gl.glGetUniformLocation(rendering_program, "inc");
int offset_y = gl.glGetUniformLocation(rendering_program, "incy");
int flag = gl.glGetUniformLocation(rendering_program, "flag");
gl.glProgramUniform1f(rendering_program, offset_loc, x);
gl.glProgramUniform1f(rendering_program, offset_y, y);
gl.glProgramUniform1f(rendering_program, flag, click);
gl.glDrawArrays(GL_TRIANGLES,0,3);
}
public void init(GLAutoDrawable drawable)
{ GL4 gl = (GL4) GLContext.getCurrentGL();
rendering_program = createShaderProgram();
gl.glGenVertexArrays(vao.length, vao, 0);
gl.glBindVertexArray(vao[0]);
}
private int createShaderProgram()
{
GL4 gl = (GL4) GLContext.getCurrentGL();
int[] vertCompiled = new int[1];
int[] fragCompiled = new int[1];
int[] linked = new int[1];
String vshaderSource[] = readShaderSource("src/vert.shader");
String fshaderSource[] = readShaderSource("src/frag.shader");
int vShader = gl.glCreateShader(GL_VERTEX_SHADER);
gl.glShaderSource(vShader, vshaderSource.length, vshaderSource, null, 0);
gl.glCompileShader(vShader);
util.checkOpenGLError();
gl.glGetShaderiv(vShader, GL_COMPILE_STATUS, vertCompiled, 0);
if(vertCompiled[0] == 1)
{
System.out.println("vertex compilation success");
}else{
System.out.println("vertex compilation failed");
util.printShaderLog(vShader);
}
int fShader = gl.glCreateShader(GL_FRAGMENT_SHADER);
gl.glShaderSource(fShader, fshaderSource.length, fshaderSource, null, 0);
gl.glCompileShader(fShader);
util.checkOpenGLError();
gl.glGetShaderiv(fShader, GL_COMPILE_STATUS, fragCompiled, 0);
if(fragCompiled[0] == 1)
{
System.out.println("fragment compilation success");
}else{
System.out.println("fragment compilation failed");
util.printShaderLog(fShader);
}
int vfprogram = gl.glCreateProgram();
gl.glAttachShader(vfprogram, vShader);
gl.glAttachShader(vfprogram, fShader);
gl.glLinkProgram(vfprogram);
util.checkOpenGLError();
gl.glGetProgramiv(vfprogram, GL_LINK_STATUS, linked, 0);
if(linked[0] == 1)
{
System.out.println("linking succeeded");
}else{
System.out.println("linking failed");
util.printProgramLog(vfprogram);
}
//gl.glDeleteShader(vShader);
//gl.glDeleteShader(fShader);
return vfprogram;
}
public static void main(String[] args) { new a1(); }
public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {}
public void dispose(GLAutoDrawable drawable) {}
private String[] readShaderSource(String filename)
{
Vector<String> lines = new Vector<String>();
Scanner sc;
try
{
sc = new Scanner(new File(filename));
}catch (IOException e)
{
System.err.println("IOException reading file: " + e);
return null;
}
while(sc.hasNext())
{
lines.addElement(sc.nextLine());
}
String[] program = new String[lines.size()];
for(int i=0; i<lines.size(); i++)
{
program[i] = (String) lines.elementAt(i)+ "\n";
}
return program;
}
}
#version 430
uniform float inc;
void main(void)
{
if(gl_VertexID == 0) gl_Position = vec4(0.25, -0.25+inc, 0.0, 1.0);
else if(gl_VertexID == 1) gl_Position = vec4(-0.25, -0.25+inc, 0.0, 1.0);
else gl_Position = vec4(0.25, 0.25+inc, 0.0, 1.0);
}
How do I edit my vertex shader so that when I click on a button the location of the vertices changes and the triangle begins to move up and down instead of left to right?
1) Don't do the update like that
2) Don't use an int array for the OpenGL resources (vao), prefer a direct integer buffer, you can use GLBuffers.newDirectIntBuffer(1); for the vao for example..
3) Don't use an FPSAnimator, use Animator instead
4) Don't inialize a new direct buffer everytime in the display() method (bkgBuffer), do it just once in the variable declaration or in the init(). You should also dispose any direct buffer, since it is not guaranteed that they are gonna be removed by the garbage collector. I have a small class for that here. A better one however is the implementation of Gouessej here.
4) Use a vertex buffer object to declare your vertices attributes (like position)
5) Declare a boolean flag variable update to trigger the update.
button2.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
update = 1;
}
});
public void display(GLAutoDrawable drawable) {
GL4 gl = (GL4) GLContext.getCurrentGL();
if(update) {
update = false;
...
}
}
now you have 2 possibilities:
you update the vertices directly
gl3.glBindBuffer(GL_ARRAY_BUFFER, bufferName.get(Buffer.VERTEX));
gl3.glBufferSubData(GL_ARRAY_BUFFER, 0, vertexBuffer.capacity(), vertexBuffer);
gl3.glBindBuffer(GL_ARRAY_BUFFER, 0);
and your vertex shader may be something like
#version 430
layout (location = POSITION) in vec2 position;
void main(void)
{
gl_Position = vec4(position, 0, 1);
}
you update only the matrix that is going to multiply the vertices
gl3.glBindBuffer(GL_UNIFORM_BUFFER, bufferName.get(Buffer.TRANSFORM));
gl3.glBufferSubData(GL_UNIFORM_BUFFER, 0, Mat4.SIZE, matBuffer);
gl3.glBindBuffer(GL_UNIFORM_BUFFER, 0);
and in this case the vertex may be
#version 430
layout (location = POSITION) in vec2 position;
layout (binding = TRANSFORM) uniform Transform
{
mat4 mat;
};
void main(void)
{
gl_Position = mat * vec4(position, 0, 1);
}
In case you need further assistance, don't hesitate to ask for help.
For inspiration, you can refer to this hello triangle of mine
The Shape interface is implemented by objects of Java 2D (Arc2D, Area, CubicCurve2D, Ellipse2D, GeneralPath etc..).
Some of the concrete objects are marked as Serializable and can be stored and restored using object serialization, but others like Area do not implement the interface and throw errors.
But since we are constantly warned that such naive serialization is not necessarily stable across Java implementations or versions, I'd prefer to use some form of serialization that is.
That leads us to storing/restoring from XML using XMLEncoder and XMLDecoder, but that is capable of handling even less of the Java 2D Shape objects.
Some results for both can be seen below. We start with 6 shapes, and attempt to store/restore them via object serialization and standard XML serialization.
How would we store all Shape objects correctly via XML?
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.border.TitledBorder;
public class Serialize2D {
private JPanel ui;
Serialize2D() {
initUI();
}
public void initUI() {
if (ui != null) {
return;
}
ui = new JPanel(new GridLayout(0, 1));
int[] xpoints = {205, 295, 205, 295};
int[] ypoints = {5, 25, 25, 45};
Polygon polygon = new Polygon(xpoints, ypoints, xpoints.length);
ArrayList<Shape> shapes = new ArrayList<Shape>();
int w = 45;
shapes.add(new Rectangle2D.Double(5, 5, 90, 40));
shapes.add(new Ellipse2D.Double(105, 5, 90, 40));
shapes.add(polygon);
shapes.add(new GeneralPath(new Rectangle2D.Double(5, 55, 90, 40)));
shapes.add(new Path2D.Double(new Rectangle2D.Double(105, 55, 90, 40)));
shapes.add(new Area(new Rectangle2D.Double(205, 55, 90, 40)));
addTitledLabelToPanel(shapes, "Original Shapes");
addTitledLabelToPanel(
serializeToFromObject(shapes), "Serialize via Object");
addTitledLabelToPanel(
serializeToFromXML(shapes), "Serialize via XML");
}
public JComponent getUI() {
return ui;
}
public ArrayList<Shape> serializeToFromObject(ArrayList<Shape> shapes) {
ArrayList<Shape> shps = new ArrayList<Shape>();
try {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
for (Shape shape : shapes) {
try {
oos.writeObject(shape);
} catch (Exception ex) {
System.err.println(ex.toString());
}
}
oos.flush();
oos.close();
System.out.println("length Obj: " + baos.toByteArray().length);
ByteArrayInputStream bais = new ByteArrayInputStream(
baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object o = null;
try {
o = ois.readObject();
} catch (NotSerializableException ex) {
System.err.println(ex.getMessage());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
while (o != null) {
shps.add((Shape) o);
try {
o = ois.readObject();
} catch (NotSerializableException ex) {
System.err.println(ex.getMessage());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
return shps;
} catch (IOException ex) {
ex.printStackTrace();
}
return shps;
}
public ArrayList<Shape> serializeToFromXML(ArrayList<Shape> shapes) {
ArrayList<Shape> shps = new ArrayList<Shape>();
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XMLEncoder xmle = new XMLEncoder(baos);
for (Shape shape : shapes) {
xmle.writeObject(shape);
}
xmle.flush();
xmle.close();
System.out.println("length XML: " + baos.toByteArray().length);
ByteArrayInputStream bais
= new ByteArrayInputStream(baos.toByteArray());
XMLDecoder xmld = new XMLDecoder(bais);
Shape shape = (Shape) xmld.readObject();
while (shape != null) {
shps.add(shape);
try {
shape = (Shape) xmld.readObject();
} catch (ArrayIndexOutOfBoundsException aioobe) {
// we've read last object
shape = null;
}
}
xmld.close();
} catch (Exception ex) {
ex.printStackTrace();
}
return shps;
}
private final static String getType(Object o) {
String s = o.getClass().getName();
String[] parts = s.split("\\.");
s = parts[parts.length - 1].split("\\$")[0];
return s;
}
public static void drawShapesToImage(
ArrayList<Shape> shapes, BufferedImage image) {
Graphics2D g = image.createGraphics();
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.WHITE);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
for (Shape shape : shapes) {
String s = getType(shape);
g.setColor(Color.GREEN);
g.fill(shape);
g.setColor(Color.BLACK);
g.draw(shape);
Rectangle r = shape.getBounds();
int x = r.x + 5;
int y = r.y + 16;
if (r.width * r.height != 0) {
g.drawString(s, x, y);
}
}
g.dispose();
}
private void addTitledLabelToPanel(ArrayList<Shape> shapes, String title) {
int w = 300;
int h = 100;
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
drawShapesToImage(shapes, bi);
JLabel l = new JLabel(new ImageIcon(bi));
l.setBorder(new TitledBorder(title));
ui.add(l);
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
Serialize2D ss = new Serialize2D();
JOptionPane.showMessageDialog(null, ss.getUI());
}
};
SwingUtilities.invokeLater(r);
}
}
Unfortunately, naive encoding/decoding of a Shape to XML using XMLEncoder/Decoder often destroys all the vital information of the Shape!
So to do this, still using the above mentioned classes, we serialize and restore properly constructed beans that represent the parts of the shape as obtained from a PathIterator. These beans are:
PathBean which stores the collection of PathSegment objects that form the shape of the Java-2D Shape.
PathSegment which stores the details of a particular part of the path (segment type, winding rule & coords).
SerializeShapes GUI
A GUI to demonstrate storing and restoring shapes.
Click the Ellipse (Ellipse2D), Rectangle (Rectangle2D) or Face (Area) buttons a couple of times.
Exit the GUI. The shapes will be serialized to disk.
Restart the GUI. The randomly drawn shapes from last time will be restored from disk & reappear in the GUI.
The selected shape will be filled in green, other shapes in red.
package serialize2d;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Random;
import java.util.Vector;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
/** A GUI to make it easy to add/remove shapes from a canvas.
It should persist the shapes between runs. */
public class SerializeShapes {
JPanel ui;
JPanel shapePanel;
Random rand;
JPanel shapeCanvas;
DefaultListModel<Shape> allShapesModel;
ListSelectionModel shapeSelectionModel;
RenderingHints renderingHints;
SerializeShapes() {
initUI();
}
public void initUI() {
if (ui != null) {
return;
}
renderingHints = new RenderingHints(RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
renderingHints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
renderingHints.put(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_NORMALIZE);
ui = new JPanel(new BorderLayout(4, 4));
ui.setBorder(new EmptyBorder(4, 4, 4, 4));
JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4));
ui.add(controls, BorderLayout.PAGE_START);
shapeCanvas = new ShapeCanvas();
ui.add(shapeCanvas);
rand = new Random();
allShapesModel = new DefaultListModel<Shape>();
JList<Shape> allShapes = new JList<Shape>(allShapesModel);
allShapes.setCellRenderer(new ShapeListCellRenderer());
shapeSelectionModel = allShapes.getSelectionModel();
shapeSelectionModel.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION);
ListSelectionListener shapesSelectionListener
= new ListSelectionListener() {
#Override
public void valueChanged(ListSelectionEvent e) {
shapeCanvas.repaint();
}
};
allShapes.addListSelectionListener(shapesSelectionListener);
JScrollPane shapesScroll = new JScrollPane(
allShapes,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
);
// TODO fix this hack..
shapesScroll.getViewport().setPreferredSize(new Dimension(60, 200));
ui.add(shapesScroll, BorderLayout.LINE_START);
Action addEllipse = new AbstractAction("Ellipse") {
#Override
public void actionPerformed(ActionEvent e) {
int w = rand.nextInt(100) + 10;
int h = rand.nextInt(100) + 10;
int x = rand.nextInt(shapeCanvas.getWidth() - w);
int y = rand.nextInt(shapeCanvas.getHeight() - h);
Ellipse2D ellipse = new Ellipse2D.Double(x, y, w, h);
addShape(ellipse);
}
};
addEllipse.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_E);
Action addRectangle = new AbstractAction("Rectangle") {
#Override
public void actionPerformed(ActionEvent e) {
int w = rand.nextInt(100) + 10;
int h = rand.nextInt(100) + 10;
int x = rand.nextInt(shapeCanvas.getWidth() - w);
int y = rand.nextInt(shapeCanvas.getHeight() - h);
Rectangle2D rectangle = new Rectangle2D.Double(x, y, w, h);
addShape(rectangle);
}
};
addRectangle.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
final int faceStart = 128513;
final int faceEnd = 128528;
final int diff = faceEnd - faceStart;
StringBuilder sb = new StringBuilder();
for (int count = faceStart; count <= faceEnd; count++) {
sb.append(Character.toChars(count));
}
final String s = sb.toString();
Vector<Font> compatibleFontList = new Vector<Font>();
GraphicsEnvironment ge
= GraphicsEnvironment.getLocalGraphicsEnvironment();
Font[] fonts = ge.getAllFonts();
for (Font font : fonts) {
if (font.canDisplayUpTo(s) < 0) {
compatibleFontList.add(font);
}
}
JComboBox fontChooser = new JComboBox(compatibleFontList);
ListCellRenderer fontRenderer = new DefaultListCellRenderer() {
#Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
Component c = super.getListCellRendererComponent(
list, value, index,
isSelected, cellHasFocus);
JLabel l = (JLabel) c;
Font font = (Font) value;
l.setText(font.getName());
return l;
}
};
fontChooser.setRenderer(fontRenderer);
final ComboBoxModel<Font> fontModel = fontChooser.getModel();
BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = bi.createGraphics();
final FontRenderContext fontRenderContext = g.getFontRenderContext();
Action addFace = new AbstractAction("Face") {
#Override
public void actionPerformed(ActionEvent e) {
int codepoint = faceStart + rand.nextInt(diff);
String text = new String(Character.toChars(codepoint));
Font font = (Font) fontModel.getSelectedItem();
Area area = new Area(
font.deriveFont(80f).
createGlyphVector(fontRenderContext, text).
getOutline());
Rectangle bounds = area.getBounds();
float x = rand.nextInt(
shapeCanvas.getWidth() - bounds.width) - bounds.x;
float y = rand.nextInt(
shapeCanvas.getHeight() - bounds.height) - bounds.y;
AffineTransform move = AffineTransform.
getTranslateInstance(x, y);
area.transform(move);
addShape(area);
}
};
addFace.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F);
Action delete = new AbstractAction("Delete") {
#Override
public void actionPerformed(ActionEvent e) {
int idx = shapeSelectionModel.getMinSelectionIndex();
if (idx < 0) {
JOptionPane.showMessageDialog(
ui,
"Select a shape to delete",
"Select a Shape",
JOptionPane.ERROR_MESSAGE);
} else {
allShapesModel.removeElementAt(idx);
shapeCanvas.repaint();
}
}
};
delete.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_D);
controls.add(new JButton(addEllipse));
controls.add(new JButton(addRectangle));
controls.add(new JButton(addFace));
controls.add(fontChooser);
controls.add(new JButton(delete));
try {
ArrayList<Shape> shapes = deserializeShapes();
for (Shape shape : shapes) {
allShapesModel.addElement(shape);
}
} catch (Exception ex) {
System.err.println("If first launch, this is as expected!");
ex.printStackTrace();
}
}
private void addShape(Shape shape) {
allShapesModel.addElement(shape);
int size = allShapesModel.getSize() - 1;
shapeSelectionModel.addSelectionInterval(size, size);
}
class ShapeCanvas extends JPanel {
ShapeCanvas() {
setBackground(Color.WHITE);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHints(renderingHints);
Stroke stroke = new BasicStroke(1.5f);
g2.setStroke(stroke);
int idx = shapeSelectionModel.getMinSelectionIndex();
Shape selectedShape = null;
if (idx > -1) {
selectedShape = allShapesModel.get(idx);
}
Enumeration en = allShapesModel.elements();
while (en.hasMoreElements()) {
Shape shape = (Shape) en.nextElement();
if (shape.equals(selectedShape)) {
g2.setColor(new Color(0, 255, 0, 191));
} else {
g2.setColor(new Color(255, 0, 0, 191));
}
g2.fill(shape);
g2.setColor(new Color(0, 0, 0, 224));
g2.draw(shape);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(500, 300);
}
}
public JComponent getUI() {
return ui;
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
SerializeShapes se = new SerializeShapes();
JFrame f = new JFrame("Serialize Shapes");
f.addWindowListener(new SerializeWindowListener(se));
f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
f.setContentPane(se.getUI());
f.setResizable(false);
f.pack();
f.setLocationByPlatform(true);
f.setVisible(true);
}
};
SwingUtilities.invokeLater(r);
}
public void serializeShapes() throws FileNotFoundException {
ArrayList<Shape> shapes
= new ArrayList<Shape>();
Enumeration en = allShapesModel.elements();
while (en.hasMoreElements()) {
Shape shape = (Shape) en.nextElement();
shapes.add(shape);
}
ShapeIO.serializeShapes(shapes, this.getClass());
try {
Desktop.getDesktop().open(
ShapeIO.getSerializeFile(this.getClass()));
} catch (Exception e) {
e.printStackTrace();
}
}
public ArrayList<Shape> deserializeShapes() throws FileNotFoundException {
return ShapeIO.deserializeShapes(this.getClass());
}
class ShapeListCellRenderer extends DefaultListCellRenderer {
#Override
public Component getListCellRendererComponent(
JList<? extends Object> list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
Component c = super.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
JLabel l = (JLabel) c;
Shape shape = (Shape) value;
ShapeIcon icon = new ShapeIcon(shape, 40);
l.setIcon(icon);
l.setText("");
return l;
}
}
class ShapeIcon implements Icon {
Shape shape;
int size;
ShapeIcon(Shape shape, int size) {
this.shape = shape;
this.size = size;
}
#Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHints(renderingHints);
Rectangle bounds = shape.getBounds();
int xOff = -bounds.x;
int yOff = -bounds.y;
double xRatio = (double) bounds.width / (double) size;
double yRatio = (double) bounds.height / (double) size;
double ratio = xRatio > yRatio ? xRatio : yRatio;
AffineTransform scale = AffineTransform.getScaleInstance(1 / ratio, 1 / ratio);
AffineTransform shift = AffineTransform.getTranslateInstance(xOff, yOff);
AffineTransform totalTransform = new AffineTransform();
totalTransform.concatenate(scale);
totalTransform.concatenate(shift);
Area b = new Area(shape).createTransformedArea(totalTransform);
bounds = b.getBounds();
g2.setColor(Color.BLACK);
g2.fill(b);
}
#Override
public int getIconWidth() {
return size;
}
#Override
public int getIconHeight() {
return size;
}
}
}
class SerializeWindowListener extends WindowAdapter {
SerializeShapes serializeShapes;
SerializeWindowListener(SerializeShapes serializeShapes) {
this.serializeShapes = serializeShapes;
}
#Override
public void windowClosing(WindowEvent e) {
try {
serializeShapes.serializeShapes();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
System.exit(1);
}
System.exit(0);
}
}
ShapeIO
Performs the I/O to/from XML.
package serialize2d;
import java.awt.Shape;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
public class ShapeIO {
/** Save the list of shapes to the file system. */
public static void serializeShapes(
ArrayList<Shape> shapes, Class serializeClass)
throws FileNotFoundException {
File f = getSerializeFile(serializeClass);
XMLEncoder xmle = new XMLEncoder(new FileOutputStream(f));
ArrayList<PathBean> pathSegmentsCollection = new ArrayList<>();
for (Shape shape : shapes) {
ArrayList<PathSegment> pathSegments =
BeanConverter.getSegmentsFromShape(shape);
PathBean as = new PathBean(pathSegments);
pathSegmentsCollection.add(as);
}
xmle.writeObject(pathSegmentsCollection);
xmle.flush();
xmle.close();
}
/** Load the list of shapes from the file system. */
public static ArrayList<Shape> deserializeShapes(Class serializeClass)
throws FileNotFoundException {
File f = getSerializeFile(serializeClass);
XMLDecoder xmld = new XMLDecoder(new FileInputStream(f));
ArrayList<PathBean> pathSegmentsCollection
= (ArrayList<PathBean>) xmld.readObject();
ArrayList<Shape> shapes = new ArrayList<Shape>();
for (PathBean pathSegments : pathSegmentsCollection) {
shapes.add(BeanConverter.getShapeFromSegments(pathSegments));
}
return shapes;
}
/** Provide an unique, reproducible & readable/writable path for a class. */
public static File getSerializeFile(Class serializeClass) {
File f = new File(System.getProperty("user.home"));
String[] nameParts = serializeClass.getCanonicalName().split("\\.");
f = new File(f, "java");
for (String namePart : nameParts) {
f = new File(f, namePart);
}
f.mkdirs();
f = new File(f, nameParts[nameParts.length-1] + ".xml");
return f;
}
}
BeanConverter
Obtains a PathIterator from the Shape and converts it to a serializable bean. Converts the bean back into a GeneralPath.
package serialize2d;
import java.awt.Shape;
import java.awt.geom.*;
import java.util.ArrayList;
/** Utility class to convert bean to/from a Shape. */
public class BeanConverter {
/** Convert a shape to a serializable bean. */
public static ArrayList<PathSegment> getSegmentsFromShape(Shape shape) {
ArrayList<PathSegment> shapeSegments = new ArrayList<PathSegment>();
for (
PathIterator pi = shape.getPathIterator(null);
!pi.isDone();
pi.next()) {
double[] coords = new double[6];
int pathSegmentType = pi.currentSegment(coords);
int windingRule = pi.getWindingRule();
PathSegment as = new PathSegment(
pathSegmentType, windingRule, coords);
shapeSegments.add(as);
}
return shapeSegments;
}
/** Convert a serializable bean to a shape. */
public static Shape getShapeFromSegments(PathBean shapeSegments) {
GeneralPath gp = new GeneralPath();
for (PathSegment shapeSegment : shapeSegments.getPathSegments()) {
double[] coords = shapeSegment.getCoords();
int pathSegmentType = shapeSegment.getPathSegmentType();
int windingRule = shapeSegment.getWindingRule();
gp.setWindingRule(windingRule);
if (pathSegmentType == PathIterator.SEG_MOVETO) {
gp.moveTo(coords[0], coords[1]);
} else if (pathSegmentType == PathIterator.SEG_LINETO) {
gp.lineTo(coords[0], coords[1]);
} else if (pathSegmentType == PathIterator.SEG_QUADTO) {
gp.quadTo(coords[0], coords[1], coords[2], coords[3]);
} else if (pathSegmentType == PathIterator.SEG_CUBICTO) {
gp.curveTo(
coords[0], coords[1], coords[2],
coords[3], coords[4], coords[5]);
} else if (pathSegmentType == PathIterator.SEG_CLOSE) {
gp.closePath();
} else {
System.err.println("Unexpected value! " + pathSegmentType);
}
}
return gp;
}
}
PathBean
Stores a collection of path segments in a seriallizable bean.
package serialize2d;
import java.awt.geom.*;
import java.util.ArrayList;
/** PathBean stores the collection of PathSegment objects
that constitute the path of a Shape. */
public class PathBean {
public ArrayList<PathSegment> pathSegments;
public PathBean() {}
public PathBean(ArrayList<PathSegment> pathSegments) {
this.pathSegments = pathSegments;
}
public ArrayList<PathSegment> getPathSegments() {
return pathSegments;
}
public void setPathSegments(ArrayList<PathSegment> pathSegments) {
this.pathSegments = pathSegments;
}
#Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
for (PathSegment pathSegment : pathSegments) {
sb.append(" \n\t");
sb.append(pathSegment.toString());
}
sb.append(" \n");
sb.append("}");
return "PathSegments: " + sb.toString();
}
}
PathSegment
Stores the path segment of one part of the entire path.
package serialize2d;
import java.util.Arrays;
/** PathSegment bean stores the detail on one segment of the path
that constitutes a Shape. */
public class PathSegment {
public int pathSegmentType;
public int windingRule;
public double[] coords;
public PathSegment() {}
public PathSegment(int pathSegmentType, int windingRule, double[] coords) {
this.pathSegmentType = pathSegmentType;
this.windingRule = windingRule;
this.coords = coords;
}
public int getPathSegmentType() {
return pathSegmentType;
}
public void setPathSegmentType(int pathSegmentType) {
this.pathSegmentType = pathSegmentType;
}
public int getWindingRule() {
return windingRule;
}
public void setWindingRule(int windingRule) {
this.windingRule = windingRule;
}
public double[] getCoords() {
return coords;
}
public void setCoords(double[] coords) {
this.coords = coords;
}
#Override
public String toString() {
String sC = (coords != null ? "" : Arrays.toString(coords));
String s = String.format(
"PathSegment: Path Segment Type:- %d \t"
+ "Winding Rule:- %d \tcoords:- %s",
getPathSegmentType(), getWindingRule(), sC);
return s;
}
}
Notes
This is intended as a proof of concept as opposed to a polished approach.
XML serialized data becomes big real fast, it would normally be zipped. Zip compression might shave 30-40% off the byte size of a serialized object or a class file, but 80-95% off XML. In any case, zip works well for the next point as well.
For the type of project where we wish to offer to serialize and restore shapes, we'll probably also want to include more details of the shapes (e.g. fill color or texture and draw color or stroke etc.) as well as other data like images or fonts. This is also where Zip comes in handy, since we can put them all in the same archive, each with best levels of compression (e.g. standard for the XML and none for images).
A zip archive of the source files in this answer can be downloaded from my cloud drive.
A custom PersistenceDelegate can be used with XMLEncoder to serialize a Path2D or GeneralPath to XML.
Consider the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
<object class="java.awt.geom.Path2D$Float">
<void property="windingRule">
<int>0</int>
</void>
<void method="moveTo">
<float>1.0</float>
<float>1.0</float>
</void>
<void method="lineTo">
<float>2.0</float>
<float>0.0</float>
</void>
<void method="lineTo">
<float>0.0</float>
<float>3.0</float>
</void>
<void method="closePath"/>
</object>
</java>
When read by an XMLEncoder instance, the following commands will be executed ...
Path2D.Float object = new Path2D.Float();
object.setWindingRule(0); // Note: 0 => Path2D.WIND_EVEN_ODD
object.moveTo(1.0, 1.0);
object.lineTo(2.0, 0.0);
object.lineTo(0.0, 3.0);
object.closePath();
... and a closed triangle object will be returned by XMLDecoder.readObject().
Based on this, we can conclude that XMLDecoder can already deserialize a Path2D shape, if it is properly encoded. What does the XMLEncoder do for us now?
Path2D.Float path = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 10);
path.moveTo(1, 1);
path.lineTo(2, 0);
path.lineTo(0, 3);
path.closePath();
try (XMLEncoder xml = new XMLEncoder(System.out)) {
xml.writeObject(path);
}
This produces the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
<object class="java.awt.geom.Path2D$Float">
<void property="windingRule">
<int>0</int>
</void>
</object>
</java>
Not great, but not too bad. We're just missing the path data. So we just need to extend the DefaultPersistenceDelegate to add the required path commands to the output.
public class Path2DPersistenceDelegate extends DefaultPersistenceDelegate {
#Override
protected void initialize(Class<?> cls, Object oldInstance, Object newInstance, Encoder out) {
super.initialize(cls, oldInstance, newInstance, out);
Shape shape = (Shape) oldInstance;
float coords[] = new float[6];
Float pnt0[] = new Float[0];
Float pnt1[] = new Float[2];
Float pnt2[] = new Float[4];
Float pnt3[] = new Float[6];
Float pnts[];
PathIterator iterator = shape.getPathIterator(null);
while (!iterator.isDone()) {
int type = iterator.currentSegment(coords);
String cmd;
switch (type) {
case PathIterator.SEG_CLOSE:
cmd = "closePath";
pnts = pnt0;
break;
case PathIterator.SEG_MOVETO:
cmd = "moveTo";
pnts = pnt1;
break;
case PathIterator.SEG_LINETO:
cmd = "lineTo";
pnts = pnt1;
break;
case PathIterator.SEG_QUADTO:
cmd = "quadTo";
pnts = pnt2;
break;
case PathIterator.SEG_CUBICTO:
cmd = "curveTo";
pnts = pnt3;
break;
default:
throw new IllegalStateException("Unexpected segment type: " + type);
}
for (int i = 0; i < pnts.length; i++) {
pnts[i] = coords[i];
}
out.writeStatement(new Statement(oldInstance, cmd, pnts));
iterator.next();
}
}
}
Then, we just register this persistence delegate with the XMLEncoder, and it will produce the XML shown at the top of this post.
Path2DPersistenceDelegate path2d_delegate = new Path2DPersistenceDelegate();
try (XMLEncoder xml = new XMLEncoder(System.out)) {
xml.setPersistenceDelegate(Path2D.Float.class, path2d_delegate);
xml.writeObject(path);
}
Since Path2D.Float is the parent class of GeneralPath, a GeneralPath will also be encoded properly. If you want properly encode Path2D.Double shapes, you will need to modify the delegate to use double values and Double objects.
Update:
To construct the Path2D.Float object with the proper windingRule property instead of setting the property afterwards, add the following constructor to the Path2DPersistenceDelegate:
public Path2DPersistenceDelegate() {
super(new String[] { "windingRule" });
}
The XML will then read:
...
<object class="java.awt.geom.Path2D$Float">
<int>0</int>
<void method="moveTo">
...
This loses some human-readable context information in the XML; a human would need to read the documentation to determine that with the Path2D.Float(int) constructor, the int parameter is the windingRule property.
Update 2:
The Polygon persistence delegate is fairly simple:
public class PolygonPersistenceDelegate extends PersistenceDelegate {
#Override
protected Expression instantiate(Object oldInstance, Encoder out) {
Polygon polygon = (Polygon) oldInstance;
return new Expression(oldInstance, oldInstance.getClass(), "new",
new Object[] { polygon.xpoints, polygon.ypoints, polygon.npoints });
}
}
Since Area Constructive Area Geometry object is more complex, it cannot be created by moveTo and lineTo type methods, but rather only by adding, subtracting, or exclusive-or-ing Shape objects. But the constructor takes a Shape object, and a Path2D.Double can be constructed from an Area object, so the persistence delegate actually can be written quite simply as well:
public class AreaPersistenceDelegate extends PersistenceDelegate {
#Override
protected Expression instantiate(Object oldInstance, Encoder out) {
Area area = (Area) oldInstance;
Path2D.Double p2d = new Path2D.Double(area);
return new Expression(oldInstance, oldInstance.getClass(), "new",
new Object[] { p2d });
}
}
Since we are using Path2D.Double internally, we would need to add both persistent delegates to the XMLEncoder:
try (XMLEncoder encoder = new XMLEncoder(baos)) {
encoder.setPersistenceDelegate(Area.class, new AreaPersistenceDelegate());
encoder.setPersistenceDelegate(Path2D.Double.class, new Path2DPersistenceDelegate.Double());
encoder.writeObject(area);
}
Update 3:
A project with the PersistenceDelegate for Area, Path2D and GeneralPath has been created on GitHub.
Notes:
The persistence delegate for Polygon was removed, as it seems to be unnecessary for Java 1.7
Update 4:
For Java 1.7, the pnts array must be allocated for each new Statement(); it cannot be allocated once and reused. Thus, the Path2D delegates must be changed as follows:
float coords[] = new float[6];
/* Removed: Float pnt0[] = new Float[0];
Float pnt1[] = new Float[0];
Float pnt2[] = new Float[4];
Float pnt3[] = new Float[6]; */
Float pnts[];
PathIterator iterator = shape.getPathIterator(null);
while (!iterator.isDone()) {
int type = iterator.currentSegment(coords);
String cmd;
switch (type) {
case PathIterator.SEG_CLOSE:
cmd = "closePath";
pnts = new Float[0]; // Allocate for each segment
break;
case PathIterator.SEG_MOVETO:
cmd = "moveTo";
pnts = new Float[2]; // Allocate for each segment
break;
/* ... etc ...*/
This program is supposed to graph and animate a sine wave by using a thread to "move over" the points in the XYSeries that draws the wave. After a few seconds, the chart starts to flash and zooms in and out randomly. I don't know if this is a problem with my code or with my computer (probably the code).
import java.awt.geom.Point2D;
import static java.lang.Math.PI;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
public class WaveGraph extends JFrame
{
double numOfWaves = 2;
double waveAmp = 2;
double waveLength = 10;
double waveRes = 20;
double median = ((waveLength*numOfWaves)/2);
int numOfPoints = (int)(numOfWaves*waveRes);
boolean going = true;
boolean left = true;
XYSeriesCollection dataset = new XYSeriesCollection();
Point2D.Double[] points;
WaveGraph()
{
XYSeries buoys = new XYSeries("Buoys");
XYSeries series = new XYSeries("Wave");
points = new Point2D.Double[numOfPoints];
for(int i=0; i<numOfPoints; i++)
{
Point2D.Double temp = new Point2D.Double();
temp.x = i*(waveLength/waveRes);
temp.y = waveAmp*Math.sin((2*PI)/waveLength*temp.x);
points[i] = temp;
series.add(points[i].x, points[i].y);
if(i==(numOfPoints/2)-1)
buoys.add(median, points[i].y);
}
buoys.add(median, 0);
dataset.addSeries(series);
dataset.addSeries(buoys);
JFreeChart chart = ChartFactory.createXYLineChart(
"Sine Wave", // chart title
"Wavelength(meters)", // x axis label
"Amplitude (meters)", dataset, // data
PlotOrientation.VERTICAL,
false, // include legend
false, // tooltips
false // urls
);
XYPlot plot = (XYPlot)chart.getPlot();
XYLineAndShapeRenderer renderer
= (XYLineAndShapeRenderer) plot.getRenderer();
renderer.setSeriesShapesFilled(1, true);
renderer.setSeriesShapesVisible(1, true);
NumberAxis domain = (NumberAxis)plot.getDomainAxis();
domain.setRange(0.00, waveLength*numOfWaves);
NumberAxis range = (NumberAxis)plot.getRangeAxis();
range.setRange(-waveAmp*1.3, waveAmp*1.3);
ChartPanel cp = new ChartPanel(chart);
cp.setPreferredSize(new java.awt.Dimension(500, 270));
setContentPane(cp);
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
WaveRunner run = new WaveRunner();
}
class WaveRunner extends Thread
{
WaveRunner()
{
this.start();
}
public void run()
{
double[] temp = new double[numOfPoints];
XYSeries temp2, temp3;
while(going)
{
temp2 = dataset.getSeries(0);
temp3 = dataset.getSeries(1);
temp2.delete(0, numOfPoints-1);
temp3.delete(0, 1);
for(int i=0; i<numOfPoints; i++)
{
if(left){
try{
temp[i] = points[i+1].y;
}
catch(java.lang.ArrayIndexOutOfBoundsException exc){
temp[numOfPoints-1] = points[0].y;
}}
else{
try{
temp[i] = points[i-1].y;
}
catch(java.lang.ArrayIndexOutOfBoundsException exc){
temp[0] = points[numOfPoints-1].y;
}}
}
for(int i=0; i<numOfPoints; i++)
{
points[i].y = temp[i];
temp2.add(points[i].x, points[i].y);
if(i==(numOfPoints/2))
{
temp3.add(median, points[i].y);
temp3.add(median, 0);
}
}
try
{
Thread.sleep(50);
}
catch(InterruptedException exc)
{}
}
}
}
public static void main(String[] args)
{
WaveGraph test = new WaveGraph();
}
}
Your example is incorrectly synchronized in that it updates the chart's dataset from WaveRunner. The chart's model is displayed in a ChartPanel, so it should be updated only on the event dispatch thread. Instead, pace the animation using Swing Timer, as shown here and here, or SwingWorker, as shown here.
I have a time history for arrays describing the pressure along a pipe. So I have an array of pressure values along the length of the pipe for each delta t. I want to plot the pressures along the length of the pipe with JFreeChart and chose which delta t to plot with a slider, so that whenever the user moves the slider the graphic is updates with values from a different delta t. I'm also resetting the tile to be the pressure at the last portion of the pipe. What happens is that the title is updates, meaning the data is being properly updates, but the curve remains the same. I have read every possible topic on forums and tried everything I could think of but it's not working! Here's the code of my class that extends JPanel, in which the method jSlider1StateChanged hears the change in the slider position, createChart creaters a new chart when the program is started, and dataSetGen(int ndt) generates the graph's new dataset based on the slider position:
public class MyMainPanel extends JPanel {
private JFreeChart jc;
private OutputPipe op;
private DefaultXYDataset ds;
private javax.swing.JFrame jFrame1;
private javax.swing.JSlider jSlider1;
private pipevisualizer.MyChartPanel pnlChartPanel;
private void jSlider1StateChanged(javax.swing.event.ChangeEvent evt) {
int ndt = ((JSlider) evt.
getSource()).
getValue();
System.out.println("Slider1: " + ((JSlider) evt.
getSource()).
getValue());
dataSetGen(ndt);
int a = 0;
jc.fireChartChanged();
}
private void dataSetGen(int ndt) {
ArrayList<OutputPipeDt> opDtArray = op.getOpLit();
OutputPipeDt opDt = opDtArray.get(ndt);
double[] H = opDt.getH();
double[] l = new double[H.length];
double[] p = new double[H.length];
double dX = op.getPipeLength() / H.length;
double slope = op.getPipeSlope();
double el = op.getPipeUSElev();
for (int i = 0; i < H.length; i++) {
l[i] = dX * i;
p[i] = el - dX * slope * i;
}
double[][] dataH = new double[2][H.length];
dataH[0] = l;
dataH[1] = H;
double[][] dataP = new double[2][H.length];
dataP[0] = l;
dataP[1] = p;
ds = new DefaultXYDataset();
ds.addSeries("pipe head", dataH);
ds.addSeries("pipe profile", dataP);
jc.setTitle("H[end] = " + Double.toString(dataH[1][l.length - 1]));
jc.fireChartChanged();
}
private JFreeChart createChart(OutputPipe op, int ndt) {
ArrayList<OutputPipeDt> opDtArray = op.getOpLit();
OutputPipeDt opDt = opDtArray.get(ndt);
double[] H = opDt.getH();
double[] l = new double[H.length];
double[] p = new double[H.length];
double dX = op.getPipeLength() / H.length;
double slope = op.getPipeSlope();
double el = op.getPipeUSElev();
for (int i = 0; i < H.length; i++) {
l[i] = dX * i;
p[i] = el - dX * slope * i;
}
double[][] dataH = new double[2][H.length];
dataH[0] = l;
dataH[1] = H;
double[][] dataP = new double[2][H.length];
dataP[0] = l;
dataP[1] = p;
DefaultXYDataset ds = new DefaultXYDataset();
ds.addSeries("pipe head", dataH);
ds.addSeries("pipe profile", dataP);
JFreeChart chart = ChartFactory.createXYLineChart(
"t = " + Double.toString(op.getOpLit().get(ndt).getT()),
// chart title
"X",
// x axis label
"Y",
// y axis label
ds,
// data
PlotOrientation.VERTICAL,
true,
// include legend
true,
// tooltips
false // urls
);
return chart;
}
}
I thought that any changes to the datasets would make the graph redraw itself.
Sorry if the code might be to big to be on the post, but I don't know exactly which parts should I paste to be more or less clear.
Absent a complete example, you may be able to use the approach shown here; it uses plot.setDataset() to replace the dataset on receiving each event.
Addendum: This example that shows temperature versus length over time may get you started.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/**
* #see https://stackoverflow.com/a/15207445/230513
*/
public class ChartSliderTest {
private static final int N = 25;
private static final double K = 273.15;
private static final Random random = new Random();
private static XYDataset getDataset(int n) {
final XYSeries series = new XYSeries("Temp (K°)");
double temperature;
for (int length = 0; length < N; length++) {
temperature = K + n * random.nextGaussian();
series.add(length + 1, temperature);
}
return new XYSeriesCollection(series);
}
private static JFreeChart createChart(final XYDataset dataset) {
JFreeChart chart = ChartFactory.createXYLineChart(
"ChartSliderTest", "Length (m)", "Temp (K°)", dataset,
PlotOrientation.VERTICAL, false, false, false);
return chart;
}
private static void display() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final List<XYDataset> list = new ArrayList<XYDataset>();
for (int i = 0; i <= 10; i++) {
list.add(getDataset(i));
}
JFreeChart chart = createChart(list.get(5));
final XYPlot plot = (XYPlot) chart.getPlot();
plot.getRangeAxis().setRangeAboutValue(K, K / 5);
ChartPanel chartPanel = new ChartPanel(chart) {
#Override
public Dimension getPreferredSize() {
return new Dimension(800, 400);
}
};
f.add(chartPanel);
final JSlider slider = new JSlider(0, 10);
slider.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
plot.setDataset(list.get(slider.getValue()));
}
});
Box p = new Box(BoxLayout.X_AXIS);
p.add(new JLabel("Time:"));
p.add(slider);
f.add(p, BorderLayout.SOUTH);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
display();
}
});
}
}
I plot a curve with JFreechart. Then the user can draw ranges by dragging the mouse. These I plot using AbstractChartAnnotation to draw a filled Path2D. So far so nice - all aligns perfectly with the curve.
When an area was already annotated the new annotation gets deleted. I use XYPlot.removeAnnotation with the new annotation.
My problem is that sometimes not only the "new" annotation gets removed, but also a second annotation elsewhere in the plot. It doesn't seem random - I kinda found annotations to the "right" side more prone to this happening.
I'm very confused what could cause this. The object that draws/deletes the new annotation is reinstated every time and only holds the current annotation - so how could the other annotation be deleted?
Would be very grateful for any hints, thanks.
As suggested I prepare a sscce example. Unfortunately it's not too short.
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.event.MouseInputListener;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.AbstractXYAnnotation;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.ui.RectangleEdge;
/**
*
* #author c.ager
*/
public class IntegrationSSCE {
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
JFrame jFrame = new JFrame();
jFrame.setLayout(new BorderLayout());
jFrame.setSize(600, 400);
jFrame.setDefaultCloseOperation(jFrame.EXIT_ON_CLOSE);
TimeSeriesCollection timeSeriesCollection = new TimeSeriesCollection();
TimeSeries timeSeries = new TimeSeries("test");
for (long i = 0; i < 1000; i++) {
double val = Math.random() + 3 * Math.exp(-Math.pow(i - 300, 2) / 1000);
timeSeries.add(new Millisecond(new Date(i)), val);
}
timeSeriesCollection.addSeries(timeSeries);
JFreeChart chart = ChartFactory.createTimeSeriesChart(
null,
null, "data", timeSeriesCollection,
true, true, false);
ChartPanel chartPanel = new ChartPanel(chart);
chartPanel.removeMouseListener(chartPanel);
Set<MyAnnot> annotSet = new TreeSet<MyAnnot>();
AnnotListener list = new AnnotListener(chartPanel, annotSet, timeSeries);
chartPanel.addMouseListener(list);
chartPanel.addMouseMotionListener(list);
jFrame.add(chartPanel, BorderLayout.CENTER);
jFrame.setVisible(true);
// TODO code application logic here
}
private static class AnnotListener implements MouseInputListener {
Point2D start, end;
MyAnnot currAnnot;
final Set<MyAnnot> annotSet;
final ChartPanel myChart;
final TimeSeries timeSeries;
public AnnotListener(ChartPanel myChart, Set<MyAnnot> annotSet, TimeSeries timeSeries) {
this.myChart = myChart;
this.annotSet = annotSet;
this.timeSeries = timeSeries;
}
#Override
public void mousePressed(MouseEvent e) {
start = convertScreePoint2DataPoint(e.getPoint());
currAnnot = new MyAnnot(start, timeSeries, myChart.getChart().getXYPlot());
myChart.getChart().getXYPlot().addAnnotation(currAnnot);
}
#Override
public void mouseDragged(MouseEvent e) {
end = convertScreePoint2DataPoint(e.getPoint());
currAnnot.updateEnd(end);
}
#Override
public void mouseReleased(MouseEvent e) {
boolean test = annotSet.add(currAnnot);
if (!test) {
myChart.getChart().getXYPlot().removeAnnotation(currAnnot);
}
}
#Override
public void mouseEntered(MouseEvent e) {
}
#Override
public void mouseClicked(MouseEvent e) {
}
#Override
public void mouseExited(MouseEvent e) {
}
#Override
public void mouseMoved(MouseEvent e) {
}
protected Point2D convertScreePoint2DataPoint(Point in) {
Rectangle2D plotArea = myChart.getScreenDataArea();
XYPlot plot = (XYPlot) myChart.getChart().getPlot();
double x = plot.getDomainAxis().java2DToValue(in.getX(), plotArea, plot.getDomainAxisEdge());
double y = plot.getRangeAxis().java2DToValue(in.getY(), plotArea, plot.getRangeAxisEdge());
return new Point2D.Double(x, y);
}
}
private static class MyAnnot extends AbstractXYAnnotation implements Comparable<MyAnnot> {
Long max;
Line2D line;
final TimeSeries timeSeries;
final XYPlot plot;
final Stroke stroke = new BasicStroke(1.5f);
public MyAnnot(Point2D start, TimeSeries timeSeries, XYPlot plot) {
this.plot = plot;
this.timeSeries = timeSeries;
line = new Line2D.Double(start, start);
findMax();
}
public void updateEnd(Point2D end) {
line.setLine(line.getP1(), end);
findMax();
fireAnnotationChanged();
}
#Override
public void draw(Graphics2D gd, XYPlot xyplot, Rectangle2D rd, ValueAxis va, ValueAxis va1, int i, PlotRenderingInfo pri) {
PlotOrientation orientation = plot.getOrientation();
RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
plot.getDomainAxisLocation(), orientation);
RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(
plot.getRangeAxisLocation(), orientation);
double m02 = va.valueToJava2D(0, rd, domainEdge);
// y-axis translation
double m12 = va1.valueToJava2D(0, rd, rangeEdge);
// x-axis scale
double m00 = va.valueToJava2D(1, rd, domainEdge) - m02;
// y-axis scale
double m11 = va1.valueToJava2D(1, rd, rangeEdge) - m12;
Shape s = null;
if (orientation == PlotOrientation.HORIZONTAL) {
AffineTransform t1 = new AffineTransform(
0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f);
AffineTransform t2 = new AffineTransform(
m11, 0.0f, 0.0f, m00, m12, m02);
s = t1.createTransformedShape(line);
s = t2.createTransformedShape(s);
} else if (orientation == PlotOrientation.VERTICAL) {
AffineTransform t = new AffineTransform(m00, 0, 0, m11, m02, m12);
s = t.createTransformedShape(line);
}
gd.setStroke(stroke);
gd.setPaint(Color.BLUE);
gd.draw(s);
addEntity(pri, s.getBounds2D(), i, getToolTipText(), getURL());
}
#Override
public int compareTo(MyAnnot o) {
return max.compareTo(o.max);
}
private void findMax() {
max = (long) line.getP1().getX();
Point2D left, right;
if (line.getP1().getX() < line.getP2().getX()) {
left = line.getP1();
right = line.getP2();
} else {
left = line.getP2();
right = line.getP1();
}
Double maxVal = left.getY();
List<TimeSeriesDataItem> items = timeSeries.getItems();
for (Iterator<TimeSeriesDataItem> it = items.iterator(); it.hasNext();) {
TimeSeriesDataItem dataItem = it.next();
if (dataItem.getPeriod().getFirstMillisecond() < left.getX()) {
continue;
}
if (dataItem.getPeriod().getFirstMillisecond() > right.getX()) {
break;
}
double curVal = dataItem.getValue().doubleValue();
if (curVal > maxVal) {
maxVal = curVal;
max = dataItem.getPeriod().getFirstMillisecond();
}
}
}
}
}
Here is the problematic behaviour. Note that images 2 and 4 were taken while the mouse button was pressed.
select a few non-overlapping lines - no problem as it should be
I have just been looking at it in the debugger - could it be that ArrayList.remove(Object o) removes the WRONG element? Seems very unlikely to me...
You might look at the Layer to which the annotation is being added. There's an example here. Naturally, an sscce that exhibits the problem you describe would help clarify the source of the problem.
Addendum: One potential problem is that your implementation of Comparable is not consistent with equals(), as the latter relies (implicitly) on the super-class implementation. A consistent implementation is required for use with a sorted Set such as TreeSet. You'll need to override hashCode(), too. Class Value is an example.