Subpages: 1. Split Panes: JSplitPane
2. Basic split pane example
3. Gas model simulation using a split pane
8.3 Gas model simulation using a split pane
In this section we'll use JSplitPane for an interesting scientific experiment: a simulation of the gas model. Left and right components represent containers holding a two-dimensional "ideal gas." The JSplitPane component provides a moveable divider between them. By moving the divider we observe how gas reacts when its volume is changed. Online educational software is ever-increasing as the internet flourishes, and here we show how Swing can be used to demonstrate one of the most basic laws of thermodynamics!
Note: As you may remember from a physics or chemistry course, an ideal gas is a physical model in which gas atoms or molecules move in random directions bouncing elastically from a container's bounds. Mutual collisions are negligible. The speed of the atoms depends on gas temperature. Several laws can be demonstrated with this model. One states that under the condition of constant temperature, multiplication of pressure P and volume V is constant: PV = const.
To model the motion of atoms we'll use threads. So this example also gives a good example of using several threads in Swing.

Figure 8.2 Gas model simulation showing moving atoms.
<<file figure8-2.gif>>
The Code: Split.java
see \Chapter8\2
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
public class Split extends JFrame implements Runnable{
protected GasPanel m_left;
protected GasPanel m_right;
public Split() {
super("Gas Pressure [Split Pane]");
setSize(600, 300);
ImageIcon ball1 = new ImageIcon("ball1.gif");
m_left = new GasPanel(30, ball1.getImage());
ImageIcon ball2 = new ImageIcon("ball2.gif");
m_right = new GasPanel(30, ball2.getImage());
JSplitPane sp = new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT, m_left, m_right);
sp.setDividerSize(10);
sp.setContinuousLayout(true);
getContentPane().add(sp, BorderLayout.CENTER);
WindowListener wndCloser = new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
};
addWindowListener(wndCloser);
setVisible(true);
new Thread(m_left).start();
new Thread(m_right).start();
new Thread(this).start();
}
public void run() {
while (true) {
int p1 = (int)m_left.m_px2;
int pv1 = p1*m_left.getWidth();
int p2 = (int)m_right.m_px1;
int pv2 = p2*m_right.getWidth();
System.out.println("Left: p="+p1+"\tpv="+pv1+
"\tRight: p="+p2+"\tpv="+pv2);
m_left.clearCounters();
m_right.clearCounters();
try {
Thread.sleep(20000);
}
catch(InterruptedException e) {}
}
}
public static void main(String argv[]) { new Split(); }
}
class GasPanel extends JPanel implements Runnable
{
protected Atom[] m_atoms;
protected Image m_img;
protected Rectangle m_rc;
public double m_px1 = 0;
public double m_px2 = 0;
public double m_py1 = 0;
public double m_py2 = 0;
public GasPanel(int nAtoms, Image img) {
setBackground(Color.white);
enableEvents(ComponentEvent.COMPONENT_RESIZED);
m_img = img;
m_atoms = new Atom[nAtoms];
m_rc = new Rectangle(getPreferredSize());
for (int k=0; k<nAtoms; k++) {
m_atoms[k] = new Atom(this);
}
}
public Dimension getPreferredSize() {
return new Dimension(300, 300);
}
public void run() {
while (true) {
for (int k=0; k<m_atoms.length; k++)
m_atoms[k].move(m_rc);
repaint();
try {
Thread.sleep(100);
}
catch(InterruptedException e) {}
}
}
public void paintComponent(Graphics g) {
g.setColor(getBackground());
g.fillRect(m_rc.x, m_rc.y, m_rc.width, m_rc.height);
for (int k=0; k<m_atoms.length; k++)
g.drawImage(m_img, m_atoms[k].getX(),
m_atoms[k].getY(), this);
}
protected void processComponentEvent(ComponentEvent e) {
if (e.getID() == ComponentEvent.COMPONENT_RESIZED) {
m_rc.setSize(getSize());
for (int k=0; k<m_atoms.length; k++)
m_atoms[k].ensureInRect(m_rc);
}
}
public void clearCounters() {
m_px1 = 0;
m_px2 = 0;
m_py1 = 0;
m_py2 = 0;
}
}
class Atom
{
protected double m_x;
protected double m_y;
protected double m_vx;
protected double m_vy;
protected GasPanel m_parent;
public Atom(GasPanel parent) {
m_parent = parent;
m_x = parent.m_rc.x + parent.m_rc.width*Math.random();
m_y = parent.m_rc.y + parent.m_rc.height*Math.random();
double angle = 2*Math.PI*Math.random();
m_vx = 10*Math.cos(angle);
m_vy = 10*Math.sin(angle);
}
public void move(Rectangle rc) {
double x = m_x + m_vx;
double y = m_y + m_vy;
int x1 = rc.x;
int x2 = rc.x + rc.width;
int y1 = rc.y;
int y2 = rc.y + rc.height;
for (int bounce = 0; bounce<2; bounce++) {
if (x < x1) {
x += 2*(x1-x);
m_vx = - m_vx;
m_parent.m_px1 += 2*Math.abs(m_vx);
}
if (x > x2) {
x -= 2*(x-x2);
m_vx = - m_vx;
m_parent.m_px2 += 2*Math.abs(m_vx);
}
if (y < y1) {
y += 2*(y1-y);
m_vy = - m_vy;
m_parent.m_py1 += 2*Math.abs(m_vy);
}
if (y > y2) {
y -= 2*(y-y2);
m_vy = - m_vy;
m_parent.m_py2 += 2*Math.abs(m_vy);
}
}
m_x = x;
m_y = y;
}
public void ensureInRect(Rectangle rc) {
if (m_x < rc.x)
m_x = rc.x;
if (m_x > rc.x + rc.width)
m_x = rc.x + rc.width;
if (m_y < rc.y)
m_y = rc.y;
if (m_y > rc.y + rc.height)
m_y = rc.y + rc.height;
}
public int getX() { return (int)m_x; }
public int getY() { return (int)m_y; }
}
Understanding the Code
Class Split
The constructor of the Split frame creates two instances of the GasPanel class (which models a gas container, see below) and places them in a JSplitPane. All other code requires little discussion, but we must comment on one thing. Both the Split and GasPanel classes implement the Runnable interface, so threads are created and started to run all three instances.
Reminder: The Runnable interface should be implemented by classes which do not intend to use any Thread functionality other than the run() method. In such a case we don't have to sub-class the Thread class. Instead we can simply implement Runnable and define the run() method. In this method we can use the static Thread.sleep() method to yield control to other threads for a specified amount of time.
The run() method of our Split class periodically interrogates the pressure on the divider from the left and right containers, as well as each container's width, which is proportional to the container's volume. Then it prints out the results of our measurements, clears the counters of the containers (see below) and sleeps for another 20 seconds.
Note: Because of the random nature of the gas model all observations are statistical. Each time you run this simulation you're likely to observe a slightly different results. The more atoms that constitute the gas, the more accurate the results achieved will be. A real gas has about 1022 atoms/cm3, and fluctuations in its parameters are very small. In our model only a few dozen "atoms" are participating, so fluctuations are considerable, and we have to wait a bit before we can obtain meaningful measurements (through averaging several results).
Class GasPanel
Class GasPanel models a two-dimensional gas container. It implements the Runnable interface to model the random motion of its contained atoms. Seven instance variables have the following meaning:
Atom[] m_atoms: array of Atom instances hosted by this container.
Image m_img: image used to draw atoms.
Rectangle m_rc: container's rectangular bounds.
double m_px1: counter to measure pressure on the left wall.
double m_px2: counter to measure pressure on the right wall.
double m_py1: counter to measure pressure on the top wall.
double m_py2: counter to measure pressure on the bottom wall.
The GasPanel constructor takes a number of atoms to be created as a parameter as well as a reference to the image to represent them. Method enableEvents() is called to enable the processing of resize events on this component. Finally, an array of atoms is created (see the Atom class below).
The getPreferredSize() method returns the preferred size of this component (used by our split pane to determine the initial position of its divider).
The run() method activates the gas container. For each child atom it invokes the Atom move() method (see below) which changes an Atom's coordinates. The component is then repainted and the calling thread sleeps for 100 ms to provide smooth continuous motion.
The paintComponent() method is overridden to draw each child atom. It clears the component's area and, for each atom, draws the specified image (typically a small ball) at the current atom location.
The processComponentEvent() method is overridden to process resizing events. It updates the m_rc rectangle (used to limit atom motion) and calls the ensureInRect() method for all child atoms to force them to stay inside this component's bounds. A check for the COMPONENT_RESIZED ID is done to skip the processing of COMPONENT_MOVED events which are also delivered to this component even though we've explicitly asked for only COMPONENT_RESIZED events (see enableEvents() call in constructor).
The clearCounters() method clears the counters used to measure the pressure on each of the walls.
Class Atom
An Atom represents a single object moving within a specific rectangular region and bouncing elastically from its walls. Instance variables:
double m_x: current x-coordinate.
double m_y: current y-coordinate.
double m_vx: current x-component of velocity.
double m_vy: current y-component of velocity.
GasPanel m_parent: reference to parent container.
The Atom constructor takes a reference to a parent GasPanel as parameter. It initializes its coordinates randomly within the parent's boudning rectangle using Math.random() as a random number generator. An atom's velocity vector is assigned a fixed absolute magnitude (10) and a random orientation in the x-y plane.
Note: In a more realistic model, velocity would be a normally distributed random value. However, this is not very significant for our purposes.
The move() method moves an atom to a new position and is called during each time the parent GasPanel's run() loop is executed. When new coordinates x, y are calculated, this method checks for possible bounces from this container's walls:
public void move(Rectangle rc) {
double x = m_x + m_vx;
double y = m_y + m_vy;
int x1 = rc.x;
int x2 = rc.x + rc.width;
int y1 = rc.y;
int y2 = rc.y + rc.height;
for (int bounce = 0; bounce<2; bounce++) {
// pseudo-code
if (x < x1) { // bounce off of left wall... }
if (x > x2) { // bounce off of right wall... }
if (y < y1) { // bounce off of top wall... }
if (y > y2) { // bounce off of bottom wall... }
}
m_x = x;
m_y = y;
}
If a new point lies behind one of four walls, a bounce occurs, which changes the coordinate and velocity vector. This contributes to the pressure on the wall the bounce occurred on (as an absolute change in the velocity's component), which is accumulated in the parent GasPanel. Note that bouncing is checked twice to take into account the rare case that two subsequent bounces occur in a single step. That can occur near the container's corners, when, after the first bounce, the moving particle is repositioned beyond the nearest perpendicular wall.
The final methods of our Atom class are fairly straightforward. The ensureInRect() method is called to ensure that an Atom's coordinates lie within the given rectangle, and the getX() and getY() methods return the current coordinates as integers.
Running the Code
Note how the gas reacts to the change in the parent container's volume by adjusting the position of the split pane divider. Also try adjusting the size of the application frame.
The following are some P and PV measurements we obtained when experimenting with this example:
Left: p=749 pv=224700 Right: p=996 pv=276888
Left: p=701 pv=210300 Right: p=1006 pv=279668
Left: p=714 pv=214200 Right: p=1028 pv=285784
Left: p=770 pv=231000 Right: p=1018 pv=283004
Left: p=805 pv=241500 Right: p=1079 pv=299962
Left: p=1586 pv=190320 Right: p=680 pv=311440
Left: p=1757 pv=210840 Right: p=594 pv=272052
Left: p=1819 pv=218280 Right: p=590 pv=270220
Left: p=1863 pv=223560 Right: p=573 pv=262434
Left: p=1792 pv=215040 Right: p=621 pv=284418
We can see tell at a certain time the divider had been moved from right to left by the increase in pressure on the left side, and a decrease in pressure on the right side. However, the PV value (in arbitrary units) remains practically unchanged.


RSS feed Java FAQ News