Wednesday, September 23, 2009

Using JavaFX Classes Directly From Java

Sitting on the Swing side of the fence the grass on the JavaFX side certainly sometimes seems a lot greener.


Knowing that the latest and greatest scene graph code is available somewhere in the pile of JavaFX jars it is tempting to try and get access to it from the Swing side without being "bothered" with the JavaFX language.

Other people like Matt Hicks have had the same thoughts (see I Hate JavaFX; I Love JavaFX!) so I started out with trying Matt's port of the JavaFX Clock and after some fiddling I got it up and running with JavaFX 1.2. Try it by pushing the launch button below.



The code follows (add all the JavaFX SDK jars to your classpath before trying this):

/**
* @author Matt Hicks (matt@matthicks.com)
*/
public class Clock {
private float radius = 77;
private float centerX = 144;
private float centerY = 144;
private Calendar calendar = Calendar.getInstance();
private int hours;
private int minutes;
private int seconds;

private FloatVariable hoursVariable;
private FloatVariable minutesVariable;
private FloatVariable secondsVariable;

public Clock() throws IOException {
nextTick();

// Build JavaFX clock
Group group = new Group();
{
ImageView imageView = new ImageView();
Image image = new Image();
// should be image.set$url() but does not work ?
image.loc$platformImage().set(
ImageIO.read(getClass().getClassLoader().getResource(
"clock_background.png")));
imageView.set$image(image);
group.loc$content().insert(imageView);

Group face = new Group();
{
Translate translate = new Translate();
translate.set$x(centerX);
translate.set$y(centerY);
face.loc$transforms.insert(translate);

// Every third hour
for (int i = 3; i <= 12; i += 3) {
Text text = new Text();
translate = new Translate();
translate.set$x(-5.0f);
translate.set$y(5.0f);
text.loc$transforms.insert(translate);
text.set$font(Font.font("Arial", 16));
text.set$x(radius * ((i + 0) % 2 * (2 - i / 3)));
text.set$y(radius * ((i + 1) % 2 * (3 - i / 3)));
text.set$content(String.valueOf(i));
face.loc$content.insert(text);
}

// Black circle for the rest of the hours
for (int i = 1; i < 12; i++) {
if (i % 3 == 0) {
continue; // Don't show a circle on every third hour
}

Circle circle = new Circle();
Rotate rotate = new Rotate();
rotate.set$angle(30.0f * i);
circle.loc$transforms.insert(rotate);
circle.set$centerX(radius);
circle.set$radius(3.0f);
circle.set$fill(Color.$BLACK);
face.loc$content.insert(circle);
}

// Center circles
Circle circle = new Circle();
circle.set$radius(5.0f);
circle.set$fill(Color.$DARKRED);

face.loc$content.insert(circle);
circle = new Circle();
circle.set$radius(3.0f);
circle.set$fill(Color.$RED);
face.loc$content.insert(circle);

// Second hand
Line line = new Line();
{
Rotate rotate = new Rotate();
BindingExpression exp = new AbstractBindingExpression() {
@Override
public void compute() {
pushValue(seconds * 6f);
}
};
secondsVariable = FloatVariable.make(exp);
rotate.loc$angle().bind(false, secondsVariable);

line.loc$transforms.insert(rotate);

line.set$endY(-radius - 3.0f);
line.set$strokeWidth(2.0f);
line.set$stroke(Color.$RED);
}
face.loc$content.insert(line);

// Hour hand
Path path = new Path();
{
Rotate rotate = new Rotate();
BindingExpression exp = new AbstractBindingExpression() {
@Override
public void compute() {
pushValue((float) (hours + minutes / 60) * 30 - 90);
}
};

hoursVariable = FloatVariable.make(exp);
rotate.loc$angle().bind(false, hoursVariable);
path.loc$transforms.insert(rotate);

path.set$fill(Color.$BLACK);

MoveTo e1 = new MoveTo();
e1.set$x(4.0f);
e1.set$y(4.0f);
path.loc$elements.insert(e1);
ArcTo e2 = new ArcTo();
e2.set$x(4.0f);
e2.set$y(-4.0f);
e2.set$radiusX(1.0f);
e2.set$radiusY(1.0f);
path.loc$elements.insert(e2);
LineTo e3 = new LineTo();
e3.set$x(radius - 15.0f);
e3.set$y(0.0f);
path.loc$elements.insert(e3);
}
face.loc$content.insert(path);

// Minute hand
path = new Path();
{
Rotate rotate = new Rotate();
BindingExpression exp = new AbstractBindingExpression() {
public void compute() {
pushValue((float) minutes * 6 - 90);
}
};
minutesVariable = FloatVariable.make(exp);
rotate.loc$angle().bind(false, minutesVariable);

path.loc$transforms.insert(rotate);
path.set$fill(Color.$BLACK);

MoveTo e1 = new MoveTo();
e1.set$x(4.0f);
e1.set$y(4.0f);
path.loc$elements.insert(e1);
ArcTo e2 = new ArcTo();
e2.set$x(4.0f);
e2.set$y(-4.0f);
e2.set$radiusX(1.0f);
e2.set$radiusY(1.0f);
path.loc$elements.insert(e2);
LineTo e3 = new LineTo();
e3.set$x(radius);
e3.set$y(0.0f);
path.loc$elements.insert(e3);
}
face.loc$content.insert(path);

group.loc$content.insert(face);
}
}

Timeline timeline = new Timeline();
timeline.set$repeatCount(Timeline.$INDEFINITE);

KeyFrame kf = new KeyFrame();
kf.set$time(Duration.valueOf(1000.0f));
kf.set$canSkip(true);
kf.set$action(new Function0<Void>() {
public Void invoke() {
nextTick();
return null;
}
});
timeline.loc$keyFrames.insert(kf);
// this is somewhat hairy JFxtras does it like this I think
Scene scene = new Scene();
scene.loc$content().insert(group);
JPanel panel = new JPanel(new BorderLayout());
TKScene fxNode = scene.get$javafx$scene$Scene$impl_peer();
panel.add(((SwingScene) fxNode).scenePanel, BorderLayout.CENTER);
JFrame frame = new JFrame("JavaFX Clock Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.setSize(295, 325);
frame.setVisible(true);
timeline.play();
}

public void nextTick() {
calendar.setTimeInMillis(System.currentTimeMillis());
seconds = calendar.get(Calendar.SECOND);
minutes = calendar.get(Calendar.MINUTE);
hours = calendar.get(Calendar.HOUR_OF_DAY);
// trigger bindings to re calc and move hands
if (secondsVariable != null) {
secondsVariable.invalidate();
minutesVariable.invalidate();
hoursVariable.invalidate();
}
}

public static void main(String[] args) throws Exception {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
new Clock();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}

For comparison the JavaFX version follows here:

public class Clock extends CustomNode {

public var radius: Number = 77;
public var centerX: Number = 144;
public var centerY: Number = 144;

public var hours:Number;
public var minutes:Number;
public var seconds:Number;

public function nextTick () {
var now = new Date();
seconds = now.getSeconds();
minutes = now.getMinutes();
hours = now.getHours();
}

public override function create() : Node {
return Group {
content: [
ImageView {
image: Image {
url: "{__DIR__}clock_background.png"
}
},
Group {
transforms: Translate {
x: centerX,
y: centerY
}
content: [
// code to display the numbers for every third hour
for (i in [3, 6, 9, 12])
Text {
transforms: Translate {
x: -5,
y: 5
}
font: Font {
size: 16
}
x: radius * (( i + 0 ) mod 2 * ( 2 - i / 3))
y: radius * (( i + 1 ) mod 2 * ( 3 - i / 3))
content: "{i}"
}, //Text


//code to display a black circle for the rest of the hours on the clock
for (i in [1..12])
if (i mod 3 != 0 ) then Circle {
transforms: Rotate {
angle: 30 * i
}
centerX: radius
radius: 3
fill: Color.BLACK
} //for
else [ ],

// code for the clock's first center circle
Circle {
radius: 5
fill: Color.DARKRED
}, //Circle
//code for the smaller center circle
Circle {
radius: 3
fill: Color.RED
}, //Circle
//code for the seconds hand
Line {
transforms: Rotate {
angle: bind seconds * 6
}
endY: -radius - 3
strokeWidth: 2
stroke: Color.RED
}, //Line
//code for the hour hand
Path {
transforms: Rotate {
angle: bind (hours + minutes / 60) * 30 - 90
}
fill: Color.BLACK
elements: [
MoveTo {
x: 4,
y: 4},
ArcTo {
x: 4
y: -4
radiusX: 1
radiusY: 1},
LineTo{
x: radius - 15
y: 0},
] //elements
}, // Path
// code for the minutes hand
Path {
transforms: Rotate {
angle: bind minutes * 6 - 90
}
fill: Color.BLACK
elements: [
MoveTo {
x: 4,
y: 4},
ArcTo {
x: 4
y: -4
radiusX: 1
radiusY: 1},
LineTo{
x: radius
y: 0},
] // elements
} // Path
] //content
}
]
};
}

init {
var timeline = Timeline {
repeatCount: Timeline.INDEFINITE
keyFrames: [
KeyFrame {
time: 1s
canSkip: true
action: function() {
nextTick();
}
}
]
}
timeline.play();
}
}

Web start depolyment is rather easy as it is possible to take advantage of the JavaFX extension and just add:



to your jnlp file and then do the usual jnlp stuff.

Saturday, September 12, 2009

Country Flag Decorations On Your Swing Components

Here is how to get country flag icon decorations on your JTable cells, JList cells, JCombobox'es and JTextFields.




First you need to grab the Flag Icons from the famfamfam site and package them into a jar file and add it to your classpath. The JTextField flag decoration is done via the BuddySupport in the xswingx project.

Then we need to set up a mapping of country names to famfamfam icon names like this:

private final String[] countries = { "AFGHANISTAN", "AF", "Ă…LAND ISLANDS",...}
Map<String, String> countryMap = new HashMap<String, String>();
for (int i = 0; i < countries.length;) {
countryMap.put(countries[i], countries[i + 1]);
i += 2;
}
countryFlagDecorator = new CountryFlagDecorator(countryMap);
// lazy do CountryFlagDecorator.getInstance() and skip the above mapping setup

Now we can decorate e.g., a JCombobox like this:

JComboBox flagCombo = new JComboBox(new String[] { "DENMARK", "SWEDEN",
"NORWAY" });
countryFlagDecorator.addCountryFlagDecorations(flagCombo);

A JTable cell like this:

JTable jt = new JTable(data, fields);
TableColumn col = jt.getColumnModel().getColumn(2);
col.setCellRenderer(new CountryFlagTableCellRendere(
countryFlagDecorator));
col.setCellEditor(new CountryFlagCellEditor(countryFlagDecorator,
new String[] { "DENMARK", "SWEDEN", "NORWAY" }));

A Jlist (with a small twist)

JList flagList = new JList(new String[] { "DK", "SE", "NO", "US" });
flagList.setCellRenderer(new CountryFlagListCellRendere(
countryFlagDecorator, new CountryNameConverter() {
@Override
public String convertCountryName(String countryName) {
if ("DK".equals(countryName)) {
return "DENMARK";
} else if ("SE".equals(countryName)) {
return "SWEDEN";
} else if ("NO".equals(countryName)) {
return "NORWAY";
} else if ("US".equals(countryName)) {
return "UNITED STATES";
}
return null;
}
}));

And a JTextField like this:

JTextField flagField = new JTextField("");
PromptSupport.setPrompt("Write name of a country, e.g., DENMARK", flagField);
countryFlagDecorator.addCountryFlag(flagField, null);

Download the source

Wednesday, September 9, 2009

Google Earth 3D Vessel Models With Swing & Jetty

Using a Swing GUI with an embedded instance of Jetty a real time stream of vessel position information (AIS data) is converted into KML.This converts Google Earth into a low cost (free) AIS Display system. Vessels can be displayed as 3D models that are scaled according to AIS information





You can give it a spin (Google Earth needs to be installed) by pushing the launch button below.



Sunday, September 6, 2009

Fancy JLabels With JHLabs Filters

Recently I was looking for a way to create "fancy" text for some title panes and I stumbled over JHLabs Image Filters.





Over at JHLabs there are examples of how to create e.g., glowing text:



Or chrome like text:



To get a simmlar effect on a JLabel just replace the JLabel with a JHLabsLabel e.g.;

JLabel label = new JHLabsLabel("Hello World", new ChromeFilter(), new ShadowFilter(5, 5, 2, .7f));


Here is the code for JHLabsLabel:

/**
* Notice that this is not perfect wrt alignment to see just uncomment super.paintComponent(g)
* Maybe somebody with BasicLabelUI knowledge could help?
*
*/
public static final class JHLabsLabel extends JLabel {

private int textX;

private int textY;

private AbstractBufferedImageOp[] filters;

public JHLabsLabel(String string, AbstractBufferedImageOp... filters) {
super(string);
this.filters = filters;
}

@Override
public void paintComponent(Graphics g) {

// super.paintComponent(g);

BufferedImage img = createTextImage(getText(), getFont());

for (AbstractBufferedImageOp f : filters) {
img = f.filter(img, null);
}

// g.drawImage(f.filter(img, null), textX, textY, null);
g.drawImage(img, textX, textY, null);
}

private BufferedImage createTextImage(String text, Font font) {

Rectangle paintIconR = new Rectangle();
Rectangle paintTextR = new Rectangle();
Rectangle paintViewR = new Rectangle();
Insets paintViewInsets = new Insets(0, 0, 0, 0);

paintViewInsets = getInsets(paintViewInsets);
paintViewR.x = paintViewInsets.left;
paintViewR.y = paintViewInsets.top;
paintViewR.width = getWidth()
- (paintViewInsets.left + paintViewInsets.right);
paintViewR.height = getHeight()
- (paintViewInsets.top + paintViewInsets.bottom);

String clippedText = SwingUtilities.layoutCompoundLabel(
(JComponent) this, getFontMetrics(getFont()), text,
getIcon(), getVerticalAlignment(),
getHorizontalAlignment(), getVerticalTextPosition(),
getHorizontalTextPosition(), paintViewR, paintIconR,
paintTextR, getIconTextGap());

boolean isAntiAliased = true;
boolean usesFractionalMetrics = false;
FontRenderContext frc = new FontRenderContext(null, isAntiAliased,
usesFractionalMetrics);
TextLayout layout = new TextLayout(clippedText, font, frc);
Rectangle2D bounds = layout.getBounds();
int w = (int) Math.ceil(bounds.getWidth());
int h = (int) Math.ceil(bounds.getHeight());
BufferedImage image = new BufferedImage(w, h,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(new Color(0, 0, 0, 0));
g.fillRect(0, 0, w, h);
g.setColor(getForeground());
g.setFont(font);
Object antiAliased = isAntiAliased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON
: RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
antiAliased);
Object fractionalMetrics = usesFractionalMetrics ? RenderingHints.VALUE_FRACTIONALMETRICS_ON
: RenderingHints.VALUE_FRACTIONALMETRICS_OFF;
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
fractionalMetrics);
g.drawString(clippedText, (float) -bounds.getX(), (float) -bounds
.getY());
// g.drawString(clippedText, (float) 0, (float) 0);
g.dispose();

textX = paintTextR.x;
textY = paintTextR.y;// + getFontMetrics(font).getAscent() / 2;
System.out.println(String.format("X=%d Y=%d, w=%d h=%d", textX,
textY, w, h));

return image;
}

}


I also could not resist to play to the JHLabs texture filters as background painters e.g.,

JXPanel woodPanel = new JXPanel();
woodPanel.setBackgroundPainter(new JHTexturePainter<JXPanel>(new CausticsFilter(), new SparkleFilter()));

Creates the initial background for the demo.

Here is the code for JHTexturePainter:

/**
*
* Paint the background using a combination of the JHLabs image filters
*
* @param <T>
*/
static final class JHTexturePainter<T> implements Painter<T> {

private AbstractBufferedImageOp[] filters;

public JHTexturePainter(AbstractBufferedImageOp... filters) {
this.filters = filters;
}

@Override
public void paint(Graphics2D g2d, T arg1, int w, int h) {
BufferedImage image = new BufferedImage(w, h,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(new Color(0, 0, 0, 0));
g.fillRect(0, 0, w, h);

for (AbstractBufferedImageOp f : filters) {
image = f.filter(image, null);
}
g.dispose();
g2d.drawImage(image, 0, 0, null);
}

}

Live Traffic Map