Thursday, May 28, 2015

Handling Keyboard Events in JavaFX

When using JavaFX, SceneBuilder is not terribly helpful when setting up keyboard events.  This is because SceneBuilder's event handlers can't take any parameters.  Consequently, there is no way to determine which key triggered the event.

I've made an amusing little program that has four triangles on the GUI, one for each arrow key.  When an arrow key is pressed, the corresponding triangle changes color.  When it is released, it switches back to the original color.  Here is the source code for its controller; constructing the rest of the GUI is an exercise for the reader.

package application;

import java.util.Map;
import java.util.TreeMap;

import javafx.fxml.FXML;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;

public class ButtonPaneController {
 @FXML
 Polygon up;
 @FXML
 Polygon down;
 @FXML
 Polygon left;
 @FXML
 Polygon right;
 @FXML
 AnchorPane pane;
 
 Color pressColor = Color.GOLD;
 Color releaseColor = Color.DODGERBLUE;
 
 Map<KeyCode,Polygon> key2poly;
 
 @FXML
 void initialize() {
  pane.setOnKeyPressed(key -> recolor(key.getCode(), 
    pressColor));
  pane.setOnKeyReleased(key -> recolor(key.getCode(), 
    releaseColor));
  pane.setOnMouseEntered(m -> pane.requestFocus());
  key2poly = new TreeMap<>();
  key2poly.put(KeyCode.LEFT, left);
  key2poly.put(KeyCode.RIGHT, right);
  key2poly.put(KeyCode.UP, up);
  key2poly.put(KeyCode.DOWN, down);
 }
 
 void recolor(KeyCode code, Color newColor) {
  if (key2poly.containsKey(code)) {
   key2poly.get(code).setFill(newColor);
  }
  pane.requestFocus();
 }
}

In the initialize() method, we set up our event handlers using lambdas. Whenever a key is pressed, recolor() is called with the key code and the target color upon a press.  Similarly, whenever a key is released, the same method call is made, but with a different target color.  Finally, we request the focus whenever the mouse enters the pane. This could easily have been set up in SceneBuilder, but I did it in code here for consistency.

Having set up the handlers, I use a Map to simplify the implementation.  By mapping each key to the corresponding GUI element that is to have its color changed, I avoid a bothersome set of if-else clauses in the recolor() method.  A key-based event handler with more substantive algorithmic content could use a Map in a similar way where the values are lambda expressions.

Monday, May 18, 2015

Using the leJOS-0.9 video library

The following program illustrates how one might use the LeJOS-0.9 webcam library in conjunction with the lambda feature from Java 8:

package edu.hendrix.demo;

import java.io.IOException;
import java.util.function.Consumer;

import lejos.hardware.BrickFinder;
import lejos.hardware.Button;
import lejos.hardware.lcd.GraphicsLCD;
import lejos.hardware.video.Video;
import lejos.hardware.video.YUYVImage;

public class FilteredCameraDemo {
  private Consumer<YUYVimage> imgConsumer;
 
  public FilteredCameraDemo(Consumer<YUYVimage> filterer) {
    imgConsumer = filterer;
  }

  public void run() {
    try {
      Video wc = BrickFinder.getDefault().getVideo();
      wc.open(160,120);
      byte[] frame = wc.createFrame();
      YUYVImage img = 
        new YUYVImage(frame, wc.getWidth(), wc.getHeight());
      while (!Button.ESCAPE.isDown()) {
        wc.grabFrame(frame);
        imgConsumer.accept(img);
      }
      wc.close();
    } catch (IOException ioe) {
      ioe.printStackTrace();
      System.out.println("Driver exception: " + ioe.getMessage());
    }
  }
 
  public static void main(String[] args) {
    GraphicsLCD g = BrickFinder.getDefault().getGraphicsLCD();
    new FilteredCameraDemo
      (img -> img.display(g, 0, 0, img.getMeanY())).run();
  }
}

To allow flexibility of implementation, the YUYVImage class is essentially a wrapper around a byte array.  So every time we grab a frame from the webcam, we dump the bytes into a byte array that, in turn, we access via a YUYVImage that maintains a reference to it.  This YUYVImage class has methods that decode the byte layout according to the YUYV standard.

We have a private data member belonging to the Consumer interface. Consumer objects have an accept() method that takes a parameter (corresponding to the parameterized type) and does not return anything.  We make use of this within the run() method's while loop.  In this case, the type parameter is a YUYVImage object.

Now, when we invoke the constructor, we can write a lambda expression to describe the desired computation.  For this example, we just display the image.  Note that the LCD screen is referenced from outside the scope of the lambda.  This still works after it has gone out of scope, as Java 8's lambda expressions are closures.  You can write code to do arbitrary things when seeing an image, just by calling the FilteredCameraDemo constructor with a different lambda parameter.

I plan to write a more elaborate example in a future blog post, but this should suffice to demonstrate the concept.  Happy coding!

Setting up leJOS-0.9 with Java 8

The premier implementation of Java for the various Lego Mindstorms robots is LeJOS.  For the Mindstorms EV3, Oracle has facilitated the LeJOS implementation by providing pre-bundled Java microeditions.  Java version 7 update 60 is ready-to-go as part of the leJOS setup.  But the Oracle download page includes the following cryptic message regarding the use of Java 8:
Java SE Embedded 8 enables developers to create customized JREs using the JRECreate tool. Starting with Java SE Embedded 8, individual JRE downloads for embedded platforms are no longer provided. To get started, download the bundle below and follow instructions to create a JRE that suits your application's needs.
Of course, for a leJOS developer who would like to use Java 8, this is not entirely helpful.  Following some hints on the LeJOS EV3 forums, I was able to figure out how to do this.  This sequence of shell commands should work on any Unix/Linux platform.  (I am using a Mac.)  I would imagine it would also work on Windows if gzip and tar programs are installed.

gunzip ejdk-8-fcs-b132-linux-arm-sflt-03_mar_2014.gz
tar xvf ejdk-8-fcs-b132-linux-arm-sflt-03_mar_2014
cd ejdk1.8.0/bin
export JAVA_HOME=/usr
./jrecreate.sh --dest ../../ejre-8u1-linux-arm-15_may_2015 --profile compact2 --vm client
cd ../..
tar cvf ejre-8u1-linux-arm-15_may_2015.tar ejre-8u1-linux-arm-15_may_2015
gzip ejre-8u1-linux-arm-15_may_2015.tar

Having created the Java 8 configuration file ejre-8u1-linux-arm-15_may_2015.tar.gz, simply specify it when creating the SD card, and everything should work fine.

The English-language summary of the above command sequence is as follows:

  • Uncompress the archive using gunzip.
  • Extract the contents of the archive using tar.
  • Go to the subdirectory containing jrecreate.
    • On Windows, you would want to use jrecreate.bat rather than jrecreate.sh.
  • Set up the JAVA_HOME environment variable if it is not already set.
  • Run jrecreate with the following options:
    • --dest specifies where the configured implementation will be placed.  
    • --profile needs to be compact2 to ensure certain features LeJOS requires are present.
    • --vm needs to be client for the same basic reason.
  • Use tar to repack the archive.
  • Use gzip to compress the archive.
  • I made up a filename that seems to be compatible with what LeJOS will be looking for.
In my next post, I'll give an example that illustrates some of the capabilities of Java 8 in this environment.