Monday, May 20, 2013

Another Xtend and JavaFX Story

For my spare time JavaFX project, I was looking for a way to export a scene to SVG. Googling a bit I stumbled over a blog entry by Gerrit Grunwald who implemented an JavaFX shape to SVG path converter class for the JFXtras project. It looked good so I decided to give it a try.

 

Java to Xtend

The class is of course written in Java. Even though there is no technical reason - Java and Xtend classes can coexist in the same project without any problems - I wanted to convert it to Xtend as well. I am a lazy guy. Luckily Krzysztof Rzymkowski had recently posted on the Xtend group that he had started to implement a Java to Xtend converter. It even has a web interface, and except for one slight issue with a for-loop it worked like a charm. Great work, Krzysztof!

Of course the resulting code is pretty Java-like, so I wanted to improve on it using more of the cool Xtend features. I found quite a few spots to do so, reducing the amount of code significantly and enhancing readability a lot. The remainder of this post is about this ongoing love of JavaFX and Xtend. The complete source code will likely be made open-source soon.

 

Dispatch Methods

The class starts with a method that only contains an instanceof-cascade to delegate to the conversion method for the specific subclass of Shape the parameter has:
public static String shapeToSvgString(final Shape SHAPE) {
   final StringBuilder fxPath = new StringBuilder();
   if (Line.class.equals(SHAPE.getClass())) {
      fxPath.append(convertLine((Line) SHAPE));
   } else if (Arc.class.equals(SHAPE.getClass())) { 
      fxPath.append(convertArc((Arc) SHAPE)); 
   } else if (QuadCurve.class.equals(SHAPE.getClass())) {
      fxPath.append(convertQuadCurve((QuadCurve) SHAPE));
   } ... 
In Xtend I can use dispatch methods to realize this: They must have the same method and number of parameters, but different parameter types. The Xtend compiler then generates the dispatcher method with the instanceof-cascade automatically. So renaming the delegate methods and marking them as dispatch made the dispatcher method obsolete.
def dispatch String toSvgString(Line line) ...
def dispatch String toSvgString(Arc arc) ...
def dispatch String toSvgString(QuadCurve quadCurve) ...
Tip: If you want to delegate from one dispatch case to another, the original methods are available with an underscore preceeding their name.

Templates

An SVG path is kind of a cryptic string. In the Java code it is assembled using a StringBuilder, e.g.
final StringBuilder fxPath = new StringBuilder();
fxPath.append("M ").append(CENTER_X).append(" ")
   .append(CENTER_Y - RADIUS).append(" ");
fxPath.append("C ")
   .append(CENTER_X + CONTROL_DISTANCE)
   .append(" ").append(CENTER_Y - RADIUS).append(" ")
   .append(CENTER_X + RADIUS).append(" ")
   .append(CENTER_Y - CONTROL_DISTANCE)
   .append(" ")
   .append(CENTER_X + RADIUS).append(" ")
   .append(CENTER_Y).append(" ");
...
That's the best you can do with Java. In Xtend we have template expressions - multiline strings which can be interrupted with values from expressions. Even IF-conditions and FOR-loops are supported. With carefully chosen regular expressions for find/replace and some manual fine-tuning the above becomes nicely readable
'''M «centerX» «centerY - radius»
   C «centerX + controlDistance» «centerY - radius» 
   «centerX + radius» «centerY - controlDistance» 
   «centerX + radius» «centerY»
...

Switch Expression

I found another finer-grained instanceof-cascade in the convertPath method:
final StringBuilder fxPath = new StringBuilder();
for (PathElement element : PATH.getElements()) {
   if (MoveTo.class.equals(element.getClass())) {
      fxPath.append("M ")
         .append(((MoveTo) element).getX()).append(" ")
         .append(((MoveTo) element).getY()).append(" ");
   } else if (LineTo.class.equals(element.getClass())) {
      fxPath.append("L ")
         .append(((LineTo) element).getX()).append(" ")
         .append(((LineTo) element).getY()).append(" ");
   } else if (CubicCurveTo.class.equals(element.getClass())) { 
      fxPath.append("C ")
...
As the bodies of the if-statements are so simple, I decided to use Xtend's switch instead. It allows to use type guards for the cases and automatically cast the switch variable to that type inside the case's body:
val it = new StringBuilder
for (element : path.elements) {
   switch element {
      MoveTo: append('''M «element.x» «element.y» ''')
      LineTo: append('''L «element.x» «element.y» ''')
      CubicCurveTo: append('''C «element.controlX1»...
I could further use the operator => instead of the Builder class and the operator <=> replacing Double.compare.

Extension Import (Client Side)

From a client side, the shapeToSvgString method is a utility method for Shapes. In Xtend, you can import such methods using a static extension import. That makes them callable in extension syntax, as if the method was defined in the class of the first parameter. To further improve readability, I renamed the methods to toSvgString such that I can now write
import static extension ...ShapeConverter.*
... 
new Rectangle.toSvgString